All findings · Lucene finding · LFV-090 · memoryindex-float16-vector-stale-assertion

MemoryIndex.addField throws AssertionError for a KnnFloat16VectorField when assertions are enabled: a stale assertion in storeVectorValues predates the FLOAT16 branch

With Java assertions on, as in Lucene's own test runs, adding any KnnFloat16VectorField to a MemoryIndex throws AssertionError, because the first line of storeVectorValues still allows only float32 and byte vector fields. With assertions off the vector is stored and read back exactly; only unreleased main and 11.0 have it.

Summary

Priority
Low, rank 1166MemoryIndex throws AssertionError under -ea for a KnnFloat16VectorField because of a stale assertion, test-suite only and unreleased.
Impact
minor
Since
main, #16383 (commit bfe5c847d3c, 2026-07-27)introduced 2026-07-27
Affected
main and branch_11x (unreleased 11.0.0); no release
Status
documented
Found by
non-formal: a float16-vs-float32 differential edge probe

AI-assisted page: this finding was first signalled by code reading or testing and then checked with AI tools; the checks it went through, and what each found, are under Prior reports, history and checks. Not yet triaged by a human. Snapshot of 2026-11-06.

The finding in 30 seconds

MemoryIndex is Lucene's one-document, in-memory index. Its vector code has one branch per encoding, but the check on its first line still lets only the two older vector field classes through.

Background: MemoryIndex and vector fields

  1. MemoryIndex holds a single document in memory. You add fields with addField (or build it with fromDocument) and search it like a one-document index; lucene/monitor uses it to match one document against stored queries.
  2. Vector fields come in one class per encoding: KnnFloatVectorField (float32), KnnByteVectorField (byte) and, on main since #16383, KnnFloat16VectorField (float16, values as short[]). Each extends Field directly; none is a subclass of another.
  3. MemoryIndex keeps one vector per field and returns it through getFloatVectorValues, getByteVectorValues or getFloat16VectorValues on its reader. It runs no kNN search for any encoding.
Diagram of the bug. MemoryIndex.addField with a KnnFloat16VectorField calls storeVectorValues. Its first line asserts that the field is a KnnFloatVectorField or a KnnByteVectorField. With assertions enabled the float16 field fails that check and addField throws java.lang.AssertionError at MemoryIndex.java:794, so the FLOAT16 branch below, which would store the vector, never runs. With assertions disabled the check is skipped, the FLOAT16 branch stores the vector, and getFloat16VectorValues returns it exactly. The fix adds KnnFloat16VectorField to the assertion.
Adding one float16 vector to a MemoryIndex: with assertions on, the first line of storeVectorValues accepts only float32 and byte vector fields and throws before the FLOAT16 branch; with assertions off, the branch stores the vector and it reads back exactly. Select the still to open it at full size. Generated with AI, checked against the code.

