Check positions on MultiPhraseQueries as well as phrase queries (#129326)

This commit is contained in:
Simon Cooper 2025-06-12 16:05:07 +01:00 committed by GitHub
parent 87ed699352
commit 3988ee1935
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
6 changed files with 67 additions and 9 deletions

View File

@ -0,0 +1,6 @@
pr: 129326
summary: Check positions on `MultiPhraseQueries` as well as phrase queries
area: Search
type: bug
issues:
- 123871

View File

@ -200,3 +200,50 @@
query: quick brown
operator: and
- match: { hits.total: 2 }
---
# Tests that this returns 400, and not 500 (#123871)
"Test multi_match phrase with no positions":
- requires:
cluster_features: [ "search.multi.match.checks.positions" ]
reason: "This previously resulted in a 5xx error code"
- do:
indices.create:
index: test
body:
settings:
analysis:
filter:
syns:
type: synonym
synonyms: [ "quick,fast" ]
analyzer:
syns:
tokenizer: standard
filter: [ "syns" ]
mappings:
properties:
field1:
type: text
index_options: freqs
analyzer: syns
- do:
index:
index: test
id: "1"
body:
field1: the quick brown fox
- do:
catch: bad_request
search:
body:
query:
multi_match:
query: the fast brown
type: phrase
fields:
- field1
- match: { status: 400 }
- match: { error.root_cause.0.type: query_shard_exception }
- match: { error.root_cause.0.reason: "failed to create query: field:[field1] was indexed without position data; cannot run MultiPhraseQuery" }

View File

@ -362,7 +362,7 @@ public class SearchAsYouTypeFieldMapper extends FieldMapper {
private void checkForPositions() {
if (getTextSearchInfo().hasPositions() == false) {
throw new IllegalStateException("field:[" + name() + "] was indexed without position data; cannot run PhraseQuery");
throw new IllegalArgumentException("field:[" + name() + "] was indexed without position data; cannot run PhraseQuery");
}
}

View File

@ -178,9 +178,9 @@ public class QueryStringIT extends ESIntegTestCase {
Exception.class,
prepareSearch("test").setQuery(queryStringQuery("f4:\"eggplant parmesan\"").lenient(false))
);
IllegalStateException ise = (IllegalStateException) ExceptionsHelper.unwrap(exc, IllegalStateException.class);
assertNotNull(ise);
assertThat(ise.getMessage(), containsString("field:[f4] was indexed without position data; cannot run PhraseQuery"));
IllegalArgumentException iae = (IllegalArgumentException) ExceptionsHelper.unwrap(exc, IllegalArgumentException.class);
assertNotNull(iae);
assertThat(iae.getMessage(), containsString("field:[f4] was indexed without position data; cannot run PhraseQuery"));
}
public void testBooleanStrictQuery() throws Exception {

View File

@ -894,9 +894,11 @@ public final class TextFieldMapper extends FieldMapper {
return Intervals.range(lowerTerm, upperTerm, includeLower, includeUpper, IndexSearcher.getMaxClauseCount());
}
private void checkForPositions() {
private void checkForPositions(boolean multi) {
if (getTextSearchInfo().hasPositions() == false) {
throw new IllegalStateException("field:[" + name() + "] was indexed without position data; cannot run PhraseQuery");
throw new IllegalArgumentException(
"field:[" + name() + "] was indexed without position data; cannot run " + (multi ? "MultiPhraseQuery" : "PhraseQuery")
);
}
}
@ -904,7 +906,7 @@ public final class TextFieldMapper extends FieldMapper {
public Query phraseQuery(TokenStream stream, int slop, boolean enablePosIncrements, SearchExecutionContext context)
throws IOException {
String field = name();
checkForPositions();
checkForPositions(false);
// we can't use the index_phrases shortcut with slop, if there are gaps in the stream,
// or if the incoming token stream is the output of a token graph due to
// https://issues.apache.org/jira/browse/LUCENE-8916
@ -939,6 +941,7 @@ public final class TextFieldMapper extends FieldMapper {
public Query multiPhraseQuery(TokenStream stream, int slop, boolean enablePositionIncrements, SearchExecutionContext context)
throws IOException {
String field = name();
checkForPositions(true);
if (indexPhrases && slop == 0 && hasGaps(stream) == false) {
stream = new FixedShingleFilter(stream, 2);
field = field + FAST_PHRASE_SUFFIX;
@ -959,7 +962,7 @@ public final class TextFieldMapper extends FieldMapper {
@Override
public Query phrasePrefixQuery(TokenStream stream, int slop, int maxExpansions, SearchExecutionContext context) throws IOException {
if (countTokens(stream) > 1) {
checkForPositions();
checkForPositions(false);
}
return analyzePhrasePrefix(stream, slop, maxExpansions);
}

View File

@ -30,6 +30,7 @@ public final class SearchFeatures implements FeatureSpecification {
);
public static final NodeFeature RESCORER_MISSING_FIELD_BAD_REQUEST = new NodeFeature("search.rescorer.missing.field.bad.request");
public static final NodeFeature INT_SORT_FOR_INT_SHORT_BYTE_FIELDS = new NodeFeature("search.sort.int_sort_for_int_short_byte_fields");
static final NodeFeature MULTI_MATCH_CHECKS_POSITIONS = new NodeFeature("search.multi.match.checks.positions");
@Override
public Set<NodeFeature> getTestFeatures() {
@ -37,7 +38,8 @@ public final class SearchFeatures implements FeatureSpecification {
RETRIEVER_RESCORER_ENABLED,
COMPLETION_FIELD_SUPPORTS_DUPLICATE_SUGGESTIONS,
RESCORER_MISSING_FIELD_BAD_REQUEST,
INT_SORT_FOR_INT_SHORT_BYTE_FIELDS
INT_SORT_FOR_INT_SHORT_BYTE_FIELDS,
MULTI_MATCH_CHECKS_POSITIONS
);
}
}