Elasticsearch 입문 22편 — RAG·Hybrid Search (Embedding·Reranking·Relevance Tuning)

2026-05-19Elasticsearch 입문에서 운영까지

Elasticsearch 입문 22편 RAG·Hybrid Search. Chunking·Embedding·Hybrid·RRF·Reranking·ELSER.

📚 Elasticsearch 입문에서 운영까지 · 22편 — RAG·Hybrid Search (Embedding·Reranking·Relevance Tuning)

이 글은 Elasticsearch 입문에서 운영까지 시리즈 38편 중 22편이에요. 21편(Vector Search)dense_vector 필드 + kNN 쿼리 같은 벡터 검색 자체 를 다뤘다면, 이번 편은 그 벡터 검색을 실제 RAG 시스템 으로 묶어 내는 한 단계 위 이야기예요. 임베딩 → 검색 → LLM 응답 흐름을 처음부터 끝까지 한 호흡에.

📚 학습 노트

이 글은 Elasticsearch 8.x 공식 docs 의 Search · ELSER · Inference 섹션을 참고해 한국어 학습 노트로 풀어쓴 자료예요.

RAG 는 *읽기만* 으로는 머리에 잘 안 박혀요. 작은 위키 100건이라도 직접 임베딩해서 BM25 + kNN 하이브리드로 한 번 돌려 보면 본문 흐름이 또렷해져요.

왜 22편이 RAG·Hybrid Search 인가

21편의 자리에서 한 발만 더 나가 보면 — 임베딩 벡터를 잘 저장하고 kNN 으로 가까운 문서 10개를 찾는다 까지는 됐어요. 그런데 실무에서 그것만으로 "좋은 RAG" 가 안 돼요. 왜냐하면 벡터만으로 잡히지 않는 키워드 자리 가 있고, kNN 상위 10개가 정말 답에 맞는지 한 번 더 거르는 자리도 필요하기 때문이에요.

이번 편이 다루는 게 그 두 자리. Hybrid Search (BM25 텍스트 + kNN 벡터 결합) 가 벡터만으로 못 잡는 키워드 매칭 자리를 잡아 주고, Rerankingretrieval 상위 N 을 LLM 호출 직전에 한 번 더 정렬 해 줘요. 그 위에 Relevance Tuning (boost·function_score·LTR) 과 ELSER (Elastic 의 sparse 임베딩) 까지 다루면, RAG 시스템 한 채 가 한 편 안에 들어와요.

RAG 파이프라인 — 1·2·3단계 흐름

RAG 는 약자 그대로 Retrieval-Augmented Generation, 검색으로 보강한 LLM 응답 이에요. LLM 이 모든 지식을 가진 게 아니라 최신 사내 문서·매뉴얼·위키 같은 외부 자료 를 검색해서 답하게 하는 패턴. 흐름은 세 단계.

1단계 — 인덱싱 (offline)

원본 문서 → chunking (적당한 크기로 자르기) → embedding (각 청크를 벡터로 변환) → Elasticsearch 저장 (텍스트 + 벡터 같이). 이 단계는 사용자 질의가 오기 전에 배치성 으로 한 번 돌아가요.

원본 PDF (100p) → chunk (500자 x 200개)
                → embed(chunk) → vector[1024]
                → ES bulk index
                  { text: "...", vector: [...] }

2단계 — Retrieval (online, 빠름)

사용자 질의가 들어오면 — 질의를 같은 임베딩 모델 로 벡터로 변환하고, ES 에 BM25 + kNN 하이브리드 로 던져서 상위 N 개 청크를 가져와요. 여기서 응답 속도가 가장 중요. 수십 ms 안에 끝나야 사용자 경험이 살아요.

3단계 — Generation (online, 느림)

retrieval 결과 N 개를 프롬프트의 context 로 박고, LLM (GPT·Claude·Gemini) 에 질의 + context 를 묶어 보내요. LLM 이 그 context 안에서만 답하도록 system prompt 로 가둬요. 이 단계가 수 초 단위라 시스템 전체 응답 시간의 대부분.

