"결제 취소 방법 알려줘." 사내 문서 검색 챗봇에 물었는데 근거로 달려 나온 문서가 엉뚱했어요. 정작 답이 적힌 안내문은 「결제취소는 마이페이지 주문내역에서 신청합니다」인데, 상위 다섯 건 안에 없었고요.
RAG를 붙이면 대개 이 장면부터 만나요. 먼저 떠오르는 가설은 "임베딩이 한국어를 잘 못 잡는다"입니다. 그런데 벡터 검색만 떼어 재 보면 정답 문서가 상위권에 멀쩡히 올라와 있곤 해요. 문제는 그 다음이에요. 하이브리드 검색이라면 어휘 검색 쪽이 같은 문서에 0점을 주고 있을 수 있거든요.
어휘 검색이 0점을 주는 이유는 띄어쓰기였어요
확인하려고 순수 파이썬 BM25(k1=1.2, b=0.75)를 짜서 한국어 안내문 10건에 돌려봤어요. 질의는 "결제 취소 방법", 정답은 "결제취소는 마이페이지…" 문서입니다. 토크나이저만 바꿔 비교했어요.
def tok_space(t): # 공백·구두점 기준
return re.findall(r"[가-힣A-Za-z0-9]+", t)
def tok_ngram(t, n=2): # 문자 2-gram
s = re.sub(r"[^가-힣A-Za-z0-9]", "", t)
return [s[i:i+n] for i in range(len(s)-n+1)]
쪼개는 단위만 바꿨는데 결과는 이렇게 갈렸어요.
[공백] 질의 토큰 = ['결제', '취소', '방법']
1위 4.097 주문 취소 방법 문의가 많아 FAQ에 정리했습니
정답(결제취소 문서) 점수 0.000 → 4위
[2-gram] 질의 토큰 = ['결제', '제취', '취소', '소방', '방법']
2위 3.796 결제취소는 마이페이지 주문내역에서 신청합니다
정답(결제취소 문서) 점수 3.796 → 2위
점수가 낮은 게 아니라 0점이라는 게 핵심이에요. 색인에 결제취소라는 토큰 하나만 있으니 질의의 결제와 취소가 어디에도 걸리지 않아요.
n-gram은 리콜을 살리는 대신 없던 단어를 만들어요
문자 2-gram으로 바꾸니 정답이 4위에서 2위로, 점수도 0에서 3.796이 됐어요. 그런데 질의 토큰에 소방이 섞여 있죠. "취소 방법"을 글자 단위로 자르다 생긴, 원문에 없는 단어예요. 수만 건 규모가 되면 이런 토큰이 상위 결과를 지저분하게 만들고요.
그래서 형태소 분석기 쪽으로 갑니다
Elasticsearch의 nori 플러그인은 mecab-ko-dic 사전으로 한국어를 형태소 단위로 분석하고, 복합명사 처리를 decompound_mode로 고르게 해줘요. 기본값 discard는 가곡역을 가곡, 역으로 쪼개며 원형을 버리고, mixed는 가곡역, 가곡, 역을 모두 남깁니다.
"tokenizer": {
"ko": { "type": "nori_tokenizer", "decompound_mode": "mixed" }
}
mixed면 결제취소 문서가 "결제 취소" 질의에도 걸리고, 2-gram과 달리 유령 토큰도 만들지 않아요.
융합 단계에서 한 번 더 갈려요
두 검색기를 합칠 때 흔히 쓰는 건 RRF(Reciprocal Rank Fusion)예요. Azure AI Search 문서에 계산식이 나와 있습니다. 각 목록에서 문서 순위를 받아 1/(순위 + k)로 점수를 매겨 더하고, k는 60쯤이 가장 잘 동작한다고 해요.
여기서 RRF는 점수가 아니라 순위를 봐요. 상한 없는 BM25 점수와 0~1인 코사인 유사도를 그대로 더하면 한쪽이 다른 쪽을 삼키니까요. 대신 앞 단계 순위가 틀리면 그 오류도 실려 옵니다. 토크나이저를 고치기 전엔 정답 문서가 BM25 목록에 없었으니, RRF가 아무리 잘 합쳐도 나올 수 없었어요.
재발을 막은 건 눈금을 따로 둔 거였어요
이 일을 겪고 바꾼 건 도구가 아니라 순서예요. 답변 품질을 먼저 보지 않고 검색만 따로 잽니다.
| AS-IS | TO-BE | |
|---|---|---|
| 무엇을 보나 | 최종 답변이 그럴듯한가 | 후보 안에 정답 문서가 있었나 |
| 지표 | 사람이 읽고 판단 | recall@10, 리랭킹 후 nDCG@5 |
질의 30~50개와 정답 문서 id를 고정한 회귀 세트면 충분해요. 눈금이 없으면 답이 나아졌을 때 검색이 좋아진 건지 모델이 더 잘 둘러댄 건지 구분이 안 되니까요. 단계마다 측정 지점을 두는 이야기는 파일럿은 통과했는데 운영에서 멈춘다에서도, 도입 전체 흐름은 6단계 가이드 1편에서 다뤘어요.
답이 이상할 때, 저희는 모델부터 보지 않아요
제일 먼저 볼 건 모델이 아니라 후보 안에 정답 문서가 있었는지예요. 없었다면 생성이 아니라 색인의 문제고, 프롬프트를 다듬어도 해결되지 않아요. RAG는 한 번 세팅하고 끝나는 레시피가 아니라, 실패한 질의를 회귀 세트에 하나씩 넣으며 계속 다듬는 파이프라인입니다. 저희가 일하는 방식도 그 위에 있고요.
자주 묻는 질문
Q. 벡터 검색만 쓰면 토크나이저 문제는 없는 거 아닌가요? A. 벡터 검색은 표현이 달라도 의미로 잡지만 사번·모델명·약어 같은 고유 문자열엔 약해요. 그래서 어휘 검색을 함께 두고, 두는 순간 토크나이저가 품질을 좌우합니다.
Q. 리랭커를 붙이면 이 문제가 해결되나요? A. 아니요. 리랭커는 넘어온 후보 안에서 순서를 고칠 뿐, 후보에 없는 문서를 만들지 못해요. recall 먼저, 정밀도는 그다음입니다.
Q. 사내 용어가 많아 형태소 사전에 없는 말이 대부분인데요?
A. nori는 user_dictionary 파일이나 user_dictionary_rules 규칙으로 사용자 명사를 추가할 수 있어요. 사전을 처음부터 채우기보다, 실패한 질의가 나올 때마다 그 단어를 사전과 회귀 세트에 같이 넣는 편이 현실적입니다.
Sources
- Hybrid search scoring (RRF) in Azure AI Search — Microsoft Learn (2026-06-08 갱신): https://learn.microsoft.com/en-us/azure/search/hybrid-search-ranking
- Korean (nori) analysis plugin — Elastic Docs: https://www.elastic.co/docs/reference/elasticsearch/plugins/analysis-nori
- nori_tokenizer reference (
decompound_mode,user_dictionary) — Elastic Docs: https://www.elastic.co/docs/reference/elasticsearch/plugins/analysis-nori-tokenizer

