Salta el contingut

3.4 Vector Stores en Profunditat

Objectiu

Entendre el funcionament intern dels vector stores: com s'emmagatzemen els embeddings, quins algorismes de cerca s'utilitzen i com escollir i configurar el vector store adequat per a cada cas d'ús en sistemes RAG i agents.


🧲 Què és un Vector Store?

Un vector store (o base de dades vectorial) és un sistema d'emmagatzematge i cerca especialitzat per a vectors d'alta dimensió (embeddings). A diferència d'una base de dades relacional que cerca per valors exactes, un vector store cerca per similitud semàntica.

Base de dades relacional:
  SELECT * FROM docs WHERE text = "xarxes neuronals"
  → Coincidència EXACTA de paraules

Vector store:
  Cerca: "com funcionen les IA modernes"
  → Retorna els 3 documents semànticament MÉS SIMILARS:
     1. "Arquitectura de les xarxes neuronals profundes" (similitud: 0.91)
     2. "Transformers i atenció en LLM"                 (similitud: 0.88)
     3. "Backpropagation i aprenentatge profund"        (similitud: 0.84)
  → Funciona fins i tot si cap paraula coincideix exactament!

📐 Embeddings: La Base dels Vector Stores

Abans d'emmagatzemar, cada fragment de text es converteix en un vector numèric (embedding) amb un model específic:

from langchain_openai import OpenAIEmbeddings
from langchain_community.embeddings import HuggingFaceEmbeddings

# Model d'embedding d'OpenAI (propietari, 1536 dimensions)
embeddings_openai = OpenAIEmbeddings(model="text-embedding-3-small")

# Model d'embedding local gratuït (768 dimensions)
embeddings_local = HuggingFaceEmbeddings(
    model_name="sentence-transformers/paraphrase-multilingual-MiniLM-L12-v2"
    # Suporta català, castellà, anglès i 50+ idiomes
)

# Com funciona internament:
text = "Els transformers van revolucionar el NLP el 2017"
vector = embeddings_openai.embed_query(text)

print(f"Dimensió del vector: {len(vector)}")   # → 1536
print(f"Primeres 5 dimensions: {vector[:5]}")  # → [0.021, -0.143, 0.089, ...]
# El vector captura el "significat" del text en un espai multidimensional

Comparació de Models d'Embedding

Model Proveïdor Dimensions Cost Idiomes Recomanat per a
text-embedding-3-small OpenAI 1.536 $0.02/1M tokens Multi RAG general, bon equilibri
text-embedding-3-large OpenAI 3.072 $0.13/1M tokens Multi Màxima qualitat
voyage-3 Anthropic/Voyage 1.024 $0.06/1M tokens Multi Amb Claude
multilingual-e5-large Microsoft 1.024 Gratuït (local) 100+ Producció multilingüe
paraphrase-multilingual-MiniLM-L12 HuggingFace 384 Gratuït (local) 50+ Prototips, poc VRAM
nomic-embed-text Nomic 768 Gratuït (Ollama) Multi Local, sense API key

📏 Mètriques de Similitud

Un vector store compara vectors usant una mètrica de distància. La més usada és la similitud cosinus:

import numpy as np

def similitud_cosinus(vec_a: list, vec_b: list) -> float:
    """
    Mesura l'angle entre dos vectors.
    1.0 = idèntics semànticament
    0.0 = cap relació
    -1.0 = significat oposat (poc freqüent en embeddings de text)
    """
    a, b = np.array(vec_a), np.array(vec_b)
    return np.dot(a, b) / (np.linalg.norm(a) * np.linalg.norm(b))

# Exemple intuïtiu
v_gat    = embeddings.embed_query("el gat menja peix")
v_felin  = embeddings.embed_query("el felí s'alimenta de peixos")
v_cotxe  = embeddings.embed_query("el vehicle circula per l'autopista")

print(similitud_cosinus(v_gat, v_felin))  # → ~0.92 (molt similar)
print(similitud_cosinus(v_gat, v_cotxe)) # → ~0.21 (poc relacionat)

Comparació de Mètriques

Mètrica Fórmula simplificada Quan usar
Cosinus angle entre vectors Textos (longitud variable), la més usada
Euclidiana (L2) distància en línia recta Vectors normalitzats, imatges
Producte escalar cos × magnitud Quan la magnitud és rellevant
Manhattan (L1) suma de diferències absolutes Vectors dispersos

