diff --git a/docs/search_dsl.rst b/docs/search_dsl.rst index 2dc1388f..ca11b442 100644 --- a/docs/search_dsl.rst +++ b/docs/search_dsl.rst @@ -666,3 +666,10 @@ If you need to execute multiple searches at the same time you can use the print("Results for query %r." % response.search.query) for hit in response: print(hit.title) + + +``EmptySearch`` +--------------- + +The ``EmptySearch`` class can be used as a fully compatible version of ``Search`` +that will return no results, regardless of any queries configured. diff --git a/elasticsearch_dsl/__init__.py b/elasticsearch_dsl/__init__.py index e7687f83..2db837b0 100644 --- a/elasticsearch_dsl/__init__.py +++ b/elasticsearch_dsl/__init__.py @@ -81,7 +81,14 @@ from .index import AsyncIndex, AsyncIndexTemplate, Index, IndexTemplate from .mapping import AsyncMapping, Mapping from .query import Q -from .search import AsyncMultiSearch, AsyncSearch, MultiSearch, Search +from .search import ( + AsyncEmptySearch, + AsyncMultiSearch, + AsyncSearch, + EmptySearch, + MultiSearch, + Search, +) from .update_by_query import AsyncUpdateByQuery, UpdateByQuery from .utils import AttrDict, AttrList, DslBase from .wrappers import Range @@ -92,6 +99,7 @@ __all__ = [ "A", "AsyncDocument", + "AsyncEmptySearch", "AsyncFacetedSearch", "AsyncIndex", "AsyncIndexTemplate", @@ -115,6 +123,7 @@ "DoubleRange", "DslBase", "ElasticsearchDslException", + "EmptySearch", "Facet", "FacetedResponse", "FacetedSearch", diff --git a/elasticsearch_dsl/_async/search.py b/elasticsearch_dsl/_async/search.py index 25885e08..faffb17f 100644 --- a/elasticsearch_dsl/_async/search.py +++ b/elasticsearch_dsl/_async/search.py @@ -144,3 +144,18 @@ async def execute(self, ignore_cache=False, raise_on_error=True): self._response = out return self._response + + +class AsyncEmptySearch(AsyncSearch): + async def count(self): + return 0 + + async def execute(self, ignore_cache=False): + return self._response_class(self, {"hits": {"total": 0, "hits": []}}) + + async def scan(self): + return + yield # a bit strange, but this forces an empty generator function + + async def delete(self): + return AttrDict({}) diff --git a/elasticsearch_dsl/_sync/search.py b/elasticsearch_dsl/_sync/search.py index a15a08aa..ae379237 100644 --- a/elasticsearch_dsl/_sync/search.py +++ b/elasticsearch_dsl/_sync/search.py @@ -136,3 +136,18 @@ def execute(self, ignore_cache=False, raise_on_error=True): self._response = out return self._response + + +class EmptySearch(Search): + def count(self): + return 0 + + def execute(self, ignore_cache=False): + return self._response_class(self, {"hits": {"total": 0, "hits": []}}) + + def scan(self): + return + yield # a bit strange, but this forces an empty generator function + + def delete(self): + return AttrDict({}) diff --git a/elasticsearch_dsl/search.py b/elasticsearch_dsl/search.py index a94e569b..04307294 100644 --- a/elasticsearch_dsl/search.py +++ b/elasticsearch_dsl/search.py @@ -15,6 +15,14 @@ # specific language governing permissions and limitations # under the License. -from elasticsearch_dsl._async.search import AsyncMultiSearch, AsyncSearch # noqa: F401 -from elasticsearch_dsl._sync.search import MultiSearch, Search # noqa: F401 +from elasticsearch_dsl._async.search import ( # noqa: F401 + AsyncEmptySearch, + AsyncMultiSearch, + AsyncSearch, +) +from elasticsearch_dsl._sync.search import ( # noqa: F401 + EmptySearch, + MultiSearch, + Search, +) from elasticsearch_dsl.search_base import Q # noqa: F401 diff --git a/tests/_async/test_search.py b/tests/_async/test_search.py index aabbce6f..2f5a9afc 100644 --- a/tests/_async/test_search.py +++ b/tests/_async/test_search.py @@ -19,7 +19,7 @@ from pytest import raises, warns -from elasticsearch_dsl import AsyncSearch, Document, Q, query +from elasticsearch_dsl import A, AsyncEmptySearch, AsyncSearch, Document, Q, query from elasticsearch_dsl.exceptions import IllegalOperation @@ -687,3 +687,14 @@ def test_rescore_query_to_dict(): }, }, } + + +async def test_empty_search(): + s = AsyncEmptySearch(index="index-name") + s = s.query("match", lang="java") + s.aggs.bucket("versions", A("terms", field="version")) + + assert await s.count() == 0 + assert [hit async for hit in s] == [] + assert [hit async for hit in s.scan()] == [] + await s.delete() # should not error diff --git a/tests/_sync/test_search.py b/tests/_sync/test_search.py index 001ce704..4e8de2b2 100644 --- a/tests/_sync/test_search.py +++ b/tests/_sync/test_search.py @@ -19,7 +19,7 @@ from pytest import raises, warns -from elasticsearch_dsl import Document, Q, Search, query +from elasticsearch_dsl import A, Document, EmptySearch, Q, Search, query from elasticsearch_dsl.exceptions import IllegalOperation @@ -685,3 +685,14 @@ def test_rescore_query_to_dict(): }, }, } + + +def test_empty_search(): + s = EmptySearch(index="index-name") + s = s.query("match", lang="java") + s.aggs.bucket("versions", A("terms", field="version")) + + assert s.count() == 0 + assert [hit for hit in s] == [] + assert [hit for hit in s.scan()] == [] + s.delete() # should not error diff --git a/utils/run-unasync.py b/utils/run-unasync.py index a0a908f5..cd8a3ab5 100644 --- a/utils/run-unasync.py +++ b/utils/run-unasync.py @@ -52,6 +52,7 @@ def main(check=False): "AsyncElasticsearch": "Elasticsearch", "AsyncSearch": "Search", "AsyncMultiSearch": "MultiSearch", + "AsyncEmptySearch": "EmptySearch", "AsyncDocument": "Document", "AsyncIndexMeta": "IndexMeta", "AsyncIndexTemplate": "IndexTemplate",