Галоўная / Артыкулы

Разбіўка parent-child і іерархічная індэксацыя для лепшага пошуку ў RAG

Чаму малыя фрагменты выйграюць супадзенне, але губляюць адказ, і як разбіўка parent-child, пашырэнне з дэдуплікацыяй і іерархічная індэксацыя вяртаюць кантэкст у пошук RAG.

Выдавец: REACTAPP.TOP10 хв чытання2363 слоў

Крыніцы: Parent–Child Chunking and Hierarchical Indexing in RAG: A Practical Guide to Better Retrieval

Калі сістэма RAG вяртае адказ, які ў большасці разоў ўсунуты, але не мае таго важлівага дэталю, прычына зазвычай крычыць у спосабе разбівання тэксту, а не у эмбедынгах. Две тэхнікі безпасова рашаюць гэтыя проблемы: метод «род-дзець», які шукае маленькія фрагменты, але чытае большыя, і іерархічны індэксавання, які звужае пошук, следуючы сэльве структуры дакумента. Обе тэхнікі маюць на меце даць мовнаму модэлю точнасць і кантэкст адночасна.

Чаму разбівання ўскладнены крок

Базавы прыемнік ўскладнены: дакументы дзеляцца на часткі, якія пасля таго укладаюцца, зберагаюцца, а для запіту выбіраюцца найболей значымыя з іх, якія потым перэдаюцца модэлю. Калі ўсё зламваецца, прычына зазвычай крыўдзіцца ў самым першым кроцы. Часткі, якія занадта вялікія, зменшаюць рэлевантнасць і спамяшваюць працэс укладання, а часткі, якія занадта малыя, выкарыстоўваюць толькі часткі тэксту, на якія залежыць запіт. Размерныя, рэкурсывныя, сэмантычныя і способы дзеляння на аб’екты рэчэй — гэта усё спробы збалансаваць гэты напярэк у межах адной едыніцы тэксту.

Варта спробаваць іншую парадыгму: перастаць шукаць адзін ідэальны размер і выкарыстоўваць два размеры разам, кожны з якіх налаштаваны для разных задач. Перш чым застосаваць такі падход, корыстна точна адразу пазнакаваць, дзе саме адзін размер не падходзіць.

Маленькі прыклад неудачы

Разглянем фрагмент з тэхнічнага дакумента, які складаецца з двух абзацаў. У першым абзаце сказана, што кэшаванне робіць сістэму значна быстрэй. У другім абзаце паведамляецца, што некорэктная анвалідацыя кэша можа залішыць за сабой застарэлыя даны.

Малагабарытны прыемнік могаў бы разлучыць гэты фрагмент на два часткі. Частка А — гэта абзац, які хваляе выконвальныя характэрыстыкі, а частка B — абзац пра застарэлыя даны, якія застаюцца пасля некорэктнай анвалідацыі. Цяпер запытаемся: якія ёсць рызыкі кэшавання? Наведзены ў ніжчым фрагменце скрыпт уключае гэты запытанне і обе часткі, а пасля порывае іх за дапамою косай сэмплірнасці.

from sentence_transformers import SentenceTransformer
import numpy as np

model = SentenceTransformer("all-MiniLM-L6-v2")

def cosine_similarity(a, b):
    return np.dot(a, b) / (np.linalg.norm(a) * np.linalg.norm(b))

query = "What is a risk of caching?"
chunk_a = "Caching improves system performance significantly."
chunk_b = "However, improper cache invalidation can lead to stale data issues."

query_embedding = model.encode(query)
score_a = cosine_similarity(query_embedding, model.encode(chunk_a))
score_b = cosine_similarity(query_embedding, model.encode(chunk_b))

print(f"Chunk A similarity: {score_a:.4f}  - '{chunk_a}'")
print(f"Chunk B similarity: {score_b:.4f}  - '{chunk_b}'")

Яго запуск выводзіць ступень супадання кожнага фрагмента з запытаннем.

Chunk A similarity: 0.5891  — 'Caching improves system performance significantly.'
Chunk B similarity: 0.5103  — 'However, improper cache invalidation can lead to stale data issues.'

