[개발기] 텍스트 검색이랑 벡터 검색, 둘 다 쓰면 안 되나? — 하이브리드 검색 편

182.221.***.***
14

이 글은 seCall — AI 에이전트 대화 세션을 수집·검색하는 CLI 도구 — 의 기능 개발기 시리즈 세 번째 편입니다. 바이브코딩 테스트 프로젝트의 일환으로 만들었으니 세부적인 코드는 모르고 알고 싶지도 않습니다. 몰라도 됩니다. 알면 다칩니다. 알고 싶으면 만든 분(?)께 물어보면 됩니다. 다만 시행착오 끝에 적용한 기능에 사용된 기술과 도입 이유를 풀어보고 싶어서 쓰는 글입니다.

  • 1편: SQLite FTS5 + 한국어 형태소 분석

  • 2편: 로컬 벡터 검색

지금까지의 상황

두 가지 검색을 만들었습니다.

  • BM25 (텍스트 검색): "모델"이라고 치면 "모델"이 들어간 대화를 찾는다. 정확하지만, "아키텍처"로는 "설계"를 못 찾는다.

  • 벡터 (의미 검색): "아키텍처"로 검색하면 "설계" 관련 대화도 찾는다. 똑똑하지만, 정확한 키워드 매칭에서는 오히려 BM25보다 못할 때가 있다.

쓰다 보면 이런 일이 생깁니다. "BM25 인덱싱"을 검색하면 — BM25는 정확히 그 단어가 나온 대화를 콕 찝어주는데, 벡터는 "검색 최적화" "인덱스 구조" 같은 비슷한 맥락의 대화를 넓게 가져옵니다. 반대로 "검색이 왜 느리지"처럼 모호하게 검색하면 BM25는 거의 못 잡는데 벡터가 관련 대화를 잘 찾아줍니다.

둘 다 장단이 있으니, 자연스러운 결론은 하나입니다: 둘 다 쓰면 안 되나?

하이브리드 검색이란

말 그대로 텍스트 검색과 벡터 검색을 동시에 돌려서 결과를 합치는 것입니다. 개념은 단순한데, 문제는 어떻게 합치느냐입니다.

BM25는 점수가 025 같은 범위로 나오고, 벡터는 01 사이의 코사인 유사도로 나옵니다. 스케일이 전혀 다릅니다. BM25에서 15점이면 높은 건지, 벡터에서 0.7이면 높은 건지, 단순 비교가 안 됩니다.

점수를 정규화해서 합칠 수도 있지만, 정규화 기준을 어떻게 잡느냐가 또 문제가 됩니다. 결과 분포가 매번 달라지니까요.

RRF — 점수 대신 순위를 합친다

여기서 나온 게 **RRF(Reciprocal Rank Fusion)**라는 알고리즘입니다. 이름은 거창한데 아이디어는 놀랍도록 단순합니다. (그리고 제가 만든게 아닙니다. 믿어주세요) 재밌는 점은 알고리즘 자체는 의외로 단순한데 ML/AI가 뜨면서 업계 표준처럼 되어버린 비운(?)의 알고리즘입니다.

점수는 무시하고, 각 검색에서 몇 등인지(순위)만 보자.

BM25에서 3등, 벡터에서 7등인 문서가 있으면 — 3등이라는 사실과 7등이라는 사실만 가지고 종합 점수를 매깁니다. 원래 점수가 뭐였든 상관없습니다.

공식도 간단합니다:

RRF 점수 = 1/(k + BM25순위) + 1/(k + 벡터순위)

k는 상수인데, seCall에서는 60을 씁니다. 이 k가 하는 역할은 — 값이 크면 순위 차이에 둔감해지고(골고루 반영), 작으면 상위 순위에 확 몰아주는 방식입니다. 60이 실무에서 많이 쓰이는 기본값이라고 하길래 그냥 썼습니다(까잇거 바이브입니다!). 솔직히 이 숫자를 튜닝할 정도의 데이터량은 아니기도 하고요.(제발)

