본문 바로가기

프로젝트/Techfork

하이브리드 검색 구현 구조 정리 — BM25 + kNN + RRF + 개인화 리랭킹

들어가며

TechFork의 검색은 단순한 키워드 매칭이 아니라, BM25 기반 lexical search와 embedding 기반 semantic search를 결합한 하이브리드 구조로 동작합니다. 두 검색 결과를 RRF(Reciprocal Rank Fusion)로 결합하고, 로그인 사용자에게는 프로필 기반 개인화 리랭킹까지 적용합니다.

 

이 글에서는 검색 파이프라인의 각 단계와 후처리까지의 전체 흐름을 정리합니다.

현재 검색 설정이 어떤 실험 과정을 거쳐 결정되었는지는 검색 품질을 5단계 실험으로 개선한 과정 글을 참고해주세요.

 

 

검색 품질을 5단계 실험으로 개선한 과정 — 필드 가중치부터 쿼리 구조까지

들어가며TechFork는 기술 블로그 글을 수집해서 검색과 추천을 제공하는 서비스입니다. 검색은 BM25 기반 lexical search와 embedding 기반 semantic search를 결합한 하이브리드 구조로 동작하는데, 이 구조에

dmoritle.tistory.com

 

검색 성능 최적화(병렬 실행, 스레드 풀 튜닝, 인덱스 준비 등)는 검색 성능 최적화 과정 글에서 다룹니다.

검색 API

검색 API는 /api/v1/search이며, 인증 여부에 따라 동작이 달라집니다. 인증 정보가 없으면 일반 하이브리드 검색을 수행하고, 인증된 사용자이면 하이브리드 검색 결과에 개인화 리랭킹을 추가로 적용합니다.

 


일반 검색 흐름

일반 검색은 lexical search와 semantic search를 비동기로 병렬 실행한 뒤, 두 결과를 RRF로 결합해 최종 ranking을 만듭니다.

 

1단계: BM25 기반 Lexical Search

BM25 검색은 title, summary, 그리고 설정값이 0보다 크면 contentChunks.chunkText 필드를 대상으로 합니다. 각 필드에 대해 exact 매칭과 fuzzy 매칭을 함께 사용하며, 이 둘은 dis_max 구조로 묶여 있습니다.

 

dis_max는 여러 매칭 중 가장 높은 점수를 기본값으로 사용하고, 나머지 매칭의 기여를 tieBreaker로 제어합니다. 이 구조를 선택한 이유는 bool should에서 발생하는 점수 누적 문제 때문입니다. title과 summary가 exact/fuzzy 양쪽에 동시에 매칭되면 점수가 과도하게 합산되어, 한 필드에만 잘 맞는 문서가 여러 필드에 고르게 매칭되는 문서보다 낮게 평가되는 불균형이 생길 수 있습니다. dis_max는 이 문제를 구조적으로 해결합니다.

 

현재 설정값은 다음과 같습니다.

항목
titleBoost 0.15
summaryBoost 0.70
bm25ChunkBoost 0.15
exactBoost 2.0
fuzzyBoost 1.0
tieBreaker 0.3

Summary에 가중치를 집중한 이유는 5단계 평가 실험에서 Summary 중심 구성이 nDCG@4, nDCG@8에서 가장 높은 랭킹 품질을 보였기 때문입니다. 다만 chunk를 완전히 제거하지 않은 것은, 기술 블로그 특성상 코드 스니펫이나 에러 메시지처럼 본문에만 존재하는 검색 대상이 있기 때문에, chunk를 안전장치로 유지하는 것이 더 안정적이라고 판단했기 때문입니다.

 

2단계: Vector 기반 Semantic Search

검색어를 embedding 벡터로 변환한 뒤, Elasticsearch의 kNN 검색을 실행합니다. 검색 대상 필드는 titleEmbeddingsummaryEmbedding이며, 각 필드에 boost를 적용합니다.

 

kNN 검색에서는 k(반환 문서 수)와 num_candidates(탐색 후보 수)로 탐색 범위를 조절합니다. 현재 설정은 k=20, num_candidates=60으로, 평가 실험에서 이 조합이 품질과 latency 모두에서 가장 유리한 결과를 보였습니다.

 

vectorChunkBoost는 현재 0으로 설정되어 있어, semantic search는 title/summary embedding만을 대상으로 동작합니다. 품질 평가 실험에서 vector chunk를 제외해도 품질 차이가 없음을 확인했고, 이후 부하 테스트에서 TPS 개선 효과까지 확인한 뒤 제거한 결정입니다. 자세한 내용은 검색 성능 최적화 글에서 다룹니다.

 

