Query Information
PPL Command/Query:
source=my_index name="POST /test-logs/_search*" | fields name
Expected Result:
Documents whose keyword name field starts with POST /test-logs/_search (i.e. the * is treated as a wildcard, matching zero or more characters).
Actual Result:
Zero hits. The same value without a space works as expected:
source=my_index name="*/test-logs/_search*" | fields name -- returns matching docs
source=my_index name="POST /test-logs/_search*" | fields name -- returns 0 docs
The only difference is the leading POST (space). The * stops working the moment the value contains whitespace.
Dataset Information
Dataset/Schema Type
Index Mapping
{
"mappings": {
"properties": {
"name": { "type": "keyword" }
}
}
}
Sample Data
{"name":"foo"}
{"name":"foobar"}
{"name":"food"}
{"name":"foo bar"}
{"name":"foo barbaz"}
{"name":"POST /test-logs/_search"}
{"name":"POST /test-logs/_search/xyz"}
Bug Description
Issue Summary:
When a search-command value contains both a space and a wildcard (* or ?), the parser emits a Lucene phrase query. Inside a phrase, * and ? are treated as literal characters — wildcard semantics are silently lost.
- On a keyword field: the query returns 0 hits, even when matching documents exist. Hard failure.
- On a text field: the query returns hits but the wildcard is silently ignored — you get whatever the bare phrase matches. Silent-correctness failure.
Root cause (code pointer):
ppl/src/main/java/org/opensearch/sql/ppl/parser/AstExpressionBuilder.java:1141
return new SearchLiteral(new Literal(content, DataType.STRING), content.contains(" "));
The isPhrase flag is set to true whenever the value contains a space. That triggers SearchLiteral.toQueryString() to wrap the value in double quotes, which becomes a Lucene phrase query — and Lucene's classic query-string parser strips wildcard interpretation inside phrases.
Steps to Reproduce:
- Create the two indices below (one keyword, one text) and bulk-load the sample docs.
- Run each PPL query in the matrix below.
- Compare emitted
query_string (from /_plugins/_ppl/_explain) and observed hits.
Index setup:
curl -X PUT localhost:9200/matrix_keyword -H 'Content-Type: application/json' \
-d '{"mappings":{"properties":{"name":{"type":"keyword"}}}}'
curl -X PUT localhost:9200/matrix_text -H 'Content-Type: application/json' \
-d '{"mappings":{"properties":{"name":{"type":"text"}}}}'
# same values loaded into both, plus text variants ending with " HTTP/1.1"
# for the last two rows in matrix_text
Measured matrix
Every row was executed end-to-end. The Emitted query_string column is extracted from /_plugins/_ppl/_explain (calcite.physical → sourceBuilder.query.query_string.query); the Hits column is from /_plugins/_ppl execution.
Corpus (7 docs per index, same values in matrix_keyword and matrix_text, except text rows for D3 append " HTTP/1.1" and row 5 is "foo barbaz here" in text): foo, foobar, food, foo bar, foo barbaz, POST /test-logs/_search, POST /test-logs/_search/xyz.
Keyword field (matrix_keyword)
| # |
PPL query |
Emitted query_string |
Hits |
Verdict |
| A-k |
name="foo" |
name:foo |
1 |
✅ expected |
| B1-k |
name="foo*" |
name:foo* |
5 |
✅ expected |
| B2-k |
name="foo?" |
name:foo? |
1 |
✅ expected |
| C-k |
name="foo bar" |
name:"foo bar" |
1 |
✅ expected |
| D1-k |
name="foo bar*" |
name:"foo bar*" |
0 |
❌ bug — corpus has foo barbaz |
| D2-k |
name="foo b?r" |
name:"foo b?r" |
0 |
❌ bug — corpus has foo bar |
| D3-k |
name="POST /test-logs/_search*" |
name:"POST \/test\-logs\/_search*" |
0 |
❌ bug — corpus has 2 matching docs (the reported case) |
Text field (matrix_text)
| # |
PPL query |
Emitted query_string |
Hits |
Verdict |
| A-t |
name="foo" |
name:foo |
3 |
✅ expected (analyzed term match) |
| B1-t |
name="foo*" |
name:foo* |
5 |
✅ expected |
| B2-t |
name="foo?" |
name:foo? |
1 |
✅ expected |
| C-t |
name="foo bar" |
name:"foo bar" |
1 |
✅ expected (positional phrase) |
| D1-t |
name="foo bar*" |
name:"foo bar*" |
1 |
⚠️ wildcard silently ignored — * did not extend match |
| D2-t |
name="foo b?r" |
name:"foo b?r" |
0 |
⚠️ wildcard silently ignored |
| D3-t |
name="POST /test-logs/_search*" |
name:"POST \/test\-logs\/_search*" |
2 |
⚠️ wildcard silently ignored — matched by phrase alone |
Summary: 8/14 rows correct (all of A / B / C). 6/14 rows show the bug — all of case D. Keyword returns 0 hits; text returns partial/wrong hits with no error signal.
Impact:
- Any PPL user searching a
keyword field with a value that contains a space cannot use wildcards at all — they get 0 hits with no error.
- The linked PPL docs (
docs/user/ppl/cmd/search.md) advertise wildcards as a supported feature for keyword fields and separately advertise quoted multi-word phrases, but never mention the two are mutually exclusive. Every wildcard example in the docs uses a whitespace-free value, so a reader following the docs would reasonably expect the space-plus-wildcard case to work.
Proposed fix
The bug is confined to the AST → query_string conversion; the Lucene layer is behaving correctly for what it is asked to do. Two focused changes:
-
AstExpressionBuilder.visitSearchLiteral — narrow the isPhrase heuristic so it does not fire when the value contains a wildcard:
// before
return new SearchLiteral(new Literal(content, DataType.STRING), content.contains(" "));
// after
boolean hasWildcard = content.indexOf('*') >= 0 || content.indexOf('?') >= 0;
boolean isPhrase = content.contains(" ") && !hasWildcard;
return new SearchLiteral(new Literal(content, DataType.STRING), isPhrase);
-
SearchLiteral.toQueryString — when NOT a phrase, escape spaces so the whole value is parsed by query_string as a single term/wildcard pattern instead of being split at whitespace into separate clauses:
// non-phrase branch
return QueryStringUtils.escapeLuceneSpecialCharacters(str).replace(" ", "\\ ");
After the fix, Case D on keyword emits name:POST\ \/test\-logs\/_search* — an unquoted wildcard term that Lucene builds as a PrefixQuery on the raw keyword term, and the reported query returns the expected matches. Cases A, B, C are untouched.
On text fields, Case D after the fix produces an honest per-token PrefixQuery (which structurally cannot match a cross-token pattern) instead of silently dropping the wildcard. This is a Lucene limitation, not a bug — the existing docs (docs/user/ppl/cmd/search.md around lines 113 / 123 / 463) already recommend a .keyword subfield for wildcard use on text.
Environment Information
OpenSearch Version: 3.8.0-SNAPSHOT (also reproduces on main)
Additional Details:
- Reproduced via
./gradlew opensearch-sql:run local cluster.
plugins.calcite.enabled=true, plugins.calcite.pushdown.enabled=false at cluster level. The query_string filter is still emitted regardless of pushdown setting; extracted via _plugins/_ppl/_explain.
- Docs page:
docs/user/ppl/cmd/search.md.
Query Information
PPL Command/Query:
Expected Result:
Documents whose keyword
namefield starts withPOST /test-logs/_search(i.e. the*is treated as a wildcard, matching zero or more characters).Actual Result:
Zero hits. The same value without a space works as expected:
The only difference is the leading
POST(space). The*stops working the moment the value contains whitespace.Dataset Information
Dataset/Schema Type
Index Mapping
{ "mappings": { "properties": { "name": { "type": "keyword" } } } }Sample Data
{"name":"foo"} {"name":"foobar"} {"name":"food"} {"name":"foo bar"} {"name":"foo barbaz"} {"name":"POST /test-logs/_search"} {"name":"POST /test-logs/_search/xyz"}Bug Description
Issue Summary:
When a
search-command value contains both a space and a wildcard (*or?), the parser emits a Lucene phrase query. Inside a phrase,*and?are treated as literal characters — wildcard semantics are silently lost.Root cause (code pointer):
ppl/src/main/java/org/opensearch/sql/ppl/parser/AstExpressionBuilder.java:1141The
isPhraseflag is set totruewhenever the value contains a space. That triggersSearchLiteral.toQueryString()to wrap the value in double quotes, which becomes a Lucene phrase query — and Lucene's classic query-string parser strips wildcard interpretation inside phrases.Steps to Reproduce:
query_string(from/_plugins/_ppl/_explain) and observed hits.Index setup:
Measured matrix
Every row was executed end-to-end. The
Emitted query_stringcolumn is extracted from/_plugins/_ppl/_explain(calcite.physical→sourceBuilder.query.query_string.query); theHitscolumn is from/_plugins/_pplexecution.Corpus (7 docs per index, same values in
matrix_keywordandmatrix_text, except text rows for D3 append" HTTP/1.1"and row 5 is"foo barbaz here"in text):foo,foobar,food,foo bar,foo barbaz,POST /test-logs/_search,POST /test-logs/_search/xyz.Keyword field (
matrix_keyword)query_stringname="foo"name:fooname="foo*"name:foo*name="foo?"name:foo?name="foo bar"name:"foo bar"name="foo bar*"name:"foo bar*"foo barbazname="foo b?r"name:"foo b?r"foo barname="POST /test-logs/_search*"name:"POST \/test\-logs\/_search*"Text field (
matrix_text)query_stringname="foo"name:fooname="foo*"name:foo*name="foo?"name:foo?name="foo bar"name:"foo bar"name="foo bar*"name:"foo bar*"*did not extend matchname="foo b?r"name:"foo b?r"name="POST /test-logs/_search*"name:"POST \/test\-logs\/_search*"Summary: 8/14 rows correct (all of A / B / C). 6/14 rows show the bug — all of case D. Keyword returns 0 hits; text returns partial/wrong hits with no error signal.
Impact:
keywordfield with a value that contains a space cannot use wildcards at all — they get 0 hits with no error.docs/user/ppl/cmd/search.md) advertise wildcards as a supported feature for keyword fields and separately advertise quoted multi-word phrases, but never mention the two are mutually exclusive. Every wildcard example in the docs uses a whitespace-free value, so a reader following the docs would reasonably expect the space-plus-wildcard case to work.Proposed fix
The bug is confined to the AST →
query_stringconversion; the Lucene layer is behaving correctly for what it is asked to do. Two focused changes:AstExpressionBuilder.visitSearchLiteral— narrow theisPhraseheuristic so it does not fire when the value contains a wildcard:SearchLiteral.toQueryString— when NOT a phrase, escape spaces so the whole value is parsed byquery_stringas a single term/wildcard pattern instead of being split at whitespace into separate clauses:After the fix, Case D on keyword emits
name:POST\ \/test\-logs\/_search*— an unquoted wildcard term that Lucene builds as aPrefixQueryon the raw keyword term, and the reported query returns the expected matches. Cases A, B, C are untouched.On
textfields, Case D after the fix produces an honest per-tokenPrefixQuery(which structurally cannot match a cross-token pattern) instead of silently dropping the wildcard. This is a Lucene limitation, not a bug — the existing docs (docs/user/ppl/cmd/search.mdaround lines 113 / 123 / 463) already recommend a.keywordsubfield for wildcard use on text.Environment Information
OpenSearch Version: 3.8.0-SNAPSHOT (also reproduces on
main)Additional Details:
./gradlew opensearch-sql:runlocal cluster.plugins.calcite.enabled=true,plugins.calcite.pushdown.enabled=falseat cluster level. Thequery_stringfilter is still emitted regardless of pushdown setting; extracted via_plugins/_ppl/_explain.docs/user/ppl/cmd/search.md.