fix(search): confidence 휴리스틱 vector-only amplify 버그 수정

vector-only 매치(match_reason == 'vector')에서 raw 코사인 0.43이
0.6으로 잘못 amplify되어 low_confidence threshold(0.5)를 못 넘기던 문제.

- vector-only 분기: amplify 제거, _cosine_to_confidence로 일관 환산
- _cosine_to_confidence: bge-m3 코사인 분포 (무관 텍스트 ~0.4) 반영
- 코사인 0.55 = threshold 경계(0.50), 0.45 미만은 명확히 low

smoke test 결과 zzzqxywvkpqxnj1234 같은 무의미 쿼리(top cosine 0.43)가
low_confidence로 잡히지 않던 문제 해결.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Hyungi Ahn
2026-04-07 08:33:25 +09:00
parent f005922483
commit 50e6b5ad90

View File

@@ -81,8 +81,16 @@ def compute_confidence(results: list[Any], mode: str) -> float:
Phase 0.3 임시 구현. Phase 2에서 QueryAnalyzer 결과 + reranker score로 교체.
하이브리드/텍스트 모드는 score가 가중치 합산이라 unbounded → match_reason과 결합.
벡터 모드는 score 코사인 유사도(0..1)라 그대로 사용.
score 의미 정리 (search.py 기준):
- mode=vector → score = 코사인 유사도 [0..1]
- mode=fts/trgm/hybrid에서 텍스트 매치 → score = 가중치 합산 (unbounded)
가중치: title=3.0 / tags=2.5 / note=2.0 / summary=1.5 / content=1.0 / fts bonus≈2.0
- mode=hybrid에서 텍스트 0건 → 벡터 결과만, score는 코사인 그대로
- mode=hybrid 텍스트+벡터 동시 매치 → score = 텍스트가중치 + 0.5*코사인,
match_reason = "<텍스트reason>+vector"
핵심: match_reason이 정확히 'vector'(=문자열 "vector")면 텍스트 매치 0건인 vector-only.
이 경우 score는 raw 코사인이므로 amplify 금지.
"""
if not results:
return 0.0
@@ -93,12 +101,9 @@ def compute_confidence(results: list[Any], mode: str) -> float:
if mode == "vector":
# 코사인 유사도 그대로
return max(0.0, min(1.0, top_score))
# text / hybrid: match_reason 강도 + score를 함께 본다
# search.py의 가중치: title=3.0, tags=2.5, note=2.0, summary=1.5, content=1.0, fts bonus=2.0
# vector boost(hybrid 합산)는 +0.5*cosine
return _cosine_to_confidence(top_score)
# text / hybrid: 강한 텍스트 매치 우선 판정
if "title" in reason and top_score >= 4.0:
return 0.95
if any(k in reason for k in ("tags", "note")) and top_score >= 3.0:
@@ -109,15 +114,36 @@ def compute_confidence(results: list[Any], mode: str) -> float:
return 0.65
if "fts" in reason and top_score >= 1.0:
return 0.55
if "vector" in reason:
# vector-only hit (텍스트 매칭 실패) → 코사인 유사도 환산
# hybrid 합산 시 vector 단독 점수는 score * 0.5로 들어옴
cosine = top_score / 0.5 if top_score < 1.0 else top_score
return max(0.2, min(0.6, cosine * 0.7))
# 약한 매치
# vector-only hit (텍스트 0건 → 코사인 raw, amplify 금지)
if reason == "vector":
return _cosine_to_confidence(top_score)
# 그 외(약한 매치 또는 알 수 없는 reason)
return 0.3
def _cosine_to_confidence(cosine: float) -> float:
"""bge-m3 임베딩 코사인 유사도 → confidence 환산.
bge-m3는 무관한 텍스트도 보통 0.3~0.5 정도 코사인을 만든다.
따라서 0.5는 "약하게 닮음", 0.7+는 "꽤 관련", 0.85+는 "매우 관련"으로 본다.
"""
if cosine >= 0.85:
return 0.95
if cosine >= 0.75:
return 0.80
if cosine >= 0.65:
return 0.65
if cosine >= 0.55:
return 0.50 # threshold 경계
if cosine >= 0.45:
return 0.35
if cosine >= 0.35:
return 0.20
return 0.10
# ─── 로깅 진입점 ─────────────────────────────────────────