Diferència entre revisions de la pàgina «QDrant. Bases de dades semàntiques»

De wikijoan
Salta a la navegació Salta a la cerca
 
(Hi ha 8 revisions intermèdies del mateix usuari que no es mostren)
Línia 1: Línia 1:
 +
__TOC__
 
=QDrant=
 
=QDrant=
−
* [[https://qdrant-qdrant-18.mintlify.app/installation|https://qdrant-qdrant-18.mintlify.app/installation]]
+
* [https://qdrant-qdrant-18.mintlify.app/installation https://qdrant-qdrant-18.mintlify.app/installation]
  
 
<pre>
 
<pre>
Línia 18: Línia 19:
 
</pre>
 
</pre>
  
−
Segueixo la guia del quickstart:
+
Run with default settings:
 +
<pre>
 +
docker run -p 6333:6333 qdrant/qdrant
 +
</pre>
 +
==Connexió des del portàtil==
 +
Normalment treballo per SSH des del portàtil (headless server). Per tant, vull connectar-me des del portàtil. La IP fixa del servidor és 192.168.1.184.
 +
 
 +
L'opció recomanada és fer un túnel SSH
 +
<pre>
 +
El teu portàtil            ->  Connexió SSH                -> Servidor 192.168.1.184
 +
Navegador: localhost:6333  <-  Túnel xifrat pel port 6333  <- Qdrant escolta al port 6333
 +
</pre>
 +
Des del portàtil ja ens connectàvem per ssh (sense password), però ara li afegim el port 6333
 +
<pre>
 +
ssh -L 6333:127.0.0.1:6333 joan@192.168.1.184
 +
</pre>
 +
 
 +
I ara ja funciona des del portàtil:
 +
* http://localhost:6333/dashboard#/welcome
 +
==Seguim amb el quickstart==
 +
* http://localhost:6333/dashboard#/tutorial/quickstart
 +
o bé la versió oline, on s'utililtza ''curl'':
 +
* https://qdrant-qdrant-18.mintlify.app/quickstart
  
 
Afegim una nova col·lecció:
 
Afegim una nova col·lecció:
Línia 35: Línia 58:
  
 
}
 
}
 +
</pre>
 +
En aquest cas hem clicat RUN, però internament la manera d'executar seria:
 +
<pre>
 +
curl -X PUT 'http://localhost:6333/collections/test_collection' \
 +
    -H 'Content-Type: application/json' \
 +
    --data-raw '{
 +
        "vectors": {
 +
          "size": 4,
 +
          "distance": "Dot"
 +
        }
 +
    }'
 
</pre>
 
</pre>
 +
  
 
Afegim punts a la col·lecció:
 
Afegim punts a la col·lecció:
Línia 82: Línia 117:
 
</pre>
 
</pre>
  
 +
o bé:
 +
<pre>
 +
curl -L -X PUT 'http://localhost:6333/collections/test_collection/points?wait=true' \
 +
    -H 'Content-Type: application/json' \
 +
    --data-raw '{
 +
        "points": [
 +
          {"id": 1, "vector": [0.05, 0.61, 0.76, 0.74], "payload": {"city": "Berlin"}},
 +
          {"id": 2, "vector": [0.19, 0.81, 0.75, 0.11], "payload": {"city": ["Berlin", "London"]}},
 +
          {"id": 3, "vector": [0.36, 0.55, 0.47, 0.94], "payload": {"city": ["Berlin", "Moscow"]}},
 +
          {"id": 4, "vector": [0.18, 0.01, 0.85, 0.80], "payload": {"city": ["London", "Moscow"]}},
 +
          {"id": 5, "vector": [0.24, 0.18, 0.22, 0.44], "payload": {"count": [0]}},
 +
          {"id": 6, "vector": [0.35, 0.08, 0.11, 0.44]}
 +
        ]
 +
    }'
 +
</pre>
  
 
Step 3: Run a search query
 
Step 3: Run a search query
Línia 126: Línia 176:
 
   "time": 0.000833372
 
   "time": 0.000833372
 
}
 
}
 +
</pre>
 +
o bé:
 +
<pre>
 +
