Skip to content

Commit

Permalink
Empty intervals needs to start in position -1 (#89962) (#89977)
Browse files Browse the repository at this point in the history
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
  • Loading branch information
romseygeek committed Sep 9, 2022
1 parent 635de5e commit 5fb9f1c
Show file tree
Hide file tree
Showing 3 changed files with 115 additions and 1 deletion.
6 changes: 6 additions & 0 deletions docs/changelog/89962.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
pr: 89962
summary: Empty intervals needs to start in position -1
area: Search
type: bug
issues:
- 89789
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/

package org.elasticsearch.search.query;

import org.apache.lucene.analysis.Analyzer;
import org.apache.lucene.analysis.TokenStream;
import org.apache.lucene.analysis.Tokenizer;
import org.apache.lucene.analysis.core.KeywordTokenizer;
import org.elasticsearch.action.search.SearchResponse;
import org.elasticsearch.index.analysis.AnalyzerProvider;
import org.elasticsearch.index.analysis.AnalyzerScope;
import org.elasticsearch.index.query.IntervalQueryBuilder;
import org.elasticsearch.index.query.IntervalsSourceProvider;
import org.elasticsearch.indices.analysis.AnalysisModule;
import org.elasticsearch.plugins.AnalysisPlugin;
import org.elasticsearch.plugins.Plugin;
import org.elasticsearch.test.ESIntegTestCase;
import org.elasticsearch.test.InternalSettingsPlugin;

import java.io.IOException;
import java.util.Arrays;
import java.util.Collection;
import java.util.Map;

import static java.util.Collections.singletonMap;
import static org.elasticsearch.test.hamcrest.ElasticsearchAssertions.assertAcked;

public class IntervalQueriesIT extends ESIntegTestCase {

@Override
protected Collection<Class<? extends Plugin>> nodePlugins() {
return Arrays.asList(InternalSettingsPlugin.class, MockAnalysisPlugin.class);
}

public void testEmptyIntervalsWithNestedMappings() throws InterruptedException {
assertAcked(prepareCreate("nested").setMapping("""
{ "_doc" : {
"properties" : {
"empty_text" : { "type" : "text", "analyzer" : "empty" },
"text" : { "type" : "text" },
"nested" : { "type" : "nested", "properties" : { "nt" : { "type" : "text" } } }
}
}}
"""));

indexRandom(
true,
client().prepareIndex("nested").setId("1").setSource("text", "the quick brown fox jumps"),
client().prepareIndex("nested").setId("2").setSource("text", "quick brown"),
client().prepareIndex("nested").setId("3").setSource("text", "quick")
);

SearchResponse resp = client().prepareSearch("nested")
.setQuery(
new IntervalQueryBuilder("empty_text", new IntervalsSourceProvider.Match("an empty query", 0, true, null, null, null))
)
.get();
assertEquals(0, resp.getFailedShards());
}

private static class EmptyAnalyzer extends Analyzer {

@Override
protected TokenStreamComponents createComponents(String fieldName) {
Tokenizer source = new KeywordTokenizer();
TokenStream sink = new TokenStream() {
@Override
public boolean incrementToken() throws IOException {
return false;
}
};
return new TokenStreamComponents(source, sink);
}
}

public static class MockAnalysisPlugin extends Plugin implements AnalysisPlugin {

@Override
public Map<String, AnalysisModule.AnalysisProvider<AnalyzerProvider<? extends Analyzer>>> getAnalyzers() {
return singletonMap("empty", (indexSettings, environment, name, settings) -> new AnalyzerProvider<>() {
@Override
public String name() {
return "empty";
}

@Override
public AnalyzerScope scope() {
return AnalyzerScope.GLOBAL;
}

@Override
public Analyzer get() {
return new EmptyAnalyzer();
}
});
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -225,6 +225,8 @@ protected List<IntervalsSource> analyzeGraph(TokenStream source) throws IOExcept
@Override
public IntervalIterator intervals(String field, LeafReaderContext ctx) {
return new IntervalIterator() {
boolean exhausted = false;

@Override
public int start() {
return NO_MORE_INTERVALS;
Expand Down Expand Up @@ -252,16 +254,18 @@ public float matchCost() {

@Override
public int docID() {
return NO_MORE_DOCS;
return exhausted ? NO_MORE_DOCS : -1;
}

@Override
public int nextDoc() {
exhausted = true;
return NO_MORE_DOCS;
}

@Override
public int advance(int target) {
exhausted = true;
return NO_MORE_DOCS;
}

Expand Down

0 comments on commit 5fb9f1c

Please sign in to comment.