🗂️ Vector Stores Principals

ChromaDB — El Vector Store Local

ChromaDB és la opció per defecte per a prototips i projectes locals. No requereix servidor extern.

from langchain_chroma import Chroma
from langchain_openai import OpenAIEmbeddings
from langchain_core.documents import Document

embeddings = OpenAIEmbeddings(model="text-embedding-3-small")

# --- INDEXACIÓ: guardar documents ---
documents = [
    Document(
        page_content="El protocol TCP/IP és la base d'internet. Usa 4 capes: aplicació, transport, internet i accés a xarxa.",
        metadata={"font": "manual_xarxes.pdf", "pagina": 12, "tema": "TCP/IP"}
    ),
    Document(
        page_content="DNS (Domain Name System) tradueix noms de domini a adreces IP. Els servidors arrel gestionen els dominis de nivell superior.",
        metadata={"font": "manual_xarxes.pdf", "pagina": 45, "tema": "DNS"}
    ),
    Document(
        page_content="DHCP assigna adreces IP automàticament als dispositius d'una xarxa. Usa el procés DORA: Discover, Offer, Request, Acknowledge.",
        metadata={"font": "manual_xarxes.pdf", "pagina": 78, "tema": "DHCP"}
    ),
]

# Crear i persistir la base de dades
db = Chroma.from_documents(
    documents=documents,
    embedding=embeddings,
    persist_directory="./chroma_db",   # Guarda al disc
    collection_name="manual_xarxes"
)

print(f"Documents indexats: {db._collection.count()}")  # → 3


# --- CERCA: recuperar documents rellevants ---
resultats = db.similarity_search_with_score(
    query="com funciona la resolució de noms de domini",
    k=2   # Retorna els 2 més similars
)

for doc, score in resultats:
    print(f"Similitud: {score:.3f}")
    print(f"Contingut: {doc.page_content[:80]}...")
    print(f"Font: {doc.metadata['font']}, pàg. {doc.metadata['pagina']}")
    print()
# → Similitud: 0.891 → DNS...
# → Similitud: 0.743 → TCP/IP...


# --- REUTILITZAR la base existent ---
db_existent = Chroma(
    persist_directory="./chroma_db",
    embedding_function=embeddings,
    collection_name="manual_xarxes"
)

Filtres per Metadades

Una funcionalitat clau dels vector stores és poder combinar cerca semàntica amb filtres exactes sobre les metadades:

# Cerca semàntica + filtre per metadades
resultats = db.similarity_search(
    query="assignació automàtica d'adreces",
    k=3,
    filter={"tema": "DHCP"}   # Filtra per metadada exacta
)

# Filtres complexos (suport varia per vector store)
resultats = db.similarity_search(
    query="protocols de xarxa",
    k=5,
    filter={
        "$and": [
            {"font": {"$eq": "manual_xarxes.pdf"}},
            {"pagina": {"$gte": 40}}   # Pàgines >= 40
        ]
    }
)

FAISS — Alta Performance Local

FAISS (Facebook AI Similarity Search) és una biblioteca optimitzada per a cerques de similitud molt ràpides en grans conjunts de dades:

from langchain_community.vectorstores import FAISS

# Crear des de documents
db_faiss = FAISS.from_documents(documents, embeddings)

# Guardar i carregar (format binari eficient)
db_faiss.save_local("./faiss_index")
db_faiss = FAISS.load_local(
    "./faiss_index",
    embeddings,
    allow_dangerous_deserialization=True
)

# Merge de múltiples índexs (útil per a actualitzacions incrementals)
db_nous = FAISS.from_documents(nous_documents, embeddings)
db_faiss.merge_from(db_nous)

Quan usar FAISS vs ChromaDB:

ChromaDB FAISS
Facilitat Alta (API simple) Mitja
Persistència Automàtica Manual (save/load)
Filtres metadades Limitat
Velocitat (>1M docs) Moderada Molt alta
Casos d'ús Prototips, <500K docs Producció, >500K docs

Pinecone — Vector Store al Núvol

Per a aplicacions de producció que requereixen escalabilitat i alta disponibilitat:

from langchain_pinecone import PineconeVectorStore
from pinecone import Pinecone, ServerlessSpec
import os