The finding, step by step

  1. Run a test with assertions on (Lucene's gradle build does for most seeds). Add one 1-dimensional float16 vector to an empty MemoryIndex: addField(new KnnFloat16VectorField("v", new short[] {Float.floatToFloat16(1f)}, EUCLIDEAN), null).
  2. You expect it to be stored like a float32 or byte vector: getFloat16VectorValues("v").vectorValue(0) should return an equal 1-element vector, 1.0 in half precision. MemoryIndex has code for exactly this since float16 vectors were added.
  3. Instead addField throws java.lang.AssertionError, with no message, at MemoryIndex.java:794, and no vector is stored (the field itself is already registered, with no values). MemoryIndex.fromDocument and Monitor.match on a single document fail the same way.
  4. Why: storeVectorValues starts with assert vectorField instanceof KnnFloatVectorField || vectorField instanceof KnnByteVectorField;, written in 2024 when those were the only two vector field classes. KnnFloat16VectorField is neither, so the FLOAT16 branch right below is never reached.
  5. With assertions off, the JVM default, the same test passes and the vector reads back exactly. Adding || vectorField instanceof KnnFloat16VectorField to the assertion fixes it.

Who is affected

Anyone who puts a KnnFloat16VectorField into a MemoryIndex with assertions on: direct addField or fromDocument, or Monitor.match on one document. In practice that means test runs, Lucene's own and applications'. With assertions off, the JVM default, it works. Only unreleased main and 11.0 have the float16 field.

Mechanism, terms and numbers535 words

MemoryIndex.addField(IndexableField, Analyzer) stores a vector field through the private storeVectorValues (MemoryIndex.java:677-679, :793), whose first statement is assert vectorField instanceof KnnFloatVectorField || vectorField instanceof KnnByteVectorField; (:794). #13633 wrote it in 2024 (first released in 9.12.0), when those were the only vector field classes, and it was correct then. #16383 (2026-07-27, unreleased 11.0.0) added KnnFloat16VectorField, which extends Field and is not a KnnFloatVectorField, together with a case FLOAT16 arm in the same method (:819-840), the MemoryIndex reader's getFloat16VectorValues (:1782-1789) and MemoryFloat16VectorValues (:2425), but did not widen the assertion and added no MemoryIndex test. So with -ea, adding any float16 vector field to a MemoryIndex (directly, through fromDocument, or through Monitor.match on one document) throws a message-less AssertionError before the FLOAT16 arm runs. With -da the arm stores the vector and it reads back exactly. Only test runs are affected, and of the upstream refs inspected only main and branch_11x have the code. The fix is one line.

Terms

  • storeVectorValues(Info, IndexableField) (MemoryIndex.java:793-865): the private method addField calls when the field type has vectorDimension() > 0 (:677-679). It switches on the field's VectorEncoding (:795), with one arm per encoding: BYTE (:796-817), FLOAT16 (:819-840) and FLOAT32 (:842-863). Each arm checks the field's class, allows one value per field, and copies the vector.
  • KnnFloat16VectorField (lucene/core/.../document/KnnFloat16VectorField.java): public class KnnFloat16VectorField extends Field (:37, @lucene.experimental at :35), whose field type has VectorEncoding.FLOAT16 (:55).

The bug

  1. addField (:620) creates the field's FieldInfo from the field type (getInfo at :622, createFieldInfo at :759-783), so the encoding is FLOAT16, and calls storeVectorValues (:678). Every fromDocument overload loops over the document's fields calling addField (:571-573).
  2. The method's first statement is assert vectorField instanceof KnnFloatVectorField || vectorField instanceof KnnByteVectorField; (:794). For a KnnFloat16VectorField both are false, so with -ea it throws java.lang.AssertionError (no message) before any vector value is stored. The field is already registered by then: getInfo put its Info and FieldInfo into the field map (:740-744), so a caller that catches the error is left with a field that FieldExistsQuery matches and getFloat16VectorValues returns null for (executed probe).
  3. With -da the assertion is skipped: the FLOAT16 arm checks instanceof KnnFloat16VectorField, copies the short[] into the field's float16 state (:1273, :1279), and the reader returns it through getFloat16VectorValues (:1782-1789) and MemoryFloat16VectorValues (:2425).
  4. The assertion adds no check the arms lack for the vector field classes: each arm already throws IllegalArgumentException for a field of the wrong class (:813-816, :836-839, :859-862). Its only other effect, with -ea, is that a custom IndexableField with vector attributes that is none of the listed classes fails there with AssertionError instead of the arm's IllegalArgumentException (read).

The numbers

One field, one dimension, value Float.floatToFloat16(1f): with -ea, 1 test run, 1 failure at MemoryIndex.java:794 (gradle and standalone, seeds DEADBEEF and 5EED1234); with -da, 1 run, 0 failures and an exact read-back. Nothing depends on data, size, codec or threads.

Exposure and likelihood299 words
  • Who is hit. Code that adds a KnnFloat16VectorField to a MemoryIndex on a JVM with -ea: MemoryIndex.addField, every MemoryIndex.fromDocument overload, and lucene/monitor's Monitor.match(Document, ...) for a single document, which builds a SingletonDocumentBatch that calls addField for every field (DocumentBatch.java:107-113). A public-API probe executed for this doc: with -ea, Monitor.match throws AssertionError from MemoryIndex.java:794 via DocumentBatch$SingletonDocumentBatch.<init>(DocumentBatch.java:110) and DocumentBatch.of(DocumentBatch.java:59); with -da the registered query matches (1 match). Multi-document Monitor batches go through IndexWriter and are not affected (read). The highlighters feed MemoryIndex token streams, not fields, so they are not affected (read).
  • When. Test runs. Lucene's gradle build enables assertions for about 75% of project seeds by default (TestsAndRandomizationPlugin.java:240-248), and LuceneTestCase requires -ea to match -Dtests.asserts (TestRuleAssertionsRequired.java:36-54). Any application test suite run with -ea sees it too: a test that adds a valid float16 field through one of these paths fails.
  • Production is fine. With -da the vector is stored and read back exactly at every affected commit tested (bfe5c847d3c, ea57360912ed, 645d4fe14a20). A service running production JVMs with -ea (unusual) would lose one operation per document with the field; AssertionError is an Error, so catch (Exception e) does not catch it (executed probe).
  • Why Lucene's tests miss it. lucene/memory/src/test has no float16 test, and no test elsewhere puts a float16 field into a MemoryIndex (read, grep).
  • Scale. Deterministic: every float16 vector field that can be constructed (any dimension and similarity; the constructor itself rejects a null or empty vector, a null similarity and a zero vector with COSINE, KnnFloat16VectorField.java:39-53), on the first call. Unreleased: main and branch_11x only, of the refs inspected; KnnFloat16VectorField is @lucene.experimental. No downstream product was tested; one downstream project is still discussing whether to expose float16 at all (elastic/elasticsearch#163176).

Minimal repro

One JUnit test, one method: add a 1-dimensional KnnFloat16VectorField to an empty MemoryIndex and read it back. Public API only, no randomness, single thread. It fails with assertions on, passes with them off, and passes with either fix.

TestMemoryIndexFloat16Vector.java (JUnit, one test)34 lines
/*
 * 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.document.KnnFloat16VectorField;
import org.apache.lucene.index.LeafReader;
import org.apache.lucene.index.VectorSimilarityFunction;
import org.apache.lucene.tests.util.LuceneTestCase;

public class TestMemoryIndexFloat16Vector extends LuceneTestCase {

  public void testAddFloat16VectorField() throws Exception {
    short[] vector = {Float.floatToFloat16(1f)};
    MemoryIndex mi = new MemoryIndex();
    // expected: the vector is stored and read back; actual (-ea): AssertionError in storeVectorValues
    mi.addField(new KnnFloat16VectorField("v", vector, VectorSimilarityFunction.EUCLIDEAN), null);
    LeafReader reader = (LeafReader) mi.createSearcher().getIndexReader();
    assertArrayEquals(vector, reader.getFloat16VectorValues("v").vectorValue(0));
  }
}
Command
cp TestMemoryIndexFloat16Vector.java $LUCENE/lucene/memory/src/test/org/apache/lucene/index/memory/
cd $LUCENE && ./gradlew :lucene:memory:test --tests org.apache.lucene.index.memory.TestMemoryIndexFloat16Vector -Ptests.seed=DEADBEEF -Ptests.asserts=true
# with -Ptests.asserts=false the test passes: the bug needs assertions enabled
Expected
getFloat16VectorValues("v").vectorValue(0) returns the 1-element vector that was added (1.0 in half precision).
Observed
With -ea: java.lang.AssertionError (no message) at MemoryIndex.storeVectorValues(MemoryIndex.java:794), called from addField; with -da the test passes.

All files of this repro: repro/LFV-090/

More files, environment, full output254 words

The test sits in org.apache.lucene.index.memory next to TestMemoryIndex; it uses only public API, so it needs no package-private access. The failure is Lucene's own message-less AssertionError, so the comment line on the failing call states expected versus actual. Pass -Ptests.asserts=true explicitly: without it the build picks assertions from the seed.

As the regression test: the same test, with its comment line wrapped (below), as the first float16 test in lucene/memory/src/test/org/apache/lucene/index/memory/; if the maintainers prefer, the same method can go into the existing TestMemoryIndex next to its float32 and byte vector tests (:769, :824). Its one comment line is longer than 100 columns, which google-java-format (:lucene:memory:check) flags; with the line wrapped the module's checks pass and the test behaves the same.

Not shown by this test, same root cause, executed elsewhere: MemoryIndex.fromDocument (the finder's second test) and Monitor.match on one document (a reach probe).

Environment

lucenemain 645d4fe14a20 (11.0.0-SNAPSHOT); the same MemoryIndex.java is on upstream main and branch_11x; no release has the float16 API
jdkOpenJDK 25.0.3 (build 25.0.3+9-LTS)
osLinux, aarch64
Command (environment)
cp TestMemoryIndexFloat16Vector.java $LUCENE/lucene/memory/src/test/org/apache/lucene/index/memory/
cd $LUCENE && ./gradlew :lucene:memory:test --tests org.apache.lucene.index.memory.TestMemoryIndexFloat16Vector -Ptests.seed=DEADBEEF -Ptests.asserts=true
# with -Ptests.asserts=false the test passes: the bug needs assertions enabled

Observed output

# gradle, main 645d4fe14a20, -Ptests.seed=DEADBEEF -Ptests.asserts=true
TestMemoryIndexFloat16Vector > testAddFloat16VectorField FAILED
    java.lang.AssertionError
        at __randomizedtesting.SeedInfo.seed([DEADBEEF:C69D0D8C0A2877EA]:0)
        at org.apache.lucene.index.memory.MemoryIndex.storeVectorValues(MemoryIndex.java:794)
        at org.apache.lucene.index.memory.MemoryIndex.addField(MemoryIndex.java:678)
        at org.apache.lucene.index.memory.TestMemoryIndexFloat16Vector.testAddFloat16VectorField(TestMemoryIndexFloat16Vector.java:30)

ERROR: 1 test has failed:
  - org.apache.lucene.index.memory.TestMemoryIndexFloat16Vector.testAddFloat16VectorField (:lucene:memory)

# -Ptests.asserts=false: 1 test, 0 failures
# 10.5.2 and 10.6.0 release jars: does not compile (cannot find symbol: class KnnFloat16VectorField)

Why it looks unintended

  • #16383 added float16 storage to MemoryIndex on purpose, including a FLOAT16 branch in this very method; the assertion above it makes that branch unreachable with -ea.
  • The assertion dates from 2024, before float16 existed; no javadoc, note or test excludes float16.
  • IndexWriter accepts the same field with assertions on.
The full argument211 words
  • Deliberate support. #16383's MemoryIndex.java diff (+110/-0) adds the KnnFloat16VectorField import, the case FLOAT16 arm with its own one-value and wrong-class checks, the float16 Info state, getFloat16VectorValues and MemoryFloat16VectorValues with a scorer. The unchanged assertion on the method's first line contradicts all of it.
  • The migration note asks for this update. lucene/MIGRATE.md:629-638 ("IEEE FLOAT16 vector APIs (GITHUB#16383)"): "Custom readers and exhaustive VectorEncoding switches need a FLOAT16 arm." This switch got its arm; its guard did not.
  • History. The assertion came with #13633 (9e831ee8097, 2024-08-07, "Add float|byte vector support to memory index", first released in 9.12.0), correct when only two vector field classes existed. #16383 did not touch it, and none of its review discussion mentions MemoryIndex. The earlier, unmerged fp16 PR #15549 had the same omission.
  • No exclusion anywhere. The addField javadoc (MemoryIndex.java:613-619) names no unsupported field type; no CHANGES.txt or MIGRATE.md entry excludes MemoryIndex; no test expects the error, and lucene/memory/src/test has no float16 test. TestMemoryIndex tests the float32 and byte vector fields (:769, :824 and their round-trip helpers).
  • Elsewhere it works. IndexWriter indexes the same float16 field with -ea (verifiers' controls).
  • Precedent. #15334 fixed an NPE in the MemoryIndex vector path as a bug although MemoryIndex has no kNN search (lucene/CHANGES.txt:1493-1494, 10.4.0).
The devil's-advocate check244 words

The case for intended: (1) the failure is assertion-only, so production with -da is unaffected; (2) KnnFloat16VectorField is @lucene.experimental and float16 support is openly incomplete (tracker #16725); (3) MemoryIndex runs no kNN search for any encoding (searchNearestVectors are no-ops, MemoryIndex.java:1800-1811; TestMemoryIndex.java:819: "we don't really do knn search right now for MemoryIndex"), so its vector support is partial anyway; (4) the assertion could be read as a precondition that only the float32 and byte classes may reach storeVectorValues.

Why it fails: (a) the float32 and byte classes carry the same @lucene.experimental tag (KnnFloatVectorField.java:38, KnnByteVectorField.java:38) and are supported and tested in MemoryIndex; (b) if float16 were meant to be excluded, #16383 would not have written a FLOAT16 arm, state, reader and values class for MemoryIndex; (c) the missing kNN search applies to every encoding and concerns searching, not storing; (d) nothing supports the precondition reading: the assertion predates the class it rejects; (e) "assertion-only" lowers the severity, not the validity: the assertion itself is the defect, and it fires in the configuration Lucene's tests use. The cross-model reviews rated the intended argument weak; the devil's advocate and the adjudication rejected it. A later cross-model fact check of this doc rated the same points moderate as a case that the defect is low-value and not worth an issue of its own, and weak as a case that it was designed so: two different questions. Verdict: not intended; a low-severity oversight.

Suggested fix

Recommended A. Widen the assertion to the float16 field class

Add || vectorField instanceof KnnFloat16VectorField to the assertion at the top of storeVectorValues (the class is already imported). Validated with gradle: the repro passes, and so do all 52 lucene/memory tests. Dropping the assertion also works, since each branch already rejects a wrong field class.

   private void storeVectorValues(Info info, IndexableField vectorField) {
-    assert vectorField instanceof KnnFloatVectorField || vectorField instanceof KnnByteVectorField;
+    assert vectorField instanceof KnnFloatVectorField
+        || vectorField instanceof KnnFloat16VectorField
+        || vectorField instanceof KnnByteVectorField;
     switch (info.fieldInfo.getVectorEncoding()) {
All options and trade-offs2 options ยท 276 words

Both options were run through gradle in a scratch worktree of 645d4fe14a20 (seed DEADBEEF). Either way, add the minimal test as the regression test.

A. Widen the assertion to the float16 field class (validated by the checks) (recommended)

MemoryIndex.java:794; KnnFloat16VectorField is already imported at :40.

   private void storeVectorValues(Info info, IndexableField vectorField) {
-    assert vectorField instanceof KnnFloatVectorField || vectorField instanceof KnnByteVectorField;
+    assert vectorField instanceof KnnFloatVectorField
+        || vectorField instanceof KnnFloat16VectorField
+        || vectorField instanceof KnnByteVectorField;
     switch (info.fieldInfo.getVectorEncoding()) {

Trade-offs

  • The smallest change; keeps the original guard and its meaning ("one of the vector field classes").
  • A future vector encoding could fall into the same trap.

What the checks validated

  • The repro passes with assertions on and off (1/1 each); it fails on stock main with assertions on.
  • All lucene/memory tests with assertions on: 4 classes, 52 tests, 0 failures (TestMemoryIndex 30, TestMemoryIndexAgainstDirectory 16, TestSlicedIntBlockPool 5, the repro 1).
  • :lucene:memory:check -x test --continue with the fix and the comment-wrapped test passes (google-java-format, ecj lint and forbidden-apis on main and test sources).

B. Drop the assertion (validated by the checks)

Remove line :794. Dropping it loses no check for the vector field classes: each arm already throws IllegalArgumentException for a field of the wrong class (:813-816, :836-839, :859-862).

Trade-offs

  • The next encoding cannot be blocked by it, and a custom vector field that is none of the three classes gets the same IllegalArgumentException with -ea as it already does with -da.
  • Removes a guard some reviewers may want to keep.

What the checks validated

  • The repro and TestMemoryIndex with assertions on: 2 classes, 31 tests, 0 failures.

Downstream note

Applications that feed documents with float16 vector fields to MemoryIndex or Monitor see this only in assertion-enabled runs, typically their test suites; production JVMs without -ea are unaffected.

How it was found

Non-formal: a differential edge probe. The hunt on float16 vectors ran the same edge cases through float16 and float32 vector fields across Lucene's APIs; the MemoryIndex case failed only with assertions on, and the failing frame pointed at the assertion. Independent reproducers, an adjudication and reviewers from another model family then confirmed it.

The method in full131 words

Method: non-formal (a differential JUnit, not a model or a bounded-exhaustive check). The hunt unit on float16 vectors wrote edge probes that run a float16 field and its float32 twin through the same APIs and compare the outcomes. TestFp16Probes#testMemoryIndex failed with -ea and passed with -da; reading the failing frame (MemoryIndex.java:794) gave the root cause.

The finder wrote a two-test repro (addField, fromDocument): 2/2 fail with -ea, 2/2 pass with -da and with the one-line fix. It was then reviewed by a reviewer from another model family (OpenAI GPT-5.6, refute and works-as-designed lenses), reproduced by independent verifiers (one from a package outside Lucene, with float32 and byte controls), judged, traced through history, cut to the one-test repro on 645d4fe14a20, and reviewed again there.

Prior reports, history and checks

Prior reports

No matching prior report found in the recorded searches of GitHub issues, PRs and commits, the JIRA archive, the Lucene mailing lists, downstream repositories or the web (searched 2026-10-26 and again 2026-11-01). Not fixed upstream, and no open PR touches the assertion.

Searches4 searches

Closest items, none a report of this bug: #16383 (introduced it), #13633 (wrote the assertion), #15549 (earlier unmerged fp16 PR with the same omission), #16725 (fp16 completion tracker, no MemoryIndex item), #15334 (a different MemoryIndex vector-path bug, an NPE, fixed as a bug), #16847 (open draft, renames in MemoryIndex only). Downstream: elastic/elasticsearch#163176 (whether to expose float16 at all) and opensearch-project/k-NN#3556 (its own percolator code). Limits: on the 2026-11-01 refresh, 18 planned queries were run by hand; GitHub search does not index review comments (those were read directly for the PRs named); no hit is not proof that nobody has noticed it.

What was searched, when, and what turned up
WhereWhenHowFound
apache/lucene issues, PRs and commits; downstream repositories2026-10-26 and 2026-11-01scripted prior-report searches plus manual GitHub searches: MemoryIndex with float16 / fp16 / half float, KnnFloat16VectorField, Float16VectorValues, getFloat16VectorValues, MemoryFloat16VectorValues, storeVectorValues, the assertion text, MemoryIndex AssertionError, monitor and percolator with float16; elasticsearch, OpenSearch, k-NN and solrno report
Pull requests by touched file2026-11-01file lists of all 409 open PRs, and of the closed PRs since 2026-07-01 (2026-10-26) and since 2026-10-20 (2026-11-01)7 open PRs touch MemoryIndex.java (#16847, #14716, #14475, #13948, #13525, #13342, #486); none touches storeVectorValues, the assertion or float16
Review threads and recent activity2026-11-01#16383, #15549, #13633, #15334, #16725 and others read directly; all issues, PRs, conversation and review comments since 2026-10-22no mention of MemoryIndex with float16
Mailing lists, JIRA archive, web2026-10-26 and 2026-11-01dev@, java-user@, issues@ and commits@ through the list archive API; JIRA-archive and web queriesonly fp16 feature threads and commit notifications; float16 postdates JIRA

History

Introduced on main by #16383 (bfe5c847d3c, 2026-07-27), which added float16 vectors and MemoryIndex's FLOAT16 branch but kept the 2024 assertion from #13633. No release has it; main and branch_11x (11.0.0) do. Executed at the introducing commit, its parent and current main.

Versions tested6 rows

Introduced by #16383 (PR title "Add fp16 (float16) vector encoding support - no quantization support"; merge commit subject "Add fp16 (float16) vector encoding (without support for quantization)"; Pulkit Gupta, merged 2026-07-27 as bfe5c847d3c; parent 606a26dafd5). The assertion line comes from #13633 (9e831ee8097, 2024-08-07; backport 38201baaf3a, first released in 9.12.0) and is correct in every release from 9.12.0 through 10.6.0, where only the BYTE and FLOAT32 arms exist. Found with git show, git blame and git log -S on the assertion text and on KnnFloat16VectorField over lucene/memory. branch_11x, cut on 2026-10-29 for 11.0.0, has the same MemoryIndex.java, so 11.0.0 would ship the bug unless it is fixed first.

Versions and branches executed, with results
Version or commitVerdictResult
main 645d4fe14a20 affectedminimal repro fails with assertions on and passes with them off (gradle and standalone); passes with fix A or B
main bfe5c847d3c (#16383), ea57360912ed introducedfails with -ea, passes with -da; at bfe5c847d3c the one-line fix makes it pass; a float32/byte control passes
main a9779ef3c193 -ea onlythe finder's two-test class fails with -ea (2 of 2); only -ea was run there; MemoryIndex.java is the same blob (efd49fdc21fb) as at bfe5c847d3c and 645d4fe14a20
main 606a26dafd5 (parent of #16383) no APIthe test does not compile (no float16 API); the float32/byte control passes
10.6.0, 10.5.2 (and every earlier release) no APIthe test does not compile (no KnnFloat16VectorField) and the float32/byte control passes, executed on 10.5.2 and 10.6.0; no release tag has the float16 API (read)
upstream main, branch_11x readthe same MemoryIndex.java (2026-11-01); the file was last changed by #16383, while line 794 itself still comes from #13633 (git blame)

Independent checks

Confirmed with high confidence: the finder, independent reproducers, an intended-behaviour devil's advocate, an adjudication and three review sessions by a reviewer from another model family (OpenAI GPT-5.6) all reproduced it and agree on the root cause and the one-line fix.

Checks and verdicts393 words

Everyone agrees the impact is small: assertion-enabled runs only, an experimental field type, unreleased.

Each independent check and its verdict
CheckVerdictWhat it found
finder candidateDifferential edge probe failed with -ea, passed with -da; its two tests (addField, fromDocument) fail 2/2 with -ea and pass with -da and with the fix.
cross-model review, refute lens (OpenAI GPT-5.6) real, highOwn repro fails with -ea, passes with -da and with the fix; float32 control passes; patched TestMemoryIndex 30/30. Intended argument weak.
independent reproducer realOwn public-API test outside org.apache.lucene: -ea 4 run / 2 failed in the same frame with float32 and byte controls passing; -da 4/4 with an exact round trip; fix 4/4; 10.5.2 does not compile.
cross-model review, works-as-designed lens (OpenAI GPT-5.6) real, highOwn repro -ea 4/2, -da 4/0; an IndexWriter float16 control passes with -ea; the fix passes its tests and TestMemoryIndex 30/30. Intended argument weak.
intended-behaviour devil's advocate real, low severityBuilt the strongest intended case and rejected it; own test -ea 4/2, -da 4/0; found the Monitor reach by reading the code.
adjudication confirmed, highRe-ran the independent reproduction's test with a new seed: -ea 4/2 at MemoryIndex.java:794, -da 4/0.
verify on 645d4fe14a20 and refute review (OpenAI GPT-5.6) real; real, highThe minimal test fails with -ea, passes with -da and with the fix; the reviewer's own runs, on ea57360912ed: its 3-dimensional test with a float32 control fails with -ea and passes with the fix, and the full TestMemoryIndex passes with the fix; its rerun on 645d4fe14a20: -ea 2 run / 1 failed, -da 1/0, fix 2/0.
this doc fails through gradle; fixes validated; Monitor reach executedGradle repro with assertions on and off, fixes A and B with the module's tests, and a Monitor.match probe (fails with -ea, matches with -da).
fact checks of this doc (Claude; OpenAI GPT-5.6) real; errors fixedBoth re-checked the claims against the code, the history and the recorded runs; the Claude check re-ran the minimal test (-ea fails at :794, -da passes, fix A passes both) and the Monitor.match probe. One overstated run record, six factual errors (none high severity), four overclaims and three missing caveats were found; all are corrected in this version.

Diagram file

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.

Sources and provenance

Finding record, Lucene and upstream sources, repro files and diagram notes872 words

Finding record

LFV-090, snapshot of 2026-11-06

Registry title
MemoryIndex.addField throws AssertionError under -ea for a KnnFloat16VectorField, because a stale assertion in storeVectorValues predates the FLOAT16 branch
Area
memory / MemoryIndex vectors
Priority
Low, rank 1166: MemoryIndex throws AssertionError under -ea for a KnnFloat16VectorField because of a stale assertion, test-suite only and unreleased.
Impact class
minor
Impact note
Only JVMs running with Java assertions enabled (-ea), such as Lucene's own tests and application test suites, that put a KnnFloat16VectorField into a MemoryIndex (addField, fromDocument, or lucene/monitor's single-document batch, executed by a Monitor.match probe); with -da the field is stored and read back correctly; unreleased main and branch_11x (11.0.0) only; ordinary scale.
Summary
You add a KnnFloat16VectorField to a MemoryIndex, through MemoryIndex.addField or MemoryIndex.fromDocument, with Java assertions enabled. You expect it to be stored and readable through getFloat16VectorValues, like a float32 or byte vector field, because MemoryIndex has a FLOAT16 storage branch and a FLOAT16 reader. Instead the first statement of storeVectorValues asserts that the field is a KnnFloatVectorField or a KnnByteVectorField, so the call throws AssertionError before the FLOAT16 branch runs; with assertions disabled the vector is stored and read back exactly.
Introduced
#16383 (bfe5c847d3c) [read: git blame, git log -S and -G over lucene/memory; exec: the repro fails at bfe5c847d3c under -ea, passes there under -da and with the one-line fix shadowed, and does not compile at its parent 606a26dafd5, which has no float16 API]; the assertion line itself was written by #13633 (9e831ee8097, 2024-08-07) and was correct then, when only the float32 and byte vector field classes existed (2026-07-27)
First affected version
unreleased (main and the 11.0 release branch branch_11x, 11.0.0-SNAPSHOT; no release tag contains bfe5c847d3c)
Affected versions
main from bfe5c847d3c (2026-07-27) through 645d4fe14a20 [exec: the repro fails under -ea and passes under -da at bfe5c847d3c, ea57360912ed and 645d4fe14a20; MemoryIndex.java is byte-identical at these commits]; branch_11x (cut 2026-10-29 for 11.0.0) has the same MemoryIndex.java [read 2026-11-01]; 10.6.0, 10.5.2 and 9.12.3 not affected (no float16 vector API, the repro does not compile) [exec]; 9.12.0, 9.12.3, 10.0.0 and 10.5.2 carry the same two-class assertion, which is correct there (a float32/byte control passes under -ea) [exec]; branch_10x and branch_10_6 have no KnnFloat16VectorField [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 agreed, an intended-behaviour check, a prior-report search and a skeptical audit; not yet triaged by a person.
Found by
non-formal
Upstream and prior reports
none found (2026-10-26, 208 read-only queries: 2 scripted prior-report searches with 54 GitHub issue/PR, 10 commit, 9 downstream, 40 mailing-list, 10 JIRA-archive and 9 web queries; 58 manual GitHub API searches; file lists of all 412 open PRs and of the 355 PRs created since 2026-07-01 and closed; 16 direct mailing-list queries; review threads of #16383, #15549, #13633, #16473, #16628, #16710, #16735, #16738, #16708 and tracker #16725 read directly); related prior items: #16383 (introduced it), #13633 (wrote the assertion), #15549 (earlier unmerged fp16 PR with the same omission), #16725 (fp16 tracker, no MemoryIndex item), #15334 (a MemoryIndex vector-path NPE fixed as a bug); open #16847 touches MemoryIndex.java but not storeVectorValues
Lucene commit tested
main 645d4fe14a20 (gradle in a scratch worktree: the minimal test with assertions on and off, stock and with fix A, all lucene/memory tests with fix A, the memory module's static checks with fix A and the comment-wrapped test, the minimal test and TestMemoryIndex with fix B; standalone runs with assertions on and off and a second seed; a Monitor.match reach probe with assertions on and off), the 10.5.2 and 10.6.0 release jars (the test does not compile: no float16 API), and, by the history step and the hunt's verifiers, main bfe5c847d3c (#16383), its parent 606a26dafd5, ea57360912ed and a9779ef3c193. MemoryIndex.java is the same blob at ea57360912ed, 645d4fe14a20, upstream main and branch_11x (read, 2026-11-01).

Lucene and upstream sources

Finding record
  • lucene/memory/src/java/org/apache/lucene/index/memory/MemoryIndex.java:794 (the two-class assertion), :819-840 (the FLOAT16 arm), :564-575 (fromDocument calls addField per field), :620-679 (addField calls storeVectorValues at :678), :1783-1789 (getFloat16VectorValues), :2425 (MemoryFloat16VectorValues)
  • lucene/core/src/java/org/apache/lucene/document/KnnFloat16VectorField.java:35-37 (@lucene.experimental; extends Field, not KnnFloatVectorField)
  • lucene/monitor/src/java/org/apache/lucene/monitor/DocumentBatch.java:107-113 (SingletonDocumentBatch calls MemoryIndex.addField for every field; first found by code reading, since executed by a Monitor.match probe on 645d4fe14a20: fails with -ea, matches with -da)
  • lucene/CHANGES.txt:168 (GITHUB#16383 in the 11.0.0 section), :2574 (GITHUB#13633)
  • lucene/memory/src/test (no float16 or fp16 reference: grep -rli 'float16\|fp16' finds no file)
  • SECURITY.md:28 (downstream application sanitizes untrusted input), :50 (in-scope reports)
The finding in 30 seconds
  • lucene/memory/src/java/org/apache/lucene/index/memory/MemoryIndex.java at 645d4fe14a20 (:564-575, :620-679, :732-783, :793-865, :1273, :1279, :1782-1789, :1800-1811, :2425)
  • lucene/core/src/java/org/apache/lucene/document/KnnFloat16VectorField.java at 645d4fe14a20 (:35, :37, :55)
Who is affected
  • lucene/monitor/src/java/org/apache/lucene/monitor/DocumentBatch.java at 645d4fe14a20 (:107-113); Monitor.java (:220, :240-242)
  • build-tools/build-infra/src/main/java/org/apache/lucene/gradle/plugins/java/TestsAndRandomizationPlugin.java at 645d4fe14a20 (:240-248, :440-441)
  • lucene/test-framework/src/java/org/apache/lucene/tests/util/TestRuleAssertionsRequired.java at 645d4fe14a20 (:36-54)
Why it looks unintended
  • lucene/MIGRATE.md at 645d4fe14a20 (:629-638); lucene/CHANGES.txt (:174, :1493-1494, :2607)
  • lucene/memory/src/test/org/apache/lucene/index/memory/TestMemoryIndex.java at 645d4fe14a20 (:769, :819, :824)
  • lucene/core/src/java/org/apache/lucene/document/KnnFloatVectorField.java (:38), KnnByteVectorField.java (:38)

Other statements trace to the hunt's working files (models, logs, reviews and briefs), which are not published.

Repro files

About the diagrams

Made in parallel with this doc from the code at 645d4fe14a20 and the repro's input and output.

Built 2026-11-06 from the hunt's record of this finding and its doc file. Lucene main under test in this hunt: ea57360912ed. Every listed finding was re-run on Lucene main e1faeba08bde on 2026-11-02.