본문으로 건너뛰기
Kreath Archive
TechProjectsBooksAbout
TechProjectsBooksAbout

내비게이션

  • Tech
  • Projects
  • Books
  • About
  • Tags

카테고리

  • AI / ML
  • 웹 개발
  • 프로그래밍
  • 개발 도구

연결

  • GitHub
  • Email
  • RSS
© 2026 Kreath Archive. All rights reserved.Built with Next.js + MDX
홈TechProjectsBooksAbout
//
  1. 홈
  2. 테크
  3. 5장: 랭킹 모델 — Bi-encoder와 Cross-encoder
2026년 2월 11일·AI / ML·

5장: 랭킹 모델 — Bi-encoder와 Cross-encoder

Bi-encoder와 Cross-encoder의 구조적 차이, Elastic Rerank의 DeBERTa v3 모델, 점수 퓨전(RRF), 학습 랭킹(LTR)을 심층적으로 다룹니다.

14분761자8개 섹션
searchai
공유
ai-search5 / 11
1234567891011
이전4장: 쿼리 이해와 확장다음6장: Elasticsearch AI 검색 통합

학습 목표

  • Bi-encoder와 Cross-encoder의 구조적 차이와 트레이드오프를 이해합니다.
  • Cross-encoder 기반 리랭킹의 작동 원리와 구현 방법을 학습합니다.
  • Elastic Rerank(DeBERTa v3)의 특징과 성능 향상 효과를 파악합니다.
  • RRF(Reciprocal Rank Fusion)를 활용한 점수 퓨전 기법을 이해합니다.
  • LTR(Learning to Rank)의 기본 개념과 적용 방법을 학습합니다.

Bi-encoder vs Cross-encoder

검색 랭킹에서 가장 중요한 구분은 Bi-encoder와 Cross-encoder의 차이입니다. 두 모델은 같은 Transformer 아키텍처를 기반으로 하지만, 입력을 처리하는 방식이 근본적으로 다릅니다.

Bi-encoder — 독립 인코딩

Bi-encoder는 쿼리와 문서를 독립적으로 인코딩합니다. 각각이 별도의 벡터로 변환된 뒤, 코사인 유사도나 내적으로 관련성을 측정합니다.

  • 장점: 문서 벡터를 미리 계산 가능, 밀리초 단위 검색
  • 단점: 쿼리-문서 간의 상호작용(토큰 수준의 관계)을 포착하지 못함

Cross-encoder — 결합 인코딩

Cross-encoder는 쿼리와 문서를 하나의 입력으로 결합하여 인코더에 통과시킵니다. Transformer의 self-attention 메커니즘이 쿼리 토큰과 문서 토큰 간의 상호작용을 직접 모델링합니다.

  • 장점: 토큰 수준의 세밀한 관련성 판단, 높은 정확도
  • 단점: 모든 쿼리-문서 쌍을 실시간 처리, 속도 느림
cross_encoder_example.py
python
from sentence_transformers import CrossEncoder
 
# Cross-encoder 모델 로드
model = CrossEncoder("cross-encoder/ms-marco-MiniLM-L-12-v2")
 
query = "파이썬 비동기 프로그래밍"
documents = [
    "Python asyncio를 활용한 비동기 웹 서버 구축 가이드",
    "JavaScript Promise와 async/await 패턴",
    "파이썬 동시성: threading vs multiprocessing 비교",
    "Node.js 이벤트 루프의 작동 원리",
]
 
# 쿼리-문서 쌍의 관련성 점수 계산
pairs = [[query, doc] for doc in documents]
scores = model.predict(pairs)
 
for doc, score in sorted(zip(documents, scores), key=lambda x: x[1], reverse=True):
    print(f"{score:.4f} | {doc}")

성능 비교

항목Bi-encoderCross-encoder
처리 방식독립 인코딩결합 인코딩
속도 (1000 문서)약 5ms약 2000ms
정확도 (NDCG@10)0.65-0.750.80-0.90
사전 계산가능 (문서 벡터)불가능
적합한 단계1차 검색 (리트리벌)2차 정밀 평가 (리랭킹)
Info

