티스토리 뷰
Vector Search의 최신 문서 누락 문제 해결: MongoDB 8.0에서 RRF 기반 Score Fusion 구현 전략
ebson 2026. 2. 14. 08:35벡터 검색만으로는 “최신”을 찾기 어려웠던 이유
RAG 파이프라인을 운영하면서 재현 가능한 문제를 하나 확인했습니다. “최신 OpenAI 업데이트 알려줘”와 같은 질의에 대해, 시스템이 가장 최근 문서가 아닌 두 번째 최신 문서를 반환하는 현상이었습니다.
기존 파이프라인은 $vectorSearch가 코사인 유사도 기준으로 limit 수만큼 결과를 먼저 선택한 뒤, 애플리케이션 레벨에서 Recency Boost를 적용하는 구조였습니다. 문제는 최신 문서와 직전 문서의 벡터 유사도 차이가 매우 작을 경우, 유사도가 근소하게 높은 이전 문서가 limit 단계에서 선택되고 최신 문서는 후보군에서 제외된다는 점이었습니다. 이후 단계에서 아무리 시간 가중치를 적용하더라도, 이미 결과 집합에 포함되지 않은 문서를 복원할 방법은 없습니다.
MongoDB Atlas 공식 문서에 따르면 $vectorSearch는 numCandidates로 후보군을 확장한 뒤 limit으로 최종 반환 수를 제한합니다. 이후의 Aggregation stage는 이 제한된 결과에 대해서만 적용됩니다. 이 동작 특성은 설계 상 자연스러운 것이지만, “최신”처럼 시간 축을 요구하는 질의에는 구조적인 한계가 될 수 있습니다.
결국 의미적 유사도와 시간 기반 정렬을 명시적으로 결합하는 접근이 필요하다고 판단했습니다.
$rankFusion과 $scoreFusion의 역할
MongoDB는 멀티 소스 검색 결과를 결합하기 위해 $rankFusion과 $scoreFusion을 제공합니다.
$rankFusion은 Reciprocal Rank Fusion(RRF) 알고리즘을 기반으로 합니다. RRF는 2009년 SIGIR 논문에서 제안된 방식으로, 다음과 같은 공식을 사용합니다.
RRF(d) = \sum \frac{w_r}{k + rank_r(d)}
MongoDB 공식 문서와 리소스 문서에 따르면 기본 smoothing factor k는 60으로 설정됩니다. 예를 들어 순위가 1인 문서는 1/(60+1) ≈ 0.0164, 순위가 60인 문서는 1/(60+60) ≈ 0.0083의 점수를 갖습니다. 상위와 하위의 차이가 완만하게 유지되므로, 특정 소스의 1위 문서가 전체 결과를 과도하게 지배하지 않는 특성이 있습니다. 또한 RRF는 원본 점수 스케일을 사용하지 않고 순위만 활용하기 때문에, 서로 다른 종류의 검색 결과를 결합하기에 적합합니다.
$scoreFusion은 점수 기반 결합 방식입니다. MongoDB 공식 문서에 따르면 min-max 정규화나 sigmoid 정규화 등을 적용한 뒤 가중 평균으로 최종 점수를 계산할 수 있습니다. 원본 점수의 상대적 크기를 보존하면서 결합하고 싶을 때 유용합니다.
MongoDB 8.0에서 $rankFusion이 도입되었고, $scoreFusion은 8.2 이상에서 제공됩니다. 현재 8.0 환경에서는 $scoreFusion을 직접 사용할 수 없기 때문에, 동일한 효과를 파이프라인 내 계산과 애플리케이션 레벨 로직으로 재현하는 방향을 선택했습니다.
세 가지 접근 방식과 선택
문제를 해결하기 위해 세 가지 방식을 검토했습니다.
첫 번째는 벡터 검색과 최신성 직접 쿼리를 독립적으로 실행한 뒤, 애플리케이션에서 RRF로 결합하는 방식입니다. 이 접근은 $rankFusion과 동일한 알고리즘을 적용할 수 있고, 벡터 검색에서 누락된 최신 문서를 보완할 수 있습니다. 다만 벡터 검색 단계 자체에는 최신성이 반영되지 않습니다.
두 번째는 파이프라인 내부에서 점수를 결합하는 방식입니다. $vectorSearch 이후 $addFields를 사용해 시간 기반 점수를 계산하고, 벡터 점수와 가중 합산하여 combinedScore로 정렬합니다. 이는 $scoreFusion과 유사한 효과를 단일 파이프라인에서 구현하는 방식입니다. 다만 limit으로 잘린 결과 집합에 대해서만 적용 가능하다는 한계는 남습니다.
세 번째는 두 방식을 결합한 하이브리드 구조입니다. 파이프라인 내에서 의미적 유사도와 최신성을 1차 결합하고, 동시에 별도의 최신성 직접 쿼리 결과를 RRF로 다시 결합합니다. 이렇게 하면 벡터 검색 단계에서 최신성이 어느 정도 반영되고, 동시에 누락된 최신 문서를 별도 소스에서 보완할 수 있습니다.
현재 환경에서는 이 하이브리드 접근이 가장 균형 잡힌 선택이라고 판단했습니다.
Exponential Decay 기반 recencyScore 설계
시간 가중치 계산에는 Exponential Decay를 사용했습니다.
recencyScore = e^{-\lambda \times daysSincePublished}
λ는 1/365 ≈ 0.00274로 설정했습니다. 365일이 경과하면 점수는 e^-1 ≈ 0.368이 됩니다. 비교를 위해 1/(1+t/365) 형태의 Hyperbolic Decay를 적용하면 같은 시점에서 0.5가 됩니다. Exponential Decay는 최근 문서에 더 명확한 선호를 부여하는 특성이 있습니다. 7일 이내 문서는 약 0.98 이상의 점수를 유지합니다.
MongoDB 공식 문서에 따르면 $dateDiff는 5.0 이상에서 제공되며 두 날짜 간 정수 차이를 반환합니다. $exp는 4.2 이상에서 제공되며 e^x를 계산합니다. 이를 활용하여 Aggregation 파이프라인 내부에서 recencyScore를 계산했습니다. published_at이 null인 경우에는 $cond와 $ifNull을 사용해 0.5의 기본값을 부여했습니다. 이렇게 하면 발행일 정보가 없는 문서도 완전히 배제되지 않으면서, 최신 문서보다는 낮은 점수를 갖도록 할 수 있습니다.
이후 벡터 점수($meta: "vectorSearchScore")와 recencyScore를 가중 결합해 combinedScore를 만들고, 이를 기준으로 정렬합니다. 연산은 $vectorSearch의 limit 이후 소수 문서에 대해서만 수행되므로 추가 비용은 크지 않습니다.
최신성 직접 쿼리와 RRF 결합
파이프라인 내부 결합만으로는 limit에서 제외된 최신 문서를 복원할 수 없기 때문에, 별도의 최신성 직접 쿼리를 병행했습니다. 일반 find()로 published_at DESC 정렬을 수행하여 최신 문서를 소수 건 조회합니다.
두 소스의 결과는 RRF 공식에 따라 결합합니다. 각 결과 집합에서의 순위를 기반으로 1/(k + rank) 형태의 점수를 계산하고, 동일 문서가 두 소스에 존재하면 점수를 합산합니다. k는 60으로 설정했습니다. 이는 MongoDB 리소스 문서와 SIGIR 원 논문에서 설명하는 기본 설정과 동일합니다.
예를 들어 한 문서가 벡터 검색에서 2위, 직접 쿼리에서 1위라면 두 점수가 합산됩니다. 직접 쿼리의 가중치를 조정하면 최신 문서의 영향력을 상황에 따라 더 높일 수 있습니다. 순위 기반 결합이기 때문에 점수 스케일 차이에 영향을 받지 않는다는 점도 장점입니다.
쿼리 유형에 따른 가중치 전략
모든 질의가 동일한 의도를 갖는 것은 아닙니다. “성능 알려줘”와 “최신 업데이트 알려줘”는 분명히 다른 요구를 가집니다.
일반 질의에서는 의미적 유사도를 더 높은 비중으로 두고, 최신성 질의에서는 시간 가중치를 상대적으로 강화했습니다. 파이프라인 내 점수 결합과 RRF 단계 모두에서 이 전략을 일관되게 적용했습니다. 기존 애플리케이션 레벨 Recency Boost에서 사용하던 가중치를 유지하여 동작 일관성도 확보했습니다.
하위 호환성과 점진적 전환
기존 동작과의 완전한 호환성도 중요한 고려 사항이었습니다. enableScoreFusion 옵션이 비활성화된 경우에는 기존 3-stage 파이프라인과 애플리케이션 레벨 Recency Boost가 그대로 동작하도록 설계했습니다.
Score Fusion이 활성화된 경우에는 파이프라인에서 이미 최신성이 반영되었으므로, 애플리케이션 단계의 Recency Boost는 생략합니다. 또한 하이브리드 검색 중 일부가 실패하더라도 벡터 검색 단독 결과로 정상 동작하도록 fallback을 구성했습니다. RRF는 단일 소스 결과만으로도 계산이 가능하기 때문에 부분 실패 상황에서도 서비스 연속성을 유지할 수 있습니다.
정리하며
벡터 검색은 의미적 유사도 측면에서는 매우 강력하지만, 시간이나 카테고리와 같은 다른 축을 함께 고려해야 하는 경우에는 명시적인 결합 전략이 필요하다는 점을 이번 설계에서 다시 확인했습니다.
MongoDB 공식 문서에서 제공하는 $rankFusion, $scoreFusion, $vectorSearch, $dateDiff, $exp의 동작을 기반으로 구조를 재구성해 보니, 현재 8.0 환경에서도 충분히 안정적인 하이브리드 검색을 구현할 수 있었습니다. RRF의 k=60 설정이 제공하는 완만한 점수 분포 특성 역시 실제 서비스 환경에서 균형 잡힌 결과를 만드는 데 도움이 되었습니다.
앞으로 MongoDB 버전이 상향되어 $scoreFusion과 $rankFusion을 보다 폭넓게 활용할 수 있게 되면, 현재 애플리케이션 사이드에서 구현한 로직을 점진적으로 네이티브 stage로 이관하는 것도 고려해 볼 수 있을 것 같습니다. 이번 작업은 특정 기능을 도입했다기보다는, 공식 문서를 기반으로 동작을 다시 점검하고 구조를 다듬어 보는 과정에 가까웠습니다. 개인적으로는 검색 품질을 설계 관점에서 다시 생각해 볼 수 있었던 의미 있는 경험이었습니다.
'TECH AND AI' 카테고리의 다른 글
- Total
- Today
- Yesterday
- Spring Batch
- Redis vs DB
- Cache Penetration
- Enum 기반 싱글톤
- Double-Checked Locking
- Hot Key 문제
- 캐시 장애
- DB 인덱스 성능
- Initialization-on-Demand Holder Idiom
- Java Performance
- Cache Aside
- 백엔드 성능
- 백엔드 아키텍처
- 스레드 생명주기
- Eager Initialization
- 트랜잭션 관리
- spring batch 5
- mybatis
- Cache Avalanche
- TTL 설계
- 백엔드 성능 설계
- Redis 성능 개선
- 백엔드 성능 튜닝
- Redis 캐시 전략
- 캐시 성능 비교
- 트래픽 처리
- 동시성처리
- InterruptedException
- 캐시와 인덱스
- DB 트랜잭션
| 일 | 월 | 화 | 수 | 목 | 금 | 토 |
|---|---|---|---|---|---|---|
| 1 | 2 | 3 | 4 | 5 | 6 | 7 |
| 8 | 9 | 10 | 11 | 12 | 13 | 14 |
| 15 | 16 | 17 | 18 | 19 | 20 | 21 |
| 22 | 23 | 24 | 25 | 26 | 27 | 28 |
| 29 | 30 | 31 |