curl -L -X POST 'http://localhost:6333/collections/test_collection/points/search' \
 +
    -H 'Content-Type: application/json' \
 +
    --data-raw '{
 +
        "vector": [0.2, 0.1, 0.9, 0.7],
 +
        "top": 3
 +
    }'
 +
</pre>
 +
resultat:
 +
<pre>
 +
{"result":[{"id":4,"version":1,"score":1.362},{"id":1,"version":1,"score":1.273},{"id":3,"version":1,"score":1.208}],"status":"ok","time":0.000478823}
 
</pre>
 
</pre>
  
−
**Persistència**: que és guardin les dades
+
==Persistència==
 +
'''Persistència''': que és guardin les dades
  
 
<pre>
 
<pre>
Línia 134: Línia 198:
 
     -v $(pwd)/qdrant_storage:/qdrant/storage:z \
 
     -v $(pwd)/qdrant_storage:/qdrant/storage:z \
 
     -v $(pwd)/qdrant_snapshots:/qdrant/snapshots:z \
 
     -v $(pwd)/qdrant_snapshots:/qdrant/snapshots:z \
−
     -v $(pwd)/config/production.yaml:/qdrant/config/production.yaml:z \
+
     -v $(pwd)/qdrant/config/production.yaml:/qdrant/config/production.yaml:z \
 
     qdrant/qdrant
 
     qdrant/qdrant
 
</pre>     
 
</pre>     
  
 
primer he de crear el fitxer production.yaml amb la configuració mínima com es comenta en el tutorial.
 
primer he de crear el fitxer production.yaml amb la configuració mínima com es comenta en el tutorial.
 +
* https://qdrant-qdrant-18.mintlify.app/quickstart
 +
 +
Understanding the volume mounts:
 +
* /qdrant/storage - Where Qdrant persists all your data (mount as volume to prevent data loss)
 +
* /qdrant/snapshots - Where Qdrant stores collection snapshots for backup/restore
 +
* /qdrant/config/production.yaml - Custom configuration file (override values from the reference config)
 +
 +
* http://localhost:6333/dashboard
  
 
<pre>
 
<pre>
 +
mkdir -p ~/qdrant/config
 +
nano ~/qdrant/config/production.yaml
 +
</pre>
 +
Contingut mínim:
 +
</pre>
 +
log_level: INFO
 +
 +
service:
 +
  host: 127.0.0.1
 +
  http_port: 6333
 +
  grpc_port: 6334
 +
 +
</pre>
 +
Però el ''production.yaml'' del tutorial és:
 +
<pre>
 +
service:
 +
  # Required: Enable API key authentication
 +
  api_key: your_secret_api_key_here
 +
 
 +
  # Optional: Enable TLS
 +
  enable_tls: true
 +
 
 +
  host: 0.0.0.0
 +
  http_port: 6333
 +
  grpc_port: 6334
 +
 +
storage:
 +
  # Persist data to disk
 +
  storage_path: ./storage
 +
  snapshots_path: ./snapshots
 +
 
 +
  # Reduce RAM usage by storing payloads on disk
 +
  on_disk_payload: true
 +
 
 +
  # WAL configuration
 +
  wal:
 +
    wal_capacity_mb: 32
 +
    wal_segments_ahead: 0
 +
</pre>
 +
 
2026-06-03T15:56:36.213394Z ERROR qdrant: Error while starting REST server: TLS file could not be opened: ./tls/cert.pem
 
2026-06-03T15:56:36.213394Z ERROR qdrant: Error while starting REST server: TLS file could not be opened: ./tls/cert.pem
  
Línia 168: Línia 280:
  
  
−
Puc executar la rest api en **línia de comanda**. Per ex:
+
Puc executar la rest api en '''línia de comanda'''. Per ex:
  
 +
Creem la col·lecció:
 +
<pre>
 +
curl -X PUT \
 +
  http://localhost:6333/collections/terraforming \
 +
  -H 'Content-Type: application/json' \
 +
  -d '{
 +
    "vectors": {
 +
      "size": 4,
 +
      "distance": "Dot"
 +
    }
 +
  }'
 +
</pre>
 +
 +
Configuració de la col·lecció, incloent-hi la mida dels vectors i la mètrica de distància:
 
<pre>
 
<pre>
 
curl -X GET \
 
