feat(vector): server-side kinds filter for search_similar (legacy fallback) + graceful solved-search degrade

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
2026-07-07 14:23:28 +02:00
co-authored by Claude Fable 5
parent 8d427a2ccb
commit a26f16ad79
7 changed files with 340 additions and 23 deletions
+17 -3
View File
@@ -516,12 +516,26 @@ def solved_search_cmd(
from tht.cli.vector_cmd import make_embedder, open_searcher
from tht.solved import SOLVED_KIND
from tht.vectorstore.embeddings import EmbeddingsError
from tht.vectorstore.rest_client import VectorRestError
cfg = _load_config_or_exit(config)
require_vector_cfg(cfg)
searcher = open_searcher(cfg)
embedder = make_embedder(cfg.embeddings)
hits = searcher.search(embedder.embed_query(question), top_n=top, kinds=[SOLVED_KIND])
# Degrado gentile: SKILL.md prescrive solved-search in F4/F6/F7 di ogni sessione,
# quindi vectordb/Ollama irraggiungibili non devono produrre un traceback grezzo
# nel transcript: avviso di una riga su stderr, stdout puro ([] in --json), exit 0.
try:
searcher = open_searcher(cfg)
embedder = make_embedder(cfg.embeddings)
hits = searcher.search(embedder.embed_query(question), top_n=top, kinds=[SOLVED_KIND])
except (VectorRestError, EmbeddingsError, OperationalError) as e:
typer.secho(
f"ATTENZIONE: exemplar non disponibili ({e}). Prosegui senza.",
fg=typer.colors.YELLOW, err=True,
)
if json_out:
typer.echo("[]")
return
results = [
{
"session_id": h.metadata.get("session_id", h.ref),
+4 -3
View File
@@ -45,10 +45,11 @@ class RestSearcher:
) -> list[VectorHit]:
hits: list[VectorHit] = []
for table in tables_for_kinds(kinds):
for row in self.client.search_similar(table, query_vec, top_n):
for row in self.client.search_similar(table, query_vec, top_n, kinds=kinds):
hits.append(hit_from_metadata(row.get("similarity", 0.0), row.get("metadata")))
# schema_records contiene sia schema_table sia schema_column: la RPC non filtra
# per kind, quindi lo facciamo lato client per parita' col path diretto (#25).
# Il filtro per kind avviene server-side (RPC con `kinds`); il post-filter resta
# come difesa per il fallback legacy (server pre-migrazione: 404 -> query senza
# filtro) e per parita' col path diretto (#25).
if kinds:
allowed = set(kinds)
hits = [h for h in hits if h.kind in allowed]
+20 -10
View File
@@ -58,18 +58,28 @@ class VectorRestClient:
return resp.json()
def search_similar(
self, table_name: str, query_embedding: list[float], limit_count: int
self, table_name: str, query_embedding: list[float], limit_count: int,
kinds: list[str] | None = None,
) -> list[dict]:
"""Ricerca per similarità coseno su `vectors.<table_name>`: ritorna le righe
`{id, similarity, metadata}` ordinate per similarity decrescente."""
return self._call(
"search_similar",
{
"query_embedding": query_embedding,
"limit_count": limit_count,
"table_name": table_name,
},
) or []
`{id, similarity, metadata}` ordinate per similarity decrescente. Con `kinds`
il filtro avviene server-side nel WHERE della RPC (evita la diluizione del
top-k quando piu' kind condividono la tabella, es. memory/solved_question).
Su un server legacy senza il parametro (PostgREST 404) ritenta senza filtro:
resta il post-filter client-side di RestSearcher."""
args = {
"query_embedding": query_embedding,
"limit_count": limit_count,
"table_name": table_name,
}
if kinds is not None:
try:
return self._call("search_similar", {**args, "kinds": kinds}) or []
except VectorRestError as e:
if "HTTP 404" not in str(e):
raise
# funzione a 3 argomenti (pre-migrazione kinds): fallback senza filtro
return self._call("search_similar", args) or []
def list_tables(self) -> list[dict]:
"""Tabelle vettoriali disponibili: `{table_name, vector_dimensions, …}`."""