finpy1789's picture
Per-session workspaces + multi-format ingestion (CSV/XLSX/XLS/JSON/images-OCR) with Markdown normalisation
450d35a verified
Raw
History Blame Contribute Delete
3.24 kB
"""ChromaDB vector store with local sentence-transformer embeddings.
Embeddings run locally so retrieval works without any API key and keeps
document content off third-party embedding endpoints.
"""
from __future__ import annotations
from functools import lru_cache
import chromadb
from config import CHROMA_DIR, COLLECTION_NAME, EMBEDDING_MODEL, TOP_K
from src.ingestion.chunker import Chunk
@lru_cache(maxsize=1)
def _embedder():
from sentence_transformers import SentenceTransformer
return SentenceTransformer(EMBEDDING_MODEL)
class VectorStore:
def __init__(self, persist_dir: str = CHROMA_DIR,
collection: str = COLLECTION_NAME):
self._client = chromadb.PersistentClient(path=persist_dir)
self._name = collection
self._collection = self._client.get_or_create_collection(
self._name, metadata={"hnsw:space": "cosine"}
)
# in-memory chunk registry for BM25 + citation lookups
self._chunks: dict[str, Chunk] = {}
# -- indexing -----------------------------------------------------------
def add(self, chunks: list[Chunk]) -> int:
if not chunks:
return 0
embeddings = _embedder().encode([c.text for c in chunks], show_progress_bar=False)
self._collection.upsert(
ids=[c.chunk_id for c in chunks],
documents=[c.text for c in chunks],
embeddings=embeddings.tolist(),
metadatas=[c.metadata for c in chunks],
)
for c in chunks:
self._chunks[c.chunk_id] = c
return len(chunks)
def clear(self):
self._client.delete_collection(self._name)
self._collection = self._client.get_or_create_collection(
self._name, metadata={"hnsw:space": "cosine"}
)
self._chunks.clear()
def drop(self):
"""Delete the collection permanently (session-workspace eviction)."""
try:
self._client.delete_collection(self._name)
except Exception:
pass
self._chunks.clear()
# -- access -------------------------------------------------------------
@property
def chunks(self) -> list[Chunk]:
return list(self._chunks.values())
def get(self, chunk_id: str) -> Chunk | None:
return self._chunks.get(chunk_id)
def doc_ids(self) -> list[str]:
return sorted({c.doc_id for c in self._chunks.values()})
# -- search -------------------------------------------------------------
def semantic_search(self, query: str, k: int = TOP_K,
doc_ids: list[str] | None = None) -> list[tuple[str, float]]:
"""Return [(chunk_id, similarity)] — cosine similarity in [0, 1]."""
if not self._chunks:
return []
where = {"doc_id": {"$in": doc_ids}} if doc_ids else None
embedding = _embedder().encode([query], show_progress_bar=False)
res = self._collection.query(
query_embeddings=embedding.tolist(),
n_results=min(k, len(self._chunks)),
where=where,
)
ids = res["ids"][0]
distances = res["distances"][0]
return [(cid, 1.0 - d) for cid, d in zip(ids, distances)]