직접 짠 자르기를 라이브러리로 갈아끼운다
Article
9·10강에서 자르기를 손으로 만들었다. 글자 수로 자르고, 겹치게 하고, 문단으로 나누고, 문장 끝을 찾아 끊고, 짧은 꼬리를 앞에 붙였다. chunk_smart 하나에 그게 다 들어 있다.
그런데 이건 남들도 다 하는 일이다. 이미 만들어져 있다.
이번 강의에서 갈아끼워 본다. 그리고 결과가 뜻밖이다.
- 라이브러리를 그냥 끼운다우리 것보다 나쁘다
- 왜 나쁜지 찾는다우리가 한 걸 안 한다
- 설정으로 고친다구분자 · keep_separator
- 다시 잰다그래서 이겼나?
파일은 계속 ai-course 폴더에 넣는다. 이번 강의는 131번부터다.
1. 깔고 그냥 돌려본다
python -m pip install langchain-text-splitters
backend.txt 하나만 놓고 우리 것과 나란히 돌려본다.
import search
from langchain_text_splitters import RecursiveCharacterTextSplitter
text = search.load_documents()[0]["text"] # backend.txt
print("=== 우리가 만든 chunk_smart(60, 20) ===")
for i, c in enumerate(search.chunk_smart(text, 60, 20)):
print(f"{i} ({len(c)}자) {c}")
print()
print("=== 라이브러리 기본값 ===")
splitter = RecursiveCharacterTextSplitter(
chunk_size=60, # 조각 하나의 최대 글자 수
chunk_overlap=0, # 겹치는 양. 9강에서 만든 그 개념이다
)
for i, c in enumerate(splitter.split_text(text)):
print(f"{i} ({len(c)}자) {c}")
- === 우리가 만든 chunk_smart(60, 20) ===
- 0 (6자) 수업용 예제
- 1 (64자) 백엔드 개발자는 서버와 데이터베이스를 다룬다. 사용자 눈에 보이지 않는 곳에서 데이터를 저장하고 꺼내는 일을 한다.
- 2 (56자) 주로 쓰는 언어는 자바, 파이썬, 노드다. 회사마다 다르지만 하나를 깊게 아는 편이 여럿을 얕게 아는
- 3 (53자) 것보다 낫다. 요즘은 클라우드 지식도 함께 요구된다. 서버를 직접 사는 회사가 줄었기 때문이다.
- 4 (69자) 신입에게 가장 많이 물어보는 것은 데이터베이스다. 인덱스가 무엇인지, 조인이 왜 느려지는지를 설명할 수 있으면 절반은 됐다.
- === 라이브러리 기본값 ===
- 0 (6자) 수업용 예제
- 1 (57자) 백엔드 개발자는 서버와 데이터베이스를 다룬다. 사용자 눈에 보이지 않는 곳에서 데이터를 저장하고 꺼내는
- 2 (6자) 일을 한다.
- 3 (56자) 주로 쓰는 언어는 자바, 파이썬, 노드다. 회사마다 다르지만 하나를 깊게 아는 편이 여럿을 얕게 아는
- 4 (53자) 것보다 낫다. 요즘은 클라우드 지식도 함께 요구된다. 서버를 직접 사는 회사가 줄었기 때문이다.
- 5 (57자) 신입에게 가장 많이 물어보는 것은 데이터베이스다. 인덱스가 무엇인지, 조인이 왜 느려지는지를 설명할 수
- 6 (11자) 있으면 절반은 됐다.
5조각이 7조각이 됐고, 늘어난 둘이 문제다.
일을 한다. 와 있으면 절반은 됐다. — 앞 문장에서 떨어져 나온 꼬리다. 이 조각들은 혼자서는 아무 뜻이 없다. 12강에서 본 대로 짧은 조각은 방향도 흐릿하다.
2. 무엇을 안 하고 있나
우리 chunk_smart 가 한 일을 다시 꺼내 본다. 10강에서 만든 그 함수다.
우리 chunk_smart
10강에서 만든 것
.를 찾아 문장 끝에서 끊는다- 짧은 조각은 앞에 도로 붙인다 (min_size)
- 그래서 60자를 조금 넘겨도 문장을 지킨다
- 우리 문서에 맞춰 만들었다
라이브러리 기본값
설정을 안 준 상태
- 빈 줄 → 줄바꿈 → 공백 순으로 끊는다
- 짧아도 그대로 둔다 — 붙이는 기능이 없다
- 60자를 절대 안 넘긴다. 대신 문장이 갈라진다
- 어느 문서에나 무난한 값이다
라이브러리가 끊을 자리를 고르는 순서는 이렇다. 큰 경계부터 시도하고, 그래도 크면 더 작은 경계로 내려간다. 이름에 「재귀」가 붙은 이유다.
- 빈 줄로 끊기\n\n — 문단 경계. 1순위
- 줄바꿈으로 끊기\n — 아직도 크면
- 공백으로 끊기" " — 여기서 문장이 갈라진다
- 그냥 글자로 끊기"" — 최후 수단. 단어가 쪼개진다
빠진 것이 보인다. 목록에 . 가 없다. 우리 cut_once 는 rfind(". ") 로 마침표를 먼저 찾았는데, 라이브러리는 그 단계를 아예 안 갖고 있다.
3. 끊을 자리를 알려준다
목록은 우리가 넘길 수 있다.
import search
from langchain_text_splitters import RecursiveCharacterTextSplitter
text = search.load_documents()[0]["text"]
splitter = RecursiveCharacterTextSplitter(
chunk_size=60,
chunk_overlap=0,
separators=["\n\n", "\n", ". ", " ", ""],
# ↑ 마침표+공백을 3순위로 끼워넣었다. 우리 cut_once 가 하던 일이다
)
for i, c in enumerate(splitter.split_text(text)):
print(f"{i} ({len(c)}자) {c}")
- 0 (6자) 수업용 예제
- 1 (24자) 백엔드 개발자는 서버와 데이터베이스를 다룬다
- 2 (40자) . 사용자 눈에 보이지 않는 곳에서 데이터를 저장하고 꺼내는 일을 한다.
- 3 (22자) 주로 쓰는 언어는 자바, 파이썬, 노드다
- 4 (41자) . 회사마다 다르지만 하나를 깊게 아는 편이 여럿을 얕게 아는 것보다 낫다
- 5 (47자) . 요즘은 클라우드 지식도 함께 요구된다. 서버를 직접 사는 회사가 줄었기 때문이다.
- 6 (26자) 신입에게 가장 많이 물어보는 것은 데이터베이스다
- 7 (43자) . 인덱스가 무엇인지, 조인이 왜 느려지는지를 설명할 수 있으면 절반은 됐다.
꼬리 조각은 사라졌다. 그런데 새 문제가 생겼다.
조각이 . 로 시작한다. 앞 문장의 마침표가 다음 조각 앞에 붙었다. 문장 끝의 마침표는 사라지고, 다음 조각 머리에 뜬금없는 점이 하나 붙어 있다.
4. 마침표를 앞 조각에 남긴다
구분자를 어느 쪽에 붙일지 정하는 설정이 따로 있다.
import search
from langchain_text_splitters import RecursiveCharacterTextSplitter
text = search.load_documents()[0]["text"]
splitter = RecursiveCharacterTextSplitter(
chunk_size=60,
chunk_overlap=0,
separators=["\n\n", "\n", ". ", " ", ""],
keep_separator="end",
# ↑ 구분자를 앞 조각의 끝에 남긴다. 기본값은 "start" 라 다음 조각 앞에 붙는다
)
for i, c in enumerate(splitter.split_text(text)):
print(f"{i} ({len(c)}자) {c}")
- 0 (6자) 수업용 예제
- 1 (25자) 백엔드 개발자는 서버와 데이터베이스를 다룬다.
- 2 (38자) 사용자 눈에 보이지 않는 곳에서 데이터를 저장하고 꺼내는 일을 한다.
- 3 (23자) 주로 쓰는 언어는 자바, 파이썬, 노드다.
- 4 (40자) 회사마다 다르지만 하나를 깊게 아는 편이 여럿을 얕게 아는 것보다 낫다.
- 5 (45자) 요즘은 클라우드 지식도 함께 요구된다. 서버를 직접 사는 회사가 줄었기 때문이다.
- 6 (27자) 신입에게 가장 많이 물어보는 것은 데이터베이스다.
- 7 (41자) 인덱스가 무엇인지, 조인이 왜 느려지는지를 설명할 수 있으면 절반은 됐다.
전부 문장 단위로 깔끔하게 끊겼다. 모든 조각이 마침표로 끝난다.
세 문서 전체로 보면 이렇다.
| 방식 | 조각 수 | 20자 미만 |
|---|---|---|
우리 chunk_smart(60, 20) | 13개 | 3개 |
| 라이브러리 기본값 | 15개 | 5개 |
라이브러리 + 마침표 + keep_separator | 16개 | 3개 |
20자 미만 조각이 3개로 내려왔다. 우리 것과 같다. 다만 아직 0은 아니다.
5. 라이브러리에 없는 기능 하나
우리 chunk_smart 에는 있고 라이브러리에는 없는 것이 하나 남았다. 10강에서 만든 그것이다.
if len(head) < min_size and out:
out[-1] = out[-1] + " " + head # 짧은 꼬리를 앞 조각에 붙인다
라이브러리에는 이런 설정이 없다. 그러니 그 부분만 우리 코드로 감싼다. search.py 맨 아래에 덧붙인다.
# ... 12강까지의 내용 아래에 덧붙인다
from langchain_text_splitters import RecursiveCharacterTextSplitter
def chunk_lib(text, size=60, min_size=20):
"""라이브러리로 자르되, 짧은 꼬리 붙이기는 우리가 한다.
10강의 chunk_smart 와 결과가 거의 같다. 자르는 일만 남에게 맡긴 것이다.
"""
splitter = RecursiveCharacterTextSplitter(
chunk_size=size,
chunk_overlap=0,
separators=["\n\n", "\n", ". ", " ", ""],
keep_separator="end",
)
out = []
for piece in splitter.split_text(text):
if out and len(piece) < min_size:
out[-1] = out[-1] + " " + piece # 10강에서 만든 그 줄 그대로다
else:
out.append(piece)
return out
def build_chunks_lib(folder=None, size=60, min_size=20):
"""chunk_lib 판 build_chunks. 부르는 함수 하나만 다르다."""
chunks = []
for doc in load_documents(folder):
for piece in chunk_lib(doc["text"], size, min_size):
if piece.strip():
chunks.append({"source": doc["source"], "text": piece})
return chunks
11강에서 build_chunks_plain 을 남겨둔 것과 같은 이유로 새 함수를 따로 만든다. 옛 방식을 지우면 비교할 대상이 사라진다.
그런데 이 문서로 돌려보면 꼬리 붙이기가 한 번도 발동하지 않는다. 20자 미만 조각 셋이 전부 각 문서의 첫 줄 수업용 예제 인데, 그 자리에서는 out 이 비어 있어 앞에 붙일 조각이 없기 때문이다.
6. 출처를 공짜로 얻는다
여기부터가 라이브러리로 갈아탈 진짜 이유다. 우리 코드에는 없던 기능이 딸려 온다.
지금 우리 조각은 {"source": ..., "text": ...} 딕셔너리다. 어느 파일에서 왔는지는 알지만 그 파일의 어디쯤인지는 모른다.
import search
from langchain_text_splitters import RecursiveCharacterTextSplitter
docs = search.load_documents()
splitter = RecursiveCharacterTextSplitter(
chunk_size=60,
chunk_overlap=0,
separators=["\n\n", "\n", ". ", " ", ""],
keep_separator="end",
add_start_index=True,
# ↑ 조각이 원본의 몇 번째 글자에서 시작했는지 기록한다
)
out = splitter.create_documents(
[d["text"] for d in docs], # 본문 목록
metadatas=[{"source": d["source"]} for d in docs], # 짝이 되는 정보 목록
)
# ↑ split_text 는 문자열만 준다. create_documents 는 정보를 같이 묶어 준다
print("총", len(out), "조각")
for d in out[:4]:
print(d.metadata, "|", d.page_content[:28])
- 문서 3건을 읽었다 (폴더: docs)
- 총 16 조각
- {'source': 'backend.txt', 'start_index': 0} | 수업용 예제
- {'source': 'backend.txt', 'start_index': 8} | 백엔드 개발자는 서버와 데이터베이스를 다룬다.
- {'source': 'backend.txt', 'start_index': 34} | 사용자 눈에 보이지 않는 곳에서 데이터를 저장하고
- {'source': 'backend.txt', 'start_index': 74} | 주로 쓰는 언어는 자바, 파이썬, 노드다.
start_index 가 붙었다. 「backend.txt 의 34번째 글자부터」 를 알 수 있다는 뜻이다. 검색 결과에 「이 답은 여기서 나왔습니다」를 붙이고, 원문의 그 자리로 보내줄 수 있다.
| 메서드 | 넣는 것 | 나오는 것 |
|---|---|---|
split_text | 문자열 하나 | 문자열 목록 |
create_documents | 문자열 목록 + 정보 목록 | Document 목록 |
split_documents | Document 목록 | Document 목록 (정보 유지) |
7. 마크다운은 제목이 공짜 구분선
우리 문서는 .txt 라 빈 줄밖에 단서가 없었다. 마크다운이라면 ## 가 이미 완벽한 경계다.
from langchain_text_splitters import MarkdownHeaderTextSplitter
md = """# 사내 규정
## 환불 정책
주문 후 7일 이내에 환불 신청이 가능하다.
단순 변심이면 왕복 배송비는 고객 부담이다.
## 배송 안내
배송은 결제일 기준 2~3일 걸린다.
5만원 이상이면 배송비가 없다.
"""
splitter = MarkdownHeaderTextSplitter(
headers_to_split_on=[("#", "문서"), ("##", "섹션")],
# ↑ 어느 제목에서 끊을지, 그 제목을 무슨 이름으로 저장할지
)
for d in splitter.split_text(md):
print(d.metadata)
print(" ", d.page_content.replace("\n", " / "))
- {'문서': '사내 규정', '섹션': '환불 정책'}
- 주문 후 7일 이내에 환불 신청이 가능하다. / 단순 변심이면 왕복 배송비는 고객 부담이다.
- {'문서': '사내 규정', '섹션': '배송 안내'}
- 배송은 결제일 기준 2~3일 걸린다. / 5만원 이상이면 배송비가 없다.
주제가 정확히 주제별로 나뉘었고, 섹션 이름이 자동으로 따라왔다. 나중에 「사내 규정 > 환불 정책」이라고 출처를 찍을 수 있다.
실무 문서 상당수가 마크다운이다. 위키, 사내 문서, README 가 전부 그렇다. 글자 수를 세기 전에 제목부터 보는 게 순서다.
- 01
제목으로 먼저 나눈다
주제가 섞이지 않게 하는 게 1순위다. 환불 이야기와 배송 이야기는 아무리 짧아도 한 조각에 들어가면 안 된다. 12강에서 본 대로 주제가 섞이면 방향이 뭉개진다.
sections = MarkdownHeaderTextSplitter( headers_to_split_on=[("#", "문서"), ("##", "섹션")] ).split_text(md) - 02
긴 섹션만 크기로 다시 자른다
섹션 하나가 A4 세 장이면 그때만 다시 자른다. 짧은 섹션은 그대로 통과한다.
split_documents를 쓰면 위에서 붙은 섹션 이름이 그대로 따라온다.sizer = RecursiveCharacterTextSplitter(chunk_size=60, chunk_overlap=0) chunks = sizer.split_documents(sections)
8. 그래서 이겼나 — 다시 잰다
11강에서 만든 질문 여섯 개, 12강에서 만든 임베딩 점수. 재는 방법을 그대로 두고 자르는 방식만 바꿔 잰다.
import search
from langchain_text_splitters import RecursiveCharacterTextSplitter
QUESTIONS = [
("인덱스가 뭔지 물어본다", "backend.txt"),
("화면이 느리면 어떻게 되나", "frontend.txt"),
("데이터가 틀리면 어떻게 되나", "data.txt"),
("타입스크립트를 쓰나", "frontend.txt"),
("파이프라인을 짜는 일", "data.txt"),
("서버와 데이터베이스를 다루는 일", "backend.txt"),
]
def build_plain_lib(size=60):
"""설정을 하나도 안 준 라이브러리 기본값. 비교용이라 여기서만 쓴다."""
splitter = RecursiveCharacterTextSplitter(chunk_size=size, chunk_overlap=0)
return [
{"source": doc["source"], "text": piece}
for doc in search.load_documents()
for piece in splitter.split_text(doc["text"])
]
cases = [
("우리 chunk_smart", search.build_chunks(size=60, min_size=20)),
("lib 기본값", build_plain_lib(size=60)),
("lib 설정 맞춤", search.build_chunks_lib(size=60, min_size=0)),
("lib + 꼬리붙이기", search.build_chunks_lib(size=60, min_size=20)),
]
for name, chunks in cases:
ranks = [search.embed_rank(chunks, q, a) for q, a in QUESTIONS]
top1 = sum(1 for r in ranks if r == 1)
print(f"{name:18s} 조각 {len(chunks):2d}개 1위 {top1}/6 평균 {sum(ranks) / 6:.2f} {ranks}")
- 문서 3건을 읽었다 (폴더: docs)
- 우리 chunk_smart 조각 13개 1위 6/6 평균 1.00 [1, 1, 1, 1, 1, 1]
- lib 기본값 조각 15개 1위 5/6 평균 1.17 [1, 1, 1, 1, 2, 1]
- lib 설정 맞춤 조각 16개 1위 6/6 평균 1.00 [1, 1, 1, 1, 1, 1]
- lib + 꼬리붙이기 조각 16개 1위 6/6 평균 1.00 [1, 1, 1, 1, 1, 1]
결론이 싱겁다. 우리가 만든 것과 똑같다.
라이브러리 기본값은 오히려 5/6 으로 더 나쁘다. 제대로 설정해야 겨우 동점이다. 며칠 걸려 만든 chunk_smart 를 라이브러리로 갈아끼웠는데 숫자가 하나도 안 움직였다.
마지막 두 줄은 조각 수까지 같다. 5절에서 본 대로 이 문서에서는 꼬리 붙이기가 발동할 자리가 없기 때문이다.
9. 그럼 왜 쓰나
| 항목 | 우리 코드 | 라이브러리 |
|---|---|---|
| 검색 품질 | 6/6 | 6/6 — 차이 없음 |
| 설정을 안 줬을 때 | 우리 문서에 맞음 | 더 나쁨 |
출처 위치(start_index) | 없다. 직접 만들어야 | 설정 한 줄 |
| 마크다운 제목 자르기 | 없다. 직접 만들어야 | 전용 도구가 있다 |
| 코드 줄 수 | cut_once + chunk_smart 28줄 | 설정 6줄 + 꼬리 5줄 |
| 새 형식이 필요할 때 | 직접 짠다 | HTML · 코드 · 표 전용이 이미 있다 |
| 이해도 | 내가 다 안다 | 안 보이는 동작이 있다 |
정리하면 이렇다. 라이브러리는 지금 있는 것을 더 잘하게 해주지 않는다. 다음에 필요할 것을 미리 갖고 있다.
start_index 와 마크다운 제목은 우리 코드에 없었고, 직접 만들려면 또 며칠이다. 앞으로 HTML 이나 소스코드를 자를 일이 생기면 그때도 이미 있다.
이번 강의에 나온 것
| 쓴 것 | 하는 일 |
|---|---|
pip install langchain-text-splitters | 자르기 도구를 깐다 (몇 MB) |
RecursiveCharacterTextSplitter(...) | 자르는 도구 하나를 만든다 |
chunk_size · chunk_overlap | 9강에서 만든 그 두 값 |
separators=[...] | 끊을 자리 후보. 앞에서부터 시도한다 |
keep_separator="end" | 구분자를 앞 조각에 남긴다. 기본값은 start |
add_start_index=True | 원본 몇 번째 글자에서 시작했나 |
.split_text(text) | 문자열 목록을 돌려준다 |
.create_documents([본문], metadatas=[정보]) | 본문과 정보를 묶어 돌려준다 |
MarkdownHeaderTextSplitter | # · ## 로 나누고 제목을 정보로 남긴다 |
.page_content · .metadata | Document 에서 본문과 정보를 꺼낸다 |
미션
- 01
[필수] keep_separator 를 빼고 눈으로 확인한다
ai-course/137_미션필수.py.keep_separator="end"만 지우고 돌려서 조각 앞에 점이 붙는 것을 직접 본다. 그 상태로136_재기.py를 돌려 점수가 떨어지는지도 확인한다. - 02
[응용] 내 마크다운 문서를 잘라본다
ai-course/138_미션응용.py. 아무 README 나 사내 문서를docs에 넣고MarkdownHeaderTextSplitter로 잘라본다. 섹션이 몇 개 나오고, 그중 제일 긴 것이 몇 자인가. - 03
[도전] 제목 + 크기 두 단계를 붙인다
ai-course/139_미션도전.py. 제목으로 나눈 뒤 긴 섹션만split_documents로 다시 자른다. 섹션 이름이 최종 조각까지 따라오는지 확인한다.
[필수] keep_separator 를 빼면충분히 고민해본 뒤 꼭 필요한 경우에만 열어보세요
설정 한 줄만 지우면 된다. 눈으로 보는 것이 이 문제의 전부다.
splitter = RecursiveCharacterTextSplitter(
chunk_size=60,
chunk_overlap=0,
separators=["\n\n", "\n", ". ", " ", ""],
# keep_separator="end", <- 이 줄만 지운다
)
for c in splitter.split_text(text):
print(repr(c[:20])) # repr 로 찍으면 앞의 공백과 점이 눈에 들어온다'. 사용자 눈에 보이지 않는 곳에서 ' 처럼 점으로 시작하는 조각이 보인다. `repr` 로 찍는 게 요령이다 — 그냥 print 하면 앞의 점을 그러려니 하고 넘긴다.
조각 16개에 1위 6/6, 평균 1.00. 점수가 하나도 안 변한다. 그래서 위험하다 — 지표가 안 잡아주니 눈으로 안 보면 그대로 지나간다.
문서가 늘고 조각이 수만 개가 되면 이런 것들이 쌓인다. 하나하나는 미미하고, 합치면 원인을 못 찾는 품질 저하가 된다.
[응용] 내 마크다운 자르기충분히 고민해본 뒤 꼭 필요한 경우에만 열어보세요
5강에서 배운 파일 읽기를 그대로 쓴다. 인코딩을 빼먹지 않는다.
from pathlib import Path
from langchain_text_splitters import MarkdownHeaderTextSplitter
md = Path("docs/README.md").read_text(encoding="utf-8")
sections = MarkdownHeaderTextSplitter(
headers_to_split_on=[("#", "문서"), ("##", "섹션"), ("###", "소제목")]
).split_text(md)
print(f"섹션 {len(sections)}개")
for d in sorted(sections, key=lambda d: -len(d.page_content))[:3]:
print(f'{len(d.page_content):5d}자 {d.metadata}')제일 긴 섹션 세 개만 보는 게 요령이다. 그것들만 다시 자르면 되기 때문이다.
제목 단계를 ### 까지 넣을지는 문서마다 다르다. 소제목이 많은 문서면 넣는 게 낫고, 소제목이 한두 줄짜리면 조각이 너무 잘게 난다.
섹션이 1개만 나왔다면 그 문서에 ## 가 없는 것이다. 모든 마크다운이 제목을 잘 쓰는 건 아니다 — 그래서 크기 자르기가 뒤에 또 필요하다.
[도전] 두 단계로 붙이기충분히 고민해본 뒤 꼭 필요한 경우에만 열어보세요
split_text 가 아니라 `split_documents` 를 쓰는 것이 핵심이다. 이름이 비슷해서 헷갈리기 쉽다.
split_text 는 문자열을 받으니 metadata 를 받을 자리가 없다. split_documents 는 Document 를 받아 정보를 그대로 물려준다.
sections = MarkdownHeaderTextSplitter(
headers_to_split_on=[("#", "문서"), ("##", "섹션")]
).split_text(md)
sizer = RecursiveCharacterTextSplitter(
chunk_size=60,
chunk_overlap=0,
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, "|", d.page_content[:26])조각마다 섹션 이름이 그대로 붙어 있어야 한다. 안 붙어 있으면 split_text 를 쓴 것이다.
짧은 섹션은 조각 수가 안 늘어난다. 「섹션 4개 → 조각 6개」처럼 긴 섹션만 쪼개진 결과가 나온다.
여기에 5절의 꼬리 붙이기까지 얹으면 완성이다. 다만 Document 목록이라 out[-1].page_content 를 고쳐야 한다 — 딕셔너리를 다룰 때와 점 하나 차이다.
정리하면
손으로 만든 자르기를 라이브러리로 갈아끼웠다. 기본값 그대로는 우리 것보다 나빴고(5/6), 구분자와 keep_separator 를 맞춰주고 나서야 같아졌다(6/6).
검색 품질은 안 변했다. 그런데도 갈아끼울 값어치가 있는 이유는 딸려 오는 것들 때문이다 — 원본 위치를 기록하는 add_start_index, 마크다운 제목으로 나누는 전용 도구, 그리고 앞으로 만날 형식들.
기억할 것 두 가지다. 가져다 쓴다고 저절로 좋아지지 않는다. 그리고 직접 만들어 본 사람만 설정값의 뜻을 안다.
이제 자르기는 됐고 점수도 됐다. 남은 문제는 12강 도전 문제에서 이미 만난 그것이다 — 질문 하나에 조각을 전부 다시 인코딩하고 있다. 한 번 만든 벡터를 어디에 남겨둘 것인가.