AWS 기술 블로그
AI Agent를 위한 OpenSearch 검색 품질 평가하기 (Part 1)
요즘 RAG(Retrieval-Augmented Generation) 기반 AI Agent를 구축하는 프로젝트가 정말 많습니다. 사내 문서를 검색해 답하는 어시스턴트, 고객 문의를 처리하는 봇, 방대한 기술 문서에서 근거를 찾아주는 에이전트까지. 그런데 이런 프로젝트를 지원하다 보면 거의 같은 질문을 드리게 됩니다. “검색 품질은 어떻게 측정하고 계신가요?”
돌아오는 답은 대개 비슷합니다. “체감상 좋아진 것 같아요.” “몇 개 돌려봤는데 괜찮던데요.” 정작 “검색이 얼마나 좋아졌는지”를 숫자로 확인하는 경우는 드뭅니다. Agent가 이상한 답을 내놓으면 우리는 보통 프롬프트를 손보고, 그래도 안 되면 모델을 바꾸고, 여전히 안 되면 검색 방식을 바꿉니다. 하지만 이 모든 시도가 정말 검색을 개선했는지 판단할 기준이 없습니다.
이 글에서는 Amazon OpenSearch Service와 Amazon Bedrock을 활용해 검색 품질을 정량적으로 측정하는 시스템을 구축한 경험을 공유합니다. 단순히 “이 방식이 좋다”가 아니라, 어떤 지표로 무엇을 측정하고, 그 측정값을 얼마나 믿을 수 있는지 검증하는 과정까지 다룹니다. 그리고 그 과정에서 확인된 함정들도 함께 이야기합니다.
참고로 이 글에서 다루는 실험들은 AI 코딩 어시스턴트를 활용해 진행했습니다. 요즘 Kiro, Codex, Cursor, Claude Code 같은 도구들이 많이 나와 있는데, 인프라 배포부터 평가 스크립트 작성, 실행, 결과 정리까지 이런 도구와 함께 작업하면 훨씬 빠르게 진행할 수 있습니다. 2부에서는 이 AI 도구를 활용한 검색 최적화 자동화와, 운영 환경에서의 지속적인 평가 루프를 본격적으로 다룹니다.
왜 검색 평가가 필요한가
RAG 시스템에서 검색은 전체 품질의 기반입니다. 아무리 뛰어난 LLM을 붙여도, 검색된 문서가 엉뚱하면 답변도 엉뚱해집니다. “쓰레기가 들어가면 쓰레기가 나온다(garbage in, garbage out)”는 격언은 RAG에서 동일하게 적용됩니다. LLM은 주어진 문서를 바탕으로 그럴듯한 문장을 만들어낼 뿐, 검색 단계에서 놓친 정답을 마법처럼 생성하지는 못합니다.
그래서 Agent 답변이 틀렸을 때 우리가 가장 먼저 의심해야 할 것은 모델이 아니라 검색(retrieval)입니다. 그런데 여기서 문제가 생깁니다. 검색 품질을 개선하려면 먼저 측정할 수 있어야 하는데, 대부분의 팀에는 그 측정 도구가 없습니다.

이 질문들에 답하려면 결국 같은 기준으로 여러 방식을 비교할 수 있는 숫자가 필요합니다. 정량적 평가가 없으면 검색 개선은 “느낌으로 운전하기”가 되고, 그 느낌은 대개 마지막으로 확인한 몇 개의 샘플에 좌우됩니다.
Agent의 검색은 사람의 검색과 다르다
한 가지 짚고 넘어갈 게 있습니다. 우리가 평가하려는 건 사람이 직접 쓰는 검색창이 아니라, AI Agent가 내부에서 돌리는 검색입니다. 이 둘은 성격이 꽤 다릅니다.
- 쿼리를 Agent가 만든다. 사람은 키워드 몇 개를 던지지만, Agent는 사용자 발화를 나름대로 해석해 검색 쿼리를 생성합니다. 때로는 장황하게, 때로는 한 질문을 여러 번 쪼개서 검색합니다. 즉 우리가 통제하지 못하는 쿼리로 검색이 일어납니다.
- 결과를 사람이 아니라 LLM이 읽는다. 사람은 검색 결과 10개 중 관련 있는 걸 눈으로 골라내지만, Agent는 검색이 데려온 문서를 그대로 LLM 컨텍스트에 넣습니다. 검색이 엉뚱한 문서를 가져오면 LLM이 그걸 근거로 그럴듯한 오답을 만들어냅니다.
- 그래서 검색이 답변의 상한을 정한다. 사람은 검색이 부실하면 다시 검색하거나 스스로 판단하지만, Agent는 검색된 문서 안에서만 답을 구성합니다. 정답 문서가 애초에 검색되지 않으면, 아무리 좋은 LLM도 그 정답을 말할 수 없습니다.
이 세 가지 때문에 Agent의 검색 품질은 사람용 검색보다 훨씬 냉정하게 측정해야 합니다. 특히 세 번째 — “검색이 답변의 상한을 정한다”는 점은 뒤에서 다룰 Recall@10이 왜 중요한지와 곧바로 이어집니다.
무엇으로 재는가, NDCG@10
검색 품질을 측정하는 대표적인 지표는 NDCG@10(Normalized Discounted Cumulative Gain)입니다. 이름은 복잡하지만 아이디어는 직관적입니다.
- 검색 결과 상위 10개를 평가합니다.
- 관련 있는 문서가 상위에 있을수록 높은 점수를 줍니다. (보통 5위에 있는 정답보다 1위에 있는 정답이 더 가치가 있습니다.)
- 이상적인 순서(정답만 위에서부터 완벽히 정렬된 상태)로 나눠 정규화하기 때문에, 항상 0~1 사이 값으로 나옵니다. 1에 가까울수록 좋습니다.
Precision@10(상위 10개 중 관련 문서 비율)이나 MAP(Mean Average Precision) 같은 지표도 함께 쓰지만, NDCG@10은 “순위까지 고려한다”는 점에서 검색 품질을 가장 균형 있게 요약해줍니다. 이 글에서는 NDCG@10을 주력 지표로 삼아 여러 검색 방식을 비교합니다.
하나 더, RAG에서는 Recall@k도 반드시 함께 봐야 합니다. Recall@k는 “전체 정답 문서 중 상위 k개 안에 몇 개나 데려왔는가”의 비율입니다. NDCG가 순서의 품질을 본다면, Recall은 놓친 정답이 얼마나 되는가를 봅니다. 이 둘은 다른 이야기를 합니다. 순위는 좋은데(NDCG 높음) 정작 정답의 대부분을 상위 k개 밖에 두고 온(Recall 낮음) 경우가 실제로 자주 생기기 때문입니다. RAG는 검색이 데려온 문서만 근거로 답을 만들므로, Recall@k가 낮으면 LLM이 아무리 좋아도 답의 상한이 막혀버립니다. 그래서 이 글에서는 NDCG@10과 Recall@10을 나란히 보겠습니다.
평가 시스템 아키텍처
전체 시스템 구성은 다음과 같습니다. 공개 벤치마크 데이터셋을 OpenSearch에 색인하고, 세 가지 검색 방식으로 질의한 뒤, 그 결과를 LLM과 정답 레이블 양쪽으로 채점하는 구조입니다.

