"""Lettura del pgvector dietro un'unica interfaccia `.search(query_vec, top_n, kinds)`, così `search.combined_search` resta agnostico al transport. Due implementazioni: - `RestSearcher` → produzione: similarity search via REST (`search_similar`). - `DirectSearcher` → dev/test: connessione diretta a Postgres/pgvector. Entrambe mappano i `kind` sulle tabelle per-dominio dello schema `vectors`. """ from sqlalchemy import Engine from tht.vectorstore.rest_client import VectorRestClient from tht.vectorstore.store import VectorHit, VectorStore, hit_from_metadata # kind Thoth → tabella dello schema `vectors`. KIND_TO_TABLE = { "schema_table": "schema_records", "schema_column": "schema_records", "evidence": "evidence", "memory": "memory", "solved_question": "memory", # coppie domanda->SQL: stessa tabella, kind dedicato } ALL_TABLES = ["schema_records", "evidence", "memory"] def tables_for_kinds(kinds: list[str] | None) -> list[str]: """Tabelle da interrogare per i kind richiesti (tutte se kinds è vuoto/None).""" if not kinds: return list(ALL_TABLES) return sorted({KIND_TO_TABLE[k] for k in kinds if k in KIND_TO_TABLE}) def _merge(hits: list[VectorHit], top_n: int) -> list[VectorHit]: return sorted(hits, key=lambda h: h.similarity, reverse=True)[:top_n] class RestSearcher: """Similarity search via REST: una chiamata `search_similar` per tabella, poi fusione.""" def __init__(self, client: VectorRestClient): self.client = client def search( self, query_vec: list[float], top_n: int = 10, kinds: list[str] | None = None ) -> list[VectorHit]: hits: list[VectorHit] = [] for table in tables_for_kinds(kinds): 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"))) # 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] return _merge(hits, top_n) class DirectSearcher: """Similarity search diretta su Postgres/pgvector, interrogando le tabelle per-dominio.""" def __init__(self, engine: Engine, schema: str = "vectors", dim: int = 768): self.engine = engine self.schema = schema self.dim = dim def search( self, query_vec: list[float], top_n: int = 10, kinds: list[str] | None = None ) -> list[VectorHit]: hits: list[VectorHit] = [] for table in tables_for_kinds(kinds): store = VectorStore(self.engine, schema=self.schema, table=table, dim=self.dim) # passa kinds: dentro schema_records filtra schema_table vs schema_column (#25). hits.extend(store.search(query_vec, top_n=top_n, kinds=kinds)) return _merge(hits, top_n)