Migrare RAG de la ChromaDB la Qdrant pentru scalare – căutare hibridă și filtrare avansată pe metadate

Configurare noua (How To)

Situatie

Soluția RAG construită anterior cu ChromaDB funcționează bine pentru câteva sute de documente, dar pe măsură ce baza de cunoștințe crește (mii de documente, milioane de fragmente), lipsa filtrării avansate pe metadate și limitările de performanță devin vizibile.

Qdrant este o bază de date vectorială concepută pentru scalare — oferă filtrare puternică pe metadate (ex. „caută doar în documente din categoria Securitate, modificate în ultimele 30 de zile”), dashboard vizual integrat și performanță mai bună la volume mari.

Backup

Migrarea nu șterge datele din ChromaDB — se creează o bază Qdrant separată, în paralel. Păstrează folderul ChromaDB existent ca fallback până confirmi funcționarea corectă.

Copy-Item "C:\RAG\db" "C:\RAG\db_chromadb_backup" -Recurse

Solutie

Pasi de urmat

Instalare Qdrant (Docker) și client Python

# Ruleaza Qdrant local intr-un container
docker run -d --name qdrant `
    -p 6333:6333 -p 6334:6334 `
    -v "C:\RAG\qdrant_storage:/qdrant/storage" `
    qdrant/qdrant

# Instaleaza clientul Python
pip install qdrant-client ollama pymupdf python-docx

Verificare funcționare Qdrant

# Dashboard-ul vizual Qdrant este disponibil in browser
Start-Process "http://localhost:6333/dashboard"

Script de migrare/indexare cu metadate extinse

Salvează ca qdrant_indexer.py:

# ============================================================
# Qdrant Indexer - migrare/indexare cu metadate avansate
# Autor: Claudiu C.
# ============================================================

import os
import fitz
import ollama
from datetime import datetime
from docx import Document
from qdrant_client import QdrantClient
from qdrant_client.models import Distance, VectorParams, PointStruct

DOCS_PATH   = r"C:\RAG\Documente"
EMBED_MODEL = "nomic-embed-text"
COLLECTION  = "documente_interne"
VECTOR_SIZE = 768   # dimensiunea vectorilor nomic-embed-text

client = QdrantClient(host="localhost", port=6333)


def ensure_collection():
    collections = [c.name for c in client.get_collections().collections]
    if COLLECTION not in collections:
        client.create_collection(
            collection_name=COLLECTION,
            vectors_config=VectorParams(size=VECTOR_SIZE, distance=Distance.COSINE)
        )


def extract_pdf(path):
    doc = fitz.open(path)
    return "\n".join(page.get_text() for page in doc)


def extract_word(path):
    doc = Document(path)
    return "\n".join(p.text for p in doc.paragraphs if p.text.strip())


def chunk_text(text, size=500):
    words, chunks, current = text.split(), [], []
    for word in words:
        current.append(word)
        if len(" ".join(current)) >= size:
            chunks.append(" ".join(current))
            current = []
    if current:
        chunks.append(" ".join(current))
    return chunks


def get_embedding(text):
    return ollama.embeddings(model=EMBED_MODEL, prompt=text)["embedding"]


def index_document(filepath, category="General"):
    filename = os.path.basename(filepath)

    if filename.endswith(".pdf"):
        text = extract_pdf(filepath)
    else:
        text = extract_word(filepath)

    chunks = chunk_text(text)
    points = []

    for i, chunk in enumerate(chunks):
        embedding = get_embedding(chunk)
        points.append(PointStruct(
            id=hash(f"{filename}_{i}") & 0x7FFFFFFF,
            vector=embedding,
            payload={
                "source": filename,
                "category": category,
                "chunk_text": chunk[:200],
                "indexed_at": datetime.now().isoformat(),
                "chunk_index": i
            }
        ))

    client.upsert(collection_name=COLLECTION, points=points)
    return len(points)


def main():
    ensure_collection()
    files = [f for f in os.listdir(DOCS_PATH) if f.endswith((".pdf", ".docx"))]

    for i, filename in enumerate(files, 1):
        print(f"[{i}/{len(files)}] Indexez: {filename}")
        filepath = os.path.join(DOCS_PATH, filename)

        # Categorie simpla dupa numele folderului sau prefix fisier
        category = "Securitate" if "securit" in filename.lower() else "General"

        count = index_document(filepath, category=category)
        print(f"   -> {count} fragmente indexate (categorie: {category})")

    print(f"\n[DONE] Total puncte in colectie: {client.count(COLLECTION).count}")


if __name__ == "__main__":
    main()

Interogare cu filtrare pe metadate

Salvează ca qdrant_query.py:

# ============================================================
# Qdrant Query - interogare cu filtrare pe metadate
# ============================================================

import ollama
from qdrant_client import QdrantClient
from qdrant_client.models import Filter, FieldCondition, MatchValue

EMBED_MODEL = "nomic-embed-text"
GEN_MODEL   = "llama3"
COLLECTION  = "documente_interne"

client = QdrantClient(host="localhost", port=6333)


def get_embedding(text):
    return ollama.embeddings(model=EMBED_MODEL, prompt=text)["embedding"]


def search(question, category_filter=None, top_k=3):
    embedding = get_embedding(question)

    query_filter = None
    if category_filter:
        query_filter = Filter(
            must=[FieldCondition(key="category", match=MatchValue(value=category_filter))]
        )

    results = client.search(
        collection_name=COLLECTION,
        query_vector=embedding,
        query_filter=query_filter,
        limit=top_k
    )
    return results


def generate_answer(question, results):
    context = "\n\n".join([r.payload["chunk_text"] for r in results])
    sources = list(set(r.payload["source"] for r in results))

    prompt = f"""Raspunde EXCLUSIV pe baza documentatiei de mai jos, in limba romana.
Daca informatia nu se regaseste, spune clar acest lucru.

DOCUMENTATIE:
{context}

INTREBARE:
{question}
"""
    response = ollama.chat(model=GEN_MODEL, messages=[{"role": "user", "content": prompt}])
    return response["message"]["content"], sources


if __name__ == "__main__":
    print("=== Chatbot RAG cu Qdrant ===")
    print("Scrie 'exit' pentru a iesi. Prefixeaza cu 'categorie:Securitate ' pentru filtrare.\n")

    while True:
        question = input("Intrebare: ").strip()
        if question.lower() == "exit":
            break

        category = None
        if question.startswith("categorie:"):
            parts = question.split(" ", 1)
            category = parts[0].replace("categorie:", "")
            question = parts[1] if len(parts) > 1 else ""

        results = search(question, category_filter=category)
        answer, sources = generate_answer(question, results)

        print(f"\nRASPUNS:\n{answer}")
        print(f"\nSurse: {', '.join(sources)}\n")
        print("-" * 50)

Rulare

# Indexare initiala (sau re-indexare la documente noi)
python qdrant_indexer.py

# Interogare interactiva
python qdrant_query.py

Tip solutie

Permanent

Plan de restaurare in caz de nefunctionare

# Opreste si sterge containerul Qdrant
docker stop qdrant
docker rm qdrant

# ChromaDB (backup-ul facut la Pasul de backup) ramane functional ca fallback
# Sterge datele Qdrant daca migrarea se anuleaza definitiv
Remove-Item “C:\RAG\qdrant_storage” -Recurse -Force

Voteaza

(16 din 30 persoane apreciaza acest articol)

Despre Autor

Leave A Comment?