* Fixed issue 769:Collapse not preserved when chaining a search instance Signed-off-by: vatsal <[email protected]> * Fixed issue 769:Collapse not preserved when chaining a search instance Signed-off-by: vatsal <[email protected]> * Fixed issue 769:Collapse not preserved when chaining a search instance Signed-off-by: vatsal <[email protected]> * Fixed issue 769:Collapse not preserved when chaining a search instance Signed-off-by: vatsal <[email protected]> * Fixed issue 769:Collapse not preserved when chaining a search instance Signed-off-by: vatsal <[email protected]> --------- Signed-off-by: vatsal <[email protected]>
641 lines
18 KiB
Python
641 lines
18 KiB
Python
# 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.
|
|
#
|
|
# 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.
|
|
|
|
from copy import deepcopy
|
|
from typing import Any
|
|
|
|
from pytest import raises
|
|
|
|
from opensearchpy import Document, Q
|
|
from opensearchpy.exceptions import IllegalOperation
|
|
from opensearchpy.helpers import query, search
|
|
|
|
|
|
def test_expand__to_dot_is_respected() -> None:
|
|
s = search.Search().query("match", a__b=42, _expand__to_dot=False)
|
|
|
|
assert {"query": {"match": {"a__b": 42}}} == s.to_dict()
|
|
|
|
|
|
def test_execute_uses_cache() -> None:
|
|
s: Any = search.Search()
|
|
r: Any = object()
|
|
s._response = r
|
|
|
|
assert r is s.execute()
|
|
|
|
|
|
def test_cache_can_be_ignored(mock_client: Any) -> None:
|
|
s: Any = search.Search(using="mock")
|
|
r: Any = object()
|
|
s._response = r
|
|
s.execute(ignore_cache=True)
|
|
|
|
mock_client.search.assert_called_once_with(index=None, body={})
|
|
|
|
|
|
def test_iter_iterates_over_hits() -> None:
|
|
s: Any = search.Search()
|
|
s._response = [1, 2, 3]
|
|
|
|
assert [1, 2, 3] == list(s)
|
|
|
|
|
|
def test_cache_isnt_cloned() -> None:
|
|
s: Any = search.Search()
|
|
s._response = object()
|
|
|
|
assert not hasattr(s._clone(), "_response")
|
|
|
|
|
|
def test_search_starts_with_no_query() -> None:
|
|
s: Any = search.Search()
|
|
|
|
assert s.query._proxied is None
|
|
|
|
|
|
def test_search_query_combines_query() -> None:
|
|
s: Any = search.Search()
|
|
|
|
s2 = s.query("match", f=42)
|
|
assert s2.query._proxied == query.Match(f=42)
|
|
assert s.query._proxied is None
|
|
|
|
s3 = s2.query("match", f=43)
|
|
assert s2.query._proxied == query.Match(f=42)
|
|
assert s3.query._proxied == query.Bool(must=[query.Match(f=42), query.Match(f=43)])
|
|
|
|
|
|
def test_query_can_be_assigned_to() -> None:
|
|
s: Any = search.Search()
|
|
|
|
q = Q("match", title="python")
|
|
s.query = q
|
|
|
|
assert s.query._proxied is q
|
|
|
|
|
|
def test_query_can_be_wrapped() -> None:
|
|
s: Any = search.Search().query("match", title="python")
|
|
|
|
s.query = Q("function_score", query=s.query, field_value_factor={"field": "rating"})
|
|
|
|
assert {
|
|
"query": {
|
|
"function_score": {
|
|
"functions": [{"field_value_factor": {"field": "rating"}}],
|
|
"query": {"match": {"title": "python"}},
|
|
}
|
|
}
|
|
} == s.to_dict()
|
|
|
|
|
|
def test_using() -> None:
|
|
o: Any = object()
|
|
o2: Any = object()
|
|
s: Any = search.Search(using=o)
|
|
assert s._using is o
|
|
s2 = s.using(o2)
|
|
assert s._using is o
|
|
assert s2._using is o2
|
|
|
|
|
|
def test_methods_are_proxied_to_the_query() -> None:
|
|
s: Any = search.Search().query("match_all")
|
|
|
|
assert s.query.to_dict() == {"match_all": {}}
|
|
|
|
|
|
def test_query_always_returns_search() -> None:
|
|
s: Any = search.Search()
|
|
|
|
assert isinstance(s.query("match", f=42), search.Search)
|
|
|
|
|
|
def test_source_copied_on_clone() -> None:
|
|
s: Any = search.Search().source(False)
|
|
assert s._clone()._source == s._source
|
|
assert s._clone()._source is False
|
|
|
|
s2: Any = search.Search().source([])
|
|
assert s2._clone()._source == s2._source
|
|
assert s2._source == []
|
|
|
|
s3: Any = search.Search().source(["some", "fields"])
|
|
assert s3._clone()._source == s3._source
|
|
assert s3._clone()._source == ["some", "fields"]
|
|
|
|
|
|
def test_copy_clones() -> None:
|
|
from copy import copy
|
|
|
|
s1: Any = search.Search().source(["some", "fields"])
|
|
s2: Any = copy(s1)
|
|
|
|
assert s1 == s2
|
|
assert s1 is not s2
|
|
|
|
|
|
def test_aggs_allow_two_metric() -> None:
|
|
s: Any = search.Search()
|
|
|
|
s.aggs.metric("a", "max", field="a").metric("b", "max", field="b")
|
|
|
|
assert s.to_dict() == {
|
|
"aggs": {"a": {"max": {"field": "a"}}, "b": {"max": {"field": "b"}}}
|
|
}
|
|
|
|
|
|
def test_aggs_get_copied_on_change() -> None:
|
|
s: Any = search.Search().query("match_all")
|
|
s.aggs.bucket("per_tag", "terms", field="f").metric(
|
|
"max_score", "max", field="score"
|
|
)
|
|
|
|
s2 = s.query("match_all")
|
|
s2.aggs.bucket("per_month", "date_histogram", field="date", interval="month")
|
|
s3 = s2.query("match_all")
|
|
s3.aggs["per_month"].metric("max_score", "max", field="score")
|
|
s4 = s3._clone()
|
|
s4.aggs.metric("max_score", "max", field="score")
|
|
|
|
d: Any = {
|
|
"query": {"match_all": {}},
|
|
"aggs": {
|
|
"per_tag": {
|
|
"terms": {"field": "f"},
|
|
"aggs": {"max_score": {"max": {"field": "score"}}},
|
|
}
|
|
},
|
|
}
|
|
|
|
assert d == s.to_dict()
|
|
d["aggs"]["per_month"] = {"date_histogram": {"field": "date", "interval": "month"}}
|
|
assert d == s2.to_dict()
|
|
d["aggs"]["per_month"]["aggs"] = {"max_score": {"max": {"field": "score"}}}
|
|
assert d == s3.to_dict()
|
|
d["aggs"]["max_score"] = {"max": {"field": "score"}}
|
|
assert d == s4.to_dict()
|
|
|
|
|
|
def test_search_index() -> None:
|
|
s = search.Search(index="i")
|
|
assert s._index == ["i"]
|
|
s = s.index("i2")
|
|
assert s._index == ["i", "i2"]
|
|
s = s.index("i3")
|
|
assert s._index == ["i", "i2", "i3"]
|
|
s = s.index()
|
|
assert s._index is None
|
|
s = search.Search(index=("i", "i2"))
|
|
assert s._index == ["i", "i2"]
|
|
s = search.Search(index=["i", "i2"])
|
|
assert s._index == ["i", "i2"]
|
|
s = search.Search()
|
|
s = s.index("i", "i2")
|
|
assert s._index == ["i", "i2"]
|
|
s2 = s.index("i3")
|
|
assert s._index == ["i", "i2"]
|
|
assert s2._index == ["i", "i2", "i3"]
|
|
s = search.Search()
|
|
s = s.index(["i", "i2"], "i3")
|
|
assert s._index == ["i", "i2", "i3"]
|
|
s2 = s.index("i4")
|
|
assert s._index == ["i", "i2", "i3"]
|
|
assert s2._index == ["i", "i2", "i3", "i4"]
|
|
s2 = s.index(["i4"])
|
|
assert s2._index == ["i", "i2", "i3", "i4"]
|
|
s2 = s.index(("i4", "i5"))
|
|
assert s2._index == ["i", "i2", "i3", "i4", "i5"]
|
|
|
|
|
|
def test_doc_type_document_class() -> None:
|
|
class MyDocument(Document):
|
|
pass
|
|
|
|
s = search.Search(doc_type=MyDocument)
|
|
assert s._doc_type == [MyDocument]
|
|
assert s._doc_type_map == {}
|
|
|
|
s = search.Search().doc_type(MyDocument)
|
|
assert s._doc_type == [MyDocument]
|
|
assert s._doc_type_map == {}
|
|
|
|
|
|
def test_sort() -> None:
|
|
s = search.Search()
|
|
s = s.sort("fielda", "-fieldb")
|
|
|
|
assert ["fielda", {"fieldb": {"order": "desc"}}] == s._sort
|
|
assert {"sort": ["fielda", {"fieldb": {"order": "desc"}}]} == s.to_dict()
|
|
|
|
s = s.sort()
|
|
assert [] == s._sort
|
|
assert search.Search().to_dict() == s.to_dict()
|
|
|
|
|
|
def test_sort_by_score() -> None:
|
|
s = search.Search()
|
|
s = s.sort("_score")
|
|
assert {"sort": ["_score"]} == s.to_dict()
|
|
|
|
s = search.Search()
|
|
with raises(IllegalOperation):
|
|
s.sort("-_score")
|
|
|
|
|
|
def test_collapse() -> None:
|
|
s = search.Search()
|
|
|
|
inner_hits = {"name": "most_recent", "size": 5, "sort": [{"@timestamp": "desc"}]}
|
|
s = s.collapse(
|
|
field="user.id", inner_hits=inner_hits, max_concurrent_group_searches=4
|
|
)
|
|
|
|
assert {
|
|
"field": "user.id",
|
|
"inner_hits": {
|
|
"name": "most_recent",
|
|
"size": 5,
|
|
"sort": [{"@timestamp": "desc"}],
|
|
},
|
|
"max_concurrent_group_searches": 4,
|
|
} == s._collapse
|
|
assert {
|
|
"collapse": {
|
|
"field": "user.id",
|
|
"inner_hits": {
|
|
"name": "most_recent",
|
|
"size": 5,
|
|
"sort": [{"@timestamp": "desc"}],
|
|
},
|
|
"max_concurrent_group_searches": 4,
|
|
}
|
|
} == s.to_dict()
|
|
|
|
s = s.collapse()
|
|
assert {} == s._collapse
|
|
assert search.Search().to_dict() == s.to_dict()
|
|
|
|
|
|
def test_slice() -> None:
|
|
s = search.Search()
|
|
assert {"from": 3, "size": 7} == s[3:10].to_dict()
|
|
assert {"from": 0, "size": 5} == s[:5].to_dict()
|
|
assert {"from": 3, "size": 10} == s[3:].to_dict()
|
|
assert {"from": 0, "size": 0} == s[0:0].to_dict()
|
|
assert {"from": 20, "size": 0} == s[20:0].to_dict()
|
|
|
|
|
|
def test_index() -> None:
|
|
s = search.Search()
|
|
assert {"from": 3, "size": 1} == s[3].to_dict()
|
|
|
|
|
|
def test_search_to_dict() -> None:
|
|
s = search.Search()
|
|
assert {} == s.to_dict()
|
|
|
|
s = s.query("match", f=42)
|
|
assert {"query": {"match": {"f": 42}}} == s.to_dict()
|
|
|
|
assert {"query": {"match": {"f": 42}}, "size": 10} == s.to_dict(size=10)
|
|
|
|
s.aggs.bucket("per_tag", "terms", field="f").metric(
|
|
"max_score", "max", field="score"
|
|
)
|
|
d = {
|
|
"aggs": {
|
|
"per_tag": {
|
|
"terms": {"field": "f"},
|
|
"aggs": {"max_score": {"max": {"field": "score"}}},
|
|
}
|
|
},
|
|
"query": {"match": {"f": 42}},
|
|
}
|
|
assert d == s.to_dict()
|
|
|
|
s = search.Search(extra={"size": 5})
|
|
assert {"size": 5} == s.to_dict()
|
|
s = s.extra(from_=42)
|
|
assert {"size": 5, "from": 42} == s.to_dict()
|
|
|
|
|
|
def test_complex_example() -> None:
|
|
s = search.Search()
|
|
s = (
|
|
s.query("match", title="python")
|
|
.query(~Q("match", title="ruby"))
|
|
.filter(Q("term", category="meetup") | Q("term", category="conference"))
|
|
.post_filter("terms", tags=["prague", "czech"])
|
|
.script_fields(more_attendees="doc['attendees'].value + 42")
|
|
)
|
|
|
|
s.aggs.bucket("per_country", "terms", field="country").metric(
|
|
"avg_attendees", "avg", field="attendees"
|
|
)
|
|
|
|
s.query.minimum_should_match = 2
|
|
|
|
s = s.highlight_options(order="score").highlight("title", "body", fragment_size=50)
|
|
|
|
assert {
|
|
"query": {
|
|
"bool": {
|
|
"filter": [
|
|
{
|
|
"bool": {
|
|
"should": [
|
|
{"term": {"category": "meetup"}},
|
|
{"term": {"category": "conference"}},
|
|
]
|
|
}
|
|
}
|
|
],
|
|
"must": [{"match": {"title": "python"}}],
|
|
"must_not": [{"match": {"title": "ruby"}}],
|
|
"minimum_should_match": 2,
|
|
}
|
|
},
|
|
"post_filter": {"terms": {"tags": ["prague", "czech"]}},
|
|
"aggs": {
|
|
"per_country": {
|
|
"terms": {"field": "country"},
|
|
"aggs": {"avg_attendees": {"avg": {"field": "attendees"}}},
|
|
}
|
|
},
|
|
"highlight": {
|
|
"order": "score",
|
|
"fields": {"title": {"fragment_size": 50}, "body": {"fragment_size": 50}},
|
|
},
|
|
"script_fields": {"more_attendees": {"script": "doc['attendees'].value + 42"}},
|
|
} == s.to_dict()
|
|
|
|
|
|
def test_reverse() -> None:
|
|
d = {
|
|
"query": {
|
|
"filtered": {
|
|
"filter": {
|
|
"bool": {
|
|
"should": [
|
|
{"term": {"category": "meetup"}},
|
|
{"term": {"category": "conference"}},
|
|
]
|
|
}
|
|
},
|
|
"query": {
|
|
"bool": {
|
|
"must": [{"match": {"title": "python"}}],
|
|
"must_not": [{"match": {"title": "ruby"}}],
|
|
"minimum_should_match": 2,
|
|
}
|
|
},
|
|
}
|
|
},
|
|
"post_filter": {"bool": {"must": [{"terms": {"tags": ["prague", "czech"]}}]}},
|
|
"aggs": {
|
|
"per_country": {
|
|
"terms": {"field": "country"},
|
|
"aggs": {"avg_attendees": {"avg": {"field": "attendees"}}},
|
|
}
|
|
},
|
|
"sort": ["title", {"category": {"order": "desc"}}, "_score"],
|
|
"size": 5,
|
|
"highlight": {"order": "score", "fields": {"title": {"fragment_size": 50}}},
|
|
"suggest": {
|
|
"my-title-suggestions-1": {
|
|
"text": "devloping distibutd saerch engies",
|
|
"term": {"size": 3, "field": "title"},
|
|
}
|
|
},
|
|
"script_fields": {"more_attendees": {"script": "doc['attendees'].value + 42"}},
|
|
}
|
|
|
|
d2 = deepcopy(d)
|
|
|
|
s = search.Search.from_dict(d)
|
|
|
|
# make sure we haven't modified anything in place
|
|
assert d == d2
|
|
assert {"size": 5} == s._extra
|
|
assert d == s.to_dict()
|
|
|
|
|
|
def test_from_dict_doesnt_need_query() -> None:
|
|
s = search.Search.from_dict({"size": 5})
|
|
|
|
assert {"size": 5} == s.to_dict()
|
|
|
|
|
|
def test_params_being_passed_to_search(mock_client: Any) -> None:
|
|
s = search.Search(using="mock")
|
|
s = s.params(routing="42")
|
|
s.execute()
|
|
|
|
mock_client.search.assert_called_once_with(index=None, body={}, routing="42")
|
|
|
|
|
|
def test_source() -> None:
|
|
assert {} == search.Search().source().to_dict()
|
|
|
|
assert {
|
|
"_source": {"includes": ["foo.bar.*"], "excludes": ["foo.one"]}
|
|
} == search.Search().source(includes=["foo.bar.*"], excludes=["foo.one"]).to_dict()
|
|
|
|
assert {"_source": False} == search.Search().source(False).to_dict()
|
|
|
|
assert {"_source": ["f1", "f2"]} == search.Search().source(
|
|
includes=["foo.bar.*"], excludes=["foo.one"]
|
|
).source(["f1", "f2"]).to_dict()
|
|
|
|
|
|
def test_source_on_clone() -> None:
|
|
assert {
|
|
"_source": {"includes": ["foo.bar.*"], "excludes": ["foo.one"]},
|
|
"query": {"bool": {"filter": [{"term": {"title": "python"}}]}},
|
|
} == search.Search().source(includes=["foo.bar.*"]).source(
|
|
excludes=["foo.one"]
|
|
).filter(
|
|
"term", title="python"
|
|
).to_dict()
|
|
assert {
|
|
"_source": False,
|
|
"query": {"bool": {"filter": [{"term": {"title": "python"}}]}},
|
|
} == search.Search().source(False).filter("term", title="python").to_dict()
|
|
|
|
|
|
def test_source_on_clear() -> None:
|
|
assert (
|
|
{}
|
|
== search.Search()
|
|
.source(includes=["foo.bar.*"])
|
|
.source(includes=None, excludes=None)
|
|
.to_dict()
|
|
)
|
|
|
|
|
|
def test_suggest_accepts_global_text() -> None:
|
|
s = search.Search.from_dict(
|
|
{
|
|
"suggest": {
|
|
"text": "the amsterdma meetpu",
|
|
"my-suggest-1": {"term": {"field": "title"}},
|
|
"my-suggest-2": {"text": "other", "term": {"field": "body"}},
|
|
}
|
|
}
|
|
)
|
|
|
|
assert {
|
|
"suggest": {
|
|
"my-suggest-1": {
|
|
"term": {"field": "title"},
|
|
"text": "the amsterdma meetpu",
|
|
},
|
|
"my-suggest-2": {"term": {"field": "body"}, "text": "other"},
|
|
}
|
|
} == s.to_dict()
|
|
|
|
|
|
def test_suggest() -> None:
|
|
s = search.Search()
|
|
s = s.suggest("my_suggestion", "pyhton", term={"field": "title"})
|
|
|
|
assert {
|
|
"suggest": {"my_suggestion": {"term": {"field": "title"}, "text": "pyhton"}}
|
|
} == s.to_dict()
|
|
|
|
|
|
def test_exclude() -> None:
|
|
s = search.Search()
|
|
s = s.exclude("match", title="python")
|
|
|
|
assert {
|
|
"query": {
|
|
"bool": {
|
|
"filter": [{"bool": {"must_not": [{"match": {"title": "python"}}]}}]
|
|
}
|
|
}
|
|
} == s.to_dict()
|
|
|
|
|
|
def test_delete_by_query(mock_client: Any) -> None:
|
|
s = search.Search(using="mock").query("match", lang="java")
|
|
s.delete()
|
|
|
|
mock_client.delete_by_query.assert_called_once_with(
|
|
index=None, body={"query": {"match": {"lang": "java"}}}
|
|
)
|
|
|
|
|
|
def test_update_from_dict() -> None:
|
|
s = search.Search()
|
|
s.update_from_dict({"indices_boost": [{"important-documents": 2}]})
|
|
s.update_from_dict({"_source": ["id", "name"]})
|
|
|
|
assert {
|
|
"indices_boost": [{"important-documents": 2}],
|
|
"_source": ["id", "name"],
|
|
} == s.to_dict()
|
|
|
|
|
|
def test_rescore_query_to_dict() -> None:
|
|
s = search.Search(index="index-name")
|
|
|
|
positive_query = Q(
|
|
"function_score",
|
|
query=Q("term", tags="a"),
|
|
script_score={"script": "_score * 1"},
|
|
)
|
|
|
|
negative_query = Q(
|
|
"function_score",
|
|
query=Q("term", tags="b"),
|
|
script_score={"script": "_score * -100"},
|
|
)
|
|
|
|
s = s.query(positive_query)
|
|
s = s.extra(
|
|
rescore={"window_size": 100, "query": {"rescore_query": negative_query}}
|
|
)
|
|
assert s.to_dict() == {
|
|
"query": {
|
|
"function_score": {
|
|
"query": {"term": {"tags": "a"}},
|
|
"functions": [{"script_score": {"script": "_score * 1"}}],
|
|
}
|
|
},
|
|
"rescore": {
|
|
"window_size": 100,
|
|
"query": {
|
|
"rescore_query": {
|
|
"function_score": {
|
|
"query": {"term": {"tags": "b"}},
|
|
"functions": [{"script_score": {"script": "_score * -100"}}],
|
|
}
|
|
}
|
|
},
|
|
},
|
|
}
|
|
|
|
assert s.to_dict(
|
|
rescore={"window_size": 10, "query": {"rescore_query": positive_query}}
|
|
) == {
|
|
"query": {
|
|
"function_score": {
|
|
"query": {"term": {"tags": "a"}},
|
|
"functions": [{"script_score": {"script": "_score * 1"}}],
|
|
}
|
|
},
|
|
"rescore": {
|
|
"window_size": 10,
|
|
"query": {
|
|
"rescore_query": {
|
|
"function_score": {
|
|
"query": {"term": {"tags": "a"}},
|
|
"functions": [{"script_score": {"script": "_score * 1"}}],
|
|
}
|
|
}
|
|
},
|
|
},
|
|
}
|
|
|
|
|
|
def test_collapse_chaining() -> None:
|
|
s = search.Search(index="index_name")
|
|
s = s.filter("term", color="red")
|
|
s = s.collapse(field="category")
|
|
s = s.filter("term", brand="something")
|
|
|
|
assert {
|
|
"query": {
|
|
"bool": {
|
|
"filter": [{"term": {"color": "red"}}, {"term": {"brand": "something"}}]
|
|
}
|
|
},
|
|
"collapse": {"field": "category"},
|
|
} == s.to_dict()
|