Retriever: RRF로 키워드+원본 질문 결합 — LLM이 핵심 주제어 누락(e.g. '처방전 보관기간' → ['보관기간']) 해도 원본 질문이 안전망. cases·guides 동일 적용
Browse files
src/kpaa/retrieval/retriever.py
CHANGED
|
@@ -211,9 +211,23 @@ async def _fetch_cases(
|
|
| 211 |
if on_progress:
|
| 212 |
await on_progress("fetch_done", {"source": "case", "count": 0, "keyword": ""})
|
| 213 |
return []
|
| 214 |
-
#
|
| 215 |
-
|
| 216 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 217 |
out: list[Excerpt] = []
|
| 218 |
for h in hits:
|
| 219 |
category = " > ".join(filter(None, (h.category1, h.category2, h.category3)))
|
|
@@ -260,8 +274,24 @@ async def _fetch_guides(
|
|
| 260 |
if on_progress:
|
| 261 |
await on_progress("fetch_done", {"source": "guide", "count": 0, "keyword": ""})
|
| 262 |
return []
|
| 263 |
-
|
| 264 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 265 |
out: list[Excerpt] = []
|
| 266 |
for h in hits:
|
| 267 |
# doc_date "YYYY.MM" → 연도만 추출해 recency 점수에 사용
|
|
|
|
| 211 |
if on_progress:
|
| 212 |
await on_progress("fetch_done", {"source": "case", "count": 0, "keyword": ""})
|
| 213 |
return []
|
| 214 |
+
# 키워드 + 원본 질문 두 쿼리로 검색 후 RRF(Reciprocal Rank Fusion)로 결합 —
|
| 215 |
+
# LLM 추출 키워드가 핵심 주제어 누락 시 원본 질문이 안전망. 단순 concat은
|
| 216 |
+
# BM25 토큰 가중치 차이로 한쪽이 독점 가능하므로 rank 기반 결합 필요.
|
| 217 |
+
_RRF_K = 60
|
| 218 |
+
queries: list[str] = []
|
| 219 |
+
if plan.search_keywords:
|
| 220 |
+
queries.append(" ".join(plan.search_keywords[:3]))
|
| 221 |
+
if plan.query and plan.query not in queries:
|
| 222 |
+
queries.append(plan.query)
|
| 223 |
+
rrf_scores: dict = {}
|
| 224 |
+
hit_map: dict = {}
|
| 225 |
+
for q in queries:
|
| 226 |
+
for rank, h in enumerate(idx.search(q, k=k)):
|
| 227 |
+
rrf_scores[h.ntt_id] = rrf_scores.get(h.ntt_id, 0.0) + 1.0 / (_RRF_K + rank)
|
| 228 |
+
hit_map.setdefault(h.ntt_id, h)
|
| 229 |
+
top_ids = sorted(rrf_scores, key=lambda i: -rrf_scores[i])[:k]
|
| 230 |
+
hits = [hit_map[i] for i in top_ids]
|
| 231 |
out: list[Excerpt] = []
|
| 232 |
for h in hits:
|
| 233 |
category = " > ".join(filter(None, (h.category1, h.category2, h.category3)))
|
|
|
|
| 274 |
if on_progress:
|
| 275 |
await on_progress("fetch_done", {"source": "guide", "count": 0, "keyword": ""})
|
| 276 |
return []
|
| 277 |
+
# 키워드 + 원본 질문 두 쿼리로 검색 후 RRF로 결합 — LLM 추출 키워드가 핵심
|
| 278 |
+
# 주제어를 누락해도 원본 질문이 안전망 (e.g. "처방전 보관기간" → ["보관기간"]만
|
| 279 |
+
# 추출돼도 원본 query에서 "처방전" 토큰 hit 가능). 단순 concat은 BM25 가중치
|
| 280 |
+
# 차이로 한쪽이 독점하므로 rank 기반 union 필요.
|
| 281 |
+
_RRF_K = 60
|
| 282 |
+
queries: list[str] = []
|
| 283 |
+
if plan.search_keywords:
|
| 284 |
+
queries.append(" ".join(plan.search_keywords[:3]))
|
| 285 |
+
if plan.query and plan.query not in queries:
|
| 286 |
+
queries.append(plan.query)
|
| 287 |
+
rrf_scores: dict = {}
|
| 288 |
+
hit_map: dict = {}
|
| 289 |
+
for q in queries:
|
| 290 |
+
for rank, h in enumerate(idx.search(q, k=k)):
|
| 291 |
+
rrf_scores[h.chunk_id] = rrf_scores.get(h.chunk_id, 0.0) + 1.0 / (_RRF_K + rank)
|
| 292 |
+
hit_map.setdefault(h.chunk_id, h)
|
| 293 |
+
top_ids = sorted(rrf_scores, key=lambda i: -rrf_scores[i])[:k]
|
| 294 |
+
hits = [hit_map[i] for i in top_ids]
|
| 295 |
out: list[Excerpt] = []
|
| 296 |
for h in hits:
|
| 297 |
# doc_date "YYYY.MM" → 연도만 추출해 recency 점수에 사용
|