사용 기술
각 구성요소는 모두 관리형 서비스로, 별도의 모델 서빙 인프라를 직접 운영할 필요가 없습니다. 검색 엔진은 Amazon OpenSearch Service가, 임베딩과 채점에 쓰는 모델은 Amazon Bedrock이 담당합니다.
| 구성요소 | 서비스 | 용도 |
|---|---|---|
| 검색 엔진 | Amazon OpenSearch Service | BM25 + k-NN 하이브리드 검색 |
| 임베딩 | Amazon Bedrock (Cohere Embed Multilingual v3) | 문서/쿼리 벡터화 |
| LLM Judge | Amazon Bedrock (Claude) | 검색 결과 관련성 평가 |
| 데이터셋 | BEIR Benchmark | SciFact, NFCorpus, TREC-COVID, Touche-2020 |
임베딩 모델로 Cohere Embed Multilingual v3를 선택한 이유는, 뒤에서 한국어 데이터(MIRACL)로도 같은 방법이 통하는지 확인하고 싶었기 때문입니다. 영어 전용 모델을 골랐다면 한국어 실험을 위해 파이프라인을 통째로 바꿔야합니다.
왜 BEIR 데이터셋인가?
자체 데이터로 검색을 평가하려면 정답 레이블(qrels)이 필요합니다. 각 쿼리에 대해 “어떤 문서가 관련 있는지”를 사람이 직접 태깅해야 하는데, 이게 생각보다 훨씬 비쌉니다. 쿼리 수백 개에 문서 수천 개를 교차로 채점하는 작업은 시간도, 비용도, 채점자 간 일관성 확보도 모두 만만치 않습니다. 많은 팀이 검색 평가를 포기하는 이유가 바로 여기 있습니다.
BEIR(Benchmarking IR)은 이미 정답 레이블이 붙어 있는 공개 데이터셋 모음입니다. 과학 논문, 의학 문헌, 코로나 연구, 논쟁적 주제 등 성격이 다른 여러 도메인을 포함하고 있어서, “우리가 만든 평가 시스템이 제대로 작동하는지”를 검증하기에 적합합니다. 이 글의 전략은 명확합니다. 정답이 있는 공개 데이터로 평가 파이프라인을 먼저 검증하고, 그 다음 그 파이프라인을 정답이 없는 자체 데이터에 적용한다. BEIR는 그 첫 단계를 위한 “정답지가 있는 연습 문제”인 셈입니다.
하지만 중요한 점은 BEIR 데이터셋에서 시멘틱 검색이 이겼다고 해서 사내 검색에서도 이긴다는 보장은 없습니다. 예를 들어 사용자가 OSS-1234 같은 티켓 번호나 사번, 에러 코드로 검색하는 서비스라면 의미 임베딩은 오히려 방해가 됩니다. 이런 식별자는 “의미”가 없어서 벡터 공간에서 엉뚱한 문서와 가까워지기 때문입니다. 반대로 BEIR 데이터는 대부분 잘 쓰인 문어체 문장이라 시멘틱에 유리합니다. 그래서 저는 BEIR을 “파이프라인이 제대로 도는지” 확인하는 용도로만 쓰고, 어떤 검색 방식을 쓸지는 실제 서비스 로그에서 쿼리를 뽑아 다시 평가합니다. 이 얘기는 뒤의 “데이터셋에 따라 결과가 다르다”에서 숫자로 다시 확인하게 됩니다.
LLM Judge, 믿을 수 있는가
사람 대신 LLM이 “이 검색 결과가 질문에 얼마나 맞는가”를 0~1 점수로 채점하는 방식을 LLM-as-a-Judge라고 부릅니다. 앞서 이야기한 “정답 레이블링이 비싸다”는 문제를 정면으로 해결해주는 접근이죠. 사람 채점자를 LLM으로 대체하면 싸고, 빠르고, 무엇보다 일관적입니다.
OpenSearch는 이 방식을 엔진 차원에서 지원합니다. OpenSearch 3.1에서 Search Relevance Workbench가 도입되면서 LLM 기반 관련성 판정(LLM_JUDGMENT)을 쓸 수 있게 되었고, 3.5부터는 채점 프롬프트를 직접 정의할 수 있습니다. Amazon OpenSearch Service에서도 엔진 버전 3.1 이상에서 이 기능을 사용할 수 있습니다(단, OpenSearch Serverless는 지원하지 않습니다). 쿼리셋과 검색 설정을 등록해두면 채점부터 지표 산출까지 클러스터 안에서 처리되므로, 정형화된 평가를 반복적으로 돌리기에 좋습니다.
다만 이 글에서는 Amazon Bedrock을 직접 호출하는 방식으로 채점기를 구현했습니다. 이유는 두 가지입니다. 첫째, 채점 로직이 코드로 드러나 있어야 “LLM이 무엇을 보고 어떻게 점수를 매기는지”가 직관적으로 이해되기 때문입니다. 프롬프트와 모델, 점수 파싱까지 한눈에 보이는 편이 이 글의 목적에 맞습니다. 둘째, 커스터마이징의 여지가 필요했습니다. 뒤에서 AgentCore의 평가 기능과 교차 검증할 때 두 채점기의 조건을 동일하게 맞춰야 했고, 2부에서는 이 채점기를 실시간 파이프라인과 Lambda 주기 평가에 함수로 끼워넣습니다. 정형화된 평가를 클러스터 안에서 반복적으로 돌리는 것이 목적이라면 Search Relevance Workbench가 더 편하고, 채점 로직을 직접 제어하거나 애플리케이션 코드에 녹여야 한다면 이 글처럼 Bedrock을 직접 호출하는 편이 낫습니다. 어느 쪽을 택하든 이어지는 검증 이야기는 그대로 적용됩니다.
그런데 여기서 대부분의 글이 그냥 넘어가는 질문이 하나 있습니다. “그 채점기를 믿어도 되나요?”
틀린 자로 길이를 재면, 그 위에 아무리 정교한 실험을 쌓아도 결론이 전부 틀립니다. LLM Judge가 사람과 전혀 다르게 채점한다면, 우리가 얻는 NDCG 값은 그럴듯해 보여도 실제 검색 품질과 무관한 숫자일 뿐입니다. 그래서 저는 이 검증 단계를 선택이 아니라 첫 번째 필수 단계로 삼았습니다.
검증 방법은 단순합니다. LLM Judge의 점수와 Gold Qrels(사람이 직접 태깅한 정답)를 나란히 놓고 얼마나 일치하는지 통계로 확인하는 것입니다.

