From f5c85159b0860e65ae317ee838f1f38ea9b9833a Mon Sep 17 00:00:00 2001 From: Dimitris Rempapis Date: Thu, 26 Mar 2026 10:54:59 +0200 Subject: [PATCH 1/8] Fix circuit breaker leak in percolator query construction (#144827) (cherry picked from commit b36fdbca3cf487c2c5e1093e3ea930fdfa096a50) # Conflicts: # modules/percolator/src/test/java/org/elasticsearch/percolator/QueryBuilderStoreTests.java # x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/enrich/ExpressionQueryList.java --- docs/changelog/144827.yaml | 5 + .../percolator/PercolateQueryBuilder.java | 15 ++ .../percolator/QueryBuilderStoreTests.java | 120 +++++++++ .../suggest/phrase/PhraseSuggester.java | 8 +- ...rHitContextBuilderCircuitBreakerTests.java | 137 ++++++++++ .../PhraseSuggesterCircuitBreakerTests.java | 251 ++++++++++++++++++ .../esql/enrich/ExpressionQueryList.java | 37 ++- ...xpressionQueryListCircuitBreakerTests.java | 167 ++++++++++++ 8 files changed, 724 insertions(+), 16 deletions(-) create mode 100644 docs/changelog/144827.yaml create mode 100644 server/src/test/java/org/elasticsearch/index/query/InnerHitContextBuilderCircuitBreakerTests.java create mode 100644 server/src/test/java/org/elasticsearch/search/suggest/phrase/PhraseSuggesterCircuitBreakerTests.java create mode 100644 x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/enrich/ExpressionQueryListCircuitBreakerTests.java diff --git a/docs/changelog/144827.yaml b/docs/changelog/144827.yaml new file mode 100644 index 0000000000000..61791428d01eb --- /dev/null +++ b/docs/changelog/144827.yaml @@ -0,0 +1,5 @@ +area: Search +issues: [] +pr: 144827 +summary: Fix circuit breaker leak in percolator query construction +type: bug diff --git a/modules/percolator/src/main/java/org/elasticsearch/percolator/PercolateQueryBuilder.java b/modules/percolator/src/main/java/org/elasticsearch/percolator/PercolateQueryBuilder.java index 2ce0f1af5b0c4..62d4704cc43a2 100644 --- a/modules/percolator/src/main/java/org/elasticsearch/percolator/PercolateQueryBuilder.java +++ b/modules/percolator/src/main/java/org/elasticsearch/percolator/PercolateQueryBuilder.java @@ -674,6 +674,21 @@ public > IFD getForField( public void addNamedQuery(String name, Query query) { delegate.addNamedQuery(name, query); } + + @Override + public void addCircuitBreakerMemory(long bytes, String label) { + source.addCircuitBreakerMemory(bytes, label); + } + + @Override + public long getQueryConstructionMemoryUsed() { + return source.getQueryConstructionMemoryUsed(); + } + + @Override + public void releaseQueryConstructionMemory() { + source.releaseQueryConstructionMemory(); + } }; } diff --git a/modules/percolator/src/test/java/org/elasticsearch/percolator/QueryBuilderStoreTests.java b/modules/percolator/src/test/java/org/elasticsearch/percolator/QueryBuilderStoreTests.java index 67527a4d27436..8669b7149144e 100644 --- a/modules/percolator/src/test/java/org/elasticsearch/percolator/QueryBuilderStoreTests.java +++ b/modules/percolator/src/test/java/org/elasticsearch/percolator/QueryBuilderStoreTests.java @@ -19,8 +19,11 @@ import org.apache.lucene.search.TermQuery; import org.apache.lucene.store.Directory; import org.elasticsearch.TransportVersion; +import org.elasticsearch.cluster.metadata.IndexMetadata; +import org.elasticsearch.common.breaker.CircuitBreaker; import org.elasticsearch.common.io.stream.NamedWriteableRegistry; import org.elasticsearch.common.settings.Settings; +import org.elasticsearch.common.unit.ByteSizeValue; import org.elasticsearch.core.CheckedFunction; import org.elasticsearch.index.IndexVersion; import org.elasticsearch.index.fielddata.plain.BytesBinaryIndexFieldData; @@ -30,8 +33,12 @@ import org.elasticsearch.index.mapper.MappedFieldType; import org.elasticsearch.index.mapper.MapperBuilderContext; import org.elasticsearch.index.mapper.TestDocumentParserContext; +import org.elasticsearch.index.query.QueryBuilder; +import org.elasticsearch.index.query.RegexpQueryBuilder; import org.elasticsearch.index.query.SearchExecutionContext; import org.elasticsearch.index.query.TermQueryBuilder; +import org.elasticsearch.index.query.WildcardQueryBuilder; +import org.elasticsearch.script.field.BinaryDocValuesField; import org.elasticsearch.search.SearchModule; import org.elasticsearch.search.aggregations.support.CoreValuesSourceType; import org.elasticsearch.test.ESTestCase; @@ -43,6 +50,8 @@ import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; +import static org.elasticsearch.index.query.SearchExecutionContextHelper.SHARD_SEARCH_STATS; +import static org.hamcrest.Matchers.greaterThan; public class QueryBuilderStoreTests extends ESTestCase { @@ -89,6 +98,7 @@ public void testStoringQueryBuilders() throws IOException { when(searchExecutionContext.getParserConfig()).thenReturn(parserConfig()); when(searchExecutionContext.getForField(fieldMapper.fieldType(), fielddataOperation)).thenReturn( new BytesBinaryIndexFieldData(fieldMapper.fullPath(), CoreValuesSourceType.KEYWORD) + ); when(searchExecutionContext.getFieldType(Mockito.anyString())).thenAnswer(invocation -> { final String fieldName = (String) invocation.getArguments()[0]; @@ -108,4 +118,114 @@ public void testStoringQueryBuilders() throws IOException { } } } + + public void testCircuitBreakerReleasedAfterPerDocumentQueryConstruction() throws IOException { + CircuitBreaker circuitBreaker = newLimitedBreaker(ByteSizeValue.ofMb(100)); + + String fieldName = "keyword_field"; + QueryBuilder[] queryBuilders = new QueryBuilder[] { + new WildcardQueryBuilder(fieldName, "test*pattern*with*wildcards"), + new RegexpQueryBuilder(fieldName, ".*test.*regexp.*pattern.*"), + new WildcardQueryBuilder(fieldName, "another*wildcard*query"), + new RegexpQueryBuilder(fieldName, "prefix[0-9]+suffix"), }; + + try (Directory directory = newDirectory()) { + IndexWriterConfig config = new IndexWriterConfig(new WhitespaceAnalyzer()); + config.setMergePolicy(NoMergePolicy.INSTANCE); + BinaryFieldMapper fieldMapper = PercolatorFieldMapper.Builder.createQueryBuilderFieldBuilder( + MapperBuilderContext.root(false, false) + ); + + IndexVersion indexVersion = IndexVersion.current(); + try (IndexWriter indexWriter = new IndexWriter(directory, config)) { + for (QueryBuilder queryBuilder : queryBuilders) { + DocumentParserContext documentParserContext = new TestDocumentParserContext(); + PercolatorFieldMapper.createQueryBuilderField( + indexVersion, + TransportVersion.current(), + fieldMapper, + queryBuilder, + documentParserContext + ); + indexWriter.addDocument(documentParserContext.doc()); + } + } + + NamedWriteableRegistry writeableRegistry = writableRegistry(); + XContentParserConfiguration parserConfig = parserConfig(); + Settings indexSettingsSettings = indexSettings(indexVersion, 1, 1).build(); + IndexSettings indexSettings = new IndexSettings( + IndexMetadata.builder("test").settings(indexSettingsSettings).build(), + Settings.EMPTY + ); + + KeywordFieldMapper keywordMapper = new KeywordFieldMapper.Builder(fieldName, indexSettings).build( + MapperBuilderContext.root(false, false) + ); + MappingLookup mappingLookup = MappingLookup.fromMappers( + Mapping.EMPTY, + List.of(keywordMapper), + Collections.emptyList(), + IndexMode.STANDARD + ); + + BytesBinaryIndexFieldData fieldData = new BytesBinaryIndexFieldData( + fieldMapper.fullPath(), + CoreValuesSourceType.KEYWORD, + BinaryDocValuesField::new + ); + BiFunction> indexFieldDataLookup = (mft, fdc) -> fieldData; + + SearchExecutionContext baseContext = new SearchExecutionContext( + 0, + 0, + indexSettings, + null, + indexFieldDataLookup, + null, + mappingLookup, + null, + null, + parserConfig, + writeableRegistry, + null, + null, + System::currentTimeMillis, + null, + null, + () -> true, + null, + Collections.emptyMap(), + null, + MapperMetrics.NOOP, + SHARD_SEARCH_STATS + ); + SearchExecutionContext searchExecutionContext = new SearchExecutionContext(baseContext, circuitBreaker); + + PercolateQuery.QueryStore queryStore = PercolateQueryBuilder.createStore( + fieldMapper.fieldType(), + false, + searchExecutionContext + ); + + try (IndexReader indexReader = DirectoryReader.open(directory)) { + LeafReaderContext leafContext = indexReader.leaves().get(0); + CheckedFunction queries = queryStore.getQueries(leafContext); + assertEquals(queryBuilders.length, leafContext.reader().numDocs()); + + long baselineUsed = circuitBreaker.getUsed(); + for (int i = 0; i < queryBuilders.length; i++) { + queries.apply(i); + assertThat( + "CB bytes should still be tracked (not leaked) after document " + i, + circuitBreaker.getUsed(), + greaterThan(baselineUsed) + ); + } + + searchExecutionContext.releaseQueryConstructionMemory(); + assertEquals("All CB bytes must be released after the request-end release", baselineUsed, circuitBreaker.getUsed()); + } + } + } } diff --git a/server/src/main/java/org/elasticsearch/search/suggest/phrase/PhraseSuggester.java b/server/src/main/java/org/elasticsearch/search/suggest/phrase/PhraseSuggester.java index ebaf969b40aef..8ce7816a9568c 100644 --- a/server/src/main/java/org/elasticsearch/search/suggest/phrase/PhraseSuggester.java +++ b/server/src/main/java/org/elasticsearch/search/suggest/phrase/PhraseSuggester.java @@ -135,8 +135,12 @@ public Suggestion> innerExecute( .createParser(searchExecutionContext.getParserConfig(), querySource) ) { QueryBuilder innerQueryBuilder = AbstractQueryBuilder.parseTopLevelQuery(parser); - final ParsedQuery parsedQuery = searchExecutionContext.toQuery(innerQueryBuilder); - collateMatch = Lucene.exists(searcher, parsedQuery.query()); + try { + final ParsedQuery parsedQuery = searchExecutionContext.toQuery(innerQueryBuilder); + collateMatch = Lucene.exists(searcher, parsedQuery.query()); + } finally { + searchExecutionContext.releaseQueryConstructionMemory(); + } } } if (collateMatch == false && collatePrune == false) { diff --git a/server/src/test/java/org/elasticsearch/index/query/InnerHitContextBuilderCircuitBreakerTests.java b/server/src/test/java/org/elasticsearch/index/query/InnerHitContextBuilderCircuitBreakerTests.java new file mode 100644 index 0000000000000..95548d856cb57 --- /dev/null +++ b/server/src/test/java/org/elasticsearch/index/query/InnerHitContextBuilderCircuitBreakerTests.java @@ -0,0 +1,137 @@ +/* + * 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", the "GNU Affero General Public License v3.0 only", 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", the "GNU Affero General Public + * License v3.0 only", or the "Server Side Public License, v 1". + */ +package org.elasticsearch.index.query; + +import org.apache.lucene.analysis.core.WhitespaceAnalyzer; +import org.apache.lucene.index.DirectoryReader; +import org.apache.lucene.index.IndexWriter; +import org.apache.lucene.index.IndexWriterConfig; +import org.apache.lucene.search.IndexSearcher; +import org.apache.lucene.store.ByteBuffersDirectory; +import org.apache.lucene.store.Directory; +import org.elasticsearch.cluster.metadata.IndexMetadata; +import org.elasticsearch.common.breaker.CircuitBreaker; +import org.elasticsearch.common.io.stream.NamedWriteableRegistry; +import org.elasticsearch.common.settings.Settings; +import org.elasticsearch.common.unit.ByteSizeValue; +import org.elasticsearch.index.IndexMode; +import org.elasticsearch.index.IndexSettings; +import org.elasticsearch.index.IndexVersion; +import org.elasticsearch.index.mapper.KeywordFieldMapper; +import org.elasticsearch.index.mapper.MapperBuilderContext; +import org.elasticsearch.index.mapper.MapperMetrics; +import org.elasticsearch.index.mapper.Mapping; +import org.elasticsearch.index.mapper.MappingLookup; +import org.elasticsearch.search.SearchModule; +import org.elasticsearch.search.fetch.subphase.InnerHitsContext; +import org.elasticsearch.search.internal.SearchContext; +import org.elasticsearch.test.ESTestCase; +import org.elasticsearch.xcontent.NamedXContentRegistry; + +import java.io.IOException; +import java.util.Collections; +import java.util.List; + +import static org.elasticsearch.index.query.SearchExecutionContextHelper.SHARD_SEARCH_STATS; +import static org.hamcrest.Matchers.equalTo; +import static org.hamcrest.Matchers.greaterThan; + +public class InnerHitContextBuilderCircuitBreakerTests extends ESTestCase { + + @Override + protected NamedWriteableRegistry writableRegistry() { + SearchModule searchModule = new SearchModule(Settings.EMPTY, Collections.emptyList()); + return new NamedWriteableRegistry(searchModule.getNamedWriteables()); + } + + @Override + protected NamedXContentRegistry xContentRegistry() { + SearchModule searchModule = new SearchModule(Settings.EMPTY, Collections.emptyList()); + return new NamedXContentRegistry(searchModule.getNamedXContents()); + } + + public void testCBTrackedDuringInnerHitsAndReleasedAtRequestEnd() throws IOException { + Directory dir = new ByteBuffersDirectory(); + // Empty index – we just need a valid IndexSearcher for the context. + try (IndexWriter writer = new IndexWriter(dir, new IndexWriterConfig(new WhitespaceAnalyzer()))) { + // intentionally empty + } + + try (DirectoryReader reader = DirectoryReader.open(dir)) { + IndexSearcher searcher = new IndexSearcher(reader); + + IndexVersion indexVersion = IndexVersion.current(); + Settings indexSettingsSettings = indexSettings(indexVersion, 1, 1).build(); + IndexSettings indexSettings = new IndexSettings( + IndexMetadata.builder("test").settings(indexSettingsSettings).build(), + Settings.EMPTY + ); + KeywordFieldMapper fieldMapper = new KeywordFieldMapper.Builder("field", indexSettings).build( + MapperBuilderContext.root(false, false) + ); + MappingLookup mappingLookup = MappingLookup.fromMappers( + Mapping.EMPTY, + List.of(fieldMapper), + Collections.emptyList(), + IndexMode.STANDARD + ); + + SearchExecutionContext baseCtx = new SearchExecutionContext( + 0, + 0, + indexSettings, + null, + null, + null, + mappingLookup, + null, + null, + parserConfig(), + writableRegistry(), + null, + searcher, + System::currentTimeMillis, + null, + null, + () -> true, + null, + Collections.emptyMap(), + null, + MapperMetrics.NOOP, + SHARD_SEARCH_STATS + ); + + CircuitBreaker cb = newLimitedBreaker(ByteSizeValue.ofMb(100)); + SearchExecutionContext ctx = new SearchExecutionContext(baseCtx, cb); + QueryBuilder innerQuery = new WildcardQueryBuilder("field", "*test*pattern*"); + InnerHitBuilder innerHitBuilder = new InnerHitBuilder("test_inner"); + InnerHitContextBuilder builder = new InnerHitContextBuilder(innerQuery, innerHitBuilder, Collections.emptyMap()) { + @Override + protected void doBuild(SearchContext parentSearchContext, InnerHitsContext innerHitsContext) {} + }; + + InnerHitsContext.InnerHitSubContext subContext = org.mockito.Mockito.mock(InnerHitsContext.InnerHitSubContext.class); + + long baselineUsed = cb.getUsed(); + + int iterations = 5; + for (int i = 0; i < iterations; i++) { + builder.setupInnerHitsContext(ctx, subContext); + assertThat( + "CB bytes must still be tracked (not released early) after iteration " + i, + cb.getUsed(), + greaterThan(baselineUsed) + ); + } + + ctx.releaseQueryConstructionMemory(); + assertThat("All CB bytes must be released after the request-end release", cb.getUsed(), equalTo(baselineUsed)); + } + } +} diff --git a/server/src/test/java/org/elasticsearch/search/suggest/phrase/PhraseSuggesterCircuitBreakerTests.java b/server/src/test/java/org/elasticsearch/search/suggest/phrase/PhraseSuggesterCircuitBreakerTests.java new file mode 100644 index 0000000000000..0977b6c423472 --- /dev/null +++ b/server/src/test/java/org/elasticsearch/search/suggest/phrase/PhraseSuggesterCircuitBreakerTests.java @@ -0,0 +1,251 @@ +/* + * 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", the "GNU Affero General Public License v3.0 only", 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", the "GNU Affero General Public + * License v3.0 only", or the "Server Side Public License, v 1". + */ +package org.elasticsearch.search.suggest.phrase; + +import org.apache.lucene.analysis.Analyzer; +import org.apache.lucene.analysis.LowerCaseFilter; +import org.apache.lucene.analysis.Tokenizer; +import org.apache.lucene.analysis.core.WhitespaceAnalyzer; +import org.apache.lucene.analysis.miscellaneous.PerFieldAnalyzerWrapper; +import org.apache.lucene.analysis.shingle.ShingleFilter; +import org.apache.lucene.analysis.standard.StandardTokenizer; +import org.apache.lucene.document.Document; +import org.apache.lucene.document.Field; +import org.apache.lucene.document.TextField; +import org.apache.lucene.index.DirectoryReader; +import org.apache.lucene.index.IndexWriter; +import org.apache.lucene.index.IndexWriterConfig; +import org.apache.lucene.search.IndexSearcher; +import org.apache.lucene.search.spell.SuggestMode; +import org.apache.lucene.store.ByteBuffersDirectory; +import org.apache.lucene.store.Directory; +import org.apache.lucene.util.BytesRef; +import org.apache.lucene.util.CharsRefBuilder; +import org.elasticsearch.cluster.metadata.IndexMetadata; +import org.elasticsearch.common.breaker.CircuitBreaker; +import org.elasticsearch.common.io.stream.NamedWriteableRegistry; +import org.elasticsearch.common.settings.Settings; +import org.elasticsearch.common.unit.ByteSizeValue; +import org.elasticsearch.index.IndexMode; +import org.elasticsearch.index.IndexSettings; +import org.elasticsearch.index.IndexVersion; +import org.elasticsearch.index.mapper.KeywordFieldMapper; +import org.elasticsearch.index.mapper.MapperBuilderContext; +import org.elasticsearch.index.mapper.MapperMetrics; +import org.elasticsearch.index.mapper.Mapping; +import org.elasticsearch.index.mapper.MappingLookup; +import org.elasticsearch.index.query.SearchExecutionContext; +import org.elasticsearch.script.TemplateScript; +import org.elasticsearch.search.SearchModule; +import org.elasticsearch.search.suggest.Suggest; +import org.elasticsearch.test.ESTestCase; +import org.elasticsearch.xcontent.NamedXContentRegistry; + +import java.io.IOException; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import static org.elasticsearch.index.query.SearchExecutionContextHelper.SHARD_SEARCH_STATS; +import static org.hamcrest.Matchers.equalTo; + +public class PhraseSuggesterCircuitBreakerTests extends ESTestCase { + + @Override + protected NamedWriteableRegistry writableRegistry() { + SearchModule searchModule = new SearchModule(Settings.EMPTY, Collections.emptyList()); + return new NamedWriteableRegistry(searchModule.getNamedWriteables()); + } + + @Override + protected NamedXContentRegistry xContentRegistry() { + SearchModule searchModule = new SearchModule(Settings.EMPTY, Collections.emptyList()); + return new NamedXContentRegistry(searchModule.getNamedXContents()); + } + + public void testCBReleasedAfterEachCollateIteration() throws IOException { + Directory dir = new ByteBuffersDirectory(); + + Map analyzerMap = new HashMap<>(); + analyzerMap.put("body_ngram", new Analyzer() { + @Override + protected TokenStreamComponents createComponents(String fieldName) { + Tokenizer t = new StandardTokenizer(); + ShingleFilter sf = new ShingleFilter(t, 2, 3); + sf.setOutputUnigrams(false); + return new TokenStreamComponents(t, new LowerCaseFilter(sf)); + } + }); + analyzerMap.put("body", new Analyzer() { + @Override + protected TokenStreamComponents createComponents(String fieldName) { + Tokenizer t = new StandardTokenizer(); + return new TokenStreamComponents(t, new LowerCaseFilter(t)); + } + }); + PerFieldAnalyzerWrapper wrapper = new PerFieldAnalyzerWrapper(new WhitespaceAnalyzer(), analyzerMap); + + IndexWriterConfig conf = new IndexWriterConfig(wrapper); + try (IndexWriter writer = new IndexWriter(dir, conf)) { + for (String line : new String[] { "captain america", "american ace", "captain marvel", "american hero", "captain planet" }) { + Document doc = new Document(); + doc.add(new Field("body", line, TextField.TYPE_NOT_STORED)); + doc.add(new Field("body_ngram", line, TextField.TYPE_NOT_STORED)); + writer.addDocument(doc); + } + } + + try (DirectoryReader reader = DirectoryReader.open(dir)) { + IndexSearcher searcher = new IndexSearcher(reader); + + IndexVersion indexVersion = IndexVersion.current(); + Settings indexSettingsSettings = indexSettings(indexVersion, 1, 1).build(); + IndexSettings indexSettings = new IndexSettings( + IndexMetadata.builder("test").settings(indexSettingsSettings).build(), + Settings.EMPTY + ); + KeywordFieldMapper bodyMapper = new KeywordFieldMapper.Builder("body", indexSettings).build( + MapperBuilderContext.root(false, false) + ); + MappingLookup mappingLookup = MappingLookup.fromMappers( + Mapping.EMPTY, + List.of(bodyMapper), + Collections.emptyList(), + IndexMode.STANDARD + ); + + SearchExecutionContext baseCtx = new SearchExecutionContext( + 0, + 0, + indexSettings, + null, + null, + null, + mappingLookup, + null, + null, + parserConfig(), + writableRegistry(), + null, + searcher, + System::currentTimeMillis, + null, + null, + () -> true, + null, + Collections.emptyMap(), + null, + MapperMetrics.NOOP, + SHARD_SEARCH_STATS + ); + CircuitBreaker cb = newLimitedBreaker(ByteSizeValue.ofMb(100)); + SearchExecutionContext ctx = new SearchExecutionContext(baseCtx, cb); + + TemplateScript.Factory scriptFactory = params -> new TemplateScript(params) { + @Override + public String execute() { + return "{\"wildcard\":{\"body\":{\"value\":\"captain*\"}}}"; + } + }; + + PhraseSuggestionContext suggestion = new PhraseSuggestionContext(ctx); + suggestion.setField("body_ngram"); + suggestion.setAnalyzer(wrapper); + suggestion.setSize(5); + suggestion.setShardSize(5); + suggestion.setGramSize(2); + suggestion.setConfidence(0.0f); + suggestion.setMaxErrors(2.0f); + suggestion.setRequireUnigram(false); + suggestion.setCollateQueryScript(scriptFactory); + suggestion.setText(new BytesRef("captan amrica")); + + PhraseSuggestionContext.DirectCandidateGenerator generator = new PhraseSuggestionContext.DirectCandidateGenerator(); + generator.setField("body"); + generator.suggestMode(SuggestMode.SUGGEST_MORE_POPULAR); + generator.size(10); + generator.accuracy(0.3f); + generator.minWordLength(2); + suggestion.addGenerator(generator); + + assertEquals("CB must be zero before innerExecute", 0L, cb.getUsed()); + + Suggest.Suggestion> result = + PhraseSuggester.INSTANCE.innerExecute("test", suggestion, searcher, new CharsRefBuilder()); + + assertNotNull("innerExecute must return a result", result); + + assertThat( + "CB tracked bytes must be fully released after innerExecute (fix: release per correction)", + ctx.getQueryConstructionMemoryUsed(), + equalTo(0L) + ); + assertThat("Raw CB usage must be zero after innerExecute", cb.getUsed(), equalTo(0L)); + } + } + + public void testNoCBUsageWithoutCollateScript() throws IOException { + Directory dir = new ByteBuffersDirectory(); + try (IndexWriter writer = new IndexWriter(dir, new IndexWriterConfig(new WhitespaceAnalyzer()))) { + Document doc = new Document(); + doc.add(new Field("body", "hello world", TextField.TYPE_NOT_STORED)); + writer.addDocument(doc); + } + + try (DirectoryReader reader = DirectoryReader.open(dir)) { + IndexSearcher searcher = new IndexSearcher(reader); + CircuitBreaker cb = newLimitedBreaker(ByteSizeValue.ofMb(100)); + + IndexVersion indexVersion = IndexVersion.current(); + Settings indexSettingsSettings = indexSettings(indexVersion, 1, 1).build(); + IndexSettings indexSettings = new IndexSettings( + IndexMetadata.builder("test").settings(indexSettingsSettings).build(), + Settings.EMPTY + ); + SearchExecutionContext baseCtx = new SearchExecutionContext( + 0, + 0, + indexSettings, + null, + null, + null, + MappingLookup.EMPTY, + null, + null, + parserConfig(), + writableRegistry(), + null, + searcher, + System::currentTimeMillis, + null, + null, + () -> true, + null, + Collections.emptyMap(), + null, + MapperMetrics.NOOP, + SHARD_SEARCH_STATS + ); + SearchExecutionContext ctx = new SearchExecutionContext(baseCtx, cb); + + PhraseSuggestionContext suggestion = new PhraseSuggestionContext(ctx); + suggestion.setField("body"); + suggestion.setAnalyzer(new WhitespaceAnalyzer()); + suggestion.setSize(5); + suggestion.setShardSize(5); + suggestion.setGramSize(1); + suggestion.setConfidence(0.0f); + suggestion.setText(new BytesRef("hello")); + + PhraseSuggester.INSTANCE.innerExecute("test", suggestion, searcher, new CharsRefBuilder()); + assertThat("No CB bytes should be used when there is no collate script", cb.getUsed(), equalTo(0L)); + } + } +} diff --git a/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/enrich/ExpressionQueryList.java b/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/enrich/ExpressionQueryList.java index d881644849f2f..ba9d7ccdd5db3 100644 --- a/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/enrich/ExpressionQueryList.java +++ b/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/enrich/ExpressionQueryList.java @@ -256,22 +256,31 @@ private void buildPreJoinFilter(PhysicalPlan rightPreJoinPlan, ClusterService cl * @return The query at the given position, or null if any of the match fields are null. */ @Override - public Query getQuery(int position) { - BooleanQuery.Builder builder = new BooleanQuery.Builder(); - for (QueryList queryList : queryLists) { - Query q = queryList.getQuery(position); - if (q == null) { - // if any of the matchFields are null, it means there is no match for this position - // A AND NULL is always NULL, so we can skip this position - return null; + public Query getQuery(int position, Page inputPage, SearchExecutionContext searchExecutionContext) { + try { + BooleanQuery.Builder builder = new BooleanQuery.Builder(); + for (QueryList queryList : queryLists) { + Query q = queryList.getQuery(position, inputPage, searchExecutionContext); + if (q == null) { + // if any of the matchFields are null, it means there is no match for this position + // A AND NULL is always NULL, so we can skip this position + return null; + } + builder.add(q, BooleanClause.Occur.FILTER); } - builder.add(q, BooleanClause.Occur.FILTER); - } - // also attach the pre-join filter if it exists - for (Query preJoinFilter : preJoinFilters) { - builder.add(preJoinFilter, BooleanClause.Occur.FILTER); + // also attach the pre-join filter if it exists + // Build queries from QueryBuilders dynamically to avoid caching stale IndexReader references + for (QueryBuilder queryBuilder : lucenePushableFilterBuilders) { + try { + builder.add(queryBuilder.toQuery(searchExecutionContext), BooleanClause.Occur.FILTER); + } catch (IOException e) { + throw new UncheckedIOException("Error while building query for Lucene pushable filter", e); + } + } + return builder.build(); + } finally { + searchExecutionContext.releaseQueryConstructionMemory(); } - return builder.build(); } /** diff --git a/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/enrich/ExpressionQueryListCircuitBreakerTests.java b/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/enrich/ExpressionQueryListCircuitBreakerTests.java new file mode 100644 index 0000000000000..3a20ce85a7ec7 --- /dev/null +++ b/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/enrich/ExpressionQueryListCircuitBreakerTests.java @@ -0,0 +1,167 @@ +/* + * 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; you may not use this file except in compliance with the Elastic License + * 2.0. + */ +package org.elasticsearch.xpack.esql.enrich; + +import org.apache.lucene.analysis.core.WhitespaceAnalyzer; +import org.apache.lucene.index.DirectoryReader; +import org.apache.lucene.index.IndexWriter; +import org.apache.lucene.index.IndexWriterConfig; +import org.apache.lucene.search.IndexSearcher; +import org.apache.lucene.store.ByteBuffersDirectory; +import org.apache.lucene.store.Directory; +import org.elasticsearch.cluster.metadata.IndexMetadata; +import org.elasticsearch.common.breaker.CircuitBreaker; +import org.elasticsearch.common.io.stream.NamedWriteableRegistry; +import org.elasticsearch.common.settings.ClusterSettings; +import org.elasticsearch.common.settings.Settings; +import org.elasticsearch.common.unit.ByteSizeValue; +import org.elasticsearch.index.IndexMode; +import org.elasticsearch.index.IndexSettings; +import org.elasticsearch.index.IndexVersion; +import org.elasticsearch.index.mapper.KeywordFieldMapper; +import org.elasticsearch.index.mapper.MapperBuilderContext; +import org.elasticsearch.index.mapper.MapperMetrics; +import org.elasticsearch.index.mapper.Mapping; +import org.elasticsearch.index.mapper.MappingLookup; +import org.elasticsearch.index.query.SearchExecutionContext; +import org.elasticsearch.index.query.WildcardQueryBuilder; +import org.elasticsearch.search.SearchModule; +import org.elasticsearch.search.internal.AliasFilter; +import org.elasticsearch.test.ClusterServiceUtils; +import org.elasticsearch.test.ESTestCase; +import org.elasticsearch.threadpool.TestThreadPool; +import org.elasticsearch.xcontent.NamedXContentRegistry; +import org.elasticsearch.xpack.esql.plugin.EsqlFlags; +import org.junit.AfterClass; +import org.junit.BeforeClass; + +import java.io.IOException; +import java.util.Collections; +import java.util.HashSet; +import java.util.List; + +import static org.elasticsearch.index.query.SearchExecutionContextHelper.SHARD_SEARCH_STATS; +import static org.hamcrest.Matchers.equalTo; + +public class ExpressionQueryListCircuitBreakerTests extends ESTestCase { + + private static TestThreadPool threadPool; + + @BeforeClass + public static void init() { + threadPool = new TestThreadPool("ExpressionQueryListCircuitBreakerTests"); + } + + @AfterClass + public static void cleanup() throws Exception { + terminate(threadPool); + threadPool = null; + } + + @Override + protected NamedXContentRegistry xContentRegistry() { + SearchModule searchModule = new SearchModule(Settings.EMPTY, Collections.emptyList()); + return new NamedXContentRegistry(searchModule.getNamedXContents()); + } + + @Override + protected NamedWriteableRegistry writableRegistry() { + SearchModule searchModule = new SearchModule(Settings.EMPTY, Collections.emptyList()); + return new NamedWriteableRegistry(searchModule.getNamedWriteables()); + } + + public void testCBReleasedAfterEachGetQueryCall() throws IOException { + Directory dir = new ByteBuffersDirectory(); + // Empty index – we only need a valid IndexSearcher for the context. + try (IndexWriter writer = new IndexWriter(dir, new IndexWriterConfig(new WhitespaceAnalyzer()))) { + // intentionally empty + } + + var registeredSettings = new HashSet<>(ClusterSettings.BUILT_IN_CLUSTER_SETTINGS); + registeredSettings.addAll(EsqlFlags.ALL_ESQL_FLAGS_SETTINGS); + + try ( + DirectoryReader reader = DirectoryReader.open(dir); + var clusterService = ClusterServiceUtils.createClusterService( + threadPool, + new ClusterSettings(Settings.EMPTY, registeredSettings) + ) + ) { + + IndexSearcher searcher = new IndexSearcher(reader); + IndexVersion indexVersion = IndexVersion.current(); + Settings indexSettingsSettings = indexSettings(indexVersion, 1, 1).build(); + IndexSettings indexSettings = new IndexSettings( + IndexMetadata.builder("test").settings(indexSettingsSettings).build(), + Settings.EMPTY + ); + KeywordFieldMapper fieldMapper = new KeywordFieldMapper.Builder("field", indexSettings).build( + MapperBuilderContext.root(false, false) + ); + MappingLookup mappingLookup = MappingLookup.fromMappers( + Mapping.EMPTY, + List.of(fieldMapper), + Collections.emptyList(), + IndexMode.STANDARD + ); + + SearchExecutionContext baseCtx = new SearchExecutionContext( + 0, + 0, + indexSettings, + null, + null, + null, + mappingLookup, + null, + null, + parserConfig(), + writableRegistry(), + null, + searcher, + System::currentTimeMillis, + null, + null, + () -> true, + null, + Collections.emptyMap(), + null, + MapperMetrics.NOOP, + SHARD_SEARCH_STATS + ); + + CircuitBreaker cb = newLimitedBreaker(ByteSizeValue.ofMb(100)); + SearchExecutionContext ctx = new SearchExecutionContext(baseCtx, cb); + WildcardQueryBuilder pushedQuery = new WildcardQueryBuilder("field", "*test*pattern*"); + ExpressionQueryList queryList = ExpressionQueryList.fieldBasedJoin( + Collections.emptyList(), + ctx, + null, + pushedQuery, + clusterService, + AliasFilter.EMPTY + ); + + assertEquals("CB must be zero before any getQuery() call", 0L, cb.getUsed()); + + int positions = 10; + for (int i = 0; i < positions; i++) { + long cbBefore = cb.getUsed(); + + queryList.getQuery(i, null, ctx); + + assertThat( + "CB tracked bytes must be zero after getQuery() (position " + i + ")", + ctx.getQueryConstructionMemoryUsed(), + equalTo(0L) + ); + assertThat("Raw CB usage must return to baseline after getQuery() (position " + i + ")", cb.getUsed(), equalTo(cbBefore)); + } + assertThat("No CB bytes should remain after all positions", cb.getUsed(), equalTo(0L)); + } + } +} From b3550bbb9d546aee38e44cb30322fd3dd8513670 Mon Sep 17 00:00:00 2001 From: elasticsearchmachine Date: Thu, 26 Mar 2026 11:57:53 +0000 Subject: [PATCH 2/8] [CI] Auto commit changes from spotless --- .../org/elasticsearch/percolator/QueryBuilderStoreTests.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/modules/percolator/src/test/java/org/elasticsearch/percolator/QueryBuilderStoreTests.java b/modules/percolator/src/test/java/org/elasticsearch/percolator/QueryBuilderStoreTests.java index 8669b7149144e..693c94709affa 100644 --- a/modules/percolator/src/test/java/org/elasticsearch/percolator/QueryBuilderStoreTests.java +++ b/modules/percolator/src/test/java/org/elasticsearch/percolator/QueryBuilderStoreTests.java @@ -48,10 +48,10 @@ import java.io.IOException; import java.util.Collections; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.when; import static org.elasticsearch.index.query.SearchExecutionContextHelper.SHARD_SEARCH_STATS; import static org.hamcrest.Matchers.greaterThan; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; public class QueryBuilderStoreTests extends ESTestCase { From 3269b9b2ad5285c0b5683e7af76c31ac9375806e Mon Sep 17 00:00:00 2001 From: Dimitris Rempapis Date: Thu, 26 Mar 2026 14:03:00 +0200 Subject: [PATCH 3/8] Update ExpressionQueryList.java --- .../esql/enrich/ExpressionQueryList.java | 37 +++++++------------ 1 file changed, 14 insertions(+), 23 deletions(-) diff --git a/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/enrich/ExpressionQueryList.java b/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/enrich/ExpressionQueryList.java index ba9d7ccdd5db3..d881644849f2f 100644 --- a/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/enrich/ExpressionQueryList.java +++ b/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/enrich/ExpressionQueryList.java @@ -256,31 +256,22 @@ private void buildPreJoinFilter(PhysicalPlan rightPreJoinPlan, ClusterService cl * @return The query at the given position, or null if any of the match fields are null. */ @Override - public Query getQuery(int position, Page inputPage, SearchExecutionContext searchExecutionContext) { - try { - BooleanQuery.Builder builder = new BooleanQuery.Builder(); - for (QueryList queryList : queryLists) { - Query q = queryList.getQuery(position, inputPage, searchExecutionContext); - if (q == null) { - // if any of the matchFields are null, it means there is no match for this position - // A AND NULL is always NULL, so we can skip this position - return null; - } - builder.add(q, BooleanClause.Occur.FILTER); - } - // also attach the pre-join filter if it exists - // Build queries from QueryBuilders dynamically to avoid caching stale IndexReader references - for (QueryBuilder queryBuilder : lucenePushableFilterBuilders) { - try { - builder.add(queryBuilder.toQuery(searchExecutionContext), BooleanClause.Occur.FILTER); - } catch (IOException e) { - throw new UncheckedIOException("Error while building query for Lucene pushable filter", e); - } + public Query getQuery(int position) { + BooleanQuery.Builder builder = new BooleanQuery.Builder(); + for (QueryList queryList : queryLists) { + Query q = queryList.getQuery(position); + if (q == null) { + // if any of the matchFields are null, it means there is no match for this position + // A AND NULL is always NULL, so we can skip this position + return null; } - return builder.build(); - } finally { - searchExecutionContext.releaseQueryConstructionMemory(); + builder.add(q, BooleanClause.Occur.FILTER); + } + // also attach the pre-join filter if it exists + for (Query preJoinFilter : preJoinFilters) { + builder.add(preJoinFilter, BooleanClause.Occur.FILTER); } + return builder.build(); } /** From 1684193dd645b519bfdf622bdac13b2cd987b127 Mon Sep 17 00:00:00 2001 From: Dimitris Rempapis Date: Thu, 26 Mar 2026 14:03:50 +0200 Subject: [PATCH 4/8] Delete x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/enrich/ExpressionQueryListCircuitBreakerTests.java --- ...xpressionQueryListCircuitBreakerTests.java | 167 ------------------ 1 file changed, 167 deletions(-) delete mode 100644 x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/enrich/ExpressionQueryListCircuitBreakerTests.java diff --git a/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/enrich/ExpressionQueryListCircuitBreakerTests.java b/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/enrich/ExpressionQueryListCircuitBreakerTests.java deleted file mode 100644 index 3a20ce85a7ec7..0000000000000 --- a/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/enrich/ExpressionQueryListCircuitBreakerTests.java +++ /dev/null @@ -1,167 +0,0 @@ -/* - * 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; you may not use this file except in compliance with the Elastic License - * 2.0. - */ -package org.elasticsearch.xpack.esql.enrich; - -import org.apache.lucene.analysis.core.WhitespaceAnalyzer; -import org.apache.lucene.index.DirectoryReader; -import org.apache.lucene.index.IndexWriter; -import org.apache.lucene.index.IndexWriterConfig; -import org.apache.lucene.search.IndexSearcher; -import org.apache.lucene.store.ByteBuffersDirectory; -import org.apache.lucene.store.Directory; -import org.elasticsearch.cluster.metadata.IndexMetadata; -import org.elasticsearch.common.breaker.CircuitBreaker; -import org.elasticsearch.common.io.stream.NamedWriteableRegistry; -import org.elasticsearch.common.settings.ClusterSettings; -import org.elasticsearch.common.settings.Settings; -import org.elasticsearch.common.unit.ByteSizeValue; -import org.elasticsearch.index.IndexMode; -import org.elasticsearch.index.IndexSettings; -import org.elasticsearch.index.IndexVersion; -import org.elasticsearch.index.mapper.KeywordFieldMapper; -import org.elasticsearch.index.mapper.MapperBuilderContext; -import org.elasticsearch.index.mapper.MapperMetrics; -import org.elasticsearch.index.mapper.Mapping; -import org.elasticsearch.index.mapper.MappingLookup; -import org.elasticsearch.index.query.SearchExecutionContext; -import org.elasticsearch.index.query.WildcardQueryBuilder; -import org.elasticsearch.search.SearchModule; -import org.elasticsearch.search.internal.AliasFilter; -import org.elasticsearch.test.ClusterServiceUtils; -import org.elasticsearch.test.ESTestCase; -import org.elasticsearch.threadpool.TestThreadPool; -import org.elasticsearch.xcontent.NamedXContentRegistry; -import org.elasticsearch.xpack.esql.plugin.EsqlFlags; -import org.junit.AfterClass; -import org.junit.BeforeClass; - -import java.io.IOException; -import java.util.Collections; -import java.util.HashSet; -import java.util.List; - -import static org.elasticsearch.index.query.SearchExecutionContextHelper.SHARD_SEARCH_STATS; -import static org.hamcrest.Matchers.equalTo; - -public class ExpressionQueryListCircuitBreakerTests extends ESTestCase { - - private static TestThreadPool threadPool; - - @BeforeClass - public static void init() { - threadPool = new TestThreadPool("ExpressionQueryListCircuitBreakerTests"); - } - - @AfterClass - public static void cleanup() throws Exception { - terminate(threadPool); - threadPool = null; - } - - @Override - protected NamedXContentRegistry xContentRegistry() { - SearchModule searchModule = new SearchModule(Settings.EMPTY, Collections.emptyList()); - return new NamedXContentRegistry(searchModule.getNamedXContents()); - } - - @Override - protected NamedWriteableRegistry writableRegistry() { - SearchModule searchModule = new SearchModule(Settings.EMPTY, Collections.emptyList()); - return new NamedWriteableRegistry(searchModule.getNamedWriteables()); - } - - public void testCBReleasedAfterEachGetQueryCall() throws IOException { - Directory dir = new ByteBuffersDirectory(); - // Empty index – we only need a valid IndexSearcher for the context. - try (IndexWriter writer = new IndexWriter(dir, new IndexWriterConfig(new WhitespaceAnalyzer()))) { - // intentionally empty - } - - var registeredSettings = new HashSet<>(ClusterSettings.BUILT_IN_CLUSTER_SETTINGS); - registeredSettings.addAll(EsqlFlags.ALL_ESQL_FLAGS_SETTINGS); - - try ( - DirectoryReader reader = DirectoryReader.open(dir); - var clusterService = ClusterServiceUtils.createClusterService( - threadPool, - new ClusterSettings(Settings.EMPTY, registeredSettings) - ) - ) { - - IndexSearcher searcher = new IndexSearcher(reader); - IndexVersion indexVersion = IndexVersion.current(); - Settings indexSettingsSettings = indexSettings(indexVersion, 1, 1).build(); - IndexSettings indexSettings = new IndexSettings( - IndexMetadata.builder("test").settings(indexSettingsSettings).build(), - Settings.EMPTY - ); - KeywordFieldMapper fieldMapper = new KeywordFieldMapper.Builder("field", indexSettings).build( - MapperBuilderContext.root(false, false) - ); - MappingLookup mappingLookup = MappingLookup.fromMappers( - Mapping.EMPTY, - List.of(fieldMapper), - Collections.emptyList(), - IndexMode.STANDARD - ); - - SearchExecutionContext baseCtx = new SearchExecutionContext( - 0, - 0, - indexSettings, - null, - null, - null, - mappingLookup, - null, - null, - parserConfig(), - writableRegistry(), - null, - searcher, - System::currentTimeMillis, - null, - null, - () -> true, - null, - Collections.emptyMap(), - null, - MapperMetrics.NOOP, - SHARD_SEARCH_STATS - ); - - CircuitBreaker cb = newLimitedBreaker(ByteSizeValue.ofMb(100)); - SearchExecutionContext ctx = new SearchExecutionContext(baseCtx, cb); - WildcardQueryBuilder pushedQuery = new WildcardQueryBuilder("field", "*test*pattern*"); - ExpressionQueryList queryList = ExpressionQueryList.fieldBasedJoin( - Collections.emptyList(), - ctx, - null, - pushedQuery, - clusterService, - AliasFilter.EMPTY - ); - - assertEquals("CB must be zero before any getQuery() call", 0L, cb.getUsed()); - - int positions = 10; - for (int i = 0; i < positions; i++) { - long cbBefore = cb.getUsed(); - - queryList.getQuery(i, null, ctx); - - assertThat( - "CB tracked bytes must be zero after getQuery() (position " + i + ")", - ctx.getQueryConstructionMemoryUsed(), - equalTo(0L) - ); - assertThat("Raw CB usage must return to baseline after getQuery() (position " + i + ")", cb.getUsed(), equalTo(cbBefore)); - } - assertThat("No CB bytes should remain after all positions", cb.getUsed(), equalTo(0L)); - } - } -} From cb4e2c28f313d13bf389c9a7f26687577137be75 Mon Sep 17 00:00:00 2001 From: Dimitris Rempapis Date: Thu, 26 Mar 2026 17:15:59 +0200 Subject: [PATCH 5/8] update code --- .../elasticsearch/percolator/QueryBuilderStoreTests.java | 5 +---- .../query/InnerHitContextBuilderCircuitBreakerTests.java | 6 ++---- .../phrase/PhraseSuggesterCircuitBreakerTests.java | 9 +++------ 3 files changed, 6 insertions(+), 14 deletions(-) diff --git a/modules/percolator/src/test/java/org/elasticsearch/percolator/QueryBuilderStoreTests.java b/modules/percolator/src/test/java/org/elasticsearch/percolator/QueryBuilderStoreTests.java index 693c94709affa..14d254f8a9a5b 100644 --- a/modules/percolator/src/test/java/org/elasticsearch/percolator/QueryBuilderStoreTests.java +++ b/modules/percolator/src/test/java/org/elasticsearch/percolator/QueryBuilderStoreTests.java @@ -48,7 +48,6 @@ import java.io.IOException; import java.util.Collections; -import static org.elasticsearch.index.query.SearchExecutionContextHelper.SHARD_SEARCH_STATS; import static org.hamcrest.Matchers.greaterThan; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; @@ -98,7 +97,6 @@ public void testStoringQueryBuilders() throws IOException { when(searchExecutionContext.getParserConfig()).thenReturn(parserConfig()); when(searchExecutionContext.getForField(fieldMapper.fieldType(), fielddataOperation)).thenReturn( new BytesBinaryIndexFieldData(fieldMapper.fullPath(), CoreValuesSourceType.KEYWORD) - ); when(searchExecutionContext.getFieldType(Mockito.anyString())).thenAnswer(invocation -> { final String fieldName = (String) invocation.getArguments()[0]; @@ -197,8 +195,7 @@ public void testCircuitBreakerReleasedAfterPerDocumentQueryConstruction() throws null, Collections.emptyMap(), null, - MapperMetrics.NOOP, - SHARD_SEARCH_STATS + MapperMetrics.NOOP ); SearchExecutionContext searchExecutionContext = new SearchExecutionContext(baseContext, circuitBreaker); diff --git a/server/src/test/java/org/elasticsearch/index/query/InnerHitContextBuilderCircuitBreakerTests.java b/server/src/test/java/org/elasticsearch/index/query/InnerHitContextBuilderCircuitBreakerTests.java index 95548d856cb57..2f8f2f7b4f102 100644 --- a/server/src/test/java/org/elasticsearch/index/query/InnerHitContextBuilderCircuitBreakerTests.java +++ b/server/src/test/java/org/elasticsearch/index/query/InnerHitContextBuilderCircuitBreakerTests.java @@ -38,7 +38,6 @@ import java.util.Collections; import java.util.List; -import static org.elasticsearch.index.query.SearchExecutionContextHelper.SHARD_SEARCH_STATS; import static org.hamcrest.Matchers.equalTo; import static org.hamcrest.Matchers.greaterThan; @@ -72,7 +71,7 @@ public void testCBTrackedDuringInnerHitsAndReleasedAtRequestEnd() throws IOExcep IndexMetadata.builder("test").settings(indexSettingsSettings).build(), Settings.EMPTY ); - KeywordFieldMapper fieldMapper = new KeywordFieldMapper.Builder("field", indexSettings).build( + KeywordFieldMapper fieldMapper = new KeywordFieldMapper.Builder("field", indexSettings.getIndexVersionCreated()).build( MapperBuilderContext.root(false, false) ); MappingLookup mappingLookup = MappingLookup.fromMappers( @@ -103,8 +102,7 @@ public void testCBTrackedDuringInnerHitsAndReleasedAtRequestEnd() throws IOExcep null, Collections.emptyMap(), null, - MapperMetrics.NOOP, - SHARD_SEARCH_STATS + MapperMetrics.NOOP ); CircuitBreaker cb = newLimitedBreaker(ByteSizeValue.ofMb(100)); diff --git a/server/src/test/java/org/elasticsearch/search/suggest/phrase/PhraseSuggesterCircuitBreakerTests.java b/server/src/test/java/org/elasticsearch/search/suggest/phrase/PhraseSuggesterCircuitBreakerTests.java index 0977b6c423472..1c084f7a65870 100644 --- a/server/src/test/java/org/elasticsearch/search/suggest/phrase/PhraseSuggesterCircuitBreakerTests.java +++ b/server/src/test/java/org/elasticsearch/search/suggest/phrase/PhraseSuggesterCircuitBreakerTests.java @@ -53,7 +53,6 @@ import java.util.List; import java.util.Map; -import static org.elasticsearch.index.query.SearchExecutionContextHelper.SHARD_SEARCH_STATS; import static org.hamcrest.Matchers.equalTo; public class PhraseSuggesterCircuitBreakerTests extends ESTestCase { @@ -111,7 +110,7 @@ protected TokenStreamComponents createComponents(String fieldName) { IndexMetadata.builder("test").settings(indexSettingsSettings).build(), Settings.EMPTY ); - KeywordFieldMapper bodyMapper = new KeywordFieldMapper.Builder("body", indexSettings).build( + KeywordFieldMapper bodyMapper = new KeywordFieldMapper.Builder("body", indexSettings.getIndexVersionCreated()).build( MapperBuilderContext.root(false, false) ); MappingLookup mappingLookup = MappingLookup.fromMappers( @@ -142,8 +141,7 @@ protected TokenStreamComponents createComponents(String fieldName) { null, Collections.emptyMap(), null, - MapperMetrics.NOOP, - SHARD_SEARCH_STATS + MapperMetrics.NOOP ); CircuitBreaker cb = newLimitedBreaker(ByteSizeValue.ofMb(100)); SearchExecutionContext ctx = new SearchExecutionContext(baseCtx, cb); @@ -230,8 +228,7 @@ public void testNoCBUsageWithoutCollateScript() throws IOException { null, Collections.emptyMap(), null, - MapperMetrics.NOOP, - SHARD_SEARCH_STATS + MapperMetrics.NOOP ); SearchExecutionContext ctx = new SearchExecutionContext(baseCtx, cb); From b49e956a4887639c5c53f9c6b54f0b4c43865958 Mon Sep 17 00:00:00 2001 From: Dimitris Rempapis Date: Thu, 26 Mar 2026 17:19:26 +0200 Subject: [PATCH 6/8] update --- .../org/elasticsearch/percolator/PercolateQueryBuilder.java | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/modules/percolator/src/main/java/org/elasticsearch/percolator/PercolateQueryBuilder.java b/modules/percolator/src/main/java/org/elasticsearch/percolator/PercolateQueryBuilder.java index 62d4704cc43a2..5f5c6222f3771 100644 --- a/modules/percolator/src/main/java/org/elasticsearch/percolator/PercolateQueryBuilder.java +++ b/modules/percolator/src/main/java/org/elasticsearch/percolator/PercolateQueryBuilder.java @@ -677,17 +677,17 @@ public void addNamedQuery(String name, Query query) { @Override public void addCircuitBreakerMemory(long bytes, String label) { - source.addCircuitBreakerMemory(bytes, label); + delegate.addCircuitBreakerMemory(bytes, label); } @Override public long getQueryConstructionMemoryUsed() { - return source.getQueryConstructionMemoryUsed(); + return delegate.getQueryConstructionMemoryUsed(); } @Override public void releaseQueryConstructionMemory() { - source.releaseQueryConstructionMemory(); + delegate.releaseQueryConstructionMemory(); } }; } From 4627ac84f57e9ca97f3db41f0365ddcd8def7fb4 Mon Sep 17 00:00:00 2001 From: Dimitris Rempapis Date: Thu, 26 Mar 2026 18:05:10 +0200 Subject: [PATCH 7/8] update --- .../percolator/QueryBuilderStoreTests.java | 17 +++++++++++++---- 1 file changed, 13 insertions(+), 4 deletions(-) diff --git a/modules/percolator/src/test/java/org/elasticsearch/percolator/QueryBuilderStoreTests.java b/modules/percolator/src/test/java/org/elasticsearch/percolator/QueryBuilderStoreTests.java index 14d254f8a9a5b..95178d60b4be2 100644 --- a/modules/percolator/src/test/java/org/elasticsearch/percolator/QueryBuilderStoreTests.java +++ b/modules/percolator/src/test/java/org/elasticsearch/percolator/QueryBuilderStoreTests.java @@ -25,13 +25,21 @@ import org.elasticsearch.common.settings.Settings; import org.elasticsearch.common.unit.ByteSizeValue; import org.elasticsearch.core.CheckedFunction; +import org.elasticsearch.index.IndexMode; +import org.elasticsearch.index.IndexSettings; import org.elasticsearch.index.IndexVersion; +import org.elasticsearch.index.fielddata.FieldDataContext; +import org.elasticsearch.index.fielddata.IndexFieldData; import org.elasticsearch.index.fielddata.plain.BytesBinaryIndexFieldData; import org.elasticsearch.index.mapper.BinaryFieldMapper; import org.elasticsearch.index.mapper.DocumentParserContext; +import org.elasticsearch.index.mapper.FieldMapper; import org.elasticsearch.index.mapper.KeywordFieldMapper; import org.elasticsearch.index.mapper.MappedFieldType; import org.elasticsearch.index.mapper.MapperBuilderContext; +import org.elasticsearch.index.mapper.MapperMetrics; +import org.elasticsearch.index.mapper.Mapping; +import org.elasticsearch.index.mapper.MappingLookup; import org.elasticsearch.index.mapper.TestDocumentParserContext; import org.elasticsearch.index.query.QueryBuilder; import org.elasticsearch.index.query.RegexpQueryBuilder; @@ -43,10 +51,13 @@ import org.elasticsearch.search.aggregations.support.CoreValuesSourceType; import org.elasticsearch.test.ESTestCase; import org.elasticsearch.xcontent.NamedXContentRegistry; +import org.elasticsearch.xcontent.XContentParserConfiguration; import org.mockito.Mockito; import java.io.IOException; import java.util.Collections; +import java.util.List; +import java.util.function.BiFunction; import static org.hamcrest.Matchers.greaterThan; import static org.mockito.Mockito.mock; @@ -157,7 +168,7 @@ public void testCircuitBreakerReleasedAfterPerDocumentQueryConstruction() throws Settings.EMPTY ); - KeywordFieldMapper keywordMapper = new KeywordFieldMapper.Builder(fieldName, indexSettings).build( + KeywordFieldMapper keywordMapper = new KeywordFieldMapper.Builder(fieldName, indexSettings.getIndexVersionCreated()).build( MapperBuilderContext.root(false, false) ); MappingLookup mappingLookup = MappingLookup.fromMappers( @@ -169,8 +180,7 @@ public void testCircuitBreakerReleasedAfterPerDocumentQueryConstruction() throws BytesBinaryIndexFieldData fieldData = new BytesBinaryIndexFieldData( fieldMapper.fullPath(), - CoreValuesSourceType.KEYWORD, - BinaryDocValuesField::new + CoreValuesSourceType.KEYWORD ); BiFunction> indexFieldDataLookup = (mft, fdc) -> fieldData; @@ -201,7 +211,6 @@ public void testCircuitBreakerReleasedAfterPerDocumentQueryConstruction() throws PercolateQuery.QueryStore queryStore = PercolateQueryBuilder.createStore( fieldMapper.fieldType(), - false, searchExecutionContext ); From 9d874975989f13bf8d04d5f87b7580fc6ed183d9 Mon Sep 17 00:00:00 2001 From: elasticsearchmachine Date: Thu, 26 Mar 2026 16:13:18 +0000 Subject: [PATCH 8/8] [CI] Auto commit changes from spotless --- .../percolator/QueryBuilderStoreTests.java | 12 ++---------- 1 file changed, 2 insertions(+), 10 deletions(-) diff --git a/modules/percolator/src/test/java/org/elasticsearch/percolator/QueryBuilderStoreTests.java b/modules/percolator/src/test/java/org/elasticsearch/percolator/QueryBuilderStoreTests.java index 95178d60b4be2..24ec8052c31c9 100644 --- a/modules/percolator/src/test/java/org/elasticsearch/percolator/QueryBuilderStoreTests.java +++ b/modules/percolator/src/test/java/org/elasticsearch/percolator/QueryBuilderStoreTests.java @@ -33,7 +33,6 @@ import org.elasticsearch.index.fielddata.plain.BytesBinaryIndexFieldData; import org.elasticsearch.index.mapper.BinaryFieldMapper; import org.elasticsearch.index.mapper.DocumentParserContext; -import org.elasticsearch.index.mapper.FieldMapper; import org.elasticsearch.index.mapper.KeywordFieldMapper; import org.elasticsearch.index.mapper.MappedFieldType; import org.elasticsearch.index.mapper.MapperBuilderContext; @@ -46,7 +45,6 @@ import org.elasticsearch.index.query.SearchExecutionContext; import org.elasticsearch.index.query.TermQueryBuilder; import org.elasticsearch.index.query.WildcardQueryBuilder; -import org.elasticsearch.script.field.BinaryDocValuesField; import org.elasticsearch.search.SearchModule; import org.elasticsearch.search.aggregations.support.CoreValuesSourceType; import org.elasticsearch.test.ESTestCase; @@ -178,10 +176,7 @@ public void testCircuitBreakerReleasedAfterPerDocumentQueryConstruction() throws IndexMode.STANDARD ); - BytesBinaryIndexFieldData fieldData = new BytesBinaryIndexFieldData( - fieldMapper.fullPath(), - CoreValuesSourceType.KEYWORD - ); + BytesBinaryIndexFieldData fieldData = new BytesBinaryIndexFieldData(fieldMapper.fullPath(), CoreValuesSourceType.KEYWORD); BiFunction> indexFieldDataLookup = (mft, fdc) -> fieldData; SearchExecutionContext baseContext = new SearchExecutionContext( @@ -209,10 +204,7 @@ public void testCircuitBreakerReleasedAfterPerDocumentQueryConstruction() throws ); SearchExecutionContext searchExecutionContext = new SearchExecutionContext(baseContext, circuitBreaker); - PercolateQuery.QueryStore queryStore = PercolateQueryBuilder.createStore( - fieldMapper.fieldType(), - searchExecutionContext - ); + PercolateQuery.QueryStore queryStore = PercolateQueryBuilder.createStore(fieldMapper.fieldType(), searchExecutionContext); try (IndexReader indexReader = DirectoryReader.open(directory)) { LeafReaderContext leafContext = indexReader.leaves().get(0);