Building Neo4j-Powered Applications with LLMs · Part III — Neo4j·Spring AI·LangChain4j로 짓는 지능형 추천 시스템

MATCH (:Chapter {n: 8})-[:CHAINS]->(:TransactionTrain)

H&M 개인화 데이터셋으로
추천 그래프를 짓다

거래는 사건의 연쇄다. 시간을 차원으로 삼아 거래를 사슬로 엮으면, 데이터 검색은 빠르고 질의는 문장처럼 읽힌다. 이 장은 H&M의 실제 대규모 데이터를 '트랜잭션 열차'로 모델링하고, 계절이라는 우리의 이해까지 그래프에 새긴다.

Ravindranatha Anthapu · Siddhant Agarwal Packt Publishing, 2025 Chapter 8 · pp. 147–160

Neo4j가 지식그래프 구축에 훌륭하다 해도, 데이터를 어떻게 모델링할지는 신중히 들여다보아야 한다. 좋은 데이터 지속 모델은 데이터 검색을 최적으로 만들고, 큰 부하를 더 잘 감당하게 해 준다. 이 장에서 우리는 한 걸음 물러서서 지식그래프를 이루는 것이 무엇인지, 그리고 Neo4j의 데이터 지속 접근으로 데이터 모델링을 다르게 바라보는 일이 더 강력한 지식그래프의 구축을 어떻게 돕는지 살핀다. 3장에서 정의한 접근들을 다시 찾아볼 필요가 있을지도 모른다. 그 접근들이 Personalized Fashion Recommendations(H&M 개인화) 데이터로 지식그래프를 짓는 힘이 되어 줄 것이다.

기술 요건

SQL과 Cypher에 익숙해야 한다. 데이터 모델링의 다양한 측면을 이해하는 데 SQLite와 Neo4j를 쓴다. 이 장에서 쓰는 도구는 다음과 같다.

Carlos García Ling, ElizabethHMGroup, FridaRim, inversion, Jaime Ferrando, Maggie, neuraloverflow, and xlsrln. H&M Personalized Fashion Recommendations. 2022. Kaggle.

3장에서 배웠듯, 좋은 그래프 데이터 모델은 RAG 흐름의 검색 부분을 더 효과적으로 만든다. 관련 데이터를 가져오는 일이 더 빠르고 쉬워지는 것이다. 그래프 데이터 모델링을 빠르게 복기하고 싶다면 3장을 다시 펼쳐도 좋다. 이 장에서 우리는 시간을 차원으로 삼아 데이터를 모델링한다. 시간을 차원으로 하는 거래의 사슬은 데이터 검색을 매우 효율적이고 성능 좋게 만들어 준다.

(:Section {n: 1})-[:MODELS]->(:HnMGraph)

H&M 개인화 데이터셋으로 추천 그래프를 모델링하다

이 절에서 우리는 실생활의 대규모 H&M 개인화 데이터셋으로 그래프 데이터 모델을 만든다. 이 모델이 다가올 장들에서 만들 추천 엔진에 동력을 공급하게 된다.

2022년 H&M은 추천 엔진 구축 경진대회의 일환으로, 고객 거래 데이터를 고객·상품 등의 메타데이터와 함께 공개했다. 이 데이터셋에는 과거 거래 데이터와 고객·상품 메타데이터가 담겨 있다. 메타데이터는 의류 종류와 고객 나이 같은 단순한 데이터부터 상품 설명의 텍스트 데이터, 의류 이미지의 이미지 데이터까지 아우른다. 데이터셋의 특성을 논하며, 한 걸음씩 데이터를 지식그래프에 적재해 나간다. 데이터셋에 담긴 데이터는 이렇다.

images/생략 주어진 article_id의 이미지. 데이터셋의 모든 품목에 이미지가 딸려 있는 것은 아니다. 그래프에 이미지를 저장하는 것은 비효율적일 뿐 아니라, 우리가 짓는 그래프 흐름에 필요하지도 않으므로 쓰지 않는다.
articles.csv사용 구매 가능한 각 품목(article)의 메타데이터. 각 행은 하나의 고유한 품목을 나타내며, 상품군(product family), 색상, 스타일, 품목이 속한 섹션과 부서(department) 같은 메타데이터를 담는다.
customers.csv사용 데이터셋 각 고객의 메타데이터 — 고객 ID, 나이, 패션 뉴스 수신 빈도, 활성 플래그, H&M 클럽 회원 상태, 우편번호.
transactions_train.csv사용 고객이 수행한 거래. 한 고객이 같은 품목을 여러 번 샀다면 여러 행으로 — 구매 품목당 한 행씩 — 담길 수 있으며, 거래 날짜, 품목 ID, 고객 ID, 가격, 판매 채널이 들어 있다.