실전에서는 Bi-encoder로 수백만 문서에서 상위 100-200개를 빠르게 추출한 뒤, Cross-encoder로 이 후보들을 정밀 재평가하는 2단계 구조를 사용합니다. 이를 "retrieve-then-rerank" 패턴이라 합니다.


Cross-encoder 리랭킹 구현

리랭킹(Reranking)은 1차 검색 결과의 순위를 재조정하는 과정입니다. Cross-encoder를 리랭커로 사용하는 전체 파이프라인을 구현해 보겠습니다.

reranking_pipeline.py
python
from sentence_transformers import SentenceTransformer, CrossEncoder
import numpy as np
 
class TwoStageSearchPipeline:
    """2단계 검색 파이프라인: Bi-encoder 리트리벌 + Cross-encoder 리랭킹"""
 
    def __init__(
        self,
        retriever_model: str = "intfloat/multilingual-e5-large",
        reranker_model: str = "cross-encoder/ms-marco-MiniLM-L-12-v2",
    ):
        self.retriever = SentenceTransformer(retriever_model)
        self.reranker = CrossEncoder(reranker_model)
        self.doc_embeddings = None
        self.documents = []
 
    def index(self, documents: list[str]):
        """문서 인덱싱 (Bi-encoder 벡터 생성)"""
        self.documents = documents
        texts = [f"passage: {doc}" for doc in documents]
        self.doc_embeddings = self.retriever.encode(texts, normalize_embeddings=True)
 
    def search(self, query: str, top_k: int = 10, rerank_k: int = 100) -> list[dict]:
        """2단계 검색: 리트리벌 후 리랭킹"""
        # 1단계: Bi-encoder 리트리벌
        query_vec = self.retriever.encode(
            f"query: {query}", normalize_embeddings=True
        )
        similarities = self.doc_embeddings @ query_vec
        top_indices = np.argsort(similarities)[::-1][:rerank_k]
        candidates = [(i, self.documents[i]) for i in top_indices]
 
        # 2단계: Cross-encoder 리랭킹
        pairs = [[query, doc] for _, doc in candidates]
        rerank_scores = self.reranker.predict(pairs)
 
        # 최종 결과 정렬
        results = []
        for (idx, doc), score in zip(candidates, rerank_scores):
            results.append({
                "doc_id": idx,
                "text": doc,
                "retrieval_score": float(similarities[idx]),
                "rerank_score": float(score),
            })
 
        results.sort(key=lambda x: x["rerank_score"], reverse=True)
        return results[:top_k]

Elastic Rerank — DeBERTa v3 기반 리랭킹

Elasticsearch 8.14부터 도입된 Elastic Rerank는 DeBERTa v3 기반의 Cross-encoder 리랭킹 모델입니다. Elasticsearch의 Inference API를 통해 별도의 ML 인프라 없이 리랭킹을 적용할 수 있습니다.

성능 향상 효과

Elastic에서 공개한 벤치마크에 따르면 Elastic Rerank는 다음과 같은 성능 향상을 보여줍니다.

  • 일반 검색 태스크: BM25 대비 약 40% NDCG 향상
  • QA(질의응답) 데이터셋: 최대 90% NDCG 향상
  • 도메인 특화 태스크: 30-60% 범위의 향상

Elasticsearch에서의 설정

rerank_inference_endpoint.json
json
{
  "service": "elasticsearch",
  "service_settings": {
    "num_allocations": 1,
    "num_threads": 1,
    "model_id": ".rerank-v1"
  }
}
rerank_search_query.json
json
{
  "retriever": {
    "text_similarity_reranker": {
      "retriever": {
        "standard": {
          "query": {
            "match": {
              "content": "벡터 데이터베이스 성능 비교"
            }
          }
        }
      },
      "field": "content",
      "inference_id": "my-rerank-model",
      "inference_text": "벡터 데이터베이스 성능 비교",
      "rank_window_size": 100
    }
  },
  "size": 10
}
Tip

Elastic Rerank의 rank_window_size는 리랭킹 대상 문서 수를 결정합니다. 값이 클수록 정확하지만 지연 시간이 증가합니다. 일반적으로 50-200 사이의 값이 적절합니다.


