MemoryIndex holds one document in memory, for percolator- and Monitor-style matching. Since 9.5.0 it can also return that document's stored fields, and they should match what IndexWriter returns.
get and numericValue() return null. Select the still to open it at full size. Generated with AI, checked against the code.- You build a MemoryIndex from one document with
StringField("id", "doc-1", Store.YES)andIntField("count", 5, Store.YES)and read it back withcreateSearcher().storedFields().document(0). - You expect what an IndexWriter index returns for the same document:
get("id")is "doc-1" andgetField("count").numericValue()is 5. - Instead both are null. The values came back as bytes:
[100, 111, 99, 45, 49]("doc-1" in UTF-8) and[-128, 0, 0, 5](the sortable point encoding of 5).KeywordField,LongField,FloatFieldandDoubleFielddo the same, and aStoredFieldbuilt from aStoredFieldDataInputis not stored at all. - Why: to store a field, MemoryIndex takes
binaryValue()if it is non-null, which for these fields is the indexed term or point. IndexWriter has asked the field for itsstoredValue()since 9.6.0; MemoryIndex was never moved to it. No error is raised, and query matching is unaffected.
Code that puts a stored StringField/KeywordField built from a String, or an IntField/LongField/FloatField/DoubleField with Store.YES, into a MemoryIndex (directly, through fromDocument, or as a one-document Monitor batch) and reads stored fields back, for example to show or highlight a matched document. Default settings and stock fields; an uncommon pattern, but then every document is hit.
Mechanism, terms and numbers591 words
MemoryIndex, Lucene's one-document in-memory index, has supported stored fields since 9.5.0 (#11999). It does not ask a field what to store (IndexableField#storedValue(), which IndexWriter uses since 9.6.0, #12116); it keeps binaryValue() if that is non-null, else numericValue(), else stringValue(). For StringField and KeywordField built from a String, binaryValue() is the indexed term, and for IntField, LongField, FloatField and DoubleField it is the sortable point encoding. So these stored values come back as bytes, and Document.get or numericValue() returns null; a StoredField(String, StoredFieldDataInput) is dropped. IndexWriter returns the String, the number and the bytes for the same document. No error is raised and query matching is unchanged. Every release from 9.6.0 to 10.5.2 and main is affected; the fix is a small change to MemoryIndex.storeValues that uses storedValue().
- MemoryIndex (
lucene/memory): a one-document index in memory. You add fields withaddField(IndexableField, Analyzer)or build it withMemoryIndex.fromDocument(doc, analyzer), then search it throughcreateSearcher(). Lucene Monitor uses one for every one-document batch. - Stored fields: the original values kept with
Field.Store.YES, read back withstoredFields().document(docId). #11999 added them to MemoryIndex in 9.5.0. binaryValue()vsstoredValue(): anIndexableFieldexposes its value through several getters.binaryValue()also serves indexing: forStringField/KeywordFieldit is the term to index, forIntFieldand the other numeric fields the encoded point.storedValue()(9.6.0, #12116) is the one value meant for stored fields: the String, the number or the bytes.
addFieldstores a field whose type saysstored()by callingstoreValues(MemoryIndex.java:673-675on maina9779ef3c193).storeValues(MemoryIndex.java:867-885) keepsfield.binaryValue()if it is non-null, elsenumericValue(), elsestringValue(). It never callsstoredValue().StringField(String, Store)setsbinaryValue = new BytesRef(value)and a StringstoredValue(StringField.java:59-66);KeywordField(String, Store)the same (KeywordField.java:96-103).IntField.binaryValue()returnsNumericUtils.intToSortableBytesof the value (IntField.java:84-88);LongField,FloatFieldandDoubleFieldlikewise. SostoreValueskeeps bytes.- A
StoredField(String, StoredFieldDataInput)returns null from all three old getters (Field.java:264-270,:419-434); onlystoredValue()exposes it (Field.java:607-624). So nothing is kept. - The stored-fields reader replays each kept object by its runtime type (
MemoryIndex.java:2242-2273): aBytesRefbecomes a binary field, soDocument.getandnumericValue()return null. - IndexWriter instead stores
field.storedValue()and throws "Cannot store a null value" when it is null (IndexingChain.java:1491-1505).
field, Store.YES | IndexWriter returns | MemoryIndex returns |
|---|---|---|
StringField("id", "doc-1") | String "doc-1" | binary [100, 111, 99, 45, 49], get("id") = null |
KeywordField("tag", "red") | String "red" | binary [114, 101, 100], get("tag") = null |
IntField(5) | Integer 5 | binary [-128, 0, 0, 5] |
LongField(7) | Long 7 | binary [-128, 0, 0, 0, 0, 0, 0, 7] |
FloatField(1.5f) | Float 1.5 | binary [-65, -64, 0, 0] |
DoubleField(2.5) | Double 2.5 | binary [-64, 4, 0, 0, 0, 0, 0, 0] |
StoredField(StoredFieldDataInput [9, 8, 7]) | binary [9, 8, 7] | absent |
Not affected (both agree): StringField(BytesRef), KeywordField(BytesRef), a StoredField built from a String, a number or bytes (tested: StoredField(String), StoredField(int), StoredField(BytesRef)), TextField(Store.YES). One expert constructor differs the other way round: for StoredField(String, CharSequence, FieldType) with a CharSequence that is not a String (a StringBuilder), IndexWriter throws, since Field.storedValue() throws IllegalStateException (Field.java:607-624), while MemoryIndex stores its toString() (executed in the fact check). For the String and numeric cases the type is wrong, but the bytes are the field's own UTF-8 term or point encoding and decode back to the value (executed for the example values "doc-1" and 5); only the StoredFieldDataInput value is lost.
Exposure and likelihood275 words
- Who. Applications that read stored fields back from a MemoryIndex searcher: percolator- or Monitor-style matching that displays or highlights the matched document, or a custom Monitor
CandidateMatcherthat reads the document. #11999 added stored fields for exactly that use. A one-document Monitor batch is a MemoryIndex (DocumentBatch.java:44-46,:55-63,:103-114). - Default vs opt-in. Stock field classes with
Store.YES, the documented way to keep a value; no special setting. Only theStoredFieldDataInputcase needs an@lucene.experimentalAPI (10.2.0 on). - Every time, silently. The outcome depends only on the field class and constructor: each affected class gave the same wrong form for all 100 random and edge values tried. No exception, no warning. (This probe and the next two ran on main
ea57360912ed;lucene/memoryandlucene/monitorare unchanged from there toa9779ef3c193.) - Inconsistent results. In Monitor, a matcher that read a stored
StringField("tenant", "t1", Store.YES)got null in a one-document batch and "t1" in a two-document batch (built with IndexWriter), for the same matching queries. - It can spread. Copying the MemoryIndex reader into a real index with
IndexWriter.addIndexes(SlowCodecReaderWrapper.wrap(...))succeeds,CheckIndexreports it clean, and the copy keeps the wrong types. - How bad. Wrong type, not lost data, for the String and numeric cases tested (the bytes are the field's own term or point encoding and decode back to the value); the
StoredFieldDataInputvalue is lost. Matching is unchanged. - How common. No in-tree module reads stored fields from a MemoryIndex (the highlighter fills one from token streams only), and no report was found in the recorded searches; one public question (2024) adds
LongField(..., Store.YES)to a MemoryIndex but never reads it back.
The test shows that MemoryIndex stores a StringField(String, Store.YES) as its term bytes (binaryValue()) instead of its storedValue(), so the stored String is lost.
/*
* 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.index.memory;
import org.apache.lucene.analysis.standard.StandardAnalyzer;
import org.apache.lucene.document.Document;
import org.apache.lucene.document.Field;
import org.apache.lucene.document.StringField;
import org.apache.lucene.tests.util.LuceneTestCase;
public class TestMemoryIndexStoredStringField extends LuceneTestCase {
public void testStoredStringFieldRoundTrips() throws Exception {
Document doc = new Document();
doc.add(new StringField("id", "doc-1", Field.Store.YES));
MemoryIndex index = MemoryIndex.fromDocument(doc, new StandardAnalyzer());
// IndexWriter returns "doc-1" for this document; MemoryIndex returns null
assertEquals("doc-1", index.createSearcher().storedFields().document(0).get("id"));
}
}
# in a copy of this finding's repro folder (repro/LFV-330/, 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/memory/build/libs/lucene-memory-$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.index.memory.TestMemoryIndexStoredStringField
- Expected vs actual
- Expected:
storedFields().document(0).get("id")is"doc-1", as with IndexWriter. Actual:null.
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:<doc-1> but was:<null>
org.junit.Assert.fail(Assert.java:89)
org.junit.Assert.failNotEquals(Assert.java:835)
org.junit.Assert.assertEquals(Assert.java:120)
org.junit.Assert.assertEquals(Assert.java:146)
org.apache.lucene.index.memory.TestMemoryIndexStoredStringField.testStoredStringFieldRoundTrips(TestMemoryIndexStoredStringField.java:32)- #12116 added
storedValue()because guessing frombinaryValue()picks the wrong value for keyword fields, and moved IndexWriter to it; MemoryIndex was missed. StringField(String, Store.YES)round-trips on 9.5.0 and fails on 9.6.0, broken by an indexing speed-up (#12139).- CHANGES 9.5.0: "MemoryIndex now supports stored fields"; its test asserts typed round trips, but only with
StoredField.
The full argument340 words
- The contract. CHANGES 9.5.0: "GITHUB#11999: MemoryIndex now supports stored fields." (
CHANGES.txt:3768).IndexableField#storedValue(): "Stored value. This method is called to populate stored fields and must return a non-null value if the field stored." (IndexableField.java:74-78).Field.Store.YES: "The value is stored in its original form, i.e. no analyzer is used before it is stored." (Field.java:596-599). CHANGES 9.6.0: "GITHUB#12116: Introduce IndexableField#storedValue() to expose the value that should be stored to IndexingChain without needing to guess the field's type." (CHANGES.txt:3569-3571). - History: an oversight. The #12116 commit message (
96136b42821) describes this exact failure:binaryValue()"is used both for sorted(_set) doc values and storage. This makes it impossible to implementkeywordfields that is stored and doc-valued, as the field returns a non-null value for both binaryValue() and stringValue() and stored fields no longer know which field to store". It addsstoredValue()so that a stored fields format cannot "mistakenly use binaryValue()/stringValue()/numericValue() instead of storedValue()". Inlucene/memoryit changed onlyTestMemoryIndex.java. No PR thread of #12116, #12054, #12139 or #14213 mentions MemoryIndex. #12139 did changeMemoryIndex.java(cce33b07e4f, +30 lines):addFieldnow indexes a field without a TokenStream by takingbinaryValue()as its single term (a newstoreTerm), with a newTestMemoryIndex.testKeywordWithoutTokenStream. It did not changestoreValues: the indexing path was adapted to the new meaning ofbinaryValue(), the storing path was not. - A regression for StringField.
StringField(String, Store.YES)round-trips on 9.5.0 and fails on 9.6.0. On the 9.6.0 jars,StringField.javafrom just before the #12139 backport passes and from #12139 ("Faster indexing of string fields") fails. - Tests.
TestMemoryIndex.testStoredFields(TestMemoryIndex.java:719-766) asserts that numbers come back as numbers, strings as strings and bytes as bytes, but only withStoredField, whosebinaryValue()is null for non-binary values. No test asserts the binary form forStringField,KeywordFieldorIntField. - Precedent. #12109 ("MemoryIndex should not fail integer fields that enable doc values") fixed another MemoryIndex/IndexingChain divergence for the new
IntFieldas a bug: it "aligns MemoryIndex with IndexingChain".
The devil's-advocate check224 words
The strongest works-as-designed case, built by an intended-behaviour verifier and by a review from another model family: the MemoryIndex class javadoc says a query selects "on the field names and associated (indexed) tokenized terms, not on the original fulltext(s) - the latter are not stored but rather thrown away immediately after tokenization" (MemoryIndex.java:133-135); MemoryIndex never promises equivalence with IndexWriter; and StoredFieldDataInput is @lucene.experimental (StoredFieldDataInput.java:27).
Why it fails: that sentence is from 2005 (the original contribution 8f9e2a15e78 says "free text(s)"; 0062898ada6 reworded it later that year) and predates stored-field support; #11999 then made stored fields a documented feature with a type-preserving test. The behaviour changed in 9.6.0 through an unrelated indexing speed-up, and nothing documents a reduced stored-field contract. For StoredFieldDataInput, neither its javadoc nor #14213 limits it to IndexWriter, and MemoryIndex drops it silently instead of storing it or throwing. Verdict: not intended, a defect. Expect the class-javadoc sentence to be quoted back and answer with the #11999 CHANGES entry and testStoredFields; a reviewer may also read #12139's MemoryIndex edit as a sign that only indexing was meant to follow the new binaryValue(), though nothing documents that stored values were meant to change. Expect pushback mainly on priority (stored fields in MemoryIndex were accepted reluctantly in #11999) and on compatibility for custom fields (see the fix trade-offs).
Recommended Store storedValue(), mapped by type; keep the old guess only when it is null
In MemoryIndex.storeValues, store field.storedValue() and keep it by StoredValue.Type, copying a DATA_INPUT value into a BytesRef; keep the old guess only for a field whose storedValue() is null (+30 lines). The repro passes 8 of 8 with assertions on and off, and all lucene/memory and lucene/monitor tests pass.
StoredValue storedValue = field.storedValue();
if (storedValue != null) {
switch (storedValue.getType()) {
case INTEGER -> info.storedValues.add(storedValue.getIntValue());
case LONG -> info.storedValues.add(storedValue.getLongValue());
case FLOAT -> info.storedValues.add(storedValue.getFloatValue());
case DOUBLE -> info.storedValues.add(storedValue.getDoubleValue());
case STRING -> info.storedValues.add(storedValue.getStringValue());
case BINARY -> info.storedValues.add(storedValue.getBinaryValue());
case DATA_INPUT -> info.storedValues.add(readBytes(storedValue.getDataInputValue()));
}
return;
}
// ... the old binaryValue() / numericValue() / stringValue() guess, unchangedAll options and trade-offs4 options · 772 words
Two variants of the same fix were applied in a scratch worktree at a9779ef3c193 and run through gradle (seed DEADBEEF); they differ only in what happens to a stored field whose storedValue() is null.
Store storedValue(), mapped by type; keep the old guess only when it is null (validated by the checks) (recommended)
In storeValues, take field.storedValue() and add its value by StoredValue.Type; a DATA_INPUT value is read into a new BytesRef of length() bytes by a small readBytes helper (an IOException becomes an UncheckedIOException, since addField declares none). If storedValue() is null (IndexWriter rejects such a field), fall through to today's binaryValue() / numericValue() / stringValue() guess. +30/-0 in MemoryIndex.java.
StoredValue storedValue = field.storedValue();
if (storedValue != null) {
switch (storedValue.getType()) {
case INTEGER -> info.storedValues.add(storedValue.getIntValue());
case LONG -> info.storedValues.add(storedValue.getLongValue());
case FLOAT -> info.storedValues.add(storedValue.getFloatValue());
case DOUBLE -> info.storedValues.add(storedValue.getDoubleValue());
case STRING -> info.storedValues.add(storedValue.getStringValue());
case BINARY -> info.storedValues.add(storedValue.getBinaryValue());
case DATA_INPUT -> info.storedValues.add(readBytes(storedValue.getDataInputValue()));
}
return;
}
// ... the old binaryValue() / numericValue() / stringValue() guess, unchangedTrade-offs
- The smaller of the two changes, because it keeps today's guess when
storedValue()returns null: a custom storedIndexableFieldwith a nullstoredValue()keeps working in MemoryIndex as today, although IndexWriter rejects it. - A and B both change a stored field whose
storedValue()throws:StoredField(name, aStringBuilder, StoredField.TYPE)now throwsIllegalStateExceptionin MemoryIndex, as it already does in IndexWriter; today MemoryIndex stores itstoString()(executed in the fact check, stock and with A). A custom field whosestoredValue()differs from its old getters changes too. - Cost: the
DATA_INPUTcopy reads the input once, the same work IndexWriter's stored-fields writer does;BINARYvalues stay by reference, as today; the other types are boxed as before. - Not tested with either fix: over-long stored strings, reuse of a
BytesRefafteraddFieldreturns, aStoredFieldDataInputthat fails to read, custom fields with several non-null getters.
What the checks validated
- Stock main
a9779ef3c193, gradle, seed DEADBEEF: the repro fails 7 of 8 with assertions on and with assertions off; the control passes. - With A: the repro passes 8 of 8 with assertions on and with assertions off; all of
lucene/memorypasses (59 tests in 4 classes: TestMemoryIndex 30, TestMemoryIndexAgainstDirectory 16, the repro 8, TestSlicedIntBlockPool 5, per the fact check's JUnit XML) and all oflucene/monitor(181 tests, 1 skipped), and:lucene:memory:checkGoogleJavaFormatand:lucene:memory:ecjLintpass. - The classpath runner with A shadowed on main: 8 of 8 pass. The hunt's earlier sketch of the same idea (it also copies
BINARYvalues) passed the finder's repro, an independent 11-case suite (8 defect cases, 3 controls) and 51 upstream memory tests onea57360912ed.
Same as the recommended option, but throw new IllegalArgumentException("Cannot store a null value") instead of the fallback; the old guess is removed. +28/-13 in MemoryIndex.java. It does not copy IndexWriter's other behaviour: there is no IndexWriter.MAX_STORED_STRING_LENGTH check (IndexingChain.java:1495-1502), and like A and today's code it keeps a BINARY value by reference, while IndexWriter's stored-fields writer copies the bytes when the document is added (Lucene90CompressingStoredFieldsWriter.java:303-319).
Trade-offs
One null rule for both writers and less code; #12116 made IndexWriter fail early on such a field ("If there is a problem ... it's currently discovered late"). But a custom stored field with a null storedValue() that works in MemoryIndex today would start to throw, so it is a visible behaviour change for such callers.
What the checks validated
- With B: all of
lucene/memorypasses (59 tests, the repro included) and all oflucene/monitor(181 tests, 1 skipped); format and lint pass. So no path that these suites ran with seed DEADBEEF adds a stored field with a nullstoredValue()to a MemoryIndex (B would throw).
Add StringField/KeywordField from a String, the four numeric fields and a StoredFieldDataInput to TestMemoryIndex.testStoredFields, or compare stored fields in TestMemoryIndexAgainstDirectory; this doc's repro can serve as the test.
Trade-offs
testStoredFields used only StoredField, whose binaryValue() is null for non-binary values, which is why the drift went unnoticed.
Index with Store.NO and add a StoredField under the same name, for example new StringField("id", "doc-1", Store.NO) plus new StoredField("id", "doc-1"), or new IntField("count", 5, Store.NO) plus new StoredField("count", 5). Checked on stock main: the term and exact queries match, get("id") = "doc-1" and numericValue() = 5.
Trade-offs
Needs a change in every application that builds such documents. For the StoredFieldDataInput case, pass the bytes as StoredField(String, BytesRef) instead; that constructor already agrees with IndexWriter.
Downstream note: a percolator or Monitor matcher that reads these stored values back from a MemoryIndex gets null, so application code that treats a missing value as "no restriction" would fail open on every such document; no such code is known.
Bounded-exhaustive translation validation on the stock jar: every stored-value-producing field class (16 kinds, one or two values each) went into a one-field document indexed by MemoryIndex and by IndexWriter, and the stored fields were compared. It found 14 mismatches (7 kinds, one and two values each); the finder reports none with the fix sketch. Reading storeValues gave the cause.
The method in full161 words
Method: bounded-exhaustive translation validation on the stock jar (semi-formal tier), in a hunt unit that checked MemoryIndex against IndexWriter as the reference.
- The harness's stored-fields scope built each stored-value-producing field class (16 kinds x 1-2 values: 32 cases) into a single-field document, indexed it once with MemoryIndex and once in a one-document IndexWriter index, and compared what a
StoredFieldVisitorsaw. - On the stock main jar it reported 14 STORED mismatches:
StringField(String),KeywordField(String),IntField,LongField,FloatField,DoubleFieldandStoredField(StoredFieldDataInput), each with one and two values; the finder reports 0 with astoredValue()-basedstoreValues(no log of that run is kept; the diagram probe with the fix shadowed agrees with IndexWriter on all 13 cases). - The finder traced the mismatches to
MemoryIndex.storeValues, wrote a public-API JUnit with the IndexWriter value asserted first, and ran it on main and on releases back to 9.5.0. Independent verifiers then reproduced it with their own tests (see the checks).
No prior report found (2026-10-26) in 78 queries over GitHub, the mailing lists, JIRA, Elasticsearch, OpenSearch, Solr, Stack Overflow and the web, plus the changed files of all 411 open PRs. Closest: a #12054 review comment on the same guessing in IndexWriter's path, which #12116 fixed.
Searches3 searches
Verdict new. The hazard of guessing from binaryValue() was known for IndexWriter's stored-fields path (the #12054 review, fixed by #12116), but MemoryIndex's own copy is different code with a different fix and is mentioned nowhere. A re-check on 2026-10-27 was blocked by GitHub's search rate limit; the fact check's read-only GitHub API searches on the same day found no new report. Keyword search cannot prove absence.
| Where | When | How | Found |
|---|---|---|---|
| apache/lucene issues, PRs and commits; dev@, java-user@, issues@, commits@; JIRA archive; downstream repositories; the web | 2026-10-26 | the hunt's prior-report tool: 16 GitHub issue/PR, 3 commit, 2 downstream, 20 mailing-list, 4 JIRA and 6 web queries (520 distinct hits; every high or medium hit created after 2022-12-06 read in full) | no report; the top hits are the introducing changes #11999, #12116, #12139, #14213 and the open design issue #12142 |
| GitHub (apache/lucene, Elasticsearch, OpenSearch, Solr, all of GitHub), lists.apache.org, Stack Overflow | 2026-10-26 | 15 manual GitHub searches, 4 GitHub API checks, 3 mailing-list and 4 Stack Overflow queries, 1 web query | no report; the #12054 review comment (same guessing, IndexWriter's path); #12109 (a different MemoryIndex/IndexingChain divergence, fixed as a bug); Stack Overflow 78129318 (stores a LongField in a MemoryIndex, never reads it back) |
| open apache/lucene PRs | 2026-10-26 | changed files of all 411 open PRs | 8 touch lucene/memory; none touches storeValues, storedValue or stored fields |
Latent since #11999 wrote storeValues (9.5.0, correct then); wrong since #12116 added storedValue() in 9.6.0 without migrating MemoryIndex, and #12139 gave StringField its binaryValue() override in the same release. Affects 9.6.0 to 10.5.2 and main; the DataInput case since 10.2.0. Not fixed upstream.
Versions tested10 rows
- Latent code: #11999 "Add support for stored fields to MemoryIndex" (@romseygeek;
66127f6e696, 2022-12-08; 9.5.0) wrotestoreValues; all 19 lines (867-885) still blame to it. It was correct then:storedValue()did not exist and no stock stored field had a binary form for a non-binary value. - Introduced in 9.6.0: #12116 "Improve document API for stored fields" (@jpountz;
96136b42821, 2023-02-06; 9.x84dcaa850b1) addedstoredValue(), moved IndexWriter to it and gave the numeric fieldsStoreconstructors, but did not changeMemoryIndex.java. In the same cycle #12054 (ab074d54839) addedKeywordFieldwith a termbinaryValue(), and #12139 (cce33b07e4f, 2023-02-21; 9.x668246439dd) gaveStringFieldone, the regression forStringField(String); #12139 also madeMemoryIndex.addFieldindex thatbinaryValue()as the term, and leftstoreValuesunchanged. - 10.2.0: #14213 (
f12e44b6515, 2025-02-19) addedStoredFieldDataInput, visible only throughstoredValue(), so MemoryIndex drops it. - The releases between the executed ones have a byte-identical
storeValuesand the same field overrides (source identity per tag). A read-only check on 2026-10-27 found upstream main andbranch_10xwith the samestoreValues.
| Version or commit | Verdict | Result |
|---|---|---|
main a9779ef3c193 | affected | this doc's JUnit 7 of 8 fail through gradle and the classpath runner, assertions on and off; control passes |
main ea57360912ed | affected | the hunt's repro 2 of 2 and the independent suite 8 of 11 fail, -ea and -da; 3 controls pass |
| 10.5.2 | affected | this doc's JUnit 7 of 8 fail; the independent suite 8 of 11 |
| 10.2.0 | DataInput case arrives | the DataInput case fails (10.1.0: the API does not exist) |
| 9.12.3, 10.0.0, 10.1.0 | affected | the independent suite 7 of 10 fail (no DataInput API); 9.12.3 also -da (the hunt's repro) |
| 9.6.0, 9.7.0 | first affected | 9.6.0: 7 of 10 fail; StringField-only variant fails on 9.6.0 and 9.7.0 |
| 9.6.0 jars, StringField.java shadowed | commit level | the file from just before the #12139 backport passes 5 of 5; the #12139 file fails 2 of 5 |
| 9.5.0 | last clean | StringField(String) and the controls pass 5 of 5 (KeywordField and stored numeric fields do not exist yet) |
| 9.4.2 | no stored fields | MemoryIndex returns no stored fields at all |
fix A (scratch worktree at a9779ef3c193) | not affected | the repro 8 of 8, -ea and -da; lucene/memory 59 and lucene/monitor 181 tests pass (fix B: the same suites pass) |
An independent reproducer and an intended-behaviour devil's advocate reproduced it with their own tests, controls passing; reading-only reviews by a model from another family (OpenAI GPT-5.6) agreed on the root cause; an adjudication confirmed it after re-running the independent suite, and this doc re-ran everything on a9779ef3c193.
Checks and verdicts462 words
Every executing check wrote or re-ran its own JUnit on stock main; every control passed.
| Check | Verdict | What it found |
|---|---|---|
| finder | candidate | Translation-validation harness (14 mismatches; the finder reports 0 with the fix sketch) and a public-API JUnit: 2 of 2 fail on main (-ea, -da) and 10.5.2, 1 of 1 on 9.12.3, 9.7.0 and 9.6.0; 9.5.0 passes; the fix passes it and 51 upstream memory tests. |
| cross-model refute review | not real (low, procedural) | An independent reviewer from another model family (OpenAI GPT-5.6) that could only read code: root cause agreed, inputs in contract, 9 of its 11 evidence items support real; it needed an executed repro of its own. A separate run re-ran the repro: 2 of 2 fail. |
| independent reproducer | real | Own 11-case suite (8 defect cases, 3 controls, IndexWriter value asserted first): 8 fail on main with -ea and -da; 11 of 11 pass with the fix; 10.5.2 8 of 11, 9.12.3 and 9.6.0 7 of 10, the DataInput case fails on 10.2.0, 9.5.0 passes. |
| cross-model intended-behaviour review | real (medium) | Same reviewer family, works-as-designed lens: the class-javadoc defence is moderate and does not hold, since #11999 superseded it and #12116 was written to end this guessing. Separate re-run: 2 of 2 fail. |
| intended-behaviour devil's advocate | real (high; medium-high for the DataInput case) | No javadoc, CHANGES, MIGRATE entry or test documents a reduced contract; re-ran main -ea/-da and 10.5.2 (fail), 9.6.0 fail, 9.5.0 pass. |
| adjudication | confirmed (high) | Re-ran the independent reproduction's suite on main: 11 run, 8 failed with -ea and with -da, controls pass. |
| Follow-up and main refresh | reproduces; keep | History runs from 9.4.2 to main, a security probe (matching unchanged in 2,600 evaluations), prior-report search; the refresh to a9779ef3c193 gives identical failures. |
| this doc's runs | reproduces; fix validated | A compact gradle JUnit fails 7 of 8 on stock main with assertions on and off, and on the 10.5.2 jars; fixes A and B were applied in a fresh scratch worktree of a9779ef3c193: the repro, all lucene/memory and lucene/monitor tests, format and lint pass; the workaround was checked on stock main. |
| Independent fact and diagram checks | errors fixed | A fact check re-ran the gradle repro (stock 7 of 8 fail, fix A 8 of 8, assertions on and off), all lucene/memory and lucene/monitor tests with fix A, the classpath runs on main and 10.5.2 and the workaround; a cross-model fact check (OpenAI GPT-5.6, read-only) re-read every citation; a diagram check compared every frame with the code. Their errors (a line count, a missing #12139 fact, the scope of "the other StoredField constructors", fix B called an exact mirror of IndexWriter) and the overclaims they listed were fixed in this doc and the brief. |
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 463 KB
Finding record, Lucene and upstream sources, repro files and diagram notes1,227 words
- Registry title
- MemoryIndex stores binaryValue() instead of IndexableField.storedValue(), so stored StringField/KeywordField values built from a String and stored IntField/LongField/FloatField/DoubleField values come back as binary bytes (Document.get and numericValue() return null), and a StoredField built from a StoredFieldDataInput is dropped (released since 9.6.0)
- Area
- memory / MemoryIndex stored fields
- Priority
- Medium, rank 237: Since 9.6.0, MemoryIndex deterministically and silently returns stored StringField and numeric values with the wrong type, but only for the niche pattern of reading stored fields from it, so it heads the narrow silent rows after LFV-237.
- Impact class
- silent wrong results
- Impact note
- Applications that put a stored StringField or KeywordField built from a String, or an IntField/LongField/FloatField/DoubleField with Store.YES, into a MemoryIndex (addField, fromDocument, or a single-document Monitor batch) and read stored fields back from its searcher, for example to display or highlight a percolated or monitored document, get the value with the wrong type on every document and no error (Document.get and numericValue() return null, although the bytes still decode to the original value), and lose a StoredField(StoredFieldDataInput) value completely (@lucene.experimental, 10.2.0 and later); default settings and stock field classes, but reading stored fields back from a MemoryIndex is an uncommon pattern with no in-tree consumer; every release from 9.6.0 through 10.5.2 and main; ordinary scale.
- Summary
- You build a MemoryIndex from a document with StringField("id", "doc-1", Store.YES), a KeywordField(String, Store.YES) or an IntField("count", 5, Store.YES) and read it back through storedFields().document(0), expecting the same stored values IndexWriter returns, since MemoryIndex has supported stored fields since 9.5.0 (#11999). Instead MemoryIndex stores the field's binaryValue() rather than its storedValue(): the String comes back as its UTF-8 bytes, so Document.get("id") returns null, and the numbers come back as their sortable point bytes (IntField 5 as [80 00 00 05]), so numericValue() returns null. A StoredField built from a StoredFieldDataInput is not stored at all; the BytesRef constructors and plain StoredField values are unaffected.
- Introduced
- #12116 (96136b42821 on main, 84dcaa850b1 on branch_9x, 'Improve document API for stored fields') [read: git log -L/-S, git blame and the PR threads; exec: release runs 9.5.0 pass and 9.6.0 fail, plus a single-class StringField shadow on the 9.6.0 jars]: it added IndexableField#storedValue(), moved IndexWriter to it and gave IntField/LongField/FloatField/DoubleField Store constructors, but did not migrate MemoryIndex.storeValues, which #11999 (66127f6e696, 2022-12-08, first released in 9.5.0) had written and which was correct then (storedValue() did not exist; all 19 lines, 867-885, blame to it, unchanged since). In the same 9.6.0 cycle #12054 (ab074d54839) added KeywordField with a term binaryValue() and #12139 (cce33b07e4f) gave StringField one, which caused the headline StringField(String) regression (#12139 also made MemoryIndex.addField index that binaryValue() as the term, but left storeValues unchanged) [exec: on the 9.6.0 jars StringField.java from ecaee57aceb (before the #12139 backport) passes 5/5 and from 668246439dd (#12139) fails 2/5]; #14213 (f12e44b6515, 10.2.0) added StoredFieldDataInput, visible only through storedValue(), so MemoryIndex drops it [exec: 10.1.0 API absent, 10.2.0 fail] (2023-02-06 (#12116); 2023-02-07 (#12054) and 2023-02-21 (#12139) in the same 9.6.0 cycle; 2025-02-19 (#14213, the StoredFieldDataInput case))
- First affected version
- 9.6.0 [exec] (9.5.0 is the last unaffected release); the StoredFieldDataInput case 10.2.0 [exec], the first release with that API
- Affected versions
- 9.6.0 through 9.12.3, 10.0.0 through 10.5.2 and main ea57360912ed: executed on 9.6.0, 9.7.0 (StringField case only), 9.12.3, 10.0.0, 10.1.0, 10.2.0, 10.5.2 and main with -ea, and also with -da on 9.12.3, 10.5.2 and main [exec]; the releases in between have a byte-identical storeValues, no storedValue() call in MemoryIndex and the same binaryValue() overrides [read: source identity per tag]; the StoredFieldDataInput case 10.2.0 and later only; 9.5.0 not affected (StringField(String) and the controls pass) [exec]; before 9.5.0 MemoryIndex returned no stored fields at all (9.4.2) [exec]; upstream main e1c4184cb983 and branch_10x unchanged on 2026-10-26 [read]
- 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: exhaustive check
- Upstream and prior reports
- none found (2026-10-26, the prior-report record, 78 read-only queries: a scripted prior-report search with 16 GitHub issue/PR, 3 commit, 2 downstream, 20 mailing-list, 4 JIRA-archive and 6 web queries, 520 distinct hits; 15 manual GitHub searches including Elasticsearch, OpenSearch and Solr, 4 GitHub API checks, 3 mailing-list, 4 Stack Overflow and 1 web query (throttled); the changed-file lists of all 411 open PRs); related prior items: #12054 (a review comment on the same binaryValue-first guessing in the IndexWriter stored-fields path, which #12116 then fixed there; MemoryIndex not mentioned), #12116 (added storedValue(), did not migrate MemoryIndex), #12139 (StringField binaryValue() override), #11999 (wrote storeValues), #14213 (StoredFieldDataInput), #12142 (open design discussion, no MemoryIndex), #12109 (a different MemoryIndex/IndexingChain divergence fixed as a bug); no open PR touches storeValues; upstream main e1c4184cb983 still has the same storeValues
- Lucene commit tested
- main
a9779ef3c193(commit of 2026-10-26): this doc's JUnit through gradle in a fresh scratch worktree (only the repro and, for the fixed runs, the patch added), stock and with fix A, assertions on and off; fix A and fix B with alllucene/memoryandlucene/monitortests, format and lint (2026-10-27); the same JUnit through the hunt's classpath runner on main (assertions on and off, and with fix A shadowed) and on the 10.5.2 release jars. Hunt and verifier runs on mainea57360912edand on 9.4.2, 9.5.0, 9.6.0, 9.7.0, 9.12.3, 10.0.0, 10.1.0, 10.2.0 and 10.5.2; the refresh toa9779ef3c193gave identical failures. An independent fact check re-ran the gradle repro (stock and fix A, assertions on and off), the suites with fix A and the classpath runs on 2026-10-27.
- Finding record
lucene/memory/src/java/org/apache/lucene/index/memory/MemoryIndex.java:867-885 (storeValues), :673-674 (stored gate in addField), :2242-2273 (stored-fields visitor), :133-135 (class javadoc: fulltexts 'not stored but rather thrown away'), :531-575 (fromDocument), :620 (addField)lucene/core/src/java/org/apache/lucene/index/IndexingChain.java:1436-1450 (IndexWriter stores field.storedValue(), throws on null)lucene/core/src/java/org/apache/lucene/index/IndexableField.java:75-78 (storedValue javadoc); lucene/core/src/java/org/apache/lucene/document/Field.java:597-598 (Store.YES: 'stored in its original form')lucene/core/src/java/org/apache/lucene/document/StringField.java:61, 95-96, 118-119; KeywordField.java:98, 107-108; IntField.java:83-88 (binaryValue() returns the term or the sortable point encoding; storedValue() the String or number)lucene/core/src/java/org/apache/lucene/index/StoredFieldDataInput.java:27 (@lucene.experimental)lucene/CHANGES.txt:3752 (GITHUB#11999 'MemoryIndex now supports stored fields'), :3553 (GITHUB#12116), :3568 (GITHUB#12054), :3612 (GITHUB#12139), :1904 (GITHUB#14213)lucene/memory/src/test/org/apache/lucene/index/memory/TestMemoryIndex.java:719 (testStoredFields: typed round trips, StoredField only)
- The finding in 30 seconds
lucene/memory/src/java/org/apache/lucene/index/memory/MemoryIndex.java:531-575 (fromDocument), :620 (addField), :673-675 (stored gate), :867-885 (storeValues), :2242-2273 (stored-fields visitor)lucene/core/src/java/org/apache/lucene/index/IndexingChain.java:1491-1505 (IndexWriter stores field.storedValue(), throws on null)lucene/core/src/java/org/apache/lucene/document/StringField.java:59-66, 95-96, 118-119; KeywordField.java:96-103, 107-108, 135-136; IntField.java:73-80, 84-88, 91-92; LongField.java:87-90; FloatField.java:82-86; DoubleField.java:82-86lucene/core/src/java/org/apache/lucene/document/Field.java:264-270, 419-434, 607-624; StoredField.java:116, 147-157 (the CharSequence constructor)KeywordField row: the repro asserts get("tag") = null; the bytes are new BytesRef("red") (KeywordField.java:98), the UTF-8 of "red"; the independent reproduction executed KeywordField("abc") -> binary[97,98,99] (verify/judge/v1-main-ea)
- Who is affected
lucene/monitor/src/java/org/apache/lucene/monitor/DocumentBatch.java:44-46, 55-63, 103-114git diff --stat ea57360912ed a9779ef3c193 -- lucene/memory lucene/monitor (empty)
- Why it looks unintended
lucene/CHANGES.txt:3768 (GITHUB#11999), :3569-3571 (GITHUB#12116), :3584 (GITHUB#12054), :3628 (GITHUB#12139), :1920 (GITHUB#14213)lucene/core/src/java/org/apache/lucene/index/IndexableField.java:74-78; document/Field.java:596-599; index/StoredFieldDataInput.java:27lucene/memory/src/java/org/apache/lucene/index/memory/MemoryIndex.java:133-135 (class javadoc)lucene/memory/src/test/org/apache/lucene/index/memory/TestMemoryIndex.java:719-766 (testStoredFields)
- Suggested fix
lucene/core/src/java/org/apache/lucene/index/IndexingChain.java:1495-1502 (MAX_STORED_STRING_LENGTH); codecs/lucene90/compressing/Lucene90CompressingStoredFieldsWriter.java:303-319 (bytes copied at addDocument)
- TestMemoryIndexStoredStringField.java (JUnit test):
repro/LFV-330/src/org/apache/lucene/index/memory/TestMemoryIndexStoredStringField.java(34 lines)
The values in the alt text are the executed repro values on main a9779ef3c193.
assets/LFV-330/bug.png(463 KB)
![Diagram of the bug. One document with StringField("id", "doc-1", Store.YES) and IntField("count", 5, Store.YES) goes into an IndexWriter index and into a MemoryIndex. IndexWriter stores storedValue() and returns "doc-1" and 5. MemoryIndex stores binaryValue() and returns the bytes [100, 111, 99, 45, 49] and [-128, 0, 0, 5], so Document.get("id") and numericValue() return null.](../assets/LFV-330/bug.png)