H&M 개인화 데이터셋의 추천용 지식그래프를 지을 때 우리 손에는 고객들이 수행한 거래의 목록이 들려 있고, 이것을 시간을 차원으로 하는 거래의 사슬로 표현하는 것이 우리에게 아주 잘 통할 수 있다. 데이터에 대한 우리의 이해를 그래프 데이터 모델에 더하면 추천은 더 값진 것이 된다. 이를테면 거래는 사건의 시퀀스다. 그러니 시퀀스로 모델링하는 것이 더 이치에 맞다. 전통적 데이터베이스와 달리 Neo4j는 이 거래들을 관계로 순차 연결된 그래프로 저장할 수 있게 해 준다.

데이터에 대한 우리의 지식을 그래프에 지속시킨다 — 그렇게 지식그래프가 만들어진다.

추천 그래프 만들기

추천 모델 그래프를 지으려면 데이터셋 각 파일 안의 데이터와 그것이 그래프에 기여하는 방식을 들여다보아야 한다. 그래프 구축에는 앞서 3장에서 논한 과정을 적용한다. 데이터를 적재하기 전에 Neo4j Desktop에서 두 가지를 해 둔다. 첫째, 로컬 데이터베이스를 만든다(생성 안내). 둘째, H&M 추천 데이터셋의 CSV 파일들을 이 데이터베이스의 import 디렉터리에 복사한다(방법이 궁금하면 커뮤니티 글 참고). 이제 데이터를 그래프 데이터베이스에 적재하자.

고객 데이터의 적재

고객 데이터에는 고객 ID, 나이, 패션 뉴스 수신 빈도, 활성 플래그, H&M 클럽 회원 상태, 우편번호가 담겨 있다. 고객 ID는 고객의 고유 ID다. 고객을 나타내는 노드가 고유하도록 UNIQUE 제약이 필요하다. 우편번호는 노드로 만든다. 우편번호로 고객을 쉽게 분리하고 싶어질 수 있기 때문이다. 데이터를 적재하기 전에, 만들어 둔 Neo4j 데이터베이스에 접속해 이 고유 제약들을 생성한다.

constraints — 고객·우편번호cypher
CREATE CONSTRAINT customer_id_idx FOR (n:Customer) REQUIRE n.id IS UNIQUE;
CREATE CONSTRAINT postal_code_idx FOR (n:PostalCode) REQUIRE n.code IS UNIQUE;
Note

LOAD CSV 질의를 Neo4j Browser에서 실행하려면 앞에 auto를 붙여야 한다.

load — customers.csv (1,000행 배치)cypher
LOAD CSV WITH HEADERS FROM "file:///customers.csv" as row
WITH row
CALL {
    WITH row
    MERGE (c:Customer {id:row.customer_id})
    SET c.age = row.age
    FOREACH( ignoreME in CASE WHEN row.fashion_news_frequency = 'Regularly'
        THEN [1] ELSE [] END |
        SET c:FN_REGULAR
    )
    FOREACH( ignoreME in CASE WHEN row.club_member_status = 'ACTIVE'
        THEN [1] ELSE [] END |
        SET c:CLUB_ACTIVE
    )
    FOREACH( ignoreME in CASE WHEN row.club_member_status = 'PRE-CREATE'
        THEN [1] ELSE [] END |
        SET c:CLUB_PRE_CREATE
    )
    FOREACH( ignoreME in CASE WHEN row.Active <> 'ACTIVE'
        THEN [1] ELSE [] END |
        SET c:INACTIVE
    )
    MERGE(p:PostalCode {code:row.postal_code})
    MERGE(c)-[:LIVES_IN]->(p)
} IN TRANSACTIONS OF 1000 ROWS

이 스크립트는 1,000행을 하나의 커밋 배치로 삼아 고객 데이터를 적재한다. 눈여겨볼 대목이 둘 있다. Customer 노드에는 고유 ID인 customer_id 말고는 age라는 프로퍼티 하나만 있다는 것, 그리고 고객 데이터의 나머지 속성들은 Customer 노드의 레이블로 매핑했다는 것이다.

