논문 chunking 이후 FTS/BM25 호출 레이어 만들기

1. 이번 작업의 출발점

이전 단계에서 논문 본문을 chunk로 나누는 규칙은 대락 정했다.

https://www.gpters.org/nocode/post/llm-wiki-thesis-chunking-YjhsBFj0tSRZCtj

이제 다음 질문이 남았다.

“이 chunk들을 실제로 검색해보면, 원하는 논문과 근거가 잘 나오나?”

그래서 이 글의 중심은 FTS/BM25로 잡는다.

여기서 FTS와 BM25는 역할이 다르다.

FTS(Full-Text Search)는 문서를 단어 단위로 쪼개서, “query에 포함된 단어가 어떤 chunk에 있는지” 빠르게 찾는 검색 인덱스다. 기본적으로 단어가 실제로 일치해야 잘 찾기 때문에, query 표현을 비교적 정확하게 넣어야 한다. 즉, FTS는 정확한 키워드 기반 검색을 빠르게 해주는 구조다.

BM25는 FTS로 찾은 후보 chunk들을 관련도 순으로 정렬하는 점수 계산 방식이다. 드문 검색어가, 적당한 길이의 chunk 안에, 충분히 반복해서 나오면 높은 관련도로 본다. 즉, BM25는 키워드 검색 결과의 순위를 정하는 방식이다.

이 글에서 다루는 것은 “FTS로 후보를 찾고, BM25로 순위를 매기는 원문 검색층”을 어떻게 다듬었는지다.


2. 추출이 덜 된 논문은 검색 인덱스에서 뺐다

29개 논문을 클로드 코드가 만들어준 파이썬 스크립트로 batch로 처리하자 처음에는 520개의 chunk가 나왔다. 그런데 그중 일부는 검색 인덱스에 그대로 넣기 어려웠다. md 추출 스킬이 아직 부족하여 섹션만 뽑히고 본문이 없는 논문 등이 있었다.

이런 자료가 인덱스에 들어가면, 논문은 존재하는 것처럼 보이지만 실제 본문 근거는 비어 있다. 검색 결과를 오염시킬 가능성이 컸다.

그래서 두 편은 batch 단계에서 skip했다.


3. SQLite FTS5로 BM25 검색층을 만들었다

여기서는 SQLite FTS5를 사용했다. 쉽게 말하면, 로컬 파일 기반 데이터베이스 안에 전문 검색 테이블을 만들고, BM25 점수로 관련 chunk를 정렬하는 방식이다.

청크 js fs 슬릿 빌드 로그 json

인덱싱은 매우 빨랐다. 505개 chunk 기준으로 거의 즉시 끝났다. 위의 사진처럼 fts.sqlite가 만들어진다.

이후 몇 가지 검색 테스트를 했는데, 대부분은 직관적으로 맞는 논문을 잘 잡았다. 표 자체도 chunk로 넣었는데, 그것도 검색에서 잘 찾아내었다.


4. 하지만 BM25에도 노이즈가 있었다

BM25가 완벽했던 것은 아니다.

가장 눈에 띈 문제는 intraoperative frozen section accuracy 검색에서 나왔다.

검색 결과 1순위가 본문에 해당하는 청크가 아니라 Funding / None.이라는 3단어짜리 메타데이터에 해당하는 chunk였다.

그런데 자세히 보면 heading은 Funding이라고 써있으나, 실제 내용물은 논문의 제목이었다. 이건 기존의 자르는 (Chunking 규칙)의 한계를 시사한다. 그런데 나는 주로 본문에서 검색을 하고 싶은거니까 일단은 Chunking 규칙은 그대로 두기로 했다.

{
  "chunk_id": "gokulu2024AccuracyIntraoperativeFrozen::body::funding::0006",
  "doc_id": "gokulu2024AccuracyIntraoperativeFrozen",
  "heading": "Funding",
  "section_type": "meta",
  "word_count": 3,
  "text": "## Funding\n\nNone.",
  "title": "Accuracy of intraoperative frozen section in surgical staging of endometrial cancer"
}

같은 SQLite DB에서 intraoperative frozen section accuracy를 검색하면 top-5도 이렇게 나왔다.

rank

heading

section

words

비고

1

Funding

meta

3

문제 chunk

2

Data availability statement

meta

13

표지 메타

3

Table 3

results

160

실제 결과 표

4

Ethics committee approval

meta

24

표지 메타

5

Declaration of competing interest

meta

31

표지 메타

즉 상위권에 메타에 해당되는 chunk들이 많이 노출되고 있었다.

  • Funding

  • Data availability statement

  • Ethics committee approval

  • Declaration of competing interest

