fix: keyword recall=0 리서치에 threshold 스케일 불일치 원인 추가 (23서버 실측)

- threshold 0.35에서 ts_rank 최대 0.089이므로 전부 필터링되는 문제 확인
- 교착어 미처리와 threshold 불일치가 중첩 원인임을 명시
- 우선순위 1(즉시 적용): threshold 분리 + prefix 매칭 동시 적용 제안

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
happybell80 2026-03-21 15:50:30 +09:00
parent 9bd9459912
commit cb39cbd8ee

View File

@ -35,6 +35,31 @@ DB 실측 (키워드 '투자', Company X 청크 대상):
`_build_keyword_tsquery()`는 Python에서 정규식 `[0-9A-Za-z가-힣]+`로 깨끗한 어절 토큰을 추출하지만, DB의 tsv 토큰에는 조사가 붙어 있어 구조적으로 불일치.
### 1-1. keyword recall = 0의 추가 직접 원인: threshold 스케일 불일치 (23서버 실측 보완)
리서치 원문은 `simple` 토크나이저의 한국어 교착어 미처리를 주 원인으로 서술했으나, **threshold가 동일하게 심각한 원인**이다.
`keyword_search()`는 213줄에서 `if keyword_score >= threshold`로 결과를 필터링한다. 호출 경로별 threshold:
| 호출 경로 | threshold | 결과 |
|-----------|-----------|------|
| `search.py` 46줄 단독 keyword | `request.threshold` (기본 0.35) | **전부 필터링 → 0건** |
| `hybrid_search()` 248줄 내부 호출 | 하드코딩 `0.0` | 통과 가능 |
23서버 실측 (`아크로셀 | 개인투자조합` 쿼리):
| 조건 | 건수 |
|------|------|
| `ts_rank >= 0.35` | **0건** |
| `ts_rank >= 0.0` | **41건** |
| prefix 매칭 `아크로셀:* \| 개인투자조합:*` | **58건** |
즉 **두 원인이 중첩**되어 있다:
1. `simple` 토크나이저 → tsv에 조사 붙은 토큰 저장 → exact match 실패 (recall 손실)
2. threshold 0.35 → ts_rank 최대 0.089 → 매칭된 결과도 전부 필터링 (precision 필터가 recall을 완전 소멸)
교착어 문제만 해결해도 threshold 0.35에서는 여전히 0건이다. **threshold를 먼저 수정하지 않으면 어떤 토크나이저 개선도 keyword 결과에 반영되지 않는다.**
### 2. grounding 실패의 세 가지 계층
재오픈 20개 중 expect_evidence=True인 11건을 분석한 결과, 실패는 세 계층에서 발생한다:
@ -95,7 +120,8 @@ hybrid 모드에서 `relevance_score`가 RRF 점수로 대체된다. RRF 점수
| 실패 | 직접 원인 | 계층 |
|------|----------|------|
| keyword recall = 0 | `simple` 토크나이저가 한국어 조사 분리 불가 | DB/인덱스 |
| keyword recall = 0 (원인1) | `simple` 토크나이저가 한국어 조사 분리 불가 → tsv 토큰과 tsquery 불일치 | DB/인덱스 |
| keyword recall = 0 (원인2) | `keyword_search()` 단독 호출 시 threshold 0.35 적용 → ts_rank 최대 0.089로 전부 필터링 | 검색/점수 |
| RRF score ≪ threshold | RRF 점수(0.01~0.03)와 threshold(0.35)의 스케일 불일치 | 검색/점수 |
| 인덱싱 문서 있는데 검색 실패 | 벡터 임베딩이 해당 청크를 근접 이웃으로 잡지 못함 + keyword 보완 불가 | 검색 |
| 3건 grounding 미진입 | intent/domain 마커에 일반 질문 패턴 미포함 | 라우팅 |
@ -104,11 +130,21 @@ hybrid 모드에서 `relevance_score`가 RRF 점수로 대체된다. RRF 점수
### keyword recall 개선 옵션
**우선순위 1 (즉시 적용 — 두 원인 동시 해결):**
| 조치 | 변경 위치 | 효과 |
|------|-----------|------|
| threshold 분리 | `keyword_search()` 단독 호출 시 threshold를 0.01 이하로 | ts_rank 필터링 해소 → 0건→41건+ |
| prefix 매칭 | `_build_keyword_tsquery()`에서 각 토큰에 `:*` 접미 | 조사 붙은 토큰도 매칭 → recall 27%→90% |
두 조치를 함께 적용하면 keyword 단독 검색에서 즉시 결과가 나온다.
**우선순위 2 (근본 해결):**
| 옵션 | 방식 | 장점 | 단점 |
|------|------|------|------|
| prefix 매칭 | tsquery에 `:*` 접미 | 즉시 적용, recall 27%→90% | 복합어 내부 매칭 불가 |
| trigram (pg_trgm) | `chunk_text % :query` | 부분 문자열 매칭 가능 | GIN 인덱스 추가 필요, 정밀도 낮음 |
| 한국어 형태소 분석 | mecab + textsearch_ko | 근본 해결 | 확장 설치 필요 (51123 서버) |
| 한국어 형태소 분석 | mecab + textsearch_ko | 근본 해결 | 확장 설치 필요 (51123 서버), PG14 EOL 고려 |
| 하이브리드 보완 | ILIKE fallback | 간단 | 인덱스 미사용, 느림 |
### 벡터 검색 품질 개선