이 접근은 앞서 논한 소비 기반(consumption-based) 데이터 모델링 접근을 따른다. 패션 뉴스를 정기 구독하는 고객들이 어떻게 행동하는지 이해하고 싶다고 하자. 레이블은 이 정보를 가져오는 쉬운 길을 열어 준다. Neo4j는 레이블 기반 접근의 검색을 최적화한다. 이 고객 행동(패션 뉴스 구독)을 프로퍼티로 만들고 인덱스를 세워 가져올 수도 있겠지만, 그러려면 더 많은 저장 공간이 들고 인덱스 조회 비용까지 짊어져야 한다. 활성 클럽 회원이면서 패션 뉴스를 정기 구독하는 고객을 쓰고 싶다면, 레이블 기반 접근이 프로퍼티 저장에 견주어 이 정보를 더 효과적으로 가져오는 우위를 준다. 게다가 이 정보를 그래프로 표시할 때 사용자는 프로퍼티를 뒤질 필요 없이 레이블에서 정보를 바로 읽는다. 이런 방식의 데이터 소비가 더 자연스럽게 느껴지고, 질의도 더 자연스럽게 읽힌다.

품목 데이터의 적재

품목(article) 데이터에는 고유한 품목 ID와 설명 외에, 품목을 묘사하는 다른 카테고리들이 담겨 있다. 품목을 묘사하는 이 속성들을 그 자체로 노드로 만든다. 이를 위해 다음 고유 제약들을 생성한다.

constraints — 품목 속성 노드 12종cypher
CREATE CONSTRAINT product_code_idx FOR (n:Product) REQUIRE n.code IS UNIQUE;
CREATE CONSTRAINT article_id_idx FOR (n:Article) REQUIRE n.id IS UNIQUE;
CREATE CONSTRAINT product_type_id_idx FOR (n:ProductType) REQUIRE n.id IS UNIQUE;
CREATE CONSTRAINT colour_group_idx FOR (n:ColorGroup) REQUIRE n.id IS UNIQUE;
CREATE CONSTRAINT product_group_name_idx FOR (n:ProductGroup) REQUIRE n.name IS UNIQUE;
CREATE CONSTRAINT graphical_appearance_id_idx FOR (n:GraphicalAppearance) REQUIRE n.id IS UNIQUE;
CREATE CONSTRAINT perceived_colour_id_idx FOR (n:PerceivedColor) REQUIRE n.id IS UNIQUE;
CREATE CONSTRAINT department_id_idx FOR (n:Department) REQUIRE n.id IS UNIQUE;
CREATE CONSTRAINT section_id_idx FOR (n:Section) REQUIRE n.id IS UNIQUE;
CREATE CONSTRAINT garment_group_id_idx FOR (n:GarmentGroup) REQUIRE n.id IS UNIQUE;
CREATE CONSTRAINT article_index_id_idx FOR (n:Index) REQUIRE n.id IS UNIQUE;
CREATE CONSTRAINT article_index_group_id_idx FOR (n:IndexGroup) REQUIRE n.id IS UNIQUE;

품목의 속성 대부분을 노드로 바꾸었음을 볼 수 있다. 그래프에 표현되는 데이터가 일종의 정규화를 거치는 셈이다. 다음 Cypher가 데이터를 그래프에 적재한다. 네 토막으로 나누어 읽는다.

