QDrant. Bases de dades semàntiques

De wikijoan
Salta a la navegació Salta a la cerca

Contingut

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