feat: hybrid search - add keyword LIKE recall merged with vector results, mark kw hits
This commit is contained in:
parent
bd03743c6f
commit
dfad26d3c7
@ -58,7 +58,27 @@ try:
|
|||||||
except:
|
except:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
# 3. Keyword recall (hybrid search: vector alone misses technical terms)
|
||||||
|
kw_ids = set()
|
||||||
|
kw_rows = []
|
||||||
|
try:
|
||||||
|
tokens = [t for t in query.split() if t][:5] or [query]
|
||||||
|
conds = []
|
||||||
|
nsq = {"kb_id": kb_id}
|
||||||
|
for i, t in enumerate(tokens):
|
||||||
|
conds.append("content LIKE ${kw_" + str(i) + "}$")
|
||||||
|
nsq["kw_" + str(i)] = "%" + t + "%"
|
||||||
|
ksql = "SELECT id, doc_id, content FROM document_chunks WHERE kb_id=${kb_id}$ AND (" + " OR ".join(conds) + ") LIMIT 20"
|
||||||
|
async with get_sor_context(env, 'rag') as sor:
|
||||||
|
krecs = await sor.sqlExe(ksql, nsq)
|
||||||
|
for r in krecs:
|
||||||
|
kw_ids.add(r.id)
|
||||||
|
kw_rows.append({"id": r.id, "doc_id": r.doc_id or "", "score": 0.99, "text": r.content or '', "kw": True})
|
||||||
|
except Exception as e:
|
||||||
|
info('[search_result] keyword recall failed: %s' % e)
|
||||||
|
|
||||||
hits = []
|
hits = []
|
||||||
|
seen = set()
|
||||||
for row in raw_rows:
|
for row in raw_rows:
|
||||||
rid = str(row.get("id", ""))
|
rid = str(row.get("id", ""))
|
||||||
score = row.get("score", 0)
|
score = row.get("score", 0)
|
||||||
@ -75,7 +95,13 @@ for row in raw_rows:
|
|||||||
chunk_text = recs[0].content or ''
|
chunk_text = recs[0].content or ''
|
||||||
doc_id = recs[0].doc_id or doc_id
|
doc_id = recs[0].doc_id or doc_id
|
||||||
if chunk_text:
|
if chunk_text:
|
||||||
hits.append({"id": rid, "doc_id": doc_id, "score": score, "text": chunk_text})
|
is_kw = rid in kw_ids
|
||||||
|
hits.append({"id": rid, "doc_id": doc_id, "score": 0.99 if is_kw else score, "text": chunk_text, "kw": is_kw})
|
||||||
|
seen.add(rid)
|
||||||
|
for kr in kw_rows:
|
||||||
|
if kr["id"] not in seen:
|
||||||
|
hits.append(kr)
|
||||||
|
seen.add(kr["id"])
|
||||||
|
|
||||||
hits.sort(key=lambda x: x.get("score", 0), reverse=True)
|
hits.sort(key=lambda x: x.get("score", 0), reverse=True)
|
||||||
hits = hits[:top_k]
|
hits = hits[:top_k]
|
||||||
@ -91,13 +117,16 @@ else:
|
|||||||
for i, h in enumerate(hits):
|
for i, h in enumerate(hits):
|
||||||
score_pct = round(float(h["score"]) * 100, 1)
|
score_pct = round(float(h["score"]) * 100, 1)
|
||||||
color = "#3b82f6" if score_pct > 60 else ("#10b981" if score_pct > 30 else "#f59e0b")
|
color = "#3b82f6" if score_pct > 60 else ("#10b981" if score_pct > 30 else "#f59e0b")
|
||||||
|
badges = [{"widgettype": "Text", "options": {"text": f"{score_pct}%", "cfontsize": 11, "bgcolor": color, "color": "#fff", "padding": "2px 8px", "borderRadius": "10px"}}]
|
||||||
|
if h.get("kw"):
|
||||||
|
badges.append({"widgettype": "Text", "options": {"text": "📌 关键词命中", "cfontsize": 11, "bgcolor": "#f59e0b", "color": "#fff", "padding": "2px 8px", "borderRadius": "10px", "marginLeft": "6px"}})
|
||||||
subwidgets.append({
|
subwidgets.append({
|
||||||
"widgettype": "VBox",
|
"widgettype": "VBox",
|
||||||
"options": {"padding": "12px 16px", "marginBottom": "8px", "border": "1px solid #e0e0e0", "borderLeft": f"3px solid {color}", "bgcolor": "#fafafa", "borderRadius": "4px"},
|
"options": {"padding": "12px 16px", "marginBottom": "8px", "border": "1px solid #e0e0e0", "borderLeft": f"3px solid {color}", "bgcolor": "#fafafa", "borderRadius": "4px"},
|
||||||
"subwidgets": [
|
"subwidgets": [
|
||||||
{"widgettype": "HBox", "options": {"alignItems": "center", "marginBottom": "6px"}, "subwidgets": [
|
{"widgettype": "HBox", "options": {"alignItems": "center", "marginBottom": "6px"}, "subwidgets": [
|
||||||
{"widgettype": "Text", "options": {"text": f"#{i+1}", "cfontsize": 12, "fontWeight": "bold", "color": color, "marginRight": "8px"}},
|
{"widgettype": "Text", "options": {"text": f"#{i+1}", "cfontsize": 12, "fontWeight": "bold", "color": color, "marginRight": "8px"}},
|
||||||
{"widgettype": "Text", "options": {"text": f"{score_pct}%", "cfontsize": 11, "bgcolor": color, "color": "#fff", "padding": "2px 8px", "borderRadius": "10px"}},
|
*badges,
|
||||||
{"widgettype": "Text", "options": {"text": f" {h['id'][:16]}", "cfontsize": 11, "color": "#999", "marginLeft": "8px"}}
|
{"widgettype": "Text", "options": {"text": f" {h['id'][:16]}", "cfontsize": 11, "color": "#999", "marginLeft": "8px"}}
|
||||||
]},
|
]},
|
||||||
{"widgettype": "Text", "options": {"text": h["text"][:300], "cfontsize": 13, "color": "#333", "lineHeight": "1.6"}}
|
{"widgettype": "Text", "options": {"text": h["text"][:300], "cfontsize": 13, "color": "#333", "lineHeight": "1.6"}}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user