diff --git a/solrorbit/conversion/query.py b/solrorbit/conversion/query.py index 5b4b2766..432b22b0 100644 --- a/solrorbit/conversion/query.py +++ b/solrorbit/conversion/query.py @@ -181,13 +181,24 @@ def _translate_query_node(node: dict, fq_list: list = None) -> str: if "range" in node: for field, bounds in node["range"].items(): field = normalize_field_name(field) - lo = bounds.get("gte", bounds.get("gt", "*")) - hi = bounds.get("lte", bounds.get("lt", "*")) + # gt/lt are exclusive; Solr spells that with a curly bracket. + if "gte" in bounds: + lo, lo_bracket = bounds["gte"], "[" + elif "gt" in bounds: + lo, lo_bracket = bounds["gt"], "{" + else: + lo, lo_bracket = "*", "[" + if "lte" in bounds: + hi, hi_bracket = bounds["lte"], "]" + elif "lt" in bounds: + hi, hi_bracket = bounds["lt"], "}" + else: + hi, hi_bracket = "*", "]" # Convert dates if format is specified (common for date fields) os_format = bounds.get("format") lo = _convert_date_to_solr_format(lo, os_format) hi = _convert_date_to_solr_format(hi, os_format) - return f"{field}:[{lo} TO {hi}]" + return f"{field}:{lo_bracket}{lo} TO {hi}{hi_bracket}" if "exists" in node: field = node["exists"].get("field", "*") diff --git a/tests/unit/solr/test_workload_converter.py b/tests/unit/solr/test_workload_converter.py index 1927a4cf..423dc9c7 100644 --- a/tests/unit/solr/test_workload_converter.py +++ b/tests/unit/solr/test_workload_converter.py @@ -209,11 +209,24 @@ def test_term_query(self): result = translate_to_solr_json_dsl(body) self.assertIn("vendor_id", result["query"]) + def _range(self, bounds): + return translate_to_solr_json_dsl( + {"query": {"range": {"fare_amount": bounds}}})["query"] + def test_range_query(self): - body = {"query": {"range": {"fare_amount": {"gte": 5, "lte": 100}}}} - result = translate_to_solr_json_dsl(body) - self.assertIn("fare_amount", result["query"]) - self.assertIn("TO", result["query"]) + self.assertEqual("fare_amount:[5 TO 100]", self._range({"gte": 5, "lte": 100})) + + def test_exclusive_bounds_use_curly_brackets(self): + self.assertEqual("fare_amount:[5 TO 100}", self._range({"gte": 5, "lt": 100})) + self.assertEqual("fare_amount:{5 TO 100]", self._range({"gt": 5, "lte": 100})) + self.assertEqual("fare_amount:{5 TO 100}", self._range({"gt": 5, "lt": 100})) + + def test_a_missing_bound_is_open_and_inclusive(self): + # An absent bound is `*`, which has nothing to exclude. + self.assertEqual("fare_amount:[5 TO *]", self._range({"gte": 5})) + self.assertEqual("fare_amount:{5 TO *]", self._range({"gt": 5})) + self.assertEqual("fare_amount:[* TO 100}", self._range({"lt": 100})) + self.assertEqual("fare_amount:[* TO 100]", self._range({"lte": 100})) def test_bool_with_filter_goes_to_fq(self): body = {