# Inicialitzar Pinecone
pc = Pinecone(api_key=os.environ["PINECONE_API_KEY"])

# Crear índex (una sola vegada)
pc.create_index(
    name="manual-xarxes",
    dimension=1536,        # Ha de coincidir amb el model d'embedding
    metric="cosine",
    spec=ServerlessSpec(cloud="aws", region="us-east-1")
)

# Connectar i usar igual que ChromaDB
db_pinecone = PineconeVectorStore(
    index_name="manual-xarxes",
    embedding=embeddings,
    namespace="v1"         # Permet múltiples col·leccions al mateix índex
)

# Upsert (inserir o actualitzar)
db_pinecone.add_documents(documents)

# Cerca (mateixa interfície que ChromaDB/FAISS)
resultats = db_pinecone.similarity_search("protocols TCP", k=3)

pgvector — Vectors dins PostgreSQL

Ideal quan ja tens una infraestructura PostgreSQL i vols evitar una base de dades addicional:

from langchain_postgres import PGVector

CONNECTION_STRING = "postgresql+psycopg://user:pass@localhost:5432/empresa_db"

db_pg = PGVector(
    embeddings=embeddings,
    collection_name="documents_empresa",
    connection=CONNECTION_STRING,
    use_jsonb=True   # Metadades com a JSONB per a filtres eficients
)

# Compatible amb totes les operacions estàndard de LangChain
db_pg.add_documents(documents)
resultats = db_pg.similarity_search("consulta", k=5)

Avantatge clau: pots fer JOINs entre la taula de vectors i les teves taules relacionals en una sola consulta SQL.


⚙️ Configuració Avançada del Retriever

Convertir un vector store en un retriever LangChain obre moltes opcions de configuració:

# Retriever bàsic
retriever_basic = db.as_retriever(
    search_type="similarity",   # "similarity", "mmr", "similarity_score_threshold"
    search_kwargs={"k": 4}
)

# MMR (Maximal Marginal Relevance): evita resultats repetitius
retriever_mmr = db.as_retriever(
    search_type="mmr",
    search_kwargs={
        "k": 4,          # Nombre de resultats finals
        "fetch_k": 20,   # Candidats inicials a considerar
        "lambda_mult": 0.5   # 0=màxima diversitat, 1=màxima similitud
    }
)

# Threshold: només retorna si supera cert llindar de similitud
retriever_threshold = db.as_retriever(
    search_type="similarity_score_threshold",
    search_kwargs={"score_threshold": 0.75}  # Descarta resultats poc rellevants
)

Retriever amb Compressió Contextual

Filtra i comprimeix els documents recuperats per eliminar informació irrellevant:

from langchain.retrievers import ContextualCompressionRetriever
from langchain.retrievers.document_compressors import LLMChainExtractor
from langchain_anthropic import ChatAnthropic

llm = ChatAnthropic(model="claude-haiku-4-5-20251001")  # Model lleuger per a compressió

# El compressor extreu NOMÉS la part del document rellevant per a la pregunta
compressor = LLMChainExtractor.from_llm(llm)

retriever_comprimit = ContextualCompressionRetriever(
    base_compressor=compressor,
    base_retriever=retriever_basic
)

# Exemple: document original té 500 paraules, el compressor extreu les 30 rellevants
docs = retriever_comprimit.invoke("Quants ports usa TCP?")
# → Retorna fragment comprimit, no el document sencer

🏗️ Vector Store en un Agent RAG Complet

import os
from langchain_chroma import Chroma
from langchain_openai import OpenAIEmbeddings, ChatOpenAI
from langchain.chains import RetrievalQA
from langchain_core.prompts import ChatPromptTemplate
from langchain_community.document_loaders import PyPDFLoader
from langchain.text_splitter import RecursiveCharacterTextSplitter

# 1. Carregar i dividir el document
loader = PyPDFLoader("manual_xarxes.pdf")
documents_raw = loader.load()

splitter = RecursiveCharacterTextSplitter(
    chunk_size=512,
    chunk_overlap=64,
    separators=["\n\n", "\n", ".", " "]
)
chunks = splitter.split_documents(documents_raw)
print(f"Document dividit en {len(chunks)} fragments")

# 2. Crear el vector store
embeddings = OpenAIEmbeddings(model="text-embedding-3-small")
db = Chroma.from_documents(
    chunks,
    embeddings,
    persist_directory="./chroma_manual"
)

