Il vector store in breve
Negli ultimi anni i modelli linguistici hanno dimostrato di saper comprendere e generare testo in modo sorprendentemente naturale. Tuttavia, da soli, questi modelli non “conoscono” i documenti di un’azienda, i manuali interni o le basi di dati proprietarie. Qui entra in gioco il vector store.
Un vector store è un sistema che permette di trasformare testi in rappresentazioni numeriche (embedding) e di conservarle in modo da poterle confrontare per somiglianza semantica. In pratica, invece di cercare una parola esatta all’interno di un testo, si cerca un significato simile. Due frasi che parlano dello stesso concetto, anche con parole diverse, finiscono vicine nello spazio matematico degli embedding.
Questo meccanismo è alla base della ricerca semantica e, soprattutto, dei sistemi di Retrieval Augmented Generation (RAG): l’utente fa una domanda, il sistema recupera i pezzi di conoscenza più rilevanti da un vector store e li fornisce al modello OpenAI come contesto per generare una risposta fondata su dati reali.
Perché è necessario dividere i testi in chunk
Un errore comune è pensare di poter vettorializzare un intero documento in un solo colpo. In realtà, un embedding rappresenta una sorta di “media semantica” del testo: se il testo è troppo lungo o contiene argomenti diversi, questa media diventa imprecisa. Il modello non riesce più a distinguere bene i concetti.
Per capire in cosa consiste la vettorializzazione leggi
Per questo motivo il testo di conoscenza viene suddiviso in chunk, ovvero porzioni coerenti e autosufficienti. Ogni chunk dovrebbe parlare di un solo argomento o di una singola unità informativa: una procedura, una descrizione, una sezione di manuale. In questo modo ogni embedding risulta più “nitido” e la ricerca per similarità diventa molto più precisa.
Spesso si introduce anche un overlap, una piccola sovrapposizione tra chunk consecutivi, per evitare di perdere informazioni che si trovano proprio al confine tra due sezioni. L’overlap non serve a correggere un cattivo chunking, ma solo a preservare continuità concettuale.