질의 → embed(질의) → ES hybrid search (BM25+kNN, RRF)
                    → top 20 청크
                    → reranker → top 5 청크
                    → LLM(질의 + top 5 context)
                    → 사용자 응답

세 단계가 깔끔하게 나뉘어 있어서 — 인덱싱은 배치, retrieval 은 ES, generation 은 LLM 으로 책임이 명확히 갈라져요. RAG 시스템을 짤 때 가장 먼저 그릴 그림이에요.

Chunking 전략 — 한국어 함정 포함

원본 문서를 그대로 임베딩하지 않고 적당히 자르는 이유 두 가지 — 임베딩 모델의 입력 길이 한도 (보통 512~8192 토큰) 와 retrieval 정밀도 (긴 글 1개보다 짧은 글 10개가 더 잘 매칭). 자르는 방식이 여러 가지예요.

전략 (1) — 고정 길이 chunking

가장 단순. "500자 단위로 자른다" 같은 룰. 빠르고 구현이 쉽지만, 문장 중간이 잘려서 retrieval 정확도가 떨어져요. 프로토타입용.

전략 (2) — 문장·문단 단위 chunking

문장 종결부호(.·?·!) 나 빈 줄로 자른 뒤, 합쳐서 대략 N자에 도달할 때까지 묶어요. 문맥이 잘 안 잘려서 retrieval 품질이 올라가요. 실무 1순위 권장.

전략 (3) — overlap 추가

청크 간에 50~100자 overlap 을 줘요. 청크 경계에 걸친 문장이 두 청크 모두에 들어가서 retrieval 누락이 줄어요. 일반 권장.

한국어 chunking 함정

영어는 띄어쓰기·문장부호가 또렷해서 자르기 쉬워요. 한국어는 — 종결어미(-요·-다·-습니다) 변형이 많고, 한 문장이 수십 어절 까지 길어지는 일이 잦아요. 그래서 "500자 고정" 같은 단순 룰이 한국어에서 영어보다 더 잘 깨져요.

권장 — kss (Korean Sentence Splitter) 나 konlpy문장 분리 함수를 써서 문장 단위로 끊고, N자 도달까지 모으기 패턴. 그리고 표·코드 블록 은 의미 단위로 한 덩어리에 묶어요 — 자르면 의미가 깨져요.

Embedding 모델 — 옵션 4가지

청크를 벡터로 바꾸는 모델 선택이 RAG 전체 품질의 절반 을 좌우해요. 옵션이 크게 네 갈래.

(1) OpenAI text-embedding-3

산업 표준 — text-embedding-3-small (1536차원, $0.02/1M tokens) 과 text-embedding-3-large (3072차원, $0.13/1M) 두 가지. 한국어 포함 100+ 언어 OK. 가장 안정.

(2) Cohere embed-v3

다국어·한국어 품질 우수. embed-multilingual-v3.0 이 1024차원. MMR (Maximal Marginal Relevance) 같은 retrieval 기능과 잘 어울려요.

(3) BGE·E5 (오픈소스)

BAAI/bge-m3 가 한국어 포함 100+ 언어를 8192 토큰까지 처리하는 오픈소스 SOTA 급. 자체 GPU 서버에서 돌리면 OpenAI 대비 비용이 0 에 수렴.

(4) 한국어 특화 — KoSimCSE·KoBERT

KoSimCSE-roberta 같은 한국어 전용 모델이 한국어만 보면 다국어 모델보다 미세하게 우위. 단, 영어 섞인 사내 문서 에선 다국어 모델이 더 안정.

Inference Endpoint vs 외부 호출

Elasticsearch 8.11+ 의 inference endpoint모델 호출 자체를 ES 안으로 끌어와요. 청크 색인 시 텍스트만 넣으면 ES 가 OpenAI·Cohere·자체 모델 API 를 부르고 벡터를 자동 저장. 외부 호출 코드를 짤 필요가 없어요. 운영 권장 패턴.

PUT _inference/text_embedding/openai-emb
{
  "service": "openai",
  "service_settings": {
    "api_key": "sk-...",
    "model_id": "text-embedding-3-small"
  }
}

