‘하이브리드 리랭킹’ 기술적으로는 별로 어려운 것은 아닌데 솔직히 진지하게 생각해 사용해본 적이 없는 기술입니다. 예전의 결정론적인 보안을 할때는 이런 방식이 너무 안이해 보였거든요. 답을 알 수 없는, 실무를 모르는 엔지니어들이 만든 일종에 대안이라 생각했는데요. 이제 통계적인 데이터 운영을 하다보니 느낀 건데 많은 속성을 하나의 수치로 뭉개서 벡터를 스칼라로 만들었던게 좀 무식하긴했다는 생각이 드네요.

참고로 제가 처음 설계시 지양하는 방향을 소형 로컬 LLM이지만 엔터프라이즈의 기능들도 공부해보고 싶어서 넣다보니 좀 애매한 솔루션이 된거 같기도 합니다. 여튼 그래서 대규모 시스템에서는 초기 세팅이 꽤… 어려울 것 같긴합니다.

엔터프라이즈 환경에서 거대 언어 모델(LLM) 기반의 검색 증강 생성(RAG) 파이프라인을 구축할 때 가장 빈번하게 발생하는 기술적 병목은 단일 차원의 검색 전략이 가지는 본질적인 한계입니다. 고밀도 분산 표현에 의존하는 벡터 전용 검색(vector_only)은 문맥의 압축된 의미론적 유사성을 파악하는 데는 유리하지만, 특정 품번, 기호, 에러 코드와 같은 고유 명사나 핵심 키워드가 포함된 완전 일치 조회 시 매칭 정합성이 저하되는 약점을 가집니다.

이와 같은 트레이드오프를 해결하고 임베딩 모델의 편향을 보완하기 위해 Bastion-RAG 프레임워크의 검색 서빙 아키텍처인 Navigator 모듈(navigator/searcher.py, navigator/reranker.py, navigator/orchestrator.py)은 고밀도 벡터 검색과 저밀도 키워드 검색을 결합한 하이브리드 검색 엔진을 구동합니다.

본 포스트에서는 상호 보완적인 두 가지 검색 모델의 순위를 연산하는 RRF(Reciprocal Rank Fusion) 융합 파이프라인과, 추출된 다차원 후보군의 정합성을 직접 교차 평가하여 재정렬하는 크로스 인코더 리랭킹(Cross-Encoder Reranking)과 하이브리드 리랭킹의 하위 소스 코드 레벨 시스템 스펙을 분석합니다.


URL Site > https://github.com/zafrem/bastion-navigator

시리즈명 : Bastion-RAG – Project 보안 RAG



1. 하이브리드+리랭킹(hybrid+rerank) 다단계 검색 서빙 플로우

Navigator 모듈의 엔드투엔드 처리 경로는 런타임 지연 마찰을 통제하면서 다차원 검색 정합성을 점진적으로 향상하도록 설계되었습니다. 시스템 진입점인 orchestrator.search(req)를 호출하면 전체 데이터 패스는 다음 4단계의 동기식 상태 전이를 거치게 됩니다.

하이브리드 리랭킹

1.1 인텐트 라우팅 및 컬렉션 분기 (Intent Routing)

유입된 유저 질의문 문자열은 router.py 패키지 내부 정규식 분류 시스템에 의해 분석되며, 정립된 위험 요인 및 질의 속성에 따라 3가지 구동 전략(vector_only, hybrid, hybrid+rerank) 중 최적의 물리적 가드가 포함된 플래그로 매핑 처리됩니다. 단기 사실 확인형 질의(FACTUAL)인 경우 연산 비용이 낮은 벡터 단독 경로를 타지만, 복합 분석형 질의(ANALYTICAL, MULTI_HOP)의 경우 RRF와 리랭킹 엔진이 모두 상주하는 최상위 파이프라인 분기로 자동 강제 유도됩니다.

1.2 후보군 과도 인출 제어 (Over-fetch Mechanism)

Navigator 엔진이 리랭킹 단계로 데이터를 handoff 하기 직전, 시스템은 최종 유저가 요청한 탑케이(top_k) 값보다 훨씬 큰 규모의 연산 범위를 선제적으로 인출합니다.

Python

# navigator/orchestrator.py

over_fetch = opts.top_k * self._cfg.search_defaults.over_fetch_multiplier
# 시스템 기본 스펙: top_k=10, multiplier=5 설정 시 over_fetch=50 유도

벡터 공간 내에서의 상위 유사도 점수와 크로스 인코더가 도출하는 직접적 문맥 연관성은 수학적으로 다른 신호입니다. 코사인 유사도 연산 상에서 35위에 머무르던 특정 텍스트 조각이 단어 교차 매칭 평가를 거치며 1위로 도약할 수 있으므로, 하위 두 검색 엔진은 각각 최대 50개의 독립 후보 풀을 개별 인출하도록 제약을 둡니다.

