[Milvus] 다국어 하이브리드 검색에서 multi analyzer는 효과가 있을까? (MIRACL 벤치마크)
11 분 소요
들어가며
Milvus 2.6.22에서 HNSW(dense) + BM25 하이브리드 검색을 하려고 한다. 클러스터는 언어권별로 따로 둔다.
KR 클러스터: 한국어 위주, 일본어·영어가 일부 섞인다.
JP 클러스터: 일본어 위주, 한국어·영어가 일부 섞인다.
용도는 앱 검색이라 유저는 주로 키워드로 검색할 것으로 본다.
고민한 질문: 문서 언어를 감지해 multi_analyzer_params로 언어별 토크나이저를 쓰면, 토크나이저 하나로 통일할 때보다 실제로 나은가? 애매한 쿼리는 여러 분석기로 fan-out해서 RRF로 합치는 것도 같이 검토했다.
공개 데이터셋으로 직접 벤치마크를 돌려 확인했다.
결론부터
질문
결론
KR 클러스터에 multi analyzer가 필요한가?
필요 없다. 한국어 토크나이저(ko-dic)는 섞여 들어오는 일본어·영어도 충분히 처리한다. multi analyzer를 쓰면 일본어 쿼리는 오히려 소폭 낮았다(질문형 −0.012만 겨우 유의).
JP 클러스터에 multi analyzer가 필요한가?
필요하다. 일본어 토크나이저(ipadic)는 한국어를 처리하지 못한다. 한국어 위주 텍스트만 ko-dic으로 보내면 한국어 검색 품질이 크게 오르고, 일본어·영어 쪽 손해는 없다.
애매한 쿼리를 fan-out해야 하나?
하지 않는다. 이득이 없고, RRF로 합치면 오히려 떨어진다.
dense + BM25 랭킹은 RRF가 최선인가?
아니다. 이 데이터에서는 RRF가 dense만 쓸 때보다도 나빴다. 앱에서 직접 min-max 정규화한 뒤 가중합(dense 0.7)한 방식이 가장 좋았다. Milvus WeightedRanker(norm_score)는 dense만 쓸 때보다 나을 게 없었다(KR +0.003, JP −0.020).
리랭커(bge-reranker-v2-m3)를 붙이면?
효과가 크다. 상위 20개를 다시 정렬하면 nDCG@10이 KR +0.056, JP +0.072 올랐다. 키워드·짧은 쿼리에서 특히 크다. 대신 쿼리마다 모델 추론 비용이 든다.
텍스트 정규화가 필요한가?
넣자. NFKC + removepunct / asciifolding / 영어 stemmer. 이 데이터에서는 효과가 작았지만 손해도 없었다.
최종 권장 flow
텍스트 정규화: insert와 query 전에 앱에서 NFKC를 적용한다.
BM25 분석기
KR: ko-dic 하나로 처리한다.
JP: 텍스트(문서 하나, 쿼리 하나)마다 우세한 언어를 정해서 multi analyzer로 보낸다(ko-dic / ipadic / english). insert와 query에 같은 규칙을 쓴다.
공통 필터: lowercase, removepunct, asciifolding, 영어 stemmer
fan-out은 하지 않는다.
후보 검색: dense 검색과 BM25 검색을 각각 실행해서 상위 50~100개씩 가져온다.
합치기: 리스트마다 min-max 정규화를 한 뒤 0.7 × dense + 0.3 × BM25로 더한다. 가중치는 실제 검색 로그로 튜닝한다.
리랭킹: bge-reranker-v2-m3로 상위 20개를 다시 정렬한다. 지연시간과 GPU 비용은 따로 확인한다.
핵심 개념과 이유 정리
왜 KR은 multi analyzer를 안 쓰고 JP만 쓰나
한 줄로 정리하면 이렇다.
영어는 어느 토크나이저를 써도 잘 잘린다. 그래서 이슈가 없다.
ko-dic은 일본어도 꽤 잘 처리한다. 한국어+일본어를 ko-dic 하나로 insert·query해도 큰 문제가 없다.
ipadic은 한국어를 처리하지 못한다. 그래서 JP에서는 규칙에 따라 multi analyzer로 한국어만 ko-dic으로 보낸다.
ko-dic → 일본어: 한자어 사전이 있어서 太陽誘電の創始者를 太陽 / 誘 / 電 / の / 創始 / 者처럼 잘게 쪼갠다. 문서와 쿼리를 똑같이 쪼개기 때문에 BM25 매칭이 유지된다. KR에서 multi analyzer를 써도 하이브리드 전체 차이는 ±0.004 이내였고, 일본어 쿼리만 보면 −0.008~−0.012로 소폭 낮았다(질문형만 겨우 유의하고 나머지는 유의하지 않음).
ipadic → 한국어: 비행장은, 제주도민들이처럼 조사가 붙은 어절을 통째로 토큰으로 만든다. 그래서 비행장으로 검색하면 매칭되지 않는다. JP에서 multi analyzer를 쓰면 한국어 쿼리가 하이브리드 +0.018~0.034, BM25 +0.10~0.16 올랐다.
그럼 JP도 ko-dic 하나로 통일하면 되지 않나? ko-dic의 일본어 처리는 ipadic에 가깝지만 같지는 않다. JP 코퍼스 전체를 ko-dic으로 통일하면 일본어 쿼리가 −0.006~−0.019 떨어졌다. KR에서는 일본어 비중이 작아 이 손해를 무시할 수 있지만, JP에서는 주력 언어라 그럴 수 없다. 그래서 JP는 일본어는 ipadic, 한국어는 ko-dic으로 나눈다.
언어 지정 규칙 (JP): insert와 query에 똑같이 적용한다
KR은 multi analyzer를 쓰지 않으므로 insert·query 모두 ko 하나로 처리한다. 아래 규칙은 JP용이다.
기준은 인스턴스의 주력 언어가 아니라 그 텍스트(문서 하나, 쿼리 하나)에서 많이 쓰인 언어다. 언어를 판별할 수 없는 경우(한자만 있음)에만 주력 언어로 보낸다.
텍스트
지정할 언어
이유
한국어 위주 (일본어·영어 단어가 섞여 있어도)
ko
ipadic은 조사가 붙은 한국어 문장을 거의 매칭하지 못한다.
일본어 위주 (한국어 고유명사·영어가 섞여 있어도)
ja
가나는 일본어라는 확실한 신호다. 카카오톡, 네이버 웹툰 같은 한국어 고유명사는 ipadic과 ko-dic이 똑같이 자른다. ipadic은 영어도 chatgpt / で / 画像처럼 잘 자르지만, 영어 분석기는 일본어를 통째로 토큰 하나로 만든다.
영어만 있음
en
어간 처리(apps → app)와 불용어 처리를 받는다. 주력 언어 분석기와 차이가 작아서 어느 쪽이어도 큰 문제는 없다.
한자만 있음
ja (인스턴스 주력 언어)
일본어인지 중국어인지 구분할 수 없다. 중국어 문서는 거의 없으니 주력 언어로 보낸다. 참고로 KR에서는 한자 키워드를 ko-dic으로 처리한 결과가 ipadic보다 좋았다(BM25 0.578 vs 0.545). 다만 JP에서는 일본어 문서가 ipadic으로 저장되므로, 한자 쿼리도 같은 ipadic으로 잘라야 토큰이 맞는다.
“한글이 하나라도 있으면 ko”로 하면 안 되는 이유: 일본어 위주 문서에 한국어 단어 몇 개가 섞였다고 ko로 저장하면, 일본어 부분까지 ko-dic으로 잘린다. 그러면 ja로 처리된 일본어 쿼리와 토큰이 어긋난다.
カカオトーク: ko-dic은 한 덩어리 / ipadic은 カカオ / トーク
画像: ko-dic은 画 / 像 / ipadic은 画像
이 규칙에 깔린 가정: 유저는 문서의 주된 언어로 검색한다.
한국어 위주 문서(ko로 저장)를 그 안에 섞인 일본어 복합어로 검색하면 토큰이 어긋날 수 있다.
영어와 한국어 고유명사는 어느 분석기든 똑같이 잘라서 영향이 없다. 하이브리드에서는 dense가 상당 부분 메워 준다.
한국어가 섞인 혼합 텍스트(한국어+일본어, 한국어+영어) 규칙은 직접 측정한 결과가 아니다. 토큰화 결과와 언어별 측정 결과로 판단한 것이다. 일본어+영어 혼합은 직접 측정했다(결과 1 참고).
insert와 query 규칙이 같아야 하는 이유: BM25는 토큰이 정확히 일치해야 점수가 난다. 같은 문자열이라도 insert 때 분석기 A, query 때 분석기 B로 자르면 토큰이 달라져 매칭이 깨진다. 그래서 같은 감지 함수를 양쪽에 쓴다.
Milvus에서 지정하는 위치
insert: multi_analyzer_params의 by_field로 지정한 필드(예: lang)에 언어 값을 넣는다.
query: 검색 파라미터에 analyzer_name으로 같은 값을 넘긴다.
fan-out은 왜 효과가 없나
fan-out은 insert 언어는 정해 두고, query 언어가 애매할 때 후보 분석기 2~3개로 각각 검색해서 합치는 방식이다.
이득이 나려면 같은 문자열을 분석기마다 다르게 자르고, 정답 문서가 다른 분석기로 저장돼 있어야 한다. 실제로는 이런 경우가 거의 없다.
영어 부분은 ko-dic, ipadic, english 모두 소문자 + 단어 단위로 똑같이 자른다. 그래서 어느 분석기로 검색해도 같은 토큰이 나오고, 같은 문서가 걸린다.
asciifolding: 악센트나 전각 라틴 문자를 ASCII로 접는다(é → e, c → c).
stemmer (english): 영어 단어를 기본형으로 줄인다(apps → app, running → run, edited → edit). 넣어도 画像, 번역 같은 일본어·한국어 토큰은 그대로라 부작용이 없었다. 필요한 경우는 두 가지다.
같은 분석기 안에서 형태가 다를 때 (더 흔하다): 문서 photo editing apps와 쿼리 photo edit app은 stemmer가 없으면 apps / editing과 app / edit으로 갈려 매칭되지 않는다. en 분석기에는 stemmer가 원래 들어 있지만 ja·ko 분석기에는 없다. 그래서 일본어·한국어 문서 속 영어 단어는 이 문제가 그대로 생긴다.
저장과 검색 분석기가 다를 때: en으로 저장된 apps는 app이 된다. 그런데 쿼리를 ja로 자르면 apps 그대로라 매칭되지 않는다. ja·ko에도 stemmer를 넣으면 양쪽 모두 app이 된다.
NFKC를 따로 해야 하는 이유: 필터는 토큰을 자른 뒤에 적용된다. 그런데 토크나이저가 전각 숫자 15를 1 / 5로 먼저 쪼개 버린다. 그래서 자르기 전에 앱에서 NFKC로 바꿔 두는 것이 확실하다.
벤치마크 구성
데이터: MIRACL dev (ko / ja / en). 사람이 판정한 정답과 hard negative가 들어 있다.
KR용: 한국어 문서 43k + 일본어 5k + 영어 2.5k
JP용: 일본어 문서 40k + 한국어 5k + 영어 2.5k
쿼리 형태: 원래 질문형(q)에 더해, 앱 검색을 흉내 내려고 정규식으로 키워드형(kw)과 2단어형(short)을 만들었다. 토크나이저로 만들면 특정 토크나이저에 유리해지기 때문에 정규식만 썼다.
RRF는 dense만 쓸 때보다 나빴다. 두 리스트를 같은 비중으로 섞는데, 이 데이터에서는 BM25가 dense보다 훨씬 약하다.
Milvus WeightedRanker의 정규화가 BM25 점수를 뭉갠다.norm_score=True이면 BM25 점수를 2·atan(s)/π로 정규화한다(v2.6). 그러면 BM25 10점은 0.94, 20점은 0.97이 되어 점수 차이가 거의 사라진다.
하이브리드가 dense만 쓸 때보다 의미 있게 나았던 건 앱에서 직접 min-max 정규화한 가중합뿐이었다(KR +0.024, JP +0.015). WeightedRanker는 KR +0.003, JP −0.020으로 사실상 이득이 없었다.
가중치 0.7은 이 데이터 기준이다. MIRACL은 위키 질문이라 dense에 유리하다. 앱 이름처럼 고유명사가 많은 검색에서는 BM25 비중을 더 높여야 할 수도 있으니, 실제 검색 로그로 다시 튜닝해야 한다.
결과 4. 리랭커 (bge-reranker-v2-m3)
C1(multi analyzer) + min-max 가중합 구성의 상위 20개를 cross-encoder 리랭커로 다시 정렬했다. KR의 권장 구성(ko-dic 단일, C0)은 리랭킹 전 점수가 0.729로 C1(0.725)과 거의 같아서, 결과도 사실상 같을 것으로 본다.
쿼리
KR
JP
전체
0.725 → 0.781 (+0.056)
0.718 → 0.790 (+0.072)
일본어 키워드 / 2단어
+0.082 / +0.072
+0.105 / +0.104
한국어 키워드 / 2단어
+0.042 / +0.085
+0.039 / +0.064
영어 키워드 / 2단어
+0.036 / +0.046
+0.039 / +0.037
질문형 (ko / ja / en)
+0.010(유의하지 않음) / +0.066 / +0.039
+0.018 / +0.056 / +0.036
KR 한국어 질문형(+0.010, 유의하지 않음)을 빼면 모든 구간에서 의미 있게 올랐고, 키워드·짧은 쿼리에서 특히 컸다. 키워드 검색이 주력이라면 analyzer 설정을 바꾸는 것보다 효과가 훨씬 크다.
비용: Mac(M4 Pro, MPS)에서 상위 20개 재정렬에 쿼리당 약 0.5초가 걸렸다(여러 쿼리를 묶어 처리한 평균). 운영 GPU에서는 훨씬 빠르겠지만, 지연시간과 GPU 비용은 따로 재 봐야 한다.
덤: analyzer 설정에서 챙길 것
위 “정규화 설정”의 필터를 모두 넣은 분석기 예시다. MIRACL에서는 NFKC + 필터의 효과가 +0.001 ~ +0.003으로 미미했지만 손해도 없었다(위키피디아에는 전각 영문이 드물다). 앱 데이터에서는 효과가 더 클 수 있다.
컬렉션에 데이터를 넣자마자 벤치마크를 돌리면 인덱스가 아직 다 만들어지지 않은 상태에서 검색된다. 실제로 처음 잰 수치가 그래서 틀어졌다. describe_index의 indexed_rows가 전체 행 수와 같아질 때까지 기다린 뒤 측정하자.
한계
bge-m3(그리고 bge-reranker-v2-m3도 가능성이 높다)는 학습 데이터에 MIRACL train 셋이 들어 있다. 평가에 쓴 dev 쿼리와는 다르지만 같은 출처라서 dense 점수와 리랭커 개선 폭(+0.056 / +0.072)이 실제보다 좋게 나왔을 수 있다. 그래서 “dense가 BM25보다 훨씬 강하다”는 결과와 가중치 0.7은 실제 데이터에서 BM25 쪽으로 바뀔 수 있다.
MIRACL은 위키피디아 데이터다. 키워드 쿼리도 정규식으로 만든 합성 데이터라서 실제 앱 이름·설명 데이터와는 분포가 다르다.
한국어+일본어, 한국어+영어가 섞인 쿼리는 데이터셋에 거의 없어서 직접 측정하지 못했다.
중국어 문서가 의미 있게 들어오는 환경이라면 ja/zh fan-out 결론이 달라질 수 있다.
결론을 확정하려면 실제 앱 카탈로그와 검색 로그 일부로 같은 비교를 돌려 보는 것이 가장 확실하다.
댓글남기기