documind-ai / tests /test_faiss_integration.py
HarryDataScience's picture
Replace with new DocuMind AI (FastAPI + FAISS)
5959a78
Raw History Blame Contribute Delete
6.02 kB
"""FAISS + real ChromaDB through VectorStoreService, compared with the Chroma-only backend."""
import hashlib
import sys
import fitz
import numpy as np
import pytest
from backend.core import settings
from backend.services.pdf_processor import PDFProcessor
from backend.services.vector_store import VectorStoreService
class FakeEmbedder:
"""Deterministic bag-of-words hashing into 64 dims (stand-in for MiniLM)."""
DIM = 64
def _vec(self, text):
v = np.zeros(self.DIM, dtype="float32")
for word in text.lower().split():
v[int(hashlib.md5(word.encode()).hexdigest(), 16) % self.DIM] += 1.0
n = np.linalg.norm(v)
return v / n if n else v
def embed_texts(self, texts):
return np.stack([self._vec(t) for t in texts])
def embed_query(self, text):
return self._vec(text)
TEXTS = {
"finance.pdf": "Quarterly revenue increased twelve percent due to subscription growth",
"biology.pdf": "Mitochondria produce cellular energy through oxidative phosphorylation",
"travel.pdf": "The ferry to the island departs every morning from the northern harbour",
}
def make_doc(tmp_path, name):
path = tmp_path / name
pdf = fitz.open(); pdf.new_page().insert_text((72, 72), TEXTS[name]); pdf.save(path); pdf.close()
return PDFProcessor().process(str(path))
def make_store(path, backend, monkeypatch):
monkeypatch.setattr(settings, "vectorstore_path", str(path))
monkeypatch.setattr(settings, "vector_backend", backend)
store = VectorStoreService()
store.embedding_service = FakeEmbedder()
return store
@pytest.fixture(params=["faiss", "chroma"])
def backend(request):
return request.param
def test_full_lifecycle_on_both_backends(tmp_path, monkeypatch, backend):
store = make_store(tmp_path / "db", backend, monkeypatch)
assert store.backend == backend
finance, biology = make_doc(tmp_path, "finance.pdf"), make_doc(tmp_path, "biology.pdf")
assert store.index_document(finance, "alice") and store.index_document(biology, "bob")
top = store.search(TEXTS["finance.pdf"], top_k=3)[0]
assert top["doc_name"] == "finance.pdf" and top["page_number"] == 1
assert top["similarity"] == pytest.approx(1.0, abs=1e-3)
assert all(h["doc_name"] == "finance.pdf" for h in store.search("energy", top_k=5, owner_id="alice"))
only_bio = store.search("revenue growth", doc_ids=[biology.doc_id], top_k=5)
assert only_bio and all(h["doc_name"] == "biology.pdf" for h in only_bio)
assert not store.delete_document(finance.doc_id, "bob")
assert store.delete_document(finance.doc_id, "alice")
assert all(h["doc_name"] != "finance.pdf" for h in store.search("revenue", top_k=5))
assert store.search("anything", doc_ids=["unknown-id"]) == []
def test_faiss_and_chroma_return_the_same_ranking(tmp_path, monkeypatch):
docs = {n: make_doc(tmp_path, n) for n in TEXTS}
results = {}
for name in ("faiss", "chroma"):
store = make_store(tmp_path / f"db_{name}", name, monkeypatch)
for doc in docs.values():
store.index_document(doc, "")
results[name] = {q: [(h["doc_name"], h["similarity"]) for h in store.search(q, top_k=3)]
for q in ("subscription revenue", "cellular energy", "ferry harbour morning")}
for query in results["faiss"]:
f, c = results["faiss"][query], results["chroma"][query]
assert [d for d, _ in f] == [d for d, _ in c]
assert [s for _, s in f] == pytest.approx([s for _, s in c], abs=2e-3)
def test_faiss_is_rebuilt_from_chroma_after_restart(tmp_path, monkeypatch):
first = make_store(tmp_path / "db", "faiss", monkeypatch)
doc = make_doc(tmp_path, "biology.pdf")
first.index_document(doc, "")
assert first._faiss.ntotal == doc.total_chunks
restarted = make_store(tmp_path / "db", "faiss", monkeypatch) # new process: FAISS starts empty
assert restarted._faiss.ntotal == 0
hits = restarted.search("mitochondria energy", top_k=3)
assert hits and hits[0]["doc_name"] == "biology.pdf"
assert restarted._faiss.ntotal == doc.total_chunks
def test_faiss_heals_itself_when_it_drifts_from_chroma(tmp_path, monkeypatch):
store = make_store(tmp_path / "db", "faiss", monkeypatch)
store.index_document(make_doc(tmp_path, "finance.pdf"), "")
store._faiss.clear() # simulate a lost / stale index
assert store.search("subscription revenue", top_k=3)[0]["doc_name"] == "finance.pdf"
def test_new_upload_after_restart_keeps_old_documents_searchable(tmp_path, monkeypatch):
first = make_store(tmp_path / "db", "faiss", monkeypatch)
first.index_document(make_doc(tmp_path, "finance.pdf"), "")
second = make_store(tmp_path / "db", "faiss", monkeypatch)
second.index_document(make_doc(tmp_path, "travel.pdf"), "") # FAISS empty before this upload
assert second.search("subscription revenue", top_k=3)[0]["doc_name"] == "finance.pdf"
assert second.search("ferry harbour", top_k=3)[0]["doc_name"] == "travel.pdf"
def test_falls_back_to_chroma_when_faiss_is_not_installed(tmp_path, monkeypatch):
monkeypatch.setitem(sys.modules, "faiss", None) # makes `import faiss` fail
store = make_store(tmp_path / "db", "faiss", monkeypatch)
assert store.backend == "chroma" and store._faiss is None
store.index_document(make_doc(tmp_path, "finance.pdf"), "")
assert store.search("subscription revenue", top_k=1)[0]["doc_name"] == "finance.pdf"
def test_invalid_backend_setting_is_rejected():
from backend.core.config import Settings
with pytest.raises(ValueError):
Settings(vector_backend="pinecone")
assert Settings(vector_backend=" FAISS ").vector_backend == "faiss"
def test_health_endpoint_reports_the_vector_backend():
from fastapi.testclient import TestClient
import main
assert TestClient(main.app).get("/api/health").json()["vector_backend"] in {"faiss", "chroma"}