Skip to content

Commit 5fb9f1c

Browse files
authored
Empty intervals needs to start in position -1 (#89962) (#89977)
If a match interval query ends up with no tokens after analysis, we return an IntervalsSource that produces an empty interval iterator. This empty iterator previously always reported its current doc id as NO_MORE_DOCS. However, unpositioned DocIdSetIterators should report their doc id as -1, so this broke the API contract, which could lead to exceptions when an empty interval query was combined in a conjunction. This commit fixes the empty interval implementation so that it returns -1 when unpositioned. Fixes #89789
1 parent 635de5e commit 5fb9f1c

File tree

3 files changed

+115
-1
lines changed

3 files changed

+115
-1
lines changed

docs/changelog/89962.yaml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
pr: 89962
2+
summary: Empty intervals needs to start in position -1
3+
area: Search
4+
type: bug
5+
issues:
6+
- 89789
Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
/*
2+
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
3+
* or more contributor license agreements. Licensed under the Elastic License
4+
* 2.0 and the Server Side Public License, v 1; you may not use this file except
5+
* in compliance with, at your election, the Elastic License 2.0 or the Server
6+
* Side Public License, v 1.
7+
*/
8+
9+
package org.elasticsearch.search.query;
10+
11+
import org.apache.lucene.analysis.Analyzer;
12+
import org.apache.lucene.analysis.TokenStream;
13+
import org.apache.lucene.analysis.Tokenizer;
14+
import org.apache.lucene.analysis.core.KeywordTokenizer;
15+
import org.elasticsearch.action.search.SearchResponse;
16+
import org.elasticsearch.index.analysis.AnalyzerProvider;
17+
import org.elasticsearch.index.analysis.AnalyzerScope;
18+
import org.elasticsearch.index.query.IntervalQueryBuilder;
19+
import org.elasticsearch.index.query.IntervalsSourceProvider;
20+
import org.elasticsearch.indices.analysis.AnalysisModule;
21+
import org.elasticsearch.plugins.AnalysisPlugin;
22+
import org.elasticsearch.plugins.Plugin;
23+
import org.elasticsearch.test.ESIntegTestCase;
24+
import org.elasticsearch.test.InternalSettingsPlugin;
25+
26+
import java.io.IOException;
27+
import java.util.Arrays;
28+
import java.util.Collection;
29+
import java.util.Map;
30+
31+
import static java.util.Collections.singletonMap;
32+
import static org.elasticsearch.test.hamcrest.ElasticsearchAssertions.assertAcked;
33+
34+
public class IntervalQueriesIT extends ESIntegTestCase {
35+
36+
@Override
37+
protected Collection<Class<? extends Plugin>> nodePlugins() {
38+
return Arrays.asList(InternalSettingsPlugin.class, MockAnalysisPlugin.class);
39+
}
40+
41+
public void testEmptyIntervalsWithNestedMappings() throws InterruptedException {
42+
assertAcked(prepareCreate("nested").setMapping("""
43+
{ "_doc" : {
44+
"properties" : {
45+
"empty_text" : { "type" : "text", "analyzer" : "empty" },
46+
"text" : { "type" : "text" },
47+
"nested" : { "type" : "nested", "properties" : { "nt" : { "type" : "text" } } }
48+
}
49+
}}
50+
"""));
51+
52+
indexRandom(
53+
true,
54+
client().prepareIndex("nested").setId("1").setSource("text", "the quick brown fox jumps"),
55+
client().prepareIndex("nested").setId("2").setSource("text", "quick brown"),
56+
client().prepareIndex("nested").setId("3").setSource("text", "quick")
57+
);
58+
59+
SearchResponse resp = client().prepareSearch("nested")
60+
.setQuery(
61+
new IntervalQueryBuilder("empty_text", new IntervalsSourceProvider.Match("an empty query", 0, true, null, null, null))
62+
)
63+
.get();
64+
assertEquals(0, resp.getFailedShards());
65+
}
66+
67+
private static class EmptyAnalyzer extends Analyzer {
68+
69+
@Override
70+
protected TokenStreamComponents createComponents(String fieldName) {
71+
Tokenizer source = new KeywordTokenizer();
72+
TokenStream sink = new TokenStream() {
73+
@Override
74+
public boolean incrementToken() throws IOException {
75+
return false;
76+
}
77+
};
78+
return new TokenStreamComponents(source, sink);
79+
}
80+
}
81+
82+
public static class MockAnalysisPlugin extends Plugin implements AnalysisPlugin {
83+
84+
@Override
85+
public Map<String, AnalysisModule.AnalysisProvider<AnalyzerProvider<? extends Analyzer>>> getAnalyzers() {
86+
return singletonMap("empty", (indexSettings, environment, name, settings) -> new AnalyzerProvider<>() {
87+
@Override
88+
public String name() {
89+
return "empty";
90+
}
91+
92+
@Override
93+
public AnalyzerScope scope() {
94+
return AnalyzerScope.GLOBAL;
95+
}
96+
97+
@Override
98+
public Analyzer get() {
99+
return new EmptyAnalyzer();
100+
}
101+
});
102+
}
103+
}
104+
}

server/src/main/java/org/elasticsearch/index/query/IntervalBuilder.java

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -225,6 +225,8 @@ protected List<IntervalsSource> analyzeGraph(TokenStream source) throws IOExcept
225225
@Override
226226
public IntervalIterator intervals(String field, LeafReaderContext ctx) {
227227
return new IntervalIterator() {
228+
boolean exhausted = false;
229+
228230
@Override
229231
public int start() {
230232
return NO_MORE_INTERVALS;
@@ -252,16 +254,18 @@ public float matchCost() {
252254

253255
@Override
254256
public int docID() {
255-
return NO_MORE_DOCS;
257+
return exhausted ? NO_MORE_DOCS : -1;
256258
}
257259

258260
@Override
259261
public int nextDoc() {
262+
exhausted = true;
260263
return NO_MORE_DOCS;
261264
}
262265

263266
@Override
264267
public int advance(int target) {
268+
exhausted = true;
265269
return NO_MORE_DOCS;
266270
}
267271

0 commit comments

Comments
 (0)