curl -X GET \
Línia 179: Línia 305:
 
{"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}
 
{"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}
 
</pre>
 
</pre>
 +
 +
Inserim dades d'exemple:
 +
<pre>
 +
curl -X PUT \
 +
  http://localhost:6333/collections/terraforming/points \
 +
  -H 'Content-Type: application/json' \
 +
  -d '{
 +
    "points": [
 +
      {
 +
        "id": 1,
 +
        "vector": [0.1, 0.2, 0.3, 0.4],
 +
        "payload": {"land": "forest", "color": "green", "life": true, "humidity": 40}
 +
      },
 +
      {
 +
        "id": 2,
 +
        "vector": [0.2, 0.3, 0.4, 0.5],
 +
        "payload": {"land": "lake", "color": "blue", "life": true, "humidity": 100}
 +
      },
 +
      {
 +
        "id": 3,
 +
        "vector": [0.3, 0.4, 0.5, 0.6],
 +
        "payload": {"land": "steppe", "color": "green", "life": false, "humidity": 25}
 +
      },
 +
      {
 +
        "id": 4,
 +
        "vector": [0.4, 0.5, 0.6, 0.7],
 +
        "payload": {"land": "desert", "color": "red", "life": false, "humidity": 5}
 +
      },
 +
      {
 +
        "id": 5,
 +
        "vector": [0.5, 0.6, 0.7, 0.8],
 +
        "payload": {"land": "marsh", "color": "black", "life": true, "humidity": 90}
 +
      },
 +
      {
 +
        "id": 6,
 +
        "vector": [0.6, 0.7, 0.8, 0.9],
 +
        "payload": {"land": "cavern", "color": "black", "life": false, "humidity": 15}
 +
      }
 +
    ]
 +
  }'
 +
</pre>
 +
Comprovem que les dades hi són:
 +
<pre>
 +
curl -X POST \
 +
  http://localhost:6333/collections/terraforming/points/scroll \
 +
  -H 'Content-Type: application/json' \
 +
  -d '{
 +
    "limit": 10,
 +
    "with_payload": true,
 +
    "with_vector": true
 +
  }'
 +
</pre>
 +
Resultat:
 +
<pre>
 +
{"result":{"points":[{"id":1,"payload":{"land":"forest","color":"green","life":true,"humidity":40},"vector":[0.1,0.2,0.3,0.4]},{"id":2,"payload":{"land":"lake","color":"blue","life":true,"humidity":100},"vector":[0.2,0.3,0.4,0.5]},{"id":3,"payload":{"land":"steppe","color":"green","life":false,"humidity":25},"vector":[0.3,0.4,0.5,0.6]},{"id":4,"payload":{"land":"desert","color":"red","life":false,"humidity":5},"vector":[0.4,0.5,0.6,0.7]},{"id":5,"payload":{"land":"marsh","color":"black","life":true,"humidity":90},"vector":[0.5,0.6,0.7,0.8]},{"id":6,"payload":{"land":"cavern","color":"black","life":false,"humidity":15},"vector":[0.6,0.7,0.8,0.9]}],"next_page_offset":null},"status":"ok","time":0.000492869}
 +
</pre>
 +
 +
Provem un filtre:
 +
<pre>
 +
curl -X POST \
 +
  http://localhost:6333/collections/terraforming/points/scroll \
 +
  -H 'Content-Type: application/json' \
 +
  -d '{
 +
    "filter": {
 +
      "must": [
 +
        {
 +
          "key": "color",
 +
          "match": {"value": "black"}
 +
        }
 +
      ]
 +
    },
 +
    "limit": 10,
 +
    "with_payload": true
 +
  }'
 +
</pre>
 +
Resultat:
 +
<pre>
 +
{"result":{"points":[{"id":5,"payload":{"land":"marsh","color":"black","life":true,"humidity":90}},{"id":6,"payload":{"land":"cavern","color":"black","life":false,"humidity":15}}],"next_page_offset":null},"status":"ok","time":0.000402086}
 +
</pre>
 +
Com que estic amb persistència, la propera vegada que arrenqui el QDrant les dades hi seran.
 +
 +
=QDrant amb GPU support=
 +
