CompositeVerifyQuery finds candidates with a cheap index query, then checks each one against an exact shape. Before reusing a cached result on a segment, the query cache asks the query whether that is safe after doc-values updates; this query answers for its shape check only.
CompositeVerifyQuery(indexQuery, predicate)in spatial-extras runsindexQueryto find candidate documents, then checks each candidate against an exact shape (the predicate). Its constructor is public and accepts any query.- A query cache (
LRUQueryCache) stores a query's matching documents per segment. Before it reuses an entry it asks the query's WeightisCacheable(segment). A Weight that reads doc values must say no once those doc values were updated in place. IndexWriter#updateNumericDocValuechanges a doc-values field without rewriting the segment. The reopened segment can keep its cache key (it does in the repro), and then only thatisCacheableanswer stops the cache from serving old matches.
- Index six shops at points (0,0) to (5,5) with a
CompositeSpatialStrategy; shops 0-2 are open (open= 1, a doc-values field). Searchnew CompositeVerifyQuery("in the area" AND open == 1, exact shape check)through a query cache: [0, 1, 2]. - Shop 0 closes and shop 4 opens: two
updateNumericDocValuecalls onopen, then reopen the reader. You expect [1, 2, 4], and an uncached searcher on the new reader returns [1, 2, 4]. - Instead the same search through the cache returns [0, 1, 2]. Asked "is your cached result still valid?", the composite checks only its shape field, which did not change, and never asks its index query, which would say no. The in-place update left the segment's cache key unchanged, so the old entry is served.
- It lasts until the segment is merged or the entry is evicted or cleared. On 7.2.0-10.5.2 and 10.6.0 the default
IndexSearchercache is enough; main has no default cache, so the application must install one.CompositeSpatialStrategy's own queries are not affected.
Applications that build CompositeVerifyQuery themselves around an index query that reads a doc-values field (an open or visible flag next to the spatial filter), update that field in place, and keep a query cache across reopens: the default IndexSearcher cache on 7.2.0-10.5.2 and 10.6.0, an installed one on main. Rare: no in-tree code builds such a query.
Mechanism, terms and numbers936 words
CompositeVerifyQuery(Query indexQuery, ShapeValuesPredicate predicate) (spatial-extras, public constructor, @lucene.experimental) finds candidates with its index query and verifies each one with the shape predicate. Its Weight iterates the index query's scorer as the approximation of every match, but its isCacheable asks only the predicate (return predicateValueSource.isCacheable(ctx);, CompositeVerifyQuery.java:113-116 on main a9779ef3c193). LRUQueryCache trusts Weight#isCacheable and keys entries on the segment's core cache key, which a doc-values update may leave unchanged (LeafReader allows it, and in the repro it stays the same). So when the index query reads a doc-values field that is updated in place (IndexWriter#updateNumericDocValue or updateBinaryDocValue), the entry cached before the update keeps being served after the reopen: [0, 1, 2] instead of [1, 2, 4] for an "in the area AND open" index query, [0, 1, 2] instead of [1, 2] for a bare newSlowExactQuery, 5025 instead of 5024 hits with the release-default cache settings. The index query on its own is correctly uncacheable on that segment. On 7.2.0-10.5.2 and 10.6.0 (released 2026-10-29) a plain IndexSearcher reproduces it with zero configuration; main has had no default cache since #14187 (unreleased), so there it needs an installed cache. CompositeSpatialStrategy's own queries (prefix-tree index queries) are not affected. Introduced by LUCENE-8042 (276e317e942), which replaced a never-cacheable "TODO delegate to PredicateValueSource?" with the predicate check and left out the index-query Weight built in the same method. The fix ANDs indexQueryWeight.isCacheable(ctx) in.
- CompositeVerifyQuery: "A Query that considers an 'indexQuery' to have approximate results, and a follow-on ShapeValuesSource is called to verify each hit" (
CompositeVerifyQuery.java:33-35; the field comment at:41says "approximation (matches more than needed)"). Public constructor (:44),@lucene.experimental(:37).CompositeSpatialStrategybuilds it around a prefix-tree (RPT) query; applications can build it around any query. - Query cache:
LRUQueryCachestores the matching documents of a query per segment, keyed on the query and the segment's core cache key. It asksWeight#isCacheable(leaf)before it reads or writes an entry (LRUQueryCache.java:816-834and:903-921). - In-place doc-values update:
IndexWriter#updateNumericDocValueandupdateBinaryDocValuechange a doc-values-only field without rewriting the segment. The reopened segment may keep its core key: "Two readers that have the same data but different sets of deleted documents or doc values updates may be considered equal" (LeafReader.java:66-70); the test asserts it does. That is why a query that reads an updated doc-values field must say it is not cacheable: "Updated doc values fields are not suitable for caching" (SegmentCacheable.java:30-32).
createWeightbuilds the index query's Weight once (CompositeVerifyQuery.java:95-97) and feeds its scorer's iterator to the predicate's two-phase iterator (:102-108): every match is an index-query match that the predicate confirmed.isCacheablereturnspredicateValueSource.isCacheable(ctx)and nothing else (:113-116).- A doc-values index query such as
NumericDocValuesField.newSlowExactQuery("open", 1)reportsDocValues.isCacheable(ctx, "open")(SortedNumericDocValuesRangeQuery.java:119-121), false once the field was updated in that segment (DocValues.java:339-346); a smallBooleanQueryANDs its clauses' answers (BooleanWeight.java:272-285). The composite's Weight never asks it. - After the update and reopen, the cache asks the composite's Weight, gets true, finds the old entry under the unchanged core key and returns it. The index query's Weight is not consulted, because the outer entry is served before the composite's scorer is built.
Six shops at (i, i), one segment, indexed with a CompositeSpatialStrategy; shops 0-2 open. CompositeVerifyQuery(BooleanQuery(FILTER rpt Intersects area, FILTER open == 1), exact shape Intersects area) returns [0, 1, 2]. After updateNumericDocValue(id:0, open, 0), updateNumericDocValue(id:4, open, 1) and openIfChanged: uncached [1, 2, 4], cached [0, 1, 2]: a closed shop returned, an open one missing. The same flag as a sibling clause outside the composite is correct through the same cache. With the bare index query newSlowExactQuery("v", 1) and doc 0 changed from v=1 to v=2: cached [0, 1, 2] instead of [1, 2]. With the release-default cache settings (LRUQueryCache(1000, 32 MB), which is the releases' min(32 MB, heap/20) for heaps of 640 MB or more, and UsageTrackingQueryCachingPolicy; 10,050 documents, query counted 10 times): 5025 instead of 5024. The composite's isCacheable is true on the updated segment while the index query's is false.
- Needs: an index query that reads a doc-values field, an in-place update of that field, the composite cached on that segment before the update, and a cache that survives the reopen. The composite must run without scores, as a count, a filter clause or a no-score collector does: only then does
IndexSearcher#createWeightwrap it in the cache (IndexSearcher.java:976-982). - Affected index queries (model-based harness, 14 stale results):
newSlowExactQuery,newSlowRangeQuery, aBooleanQuerywith such aFILTERclause,FieldExistsQueryon a doc-values field,SerializedDVStrategy.makeQueryafter a binary doc-values update. - Not affected, by the code:
CompositeSpatialStrategy.makeQuery(Intersects builds anIntersectsRPTVerifyQuery,CompositeSpatialStrategy.java:122-134; the other operations wrap an RPT query,:154-155, whose Weight is always cacheable,AbstractPrefixTreeQuery.java:107-108, or aTermQueryfor a point shape on a points-only grid, also always cacheable,RecursivePrefixTreeStrategy.java:185-187, 216-229,TermQuery.java:192-194). - No mismatch observed in the harness (its 9 index-query kinds x 9 index operations) for
TermQuery,IntPointrange and kNN index queries, an update of the predicate's own shape field, deletes, new segments andupdateDocument. - The stale entry ends when the segment is merged into a new one, the entry is evicted, the application clears the cache (
LRUQueryCache.java:458), or searches switch to another cache;LRUQueryCache#closekeeps the entries and only stops an optional cleanup thread (:264-275). The duration in a running application was not measured.
Exposure and likelihood288 words
- Intended public use. LUCENE-5579 (#6641), which added the class, "Pulled out the inner Query instances to live in the 'composite' package, so that they might be used by users who want to build with these constructs", after asking how users "can put other query/filters in-between RPT and the SerializedDVStrategy". An index query that ANDs the prefix-tree query with a doc-values flag fits that description.
- 7.2.0 to 10.5.2 and 10.6.0: the default configuration.
IndexSearcherhas a JVM-wide defaultLRUQueryCache(1000, min(32 MB, heap/20))withUsageTrackingQueryCachingPolicy(10.5.2IndexSearcher.java:83-99). It caches only segments of at least 10,000 documents that are large relative to the index (LRUQueryCache.java:251-253, 278-296), and a query only after repeated use. One 10,050-document segment and the query counted 10 times reproduce it with no cache code at all on 7.2.0, 8.11.4, 9.12.3, 10.5.2 andbranch_10_6(5025 instead of 5024); an independent program with 12,000 documents gives 6000 instead of 5999 on 10.5.2 andbranch_10_6. - main: opt-in. There is no default cache since #14187 (
IndexSearcher.java:84, not yet released);MIGRATE.md:227-236shows how to turn it back on. - Effect. Silent wrong hit lists and counts for the documents whose field changed, in both directions; no exception. With the shared default cache, any searcher in the JVM that runs the same query without scores on the same segment core can get the stale entry.
- Scale. Ordinary: six documents with a cache that accepts every segment; one segment of about 10,000 documents with the default cache (
LRUQueryCache.java:251-253). - How likely: rare. The only in-tree producer,
CompositeSpatialStrategy, passes prefix-tree queries, which cannot change in place; no Lucene query parser builds aCompositeVerifyQuery; no spatial-extras test touches caching; no outside report was found in the documented searches.
The test shows that a CompositeVerifyQuery whose index query reads doc values (NumericDocValuesField.newSlowExactQuery("v", 1)) stays cached across an in-place updateNumericDocValue and reopen, because its Weight#isCacheable asks only the shape predicate.
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.lucene.spatial.composite;
import java.io.IOException;
import org.apache.lucene.document.Document;
import org.apache.lucene.document.Field;
import org.apache.lucene.document.NumericDocValuesField;
import org.apache.lucene.document.StringField;
import org.apache.lucene.index.DirectoryReader;
import org.apache.lucene.index.IndexWriter;
import org.apache.lucene.index.IndexWriterConfig;
import org.apache.lucene.index.Term;
import org.apache.lucene.search.IndexSearcher;
import org.apache.lucene.search.LRUQueryCache;
import org.apache.lucene.search.Query;
import org.apache.lucene.search.QueryCachingPolicy;
import org.apache.lucene.spatial.query.SpatialOperation;
import org.apache.lucene.spatial.serialized.SerializedDVStrategy;
import org.apache.lucene.spatial.util.ShapeValuesPredicate;
import org.apache.lucene.store.ByteBuffersDirectory;
import org.apache.lucene.store.Directory;
import org.apache.lucene.tests.util.LuceneTestCase;
import org.locationtech.spatial4j.context.SpatialContext;
public class TestCompositeVerifyQueryCacheability extends LuceneTestCase {
public void testCachedCountAfterDocValuesUpdate() throws IOException {
SpatialContext ctx = SpatialContext.GEO;
SerializedDVStrategy shapes = new SerializedDVStrategy(ctx, "shape");
Query query =
new CompositeVerifyQuery(
NumericDocValuesField.newSlowExactQuery("v", 1),
new ShapeValuesPredicate(
shapes.makeShapeValueSource(),
SpatialOperation.Intersects,
ctx.getShapeFactory().rect(-10, 10, -10, 10)));
LRUQueryCache cache = new LRUQueryCache(100, 1 << 20, leaf -> true, Float.POSITIVE_INFINITY);
try (Directory dir = new ByteBuffersDirectory();
IndexWriter w = new IndexWriter(dir, new IndexWriterConfig())) {
Document doc = new Document();
doc.add(new StringField("id", "0", Field.Store.NO));
doc.add(new NumericDocValuesField("v", 1));
for (Field f : shapes.createIndexableFields(ctx.getShapeFactory().pointXY(0, 0))) {
doc.add(f);
}
w.addDocument(doc);
try (DirectoryReader before = DirectoryReader.open(w)) {
cachingSearcher(before, cache).count(query);
w.updateNumericDocValue(new Term("id", "0"), "v", 2);
try (DirectoryReader after = DirectoryReader.openIfChanged(before, w)) {
// expected 0 hits (doc 0 now has v=2); actual 1: the pre-update cache entry is served
assertEquals(0, cachingSearcher(after, cache).count(query));
}
}
}
}
private static IndexSearcher cachingSearcher(DirectoryReader reader, LRUQueryCache cache) {
IndexSearcher searcher = new IndexSearcher(reader);
searcher.setQueryCache(cache);
searcher.setQueryCachingPolicy(
new QueryCachingPolicy() {
@Override
public void onUse(Query query) {}
@Override
public boolean shouldCache(Query query) {
return true;
}
});
return searcher;
}
}
# in a copy of this finding's repro folder (repro/LFV-947/, which holds src/)
# Lucene checkout at the tested commit, built with: ./gradlew --no-daemon assemble (JDK 25)
LUCENE=/path/to/lucene; V=12.0.0-SNAPSHOT
CP=$LUCENE/lucene/core/build/libs/lucene-core-$V.jar
CP=$CP:$LUCENE/lucene/codecs/build/libs/lucene-codecs-$V.jar
CP=$CP:$LUCENE/lucene/test-framework/build/libs/lucene-test-framework-$V.jar
CP=$CP:$LUCENE/lucene/spatial-extras/build/libs/lucene-spatial-extras-$V.jar
CP=$CP:$LUCENE/lucene/spatial3d/build/libs/lucene-spatial3d-$V.jar
CP=$CP:randomizedtesting-runner-2.9.2.jar:junit-4.13.2.jar:hamcrest-3.0.jar # test-framework dependencies (Maven Central)
CP=$CP:junit-platform-console-standalone-6.1.3.jar # JUnit Platform console launcher (Maven Central)
javac -d classes -cp "$CP" $(find src -name '*.java')
# run 1: assertions on, seed DEADBEEF
java -ea -esa --add-modules jdk.incubator.vector --enable-native-access=ALL-UNNAMED -Dtests.seed=DEADBEEF \
-cp "classes:$CP" org.junit.platform.console.ConsoleLauncher \
execute --select-class org.apache.lucene.spatial.composite.TestCompositeVerifyQueryCacheability
# the module(s) spatial-extras also need their third-party runtime jars
- Expected vs actual
- Expected count 0 after doc 0's
vchanges from 1 to 2; actual 1 (the pre-update cache entry), with-eaand-da(expected:<0> but was:<1>).
More files, environment, full output34 words
| lucene | Lucene main e1faeba08bde (2026-11-02) |
|---|---|
| jdk | 25.0.3+9-LTS |
# Lucene main 645d4fe14a20, assertions on (-ea), seed DEADBEEF: 1 test(s) run, 1 failed
=> java.lang.AssertionError: expected:<0> but was:<1>
org.junit.Assert.fail(Assert.java:89)
org.junit.Assert.failNotEquals(Assert.java:835)
org.junit.Assert.assertEquals(Assert.java:647)
org.junit.Assert.assertEquals(Assert.java:633)
org.apache.lucene.spatial.composite.TestCompositeVerifyQueryCacheability.testCachedCountAfterDocValuesUpdate(TestCompositeVerifyQueryCacheability.java:67)- The cache contract says doc-values readers must report updated fields as not cacheable (
SegmentCacheable.java:30-32); a core test: "all composites referring to it should be uncacheable". - Core wrappers that run an inner query (
FilterWeight,ConstantScoreQuery,FunctionScoreQuery) include its answer; LUCENE-8042 left this one out. - Nothing restricts the index query; LUCENE-5579 made the inner queries public for users' own compositions.
The full argument276 words
- Contract.
SegmentCacheable.java:30-32: "Objects that depend on doc values should return DocValues#isCacheable(LeafReaderContext, String...), which will check to see if the doc values fields have been updated. Updated doc values fields are not suitable for caching."QueryCache.java:28-30: the caching wrapper "will only be equivalent if scores are not needed"; the repro collects and counts without scores. - Upstream test.
TestSegmentCacheables.java:84-92: "after field3 is updated, all composites referring to it should be uncacheable".TestLRUQueryCache.java:2222(testDocValuesUpdatesDontBreakCache) tests the cache against doc-values updates with a test query whose Weight returnsDocValues.isCacheable(DVCacheQuery,:2165,:2213): after the update that segment is no longer cached (:2271-2284). - Precedent in core and queries.
FilterWeight.java:47-50(return in.isCacheable(ctx)),ConstantScoreQuery.java:192-195(innerWeight.isCacheable(ctx)),BooleanWeight.java:272-285andFunctionScoreQuery.java:279-282(inner.isCacheable(ctx) && valueSource.isCacheable(ctx)). LUCENE-8042 gaveFunctionScoreQuerya value-source-only answer too; LUCENE-7736 (#8787;2a4dd499bbdon master and03afeb7766aon the 7.x line, both committed 2017-11-15, in 7.2.0) ANDed its inner Weight in: a precedent for the fix. - History. LUCENE-8017 (
a886a001a4c) made this Weight never cacheable:return null; // TODO delegate to PredicateValueSource?. LUCENE-8042 (276e317e942, by romseygeek, +1 by rmuir, 107 files, 7.2.0) resolved the TODO by delegating to the predicate only; theindexQueryWeightwas already in the same method. LUCENE-8017 had already madeConstantScoreQuery,FilterWeightandBooleanWeightdelegate to their inner Weights' cache helpers, and LUCENE-8042 kept that delegation inisCacheable. No comment on #9065 or #9090 mentionsCompositeVerifyQueryor its index query. - Docs. No javadoc, package doc, CHANGES or MIGRATE entry says the index query must be segment-immutable.
@lucene.experimentalis about API stability, not correctness; the siblingIntersectsRPTVerifyQueryis@lucene.internal.
The devil's-advocate check256 words
The intended-behaviour checker and a reviewer from another model family (OpenAI GPT-5.6) built the strongest case: (a) the composite package was built for RPT plus serialized shapes, and the only in-tree producer passes an RPT postings query, for which asking the predicate only is exactly right; (b) the sibling IntersectsRPTVerifyQuery uses the same predicate-only rule (IntersectsRPTVerifyQuery.java:157-158), and IndexOrDocValuesQuery#isCacheable also asks only one sub-query (IndexOrDocValuesQuery.java:211-214); (c) the class is @lucene.experimental; (d) a user can put the doc-values filter outside the composite as a sibling clause; (e) main has no default cache.
Why it fails: (a) convention is not a contract; the constructor is public, accepts any Query, and LUCENE-5579 contemplated users putting other filters between RPT and the serialized shapes. (b) IntersectsRPTVerifyQuery builds its approximation internally from prefix-tree terms, so predicate-only is correct there; IndexOrDocValuesQuery relies on its documented precondition "Both provided queries must match the same documents" (:61), and the "delegate to the indexQuery" remark on #9065 was about that query; here the two parts are different conditions. (c) Experimental allows API changes, not wrong results. (d) A workaround does not make the wrapping illegal, and nothing tells users to use it. (e) The cache is public, MIGRATE.md shows how to re-enable it, and it is the default in every release since 7.2.0 and on the 10.6 branch. Verdict: not intended: the independent reproducer rated the intended case weak, the second cross-model review moderate and rejected it, the devil's advocate "high that it is not intended".
Recommended Also ask the index query's Weight (validated)
In CompositeVerifyQuery's Weight, also ask the index query: return indexQueryWeight.isCacheable(ctx) && predicateValueSource.isCacheable(ctx); (one line, the shape of FunctionScoreQuery). The new test passes 5 of 5 and the whole spatial-extras module passes, with assertions on and off; the in-tree prefix-tree composite stays cached.
@Override
public boolean isCacheable(LeafReaderContext ctx) {
return indexQueryWeight.isCacheable(ctx) && predicateValueSource.isCacheable(ctx);
}All options and trade-offs3 options · 446 words
One fix was built and validated; two alternatives were not built.
A one-line change in CompositeVerifyQuery.createWeight's anonymous Weight (1 file, +1/-1). It is what FunctionScoreQuery does for its inner query.
@Override
public boolean isCacheable(LeafReaderContext ctx) {
return indexQueryWeight.isCacheable(ctx) && predicateValueSource.isCacheable(ctx);
}Trade-offs
- On a segment where a field the index query reads was updated in place, the composite is no longer cached until that segment is merged away. The index query on its own is already treated that way, so no correct result is lost; caching is lost, though, whenever the index query is not cacheable, for any reason (below).
- The in-tree path is unchanged: prefix-tree query Weights are always cacheable (
AbstractPrefixTreeQuery.java:107-108; so is theTermQuerybuilt for a point on a points-only grid,TermQuery.java:192-194), and the control test showsCompositeSpatialStrategy's composite still served from the cache after an unrelated doc-values update. - An index query that is not cacheable for another reason (for example a
BooleanQuerywith more than 16 clauses,BooleanWeight.java:274-280) now also keeps the composite out of the cache: correct, but a change in what gets cached. - One extra
isCacheablecall, on the index query's Weight, per leaf cacheability check. A "Bug Fixes" CHANGES line would be usual; none is drafted.
What the checks validated
- Scratch worktree of
a9779ef3c193, gradle: the new test fails 4 of 5 on the stock tree and passes 5 of 5 with the fix, with assertions on and off; the wholelucene/spatial-extrasmodule (27 suites, among themTestCompositeStrategy,TestSerializedStrategyandTestQueryEqualsHashCode; 122 tests, 0 failures, 5 skipped, 4 of them nightly-only) passes with assertions on and off;:lucene:spatial-extras:check -x test(format, ecjLint, forbiddenApis) passes. - Standalone runner with the fix shadowed in: the new test 5 of 5 (
-ea,-da); the plain-defaults program on the 10.5.2 jars printsdefault searcher count=5024, uncached count=5024. - Earlier, by the hunt: the finder's 3 tests 3 of 3, the independent reproducer's 3 of 3, the model-based harness 0 mismatches and 0 property violations, upstream
TestCompositeStrategyandTestQueryEqualsHashCode(5 seeds, together with the visit fix of LFV-951), and the history probe on the introducing commit6e4f9a62e7c. - Not run: nightly tests, a benchmark.
A javadoc note on the class or its constructor.
Trade-offs
Leaves the trap for anyone who misses the note, and documents an exception to the SegmentCacheable contract instead of following it. Not built.
Return false, as LUCENE-8017 did.
Trade-offs
Correct, but it gives up caching on the in-tree prefix-tree path, where the current answer is right. Not built.
An application that hides documents through a doc-values flag inside this index query keeps showing them after the flag changes, until the stale entry goes; deleted documents stay hidden.
A hunt on this class's cache and visitor contracts started from a planning probe, then a model-based differential harness: 162 scenarios comparing cached and uncached search with a reference model found 14 stale results, all for index queries that read doc values after an in-place update, and 0 with the fix. A public-API JUnit confirmed it.
The method in full201 words
Method: semi-formal (the hunt's hunt unit for CompositeVerifyQuery's cache and visitor contracts, 2026-10-28, main a9779ef3c193).
- Lead. A planning probe from a cross-model ideas pack on committer contracts showed the wrapped query returning [0, 1, 2] where the uncached search returns [1, 2]; the finder re-read its outputs (a planning-stage reviewer had re-run it) and reduced it to a JUnit.
- Model-based harness. Reference semantics written from the class javadoc (live documents matched by the index query AND the predicate in the current state), compared with the cached search, the uncached search and the structural property "the composite is cacheable only if its index query is" on every leaf: 9 index-query kinds x 9 index operations x 2 search APIs = 162 scenarios, 324 hit-set checks, 99 property checks. Result on the stock jars (main with assertions on and off, 10.5.2): 14 stale cached results and 8 property violations, all for doc-values index queries after an in-place doc-values update; the model agreed with the uncached search in all 162 post-operation checks. With the fix: 0 and 0.
- JUnit. The scenario reduced to a public-API test: 3 of 3 fail with assertions on and off, 30 of 30 over 10 random seeds.
No prior report found (searched 2026-10-28, re-checked): two runs of 51 queries over GitHub issues, PRs and commits, downstream repositories, the mailing lists, the JIRA archive and the web, plus manual GitHub searches and the changed files of all 414 open Lucene PRs. The only issue naming the class, #7338, is unrelated.
Searches6 searches
Related, not reports of this bug: #9090 (LUCENE-8042), which introduced the line, with no comment about the index query; #9065 (LUCENE-8017), the origin of the doc-values cacheability contract, which left the TODO; #8787 (LUCENE-7736), which fixed the same pattern in FunctionScoreQuery; #6641 (LUCENE-5579), which introduced the class and states its public use; #7338 (LUCENE-6276, matchCost), the only issue naming the class; SOLR-7379 and SOLR-10038, about Solr's own geometry cache. No open PR touches CompositeVerifyQuery.java. The hunt's own published LFV-320 page mentions this site in three sentences; it is the hunt's, so it is not a prior report. Gaps: keyword search only; GitHub Discussions and chat channels were not searched.
| Where | When | How | Found |
|---|---|---|---|
| GitHub issues, PRs, commits and downstream repositories | 2026-10-28 | two runs of a scripted prior-report search (16 issue/PR, 3 commit, 2 downstream queries each) and about 30 manual searches over apache/lucene, apache/lucene-solr, apache/solr, apache/lucenenet, Elasticsearch and OpenSearch; the titles of all 93 apache/lucene items updated since 2026-10-26; code search for the fix | no report; only the related items below |
| Open PRs | 2026-10-28 | the changed files of all 414 open apache/lucene PRs | none touches CompositeVerifyQuery.java; #709, #13948, #16757 and #16847 touch the package or LRUQueryCache, not this isCacheable |
| Mailing lists | 2026-10-28 | 20 tool queries per run over dev@, java-user@, issues@ and commits@; 10 list-archive queries over the Lucene and Solr lists | no report |
| JIRA archive | 2026-10-28 | 4 tool queries per run and 5 JQL queries over LUCENE and SOLR; the migrated issues of LUCENE-5579, 8017 and 8042 read | no report |
| Web | 2026-10-28 | 6 tool queries per run and 8 manual queries | no report |
| GitHub (re-check) | 2026-10-28 | search repo:apache/lucene CompositeVerifyQuery; isCacheable and spatial-extras items updated since 2026-10-27; the file's latest upstream commit | only #7338; nothing new; file unchanged since d0d2aa274fb (2024-06-06), upstream main 645d4fe14a2 |
Introduced in 7.2.0 by LUCENE-8042 (276e317e942, 2017), which replaced a never-cacheable "TODO delegate to PredicateValueSource?" with a predicate-only answer. Executed: the introducing commits are stale and their parents correct; every executed release from 7.2.0 to 10.5.2, 10.6.0 (run at its release revision 4ae946e5a9b) and main are stale.
Versions tested8 rows
Introduced by 276e317e942 on master, "LUCENE-8042: Add SegmentCachable interface" (romseygeek, authored 2017-11-07, committed 2017-11-10; JIRA patch workflow, migrated as #9090), in 7.2.0 through the branch_7x backport 6e4f9a62e7c. It replaced LUCENE-8017's never-cacheable getCacheHelper (return null; // TODO delegate to PredicateValueSource?, a886a001a4c, four days earlier) with predicateValueSource.isCacheable(ctx). CompositeVerifyQuery itself dates from 5.2.0 (LUCENE-5579, #6641).
7.1.0 also returns the stale count, but for an older, broader reason: the cache then ignored doc-values updates for every query, so the index query alone is stale too; LUCENE-8017 fixed that in 7.2.0. The isCacheable line is present at all 62 release tags of the history clone (7.2.0-8.8.1, 9.0.0-10.5.2), in the bytecode of the 7.2.0, 7.7.3, 8.0.0, 8.11.4, 9.0.0, 9.12.3, 10.0.0 and 10.5.2 jars, on branch_10_6 4ae946e5a9b and on upstream main 645d4fe14a2. Releases before 7.1.0 were not executed.
| Version or commit | Verdict | Result |
|---|---|---|
main a9779ef3c193 | affected | the brief's test 4 of 5 fail (gradle and the standalone runner, assertions on and off; 10 iterations); no default cache, so the zero-configuration program is correct |
branch_10_6 4ae946e5a9b (10.6.0 RC1, released 2026-10-29) | affected | the finder's test 3 of 3 (assertions on and off); zero configuration 5025 instead of 5024 |
| 10.5.2 | affected | the brief's test 4 of 5 (assertions on and off); zero configuration 5025 instead of 5024 |
| 7.2.0, 7.7.3, 8.0.0, 8.11.4, 9.0.0, 9.12.3, 10.0.0 | affected | the history probe 5025 instead of 5024, the index query alone correct; zero configuration stale on 7.2.0, 8.11.4 and 9.12.3 |
| 7.1.0 | older root cause | stale, but the index query alone too: the pre-7.2.0 cache ignored doc-values updates for every query |
a886a001a4c / 6f522dd66fe (LUCENE-8017) and 281e84b988a / faa4293090a (parents of LUCENE-8042) | commit isolation | correct: the composite was never cached |
276e317e942 / 6e4f9a62e7c (LUCENE-8042) | commit isolation | stale (5025 instead of 5024); the index query alone correct; the fix shadowed onto 6e4f9a62e7c correct |
| main + fix | not affected | the brief's test 5 of 5; the whole lucene/spatial-extras module, 27 suites and 122 tests, 0 failed (assertions on and off) |
An independent reproducer, an intended-behaviour devil's advocate and reviews by another model family (OpenAI GPT-5.6, reading-only) agree on the root cause; the adjudication re-ran the tests and confirmed it with high confidence. The brief re-ran everything on main a9779ef3c193 and validated the fix with gradle.
Checks and verdicts364 words
The cross-model reviews were reading-only: they could not run Java. The refute-lens review returned a procedural "not real, low" while agreeing on the root cause and rating the intended argument weak; a separate re-run of the repro failed 3 of 3. An executing cross-model review is still open.
| Check | Verdict | What it found |
|---|---|---|
| finder | candidate | Model-based harness: 14 stale results and 8 property violations, 0 with the fix. Public-API JUnit 3 of 3 on main (-ea, -da), 30 of 30 over 10 seeds, branch_10_6, 10.5.2, 9.12.3; zero configuration stale on 10.5.2 and branch_10_6, correct on main. |
| cross-model review, refute lens | not real, low (procedural) | OpenAI GPT-5.6, reading-only, could not run Java; agreed on the root cause and rated the intended argument weak. A separate run re-ran the repro: 3 of 3 fail. |
| Independent reproducer | real | Own JUnit outside org.apache.lucene fails 3 of 3 on main (-ea, -da; 30 of 30 over 10 seeds), 10.5.2, 9.12.3, branch_10_6: [0, 1, 2] instead of [1, 2, 5], count 3 instead of 2; its in-test controls pass; zero configuration 6000 instead of 5999; its fix 3 of 3. |
| Cross-model review, works-as-designed lens | real, medium | OpenAI GPT-5.6, reading-only; rated the intended argument moderate and rejected it. A separate run re-ran the repro: 3 of 3 fail on main and 10.5.2, the fix passes. |
| Intended-behaviour devil's advocate | real (high that it is not intended) | Built the strongest intended case and showed why it fails; re-runs fail 3 of 3 on main (-ea, -da). |
| adjudication | confirmed, high | Re-ran the independent JUnit with new seeds (-ea, -da), 3 of 3 fail, and the finder's, 3 of 3; the independent fix 3 of 3 pass. |
| Follow-up: history, prior art, security | introduced by LUCENE-8042; new; not security-sensitive | Commit-level isolation on master and branch_7x; release matrix 7.1.0 to main; zero-configuration runs. |
| brief re-runs | reproduced on a9779ef3c193 | New 5-test JUnit through gradle and the standalone runner, stock and fixed, main and 10.5.2; the spatial-extras module with the fix; the zero-configuration program on 10.5.2, stock and fixed. |
The diagram as a still image, generated with AI and checked against the code. It is under 10 MB, the limit for a GitHub attachment.
- PNG 551 KB
Finding record, Lucene and upstream sources, repro files and diagram notes1,457 words
- Registry title
- CompositeVerifyQuery's Weight reports itself cacheable from its shape predicate alone and ignores the index query it runs, so after an in-place doc-values update a query cache keeps returning the old matches
- Area
- spatial-extras / composite: CompositeVerifyQuery (Weight#isCacheable) and the query cache
- Priority
- Low, rank 354: Needs a hand-built CompositeVerifyQuery with a doc-values index query plus in-place updates and a query cache that survives reopens.
- Impact class
- silent wrong results
- Impact note
- Applications that build a CompositeVerifyQuery with its public constructor around an index query that reads numeric or binary doc values (for example NumericDocValuesField.newSlowExactQuery, or a BooleanQuery with such a FILTER clause next to an RPT or term clause), update that field in place (IndexWriter#updateNumericDocValue or updateBinaryDocValue) and keep a query cache across reopens get the matches from before the update, with no error: a document that stopped matching is still returned, one that started matching is missing and counts are off, until the segment is merged away or the entry is evicted; on 7.2.0-10.5.2 and 10.6.0 (released 2026-10-29) the default IndexSearcher cache is enough (zero-configuration repro executed with a segment of 10,000+ documents and the query repeated), on main only an application-installed cache (no default cache since #14187); CompositeSpatialStrategy's own queries (an RPT postings index query) are not affected; ordinary scale.
- Summary
- You wrap an index query that reads a doc-values field, for example NumericDocValuesField.newSlowExactQuery("v", 1), in new CompositeVerifyQuery(indexQuery, shapePredicate), run it through a query cache, then change that field for one document in place with IndexWriter#updateNumericDocValue and reopen the reader. You expect the reopened searcher to see the new value, because a query that depends on updated doc values must report itself not cacheable (SegmentCacheable), and the index query alone does. Instead CompositeVerifyQuery's Weight#isCacheable asks only the shape predicate, the segment keeps its core cache key, and the cache keeps serving the pre-update result ([0, 1, 2] instead of [1, 2] in the repro; 5025 instead of 5024 hits with the release-default cache settings).
- Introduced
- 276e317e9424252d89df7596851c7cd3559d79b1 (master, 'LUCENE-8042: Add SegmentCachable interface', by romseygeek, +1 by rmuir; JIRA patch workflow, no GitHub PR; migrated as apache/lucene#9090); branch_7x backport 6e4f9a62e7cc221dcb49788ab683c87f764f2f4a, the commit released in 7.2.0. It replaced the never-cacheable 'getCacheHelper ... return null; // TODO delegate to PredicateValueSource?' that LUCENE-8017 (a886a001a4c master / 6f522dd66fe branch_7x, #9065) had added four days earlier with 'return predicateValueSource.isCacheable(ctx)', and did not consult the indexQueryWeight built in the same createWeight [read: git blame and git log -L in the history clone; exec: commit-level builds of core, spatial3d and spatial-extras: the parents of LUCENE-8017 show an older framework-wide staleness (the index query alone is stale too), LUCENE-8017 and the parents of LUCENE-8042 (281e84b988a master, faa4293090a branch_7x) never cache the composite, 276e317e942 and 6e4f9a62e7c serve the stale count while the index-query-alone control is correct, and the fix shadowed onto 6e4f9a62e7c is correct] (2017-11-07 (author date of both commits; committed 2017-11-10 on master and branch_7x))
- First affected version
- 7.2.0 [exec] (tagged 2017-12-19). 7.1.0 also serves a stale count [exec], but for an older, framework-wide root cause (the query cache then had no per-Weight cacheability, so the index query alone is stale too), fixed by LUCENE-8017 in 7.2.0; CompositeVerifyQuery itself exists since 5.2.0 (LUCENE-5579)
- Affected versions
- every release from 7.2.0 to 10.5.2, 10.6.0 (released 2026-10-29) and main. [exec] history probe (explicit LRUQueryCache, 10,050-document segment) affected on 7.2.0, 7.7.3, 8.0.0, 8.11.4, 9.0.0, 9.12.3, 10.0.0, 10.5.2, the branch_10_6 head 4ae946e5a9b and main a9779ef3c193 (-da also on 7.2.0, 10.5.2, branch_10_6 and main), with the index-query-alone control correct on each; the candidate JUnit fails 3 of 3 on main (-ea and -da; 30 of 30 over 10 random seeds), branch_10_6 4ae946e5a9b (-ea and -da), 10.5.2, 10.0.0 and 9.12.3; zero configuration (plain new IndexSearcher, default LRUQueryCache and UsageTrackingQueryCachingPolicy, query repeated) is stale on 7.2.0, 8.11.4, 9.12.3, 10.5.2 and branch_10_6 and correct on main, which has had no default query cache since #14187 (unreleased). [read] the isCacheable line is present at all 62 release tags of the history clone (7.2.0-8.8.1, 9.0.0-10.5.2) and in the bytecode of the 8.11.4 jar (8.8.2-8.11.x were tagged in the separate lucene-solr repository). Not fixed upstream: CompositeVerifyQuery.java last changed by d0d2aa274fb (2024-06-06, #13440); upstream main head 645d4fe14a2 (2026-10-28) and branch_10_6 4ae946e5a9b still carry the line. Some branch_10_6 runs combined the branch's core jars with spatial-extras classes whose composite/, serialized/ and util/ sources are identical between 10.5.2 and the branch head (recorded by the independent reproduction and the finder). All runs on Linux aarch64 with JDK 25.
- Status
- documented (brief, checks and a per-finding doc exist)
- Verification
- Confirmed by AI-run checks: an independent repro, a cross-model review that could only read the code (its low-confidence “not real” is procedural), an intended-behaviour check, a prior-report search and a skeptical audit; not yet triaged by a person.
- Found by
- semi-formal
- Upstream and prior reports
- none found (the prior-report record, verdict new, searched 2026-10-28, read-only): two scripted prior-report searches (the follow-up's and the finder's, 51 queries each: 16 GitHub issue/PR, 3 commit, 2 downstream, 20 mailing-list (dev@, java-user@, issues@, commits@), 4 JIRA-archive and 6 web queries; 375 and 1,354 distinct hits, 0 high), manual GitHub searches over apache/lucene, apache/lucene-solr, apache/solr, apache/lucenenet, Elasticsearch and OpenSearch (including the titles of all 93 apache/lucene items updated since 2026-10-26), GitHub code search (no fork or downstream copy carries the fix), the changed-file lists of all 414 open apache/lucene PRs, 5 JQL queries on LUCENE and SOLR, Lucene and Solr mailing-list searches, and 8 web queries; 6 first attempts hit GitHub's search rate limit and were retried successfully. The only apache/lucene issue that names CompositeVerifyQuery is #7338 (LUCENE-6276, matchCost; unrelated). Related, not reports: #9090 (LUCENE-8042, introduced the line; body and all comments read, none mentions the index query), #9065 (LUCENE-8017, origin of the doc-values cacheability contract; left the TODO), #8787 (LUCENE-7736, fixed the identical pattern in FunctionScoreQuery), #6641 (LUCENE-5579, introduced the class and states its public use). Not fixed: no open PR touches CompositeVerifyQuery.java (#709, #13948, #16847 and #16757 touch the package or LRUQueryCache but change no isCacheable line). Refresh 2026-10-28 (GitHub search and REST API, read-only): 'repo:apache/lucene CompositeVerifyQuery' still finds only #7338; no apache/lucene issue or PR mentioning isCacheable or spatial-extras was updated since 2026-10-27; no new commit on the file (last d0d2aa274fb, 2024-06-06); upstream main head 645d4fe14a2. Gaps: keyword search only; GitHub Discussions and chat channels not searched.
- Lucene commit tested
- main
a9779ef3c193(commit of 2026-10-26; tested 2026-10-28) through gradle in a scratch worktree and through the standalone JUnit runner, with assertions on and off and 10 derived seeds; the 10.5.2 release jars (assertions on and off). Release runs by the hunt on 7.1.0, 7.2.0, 7.7.3, 8.0.0, 8.11.4, 9.0.0, 9.12.3, 10.0.0, 10.5.2 and thebranch_10_6head4ae946e5a9b(some of those runs used the branch's core jars with spatial-extras classes whosecomposite/,serialized/andutil/sources are identical to 10.5.2), and commit-level builds of LUCENE-8017, LUCENE-8042 and their parents. The fix was validated in the scratch worktree: the new test with assertions on and off, the wholelucene/spatial-extrasmodule with assertions on and off, and:lucene:spatial-extras:check -x test.
- Finding record
a Lucene main checkout lucene/spatial-extras/src/java/org/apache/lucene/spatial/composite/CompositeVerifyQuery.java:33-37 (class javadoc, @lucene.experimental), :40 ('approximation (matches more than needed)'), :44 (public constructor), :95-97 (indexQueryWeight), :101-109 (its scorer is the approximation of every match), :113-116 (isCacheable returns predicateValueSource.isCacheable(ctx) only); lucene/core/src/java/org/apache/lucene/search/SegmentCacheable.java:27-35; lucene/core/src/java/org/apache/lucene/search/LRUQueryCache.java:816 and :903 (the cache trusts in.isCacheable); lucene/core/src/java/org/apache/lucene/search/FilterWeight.java:48-49, ConstantScoreQuery.java:193-194, lucene/queries/src/java/org/apache/lucene/queries/function/FunctionScoreQuery.java:280-281 (wrappers delegate to or AND in the inner weight); lucene/core/src/test/org/apache/lucene/search/TestSegmentCacheables.java:84-92 ('after field3 is updated, all composites referring to it should be uncacheable'); lucene/core/src/java/org/apache/lucene/search/IndexSearcher.java:84 (DEFAULT_QUERY_CACHE = null on main) [read by the registration check]
- The finding in 30 seconds
lucene/spatial-extras/src/java/org/apache/lucene/spatial/composite/CompositeVerifyQuery.java:33-37 (at a9779ef3c193) (javadoc, @lucene.experimental), :41, :44, :95-97 (indexQueryWeight), :101-111 (scorerSupplier), :113-116 (isCacheable)lucene/core/src/java/org/apache/lucene/search/LRUQueryCache.java:816-834, (at a9779ef3c193) :903-921, :264-275 (close), :458 (clear); core/src/java/org/apache/lucene/index/LeafReader.java:66-70; core/src/java/org/apache/lucene/search/SegmentCacheable.java:30-32lucene/core/src/java/org/apache/lucene/document/NumericDocValuesField.java:110-111, (at a9779ef3c193) :144-145; core/src/java/org/apache/lucene/document/SortedNumericDocValuesRangeQuery.java:119-121; core/src/java/org/apache/lucene/index/DocValues.java:339-346; core/src/java/org/apache/lucene/search/BooleanWeight.java:272-285; core/src/java/org/apache/lucene/search/IndexSearcher.java:976-982lucene/spatial-extras/src/java/org/apache/lucene/spatial/composite/CompositeSpatialStrategy.java:122-134, (at a9779ef3c193) :154-155; spatial-extras/src/java/org/apache/lucene/spatial/prefix/RecursivePrefixTreeStrategy.java:185-187, :216-229; core/src/java/org/apache/lucene/search/TermQuery.java:192-194; spatial-extras/src/java/org/apache/lucene/spatial/prefix/AbstractPrefixTreeQuery.java:107-108
- Who is affected
a Lucene clone releases/lucene/10.5.2 lucene/core/src/java/org/apache/lucene/search/IndexSearcher.java:83-99lucene/core/src/java/org/apache/lucene/search/IndexSearcher.java:82-84; (at a9779ef3c193) lucene/MIGRATE.md:227-236; core/src/java/org/apache/lucene/search/LRUQueryCache.java:251-253, :278-296
- Why it looks unintended
lucene/core/src/java/org/apache/lucene/search/SegmentCacheable.java:30-32; (at a9779ef3c193) core/src/java/org/apache/lucene/search/QueryCache.java:28-30lucene/core/src/test/org/apache/lucene/search/TestSegmentCacheables.java:84-92; (at a9779ef3c193) core/src/test/org/apache/lucene/search/TestLRUQueryCache.java:2165, :2213, :2222, :2271-2284lucene/core/src/java/org/apache/lucene/search/FilterWeight.java:47-50; (at a9779ef3c193) core/src/java/org/apache/lucene/search/ConstantScoreQuery.java:192-195; core/src/java/org/apache/lucene/search/BooleanWeight.java:272-285; queries/src/java/org/apache/lucene/queries/function/FunctionScoreQuery.java:279-282lucene/core/src/java/org/apache/lucene/search/IndexOrDocValuesQuery.java:61, (at a9779ef3c193) :211-214; spatial-extras/src/java/org/apache/lucene/spatial/composite/IntersectsRPTVerifyQuery.java:46 (@lucene.internal), :157-158
- Suggested fix
lucene/queries/src/java/org/apache/lucene/queries/function/FunctionScoreQuery.java:279-282; (at a9779ef3c193) spatial-extras/src/java/org/apache/lucene/spatial/prefix/AbstractPrefixTreeQuery.java:107-108; core/src/java/org/apache/lucene/search/BooleanWeight.java:274-280; core/src/java/org/apache/lucene/search/AbstractMultiTermQueryConstantScoreWrapper.java:43 (threshold 16)
- Prior reports
read-only gh api on 2026-10-28: search/issues 'repo:apache/lucene CompositeVerifyQuery' (1: #7338), 'repo:apache/lucene isCacheable updated:>=2026-10-27' (0), 'repo:apache/lucene spatial-extras updated:>=2026-10-27' (0); commits?path=.../CompositeVerifyQuery.java (d0d2aa274fb); commits?sha=main (645d4fe14a2)
- TestCompositeVerifyQueryCacheability.java (JUnit test):
repro/LFV-947/src/org/apache/lucene/spatial/composite/TestCompositeVerifyQueryCacheability.java(88 lines)
It is made in parallel with this doc, and every on-screen value comes from the code or the diagram's executed probe.
assets/LFV-947/bug.png(551 KB)
