Skip to content

Commit 090279b

Browse files
authored
feat: add closing methods to components that hold Document Stores (#12169)
1 parent c57007a commit 090279b

15 files changed

Lines changed: 209 additions & 3 deletions

haystack/components/caching/cache_checker.py

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -121,3 +121,17 @@ async def run_async(self, items: list[Any]) -> dict[str, Any]:
121121
else:
122122
misses.append(item)
123123
return {"hits": found_documents, "misses": misses}
124+
125+
def close(self) -> None:
126+
"""
127+
Release the synchronous resources of the underlying Document Store.
128+
"""
129+
if hasattr(self.document_store, "close"):
130+
self.document_store.close()
131+
132+
async def close_async(self) -> None:
133+
"""
134+
Release the asynchronous resources of the underlying Document Store.
135+
"""
136+
if hasattr(self.document_store, "close_async"):
137+
await self.document_store.close_async()

haystack/components/retrievers/auto_merging_retriever.py

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -224,3 +224,17 @@ async def _try_merge_level(docs_to_merge: list[Document], docs_to_return: list[D
224224
return await _try_merge_level(merged_docs, docs_to_return)
225225

226226
return {"documents": await _try_merge_level(documents, [])}
227+
228+
def close(self) -> None:
229+
"""
230+
Release the synchronous resources of the underlying Document Store.
231+
"""
232+
if hasattr(self.document_store, "close"):
233+
self.document_store.close()
234+
235+
async def close_async(self) -> None:
236+
"""
237+
Release the asynchronous resources of the underlying Document Store.
238+
"""
239+
if hasattr(self.document_store, "close_async"):
240+
await self.document_store.close_async()

haystack/components/retrievers/filter_retriever.py

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -102,3 +102,17 @@ async def run_async(self, filters: dict[str, Any] | None = None) -> dict[str, An
102102
# 'ignore' since filter_documents_async is not defined in the Protocol but exists in the implementations
103103
out_documents = await self.document_store.filter_documents_async(filters=filters or self.filters) # type: ignore[attr-defined]
104104
return {"documents": out_documents}
105+
106+
def close(self) -> None:
107+
"""
108+
Release the synchronous resources of the underlying Document Store.
109+
"""
110+
if hasattr(self.document_store, "close"):
111+
self.document_store.close()
112+
113+
async def close_async(self) -> None:
114+
"""
115+
Release the asynchronous resources of the underlying Document Store.
116+
"""
117+
if hasattr(self.document_store, "close_async"):
118+
await self.document_store.close_async()

haystack/components/retrievers/sentence_window_retriever.py

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -319,3 +319,17 @@ def _build_filter_conditions(self, split_id: int, window_size: int, source_ids:
319319
*source_id_filters,
320320
]
321321
return {"operator": "AND", "conditions": conditions}
322+
323+
def close(self) -> None:
324+
"""
325+
Release the synchronous resources of the underlying Document Store.
326+
"""
327+
if hasattr(self.document_store, "close"):
328+
self.document_store.close()
329+
330+
async def close_async(self) -> None:
331+
"""
332+
Release the asynchronous resources of the underlying Document Store.
333+
"""
334+
if hasattr(self.document_store, "close_async"):
335+
await self.document_store.close_async()

haystack/components/writers/document_writer.py

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -125,3 +125,17 @@ async def run_async(self, documents: list[Document], policy: DuplicatePolicy | N
125125

126126
documents_written = await self.document_store.write_documents_async(documents=documents, policy=policy)
127127
return {"documents_written": documents_written}
128+
129+
def close(self) -> None:
130+
"""
131+
Release the synchronous resources of the underlying Document Store.
132+
"""
133+
if hasattr(self.document_store, "close"):
134+
self.document_store.close()
135+
136+
async def close_async(self) -> None:
137+
"""
138+
Release the asynchronous resources of the underlying Document Store.
139+
"""
140+
if hasattr(self.document_store, "close_async"):
141+
await self.document_store.close_async()
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
---
2+
features:
3+
- |
4+
Haystack components that use a document store now provide ``close`` and ``close_async`` methods for releasing
5+
resources. These methods are available on: ``AutoMergingRetriever``, ``CacheChecker``, ``DocumentWriter``,
6+
``FilterRetriever``, and ``SentenceWindowRetriever``. If the underlying Document Store does not implement the
7+
corresponding method, calling ``close`` or ``close_async`` has no effect.

test/components/caching/test_url_cache_checker.py renamed to test/components/caching/test_cache_checker.py

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
#
33
# SPDX-License-Identifier: Apache-2.0
44

5-
from unittest.mock import MagicMock
5+
from unittest.mock import MagicMock, Mock
66

77
import pytest
88

@@ -92,3 +92,14 @@ def test_filters_syntax(self):
9292
checker.run(items=["https://example.com/1"])
9393
valid_filters_syntax = {"field": "url", "operator": "==", "value": "https://example.com/1"}
9494
mocked_docstore_class.filter_documents.assert_any_call(filters=valid_filters_syntax)
95+
96+
def test_close(self):
97+
closable_document_store = Mock(spec=["close"])
98+
checker = CacheChecker(document_store=closable_document_store, cache_field="url")
99+
checker.close()
100+
closable_document_store.close.assert_called_once_with()
101+
102+
nonclosable_document_store = Mock(spec=[])
103+
checker = CacheChecker(document_store=nonclosable_document_store, cache_field="url")
104+
checker.close()
105+
assert nonclosable_document_store.mock_calls == []

test/components/caching/test_cache_checker_async.py

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
#
33
# SPDX-License-Identifier: Apache-2.0
44

5-
from unittest.mock import AsyncMock, MagicMock
5+
from unittest.mock import AsyncMock, MagicMock, Mock
66

77
import pytest
88

@@ -60,3 +60,16 @@ async def test_run_async_filters_syntax(self):
6060
await checker.run_async(items=["https://example.com/1"])
6161
expected_filters = {"field": "url", "operator": "==", "value": "https://example.com/1"}
6262
mock_store.filter_documents_async.assert_awaited_once_with(filters=expected_filters)
63+
64+
@pytest.mark.asyncio
65+
async def test_close_async(self):
66+
closable_document_store = Mock(spec=["close_async"])
67+
closable_document_store.close_async = AsyncMock()
68+
checker = CacheChecker(document_store=closable_document_store, cache_field="url")
69+
await checker.close_async()
70+
closable_document_store.close_async.assert_awaited_once_with()
71+
72+
nonclosable_document_store = Mock(spec=[])
73+
checker = CacheChecker(document_store=nonclosable_document_store, cache_field="url")
74+
await checker.close_async()
75+
assert nonclosable_document_store.mock_calls == []

test/components/retrievers/test_auto_merging_retriever.py

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@
22
#
33
# SPDX-License-Identifier: Apache-2.0
44

5+
from unittest.mock import Mock
6+
57
import pytest
68

79
from haystack import Document, Pipeline
@@ -252,3 +254,14 @@ def test_run_go_up_hierarchy_multiple_levels_hit_root_document(self, in_memory_d
252254

253255
assert len(result["documents"]) == 1
254256
assert result["documents"][0].meta["__level"] == 0 # hit root document
257+
258+
def test_close(self):
259+
closable_document_store = Mock(spec=["close"])
260+
retriever = AutoMergingRetriever(document_store=closable_document_store)
261+
retriever.close()
262+
closable_document_store.close.assert_called_once_with()
263+
264+
nonclosable_document_store = Mock(spec=[])
265+
retriever = AutoMergingRetriever(document_store=nonclosable_document_store)
266+
retriever.close()
267+
assert nonclosable_document_store.mock_calls == []

test/components/retrievers/test_auto_merging_retriever_async.py

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@
22
#
33
# SPDX-License-Identifier: Apache-2.0
44

5+
from unittest.mock import AsyncMock, Mock
6+
57
import pytest
68

79
from haystack import Document
@@ -206,3 +208,16 @@ async def test_run_go_up_hierarchy_multiple_levels_hit_root_document(self, in_me
206208

207209
assert len(result["documents"]) == 1
208210
assert result["documents"][0].meta["__level"] == 0 # hit root document
211+
212+
@pytest.mark.asyncio
213+
async def test_close_async(self):
214+
closable_document_store = Mock(spec=["close_async"])
215+
closable_document_store.close_async = AsyncMock()
216+
retriever = AutoMergingRetriever(document_store=closable_document_store)
217+
await retriever.close_async()
218+
closable_document_store.close_async.assert_awaited_once_with()
219+
220+
nonclosable_document_store = Mock(spec=[])
221+
retriever = AutoMergingRetriever(document_store=nonclosable_document_store)
222+
await retriever.close_async()
223+
assert nonclosable_document_store.mock_calls == []

0 commit comments

Comments
 (0)