Чакк А пасуюе лепша — адносаваеся да 0,59 протыяў 0,51, навет як чакк Б і ёсць тым, што адпавядае на запытанне. І адны, і другі згадваюць кэшаванне, таму выглядае, што яны асоцыяваны, а мадэль амбеддынгу не мае ніякой спецыяльной прычыны з’яўляць звязак между словам «рызык» і рэчэй пра застарэлыя даны, дзе гэтага слова ніколі не выкарыстоўваецца. Рэтрывер, які браць толькі адзін найлепшы падход, адважна перадаў бы неправільную частку пасунку модэлі.

Урок заключаецца ў тым, што сэмантычная супадзімасць не гарантуе полноты. Чакк можа быць праведна ацэнены як на тэму, але ўсё ж не маць факту, які патрэбен запытанню, і чым меньшы чакк, тым большая верагатыба, што гэты факт знаходзіцца ў суседнім рэчэй.

Чаккаванне «родык-дзеця»

Ідэя заключаецца ў тым, каб адразу зберагчы два рывні дзейнаўкі таго ж тэксту, пры чым кожны выканае разныя задачы.

Фрагменты для дзецей ў малым і точным размерэ. Яны зазвычай складаюцца з 100 да 400 токенав і прызначаны для вбудованага пошуку з высокім рывнем відтворэння рэзультатаў. Іх малы размер стварае болей чыткія векторы, адтолькі менша колькасць нескарыянага матэрыялу не паслабляе значэння кожнага вектора. Аднак вікорыстанне калькі слоў можа пагоршыць рэзультаты, адтолькі фрагмент можа не мець самастойнага значэння, неабходнага для надзеянага падбору.

Абяцкі чаккі ў большым размерэ і маюць кантэкстуальны характэр. Яны часта захопляюць ад 800 да 3000 токэнаў або больш, пакрываючы цэлы раздзіл або логічную ейкапу, і існуюць для выканання логічных расчынанняў, а не толькі для падбору падходзягучых даных. У прыкладзе кэшавання абяцкі, які мае цэлы тэкст, адбірае як першую, так і другую частку ідэі, таму, незалежна ад таго, які дзецінскі чаккі падходзіць, модэль все рава бачыць цэлыя картыну. Верхняя межа вынікае з акна кантэксту модэля, адколі калькі большых абяцкіх чаккі, зібраных у адну запрошэнне, можу заняць усё прастор, які запрошэнне павінна мець.

Цяны зв’язку можна выразіць ў адной фразе: дзецінскія чаккі — гэта тое, што шукаюць, а абяцкія — гэта тое, што чытаюць. Дзецінскі чаккі павінен быць дастаткова точным, каб выграць у соразумленасці, а яго абяцкі чаккі пераканаецца, што модэль отрымае кантэкст, ад якога залежыць яго адпаведзь.

Як гэта выглядае на практыцы

Уявіце раздзел падзеўнікам "Оптымізацыя выконвання базы дадзеных". Механізм раздзелення на родны і дзеціныя элементы працюе у два етапы. Спачатку ён выявляе межы роднага элемента – цэлага раздэлу, які абарачвае тэмы індэксавання, оптымізацыі запытоў, кэшавання і асоцыяваных з гэтым компромісаў. Потым той жа тэкст раздзеляецца на маленькія дзеціныя элементы, напрыклад:

  • элемент пра тое, як індэксавання падвышае швальце запытоў за рахунак скорачэння часу сканавання,
  • элемент пра тое, як кэшавання зменшае колькасць павтаральных вычыслаў, але створае рызык застарэлых дадзеных,
  • элемент пра перапісваць SQL-запыты для ўлучшэння ўмов выконвання.

Схема працюе таму, што кожны дзеціный элемент памятае, звядзе ён выйшаў. Найпростый спосаб – зберагчы цэлы тэкст роднага элемента ў метаданах кожнага дзеціныя элемента, як у гэтай функцыі індэксавання.

def build_parent_child_index(document_section, parent_text, child_splitter, embedding_model):
    child_chunks = child_splitter(parent_text)
    records = []
    for child_text in child_chunks:
        records.append({
            "text": child_text,
            "embedding": embedding_model.encode(child_text),
            "parent_text": parent_text  # full parent stored directly on every child
        })
    return records


parent_text = """Indexing improves query speed by reducing scan time across large tables.
Caching reduces repeated computation but may introduce staleness if not invalidated properly.
Query optimization involves rewriting SQL queries to use more efficient execution plans."""

child_texts = [
    "Indexing improves query speed by reducing scan time across large tables.",
    "Caching reduces repeated computation but may introduce staleness if not invalidated properly.",
    "Query optimization involves rewriting SQL queries to use more efficient execution plans.",
]