3단계: RRF 결합

BM25 검색 결과와 semantic 검색 결과는 서로 다른 점수 체계를 사용합니다. BM25 점수는 이론적으로 상한이 없고, cosine similarity는 -1에서 1 사이의 값입니다. 이 두 점수를 직접 가중 합산하려면 정규화가 필요한데, 정규화 방식에 따라 결과가 왜곡될 수 있습니다.

 

RRF(Reciprocal Rank Fusion)는 이 문제를 순위 기반 결합으로 우회합니다. 각 검색 결과에서의 순위만을 사용해 통합 점수를 계산하므로, 점수 스케일 차이를 신경 쓸 필요가 없습니다. 현재 구현에서는 RRF 상수 k=60을 사용합니다.

RRF_score(d) = Σ  1 / (k + rank_i(d))

여기서 rank_i(d)는 i번째 검색 방식에서 문서 d의 순위이고, k는 순위 간 차이를 완화하는 상수입니다. 이 방식의 장점은 두 검색 방식의 점수를 직접 비교하지 않고도 의미 있는 통합 순위를 만들어낼 수 있다는 점입니다.

 


개인화 검색 흐름

로그인 사용자의 경우, 일반 하이브리드 검색으로 후보군을 만든 뒤 개인화 리랭킹을 추가로 적용합니다.

사용자 프로필

TechFork에서는 사용자의 읽기 이력과 스크랩 이력을 기반으로 LLM(GPT-4o-mini)이 사용자의 관심 분야를 분석하고, 이를 벡터로 변환해 user_profiles 인덱스에 저장합니다. 이 프로필 벡터가 개인화 리랭킹의 기준이 됩니다.

리랭킹 과정

리랭킹에서는 사용자 프로필 벡터와 각 검색 결과 문서의 title 벡터, summary 벡터 사이의 cosine similarity를 계산합니다. title 유사도와 summary 유사도를 가중 합산해 personalScore를 만들고, 이를 기존 RRF 기반 hybrid score와 다시 결합해 최종 점수를 산출합니다.

사용자 프로필이 아직 생성되지 않은 경우(신규 사용자 등)에는 개인화를 생략하고 일반 검색 결과를 그대로 반환합니다.

 


후처리

검색 결과가 정렬된 뒤에는 Elasticsearch에서 가져온 문서 정보에 추가 메타데이터를 붙입니다.

MySQL에서 각 게시글의 조회수와, 로그인 사용자의 경우 스크랩 여부를 조회해 응답에 포함합니다. 또한 내부 계산에 사용한 title/summary 벡터는 API 응답 전에 제거해 외부로 노출하지 않습니다.

 


전체 흐름 요약


현재 검색 설정 전체

항목 비고
searchSize 20 한 번의 검색에서 반환하는 결과 수
titleBoost 0.15 Phase 2 실험에서 결정
summaryBoost 0.70 Phase 2 실험에서 결정
bm25ChunkBoost 0.15 Phase 2~3 실험에서 결정
vectorChunkBoost 0.00 Phase 3 실험 + 부하 테스트
exactBoost 2.0 Phase 5 실험에서 결정
fuzzyBoost 1.0 Phase 5 실험에서 결정
tieBreaker 0.3 Phase 5 실험에서 결정
knnK 20 Phase 4 실험에서 결정
knnNumCandidates 60 Phase 4 실험에서 결정
RRF k 60 고정값
쿼리 구조 dis_max Phase 5 실험에서 결정

 


정리

TechFork의 검색은 다음과 같이 이해할 수 있습니다.

 

첫째, 검색 요청이 들어오면 BM25 검색과 kNN 검색을 병렬로 실행합니다. BM25는 dis_max 구조로 점수 누적 왜곡을 방지하고, kNN은 title/summary embedding을 대상으로 합니다.

 

둘째, 두 검색 결과를 RRF로 결합합니다. 점수 스케일이 다른 두 결과를 순위 기반으로 통합하기 때문에, 별도의 점수 정규화 없이 의미 있는 랭킹을 만들 수 있습니다.

 

셋째, 로그인 사용자에게는 프로필 벡터 기반의 개인화 리랭킹을 추가로 적용합니다. 비로그인 사용자에게는 일반 하이브리드 검색 결과를 그대로 반환합니다.

 

이 구조의 핵심은 각 단계가 독립적으로 동작하면서도 순차적으로 결합된다는 점입니다. lexical retrieval, semantic retrieval, rank fusion, personalized reranking이 파이프라인으로 연결되어 있기 때문에, 특정 단계만 개선하거나 교체해도 전체 흐름에 영향을 최소화할 수 있습니다.