From b8e843ad70f7410a31cea97e70a370d8e938fa1d Mon Sep 17 00:00:00 2001 From: 72004 Date: Tue, 10 Nov 2026 00:15:57 +0500 Subject: [PATCH 1/2] Add tests for bool clauses nested under must_not and should --- tests/unit/solr/test_workload_converter.py | 29 ++++++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/tests/unit/solr/test_workload_converter.py b/tests/unit/solr/test_workload_converter.py index 29640f7e..a956ee14 100644 --- a/tests/unit/solr/test_workload_converter.py +++ b/tests/unit/solr/test_workload_converter.py @@ -298,6 +298,35 @@ def test_bool_with_filter_goes_to_fq(self): self.assertIn("filter", result) self.assertTrue(len(result["filter"]) > 0) + def test_filter_under_must_not_stays_excluded(self): + # Hoisting this filter to fq would keep only the documents the query excludes. + body = {"query": {"bool": {"must_not": [{"bool": {"filter": [{"term": {"status": "deleted"}}]}}]}}} + result = translate_to_solr_json_dsl(body) + self.assertNotIn("filter", result) + self.assertEqual("-(+(status:deleted))", result["query"]) + + def test_filters_under_should_stay_alternatives(self): + # Hoisting both filters to fq would require red AND blue instead of red OR blue. + body = {"query": {"bool": {"should": [ + {"bool": {"filter": [{"term": {"color": "red"}}]}}, + {"bool": {"filter": [{"term": {"color": "blue"}}]}}, + ]}}} + result = translate_to_solr_json_dsl(body) + self.assertNotIn("filter", result) + self.assertEqual("((+(color:red)) (+(color:blue)))", result["query"]) + + def test_nested_bool_under_should_keeps_its_grouping(self): + # (a AND b) OR c, not a AND b AND c + body = {"query": {"bool": {"should": [ + {"bool": {"must": [{"term": {"a": 1}}, {"term": {"b": 2}}]}}, + {"term": {"c": 3}}, + ]}}} + self.assertEqual("((+(a:1) +(b:2)) c:3)", translate_to_solr_json_dsl(body)["query"]) + + def test_filter_under_must_still_goes_to_fq(self): + body = {"query": {"bool": {"must": [{"bool": {"filter": [{"term": {"color": "red"}}]}}]}}} + self.assertEqual({"query": "*:*", "filter": ["color:red"]}, translate_to_solr_json_dsl(body)) + def test_sort_is_extracted(self): body = {"query": {"match_all": {}}, "sort": [{"fare_amount": "desc"}]} result = translate_to_solr_json_dsl(body) From 9e671d2bc374236af6ba6556c1d4a71409a6b653 Mon Sep 17 00:00:00 2001 From: 72004 Date: Tue, 10 Nov 2026 00:17:52 +0500 Subject: [PATCH 2/2] Keep nested bool clauses scoped when translating OpenSearch queries Filters nested under must_not or should were hoisted to top-level fq, which inverted must_not and turned should alternatives into a conjunction. A nested bool under should was also spliced in without parentheses, so its required clauses became required for the whole query. Filters are now only hoisted under must, and a nested bool under should is grouped. --- solrorbit/conversion/query.py | 20 +++++++++++++------- 1 file changed, 13 insertions(+), 7 deletions(-) diff --git a/solrorbit/conversion/query.py b/solrorbit/conversion/query.py index cf6b16ba..f77e468f 100644 --- a/solrorbit/conversion/query.py +++ b/solrorbit/conversion/query.py @@ -221,13 +221,13 @@ def _translate_query_node(node: dict, fq_list: list = None) -> str: bool_q = node["bool"] parts = [] - def _add_to_q(clauses, prefix): + def _add_to_q(clauses, prefix, nested_fq_list): if not clauses: return if isinstance(clauses, dict): clauses = [clauses] for clause in clauses: - sub = _translate_query_node(clause, fq_list=fq_list) + sub = _translate_query_node(clause, fq_list=nested_fq_list) if sub and sub != "*:*": parts.append(f"{prefix}({sub})") @@ -242,20 +242,26 @@ def _add_to_fq(clauses): if fq_str: fq_list.append(fq_str) - _add_to_q(bool_q.get("must"), "+") - _add_to_q(bool_q.get("must_not"), "-") + # A nested filter may only become a top-level fq where it is required anyway, i.e. under must. + # Under must_not it would be inverted, and under should it would turn alternatives into a conjunction. + _add_to_q(bool_q.get("must"), "+", fq_list) + _add_to_q(bool_q.get("must_not"), "-", None) # bool.filter → Solr fq when fq_list is available; otherwise inline in q if fq_list is not None: _add_to_fq(bool_q.get("filter")) else: - _add_to_q(bool_q.get("filter"), "+") + _add_to_q(bool_q.get("filter"), "+", None) shoulds = bool_q.get("should", []) if isinstance(shoulds, dict): shoulds = [shoulds] - should_parts = [_translate_query_node(s, fq_list=fq_list) for s in shoulds] - should_parts = [s for s in should_parts if s and s != "*:*"] + should_parts = [] + for clause in shoulds: + sub = _translate_query_node(clause, fq_list=None) + if sub and sub != "*:*": + # A nested bool is several clauses; group them so it stays one alternative. + should_parts.append(f"({sub})" if isinstance(clause, dict) and "bool" in clause else sub) if should_parts: parts.append("(" + " ".join(should_parts) + ")")