개요

이 글은 수천 개의 로컬 Excel 파일을 파일명이나 시트명뿐 아니라 셀 안의 텍스트로 찾기 위해, 원본 파일은 읽기 전용으로 보존하고 별도 SQLite 색인 DB에 FTS5와 MCP 검색 도구를 결합한 사례를 설명한다. 기존 XLSX 색인은 시트 이름만 읽어 본문 검색이 되지 않았고, 셀 텍스트를 색인한 뒤 표본 리콜@10이 11.5%에서 98.5%로 개선됐다.

문제 진단

  • DOCX·PPTX·PDF는 본문 추출률이 대체로 정상인데 XLSX는 27%, XLSM은 7%에 그쳤다.
  • 기존 추출기는 ZIP 컨테이너 내부의 workbook.xml에서 시트 이름만 읽고 셀 내용이 저장된 sharedStrings.xml을 읽지 않았다.
  • 목표가 “파일 찾기”에서 “파일 안 내용 찾기”로 바뀌었는데 색인기가 이전 전제를 유지한 것이 근본 원인이었다.

설계: 전체는 얕게, 후보만 깊게

1단계는 전체 Excel 파일의 후보 검색용 색인이다. sharedStrings.xml의 텍스트를 최대 16,000자까지 fts.body에 넣고, 시트별 행·열 수를 카드(JSON)에 저장한다. 이 단계는 전체 파일을 수십 ms 수준으로 처리하며 약 6천 개 파일에 추가되는 본문이 약 13MB라고 설명한다.

2단계는 검색 결과 상위 3~5개 후보만 원본 Excel에서 정밀하게 읽는다. 행·열 수를 근거로 작은 시트는 전체를 읽고, 4만 행과 같은 대용량 시트는 헤더와 앞부분만 읽어 비용과 컨텍스트 폭증을 막는다. 숫자 셀 값, 전체 집계, 의미 기반 유사어 검색은 이 2계층 FTS만으로 해결하지 않고 각각 원본 읽기·데이터 파이프라인·임베딩의 영역으로 남긴다.

저장 및 검색 도구

원본 Excel을 수정하지 않고 index.db에만 색인을 저장한다. 파일 크기·수정일은 files 컬럼, 시트 목록과 행·열 수는 card JSON, 검색 대상 텍스트는 fts.body에 둔다.

MCP 검색 계층의 핵심 도구는 다음과 같다.

  • search_docs: 경로·결과 수·폴더 필터를 지원하는 문서 검색
  • vocab_lookup: 검색어가 색인에 실제로 몇 번 등장하는지 확인하는 어휘 조회
  • 검색 결과가 0건이면 색인에 존재하는 유사어를 제안해 LLM이 예상매출 대신 예측 OR 예상처럼 재검색하도록 한다.

성능 결과와 운영 교훈

글에서 제시한 표본 150개 파일·질문 200개 평가에서는 셀 내용 색인 전 23/200건(11.5%)에서 색인 후 197/200건(98.5%)으로 리콜@10이 개선됐다. 프로덕션에서는 6,414개 Excel을 재색인해 빈약한 본문 파일을 4,752개에서 1,494개로 줄였고, 추가 본문은 약 85MB, 소요 시간은 약 12분이었다. 이 수치는 글 작성자의 자체 측정 결과이며 독립 재현 결과는 아니다.

운영 중 두 가지 실패도 기록한다. FTS5 경로 컬럼을 통한 파일별 삭제가 전체 스캔을 유발해 6,414개 재색인이 21시간으로 늘어날 뻔했으며, 내부 행번호 지도를 한 번 만들어 인덱스 기반 삭제로 바꾸자 36초로 줄었다. 또 기존에 더 긴 본문이 있던 파일을 새 16,000자 추출기로 덮어쓰면서 정보가 손실될 수 있어, 새 본문이 기존보다 길 때만 교체하는 규칙과 대량 갱신 전 DB 백업이 필요했다.

적용 시사점

검색 시스템은 색인·검색 도구·LLM의 세 층으로 설계해야 한다. 색인은 후보를 놓치지 않게 하고, 검색 도구는 0건을 다음 검색어의 힌트로 바꾸며, LLM은 후보만 원본에서 깊게 읽는다. 이 구조는 문서가 많고 파일명이 부정확한 개인 아카이브에 특히 적합하다. 다만 한국어 FTS 토크나이징, 숫자 셀 검색, 동의어·의미 검색은 별도 검증이 필요하다.

관련 노트

출처 및 검증

  • 원문: https://dbhyeong.github.io/blog/excel-files-searchable-db-llm-fts
  • 원문 캡처: raw/articles/2026-07-23-excel-files-searchable-db-llm-fts.md
  • 수집 경로: agent-reach 웹 리더(Jina Reader)
  • 원문 캡처 SHA-256: d8fd380a5643d8b8d23860ff4769f5b6aa81bbb6ca87fa0ef881f7fe41da73c7 (frontmatter를 제외한 정확한 raw body 기준)
  • 글의 성능 수치는 작성자가 제시한 자체 측정값이며, 별도 벤치마크로 재현하지 않았다.