Skip to content

Examples

dnjsgkfka edited this page Aug 26, 2026 · 1 revision

Evidence Chunker를 실제로 사용하는 방법을 설명한다.

설치

langchain/llamaindex extra는 둘 다 코어(chunk(), build_corpus())에 얹히는 얇은 export 레이어다. 프레임워크 연동 없이 표 추출만 필요하면 extra 없이 코어만 설치해도 된다.

용도 설치 명령
코어만 (표 추출) pip install -e .
LangChain 연동까지 pip install -e ".[langchain]"
LlamaIndex 연동까지 pip install -e ".[llamaindex]"
sim_threshold > 0 코사인 필터까지 pip install -e ".[similarity]"

Evidence Unit 생성

모든 표는 EvidenceChunker.chunk()의 반환값인 EvidenceUnit 인스턴스로 표현된다. 주요 필드는 다음과 같다.

필드 타입 설명
eu_id str "{doc_id}-p{page}-{idx}" 형식의 고유 식별자
caption_text str | None 매핑된 캡션. 못 찾으면 None
caption_confidence "direct" | "inferred" | "none" 캡션 매핑 신뢰도
table_html str | None 표 원본 HTML. 렌더링 실패 시 None
context_before / context_after list[str] 표 위/아래에서 흡수한 인접 단락
text str (property) LLM 컨텍스트용 전체 텍스트(HTML 포함)
retrieval_text str (property) 임베딩 검색용 축약 텍스트(HTML 제외)
retrieval_units list[str] (property) small-to-big 검색용 행 단위 조각
from evidence_chunker import EvidenceChunker

chunker = EvidenceChunker()
eus = chunker.chunk("report.pdf")

for eu in eus:
    print(eu.eu_id, "|", eu.caption_text, "|", eu.caption_confidence)

신뢰도가 낮은("inferred"/"none") EU만 따로 검수하고 싶을 때 caption_confidence로 필터링하면 된다. 단, bbox 거리·인접 페이지·병합 헤더 중 어떤 fallback으로 찾았는지는 "inferred" 하나로 뭉쳐 있어 이 필드만으로는 구분되지 않는다.

표만으로는 답이 안 되는 서술형 질문까지 커버하려면 build_corpus()로 표(EU)와 일반 본문 청크를 한 번에 받는다.

chunks = chunker.build_corpus("report.pdf")

tables = [c for c in chunks if not c.is_atomic]   # EvidenceUnit
texts = [c for c in chunks if c.is_atomic]        # 일반 본문(TextChunk)

build_corpus()는 생성자에 커스텀 parser를 넘긴 경우 NotImplementedError를 던진다(일반 본문 청킹이 Docling DocumentConverter 산출물에 직접 결합돼 있기 때문). 커스텀 파서를 쓰면서 표만 필요하면 chunk()를 쓸 것.

자세한 필드·메서드 시그니처는 API Reference 참고.


LangChain 통합

기본 (1 chunk = 1 Document)

from evidence_chunker.export.langchain import to_langchain, EvidenceRetriever
from langchain_core.vectorstores import InMemoryVectorStore
from langchain_huggingface import HuggingFaceEmbeddings

chunks = chunker.build_corpus("report.pdf")
docs = to_langchain(chunks)  # metadata["retrieval_text"]를 쓰고 싶으면 page_content 대신 직접 매핑할 것

vectorstore = InMemoryVectorStore.from_documents(docs, HuggingFaceEmbeddings(model_name="all-MiniLM-L6-v2"))
retriever = EvidenceRetriever(vectorstore, k=5)  # max-pool dedupe 기본 적용

retriever.invoke("서울 지역 Q2 매출은?")

행 단위 다중 벡터 (small-to-big)

표 안 특정 셀 값을 묻는 질의에 강하다. 같은 표의 다른 행 데이터에 유사도가 묻히는 문제를 줄인다.

from evidence_chunker.export.langchain import to_langchain_units

docs = to_langchain_units(chunks)   # EU 하나가 retrieval_units개의 작은 Document로 쪼개짐
vectorstore = InMemoryVectorStore.from_documents(docs, HuggingFaceEmbeddings(model_name="all-MiniLM-L6-v2"))

# fetch_k는 자동으로 max(k*4, 20)
retriever = EvidenceRetriever(vectorstore, k=5)
results = retriever.get_relevant_documents("서울 Q1 매출은 얼마?")

# 실제 LLM에 넘길 땐 검색에 쓰인 작은 조각(page_content) 대신 표 전체 맥락을 사용
for doc in results:
    context = doc.metadata["parent_text"]

LlamaIndex

API 대응 관계는 LangChain과 동일하다: to_llamaindex() / to_llamaindex_units() / EvidenceRetriever(llamaindex 버전).

from evidence_chunker.export.llamaindex import to_llamaindex_units, EvidenceRetriever as LlamaEvidenceRetriever
from llama_index.core import VectorStoreIndex

nodes = to_llamaindex_units(chunks)
index = VectorStoreIndex(nodes)
base_retriever = index.as_retriever(similarity_top_k=20)  # dedupe 전이라 넉넉하게

retriever = LlamaEvidenceRetriever(base_retriever, k=5)
retriever.retrieve("서울 Q2 매출은?")

sim_threshold 조정

