ml
벡터검색에서 리랭커를 뺐다 — '아기 사진 다 찾아줘'가 무너뜨린 가정
· Ascendy Engineering
TL;DR
- 사진검색 스택을 손보며 두 가지를 했다: 임베딩을 bge-m3+bge-reranker → Qwen3-VL-Embedding으로 교체하고, 리랭커를 아예 제거했다. 두 결정의 이유는 다르다.
- 리랭커를 뺀 계기는 “아기 나온 사진 다 찾아줘” — 수천 장을 돌려줘야 하는 쿼리다. 이건 precision이 아니라 recall 문제고, 리랭커는 (a) 수천 개 cross-encoder 점수가 너무 느리고/비싸며 (c) top-k 컷오프로 “다”를 잘라버린다. 리랭커는 precision@k 도구지 벌크 도구가 아니다.
- “벌크 쿼리만 리랭커 끄기” 분기는 포기했다 — 한 문장에 넣을·뺄 조건이 섞이고, “다 보여줘”의 의도(“전부” vs “그냥 많이”)를 시스템이 단정할 수 없어서. 자연어 라우팅은 신뢰 불가.
- 대신 Qwen3-VL-Embedding의 MRL(Matryoshka)로 저차원 1차 필터 → 고차원 2차 정밀화 2단계를 만들어, 별도 리랭커 없이 정밀도를 회복했다.
소스 노트. 이 글은 운영자 인터뷰로 썼다. 모델 스펙은 발행 시점에 공식 출처(HF Qwen3-VL-Embedding, arXiv 통합 프레임워크 논문)로 fact-check했고, 내부 컬렉션·인프라 식별자와 정확한 차원 튜닝값은 일반화했다(모델명은 공개 모델이라 유지). 제품 동기는 사진은 저장이 아니라 ‘못 찾는 것’이 문제였다와 이어진다.
두 개의 결정, 다른 이유
사진검색의 검색 스택을 손보면서 두 가지를 결정했는데, 둘은 별개다. 헷갈리기 쉬워 먼저 갈라둔다.
- 임베딩 교체 (bge-m3+bge-reranker → Qwen3-VL-Embedding): 사진검색은 단어 일치가 아니라 의미·멀티모달 맥락을 잡아야 한다. Qwen3-VL-Embedding은 텍스트·이미지·비디오를 함께 다루는 멀티모달 임베딩이라 사진에 맞다.
- 리랭커 제거: 이건 모델 성능 문제가 아니라 쿼리의 성격 문제였다. 아래가 그 이야기다.
”아기 사진 다 찾아줘”가 무너뜨린 가정
리랭커를 빼야겠다고 깨달은 건 테스트 중이었다. 시나리오 하나가 가정을 무너뜨렸다.
“아기가 나온 사진 다 찾아줘.”
이런 쿼리는 수천 장을 돌려줘야 한다. 그리고 여기서 중요한 구분이 있다 — 이건 precision 문제가 아니라 recall 문제다. “상위 10장이 얼마나 정확한가”가 아니라 “있는 거 다 끌어왔는가”가 성공 기준이다.
리랭커는 이 벌크에 구조적으로 안 맞았다.
- (a) 비용·지연 폭발. cross-encoder 리랭커로 수천 개 후보를 일일이 점수 매기는 건 너무 느리고 비싸다. 리랭커는 쿼리-문서 쌍마다 한 번씩 모델을 통과시키는 구조라, 후보가 늘면 비용이 선형으로 터진다.
- (c) 컷오프 절단. 일반적인 리랭커 파이프라인은 제한된 후보군(top-k)을 재정렬하는 구조다. “있는 거 다”라는 요구 앞에서, 그 top-k 컷오프가 정답을 잘라버린다.
한 줄로: 리랭커는 precision@(작은 k) 도구지, “다 찾아줘”라는 recall 도구가 아니다. 우리는 리랭커를 잘못된 일에 쓰고 있었다.
그럼 쿼리 타입으로 나누면 되잖아? — 안 된다
가장 자연스러운 반론이 여기서 나온다. “벌크 쿼리(‘다 찾아줘’)엔 리랭커 끄고, 콕 집는 쿼리(‘내 아들 촛불 부는 그 사진’)엔 켜면 되잖아.” 쿼리 타입별 분기. 고려했고, 포기했다. 두 가지 이유 때문이다.
첫째, 자연어가 조건을 깔끔히 안 가른다. 한 문장에 넣을 조건과 뺄 조건이 섞일 수 있다 — “강아지 나온 건 빼고 아기 사진은 다 보여줘”처럼. 이걸 LLM이 완벽하게 분해해서 “이 부분은 벌크, 이 부분은 정밀”로 라우팅하는 건 불가능에 가깝다. 한 번이라도 틀리면 사용자는 엉뚱한 결과를 받는다.
둘째, 설령 갈라도 의도를 추론할 수 없다. “다 보여줘”가 누군가에겐 문자 그대로 수천 장 전부이고, 누군가에겐 그냥 많이 보여줘다. 같은 표현 뒤의 의도를 시스템이 함부로 단정하면, 절반의 사용자는 매번 기대와 다른 결과를 본다. 표현은 같은데 의도가 갈리는 걸, 자연어만 보고 안전하게·일관되게 가려내긴 어렵다.
그래서 분기 대신 리랭커를 통째로 뺐다. 이 결정을 뒷받침한 건, 임베딩만으로도 정밀도가 멀쩡했다는 사실이다 — 처음 뺐을 때부터 검색 품질 자체가 나쁘지 않았다.
대체 — MRL로 만든 2단계 (리랭커 없이 정밀도)
리랭커를 빼고 정밀도를 보완할 방법을 고민했는데, 생각보다 쉽게 찾았다. Qwen3-VL-Embedding에는 MRL(Matryoshka Representation Learning)이 적용돼 있다. 하나의 임베딩 벡터를 짧게 잘라(truncate) 써도 의미가 보존되는 성질이다 — 마트료시카 인형처럼, 앞쪽 일부 차원만 떼어내도 그 자체로 쓸 만한 임베딩이 된다. 공식 스펙상 임베딩 차원은 최대 4096(2B 모델은 2048, 8B 모델은 4096)이고, 64~4096 범위에서 사용자가 출력 차원을 자유롭게 정할 수 있다.
이걸로 2단계를 만들었다.
- 저차원 1차 필터 — 낮은 차원으로 전체를 빠르고 싸게 훑어 후보를 좁힌다. 차원이 낮으니 비교가 가볍다.
- 고차원 2차 정밀화 — 좁혀진 후보만 높은 차원으로 다시 비교해 순위를 정밀하게 만든다.
별도의 cross-encoder 리랭커 모델 없이, 같은 임베딩 한 모델 안에서 리랭커와 비슷한 효과를 낸다. 단, 로직은 다르다 — 이건 차원을 달리한 2단계 dense 검색이지, 쿼리-문서 쌍을 재점수하는 cross-encoder가 아니다. coarse-to-fine으로 정밀도를 끌어올리되, 벌크(저차원 1차 필터)는 막지 않는다.
결과: 정밀도 멀쩡, 벌크 가능, 스택은 모델 하나만큼 단순해졌다.
가져갈 것
- 리랭커는 precision@k 도구지 recall/벌크 도구가 아니다. “있는 거 다 찾아줘”형 쿼리가 있다면, 리랭커의 top-k·cross-encoder 가정이 거기서 깨진다. 도구를 쿼리의 성격에 맞춰라.
- 벌크 vs 정밀을 자연어로 라우팅하지 마라. 한 문장에 조건이 섞이고, 같은 표현의 의도가 사람마다 갈린다 — 자연어만으로 안전하게 못 가른다.
- MRL coarse-to-fine은 리랭커의 가벼운 대안이 될 수 있다. 저차원 1차 필터로 벌크를 살리고, 고차원 2차로 정밀도를 회복하면, 별도 리랭커 모델 없이 한 모델로 끝난다.
- 모델을 하나 빼는 결정은 빼고도 품질이 멀쩡한지로 검증한다 — 우리 경우 임베딩만으로 정밀도가 유지된 게 결정을 뒷받침했다.
저작·인용: 이 글은 Ascendy Engineering이 작성했으며 출처 표기 시 재인용 가능합니다. 잘못된 정보를 발견하면 GitHub 이슈로 알려주세요.
Tags: vector-search, embeddings, reranker, matryoshka, retrieval, rag