From 6d95ea6bfd0beee9f735bfc879bc28a30e97af2d Mon Sep 17 00:00:00 2001 From: Alan Woodward Date: Fri, 9 Sep 2022 17:22:32 +0100 Subject: [PATCH] Empty intervals needs to start in position -1 (#89962) 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 --- docs/changelog/89962.yaml | 6 + .../search/query/IntervalQueriesIT.java | 104 ++++++++++++++++++ .../index/query/IntervalBuilder.java | 6 +- 3 files changed, 115 insertions(+), 1 deletion(-) create mode 100644 docs/changelog/89962.yaml create mode 100644 server/src/internalClusterTest/java/org/elasticsearch/search/query/IntervalQueriesIT.java diff --git a/docs/changelog/89962.yaml b/docs/changelog/89962.yaml new file mode 100644 index 0000000000000..bf13bfea9b5f1 --- /dev/null +++ b/docs/changelog/89962.yaml @@ -0,0 +1,6 @@ +pr: 89962 +summary: Empty intervals needs to start in position -1 +area: Search +type: bug +issues: + - 89789 diff --git a/server/src/internalClusterTest/java/org/elasticsearch/search/query/IntervalQueriesIT.java b/server/src/internalClusterTest/java/org/elasticsearch/search/query/IntervalQueriesIT.java new file mode 100644 index 0000000000000..c2ccfe4ee9694 --- /dev/null +++ b/server/src/internalClusterTest/java/org/elasticsearch/search/query/IntervalQueriesIT.java @@ -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> 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>> 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(); + } + }); + } + } +} diff --git a/server/src/main/java/org/elasticsearch/index/query/IntervalBuilder.java b/server/src/main/java/org/elasticsearch/index/query/IntervalBuilder.java index 5f158a1a733e0..6f75702032c75 100644 --- a/server/src/main/java/org/elasticsearch/index/query/IntervalBuilder.java +++ b/server/src/main/java/org/elasticsearch/index/query/IntervalBuilder.java @@ -225,6 +225,8 @@ protected List 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; @@ -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; }