Parent-Child Chunking

문정현·2025년 2월 3일
post-thumbnail

기존의 Retriever는 문서를 일정한 길이와 구분자(separator)를 기준으로 나누고, 일정 부분을 overlap(겹침) 시켜 저장하는 방식이다. 하지만 이러한 방법에는 몇 가지 단점이 있다.

  • 데이터 중복 문제: overlap을 두는 과정에서 원본 텍스트가 중복 저장되어 DB 용량이 증가하고 비용이 증가한다.
  • 맥락 상실 문제: 단순히 나눈 청크를 개별적으로 유사도 검색하면 문서 전체의 맥락을 충분히 반영하지 못할 가능성이 크다.

이를 해결하기 위한 방법이 Parent-Child Chunking이다. 이 방식은 문서의 부모(Parent) 문서와 자식(Child) 문서를 인덱싱하여, 유사도 검색을 통해 특정 청크(Child)를 찾은 후 부모 문서를 참조하여 더 풍부한 정보를 제공할 수 있도록 한다.


Parent-Child Chunking 개념

기본적인 흐름은 다음과 같다:

  1. 문서의 구조를 유지하면서 청크를 생성
    • 원본 문서를 큰 단위(Parent)와 작은 단위(Child)로 구분하여 저장
  2. Child 문서를 먼저 검색
    • 사용자의 쿼리에 대해 먼저 Child 문서에서 유사한 부분을 찾음
  3. Parent 문서를 참조하여 맥락을 유지
    • 검색된 Child 문서의 부모를 찾아 전체 문서의 맥락을 반영한 결과를 제공

이를 코드로 구현하여, 일반적인 embedding 방식과 Parent-Child 방식이 어떻게 다른지 비교해보자.

샘플 데이터는 나무위키에 사막여우에 대한 정보를 text파일로 저장해서 진행하도록 한다

# 사막여우 나무위키

가장 작은 개과 동물 종으로, 수컷은 머리부터 몸까지의 크기가 39~39.5cm, 꼬리 길이는 23~25cm, 귀 길이는 10cm이며 무게는 최소 1.3kg이다. 암컷은 머리부터 몸까지의 크기가 34.5~39.5cm, 꼬리 길이는 23~25cm, 귀 길이는 9~9.5cm이며 무게는 1~1.9kg이다.

몸과 머리에 비해 귀가 크고 얇게 발달되었다. 땀을 흘리지 않는 개과 동물의 특성상, 고온의 사막기후에서 몸 안의 열을 효과적으로 배출해야 하기 때문.[1] 큰 귀는 작은 소리에도 민감하여 주변을 경계하거나 작은 먹잇감을 찾을 때 용이하다. 때문에 큰 소리에는 스트레스를 잘 받으며, 성격도 경계심이 많고 예민한 편이다.

털은 모래와 같은 ■모래색(회황색)을 띠고 있으며, 부분적으로 ■황토색을 띤다. 분포 지역에 따라 색의 농도에 차이가 있다. 계절이나 밤낮에 따라 기온차가 심한 사막 생활에서 체온을 지켜주는 역할을 하며, 발바닥까지 자라난 털은 태양으로부터 뜨겁게 달궈지는 사막의 모래 표면을 걸어다닐 수 있도록 보호하는 역할을 한다.

꼬리의 털은 덥수룩하며 끝이 검고 시작점에 까만 얼룩이 있는 게 특징이다. 다만 꼬리털의 풍성함은 개체마다 차이를 보인다.[2] 꼬리 시작점의 얼룩이 꼬리끝의 얼룩과 이어지는 개체들도 있으나, 일정하게 전부 까맣게 연결되지는 않는다. 꼬리의 얼룩은 새끼일 때부터 육안으로 확인이 가능하다. 페넥폭스가 모래여우종들과 외형적으로 흡사하여 전문가도 구분하기 어렵다고 하지만, 이러한 꼬리의 특징만 안다면 누구나 쉽게 구분할 수 있다.

의외로 굉장히 오래된 종이며 늑대와 여우가 분리되는 시절에 처음 등장했다. 여우속에 속하지만, 다른 여우종들과는 생물학적 차이도 존재해 일부 학자들은 사막여우속(Fennecus)의 유일한 종으로 분류하기도 한다. 사막여우는 때로 사막에 사는 여우 를 통칭하는 의미로도 쓰이지만,[3] 기본적으로 Fennec fox(페넥폭스)의 널리 알려진 별명이다. 한국에서만 사막여우라고 부르는 게 아니다. 아래 언급되는 명칭 장난질 때문에 'desert fox'를 한국에서만 부르는 명칭으로 기록할 필요는 없다.