이렇게 등록해 두면, ingest pipeline 에서 inference processor 로 자동 벡터화 — 24편(Ingest Pipeline) 에서 깊이.

Hybrid Retrieval — BM25 + kNN + RRF

벡터 검색만으로 완벽한 RAG 가 안 되는 이유 — 고유명사·약어·숫자 같은 문자 매칭이 결정적인 자리 를 벡터가 못 잡아요. 예를 들어 "GPT-4 Turbo" 라는 정확한 모델명을 찾을 때, 벡터는 "GPT-4o"·"GPT-4" 같은 유사 모델도 같이 끌고 와요. 반면 BM25 (전통 풀텍스트 점수) 는 정확한 토큰 매칭 에 강해요.

그래서 둘을 묶은 Hybrid Search 가 표준. ES 8.x 가 Reciprocal Rank Fusion (RRF) 알고리즘으로 이 결합을 native 지원해요.

RRF (Reciprocal Rank Fusion) 알고리즘

여러 retrieval 결과를 점수가 아닌 순위 로 결합해요. 각 결과의 i번째 문서에 1/(k + rank_i) 점수를 주고 합산. k 는 일반적으로 60.

문서 A: BM25 rank 1, kNN rank 5
       → 1/(60+1) + 1/(60+5) = 0.0164 + 0.0154 = 0.0318

문서 B: BM25 rank 3, kNN rank 1
       → 1/(60+3) + 1/(60+1) = 0.0159 + 0.0164 = 0.0323

문서 B 가 RRF 점수 높음 → 1위

점수 정규화 가 필요 없는 게 RRF 의 장점. BM25 점수가 0~수십이고 kNN cosine 점수가 0~1 이라 그대로 더하면 한쪽이 묻혀요. 순위만 보면 그 문제가 사라져요.

ES 8.x Hybrid Search 예시

GET /docs/_search
{
  "retriever": {
    "rrf": {
      "retrievers": [
        {
          "standard": {
            "query": {
              "match": { "text": "Elasticsearch RAG" }
            }
          }
        },
        {
          "knn": {
            "field": "vector",
            "query_vector_builder": {
              "text_embedding": {
                "model_id": "openai-emb",
                "model_text": "Elasticsearch RAG"
              }
            },
            "k": 50,
            "num_candidates": 100
          }
        }
      ],
      "rank_window_size": 50,
      "rank_constant": 60
    }
  },
  "size": 10
}

retriever API 가 8.8 부터 들어온 표준. rank_constant 가 위 식의 k. 가중치를 조정하고 싶으면 별도 weight 파라미터 가 아니라 각 retriever 내부의 boost 로 조절.

Reranking — Cross-encoder 2차 정렬

Hybrid retrieval 로 상위 50개를 가져왔다고 해도, 정말 답에 맞는 5개 가 그 50개 안에서 1~5위에 있다는 보장이 없어요. 그래서 LLM 호출 직전에 한 번 더 정렬하는 단계가 reranking.

bi-encoder vs cross-encoder

임베딩 모델은 bi-encoder — 질의와 문서를 따로 벡터로 만든 뒤 cosine 으로 비교. 빠르지만 정밀도 한계가 있어요.

reranker 는 cross-encoder — 질의와 문서를 함께 모델에 넣고 직접 관련도 점수 를 뽑아요. 정밀도가 훨씬 높지만 느려요 (질의·문서 쌍마다 모델 1회 호출).

그래서 패턴이 — bi-encoder 로 50개 retrieve → cross-encoder 로 top 5 rerank. 빠른 retrieve + 정확한 rerank 의 조합.

옵션 — Cohere·BGE·Voyage

  • Cohere Rerank v3rerank-multilingual-v3.0 가 한국어·100+ 언어. 산업 표준.
  • BGE RerankerBAAI/bge-reranker-v2-m3 오픈소스. 자체 GPU 서버.
  • Voyage rerank-2 — 한국어 품질 우수, 토큰당 가격 저렴.

ES 8.x Reranking 통합

ES 가 reranker 도 inference endpoint 로 묶었어요. retriever 안에 text_similarity_reranker 를 박아 두면 retrieval 결과를 ES 가 자동으로 rerank.

