Skip to content

Commit 3668478

Browse files
Merge branch 'main' into main
2 parents 90f43ca + d48b7f8 commit 3668478

File tree

14 files changed

+659
-4
lines changed

14 files changed

+659
-4
lines changed

Changelog.rst

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,20 @@
33
Changelog
44
=========
55

6+
8.14.0 (2024-06-10)
7+
-------------------
8+
9+
* Added ``text_expansion`` query clause (`#1837 <https://github.com/elastic/elasticsearch-dsl-py/pull/1837>`_)
10+
* Added ``Response.search_after()`` and ``Search.search_after()`` methods for efficient iteration (`#1829 <https://github.com/elastic/elasticsearch-dsl-py/pull/1829>`_)
11+
* Added point in time support and the ``iterate()`` method in the ``Search`` class (`#1833 <https://github.com/elastic/elasticsearch-dsl-py/pull/1833>`_)
12+
* Added support for slicing multiple times in ``Search`` class (`#1771 <https://github.com/elastic/elasticsearch-dsl-py/pull/1771>`_)
13+
Added support for regular expressions in ``Completion.suggest()`` (`#1836 <https://github.com/elastic/elasticsearch-dsl-py/pull/1836>`_)
14+
* Fixed ``suggest()`` method of the ``Completion`` class to format requests correctly. (`#1836 <https://github.com/elastic/elasticsearch-dsl-py/pull/1836>`_)
15+
* Fixed ``Document.update()`` to accept fields set to ``None`` or empty (`#1820 <https://github.com/elastic/elasticsearch-dsl-py/pull/1820>`_)
16+
* Started work on type hints (Thanks Caio Fontes for leading this effort!)
17+
* Added Type hints to ``function.py`` (`#1827 <https://github.com/elastic/elasticsearch-dsl-py/pull/1827>`_)
18+
* Added Type hints to ``query.py`` (`#1821 <https://github.com/elastic/elasticsearch-dsl-py/pull/1821>`_)
19+
620
8.13.1 (2024-04-30)
721
-------------------
822

elasticsearch_dsl/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -93,7 +93,7 @@
9393
from .utils import AttrDict, AttrList, DslBase
9494
from .wrappers import Range
9595