Abans que res, hem d'instal·lar el NVIDIA Container Toolkit, és a dir, les eines per tal de que la GPU sàpiga treballar amb els contenidors.
 +
<pre>
 +
sudo apt-get update
 +
sudo apt-get install -y ca-certificates curl gnupg
 +
 +
curl -fsSL https://nvidia.github.io/libnvidia-container/gpgkey \
 +
  | sudo gpg --dearmor --yes \
 +
  -o /usr/share/keyrings/nvidia-container-toolkit-keyring.gpg
 +
 +
curl -s -L \
 +
  https://nvidia.github.io/libnvidia-container/stable/deb/nvidia-container-toolkit.list \
 +
  | sed 's#deb https://#deb [signed-by=/usr/share/keyrings/nvidia-container-toolkit-keyring.gpg] https://#g' \
 +
  | sudo tee /etc/apt/sources.list.d/nvidia-container-toolkit.list
 +
 +
sudo apt-get update
 +
sudo apt-get install -y nvidia-container-toolkit
 +
</pre>
 +
 +
Configurem docker:
 +
<pre>
 +
sudo nvidia-ctk runtime configure --runtime=docker
 +
</pre>
 +
Aquesta ordre configura Docker perquè pugui utilitzar el runtime NVIDIA. Després, reinicia Docker:
 +
<pre>
 +
sudo systemctl restart docker
 +
</pre>
 +
Comprova que Docker veu la GPU: (Hauries de veure nvidia entre els runtimes disponibles.)
 +
<pre>
 +
$ docker info | grep -i runtime
 +
Runtimes: nvidia runc io.containerd.runc.v2
 +
Default Runtime: runc
 +
</pre>
 +
 +
<pre>
 +
# Pull GPU-enabled image
 +
docker pull qdrant/qdrant:gpu-nvidia-latest
 +
 +
I ara ja podem utilitzar QDrant aprofitant-nos de la GPU:
 +
# Run with GPU access
 +
docker run -p 6333:6333 \
 +
    --gpus all \
 +
    qdrant/qdrant:gpu-nvidia-latest
 +
</pre>
 +
 +
I amb persistència: (reaprofito els punts de storage, suposant que no hi ha problema)
 +
<pre>
 +
docker run -p 6333:6333 -p 6334:6334 \
 +
    --gpus all \
 +
    -v $(pwd)/qdrant_storage:/qdrant/storage:z \
 +
    -v $(pwd)/qdrant_snapshots:/qdrant/snapshots:z \
 +
    -v $(pwd)/qdrant/config/production.yaml:/qdrant/config/production.yaml:z \
 +
    qdrant/qdrant:gpu-nvidia-latest
 +
</pre>
 +
 +
=PyMuPDF=
 +
 +
'''NOTA''': interessa extreure el text dels pdfs, i fer-ne chunks. Però les fonts documentals que tindré no seran només pdfs, sinó txt, md,... I després hauré de fer els embeddings dels chunks. Embeddings per al català. I el que s'ingestarà al QDrant seran els embeddings (és a dir, números)
 +
 +
