All findings · Lucene finding · LFV-089 · checkindex-skips-float16-vector-fields

CheckIndex silently skips FLOAT16 vector fields: it reports "OK [0 fields, 0 vectors]" and never runs checkFloat16VectorValues

On Lucene main and branch_11x (unreleased), CheckIndex at level 2 or higher skips every KnnFloat16VectorField: an index with one FLOAT16 vector reports OK [0 fields, 0 vectors], while the same index with a FLOAT32 vector reports 1 field and 1 vector. The FLOAT16 checks never run; search is unaffected.

Summary

Priority
Low, rank 750CheckIndex reports 'OK [0 fields]' and never runs the FLOAT16 checks, including in the test framework's check-on-close, which hides bugs in a new feature, but data is unaffected and it is unreleased with a one-term fix.
Impact
minor
Since
#16383 (main, 2026-07-27; unreleased 11.0.0)introduced 2026-07-27
Affected
main from #16383 (bfe5c847d3c) through 645d4fe14a20 (executed) and branch_11x (read); no release has FLOAT16 vectors
Status
documented
Found by
non-formal: code reading of every VectorEncoding dispatch site, then an fp16 twin of an upstream test

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

CheckIndex's vectors test asks the reader for float32 or byte vectors and skips the field when it gets neither. The FLOAT16 encoding brought a third getter and a FLOAT16 checker, but not a third term in that skip test, so every FLOAT16 field is skipped.

Background

  1. CheckIndex levels. Level 1, the command-line default, verifies file checksums only. Level 2 adds the logical integrity checks, such as reading every vector of a field and searching some of them; level 3 adds slow checks.
  2. One getter per encoding. A vector field has one encoding: FLOAT32 (KnnFloatVectorField), BYTE (KnnByteVectorField) or, new in the unreleased 11.0, FLOAT16 (KnnFloat16VectorField). A segment reader has one getter per encoding, and each returns null for a field of another encoding.
Diagram of the bug: an index with one KnnFloat16VectorField is checked with CheckIndex at level 2. In testVectors the skip guard asks getFloatVectorValues and getByteVectorValues; both return null for a FLOAT16 field, so the field is skipped before the FLOAT16 checker. The report says test: vectors OK [0 fields, 0 vectors], while the same index with a FLOAT32 vector says OK [1 fields, 1 vectors].
One FLOAT16 field passing through CheckIndex's vectors test: the float32 and byte getters both return null, the skip guard fires, and the report says 0 fields and 0 vectors while the FLOAT16 checker is never reached. Select the still to open it at full size. Generated with AI, checked against the code.

The finding, step by step

  1. Index one document with a 2-dimension KnnFloat16VectorField [1, 2] (default codec), then run CheckIndex at level 2, the level that adds the logical integrity checks: setLevel(2) on the API or -level 2 on the command line.
  2. You expect the vectors test to count 1 field and 1 vector and to run the FLOAT16 checks. The same index built with KnnFloatVectorField prints test: vectors.............OK [1 fields, 1 vectors].
  3. Instead it prints OK [0 fields, 0 vectors], in the same report where field infos and hnsw graphs each see the 1 field, and ends with "No problems were detected with this index."
  4. Why: before checking a field, CheckIndex asks for float32 or byte vectors and skips the field when it gets neither. A FLOAT16 field returns null from both, so the FLOAT16 checker that came with the encoding never runs.
  5. Every FLOAT16 field is skipped every time, also in Lucene's own randomized tests that run CheckIndex when an index is closed. Unreleased 11.0 code (main and branch_11x); indexing, search and file checksums are unaffected.

Who is affected

Anyone on main or branch_11x who indexes KnnFloat16VectorField and runs CheckIndex at level 2 or 3: the API, -level 2/3, TestUtil.checkIndex, the test framework's check-on-close. Every FLOAT16 field loses its vector checks and counts, so a FLOAT16 bug those checks catch (wrong dimension, missing or unsearchable vectors) passes unnoticed. Unreleased; search, data and checksums are unaffected.

Mechanism, terms and numbers606 words

On main, CheckIndex.testVectors skips a vector field when both getFloatVectorValues and getByteVectorValues return null (CheckIndex.java:2864-2867 at 645d4fe14a20). CodecReader's final getters return null for any field whose encoding is not FLOAT32 or BYTE respectively, so every FLOAT16 field (KnnFloat16VectorField, new in the unreleased 11.0 with #16383; on main and branch_11x) is skipped before it is counted: CheckIndex at level 2 or higher prints test: vectors.............OK [0 fields, 0 vectors] for an index with one FLOAT16 vector, while the same index with a FLOAT32 vector gives OK [1 fields, 1 vectors]. The FLOAT16 arm of the switch and the dedicated checkFloat16VectorValues (kNN self-search, dimension and size checks), added by the same commit, can never run. Lucene's randomized vector-format tests index FLOAT16 vectors and run CheckIndex on close, but a skipped field raises no error. File checksums and, on HNSW formats such as the default codec, the encoding-agnostic HNSW graph check still run, and indexing and search are not affected. The fix is one more term in the guard, the same widening #12064 made when byte vectors got their own getter.