2. 1단계 및 2단계: 고밀도 벡터 탐색과 저밀도 BM25-Proxy 고속 구현

Navigator 모듈은 고차원 분산 표현 벡터 스페이스를 처리하기 위해 BAAI/bge-m3 임베딩 모델(1024차원)과 백엔드 Qdrant 벡터 데이터베이스 인프라를 동기식으로 바인딩하여 탐색을 집행합니다.

Vault 단에서 안전하게 검증 완료된 테넌트 식별 메타데이터 정보는 데이터베이스 쿼리를 던지기 직전 Qdrant 전용 필터 인덱스 구조체 조건절에 동적으로 강제 주입(Must Condition)됩니다.

Python

# navigator/searcher.py

def vector_search(
    self,
    collection: str,
    vector: list[float],
    filters: dict[str, str],
    top_k: int,
    min_score: float = 0.0,
) -> list[SearchResult]:
    from qdrant_client.models import Filter, FieldCondition, MatchValue

    start = time.perf_counter()
    qdrant_filter = None
    if filters:
        # 유입된 모든 메타데이터 필터 키-밸류 세트를 논리적 AND (must) 조건절로 인젝션
        qdrant_filter = Filter(
            must=[FieldCondition(key=k, match=MatchValue(value=v))
                  for k, v in filters.items()]
        )

    hits = self._client.search(
        collection_name=collection,
        query_vector=vector, // 1024차원 BGE-M3 정규화 고밀도 벡터 임베딩 주입
        query_filter=qdrant_filter,
        limit=top_k,
        score_threshold=min_score if min_score > 0 else None,
    )
    metrics.qdrant_call_duration_seconds.labels(operation="vector_search").observe(
        time.perf_counter() - start
    )
    return [_to_search_result(h) for h in hits]

이 처리를 통해 타 테넌트나 권한 밖의 문서 도메인은 HNSW(Hierarchical Navigable Small World) 인덱스 그래프 탐색 초기 진입로에서부터 물리적으로 격리 배제되며, 연산 통과 완료 후 추출된 레코드는 _to_search_result() 파서를 거쳐 고유 자산 필드와 메타데이터 필드로 엄격하게 분리 정제됩니다.

네이티브 엔버전 역인덱스(Inverted Index) 기반 BM25 엔진을 인프라망에 추가 구성할 때 발생하는 네트워크 통신 오버헤드와 분산 동기화 비용을 억제하기 위해, NavigatorQdrant 스크롤 인터페이스와 메모리 기반 토큰 스캐너를 융합한 고속 BM25-Proxy 스코어러를 탑재했습니다.

Python

# navigator/searcher.py

def sparse_search(
    self,
    collection: str,
    query: str,
    filters: dict[str, str],
    top_k: int,
) -> list[SearchResult]:
    # 벡터 랭킹 연산 없이 정적 메타데이터 필터 조건만 걸어 물리 청크 배열을 스크롤 인출
    hits = self._client.scroll(
        collection_name=collection,
        scroll_filter=qdrant_filter,
        limit=top_k,
        with_payload=True,
    )[0]

    results = [_to_search_result(h) for h in hits]

    # BM25-Proxy 계산: 질의문 내 토큰 단어가 문서 내 평문에 포함된 빈도 점수 연산
    # 수식 사양: score = 일치 토큰 개수 / 전체 질의 토큰 개수
    q_lower = query.lower()
    for r in results:
        r.score = sum(
            1 for w in q_lower.split() if w in r.content.lower()
        ) / max(len(q_lower.split()), 1)

    return sorted(results, key=lambda r: r.score, reverse=True)

본 2단계 프록시 스코어러 엔진은 역문서 빈도(IDF) 가중치 연산을 생략하는 대신, 문자열 토큰 분할 매칭 구조를 취하여 연산 마찰을 마이크로초(µs) 단으로 제한하면서 RFF 결합 단계에 필요한 명확한 키워드 완전 일치 신호를 공급합니다.

3. 3단계: 상호 순위 기반 RRF(Reciprocal Rank Fusion) 결합 메커니즘

벡터 검색 결과와 키워드 프록시 결과 풀이 인출되면, 두 시스템의 절대적인 점수 척도(유사도 스케일)가 상이하므로 점수 기반 결합이 불가능합니다. Navigator 오케스트레이터는 각 리스트 내의 상대적 순위 좌표(Rank Index)만을 매개변수로 삼아 데이터 순위를 재연산하는 RRF 알고리즘을 수행합니다.

Python

# navigator/orchestrator.py