RRF — 점수 퓨전

**RRF(Reciprocal Rank Fusion)**는 여러 검색 결과 목록을 하나로 병합하는 알고리즘입니다. 각 문서의 순위 역수를 합산하여 최종 점수를 계산합니다. 서로 다른 스케일의 점수를 가진 검색 결과를 안정적으로 결합할 수 있다는 것이 핵심 장점입니다.

RRF 수식

각 문서 d의 RRF 점수는 다음과 같이 계산됩니다.

RRF(d) = sum(1 / (k + rank_i(d))) (모든 검색 결과 목록 i에 대해)

여기서 k는 상수(기본값 60)이며, rank_i(d)는 i번째 결과 목록에서 문서 d의 순위입니다.

rrf_fusion.py
python
def reciprocal_rank_fusion(
    ranked_lists: list[list[str]],
    k: int = 60,
) -> list[tuple[str, float]]:
    """
    RRF 점수 퓨전
    ranked_lists: 여러 검색 결과 목록 (각 목록은 문서 ID 순서)
    k: 스무딩 상수 (기본값 60)
    """
    rrf_scores: dict[str, float] = {}
 
    for ranked_list in ranked_lists:
        for rank, doc_id in enumerate(ranked_list, start=1):
            if doc_id not in rrf_scores:
                rrf_scores[doc_id] = 0.0
            rrf_scores[doc_id] += 1.0 / (k + rank)
 
    # 점수 기준 내림차순 정렬
    sorted_results = sorted(rrf_scores.items(), key=lambda x: x[1], reverse=True)
    return sorted_results
 
# BM25 결과와 시맨틱 검색 결과 병합
bm25_results = ["doc_A", "doc_B", "doc_C", "doc_D", "doc_E"]
semantic_results = ["doc_C", "doc_A", "doc_F", "doc_B", "doc_G"]
 
fused = reciprocal_rank_fusion([bm25_results, semantic_results])
for doc_id, score in fused[:5]:
    print(f"{doc_id}: {score:.6f}")

RRF vs 선형 보간

방법장점단점
RRF점수 스케일 무관, 파라미터 적음순위만 사용 (점수 정보 손실)
선형 보간점수 차이 반영스케일 정규화 필요, 가중치 튜닝
Info

Elasticsearch 8.x에서는 RRF를 하이브리드 검색의 기본 퓨전 방식으로 제공합니다. 6장에서 Elasticsearch의 RRF 구현을 자세히 다룹니다.


LTR — 학습 랭킹

**LTR(Learning to Rank, 학습 랭킹)**은 머신러닝 모델을 학습시켜 검색 결과의 순위를 결정하는 접근법입니다. BM25 점수, 시맨틱 유사도, 문서 인기도, 최신성 등 다양한 특성(feature)을 입력으로 받아 최적의 순위를 예측합니다.

LTR의 세 가지 접근법

  • Pointwise: 각 문서의 관련성 점수를 독립적으로 예측합니다. 단순하지만 문서 간 상대적 순서를 직접 고려하지 않습니다.
  • Pairwise: 두 문서 중 어느 것이 더 관련 있는지를 학습합니다. RankNet, LambdaRank가 대표적입니다.
  • Listwise: 전체 순위 목록을 한 번에 최적화합니다. NDCG 같은 메트릭을 직접 최적화할 수 있어 성능이 가장 좋습니다. LambdaMART가 대표적입니다.

LTR 특성(Feature) 설계

ltr_features.py
python
from dataclasses import dataclass
 
@dataclass
class SearchFeatures:
    """LTR 모델의 입력 특성"""
    # 텍스트 매칭 특성
    bm25_score: float
    semantic_similarity: float
    title_match_score: float
 
    # 문서 품질 특성
    doc_length: int
    doc_freshness_days: int
    doc_pagerank: float
 
    # 사용자 행동 특성
    historical_ctr: float
    avg_dwell_time: float
 
    # 쿼리-문서 상호작용 특성
    query_doc_topic_overlap: float
    entity_match_count: int
 