GET /docs/_search
{
  "retriever": {
    "text_similarity_reranker": {
      "retriever": {
        "rrf": { ... }
      },
      "field": "text",
      "rank_window_size": 50,
      "inference_id": "cohere-rerank",
      "inference_text": "Elasticsearch RAG 어떻게 짜요"
    }
  }
}

LLM 호출 직전 단계를 ES 한 쿼리 안에서 끝낼 수 있어요 — 별도 reranker 서버를 띄울 필요 X.

Relevance Tuning — boost·function_score·LTR

검색 결과가 "기술적으로는 맞는데 사용자 의도엔 부족" 한 자리를 잡는 단계가 Relevance Tuning. RAG 도 결국 검색이라 이 단계가 통째로 적용돼요.

boost — 필드 가중치

title 매치가 body 매치보다 더 중요하면 boost 로 가중. Hybrid retrieval 의 BM25 쪽 에서 가장 흔히 써요.

"query": {
  "multi_match": {
    "query": "RAG",
    "fields": ["title^3", "body"]
  }
}

function_score — 점수 가공

최신 문서를 우대 하거나 클릭률 높은 문서를 우대 하는 같은 자리. 점수 함수를 조합해서 retrieval 점수 × 비즈니스 신호 로 합쳐요.

"query": {
  "function_score": {
    "query": { ... },
    "functions": [
      { "gauss": { "published_at": { "scale": "30d" } } },
      { "field_value_factor": { "field": "ctr", "modifier": "log1p" } }
    ],
    "score_mode": "sum",
    "boost_mode": "multiply"
  }
}

LTR (Learning to Rank) — ES 8.12+ native

수동 boost 튜닝의 한계 를 넘으려면 클릭 로그 + 라벨 데이터ranking 모델을 학습 시켜요. XGBoost 같은 모델이 N개 feature (BM25 score · kNN score · CTR · 최신성 · 인기도) 를 받아 최종 순위 를 예측. ES 8.12+ 가 learning_to_rank rescorer 로 native 지원.

평가 메트릭 4가지

튜닝의 효과 를 숫자로 확인하는 메트릭 — recall·precision·MRR·NDCG.

  • Recall@k = top k 안에 정답 문서 비율. RAG 는 recall 이 결정적.
  • Precision@k = top k 중 관련 있는 문서 비율. UI 에 검색 결과 보여 줄 때.
  • MRR (Mean Reciprocal Rank) = 첫 정답의 역순위 평균. "정답이 얼마나 빨리 나오나".
  • NDCG (Normalized DCG) = 순위별 가중치까지 반영한 정밀 메트릭. 평가 표준.

평가 데이터는 질의 100~500개 + 각 질의에 대한 정답 문서 목록 으로 시작. 클릭 로그 → 자동 라벨 도 일반 패턴.

ELSER (8.11+) — Elastic 자체 sparse 임베딩

dense vector (1024차원 같은 모든 차원이 값을 가진 vector) 와 다른, sparse vector 패러다임을 Elastic 이 자체로 들고 나왔어요 — ELSER (Elastic Learned Sparse Encoder).

sparse vector 의 특징

문서를 수만 개 토큰의 score 맵 으로 표현해요. {"검색": 1.8, "RAG": 2.1, "벡터": 1.5, ...} 같은 형태. 대부분의 토큰은 0 이고, 문서에 의미 있는 수십~수백 토큰만 값을 가져요.

dense vs sparse — 비교

Dense (text-embedding-3 등) Sparse (ELSER)
차원 384~3072 고정 30,000+ (sparse)
모델 학습 데이터 필요 불필요 (즉시 사용)
검색 방식 kNN (cosine) 전통 inverted index
해석 가능성 낮음 (벡터가 의미 X) 높음 (어느 토큰이 매칭)
한국어 품질 모델 따라 다름 영어 위주 (한국어 약함)

ELSER 의 가장 큰 매력 — 모델 학습 데이터가 불필요 해서, "검색 품질 끌어올리고 싶은데 라벨 데이터가 없다" 하는 회사가 깔자마자 풀텍스트보다 나은 결과를 보장.

ELSER 사용 예시

