Collapse repeated wildcard asterisks#16382
Conversation
|
Could you add a test for "*?" x 5000 ? I am just curious what would be the behavior. |
uschindler
left a comment
There was a problem hiding this comment.
I am not oppsed to that. We should maybe have another variant of the same test where we have also an escaped wildcard and some other characters. Basically can't we compare the automatons produced for equality instead of only counting states?
Otherwise OK!
|
Just a nitpick: You may also convert the switch statement with the old-style break and possible fallthrough to the modern form (introduced around Java 17). Whenever I toiuch code using old switch statements, I convert them! |
Convert the wildcard switch to an arrow switch (dropping the fallthrough and its @SuppressWarnings), per review. Compare automaton language equality (AutomatonTestUtil.sameLanguage) instead of only state/transition counts, add a variant mixing an escaped \* with literals and '?', and add a '*?' x5000 case asserting it stays linearly sized (it does not collapse but must not explode either).
|
Pushed 9dc9ac5. @uschindler — converted the @amalatlas — added the |
|
Great, thanks! |
|
Would you add a changes entry. We have not yet decided if that can be backported to 10.x. I know for 11.x we have the optimizations regarding determinization, so with 10.x we may need some additional changes the to the test. Can you figure that out? If we shoudl backport it, please add it to the 10.x changes list. |
rmuir
left a comment
There was a problem hiding this comment.
This change requires the entire wildcard parsing to be focused on handling an abusive-case that isn't a real use-case at all
|
Added the CHANGES entry under 11.0.0 → Optimizations (276e3c7). On the backport: it's doable. The collapse itself ports cleanly, and it's arguably more valuable on 10.x since Otherwise I think this is good to go from my side. |
|
@rmuir fair point that *-spam isn't a real use case, and I don't want to add complexity for garbage input. But I think this one's worth it because it costs real queries nothing: the change is a single boolean flag in the parse loop, so for any pattern without consecutive unescaped * it builds the exact same automaton as before — identical work. The only path that changes is the degenerate one, where it goes from ~50M transitions (an OOM footgun, and a TooComplexToDeterminize crash on 10.x) to 1. It's really just collapsing ** → *, which are equivalent anyway. So: zero cost for everyone, removes a crash for the unlucky. Happy to make it leaner if you see a tidier way — but would that change your mind? |
|
I think there is a use case. Many of our query parsers by default support wildcards, also the default Solr one (e.g., dismax). If it uses way too much memory for degenerate cases, it should be handled in the most ideal case. The code introduced here is just the default pattern used for removing duplicates in such parsers. Yes, it is an additional state variable. Maybe it can be similified a bit so you don't have the boolean setter everywhere. If you have an idea, apply it to the patch. Otherwise for me it is fine! |
|
P.S.: Thanks for changing the switch statement to a more modern arrow-switch without breaks! This alone is worth the change! |
danmuzi
left a comment
There was a problem hiding this comment.
Thanks for your contribution :)
Just one small nit.
…y.java Co-authored-by: Namgyu Kim <kng0828@gmail.com>
|
I am thinking about a possible better alternative inside the automation API. This would also improve regex queries with multiple Can't we modify the addition of anyString inside the automation, so it only adds it if there's not one before? I am just thinking. If that's possible, we could catch more cases. |
|
@uschindler I like it — pushed into the automaton layer that'd also catch regex One wrinkle: dropping a segment is only safe when it's the total automaton ( Want me to try that here, or keep it separate so this one can land? |
|
I think you can do it here. You can revert the branch if it won't work. |
Replace the lastWasWildcardString boolean (which had to be set in every
switch branch) with a check on whether the previous segment is already a
total ("any string") automaton via Operations.isTotal. Behavior is
unchanged — consecutive '*' still collapse to a single any-string — and
the other branches become plain one-liners.
|
@uschindler done — dropped the boolean in the wildcard loop; the On extending it to regex: both wildcard and regex actually flatten into the same |
|
Cool. I will check later, but the tests confirm it works. |
Description
Fixes #16134.
WildcardQuery.toAutomatoncreated a separateAutomata.makeAnyString()automaton for every unescaped*. Consecutive*wildcards match the same language as a single*, but the repeated form madeOperations.concatenatebuild a much larger automaton.This collapses consecutive unescaped
*tokens while preserving escaped\*as a literal. It also adds a regression test that checks repeated asterisks produce the same automaton shape as a single asterisk.Before the change,
WildcardQuery.toAutomaton(new Term("field", "*".repeat(10000)))built50,005,000transitions locally. After the change, the same input builds1transition.Validation:
./gradlew tidy./gradlew :lucene:core:test --tests TestWildcardQuery -Dtests.seed=5EEC297244EB01E6 -Dtests.asserts=true -Dtests.file.encoding=UTF-8./gradlew :lucene:core:check -Dtests.seed=5EEC297244EB01E6 -Dtests.asserts=true -Dtests.file.encoding=UTF-8