def extract_features(query: str, doc: dict, user_profile: dict) -> SearchFeatures:
    """쿼리-문서 쌍의 LTR 특성 추출"""
    return SearchFeatures(
        bm25_score=compute_bm25(query, doc["text"]),
        semantic_similarity=compute_similarity(query, doc["embedding"]),
        title_match_score=compute_title_match(query, doc["title"]),
        doc_length=len(doc["text"]),
        doc_freshness_days=(today() - doc["published_date"]).days,
        doc_pagerank=doc.get("pagerank", 0.0),
        historical_ctr=doc.get("ctr", 0.0),
        avg_dwell_time=doc.get("avg_dwell_time", 0.0),
        query_doc_topic_overlap=compute_topic_overlap(query, doc),
        entity_match_count=count_entity_matches(query, doc),
    )
Warning

LTR 모델은 충분한 학습 데이터가 있어야 효과적입니다. 최소 수천 개의 쿼리-문서 쌍에 대한 관련성 레이블이 필요합니다. 데이터가 부족한 초기에는 Cross-encoder 리랭킹이 더 현실적인 선택입니다.


랭킹 전략 선택 가이드

프로젝트의 규모와 상황에 따라 적절한 랭킹 전략이 달라집니다.

상황권장 전략
초기 단계, 데이터 부족BM25 + 시맨틱 검색 (RRF 퓨전)
품질 향상 필요Cross-encoder 리랭킹 추가
Elasticsearch 사용 중Elastic Rerank 도입
대규모 트래픽, 풍부한 로그LTR 모델 학습
최고 품질 추구다단계: BM25+시맨틱 -> RRF -> Cross-encoder -> LTR

정리

이번 장에서는 검색 결과의 순위를 결정하는 랭킹 모델을 심층적으로 다루었습니다. Bi-encoder와 Cross-encoder의 구조적 차이와 트레이드오프를 이해했고, 두 모델을 결합한 retrieve-then-rerank 패턴을 구현했습니다. Elastic Rerank의 DeBERTa v3 모델이 가져오는 40-90%의 성능 향상, RRF를 활용한 점수 퓨전, LTR의 세 가지 접근법(Pointwise, Pairwise, Listwise)도 학습했습니다.

다음 장에서는 이러한 이론적 배경을 Elasticsearch에서 실제로 구현하는 방법을 다룹니다. kNN 검색, Inference API, semantic_text 필드, ELSER, 하이브리드 검색(RRF) 등 Elasticsearch의 AI 검색 기능을 실습합니다.

이 글이 도움이 되셨나요?

관련 주제 더 보기

#search#ai

관련 글

AI / ML

6장: Elasticsearch AI 검색 통합

Elasticsearch의 kNN 검색, Inference API, semantic_text 필드, ELSER, Elastic Rerank, 하이브리드 검색(RRF)을 실습과 함께 다룹니다.

2026년 2월 13일·12분
AI / ML

4장: 쿼리 이해와 확장

쿼리 분류, 의도 인식, 엔티티 인식부터 LLM 기반 쿼리 확장, HyDE(가상 문서 생성), 다국어 처리까지 쿼리 이해 파이프라인을 다룹니다.

2026년 2월 9일·16분
AI / ML

7장: OpenSearch와 기타 검색 엔진

OpenSearch 신경 검색, 재랭킹 파이프라인과 Algolia, Meilisearch, Typesense 등 주요 검색 엔진의 AI 검색 기능을 비교합니다.

2026년 2월 15일·12분
이전 글4장: 쿼리 이해와 확장
다음 글6장: Elasticsearch AI 검색 통합

댓글

목차

약 14분 남음
  • 학습 목표
  • Bi-encoder vs Cross-encoder
    • Bi-encoder — 독립 인코딩
    • Cross-encoder — 결합 인코딩
    • 성능 비교
  • Cross-encoder 리랭킹 구현
  • Elastic Rerank — DeBERTa v3 기반 리랭킹
    • 성능 향상 효과
    • Elasticsearch에서의 설정
  • RRF — 점수 퓨전
    • RRF 수식
    • RRF vs 선형 보간
  • LTR — 학습 랭킹
    • LTR의 세 가지 접근법
    • LTR 특성(Feature) 설계
  • 랭킹 전략 선택 가이드
  • 정리