Skip to content

Randomize range bounds in Solr's string queries - #76

Open
serhiy-bzhezytskyy wants to merge 1 commit into
apache:mainfrom
serhiy-bzhezytskyy:randomize-solr-string-queries
Open

Randomize range bounds in Solr's string queries#76
serhiy-bzhezytskyy wants to merge 1 commit into
apache:mainfrom
serhiy-bzhezytskyy:randomize-solr-string-queries

Conversation

@serhiy-bzhezytskyy

Copy link
Copy Markdown
Contributor

Description

--randomization-enabled had no effect on a Solr workload: every client sent the one query written in operations/*.json for the whole run, and the log said randomization was on.

extract_fields_and_paths walks params["body"]["query"] as an object tree looking for a range key, which is the OpenSearch query DSL. Solr's JSON DSL states the query as a string, so the walk reached a leaf immediately and returned no fields; with no fields no standard value source is called and set_range never runs.

On main at f088380, the same nyc_taxis range operation in both shapes:

Solr JSON DSL        -> []
OpenSearch query DSL -> [('total_amount', ['bool', 'filter', 'range'])]

get_randomized_values -> total_amount:[5 TO 15}     # the value source returned {"gte": 3, "lte": 7}

This finds range terms in the query string and in each entry of the filter list, since nyc_taxis' distance_amount_facet states its range there, and substitutes only the bounds:

total_amount:[5 TO 15}                          ->  total_amount:[3 TO 7}
["trip_distance:[0 TO 50}"]                     ->  ["trip_distance:[3 TO 7}"]

The brackets the query already states are kept, so an exclusive bound stays exclusive — the same way the object path keeps whichever of lt/lte it found.

Three deliberate details:

A term that leaves a bound open is left alone. * is Solr's spelling of a bound the query does not state, and the object path does not randomize a one-sided range either: check_one_of_each_name_present requires one name from each of gte/gt and lte/lt, so such a field is never collected. total_amount:[5 TO *] is therefore untouched.

The skip is applied in the substitution as well as the extraction. A term visited by the regexp but absent from fields_and_paths would otherwise consume the value meant for the next term and write it to the wrong field: total_amount:[* TO 15} AND trip_distance:[1 TO 9} with one value gave total_amount:[3 TO 7} AND trip_distance:[1 TO 9}.

Bounds are read by name, not with a default. A value source that returns unexpected keys raises KeyError rather than silently leaving the query as written — the same failure the object path gives, and the same silent no-op this PR removes.

Measured

Not only unit tests. 450 randomized queries over the five nyc_taxis operations that register a value source, across six seeds, against Solr 10.0.0 with 300,649 documents: all HTTP 200, and every matched document lands in a facet bucket — sum(bucket counts) == numFound throughout, 0 violations. The only queries that match nothing are trip_distance:[N TO N}, which upstream produces just as often from gte: 5, lt: 5.

I also checked the wider blast radius, since this makes a previously dead path live: an operation with a two-sided range term but no registered value source now fails a run with SystemSetupError from params.generate_standard_values_if_absent. Scanning every operations file in solr-orbit-workloads: 5 range terms, all 5 registered, 0 missing; geonames has none. So no existing workload starts failing.

⚠️ It does make two latent defects in solr-orbit-workloads nyc_taxis/workload.py reachable — random_money_values passes a float to random.randrange, which raises TypeError on every Python this project supports, so the range operation dies immediately. I have those ready and will open them against solr-orbit-workloads alongside this; they should land together with it.

Issues Resolved

Fixes #75

Testing

  • New functionality includes testing

Six tests in tests/workload/loader_test.py: range terms found in a query string and in the filter list, the brackets preserved through set_range, the whole get_randomized_values path on a string query, an open bound left alone, a skipped term not taking another term's value, and a mis-keyed value source raising.

Each of the three details above was mutation-checked — reverting it fails exactly the test that covers it:

reverted test that fails
guard in extract_solr_range_terms ..._leaves_a_bound_open_is_not_randomized: [('total_amount', ('query',))] != []
guard inside replace() ..._does_not_take_another_terms_value: value written to the wrong field
direct indexing → .get(name, old) ..._omits_a_bound_is_not_silently_ignored: KeyError not raised

ruff check clean; full unit suite 1116 passed / 5 skipped. make it was not run.

Query randomization looked for a range by walking the query as an object
tree in search of a "range" key. Solr's JSON DSL states the query as a
string, for example "total_amount:[5 TO 15}", so the walk reached a leaf
node immediately, returned no fields, and every randomized value was
discarded. The log still said "Query randomization is enabled", and every
client sent the one query written in operations/default.json.

Find range terms in the query string, and in each entry of the filter
list, since nyc_taxis' distance_amount_facet states its range there.
Substitute only the bounds and keep the brackets the query already uses,
so an exclusive bound stays exclusive - the same way the object-based
path keeps whichever of lt/lte it found.

A term that leaves a bound open, "total_amount:[5 TO *]", is left alone.
"*" is Solr's spelling of a bound the query does not state, and the
object-based path does not randomize a one-sided range either:
check_one_of_each_name_present requires one name from each of gte/gt and
lte/lt, so such a field is never collected. The skip is applied in the
substitution as well as in the extraction, because a term visited by the
regexp but absent from fields_and_paths would otherwise consume the value
meant for the next term and write it to the wrong field.

The bounds are read from the value source by name rather than with a
default, so a source that returns unexpected keys raises KeyError instead
of silently leaving the query as it was written - the same failure the
object-based path gives, and the same silent no-op this commit removes.

Checked against a live Solr, not only in unit tests: 450 randomized
queries over the five nyc_taxis operations that register a value source,
across six seeds, all returned HTTP 200, and every matched document
landed in a facet bucket - sum(bucket counts) == numFound throughout.
The only queries that matched nothing are trip_distance:[N TO N}, which
upstream produces just as often from gte:5, lt:5.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@serhiy-bzhezytskyy

Copy link
Copy Markdown
Contributor Author

For sequencing: the two workloads defects this makes reachable are apache/solr-orbit-workloads#20. That one is inert without this PR, and this PR breaks --randomization-enabled on nyc_taxis without that one, so they want to go together.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Bug]: query randomization has no effect on Solr workloads — the query is a string, not an object tree

1 participant