PUT _inference/sparse_embedding/elser-v2
{
  "service": "elser",
  "service_settings": { "num_allocations": 1, "num_threads": 1 }
}

POST docs/_doc/1
{ "text": "Elasticsearch RAG 어떻게 짜요" }

# 이 후 ingest pipeline 에서 ELSER 가 자동으로
# text_expansion 필드를 채워 넣음

GET docs/_search
{
  "query": {
    "text_expansion": {
      "ml.tokens": {
        "model_id": "elser-v2",
        "model_text": "Elasticsearch RAG"
      }
    }
  }
}

한국어가 약하다는 한계가 또렷해서 — 한국어 RAG 는 Cohere·BGE 다국어 dense embedding 이 1순위, 영어 RAG 는 ELSER + dense hybrid 가 표준.

자주 만나는 사고 7가지

사고 1 — chunk 너무 큼

원인 — 한 청크가 2,000자가 넘으면 retrieval 정밀도 가 떨어져요. 임베딩 한 벡터가 너무 많은 주제 를 한 번에 표현해서 어느 질의에도 미적지근하게 매칭.

해결300~800자 가 한국어 권장 범위. 한 청크가 한 주제 만 다루도록 자르고, overlap 100자 추가.

사고 2 — 임베딩 모델 불일치

원인 — 인덱싱은 OpenAI text-embedding-3-small (1536차원) 으로 했는데 retrieval 은 Cohere embed-v3 (1024차원) 으로 하면 차원도 안 맞고, 차원이 같아도 벡터 공간이 달라서 검색이 망가져요.

해결문서 임베딩과 질의 임베딩은 반드시 같은 모델. 모델 바꾸려면 전체 재색인 필요. mapping dense_vector.dims 에 차원 박아 두면 불일치 시 에러로 잡힘.

사고 3 — RRF 가중치 미튜닝

원인 — RRF rank_constant 기본 60 그대로 쓰면 BM25 와 kNN 영향력이 거의 동등해서, 키워드가 결정적인 도메인 (법률·의료) 에서 BM25 가 묻혀요.

해결고유명사·약어 비중이 높으면 BM25 retriever 의 boost 를 높이거나, 별도 BM25-only 결과를 위로 박는 linear retriever 도 ES 8.13+ 에서 가능.

사고 4 — 평가 데이터 없음

원인"체감으로 좋아진 것 같다" 만 보고 튜닝 결정을 내려서, 다음 사용자 사고가 났을 때 어디가 망가졌는지 검증이 안 돼요.

해결100~500개 평가 질의 + 정답 셋 을 일찍 만들어요. ES _rank_eval API 가 NDCG·MRR 자동 계산. 매 튜닝 전후로 돌려서 수치 비교 가 표준.

사고 5 — context window 초과

원인 — retrieval top 10 청크를 그대로 LLM 에 넣었더니 총 30,000 토큰 이 돼서 모델 context window (예 — 8K) 를 넘어 API 에러 발생.

해결top N 을 청크 길이까지 고려해 동적으로 줄이고, reranker 로 상위 5개로 압축. LLM 의 long context 모델 (Claude 200K · GPT-4 Turbo 128K) 을 쓰는 것도 옵션이지만 비용·지연시간 증가.

사고 6 — 한국어 형태소 분석 누락

원인 — 한국어 텍스트를 standard analyzer 로 색인하면 BM25 쪽이 어절 단위로만 잡혀서 Hybrid 의 BM25 contribution 이 거의 0. RRF 결과가 순수 kNN 결과 와 같아짐.

해결nori_tokenizer + 사용자 사전. 11편(Korean Analyzer) 에서 깊이.

사고 7 — RAG 응답에 출처 없음

원인 — LLM 이 retrieval context 만 보고 답하라고 했는데, 어느 청크에서 가져왔는지 표시 안 하면 사용자가 신뢰 검증 불가 하고 hallucination 책임 추적 불가.

해결 — system prompt 에 "답 끝에 출처 청크 ID 를 [1][2] 형태로 인용하라" 박고, 응답 후처리에서 청크 ID → 원본 문서 URL 매핑. 신뢰성과 운영 모두 결정적.