load — articles.csv ① 품목·상품·상품군 생성과 연결cypher
LOAD CSV WITH HEADERS FROM "file:///articles.csv" as row
WITH row
CALL {
    WITH row
    MERGE(a:Article {id:row.article_id})
    SET a.desc = row.detail_desc
    MERGE(p:Product {code:row.product_code})
    SET p.name = row.prod_name
    MERGE(a)-[:OF_PRODUCT]->(p)
    MERGE(pt:ProductType {id:row.product_type_no})
    SET pt.name = row.product_type_name
    MERGE(p)-[:HAS_TYPE]->(pt)
    WITH row, a, p
    MERGE(pg:ProductGroup {name:row.product_group_name})
    MERGE(p)-[:HAS_GROUP]->(pg)
② 그래픽 외형과 색상 연결cypher
    WITH row, a
    MERGE(g:GraphicalAppearance {id:row.graphical_appearance_no})
    SET g.name = row.graphical_appearance_name
    MERGE (a)-[:HAS_GRAPHICAL_APPEARANCE]->(g)
    WITH row, a
    MERGE (c:ColorGroup {id: row.colour_group_code})
    SET c.name = row.colour_group_name
    MERGE (a)-[:HAS_COLOR]->(c)
    WITH row, a
    MERGE (pc:PerceivedColor {id: row.perceived_colour_value_id})
    SET pc.name = row.perceived_colour_value_name
    MERGE (a)-[:HAS_PERCEIVED_COLOR]->(pc)
    MERGE (pcm:PerceivedColor {id: row.perceived_colour_master_id})
    SET pcm.name = row.perceived_colour_master_name
    MERGE (pc)-[:HAS_MASTER]->(pcm)
③ 부서·인덱스·인덱스 그룹 연결cypher
    WITH row, a
    MERGE (d:Department {id:row.department_no})
    SET d.name = row.department_name
    MERGE (a)-[:HAS_DEPARTMENT]->(d)
    WITH row, a
    MERGE (i:Index {id: row.index_code})
    SET i.name = row.index_name
    MERGE (a)-[:HAS_INDEX]->(i)
    MERGE (ig:IndexGroup {id: row.index_group_no})
    SET ig.name = row.index_group_name
    MERGE (i)-[:HAS_GROUP]->(ig)
④ 섹션·의류 그룹 연결cypher
    WITH row, a
    MERGE (s:Section {id: row.section_no})
    SET s.name = row.section_name
    MERGE (a)-[:HAS_SECTION]->(s)
    WITH row, a
    MERGE (gg:GarmentGroup {id: row.garment_group_no})
    SET gg.name = row.garment_group_name
    MERGE (a)-[:HAS_GARMENT_GROUP]->(gg)
} IN TRANSACTIONS OF 1000 ROWS

이 Cypher 질의에서 확인할 수 있듯, 그래프에는 정규화된 데이터가 지속된다. 품목을 묘사하는 여러 측면의 값들이 중복 없이 저장되는 것이다. 다음은 거래를 적재할 차례다.

거래 데이터의 적재

transactions_train.csv 데이터는 거래가 발생한 순서대로 담겨 있다. 그 덕분에 데이터를 적재하며 그래프 안에 시퀀스를 손쉽게 보존할 수 있다. 거래마다 한 행에 거래 날짜, 품목 ID, 고객 ID, 가격, 판매 채널이 담겨 있다. 유의할 점 하나 — 각 거래에는 고유 ID가 없다. 다음 Cypher로 데이터를 적재한다.

load — transactions_train.csv · 트랜잭션 열차 조립cypher
LOAD CSV WITH HEADERS FROM "file:///transactions_train.csv" as row
WITH row
CALL {
    WITH row
    MATCH (c:Customer {id:row.customer_id})
    MATCH (a:Article {id:row.article_id})
    WITH a, c, row
    CREATE (t:Transaction {date: row.t_dat, price: row.price,
        salesChannel: row.sales_channel_id})
    CREATE (t)-[:HAS_ARTICLE]->(a)
    WITH c, t
    CALL {
        -- 기존 체인이 있는 고객: LATEST를 옮겨 단다
        WITH c, t
        WITH c, t
        WHERE exists((c)-[:START_TRANSACTION]->())
           OR exists((c)-[:LATEST]->())
        MATCH (c)-[r:LATEST]->(lt)
        DELETE r
        CREATE (lt)-[:NEXT]->(t)
        CREATE (c)-[:LATEST]->(t)
        UNION
        -- 첫 거래인 고객: 열차의 머리를 만든다
        WITH c, t
        WITH c, t
        WHERE NOT ( exists((c)-[:START_TRANSACTION]->())
           OR exists((c)-[:LATEST]->()) )
        CREATE (c)-[:START_TRANSACTION]->(t)
        CREATE (c)-[:LATEST]->(t)
    }
} IN TRANSACTIONS OF 1000 ROWS

이 Cypher에서 볼 수 있듯, 주어진 고객의 첫 거래는 START_TRANSACTION 관계로 고객에 연결한다. 고객이 수행한 마지막 거래는 LATEST 관계로 추적한다. 고객의 거래가 계속 들어올 때마다 LATEST 관계를 가장 새 거래로 옮겨 달고, 이전에 LATEST로 연결되어 있던 거래와 새 거래를 NEXT 관계로 잇는다. 그러니 이 그래프에서 우리는 고객들의 거래를 트랜잭션 열차(transaction train)로 표현하고 있는 셈이다. 데이터셋 파일 이름 transactions_train.csv에 충실한 표현이다.

최종 그래프