일반적인 Chunking 방식

import langchain
from dotenv import load_dotenv
from langchain_community.document_loaders import TextLoader
from langchain_community.vectorstores import Chroma
from langchain_openai import OpenAIEmbeddings
from langchain_text_splitters import CharacterTextSplitter

langchain.debug = True

load_dotenv()

embeddings = OpenAIEmbeddings()
vector_store = Chroma(persist_directory="chroma_fox", embedding_function=embeddings)

loader = TextLoader("./data/fennec_fox.txt")
documents = loader.load()
text_splitter = CharacterTextSplitter(chunk_size=1000, chunk_overlap=0)
texts = text_splitter.split_documents(documents)
Chroma.from_documents(texts, embeddings, persist_directory="chroma_fox")

question = "사막여우의 귀가 큰 이유는?"
results = vector_store.similarity_search_with_score(question)

for result in results:
    print("\n")
    print(result[1])
    print(result[0].page_content)

🔹 결과: 단순하게 개별 청크에 대한 유사도 검색이 이루어지므로, 전체 문맥을 반영하지 못할 가능성이 있음.


Parent-Child Chunking 방식

from dotenv import load_dotenv
from langchain.retrievers import ParentDocumentRetriever
from langchain.storage import LocalFileStore, create_kv_docstore
from langchain_community.document_loaders import TextLoader
from langchain_community.vectorstores import Chroma
from langchain_openai import OpenAIEmbeddings
from langchain_text_splitters import RecursiveCharacterTextSplitter

load_dotenv()

# 두 개의 문서를 로딩
loaders = [
    TextLoader("./data/fennec_fox1.txt"),
    TextLoader("./data/fennec_fox2.txt"),
]

docs = []
for loader in loaders:
    docs.extend(loader.load())

# 부모 문서는 청크 크기를 900, 자식 문서는 300
parent_splitter = RecursiveCharacterTextSplitter(chunk_size=900)
child_splitter = RecursiveCharacterTextSplitter(chunk_size=300)

vectorstore = Chroma(
    persist_directory="chroma_parent_fox",
    collection_name="split_parents",
    embedding_function=OpenAIEmbeddings(),
)

fs = LocalFileStore(root_path="./parent_document_store")
store = create_kv_docstore(fs)

retriever = ParentDocumentRetriever(
    vectorstore=vectorstore,
    docstore=store,
    child_splitter=child_splitter,
    parent_splitter=parent_splitter,
)

retriever.add_documents(docs, ids=None, add_to_docstore=True)

🔹 결과:

  • 일반적인 방식에서는 Child 문장만 반환되지만, Parent-Child 방식에서는 더 넓은 문맥을 반영하여 Parent 문장도 함께 반환된다.

Parent-Child 방식의 장점

방식검색 범위중복 저장맥락 유지
일반 Chunking개별 청크OX
Parent-Child문서 단위XO
  1. 중복 저장 최소화: Child 문서에 대해 Parent 정보를 저장하는 방식이므로 데이터 중복을 줄일 수 있다.
  2. 맥락 유지: 검색된 청크가 포함된 문서 전체를 참조할 수 있어 더 풍부한 정보를 제공할 수 있다.
  3. 더 나은 검색 결과: 단순 청크 검색이 아니라, Parent 문서까지 고려하여 답변의 품질을 높일 수 있다.

🚀 결론 및 회고

기존 Retriever 방식은 문서를 단순히 일정한 길이로 나누고 겹쳐 저장하는 방식이었으나, Parent-Child Chunking을 활용하면 문서의 계층적 구조를 유지하면서 더 효율적인 검색을 수행할 수 있다. 이를 통해 중복 저장 문제를 해결하고 문서의 맥락을 유지하면서 검색 결과를 향상시킬 수 있다.

향후에는 이를 실제 DB에 적용하고, Query Expansion 등의 추가 기법과 결합하여 더욱 강력한 검색 시스템을 구축하는 방향으로 발전시켜 볼 수 있을 것이다.


profile
기록 == 성장

0개의 댓글