검증 결과
100개 샘플에 대해 LLM Judge 채점과 Gold Qrels를 비교한 결과입니다.
| 지표 | SciFact (영어) | MIRACL (한국어) |
|---|---|---|
| Agreement Rate | 65% | 75% |
| Cohen’s Kappa | 0.0 | 0.50 |
| Spearman Correlation | 계산 불가 | 0.94 |
각 지표를 하나씩 해석해 보겠습니다.
- Agreement Rate 65~75%: LLM이 사람과 대체로 비슷하게 판단한다는 뜻입니다. 한국어(MIRACL)에서 오히려 더 높게 나온 점도 눈여겨볼 만합니다.
- Spearman 0.94 (MIRACL): 순위 상관관계가 매우 높습니다. 절대 점수는 조금 달라도 “무엇이 더 관련 있는가”의 순서를 사람과 거의 똑같이 매긴다는 의미입니다. 검색 평가에서는 절대값보다 이 순서가 훨씬 중요합니다.
- Cohen’s Kappa 0.50 (MIRACL): 우연의 일치를 걷어낸 “실질 일치도”로, 통상 “Moderate agreement(중간 정도 일치)” 구간입니다.
SciFact에서 Kappa가 0이 나온 이유
표를 보면 이상한 부분이 있습니다. SciFact에서는 Kappa가 0.0이고 Spearman은 아예 계산조차 되지 않았습니다(NaN). LLM이 형편없이 채점했다는 뜻일까요? 아닙니다. 데이터의 함정입니다.
SciFact의 Gold Qrels를 열어보니, 레이블이 전부 “관련 있음(1)” 한 종류였습니다. 관련 없는 문서에 대한 부정 레이블(0)이 데이터셋에 포함되어 있지 않았던 것이죠. Kappa와 Spearman 같은 상관 지표는 양쪽에 변동(분산)이 있어야 계산됩니다. 한쪽이 전부 같은 값이면, 통계적으로는 “상관관계를 정의할 수 없음”이 되어 0 또는 NaN이 나옵니다.
이건 LLM Judge의 문제가 아니라 평가 설계의 문제입니다. 그리고 실무에서 정확히 이런 일이 벌어집니다. 자체 데이터로 검증할 때 긍정 샘플만 모아두면, 지표가 0으로 나와도 그게 “채점기가 나쁜 건지”, “데이터에 변동이 없는 건지” 구분할 수 없습니다. 검증 데이터에는 반드시 관련 있는 문서와 관련 없는 문서를 함께 넣어야 한다 — 이 교훈 하나만으로도 검증 단계를 거칠 가치가 충분합니다.
AgentCore ContextRelevance와 교차 검증
한 걸음 더 나아가, Amazon Bedrock AgentCore의 ContextRelevance 평가와도 비교해봤습니다. OpenSearch의 LLM Judge가 “엔진 레이어”에서 검색 결과를 채점한다면, AgentCore의 평가 기능은 “앱 레이어”에서 컨텍스트 관련성을 채점합니다. 서로 다른 층위의 두 도구가 얼마나 일관되게 움직이는지 확인하고 싶었습니다.

