Files
ThothII/harness/docs/vector-rest-kinds-migration.md
T
marcopanandClaude Opus 4.6 0cad04a8d8 docs(vector): add write-RPC migration instructions (existing_vector_hashes + upsert_vector_records)
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>
2026-07-07 15:15:55 +02:00

6.7 KiB

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):

    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:

    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:

    WHERE (kinds IS NULL OR t.kind = ANY(kinds))
    

    Se il corpo usa SQL dinamico, passa kinds come parametro (USING), non interpolarlo:

    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:

    SELECT proacl FROM pg_proc WHERE proname = 'search_similar';
    
  4. Ricarica lo schema cache di PostgREST — senza questo la nuova firma risponde 404:

    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:

    CREATE INDEX IF NOT EXISTS vectors_memory_kind_idx ON vectors.memory (kind);
    
  6. Test end-to-end (con la API key reader):

    # 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:

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:

-- 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):

# 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:

tht memory solved-index -c workspaces/psd.yaml <session_id>
tht memory solved-search -c workspaces/psd.yaml "domanda di test" --json