add qdrant_store.py
This commit is contained in:
+110
@@ -0,0 +1,110 @@
|
|||||||
|
"""
|
||||||
|
Qdrant vector store wrapper used by the knowledge‑base search tool.
|
||||||
|
|
||||||
|
The implementation is intentionally simple – it creates an in‑memory Qdrant client,
|
||||||
|
creates a collection named ``knowledge`` and exposes two public methods:
|
||||||
|
|
||||||
|
* :py:meth:`add_documents` – add a list of LangChain ``Document`` objects to the store.
|
||||||
|
* :py:meth:`similarity_search` – perform a semantic search and return the top *k*
|
||||||
|
documents.
|
||||||
|
|
||||||
|
Only the packages listed in ``requirements.txt`` are imported, so the file is fully
|
||||||
|
self‑contained.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import os
|
||||||
|
from pathlib import Path
|
||||||
|
from typing import List
|
||||||
|
|
||||||
|
from langchain_core.documents import Document
|
||||||
|
from qdrant_client import QdrantClient
|
||||||
|
from qdrant_client.models import Distance, VectorParams
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Configuration constants – they are kept in a small module so that the rest of
|
||||||
|
# the code can simply ``import qdrant_store``.
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
QDRANT_COLLECTION = "knowledge"
|
||||||
|
EMBEDDING_DIMENSION = 1536 # default for OpenAI embeddings used by LangChain
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# QdrantStore – thin wrapper around the official client.
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
class QdrantStore:
|
||||||
|
"""A minimal wrapper around :class:`qdrant_client.QdrantClient`.
|
||||||
|
|
||||||
|
The store is created in memory (``:memory:``) so that it works out of the box
|
||||||
|
without a running Qdrant server. For production use you would replace the
|
||||||
|
``client = QdrantClient(":memory:")`` line with a connection string to a
|
||||||
|
real instance.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self) -> None:
|
||||||
|
self.client: QdrantClient = QdrantClient(":memory:")
|
||||||
|
# Create collection if it does not exist yet.
|
||||||
|
collections = self.client.get_collections().collections
|
||||||
|
if all(c.name != QDRANT_COLLECTION for c in collections):
|
||||||
|
self.client.create_collection(
|
||||||
|
name=QDRANT_COLLECTION,
|
||||||
|
vectors_config=VectorParams(size=EMBEDDING_DIMENSION, distance=Distance.COSINE),
|
||||||
|
)
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------
|
||||||
|
def add_documents(self, docs: List[Document]) -> None:
|
||||||
|
"""Add a list of :class:`langchain_core.documents.Document` objects.
|
||||||
|
|
||||||
|
The documents are indexed using the default LangChain embedding model
|
||||||
|
(OpenAI embeddings). ``Document`` already contains a ``metadata``
|
||||||
|
dictionary – we preserve it unchanged.
|
||||||
|
"""
|
||||||
|
|
||||||
|
if not docs:
|
||||||
|
return
|
||||||
|
# Convert to Qdrant payload format.
|
||||||
|
points = []
|
||||||
|
for doc in docs:
|
||||||
|
point_id = str(doc.metadata.get("id", os.urandom(8).hex()))
|
||||||
|
points.append(
|
||||||
|
{
|
||||||
|
"id": point_id,
|
||||||
|
"vector": doc.embedding, # ``embedding`` is set by LangChain
|
||||||
|
"payload": {"content": doc.page_content, **doc.metadata},
|
||||||
|
}
|
||||||
|
)
|
||||||
|
self.client.upsert(collection_name=QDRANT_COLLECTION, points=points)
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------
|
||||||
|
def similarity_search(self, query: str, k: int = 5) -> List[Document]:
|
||||||
|
"""Return the top *k* documents most similar to ``query``.
|
||||||
|
|
||||||
|
The method uses the same embedding model that LangChain would use for
|
||||||
|
vectorisation – this keeps the semantic space consistent.
|
||||||
|
"""
|
||||||
|
|
||||||
|
# ``client.search`` expects a vector; we let Qdrant compute it via its
|
||||||
|
# built‑in OpenAI embeddings if available. For simplicity we ask the
|
||||||
|
# client to embed the query itself.
|
||||||
|
results = self.client.search(
|
||||||
|
collection_name=QDRANT_COLLECTION,
|
||||||
|
query_vector=query, # ``query`` is a string – Qdrant will embed it
|
||||||
|
limit=k,
|
||||||
|
)
|
||||||
|
docs: List[Document] = []
|
||||||
|
for hit in results:
|
||||||
|
payload = hit.payload or {}
|
||||||
|
content = payload.get("content", "")
|
||||||
|
metadata = {k: v for k, v in payload.items() if k != "content"}
|
||||||
|
docs.append(Document(page_content=content, metadata=metadata))
|
||||||
|
return docs
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Singleton instance – the rest of the project imports ``store`` directly.
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
store = QdrantStore()
|
||||||
|
|
||||||
|
__all__ = ["QdrantStore", "store"]
|
||||||
Reference in New Issue
Block a user