운영 권장 패턴 4가지

운영 시작 전 RAG 의 평가 셋 100~500개 부터 만들어요. 평가 셋 없이 시작하면 튜닝 효과 검증 불가 + 회귀 사고 추적 불가 가 같이 와요.

Hybrid retrieval + Reranker 를 처음부터 표준으로 깔아요. kNN 만 으로 시작하면 키워드 매칭이 결정적인 사고 가 났을 때 대규모 재설계 비용이 커요. ES 8.x retriever API + text_similarity_reranker한 쿼리 안에서 두 단계가 끝나는 게 큰 매력.

Inference endpoint 로 임베딩·reranker 호출을 ES 안에 묶어요. 외부 호출 코드를 앱에 두면 재시도·rate limit · 에러 핸들링 이 앱마다 따로 짜야 해요. ES inference endpoint 가 그 자리를 통째로 흡수.

평가 자동화 를 CI 에 박아 둡니다. mapping·analyzer·embedding 모델·reranker 모델·boost 값 중 하나가 바뀔 때마다 평가 셋이 자동으로 돌고 NDCG 변동 이 PR 코멘트로 박혀요. 튜닝 회귀 사고 의 90% 가 여기서 잡혀요.

시험 직전 한 번 더 — 압축 노트

  • RAG = Retrieval-Augmented Generation. 인덱싱(offline) → retrieval → generation(LLM) 3단계.
  • Chunking — 300~800자 + overlap 100자. 한국어는 kss 같은 문장 분리기 권장.
  • Embedding — OpenAI text-embedding-3 · Cohere embed-v3 · BGE-m3 · KoSimCSE. 인덱싱·질의 동일 모델 필수.
  • Hybrid Search = BM25 + kNN 결합. RRF (Reciprocal Rank Fusion) 이 표준 알고리즘 (1/(k+rank) 합산, k=60).
  • ES 8.x retriever APIrrf retriever 가 BM25 + kNN 결합을 한 쿼리로.
  • Reranking — cross-encoder 로 상위 N 을 2차 정렬. Cohere Rerank · BGE Reranker · Voyage.
  • Inference endpoint — 임베딩·reranker 호출을 ES 안으로 흡수. ingest pipeline 과 묶임.
  • Relevance Tuning — boost · function_score · LTR (ES 8.12+ native).
  • 평가 메트릭 — Recall@k · Precision@k · MRR · NDCG. ES _rank_eval API.
  • ELSER — Elastic 자체 sparse embedding. 학습 데이터 불필요, 영어 우수 한국어 약함.
  • 7대 사고: chunk 너무 큼 · 모델 불일치 · RRF 미튜닝 · 평가 데이터 없음 · context window 초과 · 한국어 분석기 누락 · 출처 표시 없음.

시리즈 다른 편

  • 이전 글 = 21편 Vector Search — dense_vector · kNN · HNSW
  • 다음 글 = 23편 Bulk API — 대량 색인 · 1만 건 색인 패턴
  • 11편 = Korean Analyzer — Nori · mecab-ko · 사용자 사전
  • 12편 = Search API — Query DSL 전수
  • 14편 = Full-text Query — match · match_phrase · multi_match
  • 18편 = Aggregation Pipeline — moving_avg · cumulative_sum · derivative
  • 24편 = Ingest Pipeline — inference processor 포함
  • 32편 = Spring Data Elasticsearch — Repository · Template · POJO
  • 38편 = 시리즈 마무리 — 결정 트리 · 체크리스트 · 자격증

한 줄 정리 — RAG 는 인덱싱·retrieval·generation 3단계로 나뉘고, ES 8.x 가 Hybrid (BM25+kNN+RRF) + Reranker + Inference endpoint 를 한 쿼리 안에 묶어 줘요. 평가 셋·출처 표시·임베딩 모델 일치 — 세 가지를 처음부터 박아 두면 튜닝 회귀 사고 의 90% 가 사라져요.

※ 이 포스팅은 쿠팡 파트너스 활동의 일환으로, 이에 따른 일정액의 수수료를 제공받습니다.

답글 남기기

error: Content is protected !!