records = [
    {"text": t, "parent_text": parent_text} for t in child_texts
]

for r in records:
    print(f"Child: {r['text'][:50]}...")
    print(f"  → linked to parent ({len(r['parent_text'])} chars)\n")

Даклэкаванье родніка на кожны дзецін каштуе дадзейнаўскага прастору, але не трэба аддзельнага пошуку пад час запыту, таму што роднік вялікі ўжо знаходзіцца ў запісе дзеціна. Альтэрнатыва — зберагчы толькі parent_id і запрашваць родніка, калі гэта неабходна, што заўсёды эканоміць прастор, але дадае аддзельны крок пошуку. У будзь-якім случае кожны дзецін застаецца незалежна пошуканым, паўтараючыся з іншымі фрагментамі, і ў той жытак мае прымытую дорогу назад да свайго полнага кантэксту.

З’явіць маленькі, расшырыць вялікі

Гэта перакручвае звычны лепт. Стандартны RAG выконваець: з’явіць, адправіць, адпаведзець. Версія з роднікамі і дзецінамі выконваець: з’явіць дзеціна, расшырыць да яго родніка, адправіць расшыраны кантэкст, адпаведзець. Ці ход адбываецца, продовжаючы прыклад кэшавання.

def parent_child_retrieve(query, child_records, embedding_model, top_k=3):
    query_embedding = embedding_model.encode(query)
    scored = [
        (np.dot(query_embedding, r["embedding"]) /
         (np.linalg.norm(query_embedding) * np.linalg.norm(r["embedding"])), r)
        for r in child_records
    ]
    scored.sort(key=lambda x: x[0], reverse=True)
    top_children = scored[:top_k]

    # Expand each matched child to its parent - but deduplicate first
    seen_parents = set()
    expanded_context = []

    for score, record in top_children:
        parent = record["parent_text"]
        if parent not in seen_parents:
            expanded_context.append(parent)
            seen_parents.add(parent)

     return expanded_context

for r in records:
    r["embedding"] = model.encode(r["text"])

context = parent_child_retrieve("What is a risk of caching?", records, model, top_k=2)
print("Context sent to the LLM:\n")

for c in context:
    print(c)

Зверніце увагу на пераконтроль seen_parents. Калі два верхніяя дзеці належаць каляжным адзінам абяцею, што ўзніква часта, таму што суседзяючыя фрагменты аднаго раздэлу часта маюць падобныя рэйтынгі, аддзельнае розгляданне кожнага з іх прызведзе да таго, што тэкст таго ж абяцею будзе падаць два разы, і будзе вітрачаны кантэкст. Усуненне дуплікацый на адзінам абяцею дазволяе кожнаму фрагменту кантэксту праказацца адназначы, незалежна ад таго, скількі дзецей паспадзялі.

Для часткі з кэшаваннем модэль цяпер отрымае рэчэнце, якая павінна была паспасць, а таксама пояснення ў яе або навакол, тады обе часткі падаюць разам, незалежна ад таго, калькі дзеця ўзяло першае месца.

