Repository: deepset-ai/haystack

Issue:
Deleting documents in Weaviate fails without filters
**Describe the bug**
If you have a Weaviate document store with more than 10,000 documents and try to delete a document by its ID using `weaviate_doc_store.delete_documents(ids=[my_doc_id])` without passing a `filters` parameter, the deletion fails with either a timeout or a pagination error. This happens because `delete_documents(ids=[...])` without `filters` first tries to retrieve _all_ documents, and then narrows down the list of documents to those with IDs matching the provided list.

**Error message 1**
Connection timeout

> ConnectionError: ('Connection aborted.', RemoteDisconnected('Remote end closed connection without response'))
The above exception was the direct cause of the following exception:
...
File project/src/farm-haystack/haystack/document_stores/weaviate.py:713, in WeaviateDocumentStore._get_all_documents_in_index(self, index, filters, batch_size, only_documents_without_embedding)
    711     result = query.do()
    712 except Exception as e:
--> 713     raise WeaviateDocumentStoreError(f"Weaviate raised an exception: {e}")
    715 if "errors" in result:
    716     raise WeaviateDocumentStoreError(f"Query results contain errors: {result['errors']}")


**Error message 2**
Pagination error
> WeaviateDocumentStoreError: Query results contain errors: [{'locations': [{'column': 6, 'line': 1}], 'message': 'explorer: list class: search: invalid pagination params: query maximum results exceeded', 'path': ['Get', 'MyIndexName']}]

**Expected behavior**
If we try to delete a document, or a list of documents, purely by their IDs and without further filters, then there's no need to retrieve anything from the document store first. The documents should simply be deleted.

**Additional context**
This bug is caused by the implementation of the `delete_documents()` function in haystack/document_stores/weaviate.py, [starting from line 1252](https://github.com/deepset-ai/haystack/blob/master/haystack/document_stores/weaviate.py#L1252). If there are no filters, then the code to `get_all_documents(index, filters=filters)` on line 1306 is wasteful. We can fix it by modifying the implementation to be more as follows:

```python
if not filters and not ids:
    # Recreate the index
    self._create_schema_and_index(index, recreate_index=True)
    return

# Create index if it doesn't exist yet
self._create_schema_and_index(index, recreate_index=False)

if ids and not filters:
    for id in ids:
        self.weaviate_client.data_object.delete(id)

else:
    # Use filters to restrict list of retrieved documents, before checking these against provided ids
    docs_to_delete = self.get_all_documents(index, filters=filters)
    if ids:
        docs_to_delete = [doc for doc in docs_to_delete if doc.id in ids]
    for doc in docs_to_delete:
        self.weaviate_client.data_object.delete(doc.id)
```



Discussion:

Resolving patch:
diff --git a/haystack/document_stores/weaviate.py b/haystack/document_stores/weaviate.py
--- a/haystack/document_stores/weaviate.py
+++ b/haystack/document_stores/weaviate.py
@@ -1299,10 +1299,19 @@ def delete_documents(
         index = self._sanitize_index_name(index) or self.index
 
         if not filters and not ids:
+            # Delete the existing index, then create an empty new one
             self._create_schema_and_index(index, recreate_index=True)
+            return
+
+        # Create index if it doesn't exist yet
+        self._create_schema_and_index(index, recreate_index=False)
+
+        if ids and not filters:
+            for id in ids:
+                self.weaviate_client.data_object.delete(id)
+
         else:
-            # create index if it doesn't exist yet
-            self._create_schema_and_index(index, recreate_index=False)
+            # Use filters to restrict list of retrieved documents, before checking these against provided ids
             docs_to_delete = self.get_all_documents(index, filters=filters)
             if ids:
                 docs_to_delete = [doc for doc in docs_to_delete if doc.id in ids]


Test patch:
diff --git a/test/document_stores/test_weaviate.py b/test/document_stores/test_weaviate.py
--- a/test/document_stores/test_weaviate.py
+++ b/test/document_stores/test_weaviate.py
@@ -1,4 +1,5 @@
 import uuid
+from unittest.mock import MagicMock
 
 import numpy as np
 import pytest
@@ -6,7 +7,6 @@
 from haystack.schema import Document
 from ..conftest import get_document_store
 
-
 embedding_dim = 768
 
 
@@ -123,3 +123,24 @@ def test_get_all_documents_unaffected_by_QUERY_MAXIMUM_RESULTS(document_store_wi
     monkeypatch.setattr(document_store_with_docs, "get_document_count", lambda **kwargs: 13_000)
     docs = document_store_with_docs.get_all_documents()
     assert len(docs) == 3
+
+
+@pytest.mark.weaviate
+@pytest.mark.parametrize("document_store_with_docs", ["weaviate"], indirect=True)
+def test_deleting_by_id_or_by_filters(document_store_with_docs):
+    # This test verifies that deleting an object by its ID does not first require fetching all documents. This fixes
+    # a bug, as described in https://github.com/deepset-ai/haystack/issues/2898
+    document_store_with_docs.get_all_documents = MagicMock(wraps=document_store_with_docs.get_all_documents)
+
+    assert document_store_with_docs.get_document_count() == 3
+
+    # Delete a document by its ID. This should bypass the get_all_documents() call
+    document_store_with_docs.delete_documents(ids=[DOCUMENTS_XS[0]["id"]])
+    document_store_with_docs.get_all_documents.assert_not_called()
+    assert document_store_with_docs.get_document_count() == 2
+
+    document_store_with_docs.get_all_documents.reset_mock()
+    # Delete a document with filters. Prove that using the filters will go through get_all_documents()
+    document_store_with_docs.delete_documents(filters={"name": ["filename2"]})
+    document_store_with_docs.get_all_documents.assert_called()
+    assert document_store_with_docs.get_document_count() == 1