'''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.
 +
 +
<pre>
 +
pip install pymupdf
 +
</pre>
 +
 +
Exemple bàsic: extreure tot el text (funciona)
 +
<pre>
 +
import fitz  # PyMuPDF
 +
 +
doc = fitz.open("sample.pdf")
 +
 +
text_complet = ""
 +
 +
for pagina in doc:
 +
    text_complet += pagina.get_text()
 +
 +
doc.close()
 +
 +
print(text_complet)
 +
</pre>
 +
 +
Extreure pàgina per pàgina
 +
 +
Això és útil en un RAG perquè pots guardar metadades com el número de pàgina.
 +
 +
<pre>
 +
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])
 +
</pre>
 +
 +
Obtenir text amb coordenades
 +
 +
Si més endavant vols reconstruir capítols, notes al peu o columnes:
 +
 +
<pre>
 +
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)
 +
</pre>
 +
 +
Format estructurat (molt útil)
 +
 +
<pre>
 +
import fitz
 +
import json
 +
 +
doc = fitz.open("sample.pdf")
 +
 +
estructura = doc[0].get_text("dict")
 +
 +
print(json.dumps(estructura, indent=2)[:2000])
 +
</pre>
 +
 +
Crear chunks per al RAG
 +
 +
Una aproximació senzilla:
 +
 +
<pre>
 +
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']
 +
</pre>
 +
 +
(tots els exemples funcionen)
 +
 +
Per tant, ja sé fer els chunks per al RAG
 +
 +
 +
<pre>
 +
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
 +
</pre>
 +
 +
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.
 +
 +
<pre>
 +
from llama_index.core.node_parser import SentenceSplitter
 +
 +
splitter = SentenceSplitter(
 +
    chunk_size=800,
 +
    chunk_overlap=100,
 +
)
 +
</pre>
 +
 +
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:
 +
 +
<pre>
 +
from langchain_text_splitters import RecursiveCharacterTextSplitter
 +
</pre>
 +
 +
 +
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
 +
 +
<pre>
 +
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)
 +
</pre>
 +
 +
On:
 +
 +
* chunk_size=1000: mida aproximada del fragment
 +
* chunk_overlap=150: solapament entre fragments
 +
 +
<pre>
 +
for i, chunk in enumerate(chunks):
 +
    print(f"Chunk {i}")
 +
    print(chunk[:200])
 +
</pre>
 +
   
 +
 +
<pre>
 +
Per què "Recursive"?
 +
 +
Intenta dividir respectant aquesta jerarquia:
 +
 +
Paràgrafs
 +
↓
 +
Línies
 +
↓
 +
Espais
 +
↓
 +
Caràcters
 +
</pre>
 +
 +
 +
És interessant guardar les metadades (recomanat per RAG), per ex, el número de pàgina.
 +
 +
 +
Si tens pàgines de PyMuPDF:
 +
 +
<pre>
 +
documents = [
 +
    {
 +
        "text": page_text,
 +
        "page": page_num
 +
    }
 +
]
 +
</pre>
 +
 +
per ex:
 +
 +
<pre>
 +
documents = [
 +
    {
 +
        "text": "kjh jhlkj hl",
 +
        "page": 1
 +
    }
 +
]
 +
</pre>
 +
 +
pots convertir-les en Documents:
 +
 +
<pre>
 +
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)
 +
</pre>
 +
 +
'''Exemple complet amb PyMuPDF''':
 +
 +
<pre>
 +
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)
 +
</pre>
 +
 +
(TODO)
 +
 +
* I ara que tinc tots els chunks d'un document, em falta fer la ingesta del document al QDrant. -> compte! la ingesta que farem al QDrant són els embeddings.
 +
* I fer consultes sobre el document
 +
 +
{{Autor}}, octubre 2026

Revisió de 22:48, 15 oct 2026

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

Run with default settings:

docker run -p 6333:6333 qdrant/qdrant

Connexió des del portàtil

Normalment treballo per SSH des del portàtil (headless server). Per tant, vull connectar-me des del portàtil. La IP fixa del servidor és 192.168.1.184.

L'opció recomanada és fer un túnel SSH

El teu portàtil            ->  Connexió SSH                -> Servidor 192.168.1.184
Navegador: localhost:6333  <-  Túnel xifrat pel port 6333  <- Qdrant escolta al port 6333

Des del portàtil ja ens connectàvem per ssh (sense password), però ara li afegim el port 6333

ssh -L 6333:127.0.0.1:6333 joan@192.168.1.184

I ara ja funciona des del portàtil:

Seguim amb el quickstart

o bé la versió oline, on s'utililtza curl:

Afegim una nova col·lecció:

PUT collections/star_charts

{

  "vectors": {

    "size": 4,

    "distance": "Dot"

  }

}

En aquest cas hem clicat RUN, però internament la manera d'executar seria:

curl -X PUT 'http://localhost:6333/collections/test_collection' \
    -H 'Content-Type: application/json' \
    --data-raw '{
        "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"
      }
    }
  ]
}

o bé:

curl -L -X PUT 'http://localhost:6333/collections/test_collection/points?wait=true' \
    -H 'Content-Type: application/json' \
    --data-raw '{
        "points": [
          {"id": 1, "vector": [0.05, 0.61, 0.76, 0.74], "payload": {"city": "Berlin"}},
          {"id": 2, "vector": [0.19, 0.81, 0.75, 0.11], "payload": {"city": ["Berlin", "London"]}},
          {"id": 3, "vector": [0.36, 0.55, 0.47, 0.94], "payload": {"city": ["Berlin", "Moscow"]}},
          {"id": 4, "vector": [0.18, 0.01, 0.85, 0.80], "payload": {"city": ["London", "Moscow"]}},
          {"id": 5, "vector": [0.24, 0.18, 0.22, 0.44], "payload": {"count": [0]}},
          {"id": 6, "vector": [0.35, 0.08, 0.11, 0.44]}
        ]
    }'

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
}

o bé:

curl -L -X POST 'http://localhost:6333/collections/test_collection/points/search' \
    -H 'Content-Type: application/json' \
    --data-raw '{
        "vector": [0.2, 0.1, 0.9, 0.7],
        "top": 3
    }'

resultat:

{"result":[{"id":4,"version":1,"score":1.362},{"id":1,"version":1,"score":1.273},{"id":3,"version":1,"score":1.208}],"status":"ok","time":0.000478823}

Persistència

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)/qdrant/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.

Understanding the volume mounts:

  • /qdrant/storage - Where Qdrant persists all your data (mount as volume to prevent data loss)
  • /qdrant/snapshots - Where Qdrant stores collection snapshots for backup/restore
  • /qdrant/config/production.yaml - Custom configuration file (override values from the reference config)
mkdir -p ~/qdrant/config
nano ~/qdrant/config/production.yaml

Contingut mínim:

log_level: INFO

service:

 host: 127.0.0.1
 http_port: 6333
 grpc_port: 6334

Però el production.yaml del tutorial és:

service:
  # Required: Enable API key authentication
  api_key: your_secret_api_key_here
  
  # Optional: Enable TLS
  enable_tls: true
  
  host: 0.0.0.0
  http_port: 6333
  grpc_port: 6334

storage:
  # Persist data to disk
  storage_path: ./storage
  snapshots_path: ./snapshots
  
  # Reduce RAM usage by storing payloads on disk
  on_disk_payload: true
  
  # WAL configuration
  wal:
    wal_capacity_mb: 32
    wal_segments_ahead: 0

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:

Creem la col·lecció:

curl -X PUT \
  http://localhost:6333/collections/terraforming \
  -H 'Content-Type: application/json' \
  -d '{
    "vectors": {
      "size": 4,
      "distance": "Dot"
    }
  }'

Configuració de la col·lecció, incloent-hi la mida dels vectors i la mètrica de distància:

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}

Inserim dades d'exemple:

curl -X PUT \
  http://localhost:6333/collections/terraforming/points \
  -H 'Content-Type: application/json' \
  -d '{
    "points": [
      {
        "id": 1,
        "vector": [0.1, 0.2, 0.3, 0.4],
        "payload": {"land": "forest", "color": "green", "life": true, "humidity": 40}
      },
      {
        "id": 2,
        "vector": [0.2, 0.3, 0.4, 0.5],
        "payload": {"land": "lake", "color": "blue", "life": true, "humidity": 100}
      },
      {
        "id": 3,
        "vector": [0.3, 0.4, 0.5, 0.6],
        "payload": {"land": "steppe", "color": "green", "life": false, "humidity": 25}
      },
      {
        "id": 4,
        "vector": [0.4, 0.5, 0.6, 0.7],
        "payload": {"land": "desert", "color": "red", "life": false, "humidity": 5}
      },
      {
        "id": 5,
        "vector": [0.5, 0.6, 0.7, 0.8],
        "payload": {"land": "marsh", "color": "black", "life": true, "humidity": 90}
      },
      {
        "id": 6,
        "vector": [0.6, 0.7, 0.8, 0.9],
        "payload": {"land": "cavern", "color": "black", "life": false, "humidity": 15}
      }
    ]
  }'

Comprovem que les dades hi són:

curl -X POST \
  http://localhost:6333/collections/terraforming/points/scroll \
  -H 'Content-Type: application/json' \
  -d '{
    "limit": 10,
    "with_payload": true,
    "with_vector": true
  }'

Resultat:

{"result":{"points":[{"id":1,"payload":{"land":"forest","color":"green","life":true,"humidity":40},"vector":[0.1,0.2,0.3,0.4]},{"id":2,"payload":{"land":"lake","color":"blue","life":true,"humidity":100},"vector":[0.2,0.3,0.4,0.5]},{"id":3,"payload":{"land":"steppe","color":"green","life":false,"humidity":25},"vector":[0.3,0.4,0.5,0.6]},{"id":4,"payload":{"land":"desert","color":"red","life":false,"humidity":5},"vector":[0.4,0.5,0.6,0.7]},{"id":5,"payload":{"land":"marsh","color":"black","life":true,"humidity":90},"vector":[0.5,0.6,0.7,0.8]},{"id":6,"payload":{"land":"cavern","color":"black","life":false,"humidity":15},"vector":[0.6,0.7,0.8,0.9]}],"next_page_offset":null},"status":"ok","time":0.000492869}

Provem un filtre:

curl -X POST \
  http://localhost:6333/collections/terraforming/points/scroll \
  -H 'Content-Type: application/json' \
  -d '{
    "filter": {
      "must": [
        {
          "key": "color",
          "match": {"value": "black"}
        }
      ]
    },
    "limit": 10,
    "with_payload": true
  }'

Resultat:

{"result":{"points":[{"id":5,"payload":{"land":"marsh","color":"black","life":true,"humidity":90}},{"id":6,"payload":{"land":"cavern","color":"black","life":false,"humidity":15}}],"next_page_offset":null},"status":"ok","time":0.000402086}

Com que estic amb persistència, la propera vegada que arrenqui el QDrant les dades hi seran.

QDrant amb GPU support

Abans que res, hem d'instal·lar el NVIDIA Container Toolkit, és a dir, les eines per tal de que la GPU sàpiga treballar amb els contenidors.

sudo apt-get update
sudo apt-get install -y ca-certificates curl gnupg

curl -fsSL https://nvidia.github.io/libnvidia-container/gpgkey \
  | sudo gpg --dearmor --yes \
  -o /usr/share/keyrings/nvidia-container-toolkit-keyring.gpg

curl -s -L \
  https://nvidia.github.io/libnvidia-container/stable/deb/nvidia-container-toolkit.list \
  | sed 's#deb https://#deb [signed-by=/usr/share/keyrings/nvidia-container-toolkit-keyring.gpg] https://#g' \
  | sudo tee /etc/apt/sources.list.d/nvidia-container-toolkit.list

sudo apt-get update
sudo apt-get install -y nvidia-container-toolkit

Configurem docker:

sudo nvidia-ctk runtime configure --runtime=docker

Aquesta ordre configura Docker perquè pugui utilitzar el runtime NVIDIA. Després, reinicia Docker:

sudo systemctl restart docker

Comprova que Docker veu la GPU: (Hauries de veure nvidia entre els runtimes disponibles.)

$ docker info | grep -i runtime
 Runtimes: nvidia runc io.containerd.runc.v2
 Default Runtime: runc
# Pull GPU-enabled image
docker pull qdrant/qdrant:gpu-nvidia-latest

I ara ja podem utilitzar QDrant aprofitant-nos de la GPU:
# Run with GPU access
docker run -p 6333:6333 \
    --gpus all \
    qdrant/qdrant:gpu-nvidia-latest

I amb persistència: (reaprofito els punts de storage, suposant que no hi ha problema)

docker run -p 6333:6333 -p 6334:6334 \
    --gpus all \
    -v $(pwd)/qdrant_storage:/qdrant/storage:z \
    -v $(pwd)/qdrant_snapshots:/qdrant/snapshots:z \
    -v $(pwd)/qdrant/config/production.yaml:/qdrant/config/production.yaml:z \
    qdrant/qdrant:gpu-nvidia-latest

PyMuPDF

NOTA: interessa extreure el text dels pdfs, i fer-ne chunks. Però les fonts documentals que tindré no seran només pdfs, sinó txt, md,... I després hauré de fer els embeddings dels chunks. Embeddings per al català. I el que s'ingestarà al QDrant seran els embeddings (és a dir, números)

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. -> compte! la ingesta que farem al QDrant són els embeddings.
  • I fer consultes sobre el document

creat per Joan Quintana Compte, octubre 2026