QDrant. Bases de dades semàntiques
QDrant
docker pull qdrant/qdrant docker run -p 6333:6333 qdrant/qdrant ... Access web UI at http://localhost:6333/dashboard
Qdrant is now available at:
REST API: http://localhost:6333
Web UI: http://localhost:6333/dashboard
Segueixo la guia del quickstart:
Afegim una nova col·lecció:
PUT collections/star_charts
{
"vectors": {
"size": 4,
"distance": "Dot"
}
}
Afegim punts a la col·lecció:
PUT collections/star_charts/points
{
"points": [
{
"id": 1,
"vector": [0.05, 0.61, 0.76, 0.74],
"payload": {
"colony": "Mars"
}
},
{
"id": 2,
"vector": [0.19, 0.81, 0.75, 0.11],
"payload": {
"colony": "Jupiter"
}
},
{
"id": 3,
"vector": [0.36, 0.55, 0.47, 0.94],
"payload": {
"colony": "Venus"
}
},
{
"id": 4,
"vector": [0.18, 0.01, 0.85, 0.80],
"payload": {
"colony": "Moon"
}
},
{
"id": 5,
"vector": [0.24, 0.18, 0.22, 0.44],
"payload": {
"colony": "Pluto"
}
}
]
}
Step 3: Run a search query
POST collections/star_charts/points/search
{
"vector": [0.2, 0.1, 0.9, 0.7],
"limit": 3,
"with_payload": true
}
resultat:
{
"result": [
{
"id": 4,
"version": 1,
"score": 1.362,
"payload": {
"colony": "Moon"
}
},
{
"id": 1,
"version": 1,
"score": 1.273,
"payload": {
"colony": "Mars"
}
},
{
"id": 3,
"version": 1,
"score": 1.208,
"payload": {
"colony": "Venus"
}
}
],
"status": "ok",
"time": 0.000833372
}
Persistència: que és guardin les dades
docker run -p 6333:6333 -p 6334:6334 \
-v $(pwd)/qdrant_storage:/qdrant/storage:z \
-v $(pwd)/qdrant_snapshots:/qdrant/snapshots:z \
-v $(pwd)/config/production.yaml:/qdrant/config/production.yaml:z \
qdrant/qdrant
primer he de crear el fitxer production.yaml amb la configuració mínima com es comenta en el tutorial.
2026-06-03T15:56:36.213394Z ERROR qdrant: Error while starting REST server: TLS file could not be opened: ./tls/cert.pem
Error: TLS file could not be opened: ./tls/cert.pem
Caused by:
failed to open file `./tls/cert.pem`: No such file or directory (os error 2
-> en el fitxer de configuració he de deshabilitar tls
Ja puc entrar però ara em demana una api key:
This instance of Qdrant might be protected by an API Key. If so, please enter your API Key to continue.
la api key es deshabilita en el fitxer de configuració
Ja funciona el tutorial, i ara ja tinc persistència: és a dir, puc tancar el docker i tornar-lo a obrir, i les dades encara hi són. Recordar que tenim dos volums que s'han creat.
Per ex:
GET collections/terraforming
Puc executar la rest api en línia de comanda. Per ex:
curl -X GET \ "http://localhost:6333/collections/terraforming" \ -H "Content-Type: application/json"
Resultat:
{"result":{"status":"green","optimizer_status":"ok","indexed_vectors_count":0,"points_count":6,"segments_count":8,"config":{"params":{"vectors":{"size":4,"distance":"Dot"},"shard_number":1,"replication_factor":1,"write_consistency_factor":1,"on_disk_payload":true},"hnsw_config":{"m":16,"ef_construct":100,"full_scan_threshold":10000,"max_indexing_threads":0,"on_disk":false},"optimizer_config":{"deleted_threshold":0.2,"vacuum_min_vector_number":1000,"default_segment_number":0,"max_segment_size":null,"memmap_threshold":null,"indexing_threshold":10000,"flush_interval_sec":5,"max_optimization_threads":null,"prevent_unoptimized":null},"wal_config":{"wal_capacity_mb":32,"wal_segments_ahead":0,"wal_retain_closed":1},"quantization_config":null},"payload_schema":{"life":{"data_type":"bool","points":6}},"update_queue":{"length":0}},"status":"ok","time":0.001508578}
PyMuPDF
PyMuPDF (paquet pymupdf, importat com fitz) és una de les millors opcions per extreure text de PDFs en Python: és ràpid, estable i acostuma a preservar millor l'estructura que moltes alternatives.
Necessitem pymupdf perquè hem de fer la ingesta dels documents pdf a la base de dades semàntica.
pip install pymupdf
Exemple bàsic: extreure tot el text (funciona)
import fitz # PyMuPDF
doc = fitz.open("sample.pdf")
text_complet = ""
for pagina in doc:
text_complet += pagina.get_text()
doc.close()
print(text_complet)
Extreure pàgina per pàgina
Això és útil en un RAG perquè pots guardar metadades com el número de pàgina.
import fitz
doc = fitz.open("sample.pdf")
for num_pagina, pagina in enumerate(doc, start=1):
text = pagina.get_text()
print(f"=== Pàgina {num_pagina} ===")
print(text[:500])
Obtenir text amb coordenades
Si més endavant vols reconstruir capítols, notes al peu o columnes:
import fitz
doc = fitz.open("sample.pdf")
pagina = doc[0]
blocs = pagina.get_text("blocks")
for bloc in blocs:
x0, y0, x1, y1, text, *_ = bloc
print(text)
Format estructurat (molt útil)
import fitz
import json
doc = fitz.open("sample.pdf")
estructura = doc[0].get_text("dict")
print(json.dumps(estructura, indent=2)[:2000])
Crear chunks per al RAG
Una aproximació senzilla:
import fitz
doc = fitz.open("sample.pdf")
documents = []
for page_num, page in enumerate(doc):
text = page.get_text()
documents.append({
"text": text,
"metadata": {
"source": "sample.pdf",
"page": page_num + 1
}
})
<pre>
<pre>
>>> documents[0]['text']
(tots els exemples funcionen)
Per tant, ja sé fer els chunks per al RAG
Per al teu projecte d'Història de la Ciència Jo faria: PyMuPDF → extracció Detectar si cal OCR Guardar metadades: -llibre -autor -any -pàgina -idioma -Fer chunks de 500–800 tokens -Embeddings -> quan facis textos en català necessitaré els embeddings del català -Qdrant
A l'hora de fer el chunking, es pot utilitzar llamaindex, perquè té opcions avançades com respectar frases i paràgrafs.
Opció 2: LlamaIndex
LlamaIndex aporta diverses estratègies de chunking:
SentenceSplitter -> Respecta frases i paràgrafs.
from llama_index.core.node_parser import SentenceSplitter
splitter = SentenceSplitter(
chunk_size=800,
chunk_overlap=100,
)
Això acostuma a funcionar millor que tallar exactament cada N caràcters.
Opció 3: LangChain Text Splitters
LangChain Text Splitters -> Una alternativa lleugera si no vols adoptar tot LlamaIndex.
Per exemple:
from langchain_text_splitters import RecursiveCharacterTextSplitter
Aquest és molt popular perquè:
- intenta respectar paràgrafs
- després frases
- després espais (només talla "a la força" si cal)
Per a molts projectes és suficient.
Buscant una solució lleugera (que seria un dels meus objectius), puc utilitzar langchain text splitter
pip install langchain-text-splitters
El més recomanat: ''RecursiveCharacterTextSplitter'' -> És el splitter que acostuma a donar millors resultats generals.
<pre>
from langchain_text_splitters import RecursiveCharacterTextSplitter
splitter = RecursiveCharacterTextSplitter(
chunk_size=1000,
chunk_overlap=150
)
chunks = splitter.split_text(text)
On:
- chunk_size=1000: mida aproximada del fragment
- chunk_overlap=150: solapament entre fragments
for i, chunk in enumerate(chunks):
print(f"Chunk {i}")
print(chunk[:200])
Per què "Recursive"? Intenta dividir respectant aquesta jerarquia: Paràgrafs ↓ Línies ↓ Espais ↓ Caràcters
És interessant guardar les metadades (recomanat per RAG), per ex, el número de pàgina.
Si tens pàgines de PyMuPDF:
documents = [
{
"text": page_text,
"page": page_num
}
]
per ex:
documents = [
{
"text": "kjh jhlkj hl",
"page": 1
}
]
pots convertir-les en Documents:
from langchain_core.documents import Document
docs = [
Document(
page_content=page_text,
metadata={"page": page_num}
)
]
docs = [
Document(
page_content="gshdgsj ajj dgsahjgd ahj gdhagd ha gdhsa ",
metadata={"page": 1}
)
]
chunks = splitter.split_documents(docs)
print(chunks[0].page_content)
print(chunks[0].metadata)
Exemple complet amb PyMuPDF:
import fitz
from langchain_core.documents import Document
from langchain_text_splitters import RecursiveCharacterTextSplitter
doc = fitz.open("sample.pdf")
documents = []
for page_num, page in enumerate(doc, start=1):
documents.append(
Document(
page_content=page.get_text(),
metadata={
"source": "sample.pdf",
"page": page_num
}
)
)
splitter = RecursiveCharacterTextSplitter(
chunk_size=1000,
chunk_overlap=150
)
chunks = splitter.split_documents(documents)
print(f"Chunks creats: {len(chunks)}")
print(chunks[0].metadata)
(TODO)
- I ara que tinc tots els chunks d'un document, em falta fer la ingesta del document al QDrant.
- I fer consultes sobre el document
creat per Joan Quintana Compte, octubre 2026