Чаму гэта працюе

  • Точнасць з кантэкстам. Дзеці знаходзяць правыя месцы, а абяцею падаюць тое, што адна дзець не можа несці.
  • Крашчая логіка. Модэлі маюць труднасці з несувязанымі фрагментамі, а абяцею вярнюе наратыўны ланцуг.
  • Менша колькасць выдуманых дэталяў. Бачачы асупрацоўваныя абмежэнні і адказы, мадэль мае меншае кальканне запалняць прасвяты здогадкамі.
  • Тэхнічны матэрыял. Апі-сэрвісы, документацыя SDK, інжынерныя вікі, статыі і юрыдычныі тэксты рэдка калі складаюцца з адной рэчэнкі, і часта саме адступленне або умова, якія з’яўляюцца за две рэчэнкі, маюць значэнне.
  • Іерархічны індэксаванне

    Калі структура «род-дзеці» складаецца з двух роўней, то ў юархічным індэксаванні іх мае быць многа. Уместа аднаго «роду» і яго «дзецей» ствараецца структура, якая відпавае спосабу арганізацыі дакумента. Прыклад такой іерархіі можа выглядаць так.

    Document: "Cloud Architecture Guide"
      Section: Networking
        Subsection: Load Balancing
          Chunk: Round-robin method
          Chunk: Least connections method
        Subsection: CDN usage
      Section: Security
        Subsection: IAM policies
        Subsection: Encryption
    

    Зверніце ўвагу, звядзе кампане гіерархія. Дзеякі сістэмы сортаваюць матэрыялы па катэгорыях, такіх як аддэкт, тэма чыста тагі метаданых, якія прызначаюцца незалежна ад конкрэтнага дакумента. У даным случае гіерархія вытэкае з сэльбэтных загалоў і раздзялам дакумента, з таго, як яго автар яго аранжаваў. І адна, і другая способы скурчваюць прастор для пошуку, але адна організуе цэлую базу знання па тэмах, а другая — адзін дакумент па яго зместу.

    Большасць дакументаў — гэта не проста тэкст. Загалоўныя тэксты і падраздзелы ужо маюць у сабе інформацыю пра тое, як адна ідэя адносіцца да іншай. Гіерархічны індэксаванне зберагае гэю структуру і дазволяе сістэме знайсці правы раздзял, потым падраздзел, а потым самэ точнае частка тэксту, падобна да таго, як чалавек пры прачытанні спачатку пераглядае змест, а потым чытае дакладна.

    Практычнае застосаванне адналокавання

    Падзеймім, што запіт здаецца такім: «Як працюе балансаванне навантажэння за прынцыпам мінімуму канектацый?» Сістэма знаходзіць раздзіл «Сеткі», сузае выбір да падраздзела «Балансаванне навантажэння», а пасля выкарыстоўвае інфармацыю, якая пояснюе, што метод мінімуму канектацый выбірае сервер з наіменшай колькасцю актыўных канектацый.

    Кожны крок сузэння выконваецца за дапамою конкрэтнага механізма, адтолькі нічога з гэтага не выконваецца сама сабою. Серэд варыянтаў — трэнаваны класыфікатор, які прагнозуе, да якога раздзіла належыць запіт; вызов LLM, які чытае запіт і выбірае найболей правдападобны раздзіл з вядомага спіску; або парадаўнае падабранне на аднойчыну раздзілаў і ключоўых слоў. Выбраны метод працюе на кожным роўні — спачатку для раздзіла, а потым для падраздзела, перш чым пачнёцца пошук на роўні частак інфармацыі.

    Чы гэта дадзеная глыбіна ўзрадна? Два рэвэля ўстаткуюць для большасці дакументаў. Полная багатарэвэльная іерархія карыстна для дзейнаў длігіх, глыбока структураваных, дзе адна частка містыць столькі разныя падтэмы, што пошук усьго адразу зменшыць ўзрэменнасць рэзультатаў. Короткая стороніца з часта задаваемымі запытаннямі або артыкул на адну тэму рэдка калі патрабуе больш чым два рэвэля, тады як ста-сторанковыя інструкцыі па архітэктурэ з дзесяткамі падчастак можаць. Як і ў будзь-ям рашэнні пра індэксаванне, спачатку вырахавайце, прычым лепш дадаць сложнасць.

    Короткія патрабаванні ў налаштаванні

    Пачніце з розмера частак, які знаходзяцца прысерэдзіне дыяпазона — адпрацоўваюць прыблізна 200 токэнаў, і пераканайцеся ў рэкале на калькі рэальных запытанняў прычым пераменіць што-небудзь. Запісвайце, якія «родныя» элементы выкарыстоўваюцца для кожнага запытання, каб можаць выявіць раздэлы, якія завжды прабываюць у выніку і можаць быць занадта шырокімі. Обмежыце колькасць «родных» элементаў, якія надаюцца модэлю, адтак як тры-чатыры вялікія раздэлы вядома можуць запаўніць невялікі прызмат кантэксту, і залейце аднаковы чысты тэкст частак доступным для цітатаў, каб корыстувальнікі моглі бачыць, якае самэе рэчэнне падходзіць.

    Порэванне двух падходаў

    • Меркаванне: падход «родны-частка» працуе з двума рознымі рангамі дзеяння на тым самым тэксце, тады калі іерархічны індэксінг мадэлюе весь структурны план.
    • Галоўная перадчыннасць: падход «родны-частка» адзыходзіць ад кантэксту навакол падходжання, тады калі іерархія скурчвае тое месца, дзе пачынаецца пошук.
    • Косц: мадэль «роды-дзеці» патрабуе звязкі між дзецямі і родамі, тады як іерархія патрабуе выявлення структуры і крок направлення на кожным рэвэне.
    • Падходжанне: мадэль «роды-дзеці» падходзіць для большасці дзейнаўдагах дакументаў, тады як іерархія — для вялікіх, глыбока структураваных істочнікаў.

    Яны добра супрацоўнае, адтуды ў іерархіі ў самым ніжнім рэвэне можна мець часткі, створаныя за прыметам «роды-дзеці».

    Працэс, які можна выкорыстоўваць

    1. Прыем дакументаў. Аналізаваць дакументы і выявляць загалоўкі. Завантажчык павінен зберагаць інфармацыю пра загалоўкі, тое ў якім тэкста паўіная да якога раздзела, а не прыемліваць усё як адной струмені, калі ўсе наступныя этапы залежаць ад гэтага.
    2. Разбіўка на часткі. Ствараць родовыя часткі з цэлых раздзелаў, а пасля разбіваць кожную з іх на дзецячыя часткі, спачатку выявіўшы межу роду.
    3. Індэксаванне. Уключыце дзецячыя элементы і зберагучыце лінкі да ўзростаючых элементаў: або па копіюванні тэксту узростаючага элемента ў метаданы кожнага дзецячага элемента, або па зберагучыце parent_id для пашуку.
    4. Запіт. Уключыце запіт, адшукайце дзецячыя элементы, выберыце найболей прыдатныя рэзультаты, запрашайце ўзростаючыя элементы з усуненнем дуплікацый, складзіце адгэнераваны кантэкст і адправіце яго модэлю.

    Памылкі, якіх трэба ухіліцца

    • Толькі дзецячыя элементы. Вы павышаеце точнасць, але тыраце кантэкст для разумовых выводам; гэта прычына неудачы ў прыкладзе кэшавання, калі частка B так і не атрымлеўа рэчы, якая дапраўняе адпаведзь.
    • Толькі узростаючыя элементы. Вы захоўваеце кантэкст, але тыраце точнасць адшукування, таму што уключэнне роздзела з калькама тэм у якім-однам вектары размывае інфармацыю так сама, як і занадта вялікая частка.
    • Ігнараванне структуры. Калі загрузчык адхарае загалоўкі, не застаецца жадных меж секцый, якія моглы бы быць выкарыстаны для іерархічнага маршрутызавання, незалежна ад таго, насколькі хорашы ўстройцы маршрутызавання.
    • Слабыя метаданы. Без явных звязакаў з вышэйшымі элементамі, такіх як parent_text або parent_id, пазней стае немагчымым розгортанне інфармацыі. Метаданы неабходна зафіксаваць пад час стварэння часткі, адколі ўжо немагчыма ўзяць іх знову пасля адхароўкі.

    Дзе гэта даўа наібольшы эфект

    Гэтыя патэрны ўзьміцца ў выгоду там, дзе дакументы ўяўляюць сабою дзялёныя, структураваныя і взаімаасоўаныя матэрыялы: корпоратывныя базы знанняў, внутршняя дакументацыя, боты падтрымкі, архіўы правовых і норматыўных дакументаў, інжынерныя вікі і асістэнты для даследжэнняў. У такім матэрыяле жадны адзін розмер часткі не можа задовольніць адночасна трыбоўную точнасць і кантэкст.

    RAG пераходзіць ад простага пошуку з пасляўтварэнням тэксту да модэлю, які спачатку аналізуе арганізаваныя знанні, а потым робіць вывады на аднойчынку з тым, што знайшоў. Разбіўка на часткі па прынцыпе «бацька-дзеця» павышае розуменне на месцы, таму, дзе была знайдзена адпаведнасць, разам з яю прыходзіць і адпаведныя поясненні. Іерархічная індэксацыя павышае можлівасці глобальнага навігавання, таму пошук пачынаецца ў правым рэгіоне, а не сярод усьго. Плоская разбіўка на часткі падобнае да вырывання стороніцаў з кнігі і павернення модэлю права на адгадванне сюжэту. Ббы тыя два методы заставляюць кнігу залішыцца зв’язанай, але водночас дазволяюць знайсці адпаведную строчку. На практыцы гэта значыць меншае колькасць адпаведзей, якія ўсё ж такі майжэ правыя, што ёсць той прычынны факт, які больш за ўсё раздражае корыстувачаў, а таксама шар адзысквання дакументаў, чыяя дзеянні легча поясніць, калі хтось запытаецца, чаму адпаведны фрагмент потрапіў да модэлю.