The original doc only covered search_similar; the write functions also need solved_question in their kind whitelist for the memory table. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
151 lines
6.7 KiB
Markdown
151 lines
6.7 KiB
Markdown
# Migrazione server: filtro `kinds` nella RPC `search_similar`
|
|
|
|
**Contesto.** Il vectorstore ThothII espone la similarity search via PostgREST/Supabase:
|
|
`POST <base_url>/rpc/search_similar` con payload `{query_embedding, limit_count, table_name}`.
|
|
Da quando `memory` e `solved_question` condividono la tabella `vectors.memory` (feature
|
|
"active memory", 2026-07-07), il top-k della tabella mista diluisce i risultati: il filtro
|
|
per kind avviene client-side DOPO il taglio a `limit_count`. Serve il filtro nel `WHERE`
|
|
della funzione SQL.
|
|
|
|
**Lato client: già pronto.** Il harness manda `kinds` nel payload quando filtra
|
|
(`tht memory search`, `tht memory solved-search`); se il server risponde 404
|
|
(funzione a 3 argomenti, pre-migrazione) ritenta senza `kinds` e filtra client-side.
|
|
Quindi **l'ordine di deploy è libero** — ma finché la migrazione non è fatta il filtro
|
|
resta client-side e la diluizione persiste.
|
|
|
|
## Cosa fare (istruzioni per il Claude del server)
|
|
|
|
Obiettivo: aggiungere a `search_similar` un quarto parametro `kinds text[] DEFAULT NULL`
|
|
che filtra `kind = ANY(kinds)` quando valorizzato, preservando il comportamento attuale
|
|
quando assente.
|
|
|
|
1. **Recupera la definizione corrente** (non riscriverla a memoria):
|
|
```sql
|
|
SELECT pg_get_functiondef(oid)
|
|
FROM pg_proc
|
|
WHERE proname = 'search_similar';
|
|
```
|
|
Prendi nota di: schema della funzione, `LANGUAGE`, `SECURITY DEFINER` e `SET search_path`
|
|
se presenti, shape del risultato (le righe devono restare `{id, similarity, metadata}`),
|
|
e come viene usato `table_name` (quasi certamente SQL dinamico con `EXECUTE format(...)`).
|
|
|
|
2. **Sostituisci la funzione in una sola transazione.** In Postgres non si può aggiungere
|
|
un parametro con ALTER: `CREATE OR REPLACE` con firma diversa creerebbe un **overload**,
|
|
e due overload con lo stesso nome mandano PostgREST in ambiguità (PGRST203) per le
|
|
chiamate senza `kinds`. Quindi:
|
|
```sql
|
|
BEGIN;
|
|
DROP FUNCTION search_similar(vector, integer, text); -- adatta la firma esatta trovata al punto 1
|
|
CREATE FUNCTION search_similar(
|
|
query_embedding vector,
|
|
limit_count integer,
|
|
table_name text,
|
|
kinds text[] DEFAULT NULL
|
|
) RETURNS ... -- stessa shape di prima
|
|
...
|
|
COMMIT;
|
|
```
|
|
Nel corpo, la condizione deve essere **null-safe** così i chiamanti legacy (senza
|
|
`kinds`) mantengono il comportamento attuale:
|
|
```sql
|
|
WHERE (kinds IS NULL OR t.kind = ANY(kinds))
|
|
```
|
|
Se il corpo usa SQL dinamico, passa `kinds` come parametro (`USING`), non interpolarlo:
|
|
```sql
|
|
EXECUTE format(
|
|
'SELECT metadata, 1 - (embedding <=> $1) AS similarity
|
|
FROM vectors.%I
|
|
WHERE ($3::text[] IS NULL OR kind = ANY($3))
|
|
ORDER BY embedding <=> $1
|
|
LIMIT $2', table_name)
|
|
USING query_embedding, limit_count, kinds;
|
|
```
|
|
(Adatta al corpo reale: l'esempio mostra solo dove inserire la condizione.)
|
|
|
|
3. **Ripristina i GRANT.** Il DROP cancella i grant della funzione: ri-esegui gli
|
|
`GRANT EXECUTE` che la definizione originale aveva (ruolo reader della REST, es.
|
|
`vector_reader`, e il ruolo con cui PostgREST esegue le RPC). Verifica con:
|
|
```sql
|
|
SELECT proacl FROM pg_proc WHERE proname = 'search_similar';
|
|
```
|
|
|
|
4. **Ricarica lo schema cache di PostgREST** — senza questo la nuova firma risponde 404:
|
|
```sql
|
|
NOTIFY pgrst, 'reload schema';
|
|
```
|
|
(oppure riavvia il servizio PostgREST).
|
|
|
|
5. **Verifica indice.** La colonna `kind` dovrebbe già avere un indice
|
|
(`<table>_kind_idx`); se manca sulla tabella `memory`:
|
|
```sql
|
|
CREATE INDEX IF NOT EXISTS vectors_memory_kind_idx ON vectors.memory (kind);
|
|
```
|
|
|
|
6. **Test end-to-end** (con la API key reader):
|
|
```bash
|
|
# senza kinds (comportamento legacy) — deve rispondere come prima
|
|
curl -s -X POST "<base_url>/rpc/search_similar" -H "X-API-Key: $KEY" \
|
|
-H "Content-Type: application/json" \
|
|
-d '{"query_embedding": [...], "limit_count": 3, "table_name": "memory"}'
|
|
# con kinds — deve restituire SOLO righe con metadata.kind = "solved_question"
|
|
curl -s -X POST "<base_url>/rpc/search_similar" -H "X-API-Key: $KEY" \
|
|
-H "Content-Type: application/json" \
|
|
-d '{"query_embedding": [...], "limit_count": 3, "table_name": "memory",
|
|
"kinds": ["solved_question"]}'
|
|
```
|
|
Poi dal workstation: `tht memory solved-search "<domanda>" --json` e
|
|
`tht memory search "<domanda>" --json` devono continuare a funzionare (il secondo
|
|
ora senza righe solved nei top-k).
|
|
|
|
## Funzioni di scrittura: `existing_vector_hashes` e `upsert_vector_records`
|
|
|
|
Entrambe hanno una validazione interna che ammette solo i kind storici per ogni tabella.
|
|
Per la tabella `memory`, il set ammesso va ampliato a `{"memory", "solved_question"}`.
|
|
|
|
**Come trovare la validazione.** Recupera la definizione di ciascuna funzione:
|
|
```sql
|
|
SELECT pg_get_functiondef(oid) FROM pg_proc WHERE proname = 'existing_vector_hashes';
|
|
SELECT pg_get_functiondef(oid) FROM pg_proc WHERE proname = 'upsert_vector_records';
|
|
```
|
|
Cerca la riga che controlla i kind ammessi (es. `IF kind NOT IN ('memory') THEN RAISE`,
|
|
oppure un array literal `ARRAY['memory']`, oppure una lookup su una tabella di
|
|
configurazione). La modifica è semplicemente aggiungere `'solved_question'` alla lista
|
|
ammessa per `memory`.
|
|
|
|
**Esempio di pattern tipico:**
|
|
```sql
|
|
-- PRIMA:
|
|
IF NOT (kind = ANY(ARRAY['memory'])) THEN
|
|
RAISE EXCEPTION 'Invalid kind for %', table_name;
|
|
END IF;
|
|
|
|
-- DOPO:
|
|
IF NOT (kind = ANY(ARRAY['memory', 'solved_question'])) THEN
|
|
RAISE EXCEPTION 'Invalid kind for %', table_name;
|
|
END IF;
|
|
```
|
|
|
|
Applica la stessa tecnica DROP+CREATE in transazione già usata per `search_similar`
|
|
(mai `CREATE OR REPLACE` con firma diversa). Ripristina i GRANT dopo il DROP.
|
|
|
|
**Test delle funzioni di scrittura** (con la API key writer):
|
|
```bash
|
|
# existing_vector_hashes — deve accettare kinds = ['solved_question']
|
|
curl -s -X POST "<write_base_url>/rpc/existing_vector_hashes" -H "X-API-Key: $WRITE_KEY" \
|
|
-H "Content-Type: application/json" \
|
|
-d '{"table_name": "memory", "kinds": ["solved_question"]}'
|
|
# atteso: 200 con array (vuoto o con hash)
|
|
|
|
# upsert_vector_records — deve accettare kind = 'solved_question'
|
|
curl -s -X POST "<write_base_url>/rpc/upsert_vector_records" -H "X-API-Key: $WRITE_KEY" \
|
|
-H "Content-Type: application/json" \
|
|
-d '{"table_name": "memory", "rows": [{"record_key": "solved:__test__", "kind": "solved_question", "content_hash": "test", "metadata": {"id": "solved:__test__", "kind": "solved_question"}, "embedding": [0.0]}]}'
|
|
# atteso: 200 (upsert riuscito); l'embedding dummy verrà sovrascritto dal test vero
|
|
```
|
|
|
|
Dopo aver applicato le modifiche e verificato con curl, ritesta dal workstation:
|
|
```bash
|
|
tht memory solved-index -c workspaces/psd.yaml <session_id>
|
|
tht memory solved-search -c workspaces/psd.yaml "domanda di test" --json
|
|
```
|