자르기에 딸려 오는 것들 — 원본 위치와 제목
Article
앞 편에서 TextSplitter 로 132만 자를 9,005조각으로 잘랐다. 이번엔 라이브러리에 딸려 오는 기능 두 가지를 쓴다.
- ①② 웹에서 글을 가져온다끝남
- ③ 토막으로 자른다끝남
- ④ 딸려오는 기능들지금 여기
- ⑤⑥ 숫자로 바꿔 검색다음
1. 조각이 원본 어디서 왔나
09_start_index.py
import pathlib
from langchain_text_splitters import RecursiveCharacterTextSplitter
texts = {
p.stem: p.read_text(encoding="utf-8")
for p in sorted(pathlib.Path("posts").glob("*.md"))
}
slug = max(texts, key=lambda s: len(texts[s])) # 가장 긴 글 하나로 해본다
text = texts[slug]
print("고른 글:", slug)
splitter = RecursiveCharacterTextSplitter(
chunk_size=200,
chunk_overlap=20,
separators=["\n\n", "\n", ". ", " ", ""],
keep_separator="end",
add_start_index=True, # 원본 몇 번째 글자에서 시작했는지 기록한다
)
docs = splitter.create_documents(
[text],
metadatas=[{"source": slug}],
)
for d in docs[:4]:
print(d.metadata, "|", d.page_content[:34].replace("\n", " "))
- 고른 글: ai-eng-06-measuring-chunks
- {'source': 'ai-eng-06-measuring-chunks', 'start_index': 0} | # 어느 자르기가 나은가 — 말이 아니라
- {'source': 'ai-eng-06-measuring-chunks', 'start_index': 188} | 재는 순서 1. 정답을 아는 질문을 모은다
- {'source': 'ai-eng-06-measuring-chunks', 'start_index': 364} | ## 1. 먼저 `search.py` 를 마무리한다
- {'source': 'ai-eng-06-measuring-chunks', 'start_index': 549} | ai-course/search.py ``` # search.
start_index 가 붙었다. 「이 글의 364번째 글자부터」를 알 수 있다는 뜻이다.
결과에 나온 Document 는 본문과 정보를 함께 담은 객체다. 본문은 .page_content, 정보는 .metadata 로 꺼낸다.
2. 마크다운은 제목이 곧 경계
10_markdown_headers.py
import pathlib
from langchain_text_splitters import MarkdownHeaderTextSplitter
texts = {
p.stem: p.read_text(encoding="utf-8")
for p in sorted(pathlib.Path("posts").glob("*.md"))
}
slug = max(texts, key=lambda s: len(texts[s]))
splitter = MarkdownHeaderTextSplitter(
# (어느 제목 기호에서 끊을지, 그 제목을 무슨 이름으로 저장할지)
headers_to_split_on=[("#", "글"), ("##", "섹션")],
)
sections = splitter.split_text(texts[slug])
print(f"{slug} -> 섹션 {len(sections)}개")
for d in sections[:5]:
print(" ", d.metadata, "|", len(d.page_content), "자")
- ai-eng-06-measuring-chunks -> 섹션 11개
- {'글': '어느 자르기가 나은가…'} | 335 자
- {'글': '어느 자르기가 나은가…', '섹션': '1. 먼저 `search.py` 를 마무리한다'} | 2648 자
- {'글': '어느 자르기가 나은가…', '섹션': '2. 정답을 아는 질문을 만든다'} | 934 자
- {'글': '어느 자르기가 나은가…', '섹션': '3. 정답이 몇 위에 오는지 센다'} | 1312 자
- {'글': '어느 자르기가 나은가…', '섹션': '4. 세 방식을 나란히 잰다'} | 1832 자
주제별로 나뉘었고 섹션 이름이 자동으로 따라왔다. 첫 항목에 섹션 이 없는 것은 ## 가 나오기 전의 도입부라서다.
3. 두 단계로 붙인다
섹션 길이가 165자부터 4,041자까지 제각각이다. 제목으로 먼저 나누고, 긴 것만 크기로 다시 자른다.
11_two_stage.py
import pathlib
from langchain_text_splitters import (
MarkdownHeaderTextSplitter,
RecursiveCharacterTextSplitter,
)
texts = {
p.stem: p.read_text(encoding="utf-8")
for p in sorted(pathlib.Path("posts").glob("*.md"))
}
md = texts[max(texts, key=lambda s: len(texts[s]))]
sections = MarkdownHeaderTextSplitter(
headers_to_split_on=[("#", "글"), ("##", "섹션")],
).split_text(md)
sizer = RecursiveCharacterTextSplitter(
chunk_size=200,
chunk_overlap=20,
separators=["\n\n", "\n", ". ", " ", ""],
keep_separator="end",
)
chunks = sizer.split_documents(sections) # split_text 가 아니다
print(f"섹션 {len(sections)}개 -> 조각 {len(chunks)}개")
for d in chunks[:5]:
print(d.metadata.get("섹션"), "|", d.page_content[:30].replace("\n", " "))
split_documents 를 써야 위에서 붙은 섹션 이름이 최종 조각까지 따라온다. 첫 조각에는 섹션 이 없으므로 대괄호 대신 .get() 으로 꺼낸다.
이번 편에 나온 것
| 쓴 것 | 하는 일 |
|---|---|
add_start_index=True | 원본 몇 번째 글자에서 시작했나 |
.create_documents([본문], metadatas=[정보]) | 본문 + 정보를 묶어서 |
.split_documents(docs) | Document 를 받아 정보를 물려준다 |
MarkdownHeaderTextSplitter | # · ## 로 나누고 제목을 정보로 남긴다 |
headers_to_split_on=[("##", "섹션")] | 어느 기호에서 끊고 무슨 이름으로 저장할지 |
.page_content · .metadata | Document 에서 본문과 정보를 꺼낸다 |
.get("열쇠") | 없으면 None. 대괄호는 없으면 에러 |
다음 편에서는 조각을 숫자로 바꾼다.