Chunking automatico o manuale?
Esistono strumenti che suddividono automaticamente un testo in blocchi di lunghezza fissa, ad esempio ogni 500 token. Questo approccio è rapido e può andare bene per prototipi o testi narrativi semplici. Tuttavia, quando il contenuto è strutturato – manuali, procedure, documenti legali – il chunking automatico rischia di spezzare concetti importanti a metà.
In questi casi è preferibile un chunking guidato dalla struttura semantica del testo. Un metodo molto efficace consiste nell’inserire nel testo dei tag convenzionali, come [CHUNK], [SEZIONE], [PROCEDURA], che indicano chiaramente dove inizia e finisce un’unità di conoscenza. Il codice può poi usare questi marcatori per suddividere il testo in modo controllato e coerente.
Immaginiamo di avere un testo di conoscenza strutturato così:
[CHUNK] La firma remota consente di firmare documenti digitali senza l’uso di una smart card. È particolarmente utile per chi lavora da remoto. [CHUNK] Procedura di firma remota: 1. Accedere al portale 2. Caricare il documento 3. Confermare con OTP [CHUNK] In caso di problemi con l’OTP, verificare la connessione o contattare l’assistenza.
Ogni blocco contrassegnato da [CHUNK] rappresenta un’unità semantica autonoma.
Suddivisione in chunk con python
def split_by_tag(text, tag="[CHUNK]"):
parts = text.split(tag)
return [p.strip() for p in parts if p.strip()]
Vettorializzazione dei chunk con OpenAI
from openai import OpenAI
import numpy as np
client = OpenAI()
def embed_chunks(chunks):
response = client.embeddings.create(
model="text-embedding-3-small",
input=chunks
)
vectors = [d.embedding for d in response.data]
return np.array(vectors, dtype="float32")
Creazione del vector store (FAISS)
import faiss
def create_vector_store(vectors):
faiss.normalize_L2(vectors)
dim = vectors.shape[1]
index = faiss.IndexFlatIP(dim)
index.add(vectors)
return index
Ricerca semantica
def search(query, index, chunks, k=2):
q_vec = embed_chunks([query])
faiss.normalize_L2(q_vec)
scores, ids = index.search(q_vec, k)
results = []
for score, idx in zip(scores[0], ids[0]):
results.append({
"score": float(score),
"chunk": chunks[idx]
})
return results
Codice completo
text = """
[CHUNK]
La firma remota consente di firmare documenti digitali senza l’uso di una smart card.
[CHUNK]
Procedura di firma remota:
1. Accedere al portale
2. Caricare il documento
3. Confermare con OTP
[CHUNK]
In caso di problemi con l’OTP, contattare l’assistenza.
"""
chunks = split_by_tag(text)
vectors = embed_chunks(chunks)
index = create_vector_store(vectors)
results = search("Come funziona la firma senza smart card?", index, chunks)
for r in results:
print(r["score"])
print(r["chunk"])
Il vector store non è semplicemente un “database di testi”, ma una memoria semantica che consente ai modelli OpenAI di lavorare su conoscenza esterna in modo preciso e controllabile. La qualità delle risposte non dipende tanto dal modello quanto da come la conoscenza viene preparata: la suddivisione in chunk coerenti, l’uso consapevole dell’overlap e una strategia di chunking adeguata al tipo di contenuto sono gli elementi che fanno davvero la differenza.
In altre parole, progettare bene i chunk significa progettare bene il modo in cui l’intelligenza artificiale potrà “ragionare” sui tuoi dati.
Stesso esempio con aggiunta di metadata
Immaginiamo questo testo (unico file / stringa):
[CHUNK id=1 type=overview section="Introduzione"]
La firma remota consente di firmare documenti digitali senza l’uso di una smart card.
È particolarmente utile per chi lavora da remoto.
[CHUNK id=2 type=procedura section="Firma remota"]
Procedura di firma remota:
1. Accedere al portale
2. Caricare il documento
3. Confermare con OTP
[CHUNK id=3 type=troubleshooting section="Problemi comuni"]
In caso di problemi con l’OTP, verificare la connessione
o contattare l’assistenza.
Qui i metadata sono dichiarati nel testo (pattern molto usato):
idtypesection
Parsing del testo + costruzione chunk object
import re
from typing import List, Dict
def parse_chunks(text: str) -> List[Dict]:
pattern = re.compile(
r"\[CHUNK\s+(.*?)\]\n(.*?)(?=\n\[CHUNK|\Z)",
re.DOTALL
)
chunks = []
for match in pattern.finditer(text):
meta_raw = match.group(1)
content = match.group(2).strip()
# parse metadata key=value
metadata = dict(re.findall(r'(\w+)=(?:"([^"]+)"|(\S+))', meta_raw))
# fix tuple values
metadata = {k: v1 or v2 for k, v1, v2 in metadata.items()}
chunks.append({
"text": content,
"metadata": metadata
})
return chunks
L’output è fatto più o meno così:
{
“text”: “Procedura di firma remota…”,
“metadata”: {
“id”: “2”,
“type”: “procedura”,
“section”: “Firma remota”
}
}
Vettorializzazione con OpenAI
from openai import OpenAI
import numpy as np
client = OpenAI()
def embed_chunks(chunks):
texts = [ch["text"] for ch in chunks]
response = client.embeddings.create(
model="text-embedding-3-small",
input=texts
)
vectors = np.array(
[d.embedding for d in response.data],
dtype="float32"
)
return vectors
Creazione del Vector Store (FAISS + cosine)
import faiss
def build_vector_store(vectors):
faiss.normalize_L2(vectors)
dim = vectors.shape[1]
index = faiss.IndexFlatIP(dim)
index.add(vectors)
return index
Ricerca semantica con restituzione metadata
def semantic_search(query, index, chunks, top_k=3):
q_emb = client.embeddings.create(
model="text-embedding-3-small",
input=query
).data[0].embedding
q_vec = np.array([q_emb], dtype="float32")
faiss.normalize_L2(q_vec)
scores, ids = index.search(q_vec, top_k)
results = []
for score, idx in zip(scores[0], ids[0]):
results.append({
"score": float(score),
"text": chunks[idx]["text"],
"metadata": chunks[idx]["metadata"]
})
return results
Codice completo
raw_text = """[CHUNK id=1 type=overview section="Introduzione"]
La firma remota consente di firmare documenti digitali senza l’uso di una smart card.
[CHUNK id=2 type=procedura section="Firma remota"]
Procedura di firma remota:
1. Accedere al portale
2. Caricare il documento
3. Confermare con OTP
[CHUNK id=3 type=troubleshooting section="Problemi comuni"]
In caso di problemi con l’OTP, contattare l’assistenza.
"""
chunks = parse_chunks(raw_text)
chunks = enrich_metadata(
chunks,
source="manuale_firma_remota.txt",
version="2024-03"
)
vectors = embed_chunks(chunks)
index = build_vector_store(vectors)
results = semantic_search(
"Come si firma senza smart card?",
index,
chunks
)
for r in results:
print("\nSCORE:", r["score"])
print("SEZIONE:", r["metadata"]["section"])
print("TIPO:", r["metadata"]["type"])
print("TESTO:", r["text"])