이건 BM25 자체의 실패라기보다, 검색 가능한 chunk 안에 “본문 근거가 아닌 메타데이터”가 섞여 들어간 문제였다.

그래서 단순히 점수 가중치를 만지는 대신, chunk 생성 단계에서 noise heading을 drop하기로 했다.


5. heading 기준으로 걸러보기

메타 정보는 검색 시 노이즈가 될 수 있으므로, 다음과 같은 경우를 노이즈로 인식하도록 했다.

추가한 규칙은 다음과 같은 heading을 noise로 본다.

  • Funding

  • ORCID iDs

  • Correspondence to

  • Data availability statement

  • Declaration of competing interest

  • CRediT authorship contribution statement

  • Acknowledgement / Ethics statement 계열

즉 위와 같은 양식의 chunk는 잘라내고 db에서 검색할수 있도록 요청했다.

위 규칙에 따라 DB 갱신 후 결과는 다음과 같다.

  • 505 chunks → 479 chunks

  • drop: 26 chunks, 약 5.1%

  • 26 docs 유지

특히 문제 검색어였던 intraoperative frozen section accuracy는 이렇게 회복됐다.

상태

top-1 결과

before

Funding, meta, 3w

after

Table 3, results, 160w

즉, 검색 결과 1위가 Funding / None.에서 실제 frozen section accuracy 결과 표로 바뀌었다.

사실 .md 파일 추출 품질을 좋게 하려고 애써봤지만 잘 되지 않았다. 하지만 이렇게 chunking 단계에서도 어느정도 노이즈를 제거할 수도 있겠단 생각이 들었다. 대신 논문들이 몇개 더 추가되면서 저러한 메타데이터 패턴은 다양해질 수 있으니 역시 가급적이면 수동으로라도 원하지 않는 정보나 메타데이터는 지워야겠단 생각이 들었다.


6. 검색 품질은 section filter로 한 번 더 조정했다

논문 검색에서 항상 모든 section이 같은 가치를 갖지는 않을 수 있다.

논문에서 사용한 방법론과 결과를 찾고 싶은 질문에서는 Methods와 Results가 중요하다. 반대로 Discussion은 해석과 인용이 섞여 있어, 어떤 질의에서는 오히려 노이즈가 될 수 있다.

그래서 검색 CLI에 section filter를 추가했다.

  • --section methods,results

  • --exclude-section discussion,intro,meta

필터 추가전 모드에서는 abstract나 discussion이 검색 결과 상단에 섞였다.

하지만 --section methods,results로 제한하자 Methods/Results 중심 결과가 올라왔다.

모드

top-1

평소 검색

liu2019 > ABSTRACT

--section methods,results

noriega > Material and methods

--exclude-section discussion,intro,meta

methods/results 중심 결과


7. chunk 크기와 FTS 동작도 확인했다

이 과정에서 chunk 크기 분포도 다시 봤다. 원래 목표는 대략 500–800 words였고, 1200 words를 hard max처럼 두고 있었다.

이 단어수 기준은 “정답”이라기보다, 논문 몇 편을 먼저 굴려보며 잡은 초기 운영값에 가깝다. 실제로 어느 정도 크기로 chunking해야 더 잘 찾히는지는 아직 결론내리지 않았다. 앞으로 검색을 계속 써보면서 조정해야 할 부분이다.

479개 chunk의 분포는 이랬다.

구간

개수

비율

<500w

398

83%

500–800w

46

10%

800–1200w

29

6%

1200–2000w

5

1%

≥2000w

1

0.2%


8. 이번 사례에서 남은 아키텍처 그림

이번 글 기준의 검색 구조는 이렇게 정리된다.

원문 논문 source_layers
        │
        ▼
chunk_l0.py
        │
        ├─ heading 기반 noise meta drop
        └─ section_type 부여
        │
        ▼
479 source chunks ──▶ SQLite FTS5 / BM25
                             │
                             ├─ 기본 키워드 검색
                             ├─ methods/results section filter
                             └─ 원문 근거 회수


9. 남은 한계

  • 아직 어떤 chunk 크기와 분할 단위가 최적인지는 결론내리지 않았다.

  • 의미 검색과 hybrid retrieval은 별도 단계에서 평가해야 한다. 임베딩도 시도중인데, OpenViking 돌려보는 단계에서 VLM을 Gemma4, qwen3.5 등 ollama 로컬 모델을 돌려보는데 둘다 지금 사양에서는 무리인가보다 ㅠㅠ 일단 다음 단계에는 임베딩만 먼저 얼른 시도해보는것으로.

1
1개의 답글
밀어주고 끌어주는

온·오프라인 AI 스터디

AI로 어디까지 할 수 있는지
직접 확인하실 분만 신청하세요.