Skip to content

Commit ad10f09

Browse files
authored
Merge branch 'main' into account_for_155827_bug
2 parents fa78e5c + d15503d commit ad10f09

10 files changed

Lines changed: 336 additions & 46 deletions

File tree

.buildkite/scripts/dra-workflow.sh

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,11 @@ if [[ -n "${VERSION_QUALIFIER:-}" ]]; then
5959
fi
6060

6161
echo --- install qemu for aarch64 docker image builds
62-
docker run --privileged --rm tonistiigi/binfmt:qemu-v9.2.2 --install all
62+
# NOTE: qemu-v9.2.2 mishandles openat2(O_NOFOLLOW) on aarch64 (glibc tar's
63+
# CVE-2025-45582 fix triggers this), causing "tar: ...: Cannot open: Invalid
64+
# argument" during linux/arm64 cross builds. qemu-v10.2.3 has the upstream
65+
# fix. See https://github.com/tonistiigi/binfmt/issues/285.
66+
docker run --privileged --rm tonistiigi/binfmt:qemu-v10.2.3 --install all
6367
docker buildx create --driver docker-container --use --bootstrap
6468

6569
echo --- Building release artifacts

.buildkite/scripts/fixture-deploy.sh

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,11 @@ unset DOCKER_REGISTRY_USERNAME DOCKER_REGISTRY_PASSWORD
1010
# registration does not survive the image bake -> boot cycle, so it must
1111
# be redone here on every job run. See
1212
# https://github.com/elastic/ci-agent-images/pull/2907 for details.
13-
docker run --privileged --rm tonistiigi/binfmt:qemu-v9.2.2 --install all
13+
#
14+
# NOTE: qemu-v9.2.2 mishandles openat2(O_NOFOLLOW) on aarch64 (glibc tar's
15+
# CVE-2025-45582 fix triggers this), causing "tar: ...: Cannot open: Invalid
16+
# argument" during linux/arm64 cross builds. qemu-v10.2.3 has the upstream
17+
# fix. See https://github.com/tonistiigi/binfmt/issues/285.
18+
docker run --privileged --rm tonistiigi/binfmt:qemu-v10.2.3 --install all
1419
docker buildx create --driver docker-container --use --bootstrap
1520
.ci/scripts/run-gradle.sh deployFixtureDockerImages

docs/changelog/152339.yaml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
area: Inference
2+
issues: []
3+
pr: 152339
4+
summary: "[Inference API] Use new default 'regionless' EIS URL for CCM"
5+
type: enhancement

muted-tests.yml

Lines changed: 0 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -247,12 +247,6 @@ tests:
247247
- class: org.elasticsearch.upgrades.MlMappingsUpgradeIT
248248
method: testMappingsUpgrade
249249
issue: https://github.com/elastic/elasticsearch/issues/152115
250-
- class: org.elasticsearch.xpack.esql.qa.multi_node.GenerativeIT
251-
method: test {feature:SUBQUERIES}
252-
issue: https://github.com/elastic/elasticsearch/issues/149681
253-
- class: org.elasticsearch.xpack.esql.qa.single_node.GenerativeIT
254-
method: test {feature:SUBQUERIES}
255-
issue: https://github.com/elastic/elasticsearch/issues/149681
256250
- class: org.elasticsearch.xpack.esql.action.ExternalSourceProfileIT
257251
method: testCsvCountStarScansColdThenSkipsWarm
258252
issue: https://github.com/elastic/elasticsearch/issues/152132
@@ -418,9 +412,6 @@ tests:
418412
- class: org.elasticsearch.upgrades.DataStreamsUpgradeIT
419413
method: testUpgradeDataStream
420414
issue: https://github.com/elastic/elasticsearch/issues/154593
421-
- class: org.elasticsearch.xpack.stateless.cache.SharedBlobCacheWarmingServiceTests
422-
method: testPopulateCacheWithSharedSourceInputStreamFactory
423-
issue: https://github.com/elastic/elasticsearch/issues/154597
424415
- class: org.elasticsearch.backwards.MixedClusterClientYamlTestSuiteIT
425416
method: test {p0=indices.sort/30_multi_value/Index Sort with doc_values multi_value false - integer}
426417
issue: https://github.com/elastic/elasticsearch/issues/154602