def _rrf(
    vector: list[SearchResult],
    bm25: list[SearchResult],
    vw: float = 0.7, // 고밀도 벡터 가중치 기본값 사양
    bw: float = 0.3, // 저밀도 키워드 가중치 기본값 사양
    k: float = 60.0, // 순위 압축 정규화 상수 (Standard RRF Constant)
) -> list[SearchResult]:
    scores: dict[str, float] = {}
    docs: dict[str, SearchResult] = {}

    # 고밀도 벡터 순위 기여도 누적 계산: 수식 사양 = vw / (k + rank + 1)
    for rank, r in enumerate(sorted(vector, key=lambda r: r.score, reverse=True)):
        scores[r.document_id] = scores.get(r.document_id, 0) + vw / (k + rank + 1)
        docs[r.document_id] = r

    # 저밀도 키워드 순위 기여도 누적 계산: 수식 사양 = bw / (k + rank + 1)
    for rank, r in enumerate(sorted(bm25, key=lambda r: r.score, reverse=True)):
        scores[r.document_id] = scores.get(r.document_id, 0) + bw / (k + rank + 1)
        docs[r.document_id] = r

    # 합성된 전역 RRF 가중치 점수를 기준으로 내림차순 정렬
    ranked = sorted(scores.items(), key=lambda x: x[1], reverse=True)
    return [docs[doc_id] for doc_id, _ in ranked]

3.1 RRF 순위 스무딩 상수 ($k=60$) 도입 목적

RRF 점수 변환 수식 상에서 분모에 더해지는 평활화 상수 $k=60$은 최상위 랭킹 레코드와 최하위 랭킹 레코드 간의 점수 낙차 폭을 로그 스케일 형태로 완만하게 압축(Compression)하는 가드레일 역할을 합니다. 특정 한 가지 검색 채널에서 비정상적으로 높게 산출된 단일 오염 데이터가 전역 하이브리드 리스트 전체의 지배력을 독점하는 현상을 구조적으로 차단합니다.

3.2 교차 채널 가중치 가속화 보너스 (Cross-List Bonus)

본 프로세스 모델이 단일 차원 검색을 압도하는 기술적 근거는 교차 출현 가속화 현상에 있습니다. 만약 문서 A가 벡터 랭킹에서 2위(rank=1)를 기록하고, BM25 키워드 랭킹에서도 5위(rank=4)에 동시 중복 안착했다면 가중치 스펙은 다음과 같이 결합 누적됩니다.

$$\text{RRF Score} = \frac{0.7}{60 + 1 + 1} + \frac{0.3}{60 + 4 + 1} = \frac{0.7}{62} + \frac{0.3}{65} \approx 0.01129 + 0.00461 = 0.01590$$

의미론적 맥락이 근접하면서 유저가 명시한 핵심 키워드 토큰 조건까지 완벽히 충족하는 최적의 지식 조각들이 리스트 최상단으로 강력하게 프로모션(Promotion)되는 데이터 필터링 효과를 달성하게 됩니다.

4. 4단계: 크로스 인코더(Cross-Encoder) 기반 심층 문맥 리랭킹 스펙

RRF 융합 파이프라인을 거쳐 1차 엄선된 최대 50개의 혼합 후보 자산 배열은 최종적으로 BAAI/bge-reranker-v2-m3 로컬 신경망 모델의 실시간 추론 레이어로 이관됩니다.

바이 인코더(Bi-Encoder) 구조인 벡터 임베딩 모델은 질문과 본문을 각각 독립적으로 인코딩하여 벡터 공간 좌표로 투영하므로 두 텍스트 간의 미세한 상호 인터랙션(Query-Passage Interaction) 정보를 연산 도중 소실할 수밖에 없습니다. 반면 리랭커 패키지는 사용자의 질문 원문과 대상 문서 본문 텍스트 전체를 단일 트랜스포머 입력 어텐션(Attention) 블록에 통째로 병렬 결합하여 주입(In-Process Forward Pass)합니다.

Python

# navigator/reranker.py

class LocalReranker(Reranker):
    """BAAI/bge-reranker-v2-m3 크로스 인코더 모델을 프로세스 내부 메모리에 상주시켜 서빙하는 엔진"""

    func __init__(self, model_name: str, max_length: int = 512) -> None:
        from sentence_transformers import CrossEncoder
        # 서버 부트스트랩 시점에 가중치 바이너리를 메모리에 전량 상주 로드하여 shared-resource로 운용
        self._model = CrossEncoder(model_name, max_length=max_length)

    def rerank(self, query: str, candidates: list[SearchResult], top_k: int) -> list[SearchResult]:
        if not candidates:
            return []
        start = time.perf_counter()

        # 크로스 인코더 입력 규격에 맞춰 [질문 원문, 대상 본문 평문]의 이중 페어 어레이 빌드
        pairs = [[query, c.content] for c in candidates]

        # 단일 순방향 포워드 패스(Forward Pass) 연산을 통해 각 쌍의 교차 어텐션 연관성 점수(Float) 실시간 추론
        scores = self._model.predict(pairs)

        metrics.rerank_duration_seconds.observe(time.perf_counter() - start)

        # 리랭커 추론 스코어 내림차순 정렬 처리를 거쳐 유저가 최종 요청한 top_k 규격으로 슬라이싱 출력
        ranked = sorted(zip(scores, candidates), key=lambda x: x[0], reverse=True)
        return [c for _, c in ranked[:top_k]]