두 LLM 기반 평가 방식은 서로 0.64로 꽤 일관되게 동작했습니다. 각각을 Gold와 비교했을 때(0.41, 0.38)보다 서로 간의 상관이 더 높다는 점이 흥미롭습니다. 두 채점기 모두 사람과 조금씩 다르게 보긴 하지만, 비슷한 방향으로 다르게 본다는 뜻입니다. 이는 두 도구가 경쟁 관계가 아니라 보완 관계임을 시사합니다. 엔진 레이어는 OpenSearch에서, 앱 레이어는 AgentCore에서 각각 측정하고 교차검증하면 신뢰도를 한층 높일 수 있습니다.
결론적으로 LLM Judge는 완벽하진 않지만, 대략적인 경향과 순위를 파악하기에는 충분히 신뢰할 만합니다. 특히 Gold Qrels이 아예 없는 자체 데이터를 평가할 때, 이 검증을 한 번 거친 LLM Judge는 든든한 대안이 됩니다.
BM25 vs 시멘틱 vs 하이브리드 비교
채점기를 검증했으니, 이제 본격적으로 검색 방식을 비교할 차례입니다.
검색 방식 설명
비교할 세 가지 방식은 각각 접근이 완전히 다릅니다.
| 방식 | 설명 | OpenSearch 구현 |
|---|---|---|
| BM25 | 키워드가 얼마나 겹치는지로 순위를 매기는 전통적 방식 | multi_match 쿼리 |
| 시멘틱 (k-NN) | 문장을 벡터로 바꿔 “의미가 가까운 정도”로 순위를 매기는 방식 | knn 쿼리 + Cohere Embed |
| 하이브리드 | 위 둘의 결과를 결합해 양쪽 장점을 취하는 방식 | hybrid 쿼리 + RRF |
여기서 RRF(Reciprocal Rank Fusion)는 점수 체계가 서로 다른 두 검색 결과를 점수 대신 “순위”만으로 결합하는 기법입니다. 뒤의 “코드로 살펴보기”에서 동작 방식과 실제 쿼리를 자세히 다룹니다.
BM25는 “vaccine”이라는 단어가 문서에 몇 번 나오는지를 보고, 시멘틱 검색은 “vaccine”과 “immunization”이 의미상 같다는 것을 압니다. 하이브리드는 이 두 신호를 섞습니다. 그럼 어느 방식이 가장 좋을까요? 흔히 “하이브리드가 답”이라고들 하지만, 실제로는 데이터 성격에 따라 승자가 갈립니다. 뒤에서 데이터셋마다 결과가 어떻게 달라지는지 숫자로 보시게 됩니다.
SciFact 결과 (과학 논문 검색)
먼저 영어 과학 논문 데이터셋인 SciFact 결과입니다.
| 방식 | NDCG@10 | Recall@10 |
|---|---|---|
| BM25 | 0.560 | 0.699 |
| k-NN (시멘틱) | 0.693 | 0.812 |
| Hybrid (RRF) | 0.687 | 0.815 |
결과: 시멘틱 검색이 BM25보다 NDCG@10 기준 24% 높았습니다.
Recall@10도 같은 방향입니다. k-NN이 0.812로, 정답 문서의 81%를 상위 10개 안에 데려왔습니다. NDCG(순서)와 Recall(놓친 정답)이 둘 다 시멘틱의 손을 들어주는, 지표가 일관된 경우입니다. 하지만 늘 이렇게 깔끔하지는 않습니다. 뒤에서 두 지표가 전혀 다른 이야기를 하는 데이터셋을 보게 됩니다.
SciFact는 과학적 주장의 진위를 검증하는 데이터셋입니다. “COVID vaccine effective”라는 주장과 “SARS-CoV-2 immunization efficacy demonstrated”라는 논문 초록은 단어가 거의 겹치지 않지만 의미는 같습니다. 이런 데이터에서는 단어 매칭에 의존하는 BM25가 불리하고, 의미를 이해하는 시멘틱 검색이 확실히 앞섭니다.
여기서 눈여겨볼 지점이 있습니다. 하이브리드(0.687)가 순수 시멘틱(0.693)에 거의 근접했습니다. BM25 단독(0.560)이 한참 낮은데도, RRF로 결합한 하이브리드는 성능 좋은 시멘틱을 거의 그대로 따라갔습니다. Recall@10은 오히려 하이브리드가 0.815로 가장 높았습니다. 뒤에서 보겠지만, 이렇게 순위 기반으로 결합하는 RRF는 한쪽 검색이 약하더라도 다른 쪽의 강점을 잘 살려내는 경향이 있습니다.
데이터셋에 따라 결과가 다르다
여기서 이 글에서 가장 강조하고 싶은 지점이 나옵니다. 어떤 검색 방식이 최고인지는 데이터 성격에 따라 달라집니다. 시멘틱이 압도하는 데이터가 있는가 하면, 키워드가 더 강한 데이터도 있습니다.
성격이 다른 4개 데이터셋에서 같은 실험을 반복한 결과입니다.

