feat(search): Phase 1.3 TEI reranker 통합 (코드 골격)
데이터 흐름 원칙: fusion=doc 기준 / reranker=chunk 기준 — 절대 섞지 말 것.
신규/수정:
- ai/client.py: rerank() 메서드 추가 (TEI POST /rerank API)
- services/search/rerank_service.py:
- rerank_chunks() — asyncio.Semaphore(2) + 5s soft timeout + RRF fallback
- _make_snippet/_extract_window — title + query 중심 200~400 토큰
(keyword 매치 없으면 첫 800자 fallback)
- apply_diversity() — max_per_doc=2, top score>=0.90 unlimited
- warmup_reranker() — 10회 retry + 3초 간격 (TEI 모델 로딩 대기)
- MAX_RERANK_INPUT=200, MAX_CHUNKS_PER_DOC=2 hard cap
- services/search_telemetry.py: compute_confidence_reranked() — sigmoid score 임계값
- api/search.py:
- ?rerank=true|false 파라미터 (기본 true, hybrid 모드만)
- 흐름: fused_docs(limit*5) → chunks_by_doc 회수 → rerank_chunks → apply_diversity
- text-only 매치 doc은 doc 자체를 chunk처럼 wrap (fallback)
- rerank 활성 시 confidence는 reranker score 기반
- tests/search_eval/run_eval.py: --rerank true|false 플래그
GPU 적용 보류:
- TEI 컨테이너 추가 (docker-compose.yml) — 별도 작업
- config.yaml rerank.endpoint 갱신 — GPU 직접 (commit 없음)
- 재인덱싱 완료 후 build + warmup + 평가셋 측정
This commit is contained in:
@@ -16,10 +16,17 @@ from core.database import get_session
|
||||
from core.utils import setup_logger
|
||||
from models.user import User
|
||||
from services.search.fusion_service import DEFAULT_FUSION, get_strategy, normalize_display_scores
|
||||
from services.search.rerank_service import (
|
||||
MAX_CHUNKS_PER_DOC,
|
||||
MAX_RERANK_INPUT,
|
||||
apply_diversity,
|
||||
rerank_chunks,
|
||||
)
|
||||
from services.search.retrieval_service import compress_chunks_to_docs, search_text, search_vector
|
||||
from services.search_telemetry import (
|
||||
compute_confidence,
|
||||
compute_confidence_hybrid,
|
||||
compute_confidence_reranked,
|
||||
record_search_event,
|
||||
)
|
||||
|
||||
@@ -104,6 +111,10 @@ async def search(
|
||||
pattern="^(legacy|rrf|rrf_boost)$",
|
||||
description="hybrid 모드 fusion 전략 (legacy=기존 가중합, rrf=RRF k=60, rrf_boost=RRF+강한신호 boost)",
|
||||
),
|
||||
rerank: bool = Query(
|
||||
True,
|
||||
description="bge-reranker-v2-m3 활성화 (Phase 1.3, hybrid 모드만 동작)",
|
||||
),
|
||||
debug: bool = Query(False, description="단계별 candidates + timing 응답에 포함"),
|
||||
):
|
||||
"""문서 검색 — FTS + ILIKE + 벡터 결합 (Phase 0.5: RRF fusion)"""
|
||||
@@ -145,13 +156,44 @@ async def search(
|
||||
|
||||
t2 = time.perf_counter()
|
||||
strategy = get_strategy(fusion)
|
||||
results = strategy.fuse(text_results, vector_results, q, limit)
|
||||
# fusion은 doc 기준 — 더 넓게 가져옴 (rerank 후보용)
|
||||
fusion_limit = max(limit * 5, 100) if rerank else limit
|
||||
fused_docs = strategy.fuse(text_results, vector_results, q, fusion_limit)
|
||||
timing["fusion_ms"] = (time.perf_counter() - t2) * 1000
|
||||
notes.append(f"fusion={strategy.name}")
|
||||
notes.append(
|
||||
f"chunks raw={len(raw_chunks)} compressed={len(vector_results)} "
|
||||
f"unique_docs={len(chunks_by_doc)}"
|
||||
)
|
||||
|
||||
if rerank:
|
||||
# Phase 1.3: reranker — chunk 기준 입력
|
||||
# fusion 결과 doc_id로 chunks_by_doc에서 raw chunks 회수
|
||||
t3 = time.perf_counter()
|
||||
rerank_input: list[SearchResult] = []
|
||||
for doc in fused_docs:
|
||||
chunks = chunks_by_doc.get(doc.id, [])
|
||||
if chunks:
|
||||
# doc당 max 2 chunk (latency/VRAM 보호)
|
||||
rerank_input.extend(chunks[:MAX_CHUNKS_PER_DOC])
|
||||
else:
|
||||
# text-only 매치 doc → doc 자체를 chunk처럼 wrap
|
||||
rerank_input.append(doc)
|
||||
if len(rerank_input) >= MAX_RERANK_INPUT:
|
||||
break
|
||||
rerank_input = rerank_input[:MAX_RERANK_INPUT]
|
||||
notes.append(f"rerank input={len(rerank_input)}")
|
||||
|
||||
reranked = await rerank_chunks(q, rerank_input, limit * 3)
|
||||
timing["rerank_ms"] = (time.perf_counter() - t3) * 1000
|
||||
|
||||
# diversity (chunk → doc 압축, max_per_doc=2, top score>0.90 unlimited)
|
||||
t4 = time.perf_counter()
|
||||
results = apply_diversity(reranked, max_per_doc=MAX_CHUNKS_PER_DOC)[:limit]
|
||||
timing["diversity_ms"] = (time.perf_counter() - t4) * 1000
|
||||
else:
|
||||
# rerank 비활성: fused_docs를 그대로 (limit 적용)
|
||||
results = fused_docs[:limit]
|
||||
else:
|
||||
results = text_results
|
||||
|
||||
@@ -162,8 +204,12 @@ async def search(
|
||||
timing["total_ms"] = (time.perf_counter() - t_total) * 1000
|
||||
|
||||
# confidence는 fusion 적용 전 raw 신호로 계산 (Phase 0.5 이후 fused score는 절대값 의미 없음)
|
||||
# rerank 활성 시 reranker score가 가장 신뢰할 수 있는 신호 → 우선 사용
|
||||
if mode == "hybrid":
|
||||
confidence_signal = compute_confidence_hybrid(text_results, vector_results)
|
||||
if rerank and "rerank_ms" in timing:
|
||||
confidence_signal = compute_confidence_reranked(results)
|
||||
else:
|
||||
confidence_signal = compute_confidence_hybrid(text_results, vector_results)
|
||||
elif mode == "vector":
|
||||
confidence_signal = compute_confidence(vector_results, "vector")
|
||||
else:
|
||||
|
||||
Reference in New Issue
Block a user