LocalReranker 엔진은 외부 모델 서버 인프라를 HTTP 오버헤드로 별도 호출하는 구조적 고정관념을 파괴하고, 동일 프로세스 런타임 힙 영역 내에 딥러닝 모델 컨텍스트를 직접 구동(In-Process Execution)합니다.

네트워크 홉(Network Hop) 비용과 직렬화 오버헤드가 제로인 환경에서 신경망 어텐션 행렬 연산이 즉각 수행되므로, 단순한 벡터 유사도를 뛰어넘어 유저 질의의 본질적 의도에 완벽하게 부합하는 최상의 지식 소스 10건(top_k)만을 초고속 정밀 필터링하여 상위 오케스트레이터로 최종 반환하게 됩니다.

5. 인텐트 기반 라우팅 제어 및 프로메테우스 메트릭 추적 스펙

모든 질의에 대해 무거운 크로스 인코더 행렬 추론 레이어를 고정 가동하는 것은 제한된 하드웨어 환경에서 컴퓨팅 자원 낭비를 유발합니다. 이를 통제하기 위해 _apply_routing_strategy() 게이트웨이는 유저의 의도에 맞춰 파이프라인 스펙을 최적화 분기합니다.

Python

_INTENT_STRATEGY: dict[QueryIntent, str] = {
    QueryIntent.FACTUAL:    "vector_only",    # 단기 사실 확인 질의 -> 고속 벡터 단독 처리 분기 스킵
    QueryIntent.ANALYTICAL: "hybrid+rerank",  # 심층 분석 질의 -> 전 과정 하이브리드 리랭킹 인프라 총동원
    QueryIntent.PROCEDURAL: "hybrid",         # 절차 지향 질의 -> 단어 정합성이 중요하되 리랭커 비용은 억제
    QueryIntent.MULTI_HOP:  "hybrid+rerank",  # 다중 연결 질의 -> 풀 파이프라인 가동 강제
    QueryIntent.AMBIGUOUS:  "hybrid",         # 모호한 질의 -> 기본 가용성 방어선 안착
}

결정된 라우팅 전략 세부 상태 지표는 시스템의 관제 투명성을 담보하기 위해 전역 프로메테우스(Prometheus) 모니터링 수집 버스의 레이블 상수로 실시간 주입 발행됩니다.

Python

# navigator/orchestrator.py

# 결정된 구동 전략 명칭 문자열을 라벨로 삼아 통합 Prometheus 카운터 및 옵저버 엔진 구동
metrics.searches_total.labels(strategy=strategy, tenant_id=req.tenant_id).inc()
metrics.search_duration_seconds.labels(strategy=strategy).observe(duration_ms / 1000)

이 감사 계보 연동을 통해 인프라 엔지니어는 대시보드 상에서 vector_only 대비 hybrid+rerank 전략이 소모하는 레이턴시 델타값과 테넌트별 트래픽 가동 임계치를 마이크로초 단위로 정밀 프로파일링 및 룩업할 수 있게 됩니다.

6. 결론: 밀집 및 희소 시그널의 구조적 균형점 확보

Navigator 모듈이 정립한 하이브리드 리랭킹 검색 아키텍처는 인공지능 기반 지식 룩업 인프라가 갖추어야 할 이상적인 기술적 스펙 표준이라 생각합니다. 의미론적 유사성만을 추종하다가 완전 일치 품번이나 시스템 지표 매칭에 무력하게 미끄러지던 Bi-Encoder 임베딩 모델의 근본적인 한계를, 초고속 BM25-Proxy 및 RRF 순위 스무딩 장벽을 결합하여 상쇄하고 있습니다.

전방에서 수행되는 Qdrant 사전 필터링 격리 정책으로 멀티 테넌트 보안 무결성을 완벽하게 보장하면서도, 인프로세스(In-Process) 방식으로 permanent 상주하는 크로스 인코더 모델을 통해 신경망 연산 마찰 지연 시간을 최소화하여 최종 top_k 정제 필터링 < 50ms 이내의 프로덕션 서빙을 제공하려고 합니다.

이러한 다단계 검색 고도화 파이프라인 사양은, 데이터 거버넌스 규제 준수와 LLM 프롬프트에 주입될 지식 소스의 무결한 품질 확보를 동시에 달성해야 하는 최고정보책임자(CISO)와 AI 플랫폼 리드 아키텍트들에게 신뢰할 수 있는 솔루션이 될 것입니다.

By Mark