Diferència entre revisions de la pàgina «QDrant. Bases de dades semàntiques»
(Es crea la pàgina amb «=QDrant= * https://qdrant-qdrant-18.mintlify.app/installation <pre> docker pull qdrant/qdrant docker run -p 6333:6333 qdrant/qdrant ... Access web UI at http://l...».) |
m |
||
| (Hi ha 4 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] |
<pre> | <pre> | ||
| Línia 128: | Línia 129: | ||
</pre> | </pre> | ||
| − | + | '''Persistència''': que és guardin les dades | |
<pre> | <pre> | ||
| Línia 168: | Línia 169: | ||
| − | Puc executar la rest api en | + | Puc executar la rest api en '''línia de comanda'''. Per ex: |
<pre> | <pre> | ||
| Línia 179: | Línia 180: | ||
{"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> | ||
| + | |||
| + | =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. | ||
| + | |||
| + | <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. | ||
| + | * I fer consultes sobre el document | ||
| + | |||
| + | {{Autor}}, octubre 2026 | ||
Revisió de 22:32, 7 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
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