Drive PPL search command emission from the field's index mapping (#5682) - #5697
Drive PPL search command emission from the field's index mapping (#5682)#5697penghuo wants to merge 2 commits into
Conversation
…pensearch-project#5682) On the Calcite path, `search source=idx name="foo bar*"` against a keyword field returned 0 hits instead of matching the whole-value pattern `foo bar*`. The parser marked whitespace-containing literals as phrases, which emitted `name:"foo bar*"` — inside a Lucene phrase, `*` is a literal character, so it looked for docs containing `*` in the stored value and found none. Route emission per field mapping in SearchLiteral.toQueryString(ExprType): - text-like (text, match_only_text) → quoted phrase (unchanged) - non-text (keyword, etc.) with whitespace + unescaped wildcard → unquoted term with the space escaped, so query_string keeps the value as one whole-value pattern instead of splitting into two clauses - everything else (no whitespace, or phrase without wildcard) → legacy branches (unquoted-with-escapes, quoted phrase) The Calcite RelDataType round trip in CalciteRelNodeVisitor.visitSearch collapses `text` mapping to plain VARCHAR (OpenSearchTypeFactory:208), which erased the text/keyword distinction at the emitter. Read the ExprType map directly from AbstractOpenSearchTable.getFieldTypes() instead; TODO comment marks the follow-up to move this metadata onto a RelDataType/scan annotation once the Calcite rule pipeline is audited. Thread a `Function<String, ExprType>` resolver through the SearchExpression hierarchy (SearchComparison, SearchIn, SearchAnd/Or/Not/Group, SearchLiteral) so SearchLiteral can consult the resolved field's index type at emit time. Tests: 54 new Group1-Group6 tests in CalciteSearchCommandIT covering the full text × keyword × wildcard-placement matrix on a shared fixture, plus a core-level SearchLiteralTest for the emission decision table. Verified with `./gradlew doctest -DignorePrometheus` (85 tests) and `./gradlew -DignorePrometheus :integ-test:integTest` (30m36s, 0 failures). Signed-off-by: Peng Huo <penghuo@gmail.com>
PR Code Suggestions ✨Latest suggestions up to d9d6568
Previous suggestionsSuggestions up to commit 8ad2755
|
|
@vamsimanohar Please help review. |
vamsimanohar
left a comment
There was a problem hiding this comment.
Minor nit: java.util.Map and java.util.HashMap are used fully-qualified inline in visitSearch rather than imported at the top of the file. The rest of the file imports its types — consider moving these to the import block for consistency.
Otherwise LGTM — the decision tree is well-reasoned, test coverage is thorough, and the regression-safe fallback (unknown type → phrase form) is the right call.
Replaces the whitespace heuristic that decided phrase vs. term emission.
`SearchLiteral.isPhrase` was set at parse time as `value.contains(" ")`,
which is a syntactic test standing in for a semantic question: will the
field's analyzer split this value into multiple tokens? Whitespace is a
poor proxy — `foo=bar` and `foo-bar` hold none, yet the standard analyzer
splits both.
The consequence on a text field: the value was emitted unquoted,
query_string kept it as one field-scoped term, the analyzer split it, and
default_operator=OR combined the halves. `body="foo=bar"` therefore matched
any document holding just `foo` or just `bar`.
Emission is now selected by the enclosing field's mapping, read from
AbstractOpenSearchTable.getFieldTypes():
- text / match_only_text: honor the user's quoting. Unquoted passes
through so `*` and `?` stay query_string operators; quoted becomes a
phrase. Exception: a whitespace-free value carrying a wildcard stays
unquoted, because quoting would let the analyzer discard the wildcard
(`foo*` must keep matching `foobar`). That is only safe without
whitespace — with a space, unquoted would split into separate clauses
and the tail would lose its field binding.
- keyword / constant_keyword: quoting is irrelevant, since the analyzer is
a no-op and a quoted phrase resolves to the same single term as a bare
one. Emit whole-value semantics instead — a wildcard pattern when the
value holds an unescaped wildcard, otherwise an exact term. Whitespace
is escaped in the wildcard form so query_string keeps one clause.
- date / numeric / ip / boolean / unresolved: legacy behavior, untouched.
The v2 engine is unaffected. It reaches emission through the no-arg
SearchExpression.toQueryString(), which passes a null-returning resolver
and lands in the legacy branch.
Behavior change, text fields only: a quoted value the analyzer splits is
now a phrase rather than an OR over its tokens. On the test fixture,
`name="foo-bar"` / `"foo/bar"` / `"foo@bar"` go from 7 hits to 4. Wildcard
rows are unchanged. Three examples in docs/user/ppl/cmd/search.md
documented the old over-matching and have been updated.
Tests: Group 7 added to CalciteSearchCommandIT over a dedicated fixture
(foo=bar, foo bar, foo, bar, baz) so the single-token documents that used
to OR-match are asserted absent. SearchLiteralTest covers the three
mapping branches. Verified with CalciteSearchCommandIT, SearchCommandIT
(v2), :core:test, :ppl:test, doctest, and :integ-test:integTest.
Signed-off-by: Peng Huo <penghuo@gmail.com>
|
|
||
| ```ppl | ||
| search severityText="INFO" AND `resource.attributes.service.name`="cart-service" source=otellogs | ||
| search severityText="INFO" AND `resource.attributes.service.name`="cart*" source=otellogs |
There was a problem hiding this comment.
@vamsimanohar current doc seems a bug. please help take a look.
| |------------------------------| | ||
| | Microsoft.Extensions.Hosting | | ||
| +------------------------------+ | ||
| fetched rows / total rows = 2/2 |
There was a problem hiding this comment.
@vamsimanohar current doc seems a bug. please help take a look.
| | instrumentationScope.name | | ||
| |-----------------------------------------------------------------------------| | ||
| | Microsoft.Extensions.Hosting | | ||
| | go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc | |
There was a problem hiding this comment.
@vamsimanohar current doc seems a bug. please help take a look.
Description
Two related fixes to how the
searchcommand builds itsquery_stringfilter, both on the Calcite path. The v2 engine is unaffected — it reaches emission through the no-argSearchExpression.toQueryString(), which passes a null-returning resolver and lands in the legacy branch.1. Wildcards were dropped on values containing whitespace (#5682)
search source=idx name="foo bar*"against a keyword field returned 0 hits instead of matching the whole-value pattern. The parser marked any whitespace-containing literal as a phrase, emittingname:"foo bar*"— and inside a Lucene phrase*is a literal character, so the query looked for docs containing*in the stored value.2. On text fields, a quoted value the analyzer splits became an OR over its tokens
The deeper cause was that
SearchLiteral.isPhrasewas set at parse time asvalue.contains(" ")— a syntactic test standing in for a semantic question: will the field's analyzer split this value? Whitespace is a poor proxy.foo=barholds none, yet the standard analyzer splits it into[foo, bar].So the value was emitted unquoted,
query_stringkept it as one field-scoped term, the analyzer split it, anddefault_operator=ORcombined the halves.body="foo=bar"matched any document holding justfooor justbar.Emission strategy
PPL emits a single
query_stringfilter; the Lucene query type is chosen by the parser at execution time, based on thequotedflag and how many tokens the field's analyzer produces. The emitter's job is to produce the right string, so it is now driven by the enclosing field's index mapping, read fromAbstractOpenSearchTable.getFieldTypes().The Calcite
RelDataTyperound trip cannot supply this:OpenSearchTypeFactorycollapsestextto plainVARCHAR, erasing the text/keyword distinction. ATODOmarks moving this metadata onto aRelDataType/scan annotation once the Calcite rule pipeline has been audited.text — honor the user's quoting. Unquoted passes through so wildcards stay operators; quoted becomes a phrase. The one exception is a whitespace-free value carrying a wildcard, which stays unquoted: quoting it would let the analyzer discard the wildcard, so
body="foo*"would stop matchingfoobar. That exception is gated on whitespace because with a space an unquoted value splits into separate clauses and the tail loses its field binding.keyword family — quoting carries no information: the analyzer is a no-op, so a quoted phrase and a bare term both resolve to the same single term (Lucene returns early from
createFieldQuerywhennumTokens == 1, beforequotedis read). Whole-value semantics are emitted instead. Escaping the whitespace in the wildcard form is fix #1.other mappings — these do their own value parsing and are out of scope; left on the previous code path.
Measured contract
Hit counts from
CalciteSearchCommandITagainst two indices with identical documents —namemappedkeywordin one,text(standard analyzer) in the other. 11 documents:foo,foobar,food,FOO,foo bar,foo barbaz,foo-bar,foo_bar,foo.bar,foo/bar,foo@bar.name=fooname="foo"name="foo_bar"name="foo.bar"name="foo-bar"name="foo/bar"name="foo@bar"name="foo bar"name=foo*name="foo*"name="foo_*"name="foo.*"name="foo-*"name="foo/*"name="foo bar*"name="*foo"name="*bar"name="*foo bar"name="f*r"name="foo*bar"name="foo *baz"name="*foo bar*"name="foo?"name="?oo"name="f?o"name="foo?bar"name="foo b?r"Only the three Group 2 rows change, plus the reported bug in 3.7. Every wildcard row is unchanged, and the keyword column is unchanged apart from 3.7 — non-wildcard keyword values are single-token either way, so moving them to the quoted form is behavior-neutral.
Group 7 runs against a dedicated fixture —
foo=bar,foo bar,foo,bar,baz— and asserts exact rows rather than counts, so the regression is visible:name="foo=bar"foo=bar,foo bar— the single-tokenfooandbardocs are absent (they matched before)name="foo=bar"foo=bar— exact whole valuename="foo*"name=foo-bar/name="foo-bar"Known limitation
On a text field, a wildcard combined with a character the analyzer splits on cannot match under any emission:
body="foo=ba*"returns 0. Unquoted,analyze_wildcarddefaults tofalse, so the pattern is matched against the token dictionary where no token contains=. Quoted, the analyzer discards the*and the residual token must match exactly. The indexed tokens forfoo=barare[foo, bar]— the original value is not stored anywhere the query can reach. Keyword fields handle this correctly (3 hits on the same data), since the value is matched whole.Breaking change
Text fields only: a quoted value that the analyzer splits is now a phrase rather than an OR over its tokens. Queries relying on the wider behavior will return fewer rows. Three examples in
docs/user/ppl/cmd/search.mddocumented the over-matching and have been updated — one had="cart-service"matching a service namedcart, which the fix correctly stops.Related Issues
Resolves #5682
Check List
--signoffor-s.By submitting this pull request, I confirm that my contribution is made under the terms of the Apache 2.0 license.
For more information on following Developer Certificate of Origin and signing off your commits, please check here.