# 3. Crear el retriever amb MMR per diversitat
retriever = db.as_retriever(
    search_type="mmr",
    search_kwargs={"k": 4, "fetch_k": 15}
)

# 4. Cadena RAG
llm = ChatOpenAI(model="gpt-4o-mini", temperature=0)

prompt_rag = ChatPromptTemplate.from_template("""
Respon la pregunta basant-te ÚNICAMENT en el context proporcionat.
Si la informació no és al context, digues "No trobo aquesta informació al manual."

Context:
{context}

Pregunta: {question}
Resposta en català:
""")

cadena_rag = RetrievalQA.from_chain_type(
    llm=llm,
    chain_type="stuff",
    retriever=retriever,
    chain_type_kwargs={"prompt": prompt_rag},
    return_source_documents=True
)

# 5. Consultar
resultat = cadena_rag.invoke({"query": "Explica el procés DORA de DHCP"})
print(resultat["result"])
print("\nFonts consultades:")
for doc in resultat["source_documents"]:
    print(f"  - {doc.metadata.get('source', 'desconegut')}, pàg. {doc.metadata.get('page', '?')}")

📊 Comparativa de Vector Stores

ChromaDB FAISS Pinecone pgvector Weaviate
Tipus Local/núvol Local Núvol Local/núvol Local/núvol
Escalabilitat Fins a ~1M docs Milions Il·limitada Depèn de PG Alta
Filtres metadata Limitat ✅ (SQL)
Cerca híbrida
Cost Gratuït Gratuït Des de $0.096/h Gratuït Gratuït/SaaS
Integració LangChain ✅ Nativa ✅ Nativa ✅ Nativa ✅ Nativa ✅ Nativa
Ideal per a Prototips Prod. local Prod. SaaS Ja tens PG Multimodal

Recomanació per al curs

Usa ChromaDB per a totes les pràctiques. Si necessites producció real sense infraestructura pròpia, considera Pinecone. Si ja tens PostgreSQL, pgvector evita afegir una nova base de dades al sistema.


🔑 Conceptes Clau d'aquesta Secció

Terme Definició
Embedding Representació vectorial densa d'un text que captura el seu significat semàntic.
Similitud cosinus Mètrica que mesura l'angle entre dos vectors; la més usada per a similitud textual.
Vector store Base de dades especialitzada en emmagatzemar i cercar vectors per similitud.
MMR Maximal Marginal Relevance. Algorisme que retorna resultats rellevants i diversos alhora.
Score threshold Llindar de similitud mínima per retornar un resultat (evita recuperar fragments irrellevants).
Compressió contextual Tècnica que usa un LLM per extreure únicament el fragment rellevant d'un document recuperat.
Cerca híbrida Combinació de cerca semàntica (vectors) i cerca per paraules clau (BM25) per millors resultats.
Upsert Operació que insereix un document si no existeix o l'actualitza si ja hi és.

✅ Activitats de Consolidació

Exercici 3.4.1 — Explorar Similituds

Crea un script que:

  1. Generi embeddings de 10 frases (5 sobre xarxes, 5 sobre cuina)
  2. Calculi la matriu de similituds cosinus 10×10
  3. Visualitzi la matriu com a mapa de calor (matplotlib.pyplot.imshow)
  4. Identifica quins parells de frases de temes diferents tenen similitud > 0.5

Exercici 3.4.2 — RAG sobre Documentació Tècnica

Construeix un sistema RAG que indexi la documentació oficial de Python (o qualsevol manual en PDF):

  1. Carrega i divideix el document en chunks de 512 tokens amb 64 de solapament
  2. Indexa a ChromaDB amb text-embedding-3-small
  3. Implementa un retriever MMR (k=4, fetch_k=20)
  4. Prova les consultes amb i sense threshold i compara la qualitat

Exercici 3.4.3 — Benchmark de Retrievers

Compara tres configuracions de retriever sobre el mateix vector store:

  • similarity amb k=3
  • mmr amb k=3, lambda=0.5
  • similarity_score_threshold amb threshold=0.75

Per a cada configuració: mesura el temps de resposta, el nombre de documents retornats i la qualitat subjectiva de la resposta final de l'agent.


📚 Lectures Complementàries