96-
VERSION = (8, 13, 1)
96+
VERSION = (8, 14, 0)
9797
__version__ = VERSION
9898
__versionstr__ = ".".join(map(str, VERSION))
9999
__all__ = [

elasticsearch_dsl/_async/search.py

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,8 @@
1515
# specific language governing permissions and limitations
1616
# under the License.
1717

18+
import contextlib
19+
1820
from elasticsearch.exceptions import ApiError
1921
from elasticsearch.helpers import async_scan
2022

@@ -92,6 +94,8 @@ async def scan(self):
9294
pass to the underlying ``scan`` helper from ``elasticsearch-py`` -
9395
https://elasticsearch-py.readthedocs.io/en/master/helpers.html#elasticsearch.helpers.scan
9496
97+
The ``iterate()`` method should be preferred, as it provides similar
98+
functionality using an Elasticsearch point in time.
9599
"""
96100
es = get_connection(self._using)
97101

@@ -113,6 +117,45 @@ async def delete(self):
113117
)
114118
)
115119

120+
@contextlib.asynccontextmanager
121+
async def point_in_time(self, keep_alive="1m"):
122+
"""
123+
Open a point in time (pit) that can be used across several searches.
124+
125+
This method implements a context manager that returns a search object
126+
configured to operate within the created pit.
127+
128+
:arg keep_alive: the time to live for the point in time, renewed with each search request
129+
"""
130+
es = get_connection(self._using)
131+
132+
pit = await es.open_point_in_time(
133+
index=self._index or "*", keep_alive=keep_alive
134+
)
135+
search = self.index().extra(pit={"id": pit["id"], "keep_alive": keep_alive})
136+
if not search._sort:
137+
search = search.sort("_shard_doc")
138+
yield search
139+
await es.close_point_in_time(id=pit["id"])
140+
141+
async def iterate(self, keep_alive="1m"):
142+
"""
143+
Return a generator that iterates over all the documents matching the query.
144+
145+
This method uses a point in time to provide consistent results even when
146+
the index is changing. It should be preferred over ``scan()``.
147+
148+
:arg keep_alive: the time to live for the point in time, renewed with each new search request
149+
"""
150+
async with self.point_in_time(keep_alive=keep_alive) as s:
151+
while True:
152+
r = await s.execute()
153+
for hit in r:
154+
yield hit
155+
if len(r.hits) == 0:
156+
break
157+
s = r.search_after()
158+
116159

117160
class AsyncMultiSearch(MultiSearchBase):
118161
"""

elasticsearch_dsl/_sync/search.py

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,8 @@
1515
# specific language governing permissions and limitations
1616
# under the License.
1717

18+
import contextlib
19+
1820
from elasticsearch.exceptions import ApiError
1921
from elasticsearch.helpers import scan
2022

@@ -88,6 +90,8 @@ def scan(self):
8890
pass to the underlying ``scan`` helper from ``elasticsearch-py`` -
8991
https://elasticsearch-py.readthedocs.io/en/master/helpers.html#elasticsearch.helpers.scan
9092
93+
The ``iterate()`` method should be preferred, as it provides similar
94+
functionality using an Elasticsearch point in time.
9195
"""
9296
es = get_connection(self._using)
9397

@@ -105,6 +109,43 @@ def delete(self):
105109
es.delete_by_query(index=self._index, body=self.to_dict(), **self._params)
106110
)
107111

112+
@contextlib.contextmanager
113+
def point_in_time(self, keep_alive="1m"):
114+
"""
115+
Open a point in time (pit) that can be used across several searches.
116+
117+
This method implements a context manager that returns a search object
118+
configured to operate within the created pit.
119+
120+
:arg keep_alive: the time to live for the point in time, renewed with each search request
121+
"""
122+
es = get_connection(self._using)
123+
124+
pit = es.open_point_in_time(index=self._index or "*", keep_alive=keep_alive)
125+
search = self.index().extra(pit={"id": pit["id"], "keep_alive": keep_alive})
126+
if not search._sort:
127+
search = search.sort("_shard_doc")
128+
yield search
129+
es.close_point_in_time(id=pit["id"])
130+
131+
def iterate(self, keep_alive="1m"):
132+
"""
133+
Return a generator that iterates over all the documents matching the query.
134+
135+
This method uses a point in time to provide consistent results even when
136+
the index is changing. It should be preferred over ``scan()``.
137+
138+
:arg keep_alive: the time to live for the point in time, renewed with each new search request
139+
"""
140+
with self.point_in_time(keep_alive=keep_alive) as s:
141+
while True:
142+
r = s.execute()
143+
for hit in r:
144+
yield hit
145+
if len(r.hits) == 0:
146+
break
147+
s = r.search_after()
148+
108149

109150
class MultiSearch(MultiSearchBase):
110151
"""

elasticsearch_dsl/query.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -551,6 +551,10 @@ class TermsSet(Query):
551551
name = "terms_set"
552552

553553

554+
class TextExpansion(Query):
555+
name = "text_expansion"
556+
557+
554558
class Wildcard(Query):
555559
name = "wildcard"
556560

elasticsearch_dsl/search_base.py

Lines changed: 20 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -743,7 +743,7 @@ def highlight(self, *fields, **kwargs):
743743
s._highlight[f] = kwargs
744744
return s
745745

746-
def suggest(self, name, text, **kwargs):
746+
def suggest(self, name, text=None, regex=None, **kwargs):
747747
"""
748748
Add a suggestions request to the search.
749749
@@ -754,9 +754,27 @@ def suggest(self, name, text, **kwargs):
754754
755755
s = Search()
756756
s = s.suggest('suggestion-1', 'Elasticsearch', term={'field': 'body'})
757+
758+
# regex query for Completion Suggester
759+
s = Search()
760+
s = s.suggest('suggestion-1', regex='py[thon|py]', completion={'field': 'body'})
757761
"""
762+
if text is None and regex is None:
763+
raise ValueError('You have to pass "text" or "regex" argument.')
764+
if text and regex:
765+
raise ValueError('You can only pass either "text" or "regex" argument.')
766+
if regex and "completion" not in kwargs:
767+
raise ValueError(
768+
'"regex" argument must be passed with "completion" keyword argument.'
769+
)
770+
758771
s = self._clone()
759-
s._suggest[name] = {"text": text}
772+
if regex:
773+
s._suggest[name] = {"regex": regex}
774+
elif "completion" in kwargs:
775+
s._suggest[name] = {"prefix": text}
776+
else:
777+
s._suggest[name] = {"text": text}
760778
s._suggest[name].update(kwargs)
761779
return s
762780

0 commit comments

Comments
 (0)