예를 들어:

문서

BM25 순위

벡터 순위

RRF 점수

대화A

1등

5등

1/61 + 1/65 = 0.032

대화B

10등

2등

70 + 1/62 = 0.030

대화C

3등

3등

1/63 + 1/63 = 0.032

대화 A는 BM25에서 1등이라 높고, 대화 C는 양쪽 다 3등이라 높습니다. 어느 한쪽에서만 잘 나와도, 양쪽에서 적당히 나와도 상위에 올라올 수 있습니다. 한쪽 검색에서 완전히 빠진 결과도 다른 쪽에서 상위라면 살아남습니다.

이게 RRF의 매력입니다 — 점수 스케일을 맞추는 고민 없이 순위만으로 깔끔하게 합쳐집니다.(다시 한번 제가 만든게 아닙니다. 믿음을 가지세요)

성능 최적화: 전부 다 검색하지 않는다

하이브리드 검색에서 또 하나 신경 쓴 부분이 있습니다. 벡터 검색은 기본적으로 모든 벡터를 대상으로 돌아갑니다. 대화가 수천 개고 청크가 수만 개면, 매번 전체를 훑는 건 낭비입니다.

seCall에서는 이런 순서로 돌립니다:

  1. BM25를 먼저 돌린다 — 빠르니까. 후보를 넉넉하게(요청 개수의 3배!,바이브!) 뽑는다

  2. BM25가 찾은 세션 ID 범위 안에서만 벡터 검색을 돌린다

  3. 두 결과를 RRF로 합친다

BM25가 일종의 필터 역할을 해주는 겁니다. "최소한 텍스트적으로 어느 정도 관련 있는 범위"에서만 벡터를 돌리니까 불필요한 비교가 확 줄어듭니다. BM25에서 아예 결과가 없으면 그때는 벡터 전체 검색으로 넘어갑니다.

이 구조 덕분에 대화 600개 기준으로 하이브리드 검색이 체감상 벡터 단독 검색보다 오히려 빠릅니다.(진짜요)

실제로 차이가 나나?

솔직히 대화 수백 개 수준에서는 BM25만으로도 충분할 때가 많습니다. 하이브리드가 빛을 발하는 건 이런 경우입니다:

  • "그거 뭐였더라" 같은 모호한 검색 — BM25는 키워드가 없으니 헤매는데 벡터가 맥락으로 잡아줌

  • 정확한 에러 메시지 검색 — 벡터는 비슷한 에러를 다 가져오지만 BM25가 정확히 그 메시지를 콕 찝어줌

  • 한영 혼합 검색 — "설계"로 검색할 때 "architecture" 대화도 같이 나옴

어느 한쪽만으로는 놓치는 결과가 있고, 합치면 그게 잡힙니다. 극적인 차이는 아닐 수 있는데, 한번 맛보면 돌아가기 싫어집니다.

그래서 합치니까 진짜 나아졌나?

솔직히 학술적인 벤치마크를 돌린 건 아닙니다.(돌릴 지능도 능력도 없습니다 😇) precision, recall 같은 지표를 재려면 정답 데이터셋이 있어야 하는데, 개인 대화에 정답이란게 없으니까요. 그래도 검증과정은 있어야 하니 대충 같은 검색어를 세 가지 모드로 돌려서 비교해봤습니다.

디버깅 차원에서 --lex-only(BM25만), --vec-only(벡터만), 기본(하이브리드) 세 가지 검색 모드를 만들었습니다. 검색 결과에는 BM25 점수와 벡터 점수가 따로 찍히기 때문에 어느 쪽이 기여했는지 눈으로 볼 수 있습니다.(이거 할려고 만들었던건 아니었습니다만...)