The code (main 645d4fe14a20)

  1. checkIndex runs testVectors only at level >= Level.MIN_LEVEL_FOR_INTEGRITY_CHECKS = 2 (CheckIndex.java:1114, :1141, :4754). The command-line default is level 1, "Checksum checks only" (:4748, :4837).
  2. For each field with vector values, testVectors does:
if (reader.getFloatVectorValues(fieldInfo.name) == null
    && reader.getByteVectorValues(fieldInfo.name) == null) {
  continue;
}
status.totalKnnVectorFields++;
switch (fieldInfo.getVectorEncoding()) {
  case BYTE:    checkByteVectorValues(...);
  case FLOAT16: checkFloat16VectorValues(...);
  case FLOAT32: checkFloatVectorValues(...);

(CheckIndex.java:2864-2898, abridged; the FLOAT16 arm is :2878-2884.)

  1. CodecReader.getFloatVectorValues and getByteVectorValues are final and return null unless the field's encoding is FLOAT32 or BYTE respectively (CodecReader.java:235-252, :255-265). Only getFloat16VectorValues returns values for a FLOAT16 field (:269-279).
  2. So for a FLOAT16 field the loop takes continue before the counter (CheckIndex.java:2869) and before the switch. testVectors takes a CodecReader and the getters are final, so checkFloat16VectorValues (:3164-3213) cannot run for any reader. The summary line prints both counters: OK [0 fields, 0 vectors] (:2902-2909).

What still runs, what is lost

  • Still runs: file checksums (level 1 and up) and testHnswGraphs (CheckIndex.java:2926-2945), which loops over every vector field whatever its encoding and checks the graph of each field whose reader is an HnswGraphProvider (:2937-2944), as with the default codec; the probe shows hnsw graphs ... OK [1 fields] for the FLOAT16 index.
  • Lost for FLOAT16 fields: what checkFloat16VectorValues does, the same checks FLOAT32 and BYTE fields get: it reads every vector by ordinal and checks its length against the field's dimension; for each vector whose document id is a multiple of max(size/64, 1) (every vector, for fields with fewer than 128) it runs a k=10 kNN search that must return a hit (when the format supports search); and it checks that the number of vectors read equals size(). Plus correct field and vector counts.

The numbers

One document, one 2-dimension EUCLIDEAN vector [1, 2], default codec, CheckIndex <dir> -level 2 on the stock jar of 645d4fe14a20:

field typetest: vectorstest: hnsw graphs
KnnFloatVectorFieldOK [1 fields, 1 vectors]OK [1 fields]
KnnFloat16VectorFieldOK [0 fields, 0 vectors]OK [1 fields]
KnnFloat16VectorField, with the fixOK [1 fields, 1 vectors]OK [1 fields]

The finder's test and the verifiers saw the same on larger indexes: a 2-field, 3-vector FLOAT16 index reports 0 fields / 0 vectors, and a segment with a FLOAT32 (7 vectors), a BYTE (5) and a FLOAT16 (11) field reports OK [2 fields, 12 vectors]: only the FLOAT16 field is missing.

Exposure and likelihood241 words
  • Who is hit. Main and branch_11x (FLOAT16 vectors are unreleased, 11.0.0). Default codec, any index size, any similarity. CheckIndex at level 2 or 3: the API with setLevel, the command line with -level 2/3, TestUtil.checkIndex(Directory) (level 3, TestUtil.java:302-303) and the test framework's check-on-close (level 3, or 2 without slow checks; BaseDirectoryWrapper.java:32-33, :44-45, :64-70). The command-line default, level 1, checks only checksums for every encoding.
  • Deterministic. Every FLOAT16 field is skipped, every time: over 10 random-seed iterations every FLOAT16 test failed and every FLOAT32/BYTE control passed.
  • CheckIndex's part of Lucene's test net. Lucene tests FLOAT16 reading, search and graphs directly (for example TestHnswFloat16VectorGraph, TestFloat16VectorSimilarityQuery). What misses FLOAT16 is the CheckIndex layer: BaseKnnVectorsFormatTestCase draws the vector encoding at random per test, FLOAT16 included (BaseKnnVectorsFormatTestCase.java:128, :147-157, :1351), and the indexes its tests make with newDirectory() are checked on close. With the bug, a FLOAT16 defect that only CheckIndex would catch (wrong dimension, missing or unsearchable vectors) passes that check. FLOAT16 is new code still being completed (tracker #16725), which is where such checks matter most.
  • Default vs opt-in. The codec is the default; the experimental field type (KnnFloat16VectorField is @lucene.experimental) and CheckIndex level 2 or higher are the opt-ins.
  • Not affected. Indexing, search hits, scores, live docs and stored data. File checksums and, on HNSW formats such as the default codec, the HNSW graph check still run for FLOAT16 fields.

Minimal repro

One JUnit test on the public API: one document with one 2-dimension FLOAT16 vector, the default codec, CheckIndex at level 2, then assert that the vector status counts 1 field and 1 vector. Single thread, deterministic, any seed.

TestCheckIndexFloat16Vectors.java (one test)47 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;

import org.apache.lucene.document.Document;
import org.apache.lucene.document.KnnFloat16VectorField;
import org.apache.lucene.store.ByteBuffersDirectory;
import org.apache.lucene.store.Directory;
import org.apache.lucene.tests.util.LuceneTestCase;
import org.apache.lucene.tests.util.TestUtil;

public class TestCheckIndexFloat16Vectors extends LuceneTestCase {

  public void testFloat16VectorFieldIsChecked() throws Exception {
    try (Directory dir = new ByteBuffersDirectory()) {
      IndexWriterConfig iwc = new IndexWriterConfig().setCodec(TestUtil.getDefaultCodec());
      try (IndexWriter w = new IndexWriter(dir, iwc)) {
        Document doc = new Document();
        short[] v = {Float.floatToFloat16(1f), Float.floatToFloat16(2f)};
        doc.add(new KnnFloat16VectorField("v", v, VectorSimilarityFunction.EUCLIDEAN));
        w.addDocument(doc);
      }
      try (CheckIndex checker = new CheckIndex(dir)) {
        checker.setLevel(CheckIndex.Level.MIN_LEVEL_FOR_INTEGRITY_CHECKS);
        CheckIndex.Status.VectorValuesStatus s =
            checker.checkIndex().segmentInfos.get(0).vectorValuesStatus;
        // 1 FLOAT16 field with 1 vector was indexed; CheckIndex reports 0 fields, 0 vectors
        assertEquals("vector fields checked", 1, s.totalKnnVectorFields);
        assertEquals("vector values checked", 1, s.totalVectorValues);
      }
    }
  }
}
Command
cd $LUCENE && ./gradlew :lucene:core:test --tests TestCheckIndexFloat16Vectors -Ptests.seed=DEADBEEF
Expected
totalKnnVectorFields = 1 and totalVectorValues = 1: the one FLOAT16 field and its one vector are checked.
Observed
AssertionError: vector fields checked expected:<1> but was:<0>, with assertions on and off.

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

More files, environment, full output241 words

KnnFloat16VectorField, IndexWriter, CheckIndex.setLevel and CheckIndex.Status, plus Lucene's test framework (LuceneTestCase, TestUtil), so it lives in Lucene's test tree; no package-private members; the default codec is pinned with TestUtil.getDefaultCodec() because the test framework otherwise randomizes it; ByteBuffersDirectory. Copy the file into lucene/core/src/test/org/apache/lucene/index/ and run the command; the same result with -Ptests.asserts=false.

Regression test: this same test; it passes with the fix and fits as one more method in TestCheckIndex.

Environment

lucenemain 645d4fe14a20 (11.0.0-SNAPSHOT): fails; with the fix: passes. Releases 10.6.0, 10.5.2, 9.12.3: no FLOAT16 API, the test does not compile
jdkOpenJDK 25.0.3 (build 25.0.3+9-LTS)
osLinux, aarch64 (the only platform run; the code path is pure Java)
Command (environment)
cp $REPRO/TestCheckIndexFloat16Vectors.java $LUCENE/lucene/core/src/test/org/apache/lucene/index/
cd $LUCENE && ./gradlew :lucene:core:test --tests TestCheckIndexFloat16Vectors -Ptests.seed=DEADBEEF
# the same result with -Ptests.asserts=false

Observed output

# gradle, stock 645d4fe14a20, -Ptests.seed=DEADBEEF (assertions on; the same with -Ptests.asserts=false)
TestCheckIndexFloat16Vectors > testFloat16VectorFieldIsChecked FAILED
    java.lang.AssertionError: vector fields checked expected:<1> but was:<0>
        at __randomizedtesting.SeedInfo.seed([DEADBEEF:77A3C607A3646CF3]:0)
        at org.junit.Assert.fail(Assert.java:89)
        at org.junit.Assert.failNotEquals(Assert.java:835)
        at org.junit.Assert.assertEquals(Assert.java:647)
        at org.apache.lucene.index.TestCheckIndexFloat16Vectors.testFloat16VectorFieldIsChecked(TestCheckIndexFloat16Vectors.java:42)

# with the recommended fix: 1 test, 0 failures (assertions on and off)

# the same one-vector index through the command line, CheckIndex <dir> -level 2, stock jar:
    test: field infos.........OK [1 fields] [took 0.000 sec]
    test: vectors.............OK [0 fields, 0 vectors] [took 0.000 sec]
    test: hnsw graphs.........OK [1 fields] [took 0.002 sec]
No problems were detected with this index.
# with the fix: test: vectors.............OK [1 fields, 1 vectors]

Why it looks unintended

  • The same commit (#16383) added a FLOAT16 arm and a dedicated checkFloat16VectorValues; the guard in front of them makes both unreachable.
  • When byte vectors got their own getter (#12064), this same guard was widened for it in the same change.
  • totalKnnVectorFields is documented as "Total number of fields with vectors"; a field with a FLOAT16 vector reports 0.
The full argument240 words
  • A checker that cannot run. git log -S 'checkFloat16VectorValues' and git log -S 'case FLOAT16:' on CheckIndex.java return only #16383 (bfe5c847d3c). Nobody writes a checker in order never to call it.
  • Precedent in the same guard. #12064 (cc29102a24a, 2023) gave byte vectors their own field and getter (KnnByteVectorField, getByteVectorValues; byte-encoded vectors themselves date from LUCENE-10577, released in 9.4.0), made the getters return null on an encoding mismatch, and widened this guard from one getter to two in the same change.
  • Contract text. totalKnnVectorFields: "Total number of fields with vectors" (CheckIndex.java:420-421). Level 2: "All level 1 checks + logical integrity checks" (:4838). LeafReader.getFloat16VectorValues returns values "or null if no Float16VectorValues were indexed" (LeafReader.java:237-243), the same contract as the other getters. MIGRATE.md's #16383 section: "exhaustive VectorEncoding switches need a FLOAT16 arm" (MIGRATE.md:629-645); CheckIndex's switch did get that arm and the omission is the null guard in front of it, so this supports "every encoding dispatch must handle FLOAT16" rather than naming this guard.
  • Tests. testCheckIndexIncludesVectors (BaseKnnVectorsFormatTestCase.java:1947-1976: "Make sure CheckIndex in fact declares that it is testing vectors!") and TestCheckIndex.java:203-207 assert the vector counts, for KnnFloatVectorField only. No test, javadoc, CHANGES or MIGRATE entry says CheckIndex excludes FLOAT16.
  • History. None of the 101 texts of the #16383 thread mentions CheckIndex; the earlier, unmerged fp16 PR #15549 had the identical CheckIndex hunk without the guard term.
The devil's-advocate check169 words

The strongest case for "intended": FLOAT16 is unreleased and @lucene.experimental, and so is CheckIndex itself (CheckIndex.java:110); the fp16 work is openly incomplete (tracker #16725 says "there might be more" missing places); the guard is old and deliberate (it skips fields the codec has no values for); nothing is corrupted or mis-searched; and the call-site comment still reads "// Test FloatVectorValues and ByteVectorValues" (:1140).

Why it fails: every point is about impact or timing, not intent. The same commit wrote the FLOAT16 checker, so the skip is an omission. The guard's purpose, skipping fields with no values, is kept by the fix, since getFloat16VectorValues returns null exactly when no FLOAT16 values exist. The comment is pre-FLOAT16 text, not a design statement. The cross-model reviews rated the intended argument weak; a later cross-model fact check agreed, calling it moderate only as a triage argument (maintainers may fold it into the incomplete fp16 work of #16725 at low priority). Verdict: not intended, a defect of low severity.

Suggested fix

Recommended Add the FLOAT16 getter to the guard

Add && reader.getFloat16VectorValues(fieldInfo.name) == null to the skip guard in CheckIndex.testVectors, the same widening #12064 made for BYTE. The repro passes with it, the FLOAT16 index then reports OK [1 fields, 1 vectors], and the closest suites, which check randomized FLOAT16 indexes on close, still pass.

if (reader.getFloatVectorValues(fieldInfo.name) == null
    && reader.getByteVectorValues(fieldInfo.name) == null
    && reader.getFloat16VectorValues(fieldInfo.name) == null) {
  continue;
}
All options and trade-offs2 options ยท 447 words

One option was built and validated; the alternative makes the compiler catch the next new encoding.

Add the FLOAT16 getter to the guard (validated by the checks) (recommended)

One term in CheckIndex.testVectors (CheckIndex.java:2864-2867), +2/-1. Optionally also update the stale call-site comment at :1140 ("Test FloatVectorValues and ByteVectorValues"); comment only, not part of the validated diff.

if (reader.getFloatVectorValues(fieldInfo.name) == null
    && reader.getByteVectorValues(fieldInfo.name) == null
    && reader.getFloat16VectorValues(fieldInfo.name) == null) {
  continue;
}

Trade-offs

  • Cost. Not benchmarked. The re-enabled checker has the same shape as the FLOAT32 and BYTE ones (one pass over the vectors plus sampled searches), so a level-2 check of a FLOAT16 field is expected to cost about what a FLOAT32 field costs.
  • Side effect, intended. The checker now runs in every test that closes a FLOAT16 index, so randomized runs could surface latent FLOAT16 codec bugs; the suites below found none.

What the checks validated

Applied to a scratch worktree of 645d4fe14a20 and run through gradle with OpenJDK 25.0.3, seed DEADBEEF (2026-10-31; an independent fact check re-ran the stock and fixed gradle runs, assertions on and off):

  • stock: the minimal test fails with assertions on and off;
  • with the fix: the minimal test passes with assertions on and off;
  • the command-line probe prints OK [1 fields, 1 vectors] for the one-vector FLOAT16 index (unchanged OK [1 fields, 1 vectors] for FLOAT32);
  • the closest suites pass: TestCheckIndex (9 tests), every core vector-format suite (lucene99.*, lucene104.*, TestPerFieldKnnVectorsFormat) and the FLOAT16 query and HNSW graph tests (21 classes with the other tests the package globs picked up, such as TestForUtil: 509 tests, 0 failures); TestSimpleTextKnnVectorsFormat and the sandbox TestDedupHnswVectorsFormat, 57 tests each, 0 failures. These suites draw the encoding at random per test, FLOAT16 included, and check the indexes they make with newDirectory() on close. Core lint (checkGoogleJavaFormat, ecjLintMain, forbiddenApisMain) passes;
  • earlier: the finder's sweep of 396 indexes (11 stock vector formats and encodings x 4 similarities x dimensions 3, 64, 1024 x 3 index shapes) was clean with the fix while the stock jar failed at the first index, and the verifiers' and the cross-model reviewers' own tests passed with it.

Pick the getter by encoding (not run)

Fetch the values once with a switch expression on fieldInfo.getVectorEncoding() (BYTE -> getByteVectorValues, FLOAT16 -> getFloat16VectorValues, FLOAT32 -> getFloatVectorValues) and skip when the result is null.

Trade-offs

A switch expression over an enum must be exhaustive, so without a default arm javac forces whoever adds a fourth encoding to handle it, the omission that happened here (a classic switch statement would not). A larger diff with the same behaviour as the first option today.

Downstream note

Tools that run CheckIndex at level 2 or higher as an index health check on main get no vector checks for FLOAT16 fields until a fix ships.

How it was found

While building an fp16-vs-fp32 differential harness, the finder listed every place Lucene dispatches on the vector encoding and checked each for a FLOAT16 arm. CheckIndex had one, behind a guard that tests only the float32 and byte getters; a FLOAT16 twin of the upstream test testCheckIndexIncludesVectors then failed on the stock main jars.

The method in full134 words

Method: non-formal, code reading followed by an executed test (hunt unit on fp16 vectors, 2026-10-25, main ea57360912ed).

  • The finder enumerated every reader and codec dispatch site on VectorEncoding with grep and checked each for a FLOAT16 arm. CheckIndex.testVectors has one, but the guard in front of it tests only getFloatVectorValues and getByteVectorValues.
  • It wrote the FLOAT16 twin of the upstream BaseKnnVectorsFormatTestCase#testCheckIndexIncludesVectors (2 documents, 2 fields, 3 vectors) next to a FLOAT32 control. On the stock jar the FLOAT16 test failed with OK [0 fields, 0 vectors] expected:<3> but was:<0>, with assertions on and off; the control passed.
  • The one-term fix made it pass, and a 396-index sweep over the stock vector formats found no false positive with the fix.
  • A later minimization reduced it to the one-document test shown here (2026-10-29).

Prior reports, history and checks

Prior reports

No prior report found (2026-10-25 and 2026-10-26): GitHub issues, PRs and commits, the JIRA archive, four mailing lists, downstream projects, the web and the files of all 412 open PRs. Related: #16383 introduced the code; the open fp16 tracker #16725 does not list CheckIndex.

Searches2 searches

Related items, none of them this bug:

  • #16383 introduced the code; #15549, the earlier unmerged fp16 PR, had the same omission.
  • #16725, the open fp16 completion tracker, has no CheckIndex item: a natural place to link a report.
  • #12064 widened the same guard for BYTE vectors; #11916 (the kNN self-search, float only) and #13984 (the encoding-agnostic HNSW graph check, which still runs for FLOAT16 on HNSW formats) are older CheckIndex vector work.
  • Open draft #16847 edits lines inside checkFloat16VectorValues but not the guard, so the method stays unreachable.

Searches are best effort: no hit does not prove that nobody has noticed it.

What was searched, when, and what turned up
WhereWhenHowFound
GitHub issues, PRs and commits (apache/lucene incl. the migrated JIRA archive), downstream projects, mailing lists (dev@, java-user@, issues@, commits@), JIRA, web2026-10-25, 2026-10-26the scripted prior-report search, 59 queries (24 GitHub issue/PR, 3 commit, 2 downstream, 20 mailing-list, 4 JIRA-archive, 6 web; 604 distinct hits, the 33 medium-overlap hits read)no report of this bug
GitHub (apache/lucene; 4 of the 30 on downstream projects), manual2026-10-2630 issue and commit searches (for example CheckIndex float16, "checkFloat16VectorValues", "totalKnnVectorFields", "KnnFloat16VectorField"); the changed-file lists of all 412 open PRs; full reads of the #16383 and #15549 threads and of tracker #16725no report; no open PR changes the guard

History

Introduced on main by #16383 (bfe5c847d3c, 2026-07-27), which added FLOAT16 vectors, the FLOAT16 arm and its checker, but not the guard term. Not a regression: the checks were unreachable from the start. No release has FLOAT16 vectors; upstream main and branch_11x still have the two-term guard.

Versions tested6 rows

Introduced by #16383 "Add fp16 (float16) vector encoding support - no quantization support" (bfe5c847d3c, merged 2026-07-27, listed under 11.0.0 in CHANGES.txt). The guard is older: c02f07f2d5d (LUCENE-9322, 2020) wrote if (values == null) continue;, cc29102a24a (#12064, 2023) widened it for BYTE, and 92e67ec626b (#12105, 2023) renamed the getter. git tag --contains bfe5c847d3c lists no release tag.

Versions and branches executed, with results
Version or commitVerdictResult
main 645d4fe14a20 affectedminimal test fails, assertions on and off; passes with the fix; the command line prints OK [0 fields, 0 vectors]
bfe5c847d3c (#16383), 091a987a14d, 55fd2e75fb2, 7cecf795236, ea57360912ed affectedthe finder's test fails the same way (assertions on and off at bfe5c847d3c and ea57360912ed); the one-term fix passes at bfe5c847d3c
parent 606a26dafd5, 10.5.2, 9.12.3, 10.6.0 (RC1 and released jars, 4ae946e5a9b9) not affectedno FLOAT16 API: the test does not compile
branch_10x, the 10.6.0 tag readno FLOAT16 in VectorEncoding
branch_11x 603af870375 (2026-10-29) readcontains bfe5c847d3c; the same two-term guard (CheckIndex.java:2864-2866); #16383 listed under 11.0.0
upstream main 2f424fd428d (2026-10-30, now 12.0.0-SNAPSHOT) readthe two-term guard is unchanged; no commit since 645d4fe14a20 touches CheckIndex.java

Independent checks

Confirmed by an executed cross-model review (OpenAI GPT-5.6), an independent reproducer, a second cross-model review with a works-as-designed lens, a devil's-advocate verifier and an adjudication; the version-history runs, a minimization and a re-verification on 645d4fe14a20 re-ran it. No reviewer dissented. This doc's gradle run reproduced it and validated the fix; an independent and a cross-model fact check re-checked the doc.

Checks and verdicts368 words

Every check ran its own test on the stock main jars; the adjudication re-ran the independent reproduction's class with new seeds.

Each independent check and its verdict
CheckVerdictWhat it found
Finder (grep inventory of every VectorEncoding dispatch site, then JUnit) candidateFLOAT16 twin of testCheckIndexIncludesVectors fails 1 of 2 on main with assertions on and off; the FLOAT32 control passes; the fix passes; 396-index sweep clean with the fix.
refute review (OpenAI GPT-5.6, executed) real (high)Own one-document test fails (on, off, 3 iterations), the FLOAT32 twin passes; the fix passes it and TestCheckIndex (9/0). Added that level 2 or higher is needed.
Independent reproducer realOwn 6-test class: the 3 FLOAT16 tests fail, the 3 FLOAT32/BYTE controls pass (on, off; 30 of 60 over 10 seeds); a mixed segment reports 2 fields / 12 vectors; the fix passes 6/6.
Intended lens (OpenAI GPT-5.6, executed) real (high)Intended argument weak; own direct CheckIndex test fails 1 of 2 (on, off) and passes with the fix.
Intended-behaviour devil's advocate real (low severity)Own mixed-encoding test fails (on, off); the experimental/unreleased/no-data-impact case is about impact, not intent.
Adjudication confirmed (high)Re-ran the independent reproduction's class with new seeds: 3 of 6 fail (on, off), 6/6 pass with the fix; lowered the class to minor.
Minimization and re-verification on 645d4fe14a20 still reproducesThe one-test class fails (on, off; two seeds) and passes with the fix; the finder's, the independent reproduction's and the intended-behaviour check's tests still fail; the 10.6.0 RC1 jars do not compile the finder's test.
This doc (gradle, scratch worktree) real; fix validatedMinimal test fails on stock and passes with the fix (on, off); command-line probe; closest suites pass with the fix.
Independent fact check of this doc real; two history wordings correctedRe-ran the minimal test (standalone runner and the gradle command, on and off) on stock and with the fix, the command-line probe, and the released 10.6.0 jars (the test does not compile).
Cross-model fact check (OpenAI GPT-5.6, read-only) real; intended argument weakReviewed the brief against the code and the run logs; found the branch_11x scope and checker-description wordings, corrected in this doc.

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 notes997 words

Finding record

LFV-089, snapshot of 2026-11-06

Registry title
CheckIndex silently skips FLOAT16 vector fields: it reports 'OK [0 fields, 0 vectors]' and never runs checkFloat16VectorValues
Area
index / CheckIndex vectors
Priority
Low, rank 750: CheckIndex reports 'OK [0 fields]' and never runs the FLOAT16 checks, including in the test framework's check-on-close, which hides bugs in a new feature, but data is unaffected and it is unreleased with a one-term fix.
Impact class
minor
Impact note
Anyone who runs CheckIndex at level 2 or higher (the CheckIndex API with setLevel, the CLI with -level 2/3, TestUtil.checkIndex and the test framework's check-on-close) over an index with a KnnFloat16VectorField on unreleased main (11.0.0), default codec, any size; indexing and search are unaffected and file checksums and the HNSW graph check still run, so what is lost is the logical FLOAT16 vector checks and correct field/vector counts.
Summary
You index vectors with KnnFloat16VectorField (default codec) and run CheckIndex at level 2 or higher. You expect it to count and check the FLOAT16 fields the way it does FLOAT32 and BYTE fields; the same 2-field, 3-vector index built with KnnFloatVectorField reports 2 fields / 3 vectors. Instead it prints 'test: vectors.............OK [0 fields, 0 vectors]', because the skip guard in CheckIndex.testVectors tests only the FLOAT32 and BYTE getters, so every FLOAT16 field is skipped and the dedicated checkFloat16VectorValues (iteration, dimension, size and kNN self-search checks) never runs.
Introduced
bfe5c847d3c (#16383 'Add fp16 (float16) vector encoding') [read: git blame, and git log -S 'checkFloat16VectorValues' / -S 'case FLOAT16:' on CheckIndex.java return only this commit; exec: the repro fails at bfe5c847d3c with -ea and -da and passes there with the one-term guard fix, and its parent 606a26dafd5 has no FLOAT16 API]; the guard itself is older (c02f07f2d5d LUCENE-9322, widened for BYTE in cc29102a24a #12064, renamed in 92e67ec626b #12105); not a regression: the FLOAT16 arm and checker were unreachable from the commit that added them (2026-07-27)
First affected version
unreleased (main only, 11.0.0-SNAPSHOT; no release tag contains bfe5c847d3c)
Affected versions
main from bfe5c847d3c (2026-07-27) through 645d4fe14a2 [exec: bfe5c847d3c, 091a987a14d, 55fd2e75fb2, 7cecf795236, ea57360912ed and 645d4fe14a2 fail; -ea and -da at bfe5c847d3c, ea57360912ed and 645d4fe14a2]; 10.5.2, 9.12.3 and the parent 606a26dafd5 not affected (no FLOAT16 API, the repro does not compile) [exec]; branch_10x 657014b3c579 (after #16905) and branch_10_6 4ae946e5a9b (10.6.0 RC1) have no FLOAT16 encoding [read; the repro does not compile on the 4ae946e5a9b jars, exec]; not fixed upstream as of main 645d4fe14a2 [exec 2026-10-29]
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: code reading
Upstream and prior reports
none found (2026-10-25 and 2026-10-26: a scripted prior-report search with 24 GitHub issue/PR, 3 commit, 2 downstream, 20 mailing-list (dev@, java-user@, issues@, commits@), 4 JIRA-archive and 6 web queries; 30 manual read-only gh issue/commit searches; changed-file lists of all 412 open PRs; full reads of the #16383 and #15549 threads and of the open fp16 tracker #16725); related prior items: #16383 (introduced the code), #15549 (earlier unmerged fp16 PR with the same omission), #16725 (open fp16 completion tracker, does not list CheckIndex), #12064 (widened the same guard for BYTE), #13984 and #11916 (older float-only CheckIndex vector checks); open draft #16847 edits lines inside checkFloat16VectorValues but not the guard
Lucene commit tested
main 645d4fe14a20 (2026-10-28): the minimal test fails with assertions on and off, through gradle in a scratch worktree and through a standalone runner (two seeds); with the one-term fix it passes, and so do the closest suites (TestCheckIndex, every core vector-format suite and the FLOAT16 query and graph tests: 21 classes with the other tests the package globs picked up, 509 tests, 0 failures; the SimpleText and dedup vector formats: 57 + 57 tests, 0 failures) and core lint. A command-line probe on the stock jar prints OK [0 fields, 0 vectors] for a one-vector FLOAT16 index and OK [1 fields, 1 vectors] with the fix. History: bfe5c847d3c (#16383) and every later commit run fail; 10.5.2, 9.12.3 and 10.6.0 (the RC1 and the released jars) cannot compile the test (no FLOAT16 API). Upstream main 2f424fd428d and branch_11x 603af870375 still have the two-term guard (read, 2026-10-31).

Lucene and upstream sources

Finding record
  • lucene/core/src/java/org/apache/lucene/index/CheckIndex.java:417-421 (VectorValuesStatus javadoc: 'Total number of vector values tested', 'Total number of fields with vectors'), :1114 (testVectors runs only at level >= MIN_LEVEL_FOR_INTEGRITY_CHECKS), :2864-2867 (skip guard, FLOAT32 and BYTE getters only), :2878-2884 (FLOAT16 arm), :3164-3213 (checkFloat16VectorValues), :4673 and :4748-4754 (CLI default level 1; MIN_LEVEL_FOR_INTEGRITY_CHECKS = 2)
  • lucene/core/src/java/org/apache/lucene/index/CodecReader.java:233-271 (final getters return null unless the field's encoding matches)
  • lucene/test-framework/src/java/org/apache/lucene/tests/index/BaseKnnVectorsFormatTestCase.java:1947-1976 (testCheckIndexIncludesVectors, FLOAT32 only); lucene/core/src/test/org/apache/lucene/index/TestCheckIndex.java:203-207 (vector counts asserted for FLOAT32 only)
  • lucene/test-framework/src/java/org/apache/lucene/tests/util/TestUtil.java:302-303 (checkIndex(Directory) uses level 3); lucene/test-framework/src/java/org/apache/lucene/tests/store/BaseDirectoryWrapper.java:32-33, 44-45 (check-on-close)
  • lucene/MIGRATE.md:629-645 ('IEEE FLOAT16 vector APIs (GITHUB#16383)': VectorEncoding switches need a FLOAT16 arm)
  • lucene/core/src/java/org/apache/lucene/index/CodecReader.java:234-280 (vector getters on main 645d4fe14a2)
The finding in 30 seconds
  • lucene/core/src/java/org/apache/lucene/index/CheckIndex.java:417-421, (at 645d4fe14a20) :1114, :1140-1144, :2843, :2864-2898, :2902-2909, :2926-2945, :3164-3213, :4748, :4754, :4837-4838
  • lucene/core/src/java/org/apache/lucene/index/CodecReader.java:234-280 (at 645d4fe14a20)
Who is affected
  • lucene/test-framework/src/java/org/apache/lucene/tests/util/TestUtil.java:302-303; (at 645d4fe14a20) lucene/test-framework/src/java/org/apache/lucene/tests/store/BaseDirectoryWrapper.java:32-33, :44-45, :64-70; lucene/test-framework/src/java/org/apache/lucene/tests/index/BaseKnnVectorsFormatTestCase.java:124-130, :147-157, :1351-1353; lucene/core/src/java/org/apache/lucene/document/KnnFloat16VectorField.java:25-37
Why it looks unintended
  • lucene/core/src/java/org/apache/lucene/index/CheckIndex.java:103-112, (at 645d4fe14a20) :417-421, :1140, :4836-4839; lucene/core/src/java/org/apache/lucene/index/LeafReader.java:237-243; lucene/MIGRATE.md:629-645
  • lucene/test-framework/src/java/org/apache/lucene/tests/index/BaseKnnVectorsFormatTestCase.java:1947-1976; (at 645d4fe14a20) lucene/core/src/test/org/apache/lucene/index/TestCheckIndex.java:104-105, :203-207
Suggested fix
  • lucene/core/src/java/org/apache/lucene/index/CheckIndex.java:1140, (at 645d4fe14a20) :2864-2867, :3112-3264
History
  • a Lucene clone: git show releases/lucene/10.6.0:lucene/core/src/java/org/apache/lucene/index/VectorEncoding.java; git show origin/main (2f424fd428d):lucene/core/src/java/org/apache/lucene/index/CheckIndex.java:2864-2866; git log 645d4fe14a20..origin/main -- CheckIndex.java (empty) (read, 2026-10-31)
  • lucene/CHANGES.txt:174 (at 645d4fe14a20)
  • a Lucene clone: git merge-base --is-ancestor bfe5c847d3c origin/branch_11x (603af870375); git show origin/branch_11x:lucene/core/src/java/org/apache/lucene/index/CheckIndex.java:2864-2866; CHANGES.txt headers on origin/main (12.0.0) and origin/branch_11x (11.0.0) (read, 2026-10-31)

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, the minimal repro and the command-line probe of this brief.

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.