기본값(sim_threshold=0.0)은 bbox 거리만으로 인접 단락을 채택해 sentence-transformers를 아예 로드하지 않는다. 표 주변에 무관한 텍스트(예: 여러 단이 좁은 간격으로 배치된 논문)가 자주 섞여 들어온다면 코사인 필터를 추가로 켠다.

# pip install -e ".[similarity]" 로 sentence-transformers 설치 필요
chunker = EvidenceChunker(bbox_threshold=300.0, sim_threshold=0.35)
eus = chunker.chunk("dense_two_column_paper.pdf")

기본값은 0.0이며, 근거가 되는 실험 로그는 Experiments 또는 Configuration 참고.


bbox_threshold 조정

bbox_threshold(기본 300pt)는 표 위/아래로 몇 pt 이내의 단락까지 인접 문맥으로 볼지 결정한다. 표와 설명 단락 사이 여백이 유난히 넓은 보고서(예: 정부 공시 문서)라면 값을 올려야 문맥이 누락되지 않는다.

# 여백이 넓은 문서: 더 넓게 탐색
chunker_wide = EvidenceChunker(bbox_threshold=500.0)

# 표가 밀집된 문서: 옆 표의 캡션이 잘못 붙는 걸 방지하려면 좁게
chunker_tight = EvidenceChunker(bbox_threshold=150.0)

300pt를 기본값으로 정한 근거는 Experiments 또는 Configuration 참고.


커스텀 파서 주입

표 추출 및 청킹 알고리즘(caption.py, context.py 등)은 특정 PDF 분석 도구(예: Docling)에 묶여 있지 않고, ParsedDoc(parser.base.ParsedDoc)이라는 공통 양식(인터페이스)만 바라보고 동작하도록 설계하였다.

따라서 PdfParser 프로토콜(parse(path) -> ParsedDoc)만 구현하면 다른 PDF 파서로 교체할 수 있다.

from evidence_chunker.parser.base import PdfParser, ParsedDoc

class MyCustomParser:
    def parse(self, path: str) -> ParsedDoc:
        ...  # 다른 파서(예: MinerU)의 출력을 ParsedDoc으로 매핑

chunker = EvidenceChunker(parser=MyCustomParser())
eus = chunker.chunk("report.pdf")   # OK — chunk()는 parser 교체를 지원
chunker.build_corpus("report.pdf")  # NotImplementedError — 위 "핵심 모듈: Evidence Unit 생성" 참고

현재 저장소에는 DoclingParser 구현체만 포함되어 있다.


여러 PDF 배치 처리

doc_id를 지정하지 않으면 파일명 stem이 자동으로 쓰이지만, 여러 문서를 하나의 벡터스토어에 합칠 때는 충돌 방지를 위해 명시적으로 지정하는 걸 권장한다.

import glob
from pathlib import Path

chunker = EvidenceChunker()
all_chunks = []
for path in glob.glob("data/pdfs/*.pdf"):
    doc_id = Path(path).stem
    all_chunks.extend(chunker.build_corpus(path, doc_id=doc_id))

print(f"총 {len(all_chunks)}개 청크 (표 {sum(not c.is_atomic for c in all_chunks)}개)")

EvidenceChunker는 Docling DocumentConverter를 첫 파싱 시점에 lazy하게 한 번만 생성해 재사용하므로, 같은 인스턴스로 여러 PDF를 반복 처리해도 매번 모델을 다시 로드하지 않는다.


raw vectorstore에서 직접 dedupe하기

EvidenceRetriever를 안 쓰고 벡터스토어 API를 직접 호출하는 기존 코드에 끼워 넣고 싶다면 dedupe_by_chunk_id()만 가져다 쓸 수 있다.

from evidence_chunker.export.langchain import dedupe_by_chunk_id

results = vectorstore.similarity_search_with_score(query, k=20)  # k는 넉넉하게
top5 = dedupe_by_chunk_id(results, k=5)

(Document, score) 튜플 리스트와 Document 리스트 둘 다 받는다. LlamaIndex는 evidence_chunker.export.llamaindex.dedupe_by_chunk_id가 동일하게 동작하며 NodeWithScore/TextNode를 구분 없이 처리한다.


캡션 없는 표 / 목차 표 오탐

  • 캡션 없는 표: direct(captions 참조) 매칭이 실패하면 bbox 거리 → 인접 페이지 → 병합 헤더 순으로 fallback한다. 넷 다 실패하면 caption_text=None, caption_confidence="none"으로 남으니, 이 경우 retrieval_text에 표 제목이 없다는 걸 감안해 상위 애플리케이션에서 별도 처리(예: 섹션 헤더로 대체)할 수 있다.
  • 목차(TOC)/그림 목차(LOF) 표 오탐: 텍스트가 표 형태로 파싱된 목차 페이지는 filters.is_toc_or_lof_decoy()로 자동 제외된다. 별도 설정 없이 기본 동작이다.
  • 동일 표 중복 인식: Docling이 표 하나를 TableItem 2개로 중복 인식하는 경우, 더 세밀하게 구조화된 쪽만 남기고 캡션은 살아남은 쪽에 이전한다(filters.find_duplicate_tables()).

알고리즘 세부사항은 Architecture 참고.

Clone this wiki locally