몇 가지 패턴이 있습니다:

  • "BM25 인덱싱" 같은 정확한 키워드 검색 BM25: 정확히 그 단어가 나온 대화 5개를 콕 찝어줌 X 벡터: "검색 최적화", "인덱스 구조" 같은 주변 대화까지 넓게 15개 가져옴. 관련은 있는데 원하는 건 아닌 것도 섞임 하이브리드: BM25가 찾은 정확한 5개가 상위, 벡터가 찾은 관련 대화가 그 아래. 정확한 게 먼저 나오면서 범위도 넓어짐

  • "그거 왜 안 됐더라" 같은 모호한 검색 BM25: "안", "됐" 같은 토큰이 너무 흔해서 관련 없는 대화가 올라옴 X 벡터: 에러/실패 맥락의 대화를 잘 잡아줌 OK 하이브리드: 벡터 결과가 주도하되, 혹시 BM25에서 정확히 걸린 게 있으면 올라옴

  • "설계"로 검색했을 때 "architecture"가 나오는가 BM25: 당연히 안 나옴 X 벡터: 나옴 OK 하이브리드: 나옴. "설계"가 정확히 쓰인 대화가 먼저, "architecture" 대화가 그 뒤

결론은 - 하이브리드가 어느 한쪽보다 못한 경우는 없었습니다. 최악의 경우에도 둘 중 잘하는 쪽의 결과가 올라오니까요. RRF의 "순위 합산"이 이런 안전망 역할을 잘 해주는 걸로 보입니다. 한쪽이 헛발질을 해도 다른 쪽이 보정해주는 구조이니까요. 물론 600개 정도의 대화에서 감으로 비교한 거라 학술적 검증과는 거리가 있습니다. 다만 개인 도구에서는 "내가 쓸 때 원하는 게 잘 나오는가"가 사실상 유일한 지표이고, 그 기준에서는 하이브리드 방식이 현재로서는 최선이라고 봅니다(제발).

정리

기술

역할

선택 이유

하이브리드 검색

BM25 + 벡터 동시 실행

텍스트 정확도 + 의미 유사도 둘 다 잡기

RRF (k=60)

결과 합치기 알고리즘

점수 스케일 무시, 순위만으로 합산 — 구현 간단

BM25-first 파이프라인

벡터 검색 범위 제한

불필요한 벡터 비교 줄여 속도 최적화

돌이켜보면 #1에서 FTS5 + 형태소 분석, #2에서 벡터 검색을 각각 만들어놓고, #3에서 RRF로 합친 건 — 계획이 있었다기보다 각각 부족한 부분을 메꾸려고 고민하다보니 보니 자연스럽게 이 구조가 됐습니다.(친구는 없고 AI랑만 계속 얘기하다보니 말투도 자연스럽게 이렇게 되어버렸습니다.🫢) 바이브코딩이 원래 그렇습니다. 계획대로 되는 게 아니라 삽질하다 보면 어느새 그럴듯한 구조가 나옵니다. 다만 검증은 늘 필요합니다. (이 과정에서 워크플로우는 아이디어 - 구현 - 검증 - 보강 - 검증 - 보강 - 식사 - 검증 - 보강 - 으악!... 지옥의 무한루프입니다)

다음 편에서는 이렇게 수집·검색한 대화를 Obsidian Vault에 마크다운 위키로 정리하는 이야기로 시리즈(?)를 마무리 해보겠습니다. 검색도 좋지만, 여유 시간에 대화나 위키 형태로 정리된 문서를 쭉 둘러보면서 복기하는 것도 나름의 재미가 있더라구요.


본 포스트는 안(no)바이브 작성입니다. 안심하고 읽으셔도 됩니다 😁 맨땅에 헤딩하는게 에이전틱 개발이 아닙니다. 다양한 경험을 통해 쌓아올린 인간지능의 도메인지식과 견고한 마구를 씌운 인공지능의 지치지 않는 정확한 작업의 이인삼각, 대환장콜라보입니다.

로그인한 회원만 댓글 등록이 가능합니다.

개발한당

KR | ID | EN
  • IDR
  • KOR
8.34 =0.00

2026.07.11 KEB 하나은행 고시회차 1694회

다가오는 한인 행사일정

  • 등록 된 일정이 없어요!