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 조건에서 다르게 나왔다.
- 도구/기술: SQLite
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.