데이터셋마다 승자가 다릅니다. SciFact와 TREC-COVID에서는 시멘틱(k-NN)이 앞섰고, 특히 TREC-COVID는 BM25(0.307)와 k-NN(0.789)의 격차가 2배가 넘습니다. 반면 NFCorpus와 Touche-2020에서는 하이브리드(RRF)가 1위를 차지했습니다.
여기서 앞서 말한 RRF의 성격이 드러납니다. 하이브리드가 이기지 못한 SciFact·TREC-COVID에서도 2위권에 바짝 붙어 있습니다. BM25가 한참 뒤처지는 데이터에서조차, 순위로 결합하는 RRF는 약한 쪽에 끌려 내려가지 않고 강한 쪽(시멘틱)을 거의 그대로 따라갔습니다. 즉 어느 데이터에서도 크게 뒤처지지 않는 것이 RRF 하이브리드의 강점입니다. 그중에서도 Touche-2020의 흐름이 특히 흥미롭습니다.
Touche-2020에서 하이브리드가 이긴 이유
Touche-2020은 “Should teachers get tenure?(교사에게 정년을 보장해야 하나?)”, “Is vaping safe?(전자담배는 안전한가?)” 같은 논쟁적 질문을 다루는 데이터셋입니다. 여기서는 BM25(0.778)가 시멘틱(0.756)을 앞섰고, 둘을 RRF로 결합한 하이브리드(0.796)가 최고 성능을 냈습니다. 이 데이터셋은 다른 셋과 달리 BM25가 시멘틱보다 강한, 흐름이 뒤집힌 경우입니다.
왜 이런 질문에서는 키워드 매칭이 강할까요?
- 특정 키워드 자체가 쟁점입니다. “tenure”, “vaping”, “safe” 같은 단어는 다른 말로 바꾸기 어려운, 논쟁의 핵심 용어입니다.
- 의미적 유사성이 오히려 독이 될 수 있습니다. 시멘틱 검색은 “vaping”과 “smoking”을 가깝게 보지만, 이 질문에서는 둘을 구분하는 게 중요합니다.
- 그래서 정확한 용어를 잡아내는 BM25가 시멘틱보다 강했습니다. 그리고 여기에 시멘틱의 문맥 이해를 순위로 더한 하이브리드(RRF)가, 둘 중 어느 하나보다도 높은 최고 성능을 냈습니다.
이건 Agent 서비스 설계에 그대로 시사점을 줍니다. 사용자가 에러 코드, 티켓 번호, 사번, 제품 모델명처럼 정확한 식별자로 검색하는 Agent라면, “시멘틱이 최신 기술이니까”라는 이유로 벡터 검색만 붙였다가는 오히려 품질이 떨어질 수 있습니다. Touche-2020이 딱 그런 데이터의 축소판입니다.
NDCG가 높아도 안심할 수 없다 — TREC-COVID의 Recall
여기서 앞서 예고한, 두 지표가 정반대 이야기를 하는 사례가 나옵니다. TREC-COVID에서 k-NN의 NDCG@10은 0.789로, 네 데이터셋 중 가장 높습니다. 이 숫자만 보면 “코로나 문헌 검색은 거의 완벽하다”고 말하고 싶어집니다. 그런데 같은 검색의 Recall@10을 보면 겨우 0.020, 즉 2%입니다.
| 방식 | NDCG@10 | Recall@10 |
|---|---|---|
| BM25 | 0.307 | 0.008 |
| k-NN (시멘틱) | 0.789 | 0.020 |
| Hybrid (RRF) | 0.622 | 0.015 |
왜 이런 일이 벌어질까요? TREC-COVID는 쿼리 하나에 정답 문서가 수백 개씩 달려 있는 데이터셋입니다. 상위 10개를 아무리 잘 정렬해도(그래서 NDCG는 높아도) 수백 개 정답 중 10개 남짓만 담을 수 있으니, Recall은 구조적으로 낮을 수밖에 없습니다. NDCG@10은 “상위 10개를 잘 줄 세웠는가”만 보기 때문에 이 사실을 전혀 드러내지 않습니다.
이게 바로 Recall을 함께 봐야 하는 이유입니다. 만약 이 검색을 RAG에 그대로 쓴다면, LLM은 존재하는 정답의 98%를 아예 보지도 못한 채 답을 만들게 됩니다. “이 주제에 대해 알려진 근거를 폭넓게 모아라” 같은 요구라면 이 검색은 실격입니다. NDCG@10만 보고 있었다면 이 치명적인 한계를 놓쳤을 겁니다. 지표 하나로는 결코 검색 품질을 다 말할 수 없습니다.
한국어(MIRACL)에서는 무슨 일이 벌어졌나
영어 데이터에서 검증한 방법이 한국어에서도 통할지 궁금해서 MIRACL 한국어 데이터셋으로도 돌려봤습니다. 결과는 이렇습니다.
| 방식 | NDCG@10 | Recall@10 |
|---|---|---|
| BM25 | 0.032 | 0.035 |
| k-NN (시멘틱) | 0.044 | 0.041 |
| Hybrid (RRF) | 0.045 | 0.040 |
절대 점수가 영어 데이터(0.5~0.8)와 비교하면 자릿수가 다릅니다. 하지만 이 숫자만 보고 “한국어 검색은 안 된다”고 결론 내리면 곤란합니다. MIRACL 한국어는 위키피디아 전체를 대상으로 하는 대규모·고난도 데이터셋이라 절대 점수 자체가 원래 낮게 나오는 벤치마크입니다. 여기서 중요한 건 절대값이 아니라 상대 순서이고, 한국어에서도 k-NN(0.044)이 BM25(0.032)를 앞섰으며 하이브리드(RRF)가 NDCG@10 0.045로 가장 높았습니다. 영어에서 본 것과 같은 순서입니다. 앞서 본 LLM Judge 검증에서도 한국어의 Spearman이 0.94로 매우 높았습니다. 즉 “의미 검색이 유리하고, LLM Judge로 순위를 잴 수 있다”는 방법론 자체는 언어를 건너 그대로 작동했습니다.
함정 둘, 한국어 BM25가 절반 토막 났던 이유
사실 이 표에 도달하기까지 한 번 크게 헤맸습니다. 처음 한국어를 색인했을 때 BM25 NDCG@10이 0.017밖에 안 나왔거든요. 위 표의 0.032와 비교하면 절반 수준입니다. k-NN(0.044)과의 격차도 지금보다 훨씬 컸고, “역시 한국어 키워드 검색은 답이 없나” 싶을 정도였습니다.
원인은 데이터가 아니라 형태소 분석기였습니다. 처음엔 별생각 없이 OpenSearch 기본 standard analyzer로 색인했는데, 이 analyzer는 한국어를 제대로 된 형태소가 아니라 글자·공백 단위로 어설프게 쪼갭니다. “전자담배는”을 통째로 한 토큰으로 잡아버려서, 사용자가 “전자담배”로 검색해도 매칭이 안 되는 식이죠. 조사·어미가 붙는 한국어에서는 이게 치명적입니다. 그래서 Nori 형태소 분석 플러그인을 붙여 다시 색인했더니, 같은 데이터·같은 쿼리인데 결과가 확 달라졌습니다.
| 방식 | standard analyzer | Nori 적용 | 변화 |
|---|---|---|---|
| BM25 | 0.017 | 0.032 | +87% |
| k-NN (시멘틱) | 0.044 | 0.044 | 변화 없음 |
Nori를 적용하자 BM25의 한국어 NDCG@10이 87% 뛰었습니다. 반대로 k-NN은 0.044 그대로였습니다. 임베딩은 텍스트를 벡터로 바꿔 검색하므로 analyzer의 영향을 받지 않기 때문입니다. 이 대비가 두 검색 방식의 성격을 선명하게 보여줍니다. 키워드 검색(BM25)은 언어별 전처리(토크나이징)에 크게 좌우되지만, 시멘틱 검색(k-NN)은 그 전처리로부터 자유롭습니다.
여기서 얻은 실전 교훈이 둘입니다. 첫째, 한국어로 BM25·하이브리드 검색을 쓸 거라면 Nori 같은 형태소 분석기는 선택이 아니라 필수입니다. 이걸 빼먹으면 검색 품질을 절반씩 깎아먹으면서도 “원래 한국어가 어렵다”고 오해하기 쉽습니다. 둘째, 평가 시스템이 없었다면 이 문제를 영영 몰랐을 것입니다. 0.017이라는 숫자가 있었기에 “이상하다, 왜 이렇게 낮지?”라고 파고들 수 있었고, Nori 적용 후 0.032로 오른 것을 숫자로 확인할 수 있었습니다. 측정이 곧 문제 발견의 출발점이라는, 이 글 전체의 주제가 한국어에서 다시 한번 확인된 셈입니다.
시사점
이 모든 결과를 한 장으로 요약하면 이렇습니다.
| 데이터 특성 | 추천 방식 |
|---|---|
| 동의어·paraphrase가 많음 (의미가 다양하게 표현됨) | 시멘틱 (k-NN) |
| 전문 용어·고유 키워드가 결정적 | 키워드 (BM25) |
| 어느 쪽이 유리한지 모르거나 섞여 있음 | 하이브리드 (RRF) |
결론: “시멘틱이 좋다”, “하이브리드가 답이다” 같은 일반화는 위험합니다. 최적의 검색 방식은 데이터 특성에 따라 다르며, 결국 직접 평가해봐야 알 수 있습니다. 다만 이번 4개 데이터셋에서 RRF 하이브리드는 어느 데이터에서도 크게 뒤처지지 않았고 절반에서는 1위였습니다. “무엇이 최적인지 아직 모르겠다”면 하이브리드(RRF)가 안전한 출발점이 될 수 있습니다. 그리고 그 “직접 평가”를 가능하게 하는 것이 바로 이 글에서 구축한 시스템입니다.
한 가지 덧붙이고 싶은 게 있습니다. 이런 NDCG 표만 보면 “그럼 시멘틱으로 가면 되겠네”라고 결론 내리기 쉽지만, 실제로 검색 방식을 정할 때 저울에 올리는 건 품질 하나가 아닙니다. 벡터 검색을 도입하는 순간 임베딩 비용(문서가 늘 때마다 다시 임베딩해야 합니다), 인덱스 크기(Cohere Embed v3는 1024차원이라 문서마다 수 KB짜리 벡터가 추가로 쌓입니다), 색인과 검색의 지연시간, 그리고 새 문서가 들어올 때마다 돌려야 하는 재색인 파이프라인까지 함께 떠안게 됩니다. 반면 BM25는 이런 추가 부담이 거의 없습니다. 그래서 BM25만으로도 NDCG 0.77이 나오는 Touche-2020 같은 데이터라면, 벡터 검색의 운영 부담을 감수할 만한 가치가 있는지 한 번 더 따져보게 됩니다. 품질 지표는 이 판단의 출발점이지 종착점이 아닙니다.
코드로 살펴보기
개념만으로는 손에 잡히지 않으니, 핵심 부분을 실제 코드로 보겠습니다. 전체를 배포하는 대신 이해에 필요한 스니펫만 발췌했습니다.
OpenSearch 하이브리드 검색 쿼리
하이브리드 검색의 핵심은 서로 다른 두 검색의 결과를 어떻게 합치느냐입니다. 앞서 언급한 RRF(Reciprocal Rank Fusion)를 여기서 자세히 보겠습니다. BM25 점수(스케일 제한 없음)와 k-NN 점수(0~1)처럼 점수 체계가 완전히 다른 두 검색을, 점수 대신 “순위”만으로 결합하는 기법입니다. 각 문서는 두 검색에서의 등수를 기반으로 1/(k+순위) 점수를 받고, 이를 합산해 최종 순위를 정합니다. 점수 정규화가 필요 없어 튜닝 부담이 적고, 최근 대부분의 검색 엔진이 하이브리드 기본값으로 채택하는 방식입니다.
OpenSearch에서는 이 RRF 결합을 검색 파이프라인(search pipeline)으로 선언해두고, 질의할 때 hybrid 쿼리로 두 검색을 함께 넘기기만 하면 됩니다. 순위 결합은 OpenSearch가 내부에서 처리합니다.
# 1) RRF 결합을 수행하는 검색 파이프라인을 한 번 등록해둔다
client.transport.perform_request("PUT", "/_search/pipeline/rrf-pipeline", body={
"description": "RRF hybrid",
"phase_results_processors": [
{"score-ranker-processor": {"combination": {"technique": "rrf"}}}
]
})
# 2) hybrid 쿼리로 BM25와 k-NN을 함께 넘기고, 파이프라인이 순위를 결합한다
def search_hybrid(client, index_name, query_text, embedding, k=10):
"""OpenSearch hybrid 쿼리 + RRF 파이프라인"""
body = {
"size": k,
"query": {
"hybrid": {
"queries": [
{"multi_match": {"query": query_text, "fields": ["title^2", "text"]}},
{"knn": {"embedding": {"vector": embedding, "k": k * 2}}}
]
}
}
}
return client.search(index=index_name, body=body,
params={"search_pipeline": "rrf-pipeline"})
RRF는 순위만 쓰기 때문에 가중치 같은 파라미터를 거의 조정하지 않아도 됩니다. 다만 하이브리드를 튜닝하는 다른 방법(점수 정규화 후 가중치 조절)도 있는데, 2부에서 이 가중치 방식과 Rerank를 함께 다룹니다.
LLM Judge 평가
앞서 이야기한 대로, 채점 로직을 직접 제어하기 위해 Bedrock의 Claude를 채점기로 호출했습니다. 쿼리와 문서를 함께 주고, 관련성을 0~1 사이 숫자 하나로만 답하도록 프롬프트를 좁게 설계한 것이 포인트입니다. 채점 결과를 파싱하기 쉽고, 모델이 장황하게 설명하며 벗어나는 것을 막아줍니다.
def llm_judge_relevance(bedrock_client, query, document):
"""Claude를 사용한 관련성 평가"""
prompt = f"""You are a search relevance judge.
Rate how relevant the document is to the query on a scale of 0 to 1.
Query: {query}
Document: {document[:1000]}
Respond with ONLY a single number between 0 and 1."""
response = bedrock_client.invoke_model(
modelId="us.anthropic.claude-haiku-4-5-20251001-v1:0",
body=json.dumps({
"anthropic_version": "bedrock-2023-05-31",
"max_tokens": 10,
"messages": [{"role": "user", "content": prompt}]
})
)
result = json.loads(response["body"].read())
return float(result["content"][0]["text"].strip())
대량 채점에는 지연시간과 비용이 낮은 Claude Haiku 계열이 잘 맞습니다. 앞서 검증에서 확인했듯, 이 정도 규모의 채점에서도 사람과 0.9대의 순위 상관을 보였습니다.
다만 이 스니펫이 간단해 보인다고 실제 규모에서도 간단한 건 아닙니다. 막상 돌려보면 세 가지가 발목을 잡습니다. 첫째는 비용입니다. 쿼리 200개에 결과 10개씩만 채점해도 한 번의 평가가 2,000회 호출인데, 검색 방식을 바꿔가며 실험을 반복하면 호출 수가 금세 수만 회로 불어납니다. 그래서 같은 (쿼리·문서) 쌍은 두 번 채점하지 않도록 결과를 캐싱해두는 게 사실상 필수입니다. 둘째는 파싱 실패입니다. “숫자 하나만 답하라(ONLY a single number)”고 못을 박아도 모델은 가끔 0.8입니다 같은 사족을 붙입니다. 정규식으로 숫자만 뽑아내고, 그래도 실패하면 재시도하거나 기본값으로 떨어뜨리는 방어 코드가 없으면 평가 도중에 파이프라인이 멈춥니다. 셋째는 처리량입니다. 2,000회를 순차로 호출하면 하염없이 느리기 때문에 병렬로 던져야 하는데, 이때 계정의 TPM(분당 토큰) 쿼터에 걸리지 않도록 동시성을 적절히 조절해야 합니다. 이 세 가지는 검색 평가를 한두 번 해보면 반드시 만나게 되는 벽이라, 처음부터 캐싱·재시도·동시성 제어를 염두에 두고 짜두는 편이 낫습니다.
정리 및 다음 글 예고
이 글에서 다룬 내용
- 검색 평가의 필요성: 정량적 측정 없이는 개선도 없습니다. 검색이 틀리면 그 위에 무엇을 쌓아도 결과가 틀립니다.
- LLM Judge 검증: 채점기를 무작정 믿지 말고 먼저 검증하세요. Gold Qrels 대비 65~75% 일치, 순위 상관 0.9대로 실무 활용이 가능했습니다. 단, 검증 데이터에는 긍정·부정 샘플을 반드시 함께 넣어야 합니다(SciFact Kappa=0의 교훈).
- 검색 방식 비교: 데이터 특성에 따라 최적 방식이 달라집니다. 4개 데이터셋 중 2개는 시멘틱(k-NN)이, 2개는 하이브리드(RRF)가 이겼습니다. 하이브리드는 이기지 못한 데이터셋에서도 2위권에 붙어, 어디서도 크게 뒤처지지 않았습니다.
- 지표 하나로는 부족하다: NDCG@10과 Recall@10을 함께 봐야 합니다. TREC-COVID처럼 NDCG는 0.79로 높은데 Recall@10은 2%라, 순위는 좋아도 정답의 대부분을 놓치는 경우가 있습니다. 특히 RAG에서는 Recall이 답변 품질의 상한을 결정합니다.
- 핵심 교훈: “시멘틱이 좋다”, “하이브리드가 답이다”는 일반화는 위험합니다. 직접 재봐야 합니다.
다음 글 예고
평가 시스템을 갖췄으니, 이제 개선할 차례입니다. 2부에서는 AI 기반 검색 품질 자동 개선을 다룹니다.
- Cohere Rerank 적용 효과 — 데이터셋마다 극과 극으로 갈립니다.
- 하이브리드 가중치 자동 튜닝 (Grid Search).
- Claude Code 같은 AI 코딩 어시스턴트를 활용한 “측정 → 개선 → 재측정” 자동화 루프.
- 운영 환경에서 검색 품질을 지속적으로 모니터링하는 방법.
흥미로운 예고편 하나. Rerank는 흔히 “붙이면 무조건 좋아진다”고 알려져 있지만, 실제로는 그렇지 않았습니다. SciFact에서는 +22% 개선되었지만, Touche-2020에서는 오히려 -11% 악화되었습니다. 왜 이런 정반대의 결과가 나왔는지, 그리고 이걸 어떻게 자동으로 판별하는지 2부에서 자세히 다루겠습니다.
지금 바로 시작해 보기
이 글의 방법을 직접 따라 해보고 싶다면 아래에서 시작할 수 있습니다.
- 하이브리드 검색을 처음 구성한다면 Amazon OpenSearch Service 하이브리드 검색 문서부터 살펴보세요.
- 채점기로 쓸 모델을 고르고 있다면 Amazon Bedrock에서 제공하는 모델을 확인해 보세요.
- 평가에 쓸 공개 벤치마크 데이터가 필요하다면 BEIR Benchmark에서 시작하는 것을 권합니다.
무엇보다, 지금 운영 중인 검색이나 AI Agent가 있다면 대표 쿼리 몇 개라도 골라 NDCG를 재보는 것부터 시작해 보시길 권합니다. 2부에서는 이렇게 측정한 결과를 바탕으로, 검색 품질을 자동으로 끌어올리는 방법을 이어서 다루겠습니다.