모든 데이터를 적재하고 나면 그래프 모델은 그림 8.1과 같은 모습이 된다.

PostalCode Customer LIVES_IN Trans-action START_TRANSACTION LATEST NEXT Article HAS_ARTICLE PerceivedColor HAS_MASTER HAS_PERCEIVED_COLOR ColorGroup HAS_COLOR Section HAS_SECTION Product OF_PRODUCT ProductType HAS_TYPE ProductGroup HAS_GROUP Index HAS_INDEX IndexGroup HAS_GROUP GarmentGroup HAS_GARMENT_GROUP Depart-ment HAS_DEPARTMENT GraphicalAppearance
그림 8.1 — H&M 데이터 적재 후의 그래프 데이터 모델

이 그래프에서 품목의 속성들이 여러 개별 노드로 부챗살처럼 펼쳐진(fanned out) 모습을 볼 수 있다. Customer 노드는 우편번호와 첫·마지막 거래에 연결되어 있다. Transaction은 Article과 연관되고, Transaction 노드들은 주어진 고객의 다음 거래로도 이어진다. 데이터가 적재되었으니, 이 데이터에서 그래프를 한층 더 강화해 데이터에 대한 우리의 이해와 아이디어를 그래프에 더하는 방법을 탐사하자.

(:Section {n: 2})-[:ENRICHES]->(:Season)

추천을 위한 최적화 — 그래프 모델링의 모범 사례

이제 우리가 소비하고 싶은 방식대로 데이터가 적재되고, 데이터의 문맥을 표현하는 그래프가 손에 있다. 그러나 이 그래프가 표현하는 것은 아직 원래 주어진 문맥뿐이다. 계절과 연도로 데이터를 소비하고 싶다면 여전히 그것을 가져오는 질의를 지어야 한다. Neo4j는 스키마가 선택적이니, 후처리(post-processing)로 추가 관계를 더해 그런 방식의 데이터 소비를 열어 볼 수 있다.

다음 Cypher 스크립트로 계절 관계를 만든다. 다섯 단계다.

  1. 고객마다 거래를 순회한다. START_TRANSACTION과 LATEST를 잡아 (s)-[:NEXT*]->(e) 경로의 노드들을 펼친다. 월과 연도에 기초해 계절 값을 배정할 준비다.
  2. 연도를 조정한다. 이를테면 월이 1이고 연도가 2020이면, 그 거래의 문맥으로는 WINTER_2019가 계절 이름이 된다 — 12월이 속한 겨울의 연도로 묶는 것이다.
  3. 계절 값별로 거래를 모은다. 12·1·2월은 WINTER, 3·4·5월은 SPRING, 6·7·8월은 SUMMER, 9·10·11월은 FALL로 분류한다.
  4. 계절 값별 컬렉션의 첫 레코드를 얻는다. head(collect(node))로 각 계절의 시작 거래를 잡는다.
  5. 고객과 그 거래 사이에 계절 이름의 관계를 만든다. 관계 이름이 동적이므로 apoc.create.relationship 프로시저를 쓴다.
WINTER12 · 01 · 0212월의 연도를 따른다 — 1·2월은 year−1
SPRING03 · 04 · 05해당 연도 그대로
SUMMER06 · 07 · 08해당 연도 그대로
FALL09 · 10 · 11해당 연도 그대로
seasonal relationships — 계절 관계 생성cypher
MATCH (c:Customer)
WITH c
CALL {
    WITH c
    MATCH (c)-[:START_TRANSACTION]->(s)
    MATCH (c)-[:LATEST]->(e)
    WITH c, s, e
    MATCH p=(s)-[:NEXT*]->(e)
    WITH c, nodes(p) as nodes
    UNWIND nodes as node
    WITH c, node, node.date as d
    WITH c, node, toInteger(substring(d, 0,4)) as year,
         substring(d, 5,2) as month
    WITH c, node,
        CASE WHEN month="12" THEN year
             WHEN month="01" OR month="02" THEN year-1
             ELSE year
        END as year,
        CASE WHEN month="12" OR month="01" OR month="02" THEN "WINTER"
             WHEN month="03" OR month="04" OR month="05" THEN "SPRING"
             WHEN month="06" OR month="07" OR month="08" THEN "SUMMER"
             WHEN month="09" OR month="10" OR month="11" THEN "FALL"
        END as season
    WITH c, node, season+'_'+year as relName
    WITH c, relName, head(collect(node)) as start
    WHERE relName is not null
    CALL apoc.create.relationship(c, relName, {}, start) YIELD rel
    WITH 1 as out
    return DISTINCT out
} IN TRANSACTIONS OF 1000 ROWS
WITH 1 as r
RETURN DISTINCT r

