mirror of
https://github.com/tiennm99/DocsGPT.git
synced 2026-10-15 04:17:56 +00:00
Chunks preview
This commit is contained in:
1 parent
73c3dfb5c4
commit
0447eb9b8d
33 files changed
+1961
-109
No files matched your search
@@ -384,3 +384,34 @@ class TestBaseVectorStore:
|
||||
result = store._get_embeddings("some_custom_embedding")
|
||||
assert result is mock_emb
|
||||
mock_get_instance.assert_called_with("some_custom_embedding")
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
class TestSearchWithScoresDefault:
|
||||
def test_pairs_hits_with_none(self):
|
||||
"""A store that reports no score still satisfies the contract, so the
|
||||
retriever never has to special-case it."""
|
||||
from application.vectorstore.base import BaseVectorStore
|
||||
|
||||
class _Store(BaseVectorStore):
|
||||
def search(self, question, k=2, *args, **kwargs):
|
||||
return ["a", "b"]
|
||||
|
||||
def add_texts(self, texts, metadatas=None, *args, **kwargs):
|
||||
return []
|
||||
|
||||
store = _Store()
|
||||
assert store.score_kind is None
|
||||
assert store.search_with_scores("q", k=2) == [("a", None), ("b", None)]
|
||||
|
||||
def test_handles_store_returning_none(self):
|
||||
from application.vectorstore.base import BaseVectorStore
|
||||
|
||||
class _Store(BaseVectorStore):
|
||||
def search(self, question, k=2, *args, **kwargs):
|
||||
return None
|
||||
|
||||
def add_texts(self, texts, metadatas=None, *args, **kwargs):
|
||||
return []
|
||||
|
||||
assert _Store().search_with_scores("q") == []
|
||||
@@ -562,3 +562,67 @@ class TestFaissStoreAssertEmbeddingDimensionsMatch:
|
||||
# Should not raise since embedding name is not the huggingface one
|
||||
store = FaissStore(source_id="t", embeddings_key="k", docs_init=[Mock()])
|
||||
assert store is not None
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
class TestFaissSearchWithScores:
|
||||
@staticmethod
|
||||
def _build(mock_faiss, mock_get_emb, mock_settings, mock_storage_creator, ds):
|
||||
mock_settings.EMBEDDINGS_NAME = "test_model"
|
||||
mock_get_emb.return_value = Mock(dimension=3)
|
||||
mock_faiss.from_documents.return_value = ds
|
||||
mock_storage_creator.get_storage.return_value = Mock()
|
||||
|
||||
from application.vectorstore.faiss import FaissStore
|
||||
|
||||
return FaissStore(source_id="t", embeddings_key="k", docs_init=[Mock()])
|
||||
|
||||
@patch("application.vectorstore.faiss.StorageCreator")
|
||||
@patch("application.vectorstore.faiss.FAISS")
|
||||
@patch.object(
|
||||
__import__("application.vectorstore.base", fromlist=["BaseVectorStore"]).BaseVectorStore,
|
||||
"_get_embeddings",
|
||||
)
|
||||
@patch("application.vectorstore.faiss.settings")
|
||||
def test_reports_l2_distance_and_drops_threshold(
|
||||
self, mock_settings, mock_get_emb, mock_faiss, mock_storage_creator
|
||||
):
|
||||
doc = Mock(page_content="text1", metadata={"source": "a"})
|
||||
ds = Mock(index=Mock(d=3))
|
||||
ds.similarity_search_with_score.return_value = [(doc, 0.42)]
|
||||
store = self._build(
|
||||
mock_faiss, mock_get_emb, mock_settings, mock_storage_creator, ds
|
||||
)
|
||||
|
||||
results = store.search_with_scores("query", k=5, score_threshold=0.9)
|
||||
|
||||
# LangChain's FAISS ranks by L2 distance, NOT cosine similarity — the
|
||||
# label must say so, and score_threshold must be dropped (FAISS has no
|
||||
# such knob and would crash on it).
|
||||
assert store.score_kind == "l2_distance"
|
||||
ds.similarity_search_with_score.assert_called_once_with("query", k=5)
|
||||
assert results == [(doc, 0.42)]
|
||||
|
||||
@patch("application.vectorstore.faiss.StorageCreator")
|
||||
@patch("application.vectorstore.faiss.FAISS")
|
||||
@patch.object(
|
||||
__import__("application.vectorstore.base", fromlist=["BaseVectorStore"]).BaseVectorStore,
|
||||
"_get_embeddings",
|
||||
)
|
||||
@patch("application.vectorstore.faiss.settings")
|
||||
def test_does_not_mutate_docstore_metadata(
|
||||
self, mock_settings, mock_get_emb, mock_faiss, mock_storage_creator
|
||||
):
|
||||
# similarity_search_with_score hands back the LIVE docstore Documents;
|
||||
# writing a score into their metadata would pollute the in-memory index
|
||||
# and could be persisted back to storage by a later add_chunk.
|
||||
doc = Mock(page_content="text1", metadata={"source": "a"})
|
||||
ds = Mock(index=Mock(d=3))
|
||||
ds.similarity_search_with_score.return_value = [(doc, 0.42)]
|
||||
store = self._build(
|
||||
mock_faiss, mock_get_emb, mock_settings, mock_storage_creator, ds
|
||||
)
|
||||
|
||||
store.search_with_scores("query", k=1)
|
||||
|
||||
assert doc.metadata == {"source": "a"}
|
||||
@@ -287,3 +287,40 @@ class TestMongoDBVectorStoreDeleteChunk:
|
||||
|
||||
result = store.delete_chunk("bad_id")
|
||||
assert result is False
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
class TestMongoDBSearchWithScores:
|
||||
def test_reports_vector_search_score(self):
|
||||
store, mock_collection, _ = _make_mongodb_store()
|
||||
mock_collection.aggregate.return_value = iter(
|
||||
[
|
||||
{
|
||||
"_id": "id1",
|
||||
"text": "hello",
|
||||
"embedding": [0.1],
|
||||
"source": "a",
|
||||
"_score": 0.83,
|
||||
}
|
||||
]
|
||||
)
|
||||
|
||||
results = store.search_with_scores("query", k=1)
|
||||
|
||||
assert store.score_kind == "cosine_similarity"
|
||||
assert results[0][0].page_content == "hello"
|
||||
assert results[0][1] == pytest.approx(0.83)
|
||||
# The score must not leak into metadata — it rides alongside the doc.
|
||||
assert "_score" not in results[0][0].metadata
|
||||
|
||||
def test_score_is_added_even_without_a_threshold(self):
|
||||
"""The $addFields stage is unconditional, so an unfiltered search still
|
||||
carries a score (the whole point of the retrieval tester)."""
|
||||
store, mock_collection, _ = _make_mongodb_store()
|
||||
mock_collection.aggregate.return_value = iter([])
|
||||
|
||||
store.search_with_scores("query", k=1)
|
||||
|
||||
pipeline = mock_collection.aggregate.call_args[0][0]
|
||||
assert any("$addFields" in stage for stage in pipeline)
|
||||
assert not any("$match" in stage for stage in pipeline)
|
||||
@@ -403,3 +403,38 @@ class TestPGVectorStoreConnection:
|
||||
|
||||
store.__del__()
|
||||
mock_conn.close.assert_called_once()
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
class TestPGVectorSearchWithScores:
|
||||
def test_reports_cosine_similarity(self):
|
||||
store, _, mock_cursor, _ = _make_store()
|
||||
mock_cursor.fetchall.return_value = [
|
||||
("close", {"source": "a.txt"}, 0.10),
|
||||
("far", {"source": "b.txt"}, 0.40),
|
||||
]
|
||||
|
||||
results = store.search_with_scores("query", k=2)
|
||||
|
||||
assert store.score_kind == "cosine_similarity"
|
||||
assert [doc.page_content for doc, _ in results] == ["close", "far"]
|
||||
# similarity = 1 - cosine distance, the quantity score_threshold uses.
|
||||
assert results[0][1] == pytest.approx(0.90)
|
||||
assert results[1][1] == pytest.approx(0.60)
|
||||
|
||||
def test_honours_score_threshold(self):
|
||||
store, _, mock_cursor, _ = _make_store()
|
||||
mock_cursor.fetchall.return_value = [
|
||||
("close", {}, 0.10), # sim 0.90 → kept
|
||||
("far", {}, 0.40), # sim 0.60 → dropped
|
||||
]
|
||||
|
||||
results = store.search_with_scores("query", k=5, score_threshold=0.85)
|
||||
|
||||
assert [doc.page_content for doc, _ in results] == ["close"]
|
||||
|
||||
def test_returns_empty_on_error(self):
|
||||
store, _, mock_cursor, _ = _make_store()
|
||||
mock_cursor.execute.side_effect = Exception("connection lost")
|
||||
|
||||
assert store.search_with_scores("query") == []
|
||||
Reference in new issue
Block a user