fix: KB dashboard search was applying the strict fiscal name-match rule (used to authorize automatic writes) to human browsing, so a natural query (e.g. missing the unit word) found nothing; now a separate, lenient, global, paginated server-side search — filter over the whole KB, then count/order/paginate. 1.3.6 (Hub only)
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
@@ -100,23 +100,50 @@ def _candidate_cards(session, products) -> list[dict]:
|
||||
return out
|
||||
|
||||
|
||||
def kb_search(session, query: str, limit: int = 6) -> list[dict]:
|
||||
from ..models import KbProduct
|
||||
from . import kb as kbm
|
||||
def kb_search_ids(session, query: str) -> list[int]:
|
||||
"""GLOBAL, server-side match over the whole KB (never limited to a rendered page): GTIN exact, or every normalized query word present somewhere among the
|
||||
product's known names (any alias row OR its own normalized_description - words can come from different aliases of the same product). Deliberately lenient
|
||||
(a human exploring the KB, e.g. "COCA COLA LATA 350" without the unit, must still find "COCA-COLA ORIGINAL LATA 350 ML"); this is DIFFERENT from
|
||||
`kb.alias_match`, the strict rule that authorizes an automatic fiscal write, which stays untouched."""
|
||||
import re
|
||||
|
||||
from ..models import KbKnowledge, KbProduct
|
||||
from . import kb as kbm, textnorm
|
||||
q = (query or "").strip()
|
||||
if not q:
|
||||
return []
|
||||
digits = "".join(ch for ch in q if ch.isdigit())
|
||||
products = []
|
||||
if len(digits) >= 8 and len(digits) == len(q.replace(" ", "")):
|
||||
if len(digits) >= 8 and re.fullmatch(r"[\d\s.\-]+", q):
|
||||
p = kbm.find_by_gtin(session, digits.zfill(14))
|
||||
products = [p] if p else []
|
||||
else:
|
||||
products = kbm.name_candidates(session, q)[:limit]
|
||||
return _candidate_cards(session, products)
|
||||
return [p.id] if p else []
|
||||
tokens = [t for t in textnorm.tokens(q) if t not in textnorm.MARKETING]
|
||||
if not tokens:
|
||||
return []
|
||||
ids: set[int] | None = None
|
||||
for token in tokens: # SQL ILIKE narrows the candidates (portable: Postgres in production, SQLite in tests); the exact whole-word
|
||||
like = f"%{token}%" # check happens in Python so "ML" never matches inside "SIMPLES", etc.
|
||||
matched = {pid for pid, val in session.execute(select(KbKnowledge.kb_product_id, KbKnowledge.value).where(KbKnowledge.knowledge_key == "alias", KbKnowledge.value.ilike(like))) if token in (val or "").split()}
|
||||
matched |= {pid for pid, val in session.execute(select(KbProduct.id, KbProduct.normalized_description).where(KbProduct.normalized_description.ilike(like))) if token in (val or "").split()}
|
||||
ids = matched if ids is None else (ids & matched)
|
||||
if not ids:
|
||||
return []
|
||||
return sorted(ids)
|
||||
|
||||
|
||||
def kb_summary(session, query: str | None = None) -> dict:
|
||||
def kb_search(session, query: str, *, page: int = 1, page_size: int = 10) -> dict:
|
||||
"""query/filter GLOBAL over the whole KB -> count -> order -> pagination (never the other way round)."""
|
||||
from ..models import KbProduct
|
||||
ids = kb_search_ids(session, query)
|
||||
total = len(ids)
|
||||
page = max(1, page)
|
||||
pages = max(1, -(-total // page_size))
|
||||
page = min(page, pages)
|
||||
page_ids = ids[(page - 1) * page_size: page * page_size]
|
||||
products = [session.get(KbProduct, i) for i in page_ids]
|
||||
return {"results": _candidate_cards(session, products), "total": total, "page": page, "pages": pages, "page_size": page_size}
|
||||
|
||||
|
||||
def kb_summary(session, query: str | None = None, page: int = 1) -> dict:
|
||||
"""Compact Knowledge Base panel: known products, statuses of the NCM knowledge, hits today, and the latest conflicts with their evidence (no dashboard of its own)."""
|
||||
from ..models import KbKnowledge, KbProduct
|
||||
today = datetime.now(timezone.utc).strftime("%Y-%m-%d")
|
||||
@@ -134,4 +161,5 @@ def kb_summary(session, query: str | None = None) -> dict:
|
||||
"candidates": [{"ncm": i.value, "clients": sorted({c for c in (i.evidence or {}).get("clients", [])}), "documents": sum(int(l.get("docs", 0)) for l in ((i.evidence or {}).get("lineages") or {}).values()),
|
||||
"providers": sorted(f for f in ((i.evidence or {}).get("families") or {}) if f.startswith("PROVIDER:"))} for i in items]})
|
||||
return {"products": products, "verified": by.get("VERIFIED_KB", 0), "corroborated": by.get("CORROBORATED", 0) + by.get("EXTERNAL_CORROBORATED", 0), "observed": by.get("OBSERVED", 0),
|
||||
"conflicts": by.get("CONFLICT", 0), "stale": by.get("STALE", 0), "hits_today": hits, "recent_conflicts": conflicts, "search": kb_search(session, query) if query else []}
|
||||
"conflicts": by.get("CONFLICT", 0), "stale": by.get("STALE", 0), "hits_today": hits, "recent_conflicts": conflicts,
|
||||
"search": kb_search(session, query, page=page) if query else {"results": [], "total": 0, "page": 1, "pages": 1, "page_size": 10}}
|
||||
|
||||
Reference in New Issue
Block a user