이것이 아주 기초적인 접근이라는 점은 새겨 두자. 요점은, 데이터에 대한 우리의 이해에 기초해 그래프 안에 추가 문맥을 만들 수 있다는 것이다. 이런 접근들이 Neo4j를 지식그래프 구축에 매우 알맞게 만든다. 이렇게 데이터 접근을 쉽게 만들어 두면, 같은 데이터를 다르게 바라보아 더 많은 지능을 추출하는 새 아이디어들이 열린다. 그것도 추적 가능하고 이해 가능한, 단순한 방식으로.

Note — 데이터베이스 스냅숏

데이터를 수동으로 적재하고 싶지 않다면, 데이터베이스 덤프 파일을 내려받을 수 있다.

이제 데이터에 더 많은 문맥이 더해졌다. 그래프 데이터 모델을 다시 보자.

PostalCode Customer LIVES_IN Trans-action NEXT SUMMER_2020 FALL_2018 · WINTER_2018 SPRING_2020 SUMMER_2019 LATEST FALL_2020 · SPRING_2019 FALL_2019 START_TRANSACTION WINTER_2019 Article HAS_ARTICLE …속성 노드들
그림 8.2 — 계절 관계로 강화한 뒤의 H&M 그래프 데이터 모델

데이터에 대한 우리의 이해를 이용해, 임의의 고객이 2019년 여름에 산 품목들을 가져오는 질의를 지어 보자.

query — SUMMER_2019 구매 품목 검색cypher
MATCH (c:Customer)-[:SUMMER_2019]->(start),
      (c)-[:FALL_2019]->()<-[:NEXT]-(end)
WITH c, start, end SKIP 100 LIMIT 1
MATCH p=(start)-[:NEXT*]->(end)
WITH nodes(p) as nodes, relationships(p) as rels
UNWIND nodes as node
MATCH p=(node)-[:HAS_ARTICLE]->(a)
RETURN a.desc as article

이 질의로 우리는 2019년 여름과 가을 모두에 구매 기록이 있는 고객들을 찾고, 그 목록에서 한 명을 골라, 품목 설명들을 가져온다. 질의의 출력은 다음과 같은 모습이다.

a.desc as articleStarted streaming 4 records after 436 ms and completed after 532 ms.
1"Ankle-length trousers in woven fabric with a high, elasticated waist, pleats at the front, a zip fly and buttons, side pockets, fake back pockets and straight, wide legs."
2"V-neck T-shirt in soft cotton jersey."
3"Round-necked T-shirt in soft jersey."
4"Scarf in a soft weave with fringes on the short sides. Size 50x180 cm."

그림 8.3 — 한 고객의 SUMMER_2019 구매를 가져오는 Cypher 질의의 출력

질의를 보기만 해도 무엇을 하는 질의인지 이해하기 쉽다. SUMMER_2019를 시작점으로, FALL_2019 관계 앞의 거래를 끝점으로 삼아, 시작점에서 끝점까지 순회하며 그 거래들의 품목을 가져온다.

프로퍼티 기반 필터 대신 그래프 순회에 온전히 기대고 있음을 볼 수 있다. 그 덕분에 이 질의의 실행은 매우 효율적이다. Neo4j는 바로 이런 종류의 질의를 매우 효율적으로 실행하도록 지어졌다.

요약

이 장에서 우리는 그래프 데이터 모델을 바라보는 법을, 그리고 소비하는 방식에 기초해 모델을 지으면 데이터의 효율적 검색이 얼마나 쉬워지는지를 보았다. H&M 추천 데이터셋을 그 원리로 적재했고, 프로퍼티와 데이터에 대한 우리의 이해를 이용해 그래프를 증강하기까지 했다. 그래프에 더 많은 문맥이 더해졌고, 데이터 질의도 단순해졌다. 질의는 더 읽기 쉽고, 남에게 더 간명하게 설명할 수 있는 것이 되었다.

다음 장에서는 이 데이터 위에서 LLM을 이용해 그래프를 한층 더 강화하고, LLM이 어떻게 더 유능한 지식그래프를 우리 손에 쥐여 주는지 본다.

NEXT → Chapter 9 · Integrating LangChain4j and Spring AI with Neo4j