Files
deepset-ai--haystack/haystack/testing/document_store.py
T
wehub-resource-sync c56bef871b
Sync docs with Docusaurus / sync (push) Waiting to run
Tests / Check if changed (push) Waiting to run
Tests / format (push) Blocked by required conditions
Tests / check-imports (push) Blocked by required conditions
Tests / Unit / macos-latest (push) Blocked by required conditions
Tests / Unit / ubuntu-latest (push) Blocked by required conditions
Tests / Unit / windows-latest (push) Blocked by required conditions
Tests / mypy (push) Blocked by required conditions
Tests / Integration / ubuntu-latest (push) Blocked by required conditions
Tests / Integration / macos-latest (push) Blocked by required conditions
Tests / Integration / windows-latest (push) Blocked by required conditions
Tests / notify-slack-on-failure (push) Blocked by required conditions
Tests / Mark tests as completed (push) Blocked by required conditions
Docker image release / Build base image (push) Waiting to run
CodeQL / Analyze (python) (push) Has been cancelled
Update Platform Components Table / update (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 13:22:28 +08:00

1259 lines
56 KiB
Python

# SPDX-FileCopyrightText: 2022-present deepset GmbH <info@deepset.ai>
#
# SPDX-License-Identifier: Apache-2.0
import inspect
import random
from datetime import datetime
from haystack.dataclasses import Document
from haystack.document_stores.errors import DuplicateDocumentError
from haystack.document_stores.types import DocumentStore, DuplicatePolicy
from haystack.errors import FilterError
from haystack.lazy_imports import LazyImport
with LazyImport("Run 'pip install pytest'") as pytest_import:
import pytest
def _random_embeddings(n):
return [random.random() for _ in range(n)]
# These are random embedding that are used to test filters.
# We declare them here as they're used both in the `filterable_docs` fixture
# and the body of several `filter_documents` tests.
TEST_EMBEDDING_1 = _random_embeddings(768)
TEST_EMBEDDING_2 = _random_embeddings(768)
class AssertDocumentsEqualMixin:
@staticmethod
def assert_documents_are_equal(received: list[Document], expected: list[Document]):
"""
Assert that two lists of Documents are equal.
This is used in every test, if a Document Store implementation has a different behaviour
it should override this method. This can happen for example when the Document Store sets
a score to returned Documents. Since we can't know what the score will be, we can't compare
the Documents reliably.
"""
assert received == expected
class CountDocumentsTest:
"""
Utility class to test a Document Store `count_documents` method.
To use it create a custom test class and override the `document_store` fixture to return your Document Store.
Example usage:
```python
class MyDocumentStoreTest(CountDocumentsTest):
@pytest.fixture
def document_store(self):
return MyDocumentStore()
```
"""
@staticmethod
def test_count_empty(document_store: DocumentStore):
"""Test count is zero for an empty document store"""
assert document_store.count_documents() == 0
@staticmethod
def test_count_not_empty(document_store: DocumentStore):
"""Test count is greater than zero if the document store contains documents"""
document_store.write_documents(
[Document(content="test doc 1"), Document(content="test doc 2"), Document(content="test doc 3")]
)
assert document_store.count_documents() == 3
class WriteDocumentsTest(AssertDocumentsEqualMixin):
"""
Utility class to test a Document Store `write_documents` method.
To use it create a custom test class and override the `document_store` fixture to return your Document Store.
The Document Store `filter_documents` method must be at least partly implemented to return all stored Documents
for these tests to work correctly.
Example usage:
```python
class MyDocumentStoreTest(WriteDocumentsTest):
@pytest.fixture
def document_store(self):
return MyDocumentStore()
```
"""
def test_write_documents(self, document_store: DocumentStore):
"""
Test write_documents() default behaviour.
"""
msg = (
"Default write_documents() behaviour depends on the Document Store implementation, "
"as we don't enforce a default behaviour when no policy is set. "
"Override this test in your custom test class."
)
raise NotImplementedError(msg)
def test_write_documents_duplicate_fail(self, document_store: DocumentStore):
"""Test write_documents() fails when writing documents with same id and `DuplicatePolicy.FAIL`."""
doc = Document(content="test doc")
assert document_store.write_documents([doc], policy=DuplicatePolicy.FAIL) == 1
with pytest.raises(DuplicateDocumentError):
document_store.write_documents(documents=[doc], policy=DuplicatePolicy.FAIL)
self.assert_documents_are_equal(document_store.filter_documents(), [doc])
@staticmethod
def test_write_documents_duplicate_skip(document_store: DocumentStore):
"""Test write_documents() skips writing when using DuplicatePolicy.SKIP."""
doc = Document(content="test doc")
assert document_store.write_documents([doc], policy=DuplicatePolicy.SKIP) == 1
assert document_store.write_documents(documents=[doc], policy=DuplicatePolicy.SKIP) == 0
def test_write_documents_duplicate_overwrite(self, document_store: DocumentStore):
"""Test write_documents() overwrites when using DuplicatePolicy.OVERWRITE."""
doc1 = Document(id="1", content="test doc 1")
doc2 = Document(id="1", content="test doc 2")
assert document_store.write_documents([doc2], policy=DuplicatePolicy.OVERWRITE) == 1
self.assert_documents_are_equal(document_store.filter_documents(), [doc2])
assert document_store.write_documents(documents=[doc1], policy=DuplicatePolicy.OVERWRITE) == 1
self.assert_documents_are_equal(document_store.filter_documents(), [doc1])
@staticmethod
def test_write_documents_invalid_input(document_store: DocumentStore):
"""Test write_documents() fails when providing unexpected input."""
with pytest.raises(ValueError):
document_store.write_documents(["not a document for sure"]) # type: ignore
with pytest.raises(ValueError):
document_store.write_documents("not a list actually") # type: ignore
class DeleteDocumentsTest:
"""
Utility class to test a Document Store `delete_documents` method.
To use it create a custom test class and override the `document_store` fixture to return your Document Store.
The Document Store `write_documents` and `count_documents` methods must be implemented for this tests to work
correctly.
Example usage:
```python
class MyDocumentStoreTest(DeleteDocumentsTest):
@pytest.fixture
def document_store(self):
return MyDocumentStore()
```
"""
@staticmethod
def test_delete_documents(document_store: DocumentStore):
"""Test delete_documents() normal behaviour."""
doc = Document(content="test doc")
document_store.write_documents([doc])
assert document_store.count_documents() == 1
document_store.delete_documents([doc.id])
assert document_store.count_documents() == 0
@staticmethod
def test_delete_documents_empty_document_store(document_store: DocumentStore):
"""Test delete_documents() doesn't fail when called using an empty Document Store."""
document_store.delete_documents(["non_existing_id"])
@staticmethod
def test_delete_documents_non_existing_document(document_store: DocumentStore):
"""Test delete_documents() doesn't delete any Document when called with non-existing id."""
doc = Document(content="test doc")
document_store.write_documents([doc])
assert document_store.count_documents() == 1
document_store.delete_documents(["non_existing_id"])
# No Document has been deleted
assert document_store.count_documents() == 1
def create_filterable_docs() -> list[Document]:
"""
Create a list of filterable documents to be used in the filterable_docs fixture.
"""
documents = []
for i in range(3):
documents.append(
Document(
content=f"A Foo Document {i}",
meta={
"name": f"name_{i}",
"page": "100",
"chapter": "intro",
"number": 2,
"date": "1969-07-21T20:17:40",
},
embedding=_random_embeddings(768),
)
)
documents.append(
Document(
content=f"A Bar Document {i}",
meta={
"name": f"name_{i}",
"page": "123",
"chapter": "abstract",
"number": -2,
"date": "1972-12-11T19:54:58",
},
embedding=_random_embeddings(768),
)
)
documents.append(
Document(
content=f"A Foobar Document {i}",
meta={
"name": f"name_{i}",
"page": "90",
"chapter": "conclusion",
"number": -10,
"date": "1989-11-09T17:53:00",
},
embedding=_random_embeddings(768),
)
)
documents.append(
Document(
content=f"Document {i} without embedding",
meta={"name": f"name_{i}", "no_embedding": True, "chapter": "conclusion"},
)
)
documents.append(
Document(content=f"Doc {i} with zeros emb", meta={"name": "zeros_doc"}, embedding=TEST_EMBEDDING_1)
)
documents.append(
Document(content=f"Doc {i} with ones emb", meta={"name": "ones_doc"}, embedding=TEST_EMBEDDING_2)
)
return documents
class FilterableDocsFixtureMixin:
"""
Mixin class that adds a filterable_docs() fixture to a test class.
"""
@pytest.fixture
def filterable_docs(self) -> list[Document]:
"""Fixture that returns a list of Documents that can be used to test filtering."""
return create_filterable_docs()
class FilterDocumentsTest(AssertDocumentsEqualMixin, FilterableDocsFixtureMixin):
"""
Utility class to test a Document Store `filter_documents` method using different types of filters.
To use it create a custom test class and override the `document_store` fixture to return your Document Store.
Example usage:
```python
class MyDocumentStoreTest(FilterDocumentsTest):
@pytest.fixture
def document_store(self):
return MyDocumentStore()
```
"""
def test_no_filters(self, document_store):
"""Test filter_documents() with empty filters"""
self.assert_documents_are_equal(document_store.filter_documents(), [])
self.assert_documents_are_equal(document_store.filter_documents(filters={}), [])
docs = [Document(content="test doc")]
document_store.write_documents(docs)
self.assert_documents_are_equal(document_store.filter_documents(), docs)
self.assert_documents_are_equal(document_store.filter_documents(filters={}), docs)
# == comparator
def test_comparison_equal(self, document_store, filterable_docs):
"""Test filter_documents() with == comparator"""
document_store.write_documents(filterable_docs)
result = document_store.filter_documents(filters={"field": "meta.number", "operator": "==", "value": 100})
self.assert_documents_are_equal(result, [d for d in filterable_docs if d.meta.get("number") == 100])
def test_comparison_equal_with_none(self, document_store, filterable_docs):
"""Test filter_documents() with == comparator and None"""
document_store.write_documents(filterable_docs)
result = document_store.filter_documents(filters={"field": "meta.number", "operator": "==", "value": None})
self.assert_documents_are_equal(result, [d for d in filterable_docs if d.meta.get("number") is None])
# != comparator
def test_comparison_not_equal(self, document_store, filterable_docs):
"""Test filter_documents() with != comparator"""
document_store.write_documents(filterable_docs)
result = document_store.filter_documents({"field": "meta.number", "operator": "!=", "value": 100})
self.assert_documents_are_equal(result, [d for d in filterable_docs if d.meta.get("number") != 100])
def test_comparison_not_equal_with_none(self, document_store, filterable_docs):
"""Test filter_documents() with != comparator and None"""
document_store.write_documents(filterable_docs)
result = document_store.filter_documents(filters={"field": "meta.number", "operator": "!=", "value": None})
self.assert_documents_are_equal(result, [d for d in filterable_docs if d.meta.get("number") is not None])
# > comparator
def test_comparison_greater_than(self, document_store, filterable_docs):
"""Test filter_documents() with > comparator"""
document_store.write_documents(filterable_docs)
result = document_store.filter_documents({"field": "meta.number", "operator": ">", "value": 0})
self.assert_documents_are_equal(
result, [d for d in filterable_docs if d.meta.get("number") is not None and d.meta["number"] > 0]
)
def test_comparison_greater_than_with_iso_date(self, document_store, filterable_docs):
"""Test filter_documents() with > comparator and datetime"""
document_store.write_documents(filterable_docs)
result = document_store.filter_documents(
{"field": "meta.date", "operator": ">", "value": "1972-12-11T19:54:58"}
)
self.assert_documents_are_equal(
result,
[
d
for d in filterable_docs
if d.meta.get("date") is not None
and datetime.fromisoformat(d.meta["date"]) > datetime.fromisoformat("1972-12-11T19:54:58")
],
)
@staticmethod
def test_comparison_greater_than_with_string(document_store, filterable_docs):
"""Test filter_documents() with > comparator and string"""
document_store.write_documents(filterable_docs)
with pytest.raises(FilterError):
document_store.filter_documents(filters={"field": "meta.number", "operator": ">", "value": "1"})
@staticmethod
def test_comparison_greater_than_with_list(document_store, filterable_docs):
"""Test filter_documents() with > comparator and list"""
document_store.write_documents(filterable_docs)
with pytest.raises(FilterError):
document_store.filter_documents(filters={"field": "meta.number", "operator": ">", "value": [1]})
def test_comparison_greater_than_with_none(self, document_store, filterable_docs):
"""Test filter_documents() with > comparator and None"""
document_store.write_documents(filterable_docs)
result = document_store.filter_documents(filters={"field": "meta.number", "operator": ">", "value": None})
self.assert_documents_are_equal(result, [])
# >= comparator
def test_comparison_greater_than_equal(self, document_store, filterable_docs):
"""Test filter_documents() with >= comparator"""
document_store.write_documents(filterable_docs)
result = document_store.filter_documents({"field": "meta.number", "operator": ">=", "value": 0})
self.assert_documents_are_equal(
result, [d for d in filterable_docs if d.meta.get("number") is not None and d.meta["number"] >= 0]
)
def test_comparison_greater_than_equal_with_iso_date(self, document_store, filterable_docs):
"""Test filter_documents() with >= comparator and datetime"""
document_store.write_documents(filterable_docs)
result = document_store.filter_documents(
{"field": "meta.date", "operator": ">=", "value": "1969-07-21T20:17:40"}
)
self.assert_documents_are_equal(
result,
[
d
for d in filterable_docs
if d.meta.get("date") is not None
and datetime.fromisoformat(d.meta["date"]) >= datetime.fromisoformat("1969-07-21T20:17:40")
],
)
@staticmethod
def test_comparison_greater_than_equal_with_string(document_store, filterable_docs):
"""Test filter_documents() with >= comparator and string"""
document_store.write_documents(filterable_docs)
with pytest.raises(FilterError):
document_store.filter_documents(filters={"field": "meta.number", "operator": ">=", "value": "1"})
@staticmethod
def test_comparison_greater_than_equal_with_list(document_store, filterable_docs):
"""Test filter_documents() with >= comparator and list"""
document_store.write_documents(filterable_docs)
with pytest.raises(FilterError):
document_store.filter_documents(filters={"field": "meta.number", "operator": ">=", "value": [1]})
def test_comparison_greater_than_equal_with_none(self, document_store, filterable_docs):
"""Test filter_documents() with >= comparator and None"""
document_store.write_documents(filterable_docs)
result = document_store.filter_documents(filters={"field": "meta.number", "operator": ">=", "value": None})
self.assert_documents_are_equal(result, [])
# < comparator
def test_comparison_less_than(self, document_store, filterable_docs):
"""Test filter_documents() with < comparator"""
document_store.write_documents(filterable_docs)
result = document_store.filter_documents({"field": "meta.number", "operator": "<", "value": 0})
self.assert_documents_are_equal(
result, [d for d in filterable_docs if d.meta.get("number") is not None and d.meta["number"] < 0]
)
def test_comparison_less_than_with_iso_date(self, document_store, filterable_docs):
"""Test filter_documents() with < comparator and datetime"""
document_store.write_documents(filterable_docs)
result = document_store.filter_documents(
{"field": "meta.date", "operator": "<", "value": "1969-07-21T20:17:40"}
)
self.assert_documents_are_equal(
result,
[
d
for d in filterable_docs
if d.meta.get("date") is not None
and datetime.fromisoformat(d.meta["date"]) < datetime.fromisoformat("1969-07-21T20:17:40")
],
)
@staticmethod
def test_comparison_less_than_with_string(document_store, filterable_docs):
"""Test filter_documents() with < comparator and string"""
document_store.write_documents(filterable_docs)
with pytest.raises(FilterError):
document_store.filter_documents(filters={"field": "meta.number", "operator": "<", "value": "1"})
@staticmethod
def test_comparison_less_than_with_list(document_store, filterable_docs):
"""Test filter_documents() with < comparator and list"""
document_store.write_documents(filterable_docs)
with pytest.raises(FilterError):
document_store.filter_documents(filters={"field": "meta.number", "operator": "<", "value": [1]})
def test_comparison_less_than_with_none(self, document_store, filterable_docs):
"""Test filter_documents() with < comparator and None"""
document_store.write_documents(filterable_docs)
result = document_store.filter_documents(filters={"field": "meta.number", "operator": "<", "value": None})
self.assert_documents_are_equal(result, [])
# <= comparator
def test_comparison_less_than_equal(self, document_store, filterable_docs):
"""Test filter_documents() with <="""
document_store.write_documents(filterable_docs)
result = document_store.filter_documents({"field": "meta.number", "operator": "<=", "value": 0})
self.assert_documents_are_equal(
result, [d for d in filterable_docs if d.meta.get("number") is not None and d.meta["number"] <= 0]
)
def test_comparison_less_than_equal_with_iso_date(self, document_store, filterable_docs):
"""Test filter_documents() with <= comparator and datetime"""
document_store.write_documents(filterable_docs)
result = document_store.filter_documents(
{"field": "meta.date", "operator": "<=", "value": "1969-07-21T20:17:40"}
)
self.assert_documents_are_equal(
result,
[
d
for d in filterable_docs
if d.meta.get("date") is not None
and datetime.fromisoformat(d.meta["date"]) <= datetime.fromisoformat("1969-07-21T20:17:40")
],
)
@staticmethod
def test_comparison_less_than_equal_with_string(document_store, filterable_docs):
"""Test filter_documents() with <= comparator and string"""
document_store.write_documents(filterable_docs)
with pytest.raises(FilterError):
document_store.filter_documents(filters={"field": "meta.number", "operator": "<=", "value": "1"})
@staticmethod
def test_comparison_less_than_equal_with_list(document_store, filterable_docs):
"""Test filter_documents() with <= comparator and list"""
document_store.write_documents(filterable_docs)
with pytest.raises(FilterError):
document_store.filter_documents(filters={"field": "meta.number", "operator": "<=", "value": [1]})
def test_comparison_less_than_equal_with_none(self, document_store, filterable_docs):
"""Test filter_documents() with <= comparator and None"""
document_store.write_documents(filterable_docs)
result = document_store.filter_documents(filters={"field": "meta.number", "operator": "<=", "value": None})
self.assert_documents_are_equal(result, [])
# in comparator
def test_comparison_in(self, document_store, filterable_docs):
"""Test filter_documents() with 'in' comparator"""
document_store.write_documents(filterable_docs)
result = document_store.filter_documents({"field": "meta.number", "operator": "in", "value": [10, -10]})
assert len(result)
expected = [d for d in filterable_docs if d.meta.get("number") is not None and d.meta["number"] in [10, -10]]
self.assert_documents_are_equal(result, expected)
@staticmethod
def test_comparison_in_with_with_non_list(document_store, filterable_docs):
"""Test filter_documents() with 'in' comparator and non-iterable"""
document_store.write_documents(filterable_docs)
with pytest.raises(FilterError):
document_store.filter_documents({"field": "meta.number", "operator": "in", "value": 9})
@staticmethod
def test_comparison_in_with_with_non_list_iterable(document_store, filterable_docs):
"""Test filter_documents() with 'in' comparator and iterable"""
document_store.write_documents(filterable_docs)
with pytest.raises(FilterError):
document_store.filter_documents({"field": "meta.number", "operator": "in", "value": (10, 11)})
# not in comparator
def test_comparison_not_in(self, document_store, filterable_docs):
"""Test filter_documents() with 'not in' comparator"""
document_store.write_documents(filterable_docs)
result = document_store.filter_documents({"field": "meta.number", "operator": "not in", "value": [9, 10]})
self.assert_documents_are_equal(result, [d for d in filterable_docs if d.meta.get("number") not in [9, 10]])
@staticmethod
def test_comparison_not_in_with_with_non_list(document_store, filterable_docs):
"""Test filter_documents() with 'not in' comparator and non-iterable"""
document_store.write_documents(filterable_docs)
with pytest.raises(FilterError):
document_store.filter_documents({"field": "meta.number", "operator": "not in", "value": 9})
@staticmethod
def test_comparison_not_in_with_with_non_list_iterable(document_store, filterable_docs):
"""Test filter_documents() with 'not in' comparator and iterable"""
document_store.write_documents(filterable_docs)
with pytest.raises(FilterError):
document_store.filter_documents({"field": "meta.number", "operator": "not in", "value": (10, 11)})
# Logical operator
def test_and_operator(self, document_store, filterable_docs):
"""Test filter_documents() with 'AND' operator"""
document_store.write_documents(filterable_docs)
result = document_store.filter_documents(
filters={
"operator": "AND",
"conditions": [
{"field": "meta.number", "operator": "==", "value": 100},
{"field": "meta.name", "operator": "==", "value": "name_0"},
],
}
)
self.assert_documents_are_equal(
result, [d for d in filterable_docs if d.meta.get("number") == 100 and d.meta.get("name") == "name_0"]
)
def test_or_operator(self, document_store, filterable_docs):
"""Test filter_documents() with 'OR' operator"""
document_store.write_documents(filterable_docs)
result = document_store.filter_documents(
filters={
"operator": "OR",
"conditions": [
{"field": "meta.number", "operator": "==", "value": 100},
{"field": "meta.name", "operator": "==", "value": "name_0"},
],
}
)
self.assert_documents_are_equal(
result, [d for d in filterable_docs if d.meta.get("number") == 100 or d.meta.get("name") == "name_0"]
)
def test_not_operator(self, document_store, filterable_docs):
"""Test filter_documents() with 'NOT' operator"""
document_store.write_documents(filterable_docs)
result = document_store.filter_documents(
filters={
"operator": "NOT",
"conditions": [
{"field": "meta.number", "operator": "==", "value": 100},
{"field": "meta.name", "operator": "==", "value": "name_0"},
],
}
)
self.assert_documents_are_equal(
result, [d for d in filterable_docs if not (d.meta.get("number") == 100 and d.meta.get("name") == "name_0")]
)
# Malformed filters
def test_missing_top_level_operator_key(self, document_store, filterable_docs):
"""Test filter_documents() with top-level operator"""
document_store.write_documents(filterable_docs)
with pytest.raises(FilterError):
document_store.filter_documents(
filters={"conditions": [{"field": "meta.name", "operator": "==", "value": "test"}]}
)
def test_missing_top_level_conditions_key(self, document_store, filterable_docs):
"""Test filter_documents() with missing top-level condition key"""
document_store.write_documents(filterable_docs)
with pytest.raises(FilterError):
document_store.filter_documents(filters={"operator": "AND"})
def test_missing_condition_field_key(self, document_store, filterable_docs):
"""Test filter_documents() with missing condition key"""
document_store.write_documents(filterable_docs)
with pytest.raises(FilterError):
document_store.filter_documents(
filters={"operator": "AND", "conditions": [{"operator": "==", "value": "test"}]}
)
def test_missing_condition_operator_key(self, document_store, filterable_docs):
"""Test filter_documents() with missing operator key"""
document_store.write_documents(filterable_docs)
with pytest.raises(FilterError):
document_store.filter_documents(
filters={"operator": "AND", "conditions": [{"field": "meta.name", "value": "test"}]}
)
def test_missing_condition_value_key(self, document_store, filterable_docs):
"""Test filter_documents() with missing condition value"""
document_store.write_documents(filterable_docs)
with pytest.raises(FilterError):
document_store.filter_documents(
filters={"operator": "AND", "conditions": [{"field": "meta.name", "operator": "=="}]}
)
class DeleteAllTest:
"""
Tests for Document Store delete_all_documents().
To use it create a custom test class and override the `document_store` fixture.
Only mix in for stores that implement delete_all_documents.
"""
@staticmethod
def test_delete_all_documents(document_store: DocumentStore):
"""
Test delete_all_documents() normal behaviour.
This test verifies that delete_all_documents() removes all documents from the store
and that the store remains functional after deletion.
"""
docs = [Document(content="first doc", id="1"), Document(content="second doc", id="2")]
document_store.write_documents(docs)
assert document_store.count_documents() == 2
document_store.delete_all_documents() # type:ignore[attr-defined]
assert document_store.count_documents() == 0
new_doc = Document(content="new doc after delete all", id="3")
document_store.write_documents([new_doc])
assert document_store.count_documents() == 1
@staticmethod
def test_delete_all_documents_empty_store(document_store: DocumentStore):
"""
Test delete_all_documents() on an empty store.
This should not raise an error and should leave the store empty.
"""
assert document_store.count_documents() == 0
document_store.delete_all_documents() # type:ignore[attr-defined]
assert document_store.count_documents() == 0
@staticmethod
def _delete_all_supports_recreate(document_store: DocumentStore) -> str | None:
"""
Return the recreate parameter name if delete_all_documents supports it, else None.
"""
sig = inspect.signature(document_store.delete_all_documents) # type:ignore[attr-defined]
if "recreate_index" in sig.parameters:
return "recreate_index"
if "recreate_collection" in sig.parameters:
return "recreate_collection"
return None
@staticmethod
def test_delete_all_documents_without_recreate_index(document_store: DocumentStore):
"""
Test delete_all_documents() with recreate_index/recreate_collection=False when supported.
Skipped if the store's delete_all_documents does not have recreate_index or recreate_collection.
"""
param_name = DeleteAllTest._delete_all_supports_recreate(document_store)
if param_name is None:
pytest.skip("delete_all_documents has no recreate_index or recreate_collection parameter")
docs = [Document(id="1", content="A first document"), Document(id="2", content="Second document")]
document_store.write_documents(docs)
assert document_store.count_documents() == 2
document_store.delete_all_documents(**{param_name: False}) # type:ignore[attr-defined]
assert document_store.count_documents() == 0
new_doc = Document(id="3", content="New document after delete all")
document_store.write_documents([new_doc])
assert document_store.count_documents() == 1
@staticmethod
def test_delete_all_documents_with_recreate_index(document_store: DocumentStore):
"""
Test delete_all_documents() with recreate_index/recreate_collection=True when supported.
Skipped if the store's delete_all_documents does not have recreate_index or recreate_collection.
"""
param_name = DeleteAllTest._delete_all_supports_recreate(document_store)
if param_name is None:
pytest.skip("delete_all_documents has no recreate_index or recreate_collection parameter")
docs = [Document(id="1", content="A first document"), Document(id="2", content="Second document")]
document_store.write_documents(docs)
assert document_store.count_documents() == 2
document_store.delete_all_documents(**{param_name: True}) # type:ignore[attr-defined]
assert document_store.count_documents() == 0
new_doc = Document(id="3", content="New document after delete all with recreate")
document_store.write_documents([new_doc])
assert document_store.count_documents() == 1
retrieved = document_store.filter_documents()
assert len(retrieved) == 1
assert retrieved[0].content == "New document after delete all with recreate"
class DeleteByFilterTest:
"""
Tests for Document Store delete_by_filter().
"""
@staticmethod
def test_delete_by_filter(document_store: DocumentStore):
"""Delete documents matching a filter and verify count and remaining docs."""
docs = [
Document(content="Doc 1", meta={"category": "Alpha"}),
Document(content="Doc 2", meta={"category": "Beta"}),
Document(content="Doc 3", meta={"category": "Alpha"}),
]
document_store.write_documents(docs)
assert document_store.count_documents() == 3
# `delete_by_filter` is not part of the DocumentStore protocol
sig = inspect.signature(document_store.delete_by_filter) # type:ignore[attr-defined]
params = {"refresh": True} if "refresh" in sig.parameters else {}
deleted_count = document_store.delete_by_filter( # type:ignore[attr-defined]
filters={"field": "meta.category", "operator": "==", "value": "Alpha"}, **params
)
assert deleted_count == 2
assert document_store.count_documents() == 1
remaining_docs = document_store.filter_documents()
assert len(remaining_docs) == 1
assert remaining_docs[0].meta["category"] == "Beta"
@staticmethod
def test_delete_by_filter_no_matches(document_store: DocumentStore):
"""Delete with a filter that matches no documents returns 0 and leaves store unchanged."""
docs = [
Document(content="Doc 1", meta={"category": "Alpha"}),
Document(content="Doc 2", meta={"category": "Beta"}),
]
document_store.write_documents(docs)
assert document_store.count_documents() == 2
deleted_count = document_store.delete_by_filter( # type:ignore[attr-defined]
filters={"field": "meta.category", "operator": "==", "value": "Gamma"}
)
assert deleted_count == 0
assert document_store.count_documents() == 2
@staticmethod
def test_delete_by_filter_advanced_filters(document_store: DocumentStore):
"""Delete with AND/OR filter combinations and verify remaining documents."""
docs = [
Document(content="Doc 1", meta={"category": "Alpha", "year": 2023, "status": "draft"}),
Document(content="Doc 2", meta={"category": "Alpha", "year": 2024, "status": "published"}),
Document(content="Doc 3", meta={"category": "Beta", "year": 2023, "status": "draft"}),
]
document_store.write_documents(docs)
assert document_store.count_documents() == 3
# `delete_by_filter` is not part of the DocumentStore protocol
sig = inspect.signature(document_store.delete_by_filter) # type:ignore[attr-defined]
params = {"refresh": True} if "refresh" in sig.parameters else {}
deleted_count = document_store.delete_by_filter( # type:ignore[attr-defined]
filters={
"operator": "AND",
"conditions": [
{"field": "meta.category", "operator": "==", "value": "Alpha"},
{"field": "meta.year", "operator": "==", "value": 2023},
],
},
**params,
)
assert deleted_count == 1
assert document_store.count_documents() == 2
deleted_count = document_store.delete_by_filter( # type:ignore[attr-defined]
filters={
"operator": "OR",
"conditions": [
{"field": "meta.category", "operator": "==", "value": "Beta"},
{"field": "meta.status", "operator": "==", "value": "published"},
],
},
**params,
)
assert deleted_count == 2
assert document_store.count_documents() == 0
class UpdateByFilterTest:
"""
Tests for Document Store update_by_filter().
"""
@staticmethod
def test_update_by_filter(document_store: DocumentStore, filterable_docs: list[Document]):
"""Update documents matching a filter and verify count and meta changes."""
document_store.write_documents(filterable_docs)
expected_count = len([d for d in filterable_docs if d.meta.get("chapter") == "intro"])
assert document_store.count_documents() == len(filterable_docs)
# `update_by_filter` is not part of the DocumentStore protocol
sig = inspect.signature(document_store.update_by_filter) # type:ignore[attr-defined]
params = {"refresh": True} if "refresh" in sig.parameters else {}
updated_count = document_store.update_by_filter( # type:ignore[attr-defined]
filters={"field": "meta.chapter", "operator": "==", "value": "intro"}, meta={"updated": True}, **params
)
assert updated_count == expected_count
updated_docs = document_store.filter_documents(
filters={"field": "meta.updated", "operator": "==", "value": True}
)
assert len(updated_docs) == expected_count
for doc in updated_docs:
assert doc.meta["chapter"] == "intro"
assert doc.meta["updated"] is True
not_updated_docs = document_store.filter_documents(
filters={"field": "meta.chapter", "operator": "==", "value": "abstract"}
)
for doc in not_updated_docs:
assert doc.meta.get("updated") is not True
@staticmethod
def test_update_by_filter_no_matches(document_store: DocumentStore, filterable_docs: list[Document]):
"""Update with a filter that matches no documents returns 0 and leaves store unchanged."""
document_store.write_documents(filterable_docs)
initial_count = len(filterable_docs)
assert document_store.count_documents() == initial_count
updated_count = document_store.update_by_filter( # type:ignore[attr-defined]
filters={"field": "meta.chapter", "operator": "==", "value": "nonexistent_chapter"}, meta={"updated": True}
)
assert updated_count == 0
assert document_store.count_documents() == initial_count
@staticmethod
def test_update_by_filter_multiple_fields(document_store: DocumentStore, filterable_docs: list[Document]):
"""Update matching documents with multiple meta fields and verify all are set."""
document_store.write_documents(filterable_docs)
expected_count = len([d for d in filterable_docs if d.meta.get("chapter") == "intro"])
assert document_store.count_documents() == len(filterable_docs)
# `update_by_filter` is not part of the DocumentStore protocol
sig = inspect.signature(document_store.update_by_filter) # type:ignore[attr-defined]
params = {"refresh": True} if "refresh" in sig.parameters else {}
updated_count = document_store.update_by_filter( # type:ignore[attr-defined]
filters={"field": "meta.chapter", "operator": "==", "value": "intro"},
meta={"updated": True, "extra_field": "set"},
**params,
)
assert updated_count == expected_count
updated_docs = document_store.filter_documents(
filters={"field": "meta.extra_field", "operator": "==", "value": "set"}
)
assert len(updated_docs) == expected_count
for doc in updated_docs:
assert doc.meta["updated"] is True
assert doc.meta["extra_field"] == "set"
assert doc.meta["chapter"] == "intro"
assert doc.meta.get("number") == 2
not_updated_docs = document_store.filter_documents(
filters={"field": "meta.chapter", "operator": "==", "value": "abstract"}
)
for doc in not_updated_docs:
assert doc.meta.get("extra_field") != "set"
@staticmethod
def test_update_by_filter_advanced_filters(document_store: DocumentStore):
"""Update with AND/OR filter combinations and verify updated documents."""
docs = [
Document(content="Doc 1", meta={"category": "Alpha", "year": 2023, "status": "draft"}),
Document(content="Doc 2", meta={"category": "Alpha", "year": 2024, "status": "draft"}),
Document(content="Doc 3", meta={"category": "Beta", "year": 2023, "status": "draft"}),
]
document_store.write_documents(docs)
assert document_store.count_documents() == 3
# `update_by_filter` is not part of the DocumentStore protocol
sig = inspect.signature(document_store.update_by_filter) # type:ignore[attr-defined]
params = {"refresh": True} if "refresh" in sig.parameters else {}
updated_count = document_store.update_by_filter( # type:ignore[attr-defined]
filters={
"operator": "AND",
"conditions": [
{"field": "meta.category", "operator": "==", "value": "Alpha"},
{"field": "meta.year", "operator": "==", "value": 2023},
],
},
meta={"status": "published"},
**params,
)
assert updated_count == 1
published_docs = document_store.filter_documents(
filters={"field": "meta.status", "operator": "==", "value": "published"}
)
assert len(published_docs) == 1
assert published_docs[0].meta["category"] == "Alpha"
assert published_docs[0].meta["year"] == 2023
updated_count = document_store.update_by_filter( # type:ignore[attr-defined]
filters={
"operator": "OR",
"conditions": [
{"field": "meta.category", "operator": "==", "value": "Beta"},
{"field": "meta.year", "operator": "==", "value": 2024},
],
},
meta={"featured": True},
**params,
)
assert updated_count == 2
featured_docs = document_store.filter_documents(
filters={"field": "meta.featured", "operator": "==", "value": True}
)
assert len(featured_docs) == 2
class CountDocumentsByFilterTest:
"""
Tests for Document Store count_documents_by_filter().
Only mix in for stores that implement count_documents_by_filter.
"""
@staticmethod
def test_count_documents_by_filter_simple(document_store: DocumentStore):
"""Test count_documents_by_filter() with a simple equality filter."""
docs = [
Document(content="Doc 1", meta={"category": "A", "status": "active"}),
Document(content="Doc 2", meta={"category": "B", "status": "active"}),
Document(content="Doc 3", meta={"category": "A", "status": "inactive"}),
Document(content="Doc 4", meta={"category": "A", "status": "active"}),
]
document_store.write_documents(docs)
assert document_store.count_documents() == 4
count = document_store.count_documents_by_filter( # type:ignore[attr-defined]
filters={"field": "meta.category", "operator": "==", "value": "A"}
)
assert count == 3
count = document_store.count_documents_by_filter( # type:ignore[attr-defined]
filters={"field": "meta.category", "operator": "==", "value": "B"}
)
assert count == 1
@staticmethod
def test_count_documents_by_filter_compound(document_store: DocumentStore):
"""Test count_documents_by_filter() with AND filter."""
docs = [
Document(content="Doc 1", meta={"category": "A", "status": "active"}),
Document(content="Doc 2", meta={"category": "B", "status": "active"}),
Document(content="Doc 3", meta={"category": "A", "status": "inactive"}),
Document(content="Doc 4", meta={"category": "A", "status": "active"}),
]
document_store.write_documents(docs)
assert document_store.count_documents() == 4
count = document_store.count_documents_by_filter( # type:ignore[attr-defined]
filters={
"operator": "AND",
"conditions": [
{"field": "meta.category", "operator": "==", "value": "A"},
{"field": "meta.status", "operator": "==", "value": "active"},
],
}
)
assert count == 2
@staticmethod
def test_count_documents_by_filter_no_matches(document_store: DocumentStore):
"""Test count_documents_by_filter() when filter matches no documents."""
docs = [Document(content="Doc 1", meta={"category": "A"}), Document(content="Doc 2", meta={"category": "B"})]
document_store.write_documents(docs)
assert document_store.count_documents() == 2
count = document_store.count_documents_by_filter( # type:ignore[attr-defined]
filters={"field": "meta.category", "operator": "==", "value": "Z"}
)
assert count == 0
@staticmethod
def test_count_documents_by_filter_empty_collection(document_store: DocumentStore):
"""Test count_documents_by_filter() on an empty store."""
assert document_store.count_documents() == 0
count = document_store.count_documents_by_filter( # type:ignore[attr-defined]
filters={"field": "meta.category", "operator": "==", "value": "A"}
)
assert count == 0
class CountUniqueMetadataByFilterTest:
"""
Tests for Document Store count_unique_metadata_by_filter().
Only mix in for stores that implement count_unique_metadata_by_filter.
"""
@staticmethod
def test_count_unique_metadata_by_filter_all_documents(document_store: DocumentStore):
"""Test count_unique_metadata_by_filter() with no filter returns distinct counts for all docs."""
docs = [
Document(content="Doc 1", meta={"category": "A", "status": "active", "priority": 1}),
Document(content="Doc 2", meta={"category": "B", "status": "active", "priority": 2}),
Document(content="Doc 3", meta={"category": "A", "status": "inactive", "priority": 1}),
Document(content="Doc 4", meta={"category": "A", "status": "active", "priority": 3}),
Document(content="Doc 5", meta={"category": "C", "status": "active", "priority": 2}),
]
document_store.write_documents(docs)
assert document_store.count_documents() == 5
counts = document_store.count_unique_metadata_by_filter( # type:ignore[attr-defined]
filters={}, metadata_fields=["category", "status", "priority"]
)
assert counts["category"] == 3
assert counts["status"] == 2
assert counts["priority"] == 3
@staticmethod
def test_count_unique_metadata_by_filter_with_filter(document_store: DocumentStore):
"""Test count_unique_metadata_by_filter() with a filter."""
docs = [
Document(content="Doc 1", meta={"category": "A", "status": "active", "priority": 1}),
Document(content="Doc 2", meta={"category": "B", "status": "active", "priority": 2}),
Document(content="Doc 3", meta={"category": "A", "status": "inactive", "priority": 1}),
Document(content="Doc 4", meta={"category": "A", "status": "active", "priority": 3}),
]
document_store.write_documents(docs)
assert document_store.count_documents() == 4
counts = document_store.count_unique_metadata_by_filter( # type:ignore[attr-defined]
filters={"field": "meta.category", "operator": "==", "value": "A"}, metadata_fields=["status", "priority"]
)
assert counts["status"] == 2
assert counts["priority"] == 2
@staticmethod
def test_count_unique_metadata_by_filter_with_multiple_filters(document_store: DocumentStore):
"""Test counting with multiple filters"""
docs = [
Document(content="Doc 1", meta={"category": "A", "year": 2023}),
Document(content="Doc 2", meta={"category": "A", "year": 2024}),
Document(content="Doc 3", meta={"category": "B", "year": 2023}),
Document(content="Doc 4", meta={"category": "B", "year": 2024}),
]
document_store.write_documents(docs)
count = document_store.count_documents_by_filter( # type:ignore[attr-defined]
filters={
"operator": "AND",
"conditions": [
{"field": "meta.category", "operator": "==", "value": "B"},
{"field": "meta.year", "operator": "==", "value": 2023},
],
}
)
assert count == 1
class GetMetadataFieldsInfoTest:
"""
Tests for Document Store get_metadata_fields_info().
Only mix in for stores that implement get_metadata_fields_info.
"""
@staticmethod
def test_get_metadata_fields_info(document_store: DocumentStore):
"""Test get_metadata_fields_info() returns field names and types after writing documents."""
docs = [
Document(content="Doc 1", meta={"category": "A", "status": "active", "priority": 1}),
Document(content="Doc 2", meta={"category": "B", "status": "inactive", "rating": 0.5}),
]
document_store.write_documents(docs)
assert document_store.count_documents() == 2
fields_info = document_store.get_metadata_fields_info() # type:ignore[attr-defined]
assert "category" in fields_info
assert "status" in fields_info
assert "priority" in fields_info
assert "rating" in fields_info
for field_name, info in fields_info.items(): # noqa: B007, PERF102
assert isinstance(info, dict)
assert "type" in info
@staticmethod
def test_get_metadata_fields_info_empty_collection(document_store: DocumentStore):
"""Test get_metadata_fields_info() on an empty store."""
assert document_store.count_documents() == 0
fields_info = document_store.get_metadata_fields_info() # type:ignore[attr-defined]
assert fields_info == {}
class GetMetadataFieldMinMaxTest:
"""
Tests for Document Store get_metadata_field_min_max().
Only mix in for stores that implement get_metadata_field_min_max.
"""
@staticmethod
def test_get_metadata_field_min_max_numeric(document_store: DocumentStore):
"""Test get_metadata_field_min_max() with integer field."""
docs = [
Document(content="Doc 1", meta={"priority": 1}),
Document(content="Doc 2", meta={"priority": 5}),
Document(content="Doc 3", meta={"priority": 3}),
Document(content="Doc 4", meta={"priority": 10}),
]
document_store.write_documents(docs)
assert document_store.count_documents() == 4
result = document_store.get_metadata_field_min_max("priority") # type:ignore[attr-defined]
assert result["min"] == 1
assert result["max"] == 10
@staticmethod
def test_get_metadata_field_min_max_float(document_store: DocumentStore):
"""Test get_metadata_field_min_max() with float field."""
docs = [
Document(content="Doc 1", meta={"rating": 0.6}),
Document(content="Doc 2", meta={"rating": 0.95}),
Document(content="Doc 3", meta={"rating": 0.8}),
]
document_store.write_documents(docs)
assert document_store.count_documents() == 3
result = document_store.get_metadata_field_min_max("rating") # type:ignore[attr-defined]
assert result["min"] == pytest.approx(0.6)
assert result["max"] == pytest.approx(0.95)
@staticmethod
def test_get_metadata_field_min_max_single_value(document_store: DocumentStore):
"""Test get_metadata_field_min_max() when field has only one value."""
docs = [Document(content="Doc 1", meta={"priority": 42})]
document_store.write_documents(docs)
assert document_store.count_documents() == 1
result = document_store.get_metadata_field_min_max("priority") # type:ignore[attr-defined]
assert result["min"] == 42
assert result["max"] == 42
@staticmethod
def test_get_metadata_field_min_max_empty_collection(document_store: DocumentStore):
"""Test get_metadata_field_min_max() on an empty store."""
assert document_store.count_documents() == 0
result = document_store.get_metadata_field_min_max("priority") # type:ignore[attr-defined]
assert result["min"] is None
assert result["max"] is None
@staticmethod
def test_get_metadata_field_min_max_meta_prefix(document_store: DocumentStore):
"""Test get_metadata_field_min_max() with field names that include 'meta.' prefix."""
docs = [
Document(content="Doc 1", meta={"priority": 1, "age": 10}),
Document(content="Doc 2", meta={"priority": 5, "age": 20}),
Document(content="Doc 3", meta={"priority": 3, "age": 15}),
Document(content="Doc 4", meta={"priority": 10, "age": 5}),
Document(content="Doc 6", meta={"rating": 10.5}),
Document(content="Doc 7", meta={"rating": 20.3}),
Document(content="Doc 8", meta={"rating": 15.7}),
Document(content="Doc 9", meta={"rating": 5.2}),
]
document_store.write_documents(docs)
min_max_priority = document_store.get_metadata_field_min_max("meta.priority") # type:ignore[attr-defined]
assert min_max_priority["min"] == 1
assert min_max_priority["max"] == 10
# Test with float values and "meta." prefix
min_max_score = document_store.get_metadata_field_min_max("meta.rating") # type:ignore[attr-defined]
assert min_max_score["min"] == pytest.approx(5.2)
assert min_max_score["max"] == pytest.approx(20.3)
class GetMetadataFieldUniqueValuesTest:
"""
Tests for Document Store get_metadata_field_unique_values().
Only mix in for stores that implement get_metadata_field_unique_values.
Expects the method to return (values_list, total_count) or (values_list, pagination_key).
"""
@staticmethod
def test_get_metadata_field_unique_values_basic(document_store: DocumentStore):
"""Test get_metadata_field_unique_values() returns unique values and total count."""
docs = [
Document(content="Doc 1", meta={"category": "A"}),
Document(content="Doc 2", meta={"category": "B"}),
Document(content="Doc 3", meta={"category": "A"}),
Document(content="Doc 4", meta={"category": "C"}),
Document(content="Doc 5", meta={"category": "B"}),
]
document_store.write_documents(docs)
assert document_store.count_documents() == 5
sig = inspect.signature(document_store.get_metadata_field_unique_values) # type:ignore[attr-defined]
params: dict = {}
if "search_term" in sig.parameters:
params["search_term"] = None
if "from_" in sig.parameters:
params["from_"] = 0
elif "offset" in sig.parameters:
params["offset"] = 0
if "size" in sig.parameters:
params["size"] = 10
elif "limit" in sig.parameters:
params["limit"] = 10
result = document_store.get_metadata_field_unique_values("category", **params) # type:ignore[attr-defined]
values = result[0] if isinstance(result, tuple) else result
assert isinstance(values, list)
assert set(values) == {"A", "B", "C"}
if isinstance(result, tuple) and len(result) >= 2 and isinstance(result[1], int):
assert result[1] == 3
class DocumentStoreBaseTests(CountDocumentsTest, DeleteDocumentsTest, FilterDocumentsTest, WriteDocumentsTest):
@pytest.fixture
def document_store(self) -> DocumentStore:
"""Base fixture, to be reimplemented when deriving from DocumentStoreBaseTests"""
raise NotImplementedError()
class DocumentStoreBaseExtendedTests(DocumentStoreBaseTests, DeleteAllTest, DeleteByFilterTest, UpdateByFilterTest):
"""
Extended tests for Document Stores.
Besides the base tests, it also tests for:
- delete_all_documents()
- delete_by_filter()
- update_by_filter()
"""
@pytest.fixture
def document_store(self) -> DocumentStore:
"""Base fixture, to be reimplemented when deriving from DocumentStoreBaseTests"""
raise NotImplementedError()