~/ / Articoli sull'IA / LLM locali su un VPS / Salvare e cercare gli embedding di Ollama in PostgreSQL con pgvector
Salvare e cercare gli embedding di Ollama in PostgreSQL con pgvector
Gli embedding di /api/embed di Ollama servono per la ricerca solo dopo essere stati salvati da qualche parte. pgvector è un'estensione open source che aggiunge a PostgreSQL la ricerca per similarità di vettori, così i vettori possono stare nello stesso database dei testi e del resto dei tuoi dati, sullo stesso server. Questo articolo spiega come installarla, creare la tabella, scegliere la distanza e l'indice, e le impostazioni che cambiano i risultati, in base al README di pgvector per la versione 0.8.7. Poi passa i risultati della ricerca a /api/chat di Ollama perché la risposta indichi le sue fonti, e verifica quelle citazioni. Abbiamo eseguito l'SQL su pgvector 0.8.7 con PostgreSQL 16, usando vettori reali di Ollama, e le richieste di chat su Ollama 0.40.11 con qwen2.5:7b. Quattro strumenti in questa pagina scrivono l'SQL e la richiesta e verificano la risposta nel tuo browser.
Pubblicato: · 11 min di lettura · Fatti verificati sul README e sul codice sorgente di pgvector per la versione 0.8.7 e sulla documentazione e sul codice sorgente di Ollama (0.40.11) il 2026-11-05. Controlla la documentazione attuale prima di farci affidamento.
Cosa aggiunge pgvector a PostgreSQL
Il README di pgvector lo descrive come ricerca per similarità di vettori open source per Postgres. Supporta la ricerca esatta e approssimata dei vicini più prossimi; vettori a precisione singola, a mezza precisione, binari e sparsi; e distanza L2, prodotto interno, distanza del coseno, distanza L1, distanza di Hamming e distanza di Jaccard. Poiché i vettori sono normali colonne di PostgreSQL, hanno anche la conformità ACID, il ripristino a un punto nel tempo e i JOIN.
Per una ricerca nei tuoi documenti, salvi una riga per ogni frammento con il suo testo e il suo vettore, calcoli l'embedding della domanda con lo stesso modello e ordini le righe in base alla loro distanza dal vettore della domanda.
Installarlo
pgvector supporta PostgreSQL 13 e successivi. Il README elenca vari modi per installarlo, tra cui questi:
| Metodo | Cosa dice il README |
|---|---|
| Compilare dai sorgenti | git clone --branch v0.8.7, poi make e make install (può servire sudo) |
| Docker | L'immagine pgvector/pgvector aggiunge pgvector all'immagine di Postgres. Tag come pg18-trixie o 0.8.7-pg16 scelgono la versione di Postgres. |
| APT (Debian, Ubuntu) | sudo apt install postgresql-18-pgvector dal repository APT di PostgreSQL, sostituendo 18 con la tua versione di Postgres |
| Homebrew | brew install pgvector, che lo aggiunge solo alle formule postgresql@18 e postgresql@17 |
Poi attivalo una volta in ogni database in cui vuoi usarlo, con CREATE EXTENSION vector;. Per questo articolo abbiamo usato l'immagine Docker pgvector/pgvector:0.8.7-pg16.
Creare la tabella
Una colonna di tipo vector(n) contiene vettori con esattamente n dimensioni, e n deve essere la lunghezza dei vettori del tuo modello di embedding. Un vettore di lunghezza diversa viene rifiutato: quando abbiamo inserito un vettore con 2 dimensioni in una colonna vector(3), PostgreSQL ha restituito "expected 3 dimensions, not 2". all-minilm restituisce 384 dimensioni, quindi la sua colonna è vector(384).
Il README indica che ogni vettore occupa 4 × dimensioni + 8 byte, con ogni elemento a virgola mobile a precisione singola, e consente fino a 16,000 dimensioni in una colonna. Un indice supporta meno dimensioni, come mostrato più avanti. Il tipo halfvec memorizza valori a mezza precisione in 2 × dimensioni + 8 byte.
| Dimensioni | vector | halfvec |
|---|---|---|
| 384 | 1,544 byte | 776 byte |
| 768 | 3,080 byte | 1,544 byte |
| 1024 | 4,104 byte | 2,056 byte |
Sulla nostra tabella di prova, pg_column_size ha dato 1,544 byte per vector(384) e 776 byte per halfvec(384), in accordo con la formula. Questo è solo il vettore; la colonna di testo e gli indici occupano spazio aggiuntivo.
Caricare i vettori di Ollama
pgvector legge un vettore come testo tra parentesi quadre, come '[1,2,3]', e il README dice che tutti gli elementi devono essere finiti (niente NaN, Infinity o -Infinity). Ogni array nel campo embeddings di una risposta di /api/embed ha già questa forma, e la risposta mantiene l'ordine dell'input, quindi il primo testo corrisponde al primo vettore.
Il README mostra INSERT per poche righe, INSERT … ON CONFLICT per gli upsert e COPY per il caricamento in blocco. Il convertitore qui sotto abbina i tuoi testi alla risposta e scrive istruzioni INSERT con gli apici escapati. Abbiamo eseguito il suo output con testi contenenti apici, backslash, a capo ed emoji, e sono tornati invariati.
Provalo: Embeddings to pgvector INSERT
L'esempio usa vettori inventati, di lunghezza unitaria e con 4 dimensioni, per poterli leggere. Gli embedding reali hanno centinaia o migliaia di dimensioni.
3 righe con 4 dimensioni. Testi letti come il corpo della richiesta a /api/embed, vettori come una risposta di Ollama /api/embed.
Istruzioni INSERT
Eseguile in psql o in un altro client. Le righe vengono inserite in un'unica transazione.
BEGIN;
INSERT INTO documents (content, embedding) VALUES
('Embeddings turn text into numeric vectors.', '[0.620186,0.410123,0.550165,0.380114]'),
('pgvector stores vectors in PostgreSQL, next to the rest of your data.', '[0.578815,0.469039,0.518937,0.419142]'),
('Ollama''s /api/embed returns one vector per input, in the same order.', '[0.119982,0.809879,-0.329951,0.46993]');
COMMIT;Formato dei vettori dal README di pgvector, verificato con pgvector 0.8.7 il 2026-11-04. README di pgvector
Tutto avviene nel tuo browser. Lo strumento scrive SQL e non si connette a nulla.
Scegliere la distanza
La documentazione di Ollama consiglia la similarità del coseno per la maggior parte dei casi di ricerca semantica e dice che /api/embed restituisce vettori di lunghezza 1. Per tali vettori, la distanza del coseno, la distanza L2 e il prodotto interno ordinano le righe allo stesso modo, perché il quadrato della distanza L2 è 2 meno 2 volte la similarità del coseno. Nel nostro test con all-minilm, le prime tre righe erano le stesse con tutte e tre, e la distanza L2 di 0.931 corrispondeva alla similarità del coseno di 0.566.
Ogni distanza ha il proprio operatore. Una ricerca ordina per l'operatore e prende le prime righe con LIMIT, e un indice viene creato per una classe di operatori.
| Operatore | Restituisce | Classe di operatori dell'indice |
|---|---|---|
| <=> | Distanza del coseno. La similarità del coseno è 1 - (a <=> b). | vector_cosine_ops |
| <-> | Distanza L2 (euclidea) | vector_l2_ops |
| <#> | Il prodotto interno negativo. Moltiplica per -1 per ottenere il prodotto interno. | vector_ip_ops |
| <+> | Distanza L1 | vector_l1_ops |
Il README spiega che <#> restituisce il prodotto interno negativo perché Postgres supporta solo scansioni di indice in ordine ASC sugli operatori. Per halfvec, le classi di operatori sono halfvec_cosine_ops, halfvec_l2_ops e così via.
Ricerca esatta o indice
Senza indice, pgvector esegue una ricerca esatta dei vicini più prossimi, che secondo il README offre un recall perfetto. Un indice approssimato cede un po' di recall in cambio di velocità, e il README avverte che le query possono restituire risultati diversi dopo averne aggiunto uno.
Un indice viene usato solo quando la query ha ORDER BY e LIMIT e l'ORDER BY è l'operatore di distanza stesso in ordine crescente. ORDER BY embedding <=> $1 LIMIT 5 può usare l'indice; ORDER BY 1 - (embedding <=> $1) DESC no. Il README osserva anche che una scansione della tabella può essere più veloce quando la tabella è piccola.
| Aspetto | HNSW | IVFFlat |
|---|---|---|
| Come funziona | Un grafo a più livelli | Divide i vettori in liste e cerca nelle liste più vicine alla query |
| Rispetto all'altro | Miglior compromesso tra velocità e recall, ma più lento da costruire e usa più memoria | Più veloce da costruire e usa meno memoria, ma con un compromesso peggiore tra velocità e recall |
| Quando crearlo | Si può creare su una tabella vuota (nessun passo di addestramento) | Dopo che la tabella contiene dei dati |
| Opzioni di costruzione | m (16 per impostazione predefinita) ed ef_construction (64 per impostazione predefinita) | lists: inizia con rows / 1000 fino a 1M righe, e con sqrt(rows) oltre |
| Opzione di ricerca | hnsw.ef_search, 40 per impostazione predefinita | ivfflat.probes, 1 per impostazione predefinita; inizia con sqrt(lists) |
| Dimensioni | vector fino a 2,000, halfvec fino a 4,000 | vector fino a 2,000, halfvec fino a 4,000 |
Su pgvector 0.8.7 abbiamo anche confermato che una colonna vector(2001) non può avere un indice HNSW, che IVFFlat non ha una classe di operatori per la distanza L1 e che ef_construction deve essere almeno 2 × m. Per più dimensioni, le FAQ del README elencano halfvec, la quantizzazione binaria e altre opzioni.
Provalo: pgvector SQL Builder
Ogni vettore occupa 1,544 byte, quindi 10,000 righe occupano circa 15.4 MB per i soli vettori (indici e altre colonne esclusi).
1. Estensione e tabella
CREATE EXTENSION IF NOT EXISTS vector; CREATE TABLE documents ( id bigserial PRIMARY KEY, content text NOT NULL, embedding vector(384) NOT NULL );
2. Indice
CREATE INDEX ON documents USING hnsw (embedding vector_cosine_ops) WITH (m = 16, ef_construction = 64);
3. Ricerca
PREPARE riceve come $1 il vettore della domanda ottenuto da /api/embed. Passalo come testo, per esempio '[0.01,-0.02,…]'.
PREPARE search_documents(vector(384)) AS
SELECT id, content, 1 - (embedding <=> $1) AS cosine_similarity
FROM documents
ORDER BY embedding <=> $1
LIMIT 5;
-- EXECUTE search_documents('[0.0123, -0.0456, ...]');Limiti e valori predefiniti dal README e dal codice sorgente di pgvector 0.8.7, verificati il 2026-11-04. README di pgvector
Tutto avviene nel tuo browser. Lo strumento scrive SQL e non si connette a nulla.
Perché una ricerca può restituire meno righe di LIMIT
Con HNSW, i risultati sono limitati da hnsw.ef_search, che è 40 per impostazione predefinita. Su una tabella di prova di 200 righe con un indice HNSW, LIMIT 100 ha restituito 40 righe; dopo SET hnsw.ef_search = 100, ne ha restituite 100. Imposta hnsw.ef_search ad almeno il numero di righe che ti servono. SET LOCAL all'interno di una transazione lo cambia per una sola query.
Anche il filtraggio riduce i risultati. Con gli indici approssimati, il README dice che la condizione WHERE viene applicata dopo la scansione dell'indice: se una condizione corrisponde al 10% delle righe, HNSW con il valore predefinito di hnsw.ef_search, 40, restituisce in media solo 4 righe. Dalla versione 0.8.0, le scansioni iterative dell'indice (SET hnsw.iterative_scan = strict_order, oppure relaxed_order) scorrono più parte dell'indice finché non trovano abbastanza righe, fino a hnsw.max_scan_tuples (20,000 per impostazione predefinita).
Il README aggiunge che i vettori NULL non vengono indicizzati, e neppure i vettori zero per la distanza del coseno.
Passare i risultati a /api/chat con fonti numerate
Quando la ricerca restituisce le righe più vicine, il modello ne ha bisogno nel suo prompt. /api/chat di Ollama accetta un elenco di messaggi, ciascuno con un ruolo (system, user, assistant o tool) e il suo content. Una struttura semplice è un messaggio system con le regole e un messaggio user che elenca le righe, ognuna con un numero, come [1] e [2], seguite dalla domanda. Con i numeri, la risposta può dire quali righe ha usato, e puoi ricondurre ogni numero alla sua riga.
Per ottenere le citazioni in una forma fissa, dai al campo format un JSON Schema con la risposta e un elenco di citazioni, ciascuna con il numero della fonte e un breve testo citato. La pagina di Ollama sugli output strutturati consiglia di scrivere lo schema anche nel prompt e di abbassare la temperatura (temperature), per esempio a 0, per un output più deterministico. In psql, la funzione json_agg raccoglie le righe di una query in un array JSON, che lo strumento qui sotto legge direttamente.
Provalo: RAG Prompt Builder
Le fonti dell'esempio sono scritte a mano nella forma che restituisce json_agg.
3 fonti, lette come un array JSON di oggetti.
Comando curl
Invia la richiesta a Ollama su questa macchina (localhost:11434).
curl http://localhost:11434/api/chat -d '{
"model": "qwen2.5:7b",
"messages": [
{
"role": "system",
"content": "Answer the question using only the numbered sources in the user message. Cite the number of each source you use. If the sources do not contain the answer, say that you do not know and cite nothing. Answer in the same language as the question. For each citation, give the source number and copy a short quote from that source exactly as it is written. Return JSON that matches this schema: {\"type\":\"object\",\"properties\":{\"answer\":{\"type\":\"string\"},\"citations\":{\"type\":\"array\",\"items\":{\"type\":\"object\",\"properties\":{\"source\":{\"type\":\"integer\"},\"quote\":{\"type\":\"string\"}},\"required\":[\"source\",\"quote\"]}}},\"required\":[\"answer\",\"citations\"]}"
},
{
"role": "user",
"content": "Sources:\n\n[1] (id: 12)\nWith HNSW, hnsw.ef_search is 40 by default, so a query returns at most 40 rows unless you raise it.\n\n[2] (id: 7)\nSET LOCAL inside a transaction changes hnsw.ef_search for a single query.\n\n[3] (id: 3)\npgvector supports PostgreSQL 13 and later.\n\nQuestion: Why does my HNSW search return only 40 rows with LIMIT 100?"
}
],
"stream": false,
"format": {
"type": "object",
"properties": {
"answer": {
"type": "string"
},
"citations": {
"type": "array",
"items": {
"type": "object",
"properties": {
"source": {
"type": "integer"
},
"quote": {
"type": "string"
}
},
"required": [
"source",
"quote"
]
}
}
},
"required": [
"answer",
"citations"
]
},
"options": {
"temperature": 0
},
"truncate": false
}'Corpo della richiesta (JSON)
{
"model": "qwen2.5:7b",
"messages": [
{
"role": "system",
"content": "Answer the question using only the numbered sources in the user message. Cite the number of each source you use. If the sources do not contain the answer, say that you do not know and cite nothing. Answer in the same language as the question. For each citation, give the source number and copy a short quote from that source exactly as it is written. Return JSON that matches this schema: {\"type\":\"object\",\"properties\":{\"answer\":{\"type\":\"string\"},\"citations\":{\"type\":\"array\",\"items\":{\"type\":\"object\",\"properties\":{\"source\":{\"type\":\"integer\"},\"quote\":{\"type\":\"string\"}},\"required\":[\"source\",\"quote\"]}}},\"required\":[\"answer\",\"citations\"]}"
},
{
"role": "user",
"content": "Sources:\n\n[1] (id: 12)\nWith HNSW, hnsw.ef_search is 40 by default, so a query returns at most 40 rows unless you raise it.\n\n[2] (id: 7)\nSET LOCAL inside a transaction changes hnsw.ef_search for a single query.\n\n[3] (id: 3)\npgvector supports PostgreSQL 13 and later.\n\nQuestion: Why does my HNSW search return only 40 rows with LIMIT 100?"
}
],
"stream": false,
"format": {
"type": "object",
"properties": {
"answer": {
"type": "string"
},
"citations": {
"type": "array",
"items": {
"type": "object",
"properties": {
"source": {
"type": "integer"
},
"quote": {
"type": "string"
}
},
"required": [
"source",
"quote"
]
}
}
},
"required": [
"answer",
"citations"
]
},
"options": {
"temperature": 0
},
"truncate": false
}Nella risposta, il testo si trova in message.content. Se prompt_eval_count è molto più basso di quanto ti aspetti, il prompt potrebbe essere stato tagliato.
Campi della richiesta dalla documentazione di riferimento dell'API e dal codice sorgente di Ollama, verificati su Ollama 0.40.11 il 2026-11-05. Ollama API: /api/chat
Tutto avviene nel tuo browser. Lo strumento scrive la richiesta e non si collega a nulla.
Quando le fonti non ci stanno, Ollama taglia l'inizio del prompt
Le regole, le righe e la domanda devono stare tutte nella lunghezza del contesto. La pagina di Ollama sulla lunghezza del contesto indica un valore predefinito di 4k token con meno di 24 GiB di VRAM, e le sue FAQ indicano 4096 token; ollama ps sulla nostra macchina di prova mostrava 4096. num_ctx in options o la variabile d'ambiente OLLAMA_CONTEXT_LENGTH lo cambiano.
Quando il prompt è più lungo, /api/chat per impostazione predefinita non restituisce un errore. Nel codice sorgente di Ollama (0.40.11), il server scarta prima interi messaggi precedenti, mantenendo i messaggi system e l'ultimo messaggio. Se il prompt rimasto è ancora troppo lungo, mantiene i primi token (num_keep, 4 per impostazione predefinita) e la fine del prompt e scarta la parte in mezzo. Nel nostro test con qwen2.5:7b e num_ctx 1024, un prompt di 2,068 token è stato ridotto a 514. Il messaggio system e le prime fonti sono andati persi: con la risposta nella fonte [1], il modello ha risposto con una parola in codice che non era in nessuna fonte, e con la risposta nell'ultima fonte ha risposto correttamente ma senza il numero della fonte che il messaggio system richiedeva. La risposta non conteneva alcun errore; il log del server riportava l'avviso "truncating input prompt".
Il codice sorgente di Ollama ha anche un campo truncate per le richieste di chat, che non è elencato nella pagina di /api/chat. Con "truncate": false, la stessa richiesta ha restituito HTTP 400 con l'errore "request (2068 tokens) exceeds the available context size (1024 tokens), try increasing it". Il campo prompt_eval_count di una risposta è il numero di token del prompt; nel test con il taglio era 514, non 2,068.
Poiché la fine del prompt viene mantenuta, metti la domanda dopo le fonti. Per scoprire quando le fonti non ci stanno, invia "truncate": false e poi aumenta num_ctx o invia meno righe. Un num_ctx più grande richiede più memoria.
Verificare le citazioni
Una citazione si può verificare in modo meccanico: il numero deve riferirsi a una fonte che è stata inviata, e il testo citato dovrebbe comparire in quella fonte. Nel nostro test con quattro fonti e qwen2.5:7b, i testi citati nelle risposte in JSON corrispondevano parola per parola alle fonti. Questo non dimostra che la risposta sia giusta: alla domanda sulla dimensione di un vettore a 384 dimensioni, il modello ha citato la regola corretta (4 volte le dimensioni più 8 byte) e ha comunque risposto 1552 byte invece di 1,544.
Guarda anche le risposte senza citazioni. Quando le fonti non contenevano la risposta, il modello una volta ha risposto con le proprie conoscenze senza citazioni, e un'altra volta ha detto di non saperlo ma ha citato comunque una fonte senza attinenza. Se done_reason nella risposta è "length", la risposta è stata interrotta da num_predict e potrebbero mancare le citazioni.
Il verificatore qui sotto confronta solo il testo. Se una fonte sostenga davvero una frase deve ancora giudicarlo una persona.
Provalo: RAG Citation Checker
La risposta dell'esempio è scritta a mano per mostrare un testo citato che non è nella sua fonte.
4 fonti (un array JSON di stringhe). Risposta letta come una risposta di /api/chat di Ollama, nella forma di JSON con citazioni.
Token del prompt (prompt_eval_count): 306
- Il testo citato della citazione #2 non è nella fonte [4].
Citazioni
| Fonte | Testo citato | Risultato |
|---|---|---|
| [2] | hnsw.ef_search is 40 by default | Trovato così com'è |
| [4] | 4 bytes per dimension plus 8 | Non è in questa fonte |
Questo verifica solo che i numeri e i testi citati corrispondano alle fonti. Non può dire se la risposta è corretta né se una fonte sostiene davvero una frase.
Formato della risposta dalla documentazione di riferimento dell'API e dal codice sorgente di Ollama, verificato su Ollama 0.40.11 il 2026-11-05. Ollama API: /api/chat
Tutto avviene nel tuo browser. Lo strumento non si collega a nulla.
La memoria su un server piccolo
Il README dice che un indice HNSW si costruisce in modo molto più veloce quando il grafo sta in maintenance_work_mem, mostra un avviso quando non ci sta più e avverte di non impostare maintenance_work_mem così alto da esaurire la memoria del server. Come per gli altri indici, è più veloce creare l'indice dopo aver caricato i dati iniziali. Gli indici non devono per forza stare in memoria, ma probabilmente otterrai prestazioni migliori se ci stanno; SELECT pg_size_pretty(pg_relation_size('index_name')) mostra la dimensione di un indice.
Per un insieme di lavoro e indici più piccoli, il README suggerisce halfvec, l'indicizzazione a mezza precisione o la quantizzazione binaria. Se Ollama gira sullo stesso server, PostgreSQL e i modelli condividono la stessa memoria; il nostro articolo su come tenere d'occhio la memoria di Ollama spiega cosa decide quanta ne usano i modelli.
Fonti
- pgvector README (v0.8.7)
- pgvector source: HNSW limits (src/hnsw.h)
- pgvector source: IVFFlat limits (src/ivfflat.h)
- Ollama: Embeddings
- Ollama API: Generate embeddings (/api/embed)
- PostgreSQL documentation: Lexical Structure (string constants)
- Ollama API: Generate a chat message (/api/chat)
- Ollama: Structured Outputs
- Ollama: Context length
- Ollama FAQ (context window size)
- Ollama source v0.40.11: server/prompt.go (dropping earlier messages)
- Ollama source v0.40.11: llm/llama_server.go (truncating the prompt)
- Ollama source v0.40.11: api/types.go (truncate, num_keep)
- PostgreSQL documentation: Aggregate Functions (json_agg)
Leggi anche
- Embedding con Ollama: le basi per ricerca e RAGCrea embedding con l'/api/embed di Ollama: i modelli e le loro dimensioni, truncate e dimensions, vettori di lunghezza unitaria e similarità del coseno, secondo la documentazione ufficiale.
- Tenere d'occhio la memoria di Ollama su una piccola VPSScopri cosa Ollama tiene in memoria, per quanto tempo e quali impostazioni lo cambiano: ollama ps, keep_alive e i limiti, dalle FAQ ufficiali.
- Collegare Dify a Ollama sul proprio VPSEsegui Dify sul tuo VPS e usa Ollama come modello: installazione, campi del plugin, il problema dell'indirizzo in Docker e gli embedding, dalla documentazione ufficiale.
- Avviare un LLM locale senza un PC costoso: guida per principianti all'uso di una VPSNon hai un PC potente? Affitta una VPS ed esegui un piccolo LLM locale dal browser: sei passaggi, dalla stima della memoria a una chat protetta, con i limiti dichiarati con onestà.