diff --git a/elastic_enterprise_search/_async/client/app_search.py b/elastic_enterprise_search/_async/client/app_search.py index 9449191..a4bfc7d 100644 --- a/elastic_enterprise_search/_async/client/app_search.py +++ b/elastic_enterprise_search/_async/client/app_search.py @@ -24,6 +24,49 @@ class AsyncAppSearch(BaseClient): + @_rewrite_parameters(body_name="body", ignore_deprecated_options={"body", "params"}) + async def search_es_search( + self, + *, + engine_name: str, + params: t.Optional[t.Mapping[str, t.Any]] = None, + body: t.Optional[t.Mapping[str, t.Any]] = None, + analytics_query: t.Optional[str] = None, + analytics_tags: t.Optional[t.Union[str, t.Sequence[str]]] = None, + ) -> ObjectApiResponse[t.Any]: + """ + Execute the provided Elasticsearch search query against an App Search Engine + + ``_ + + :param engine_name: Name of the engine + :param params: Query parameters for the Elasticsearch Search request. + :param body: Body parameters for the Elasticsearch Search request. + :param analytics_query: The search query associated with this request when recording search analytics. + :param analytics_tags: The tags to apply to the query when recording search analytics. + """ + if engine_name in SKIP_IN_PATH: + raise ValueError("Empty value passed for parameter 'engine_name'") + + if params is not None and any(not isinstance(x, str) for x in params.values()): + raise TypeError("Values for 'params' parameter must be of type 'str'") + + __headers = {"accept": "application/json", "content-type": "application/json"} + if analytics_query is not None: + __headers["X-Enterprise-Search-Analytics"] = analytics_query + if analytics_tags is not None: + if isinstance(analytics_tags, str): + analytics_tags = (analytics_tags,) + __headers["X-Enterprise-Search-Analytics-Tags"] = ",".join(analytics_tags) + + return await self.perform_request( # type: ignore[return-value] + "POST", + f"/api/as/v0/engines/{_quote(engine_name)}/elasticsearch/_search", + params=params, + body=body, + headers=__headers, + ) + # AUTO-GENERATED-API-DEFINITIONS # @_rewrite_parameters() @@ -2132,42 +2175,6 @@ async def add_meta_engine_source( headers=__headers, ) - @_rewrite_parameters( - body_fields=True, - ) - async def search_es_search( - self, - *, - engine_name: str, - request: t.Mapping[str, t.Any], - analytics: t.Optional[t.Mapping[str, t.Any]] = None, - ) -> ObjectApiResponse[t.Any]: - """ - Execute the provided Elasticsearch search query against an App Search Engine - - ``_ - - :param engine_name: Name of the engine - :param request: - :param analytics: - """ - if engine_name in SKIP_IN_PATH: - raise ValueError("Empty value passed for parameter 'engine_name'") - if request is None: - raise ValueError("Empty value passed for parameter 'request'") - __body: t.Dict[str, t.Any] = {} - if request is not None: - __body["request"] = request - if analytics is not None: - __body["analytics"] = analytics - __headers = {"accept": "application/json", "content-type": "application/json"} - return await self.perform_request( # type: ignore[return-value] - "POST", - f"/api/as/v0/engines/{_quote(engine_name)}/elasticsearch/_search", - body=__body, - headers=__headers, - ) - @_rewrite_parameters( body_fields=True, ) diff --git a/elastic_enterprise_search/_sync/client/app_search.py b/elastic_enterprise_search/_sync/client/app_search.py index fd7413f..9e27cfd 100644 --- a/elastic_enterprise_search/_sync/client/app_search.py +++ b/elastic_enterprise_search/_sync/client/app_search.py @@ -24,6 +24,49 @@ class AppSearch(BaseClient): + @_rewrite_parameters(body_name="body", ignore_deprecated_options={"body", "params"}) + def search_es_search( + self, + *, + engine_name: str, + params: t.Optional[t.Mapping[str, t.Any]] = None, + body: t.Optional[t.Mapping[str, t.Any]] = None, + analytics_query: t.Optional[str] = None, + analytics_tags: t.Optional[t.Union[str, t.Sequence[str]]] = None, + ) -> ObjectApiResponse[t.Any]: + """ + Execute the provided Elasticsearch search query against an App Search Engine + + ``_ + + :param engine_name: Name of the engine + :param params: Query parameters for the Elasticsearch Search request. + :param body: Body parameters for the Elasticsearch Search request. + :param analytics_query: The search query associated with this request when recording search analytics. + :param analytics_tags: The tags to apply to the query when recording search analytics. + """ + if engine_name in SKIP_IN_PATH: + raise ValueError("Empty value passed for parameter 'engine_name'") + + if params is not None and any(not isinstance(x, str) for x in params.values()): + raise TypeError("Values for 'params' parameter must be of type 'str'") + + __headers = {"accept": "application/json", "content-type": "application/json"} + if analytics_query is not None: + __headers["X-Enterprise-Search-Analytics"] = analytics_query + if analytics_tags is not None: + if isinstance(analytics_tags, str): + analytics_tags = (analytics_tags,) + __headers["X-Enterprise-Search-Analytics-Tags"] = ",".join(analytics_tags) + + return self.perform_request( # type: ignore[return-value] + "POST", + f"/api/as/v0/engines/{_quote(engine_name)}/elasticsearch/_search", + params=params, + body=body, + headers=__headers, + ) + # AUTO-GENERATED-API-DEFINITIONS # @_rewrite_parameters() @@ -2132,42 +2175,6 @@ def add_meta_engine_source( headers=__headers, ) - @_rewrite_parameters( - body_fields=True, - ) - def search_es_search( - self, - *, - engine_name: str, - request: t.Mapping[str, t.Any], - analytics: t.Optional[t.Mapping[str, t.Any]] = None, - ) -> ObjectApiResponse[t.Any]: - """ - Execute the provided Elasticsearch search query against an App Search Engine - - ``_ - - :param engine_name: Name of the engine - :param request: - :param analytics: - """ - if engine_name in SKIP_IN_PATH: - raise ValueError("Empty value passed for parameter 'engine_name'") - if request is None: - raise ValueError("Empty value passed for parameter 'request'") - __body: t.Dict[str, t.Any] = {} - if request is not None: - __body["request"] = request - if analytics is not None: - __body["analytics"] = analytics - __headers = {"accept": "application/json", "content-type": "application/json"} - return self.perform_request( # type: ignore[return-value] - "POST", - f"/api/as/v0/engines/{_quote(engine_name)}/elasticsearch/_search", - body=__body, - headers=__headers, - ) - @_rewrite_parameters( body_fields=True, ) diff --git a/tests/client/app_search/cassettes/test_search_es_search.yaml b/tests/client/app_search/cassettes/test_search_es_search.yaml new file mode 100644 index 0000000..3c31479 --- /dev/null +++ b/tests/client/app_search/cassettes/test_search_es_search.yaml @@ -0,0 +1,49 @@ +interactions: +- request: + body: '{"query":{"match":{"*":"client"}}}' + headers: + accept: + - application/json + authorization: + - Bearer private-ybzoyx7cok65hpxyxkwaarnn + connection: + - keep-alive + content-type: + - application/json + method: POST + uri: https://my-deployment-c6095a.ent.us-central1.gcp.cloud.es.io/api/as/v0/engines/elastic-docs/elasticsearch/_search?sort=_score + response: + body: + string: '{"took":0,"timed_out":false,"_shards":{"total":1,"successful":1,"skipped":0,"failed":0},"hits":{"total":{"value":0,"relation":"eq"},"max_score":null,"hits":[]}}' + headers: + Cache-Control: + - max-age=0, private, must-revalidate + Content-Length: + - '160' + Content-Type: + - application/json;charset=utf-8 + Date: + - Mon, 08 Aug 2022 19:44:31 GMT + Etag: + - W/"95041d5366989a0ed1304624d63355eb" + Server: + - Jetty(9.4.43.v20210629) + Vary: + - Origin + - Accept-Encoding, User-Agent + X-App-Search-Version: + - 8.4.0 + X-Cloud-Request-Id: + - opbW3tOmRSOqGzamTfbLOQ + X-Found-Handling-Cluster: + - c99499ef963b4d11a9312e341f6d32cb + X-Found-Handling-Instance: + - instance-0000000001 + X-Request-Id: + - opbW3tOmRSOqGzamTfbLOQ + X-Runtime: + - '0.084247' + status: + code: 200 + message: OK +version: 1 diff --git a/tests/client/app_search/test_search_es_search.py b/tests/client/app_search/test_search_es_search.py new file mode 100644 index 0000000..5cbfdf6 --- /dev/null +++ b/tests/client/app_search/test_search_es_search.py @@ -0,0 +1,108 @@ +# Licensed to Elasticsearch B.V. under one or more contributor +# license agreements. See the NOTICE file distributed with +# this work for additional information regarding copyright +# ownership. Elasticsearch B.V. licenses this file to you under +# the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. + +import pytest +from elastic_transport.client_utils import DEFAULT + +from elastic_enterprise_search import AppSearch +from tests.conftest import DummyNode + + +def test_mock_request(): + client = AppSearch(node_class=DummyNode, meta_header=False) + client.search_es_search( + engine_name="test", + params={"key": "val"}, + body={"k": ["v", 2]}, + analytics_query="analytics-query", + ) + + calls = client.transport.node_pool.get().calls + assert len(calls) == 1 + assert calls[-1][1].pop("request_timeout") is DEFAULT + assert calls[-1] == ( + ( + "POST", + "/api/as/v0/engines/test/elasticsearch/_search?key=val", + ), + { + "body": b'{"k":["v",2]}', + "headers": { + "accept": "application/json", + "content-type": "application/json", + "x-enterprise-search-analytics": "analytics-query", + }, + }, + ) + + +@pytest.mark.parametrize("analytics_tags", ["a,b", ["a", "b"]]) +def test_analytics_tags(analytics_tags): + client = AppSearch(node_class=DummyNode, meta_header=False) + client.options(headers={"Extra": "value"}).search_es_search( + engine_name="test", analytics_tags=analytics_tags + ) + + calls = client.transport.node_pool.get().calls + assert len(calls) == 1 + assert calls[-1][1].pop("request_timeout") is DEFAULT + assert calls[-1] == ( + ( + "POST", + "/api/as/v0/engines/test/elasticsearch/_search", + ), + { + "body": None, + "headers": { + "extra": "value", + "accept": "application/json", + "content-type": "application/json", + "x-enterprise-search-analytics-tags": "a,b", + }, + }, + ) + + +@pytest.mark.parametrize("param_value", [object(), 1, 2.0, (), [3]]) +def test_search_es_search_params_type_error(param_value): + client = AppSearch(node_class=DummyNode) + + with pytest.raises(TypeError) as e: + client.search_es_search( + engine_name="test", + params={"key": param_value}, + ) + assert str(e.value) == "Values for 'params' parameter must be of type 'str'" + + +@pytest.mark.vcr() +def test_search_es_search(app_search): + resp = app_search.search_es_search( + engine_name="elastic-docs", + params={"sort": "_score"}, + body={"query": {"match": {"*": "client"}}}, + ) + assert resp.body == { + "took": 0, + "timed_out": False, + "_shards": {"total": 1, "successful": 1, "skipped": 0, "failed": 0}, + "hits": { + "total": {"value": 0, "relation": "eq"}, + "max_score": None, + "hits": [], + }, + }