Post

SQLite 하나로 하이브리드 검색을 해보자

FTS5(BM25) 키워드 검색과 sqlite-vec 벡터 KNN을 RRF(k=60)로 합쳐 별도 벡터 DB 없이 하이브리드 검색을 SQLite 파일 하나에 담았다

SQLite 하나로 하이브리드 검색을 해보자

SQLite 하나로 하이브리드 검색을 해보자

개인 검색 인프라 프로젝트를 만들며 키워드 검색만으로는 의미는 비슷한데 단어가 다른 문서를 못 찾는 문제를 겪었다. FTS5와 sqlite-vec을 같은 SQLite 파일 안에 두고 RRF로 결과를 합쳐 해결했는데, 그 과정에서 한 번 기각했다가 되돌린 결정이 있었다.

1. 서론 (Introduction)

  • 문제/상황 (Problem): FTS5 BM25 키워드 검색은 정확한 단어 일치에는 강하지만, 질문과 문서가 같은 의미를 다른 단어로 표현하면 후보에서 아예 빠졌다.
  • 목적 (Purpose): 키워드 검색 결과와 벡터 KNN 결과를 RRF로 결합해, 별도 벡터 DB 없이 SQLite 하나로 하이브리드 검색을 구현한다.
  • 대상 (Target Audience): 검색 시스템에 의미 기반 검색을 얹고 싶지만 인프라를 늘리고 싶지 않은 개발자.

2. 방법 및 과정 (Methods & Process)

  • 배경 조사 및 데이터 (Data Collection): 키워드 전용·벡터 전용·하이브리드 세 방식을 비교했다.

    방식강점약점
    FTS5(BM25) 단독정확한 단어 일치, 빠름동의어·의미 유사 문서 누락
    벡터(코사인 KNN) 단독의미 유사 문서 탐색정확한 고유명사·코드 매칭에 약함
    하이브리드(RRF 결합)두 순위를 상호 보완결합 시점의 후보군 조건에 따라 결과가 달라짐

    faiss 같은 별도 벡터 DB를 쓰지 않고, sqlite_vec.load(conn)으로 sqlite-vec 확장을 SQLite 커넥션에 로드해 vec0 벡터 테이블을 FTS5 가상 테이블과 같은 파일 안에 뒀다.

  • 접근 방법 (Approach Methods):
    • [방법 1]: FTS5 가상 테이블에 문서를 색인하고 BM25 순위로 top-k 후보를 뽑는다.
    • [방법 2]: vec0 벡터 테이블에서 코사인 거리 기준 KNN으로 top-k 후보를 뽑는다.
  • 분석 및 해결 프로세스 (Analysis Flow):
    • 도구/기술: SQLite fts5 가상 테이블, sqlite-vec 확장(vec0), RRF(Reciprocal Rank Fusion, k=60).
    • 주요 단계: FTS5 색인 $\rightarrow$ 벡터 색인(vec0) $\rightarrow$ 두 순위 리스트를 RRF(k=60)로 결합 $\rightarrow$ 결합 순위로 재정렬.
    • 결과 도출 및 검증: PoC 단계(키워드 후보만 있는 상태)에서 벡터 융합을 얹었더니 recall이 27%에서 73%로 크게 올랐다. 그런데 이 실험은 나중에 production 조건에서 다르게 나왔다.

3. 결과 (Results)

  • 분석 결과 요약: 벡터 KNN을 117k개 임베딩에 대해 brute-force로 계산하면 약 2466ms가 걸려 지연 병목이었다. RRF 결합 자체는 한때 기각한 적이 있다 — PoC에서는 recall이 27%→73%로 좋아 보였지만, 실제 production 후보군(이미 동의어 확장이 적용된 상태)에 얹으니 골든 정확도가 83.9%에서 77.4%로 오히려 떨어져 한동안 title-only 방식으로 되돌렸다. 이후 벡터 인덱스 병목을 별도로 정리하고 재도입한 현재 production 하이브리드 실측치는 정확도 50.0%, 응답 2500ms다. 이 수치도 한 번은 의심해봐야 했다 — 한때 “동의어 48.4%”라는 기록이 남아있었는데, 알고 보니 동의어 사전이 실제 검색 경로(HybridRetriever/KeywordRetriever) 어디에도 연결되지 않은 애드혹 실험치였다. 정상 경로로 재측정하니 57.7%(제목 기준 38.7%)로 떨어졌고, 그 38.7%조차 벤치마크가 production 조립 경로를 그대로 타지 않고 동의어 확장 계층만 건너뛴 채 하이브리드 검색기를 직접 호출해서 나온 과소측정값이었다. 검색기 조립 로직을 서버 코드에 인라인으로만 박아두지 않고 함수 하나로 뽑아내 벤치와 서버가 항상 같은 경로를 타게 고치고 나서야 숫자를 믿을 수 있었다.

4. 인사이트 및 액션 (Insights & Action)

  • 인사이트 (Insight): PoC에서 좋았던 방법이 production 조건에서 그대로 좋으리라는 보장은 없다. PoC의 키워드 후보군은 동의어 확장이 안 된 “가난한” 후보였고, 벡터 융합이 그 빈틈을 메워 recall이 크게 뛴 것처럼 보였다. production 후보군은 이미 동의어 확장으로 빈틈이 메워져 있었으니, 같은 벡터 융합이 오히려 노이즈를 더 얹는 결과가 됐다. 개선안 자체를 의심하기 전에, 그 수치를 낸 측정 파이프라인이 정말 같은 검색 경로를 타고 있는지부터 의심해야 했다.
  • 실행 방안 (Action Plan): 새 결합 방법을 도입하기 전에는 항상 “지금 비교 대상이 되는 베이스라인이 이미 어떤 보정을 거쳤는지”부터 확인한다. 베이스라인이 이미 강화돼 있다면, 같은 개선이 반대 효과를 낼 수 있다는 전제로 다시 측정한다.
  • 한 줄 결론 (Key Takeaway): RRF 하이브리드 검색은 강력하지만, PoC 조건과 production 조건의 베이스라인 차이를 확인하지 않으면 좋아 보이던 개선이 역효과로 뒤집힐 수 있다. 샘플 코드
  • 다음 스텝 (Next Step): 벡터 KNN의 brute-force 지연(117k건 기준 ~2466ms)은 아직 근본 해결이 아니다. vec0의 근사 인덱싱 옵션이나 사전 클러스터링으로 지연을 줄이는 실험이 남아 있다.
This post is licensed under CC BY 4.0 by the author.