2023-10-26 13:34:34 -04:00
|
|
|
# -*- coding: utf-8 -*-
|
2023-03-17 12:03:46 -07:00
|
|
|
# SPDX-License-Identifier: Apache-2.0
|
|
|
|
|
#
|
|
|
|
|
# The OpenSearch Contributors require contributions made to
|
|
|
|
|
# this file be licensed under the Apache-2.0 license or a
|
|
|
|
|
# compatible open source license.
|
|
|
|
|
#
|
|
|
|
|
# Modifications Copyright OpenSearch Contributors. See
|
|
|
|
|
# GitHub history for details.
|
|
|
|
|
|
|
|
|
|
import copy
|
2023-11-06 13:08:19 -05:00
|
|
|
from typing import Any, Sequence
|
2023-03-17 12:03:46 -07:00
|
|
|
|
|
|
|
|
from six import iteritems, string_types
|
|
|
|
|
|
|
|
|
|
from opensearchpy._async.helpers.actions import aiter, async_scan
|
|
|
|
|
from opensearchpy.connection.async_connections import get_connection
|
|
|
|
|
from opensearchpy.exceptions import IllegalOperation, TransportError
|
|
|
|
|
from opensearchpy.helpers.aggs import A
|
|
|
|
|
from opensearchpy.helpers.query import Bool, Q
|
|
|
|
|
from opensearchpy.helpers.response import Response
|
|
|
|
|
from opensearchpy.helpers.search import AggsProxy, ProxyDescriptor, QueryProxy, Request
|
|
|
|
|
from opensearchpy.helpers.utils import AttrDict, recursive_to_dict
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class AsyncSearch(Request):
|
|
|
|
|
query = ProxyDescriptor("query")
|
|
|
|
|
post_filter = ProxyDescriptor("post_filter")
|
|
|
|
|
|
2023-11-06 13:08:19 -05:00
|
|
|
def __init__(self, **kwargs: Any) -> None:
|
2023-03-17 12:03:46 -07:00
|
|
|
"""
|
|
|
|
|
Search request to opensearch.
|
|
|
|
|
|
|
|
|
|
:arg using: `AsyncOpenSearch` instance to use
|
|
|
|
|
:arg index: limit the search to index
|
|
|
|
|
:arg doc_type: only query this type.
|
|
|
|
|
|
|
|
|
|
All the parameters supplied (or omitted) at creation type can be later
|
|
|
|
|
overridden by methods (`using`, `index` and `doc_type` respectively).
|
|
|
|
|
"""
|
|
|
|
|
super(AsyncSearch, self).__init__(**kwargs)
|
|
|
|
|
|
|
|
|
|
self.aggs = AggsProxy(self)
|
2023-11-06 13:08:19 -05:00
|
|
|
self._sort: Sequence[Any] = []
|
|
|
|
|
self._source: Any = None
|
|
|
|
|
self._highlight: Any = {}
|
|
|
|
|
self._highlight_opts: Any = {}
|
|
|
|
|
self._suggest: Any = {}
|
|
|
|
|
self._script_fields: Any = {}
|
|
|
|
|
self._response_class: Any = Response
|
2023-03-17 12:03:46 -07:00
|
|
|
|
|
|
|
|
self._query_proxy = QueryProxy(self, "query")
|
|
|
|
|
self._post_filter_proxy = QueryProxy(self, "post_filter")
|
|
|
|
|
|
2023-11-06 13:08:19 -05:00
|
|
|
def filter(self, *args: Any, **kwargs: Any) -> Any:
|
2023-03-17 12:03:46 -07:00
|
|
|
return self.query(Bool(filter=[Q(*args, **kwargs)]))
|
|
|
|
|
|
2023-11-06 13:08:19 -05:00
|
|
|
def exclude(self, *args: Any, **kwargs: Any) -> Any:
|
2023-03-17 12:03:46 -07:00
|
|
|
return self.query(Bool(filter=[~Q(*args, **kwargs)]))
|
|
|
|
|
|
2023-11-06 13:08:19 -05:00
|
|
|
def __getitem__(self, n: Any) -> Any:
|
2023-03-17 12:03:46 -07:00
|
|
|
"""
|
|
|
|
|
Support slicing the `AsyncSearch` instance for pagination.
|
|
|
|
|
|
|
|
|
|
Slicing equates to the from/size parameters. E.g.::
|
|
|
|
|
|
|
|
|
|
s = AsyncSearch().query(...)[0:25]
|
|
|
|
|
|
|
|
|
|
is equivalent to::
|
|
|
|
|
|
|
|
|
|
s = AsyncSearch().query(...).extra(from_=0, size=25)
|
|
|
|
|
|
|
|
|
|
"""
|
|
|
|
|
s = self._clone()
|
|
|
|
|
|
|
|
|
|
if isinstance(n, slice):
|
|
|
|
|
# If negative slicing, abort.
|
|
|
|
|
if n.start and n.start < 0 or n.stop and n.stop < 0:
|
|
|
|
|
raise ValueError("AsyncSearch does not support negative slicing.")
|
|
|
|
|
# OpenSearch won't get all results so we default to size: 10 if
|
|
|
|
|
# stop not given.
|
|
|
|
|
s._extra["from"] = n.start or 0
|
|
|
|
|
s._extra["size"] = max(
|
|
|
|
|
0, n.stop - (n.start or 0) if n.stop is not None else 10
|
|
|
|
|
)
|
|
|
|
|
return s
|
|
|
|
|
else: # This is an index lookup, equivalent to slicing by [n:n+1].
|
|
|
|
|
# If negative index, abort.
|
|
|
|
|
if n < 0:
|
|
|
|
|
raise ValueError("AsyncSearch does not support negative indexing.")
|
|
|
|
|
s._extra["from"] = n
|
|
|
|
|
s._extra["size"] = 1
|
|
|
|
|
return s
|
|
|
|
|
|
|
|
|
|
@classmethod
|
2023-11-06 13:08:19 -05:00
|
|
|
def from_dict(cls, d: Any) -> Any:
|
2023-03-17 12:03:46 -07:00
|
|
|
"""
|
|
|
|
|
Construct a new `AsyncSearch` instance from a raw dict containing the search
|
|
|
|
|
body. Useful when migrating from raw dictionaries.
|
|
|
|
|
|
|
|
|
|
Example::
|
|
|
|
|
|
|
|
|
|
s = AsyncSearch.from_dict({
|
|
|
|
|
"query": {
|
|
|
|
|
"bool": {
|
|
|
|
|
"must": [...]
|
|
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
"aggs": {...}
|
|
|
|
|
})
|
|
|
|
|
s = s.filter('term', published=True)
|
|
|
|
|
"""
|
|
|
|
|
s = cls()
|
|
|
|
|
s.update_from_dict(d)
|
|
|
|
|
return s
|
|
|
|
|
|
2023-11-06 13:08:19 -05:00
|
|
|
def _clone(self) -> Any:
|
2023-03-17 12:03:46 -07:00
|
|
|
"""
|
|
|
|
|
Return a clone of the current search request. Performs a shallow copy
|
|
|
|
|
of all the underlying objects. Used internally by most state modifying
|
|
|
|
|
APIs.
|
|
|
|
|
"""
|
|
|
|
|
s = super(AsyncSearch, self)._clone()
|
|
|
|
|
|
|
|
|
|
s._response_class = self._response_class
|
|
|
|
|
s._sort = self._sort[:]
|
|
|
|
|
s._source = copy.copy(self._source) if self._source is not None else None
|
|
|
|
|
s._highlight = self._highlight.copy()
|
|
|
|
|
s._highlight_opts = self._highlight_opts.copy()
|
|
|
|
|
s._suggest = self._suggest.copy()
|
|
|
|
|
s._script_fields = self._script_fields.copy()
|
|
|
|
|
for x in ("query", "post_filter"):
|
|
|
|
|
getattr(s, x)._proxied = getattr(self, x)._proxied
|
|
|
|
|
|
|
|
|
|
# copy top-level bucket definitions
|
|
|
|
|
if self.aggs._params.get("aggs"):
|
|
|
|
|
s.aggs._params = {"aggs": self.aggs._params["aggs"].copy()}
|
|
|
|
|
return s
|
|
|
|
|
|
2023-11-06 13:08:19 -05:00
|
|
|
def response_class(self, cls: Any) -> Any:
|
2023-03-17 12:03:46 -07:00
|
|
|
"""
|
|
|
|
|
Override the default wrapper used for the response.
|
|
|
|
|
"""
|
|
|
|
|
s = self._clone()
|
|
|
|
|
s._response_class = cls
|
|
|
|
|
return s
|
|
|
|
|
|
2023-11-06 13:08:19 -05:00
|
|
|
def update_from_dict(self, d: Any) -> "AsyncSearch":
|
2023-03-17 12:03:46 -07:00
|
|
|
"""
|
|
|
|
|
Apply options from a serialized body to the current instance. Modifies
|
|
|
|
|
the object in-place. Used mostly by ``from_dict``.
|
|
|
|
|
"""
|
|
|
|
|
d = d.copy()
|
|
|
|
|
if "query" in d:
|
|
|
|
|
self.query._proxied = Q(d.pop("query"))
|
|
|
|
|
if "post_filter" in d:
|
|
|
|
|
self.post_filter._proxied = Q(d.pop("post_filter"))
|
|
|
|
|
|
|
|
|
|
aggs = d.pop("aggs", d.pop("aggregations", {}))
|
|
|
|
|
if aggs:
|
|
|
|
|
self.aggs._params = {
|
|
|
|
|
"aggs": {name: A(value) for (name, value) in iteritems(aggs)}
|
|
|
|
|
}
|
|
|
|
|
if "sort" in d:
|
|
|
|
|
self._sort = d.pop("sort")
|
|
|
|
|
if "_source" in d:
|
|
|
|
|
self._source = d.pop("_source")
|
|
|
|
|
if "highlight" in d:
|
|
|
|
|
high = d.pop("highlight").copy()
|
|
|
|
|
self._highlight = high.pop("fields")
|
|
|
|
|
self._highlight_opts = high
|
|
|
|
|
if "suggest" in d:
|
|
|
|
|
self._suggest = d.pop("suggest")
|
|
|
|
|
if "text" in self._suggest:
|
|
|
|
|
text = self._suggest.pop("text")
|
|
|
|
|
for s in self._suggest.values():
|
|
|
|
|
s.setdefault("text", text)
|
|
|
|
|
if "script_fields" in d:
|
|
|
|
|
self._script_fields = d.pop("script_fields")
|
|
|
|
|
self._extra.update(d)
|
|
|
|
|
return self
|
|
|
|
|
|
2023-11-06 13:08:19 -05:00
|
|
|
def script_fields(self, **kwargs: Any) -> Any:
|
2023-03-17 12:03:46 -07:00
|
|
|
"""
|
|
|
|
|
Define script fields to be calculated on hits.
|
|
|
|
|
|
|
|
|
|
Example::
|
|
|
|
|
|
|
|
|
|
s = AsyncSearch()
|
|
|
|
|
s = s.script_fields(times_two="doc['field'].value * 2")
|
|
|
|
|
s = s.script_fields(
|
|
|
|
|
times_three={
|
|
|
|
|
'script': {
|
|
|
|
|
'lang': 'painless',
|
|
|
|
|
'source': "doc['field'].value * params.n",
|
|
|
|
|
'params': {'n': 3}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
"""
|
|
|
|
|
s = self._clone()
|
|
|
|
|
for name in kwargs:
|
|
|
|
|
if isinstance(kwargs[name], string_types):
|
|
|
|
|
kwargs[name] = {"script": kwargs[name]}
|
|
|
|
|
s._script_fields.update(kwargs)
|
|
|
|
|
return s
|
|
|
|
|
|
2023-11-06 13:08:19 -05:00
|
|
|
def source(self, fields: Any = None, **kwargs: Any) -> Any:
|
2023-03-17 12:03:46 -07:00
|
|
|
"""
|
|
|
|
|
Selectively control how the _source field is returned.
|
|
|
|
|
|
|
|
|
|
:arg fields: wildcard string, array of wildcards, or dictionary of includes and excludes
|
|
|
|
|
|
|
|
|
|
If ``fields`` is None, the entire document will be returned for
|
|
|
|
|
each hit. If fields is a dictionary with keys of 'includes' and/or
|
|
|
|
|
'excludes' the fields will be either included or excluded appropriately.
|
|
|
|
|
|
|
|
|
|
Calling this multiple times with the same named parameter will override the
|
|
|
|
|
previous values with the new ones.
|
|
|
|
|
|
|
|
|
|
Example::
|
|
|
|
|
|
|
|
|
|
s = AsyncSearch()
|
|
|
|
|
s = s.source(includes=['obj1.*'], excludes=["*.description"])
|
|
|
|
|
|
|
|
|
|
s = AsyncSearch()
|
|
|
|
|
s = s.source(includes=['obj1.*']).source(excludes=["*.description"])
|
|
|
|
|
|
|
|
|
|
"""
|
|
|
|
|
s = self._clone()
|
|
|
|
|
|
|
|
|
|
if fields and kwargs:
|
|
|
|
|
raise ValueError("You cannot specify fields and kwargs at the same time.")
|
|
|
|
|
|
|
|
|
|
if fields is not None:
|
|
|
|
|
s._source = fields
|
|
|
|
|
return s
|
|
|
|
|
|
|
|
|
|
if kwargs and not isinstance(s._source, dict):
|
|
|
|
|
s._source = {}
|
|
|
|
|
|
|
|
|
|
for key, value in kwargs.items():
|
|
|
|
|
if value is None:
|
|
|
|
|
try:
|
|
|
|
|
del s._source[key]
|
|
|
|
|
except KeyError:
|
|
|
|
|
pass
|
|
|
|
|
else:
|
|
|
|
|
s._source[key] = value
|
|
|
|
|
|
|
|
|
|
return s
|
|
|
|
|
|
2023-11-06 13:08:19 -05:00
|
|
|
def sort(self, *keys: Any) -> Any:
|
2023-03-17 12:03:46 -07:00
|
|
|
"""
|
|
|
|
|
Add sorting information to the search request. If called without
|
|
|
|
|
arguments it will remove all sort requirements. Otherwise it will
|
|
|
|
|
replace them. Acceptable arguments are::
|
|
|
|
|
|
|
|
|
|
'some.field'
|
|
|
|
|
'-some.other.field'
|
|
|
|
|
{'different.field': {'any': 'dict'}}
|
|
|
|
|
|
|
|
|
|
so for example::
|
|
|
|
|
|
|
|
|
|
s = AsyncSearch().sort(
|
|
|
|
|
'category',
|
|
|
|
|
'-title',
|
|
|
|
|
{"price" : {"order" : "asc", "mode" : "avg"}}
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
will sort by ``category``, ``title`` (in descending order) and
|
|
|
|
|
``price`` in ascending order using the ``avg`` mode.
|
|
|
|
|
|
|
|
|
|
The API returns a copy of the AsyncSearch object and can thus be chained.
|
|
|
|
|
"""
|
|
|
|
|
s = self._clone()
|
|
|
|
|
s._sort = []
|
|
|
|
|
for k in keys:
|
|
|
|
|
if isinstance(k, string_types) and k.startswith("-"):
|
|
|
|
|
if k[1:] == "_score":
|
|
|
|
|
raise IllegalOperation("Sorting by `-_score` is not allowed.")
|
|
|
|
|
k = {k[1:]: {"order": "desc"}}
|
|
|
|
|
s._sort.append(k)
|
|
|
|
|
return s
|
|
|
|
|
|
2023-11-06 13:08:19 -05:00
|
|
|
def highlight_options(self, **kwargs: Any) -> Any:
|
2023-03-17 12:03:46 -07:00
|
|
|
"""
|
|
|
|
|
Update the global highlighting options used for this request. For
|
|
|
|
|
example::
|
|
|
|
|
|
|
|
|
|
s = AsyncSearch()
|
|
|
|
|
s = s.highlight_options(order='score')
|
|
|
|
|
"""
|
|
|
|
|
s = self._clone()
|
|
|
|
|
s._highlight_opts.update(kwargs)
|
|
|
|
|
return s
|
|
|
|
|
|
2023-11-06 13:08:19 -05:00
|
|
|
def highlight(self, *fields: Any, **kwargs: Any) -> Any:
|
2023-03-17 12:03:46 -07:00
|
|
|
"""
|
|
|
|
|
Request highlighting of some fields. All keyword arguments passed in will be
|
|
|
|
|
used as parameters for all the fields in the ``fields`` parameter. Example::
|
|
|
|
|
|
|
|
|
|
AsyncSearch().highlight('title', 'body', fragment_size=50)
|
|
|
|
|
|
|
|
|
|
will produce the equivalent of::
|
|
|
|
|
|
|
|
|
|
{
|
|
|
|
|
"highlight": {
|
|
|
|
|
"fields": {
|
|
|
|
|
"body": {"fragment_size": 50},
|
|
|
|
|
"title": {"fragment_size": 50}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
If you want to have different options for different fields
|
|
|
|
|
you can call ``highlight`` twice::
|
|
|
|
|
|
|
|
|
|
AsyncSearch().highlight('title', fragment_size=50).highlight('body', fragment_size=100)
|
|
|
|
|
|
|
|
|
|
which will produce::
|
|
|
|
|
|
|
|
|
|
{
|
|
|
|
|
"highlight": {
|
|
|
|
|
"fields": {
|
|
|
|
|
"body": {"fragment_size": 100},
|
|
|
|
|
"title": {"fragment_size": 50}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
"""
|
|
|
|
|
s = self._clone()
|
|
|
|
|
for f in fields:
|
|
|
|
|
s._highlight[f] = kwargs
|
|
|
|
|
return s
|
|
|
|
|
|
2023-11-06 13:08:19 -05:00
|
|
|
def suggest(self, name: str, text: str, **kwargs: Any) -> Any:
|
2023-03-17 12:03:46 -07:00
|
|
|
"""
|
|
|
|
|
Add a suggestions request to the search.
|
|
|
|
|
|
|
|
|
|
:arg name: name of the suggestion
|
|
|
|
|
:arg text: text to suggest on
|
|
|
|
|
|
|
|
|
|
All keyword arguments will be added to the suggestions body. For example::
|
|
|
|
|
|
|
|
|
|
s = AsyncSearch()
|
|
|
|
|
s = s.suggest('suggestion-1', 'AsyncOpenSearch', term={'field': 'body'})
|
|
|
|
|
"""
|
|
|
|
|
s = self._clone()
|
|
|
|
|
s._suggest[name] = {"text": text}
|
|
|
|
|
s._suggest[name].update(kwargs)
|
|
|
|
|
return s
|
|
|
|
|
|
2023-11-06 13:08:19 -05:00
|
|
|
def to_dict(self, count: bool = False, **kwargs: Any) -> Any:
|
2023-03-17 12:03:46 -07:00
|
|
|
"""
|
|
|
|
|
Serialize the search into the dictionary that will be sent over as the
|
|
|
|
|
request's body.
|
|
|
|
|
|
|
|
|
|
:arg count: a flag to specify if we are interested in a body for count -
|
|
|
|
|
no aggregations, no pagination bounds etc.
|
|
|
|
|
|
|
|
|
|
All additional keyword arguments will be included into the dictionary.
|
|
|
|
|
"""
|
|
|
|
|
d = {}
|
|
|
|
|
|
|
|
|
|
if self.query:
|
|
|
|
|
d["query"] = self.query.to_dict()
|
|
|
|
|
|
|
|
|
|
# count request doesn't care for sorting and other things
|
|
|
|
|
if not count:
|
|
|
|
|
if self.post_filter:
|
|
|
|
|
d["post_filter"] = self.post_filter.to_dict()
|
|
|
|
|
|
|
|
|
|
if self.aggs.aggs:
|
|
|
|
|
d.update(self.aggs.to_dict())
|
|
|
|
|
|
|
|
|
|
if self._sort:
|
|
|
|
|
d["sort"] = self._sort
|
|
|
|
|
|
|
|
|
|
d.update(recursive_to_dict(self._extra))
|
|
|
|
|
|
|
|
|
|
if self._source not in (None, {}):
|
|
|
|
|
d["_source"] = self._source
|
|
|
|
|
|
|
|
|
|
if self._highlight:
|
|
|
|
|
d["highlight"] = {"fields": self._highlight}
|
|
|
|
|
d["highlight"].update(self._highlight_opts)
|
|
|
|
|
|
|
|
|
|
if self._suggest:
|
|
|
|
|
d["suggest"] = self._suggest
|
|
|
|
|
|
|
|
|
|
if self._script_fields:
|
|
|
|
|
d["script_fields"] = self._script_fields
|
|
|
|
|
|
|
|
|
|
d.update(recursive_to_dict(kwargs))
|
|
|
|
|
return d
|
|
|
|
|
|
2023-11-06 13:08:19 -05:00
|
|
|
async def count(self) -> Any:
|
2023-03-17 12:03:46 -07:00
|
|
|
"""
|
|
|
|
|
Return the number of hits matching the query and filters. Note that
|
|
|
|
|
only the actual number is returned.
|
|
|
|
|
"""
|
|
|
|
|
if hasattr(self, "_response") and self._response.hits.total.relation == "eq":
|
|
|
|
|
return self._response.hits.total.value
|
|
|
|
|
|
|
|
|
|
opensearch = await get_connection(self._using)
|
|
|
|
|
|
|
|
|
|
d = self.to_dict(count=True)
|
|
|
|
|
# TODO: failed shards detection
|
|
|
|
|
return (await opensearch.count(index=self._index, body=d, **self._params))[
|
|
|
|
|
"count"
|
|
|
|
|
]
|
|
|
|
|
|
2023-11-06 13:08:19 -05:00
|
|
|
async def execute(self, ignore_cache: bool = False) -> Any:
|
2023-03-17 12:03:46 -07:00
|
|
|
"""
|
|
|
|
|
Execute the search and return an instance of ``Response`` wrapping all
|
|
|
|
|
the data.
|
|
|
|
|
|
|
|
|
|
:arg ignore_cache: if set to ``True``, consecutive calls will hit
|
|
|
|
|
AsyncOpenSearch, while cached result will be ignored. Defaults to `False`
|
|
|
|
|
"""
|
|
|
|
|
if ignore_cache or not hasattr(self, "_response"):
|
|
|
|
|
opensearch = await get_connection(self._using)
|
|
|
|
|
|
|
|
|
|
self._response = self._response_class(
|
|
|
|
|
self,
|
|
|
|
|
await opensearch.search(
|
|
|
|
|
index=self._index, body=self.to_dict(), **self._params
|
|
|
|
|
),
|
|
|
|
|
)
|
|
|
|
|
return self._response
|
|
|
|
|
|
2023-11-06 13:08:19 -05:00
|
|
|
async def scan(self) -> Any:
|
2023-03-17 12:03:46 -07:00
|
|
|
"""
|
|
|
|
|
Turn the search into a scan search and return a generator that will
|
|
|
|
|
iterate over all the documents matching the query.
|
|
|
|
|
|
|
|
|
|
Use ``params`` method to specify any additional arguments you with to
|
|
|
|
|
pass to the underlying ``async_scan`` helper from ``opensearchpy``
|
|
|
|
|
|
|
|
|
|
"""
|
|
|
|
|
opensearch = await get_connection(self._using)
|
|
|
|
|
|
|
|
|
|
async for hit in aiter(
|
|
|
|
|
async_scan(
|
|
|
|
|
opensearch, query=self.to_dict(), index=self._index, **self._params
|
|
|
|
|
)
|
|
|
|
|
):
|
|
|
|
|
yield self._get_result(hit)
|
|
|
|
|
|
2023-11-06 13:08:19 -05:00
|
|
|
async def delete(self) -> Any:
|
2023-03-17 12:03:46 -07:00
|
|
|
"""
|
|
|
|
|
delete() executes the query by delegating to delete_by_query()
|
|
|
|
|
"""
|
|
|
|
|
|
|
|
|
|
opensearch = await get_connection(self._using)
|
|
|
|
|
|
|
|
|
|
return AttrDict(
|
|
|
|
|
await opensearch.delete_by_query(
|
|
|
|
|
index=self._index, body=self.to_dict(), **self._params
|
|
|
|
|
)
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class AsyncMultiSearch(Request):
|
|
|
|
|
"""
|
|
|
|
|
Combine multiple :class:`~opensearchpy.AsyncSearch` objects into a single
|
|
|
|
|
request.
|
|
|
|
|
"""
|
|
|
|
|
|
2023-11-06 13:08:19 -05:00
|
|
|
def __init__(self, **kwargs: Any) -> None:
|
2023-03-17 12:03:46 -07:00
|
|
|
super(AsyncMultiSearch, self).__init__(**kwargs)
|
2023-11-06 13:08:19 -05:00
|
|
|
self._searches: Any = []
|
2023-03-17 12:03:46 -07:00
|
|
|
|
2023-11-06 13:08:19 -05:00
|
|
|
def __getitem__(self, key: Any) -> Any:
|
2023-03-17 12:03:46 -07:00
|
|
|
return self._searches[key]
|
|
|
|
|
|
2023-11-06 13:08:19 -05:00
|
|
|
def __iter__(self) -> Any:
|
2023-03-17 12:03:46 -07:00
|
|
|
return iter(self._searches)
|
|
|
|
|
|
2023-11-06 13:08:19 -05:00
|
|
|
def _clone(self) -> Any:
|
2023-03-17 12:03:46 -07:00
|
|
|
ms = super(AsyncMultiSearch, self)._clone()
|
|
|
|
|
ms._searches = self._searches[:]
|
|
|
|
|
return ms
|
|
|
|
|
|
2023-11-06 13:08:19 -05:00
|
|
|
def add(self, search: Any) -> Any:
|
2023-03-17 12:03:46 -07:00
|
|
|
"""
|
|
|
|
|
Adds a new :class:`~opensearchpy.AsyncSearch` object to the request::
|
|
|
|
|
|
|
|
|
|
ms = AsyncMultiSearch(index='my-index')
|
|
|
|
|
ms = ms.add(AsyncSearch(doc_type=Category).filter('term', category='python'))
|
|
|
|
|
ms = ms.add(AsyncSearch(doc_type=Blog))
|
|
|
|
|
"""
|
|
|
|
|
ms = self._clone()
|
|
|
|
|
ms._searches.append(search)
|
|
|
|
|
return ms
|
|
|
|
|
|
2023-11-06 13:08:19 -05:00
|
|
|
def to_dict(self) -> Any:
|
2023-03-17 12:03:46 -07:00
|
|
|
out = []
|
|
|
|
|
for s in self._searches:
|
|
|
|
|
meta = {}
|
|
|
|
|
if s._index:
|
|
|
|
|
meta["index"] = s._index
|
|
|
|
|
meta.update(s._params)
|
|
|
|
|
|
|
|
|
|
out.append(meta)
|
|
|
|
|
out.append(s.to_dict())
|
|
|
|
|
|
|
|
|
|
return out
|
|
|
|
|
|
2023-11-06 13:08:19 -05:00
|
|
|
async def execute(
|
|
|
|
|
self, ignore_cache: bool = False, raise_on_error: bool = True
|
|
|
|
|
) -> Any:
|
2023-03-17 12:03:46 -07:00
|
|
|
"""
|
|
|
|
|
Execute the multi search request and return a list of search results.
|
|
|
|
|
"""
|
|
|
|
|
if ignore_cache or not hasattr(self, "_response"):
|
|
|
|
|
opensearch = await get_connection(self._using)
|
|
|
|
|
|
|
|
|
|
responses = await opensearch.msearch(
|
|
|
|
|
index=self._index, body=self.to_dict(), **self._params
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
out = []
|
|
|
|
|
for s, r in zip(self._searches, responses["responses"]):
|
|
|
|
|
if r.get("error", False):
|
|
|
|
|
if raise_on_error:
|
|
|
|
|
raise TransportError("N/A", r["error"]["type"], r["error"])
|
|
|
|
|
r = None
|
|
|
|
|
else:
|
|
|
|
|
r = Response(s, r)
|
|
|
|
|
out.append(r)
|
|
|
|
|
|
|
|
|
|
self._response = out
|
|
|
|
|
|
|
|
|
|
return self._response
|