x-pack/plugin/esql/compute/src/main/java/org/elasticsearch/compute/operator/HighlightOperator.java

Lines changed: 144 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -8,10 +8,17 @@
88
package org.elasticsearch.compute.operator;
99

1010
import org.apache.lucene.analysis.Analyzer;
11-
import org.apache.lucene.index.LeafReaderContext;
11+
import org.apache.lucene.analysis.CharArraySet;
12+
import org.apache.lucene.analysis.FilteringTokenFilter;
13+
import org.apache.lucene.analysis.TokenStream;
14+
import org.apache.lucene.analysis.tokenattributes.CharTermAttribute;
15+
import org.apache.lucene.index.LeafReader;
16+
import org.apache.lucene.index.Term;
1217
import org.apache.lucene.index.memory.MemoryIndex;
18+
import org.apache.lucene.search.BooleanClause;
1319
import org.apache.lucene.search.IndexSearcher;
1420
import org.apache.lucene.search.Query;
21+
import org.apache.lucene.search.QueryVisitor;
1522
import org.apache.lucene.search.highlight.DefaultEncoder;
1623
import org.apache.lucene.search.highlight.Encoder;
1724
import org.apache.lucene.search.highlight.SimpleHTMLEncoder;
@@ -20,6 +27,7 @@
2027
import org.apache.lucene.search.uhighlight.SplittingBreakIterator;
2128
import org.apache.lucene.search.uhighlight.UnifiedHighlighter;
2229
import org.apache.lucene.util.BytesRef;
30+
import org.apache.lucene.util.automaton.ByteRunAutomaton;
2331
import org.elasticsearch.common.settings.Settings;
2432
import org.elasticsearch.compute.data.Block;
2533
import org.elasticsearch.compute.data.BlockFactory;
@@ -88,6 +96,10 @@ public String describe() {
8896
private final int highlighterNumberOfFragments;
8997
private final Supplier<BreakIterator> breakIteratorSupplier;
9098
private final ExpressionEvaluator[] fieldEvaluators;
99+
private final MemoryIndex memoryIndex;
100+
private LeafReader memoryIndexReader;
101+
private final CustomUnifiedHighlighter[] highlighters;
102+
private final CharArraySet termsToKeep;
91103

92104
public HighlightOperator(BlockFactory blockFactory, HighlightConfig config, ExpressionEvaluator[] fieldEvaluators) {
93105
this.blockFactory = blockFactory;
@@ -116,6 +128,76 @@ public HighlightOperator(BlockFactory blockFactory, HighlightConfig config, Expr
116128
config.wordBoundary(),
117129
config.locale()
118130
);
131+
this.memoryIndex = new MemoryIndex(true); // true == store offsets, required by OffsetSource.POSTINGS
132+
// Term extraction for the highlighters only.
133+
IndexSearcher searcher = memoryIndex.createSearcher();
134+
this.highlighters = new CustomUnifiedHighlighter[fieldNames.size()];
135+
for (int i = 0; i < fieldNames.size(); i++) {
136+
UnifiedHighlighter.Builder builder = UnifiedHighlighter.builder(searcher, analyzer);
137+
builder.withFormatter(formatter);
138+
builder.withBreakIterator(breakIteratorSupplier);
139+
highlighters[i] = new CustomUnifiedHighlighter(
140+
builder,
141+
UnifiedHighlighter.OffsetSource.POSTINGS,
142+
null,
143+
"",
144+
fieldNames.get(i),
145+
query,
146+
config.noMatchSize(),
147+
highlighterNumberOfFragments,
148+
indexMaxAnalyzedOffset,
149+
queryMaxAnalyzedOffset,
150+
true,
151+
true
152+
);
153+
}
154+
this.termsToKeep = termsToKeep(query);
155+
}
156+
157+
/**
158+
* Collects the terms the query needs, either to highlight or to decide whether a row matches. Returns {@code null}
159+
* when a clause's terms cannot be enumerated (wildcards, regexps, or any leaf that reports no terms), which turns
160+
* filtering off. Keeping too many tokens only costs time. Keeping too few would drop real highlights.
161+
* <p>
162+
* One set covers every ON field, so a field can keep tokens that only another field's query mentions. Those never
163+
* become a highlight, because the highlighter looks up postings per {@code field:term}.
164+
* <p>
165+
* {@code MUST_NOT} terms are kept as well. The query still runs against the memory index to decide whether the row
166+
* matches, so dropping them would turn an excluded row into a match.
167+
*/
168+
private static CharArraySet termsToKeep(Query query) {
169+
TermCollector collector = new TermCollector();
170+
query.visit(collector);
171+
return collector.unfilterable || collector.terms.isEmpty() ? null : collector.terms;
172+
}
173+
174+
private static final class TermCollector extends QueryVisitor {
175+
// Create terms set with an initial capacity.
176+
private final CharArraySet terms = new CharArraySet(8, false);
177+
private boolean unfilterable;
178+
179+
@Override
180+
public void consumeTerms(Query query, Term... queryTerms) {
181+
for (Term term : queryTerms) {
182+
terms.add(term.text());
183+
}
184+
}
185+
186+
@Override
187+
public void consumeTermsMatching(Query query, String field, Supplier<ByteRunAutomaton> automaton) {
188+
unfilterable = true; // wildcard/prefix/regexp, whose terms cannot be enumerated
189+
}
190+
191+
@Override
192+
public void visitLeaf(Query query) {
193+
unfilterable = true; // leaf that reported no terms, we don't know what it matches
194+
}
195+
196+
@Override
197+
public QueryVisitor getSubVisitor(BooleanClause.Occur occur, Query parent) {
198+
// QueryVisitor's default returns EMPTY_VISITOR for MUST_NOT, which would skip its terms.
199+
return this;
200+
}
119201
}
120202

121203
// Mirrors DefaultHighlighter#getBreakIterator: the word scanner ignores fragment_size, the sentence scanner honours it.
@@ -200,28 +282,78 @@ private void highlightRow(int row, HighlightField[] fields, BytesRef scratch) {
200282
appendNulls(fields);
201283
return;
202284
}
203-
IndexSearcher searcher = createRowSearcher(fields);
204-
for (HighlightField field : fields) {
285+
if (indexRow(fields) == false) {
286+
appendNulls(fields);
287+
return;
288+
}
289+
for (int fieldIndex = 0; fieldIndex < fields.length; fieldIndex++) {
290+
HighlightField field = fields[fieldIndex];
205291
if (field.rowText == null) {
206292
field.builder.appendNull();
207293
continue;
208294
}
209295
try {
210-
appendSnippets(field.builder, highlight(searcher, field.name, field.rowText));
296+
appendSnippets(field.builder, highlight(fieldIndex, field.rowText));
211297
} catch (IOException e) {
212298
throw new IllegalStateException("HIGHLIGHT failed for ON field [" + field.name + "]", e);
213299
}
214300
}
215301
}
216302

217-
private IndexSearcher createRowSearcher(HighlightField[] fields) {
218-
MemoryIndex memoryIndex = new MemoryIndex(true);
303+
/**
304+
* Analyzes this row's values into the shared memory index. Returns {@code false} when filtering kept nothing the
305+
* query could match and {@code no_match_size} is 0, so every field of the row is {@code null} and the caller can
306+
* skip the highlighters.
307+
*/
308+
private boolean indexRow(HighlightField[] fields) {
309+
memoryIndex.reset();
310+
boolean keptToken = false;
219311
for (HighlightField field : fields) {
220-
if (field.rowText != null) {
312+
if (field.rowText == null) {
313+
continue;
314+
}
315+
if (termsToKeep == null) {
221316
memoryIndex.addField(field.name, field.rowText, memoryIndexAnalyzer);
317+
} else {
318+
TokenStream tokenStream = memoryIndexAnalyzer.tokenStream(field.name, field.rowText);
319+
KeepQueryTermsFilter filtered = new KeepQueryTermsFilter(tokenStream, termsToKeep);
320+
memoryIndex.addField(field.name, filtered); // addField resets and closes the stream
321+
keptToken |= filtered.keptToken;
222322
}
223323
}
224-
return memoryIndex.createSearcher();
324+
// With filtering off keptToken stays false, so it says nothing about the row.
325+
if (termsToKeep != null && keptToken == false && config.noMatchSize() == 0) {
326+
return false;
327+
}
328+
// MemoryIndex snapshots FieldInfos at reader construction, so create it after addField.
329+
memoryIndexReader = (LeafReader) memoryIndex.createSearcher().getIndexReader();
330+
return true;
331+
}
332+
333+
/**
334+
* Drops tokens the query cannot match, so the memory index only hashes and sorts the terms the query asks for.
335+
* {@link FilteringTokenFilter} accumulates the position increments of dropped tokens, so kept tokens keep their
336+
* original positions and phrase queries match as they would against an unfiltered index.
337+
* <p>
338+
* Query DSL highlighting gets the same filtering for free from Lucene's {@code MemoryIndexOffsetStrategy}, but that
339+
* strategy indexes one field at a time, and cross-field queries here need every ON field in one index.
340+
*/
341+
private static final class KeepQueryTermsFilter extends FilteringTokenFilter {
342+
private final CharArraySet terms;
343+
private final CharTermAttribute termAtt = addAttribute(CharTermAttribute.class);
344+
private boolean keptToken;
345+
346+
KeepQueryTermsFilter(TokenStream in, CharArraySet terms) {
347+
super(in);
348+
this.terms = terms;
349+
}
350+
351+
@Override
352+
protected boolean accept() {
353+
boolean keep = terms.contains(termAtt.buffer(), 0, termAtt.length());
354+
keptToken |= keep;
355+
return keep;
356+
}
225357
}
226358

227359
private static void appendNulls(HighlightField[] fields) {
@@ -275,28 +407,10 @@ private static String joinValues(BytesRefBlock fieldValues, int row, int valueCo
275407
return sb.toString();
276408
}
277409

278-
// TODO(perf): reuse a per-field CustomUnifiedHighlighter across rows; the Query is constant and the searcher
279-
// argument is unused under POSTINGS + WEIGHT_MATCHES today (Lucene internal — guard with a multi-row test).
280-
private Snippet[] highlight(IndexSearcher searcher, String field, String text) throws IOException {
281-
UnifiedHighlighter.Builder builder = UnifiedHighlighter.builder(searcher, analyzer);
282-
builder.withFormatter(formatter);
283-
builder.withBreakIterator(breakIteratorSupplier);
284-
CustomUnifiedHighlighter highlighter = new CustomUnifiedHighlighter(
285-
builder,
286-
UnifiedHighlighter.OffsetSource.POSTINGS,
287-
null,
288-
"",
289-
field,
290-
query,
291-
config.noMatchSize(),
292-
highlighterNumberOfFragments,
293-
indexMaxAnalyzedOffset,
294-
queryMaxAnalyzedOffset,
295-
true,
296-
true
297-
);
298-
LeafReaderContext leaf = searcher.getIndexReader().leaves().getFirst();
299-
return highlighter.highlightField(leaf.reader(), 0, () -> text);
410+
// CustomUnifiedHighlighter derives its FieldHighlighter from the query at build time and caches nothing from the
411+
// reader, so the constructor's per-field instances can be reused for every row and page.
412+
private Snippet[] highlight(int fieldIndex, String text) throws IOException {
413+
return highlighters[fieldIndex].highlightField(memoryIndexReader, 0, () -> text);
300414
}
301415

302416
/**

0 commit comments

Comments
 (0)