FSTCompletionLookup keeps two views of one FST: a by-weight view for lookup(key, true, num) and an alphabetical view for lookup(key, false, num). build() and load() pass exactMatchFirst only to the alphabetical view, where an exact match sorts first anyway. The by-weight view always keeps its "exact first" default.
lookup("star", true, 3) returns [star/0, star wars/6, start/3] instead of [star wars/6, start/3, star/0]. Select the still to open it at full size. Generated with AI, checked against the code.- You create
new FSTCompletionLookup(dir, "fst", 10, false)to rank suggestions purely by popularity andbuild()it from star (weight 1), star wars (weight 100) and start (weight 50). With 10 buckets they land in buckets 0, 6 and 3. - The user types "star" and you ask for the most popular completions:
lookup("star", true, 3). The javadoc ofexactMatchFirstsays that withfalseexact matches "appear in the order of discretized weight", so you expect[star wars/6, start/3, star/0]. - Instead you get
[star/0, star wars/6, start/3]: the least popular entry comes first, only because it equals the typed text. Withlookup("star", true, 1)the only suggestion isstar/0. No error, andexactMatchFirst=truereturns the same lists. - Why: the lookup keeps two views of one FST.
build()andload()pass the flag only to the alphabetical view; the by-weight view is made withnew FSTCompletion(fst), which means "higher weights first and exact first". - It is the same after
store()andload()into an instance built withfalse. The other constructor, which takes a pre-builtFSTCompletion, passes the flag to both views and returns[star wars/6, start/3, star/0].
Applications that build the experimental FSTCompletionLookup with the non-default exactMatchFirst=false and ask for by-weight suggestions, lookup(key, true, num). Any typed prefix that is itself a less popular entry is ranked first, every time; with a small num it pushes out a correct entry. The default true and alphabetical lookups are unaffected.
Mechanism, terms and numbers598 words
FSTCompletionLookup has a constructor argument exactMatchFirst; its javadoc says that with false exact matches "appear in the order of discretized weight and alphabetical within the bucket". The lookup keeps two FSTCompletion views over one FST: a by-weight view for lookup(key, true, num) and an alphabetical view for lookup(key, false, num). build() and load() pass the flag only to the alphabetical view; the by-weight view is created with the one-argument FSTCompletion(FST), "higher weights first and exact first". So by-weight lookups always put the exact match first, whatever its weight, with no error, and on distinct keys the argument has no visible effect at all. With star (1), star wars (100), start (50) and 10 buckets, lookup("star", true, 3) returns [star/0, star wars/6, start/3] instead of [star wars/6, start/3, star/0]. It came with the class in SOLR-2888 (3.6.0), which replaced FSTLookup, a class that honoured the flag; every release since is affected. Two statements fix it: create the by-weight view with new FSTCompletion(fst, true, exactMatchFirst) in build() and load(), as the pre-built-completion constructor already does.
- Buckets:
build()sorts the entries by weight and splits them intobucketsequal ranges,bucket = (int) (line * buckets / inputLineCount)(FSTCompletionLookup.java:222-227). A result shows the bucket, not the weight:star wars/6. With 3 entries and 10 buckets: star (weight 1) gets 0, start (50) gets 3, star wars (100) gets 6. - Two views:
higherWeightsCompletion(by weight, highest bucket first) andnormalCompletion(alphabetical), twoFSTCompletionwrappers over the same FST (FSTCompletionLookup.java:96-100).lookup(key, contexts, higherWeightsFirst, num)picks the first whenhigherWeightsFirstis true (:268-272);Lookup.lookup(key, onlyMorePopular, num)delegates to it (Lookup.java:193-195). - Exact first: an
FSTCompletionwithexactFirstset looks for the key itself, puts it at the head of the results and removes it from the rest (FSTCompletion.java:233-283).
- The buckets constructor
FSTCompletionLookup(Directory, String, int, boolean)storesexactMatchFirst(FSTCompletionLookup.java:130-136). build()ends withthis.higherWeightsCompletion = builder.build();(:242) andthis.normalCompletion = new FSTCompletion(higherWeightsCompletion.getFST(), false, exactMatchFirst);(:243-244).FSTCompletionBuilder.build()returnsnew FSTCompletion(automaton)(FSTCompletionBuilder.java:197), and that constructor is "Defaults to higher weights first and exact first",this(automaton, true, true)(FSTCompletion.java:124-131).load()does the same withnew FSTCompletion(new FST<>(readMetadata(...), input))(FSTCompletionLookup.java:305-306); again onlynormalCompletiongets the flag (:307-308).- In the alphabetical view the flag cannot show: when more than one bucket is in use the results are sorted before they are cut (
FSTCompletion.java:202-206), and an exact match is a prefix of all its completions, so it sorts first anyway; with one bucket the depth-first walk already emits it first. On stock main, instances built withtrueand withfalsegave 0 different results over about 300,000 lookups in each mode on random dictionaries of distinct keys; with the fix about 37,000 by-weight lookups differ. - The fourth constructor,
FSTCompletionLookup(Directory, String, FSTCompletion, boolean), creates both views with the flag (FSTCompletionLookup.java:152-153) and returns the documented order.
star (1), star wars (100), start (50), 10 buckets, exactMatchFirst=false: lookup("star", true, 3) = [star/0, star wars/6, start/3], expected [star wars/6, start/3, star/0]; lookup("star", true, 1) = [star/0], expected [star wars/6]. The same after store() + load(). The alphabetical lookup("star", false, 3) = [star/0, star wars/6, start/3], which is correct UTF-8 order. Another dictionary, {foo: 1, bar: 5, fool: 200, food: 500, foobar: 1000}: lookup("foo", true, 1) = [foo/0] instead of [foobar/8]. The smallest case: {a: 0, aa: 1}, lookup("a", true, 1) = [a/0] instead of [aa/5].
Exposure and likelihood287 words
- Who. Code that calls the buckets constructor
(Directory, String, int, boolean)withexactMatchFirst=false, for example to rank suggestions purely by popularity, and thenlookup(key, true, num), afterbuild()or afterload(). - When. For distinct keys and
num > 0: whenever the typed text is itself an entry and another of its completions is in a higher bucket: ordinary autocomplete input, such as "star" while "star wars" is far more popular. Deterministic; no scale, concurrency or unusual data needed. - What goes wrong. Silent ranking error. If
numcovers all completions only the order is wrong; ifnumis small the exact match takes a slot and the lowest-ranked correct entry drops out, so the top-1 suggestion can be the least popular entry. - Not affected. The default (
FSTCompletionLookup(dir, prefix)usestrue,FSTCompletionLookup.java:116-118), alphabetical lookups,get(key)(it readsnormalCompletion,:287-290), and the pre-built-completion constructor. The class is@lucene.experimental(:71). - Size of the check. A bounded-exhaustive differential on the stock jars over every set of 1-3 distinct keys from the 39 strings of length 1-3 over {a, b, c}, with weights {0, 1, 2, 2^31-1} (596,908 dictionaries, about 4.76 billion lookups) found 265,860 order mismatches each for 3 and 10 buckets with
false, and none withtrueor with 1 bucket. That full scope was not re-run with the fix; a smaller exhaustive run with the fix shadowed (keys of length 1-2, sets of 1-4 keys: 141,904 dictionaries, about 1.13 billion lookups) found noFSTCompletionLookupmismatch. - Distinct keys only. Every check used dictionaries of distinct keys; a dictionary holding the same key twice, with weights in different buckets, was not covered, so the doc makes no claim about it.
The test shows that FSTCompletionLookup(dir, "fst", 10, false) still promotes the exact match in by-weight lookups after build(): for star (weight 1) and star wars (weight 100), lookup("star", true, 2) puts star first.
/*
* 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.search.suggest.fst;
import java.io.StringReader;
import org.apache.lucene.search.suggest.FileDictionary;
import org.apache.lucene.store.ByteBuffersDirectory;
import org.apache.lucene.store.Directory;
import org.apache.lucene.tests.util.LuceneTestCase;
public class TestFSTCompletionLookupExactMatchFirstFalse extends LuceneTestCase {
public void testExactMatchNotPromotedAfterBuild() throws Exception {
try (Directory dir = new ByteBuffersDirectory()) {
FSTCompletionLookup lookup = new FSTCompletionLookup(dir, "fst", 10, false);
lookup.build(new FileDictionary(new StringReader("star\t1\nstar wars\t100\n")));
// exactMatchFirst=false: by discretized weight, no promotion; actual: [star/0, star wars/5]
assertEquals("[star wars/5, star/0]", lookup.lookup("star", true, 2).toString());
}
}
}
# in a copy of this finding's repro folder (repro/LFV-706/, 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/analysis/common/build/libs/lucene-analysis-common-$V.jar
CP=$CP:$LUCENE/lucene/suggest/build/libs/lucene-suggest-$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.search.suggest.fst.TestFSTCompletionLookupExactMatchFirstFalse
- Expected vs actual
- Expected
[star wars/5, star/0](order of discretized weight, as theexactMatchFirstjavadoc says); actual[star/0, star wars/5], with-eaand-da; passes with the two-site fix (by-weight view built withexactMatchFirst).
More files, environment, full output36 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
=> org.junit.ComparisonFailure: expected:<[star[ wars/5, star/0]]> but was:<[star[/0, star wars/5]]>
org.junit.Assert.assertEquals(Assert.java:117)
org.junit.Assert.assertEquals(Assert.java:146)
org.apache.lucene.search.suggest.fst.TestFSTCompletionLookupExactMatchFirstFalse.testExactMatchNotPromotedAfterBuild(TestFSTCompletionLookupExactMatchFirstFalse.java:32)- The javadoc: with
false, exact matches "appear in the order of discretized weight and alphabetical within the bucket". - The pre-built-completion constructor passes the flag to both views and returns that order;
TestFSTCompletionasserts thatFSTCompletion(fst, true, false)does not promote. - A regression: the replaced
FSTLookuphonouredfalse(3.5.0:[star wars, start, star]); 3.6.0FSTCompletionLookupdoes not.
The full argument224 words
- The contract.
FSTCompletionLookup.java:126-128: "@param exactMatchFirst If <code>true</code> exact matches are promoted to the top of the suggestions list. Otherwise they appear in the order of discretized weight and alphabetical within the bucket." "Order of discretized weight" only describes the by-weight path, the one that ignores the flag. The same text is on the pre-built-completion constructor (:142-144), which honours it (:152-153). - The lower-level switch is documented and tested.
FSTCompletion'sexactFirst: "Find and push an exact match to the first position of the result list if found" (FSTCompletion.java:109-110).TestFSTCompletion.testRequestedCountasserts thatnew FSTCompletion(fst, true, false)does not promote the low-weight "one" (TestFSTCompletion.java:136-139). The siblingWFSTCompletionLookuphonoursexactFirst=false(TestWFSTCompletion.testNonExactFirst). No upstream test buildsFSTCompletionLookupwithfalse. - History. SOLR-2378 added exact-match promotion "(weighted mode)" to the old
FSTLookup, and SOLR-2762 fixed it there, so the flag was a feature of the weighted path. SOLR-2888 replacedFSTLookupwithFSTCompletionLookupand switched Solr'sFSTLookupFactoryfromnew FSTLookup(buckets, exactMatchFirst)tonew FSTCompletionLookup(buckets, exactMatchFirst)as a drop-in; its CHANGES entry announces no change in behaviour. Executed on the same input: 3.5.0FSTLookup(10, false)returns[star wars, start, star]; 3.6.0FSTCompletionLookup(10, false)returns[star/0, star wars/6, start/3]. Three days after the class was committed, its author added the "Otherwise ..." javadoc (3408defc8db, "JavaDoc clarifications").
The devil's-advocate check299 words
The strongest works-as-designed case (built by the intended-behaviour verifier, reviews by a model from another family, OpenAI GPT-5.6, and the adjudication): FSTCompletionBuilder's class javadoc says "If there is an exact match in the automaton, it is returned first on the results list (even with by-weight sorting)" (FSTCompletionBuilder.java:77-78), its no-argument constructor makes an FSTCompletion "with default options: 10 buckets, exact match promoted to first position" (:127-131), and FSTCompletion(FST) defaults to exact first; the class is @lucene.experimental; nobody complained in about 15 years; and for load(), the no-argument constructor "should only be used to read a previously saved suggester" (FSTCompletionLookup.java:105), defaults to true, and store() does not record the flag. Nearby javadoc is also loose: FSTCompletion says higherWeightsFirst=false "has no effect" (FSTCompletion.java:106-108), yet lookup then sorts alphabetically when more than one bucket is in use (:202-206), and Lookup documents the by-weight switch as onlyMorePopular, "return only more popular results" (Lookup.java:189).
Why it fails: the builder and one-argument docs describe the default view, which the explicit argument exists to override, and its own javadoc says so; experimental status allows API changes, not a documented argument that does nothing; few users set false, and Solr's modern suggest component uses the unaffected alphabetical path; load() itself applies this.exactMatchFirst to normalCompletion, nothing forbids loading into a configured instance, Solr does exactly that, and the build() path alone proves the bug. The exactMatchFirst text, unlike the loose sentences, is specific to this class and argument, was added by the class's author three days after the class, and is implemented by the pre-built-completion constructor. Every check rated the intended argument weak; the cross-model fact check of this doc rated it weak for build() and moderate for load().
Recommended Build the by-weight view with the flag in build() and load()
Create the by-weight view with the flag, new FSTCompletion(fst, true, exactMatchFirst), in build() and load(), as the pre-built-completion constructor already does: two statements. Validated: the repro and a new TestFSTCompletion test fail on stock and pass with it, assertions on and off, and the whole suggest module passes (240 tests).
// The two FSTCompletions share the same automaton.
- this.higherWeightsCompletion = builder.build();
+ this.higherWeightsCompletion =
+ new FSTCompletion(builder.build().getFST(), true, exactMatchFirst);
...
this.higherWeightsCompletion =
- new FSTCompletion(new FST<>(readMetadata(input, NoOutputs.getSingleton()), input));
+ new FSTCompletion(
+ new FST<>(readMetadata(input, NoOutputs.getSingleton()), input), true, exactMatchFirst);All options and trade-offs4 options ยท 538 words
One fix was applied in a scratch worktree at a9779ef3c193 and run through gradle (seed DEADBEEF), together with a regression test in TestFSTCompletion; the alternatives were not run.
Build the by-weight view with the flag in build() and load() (validated by the checks) (recommended)
Two statements in FSTCompletionLookup.java (+4/-2 lines after wrapping): create higherWeightsCompletion as new FSTCompletion(fst, true, exactMatchFirst), as the pre-built-completion constructor already does (:153). The FST and the stored format do not change.
// The two FSTCompletions share the same automaton.
- this.higherWeightsCompletion = builder.build();
+ this.higherWeightsCompletion =
+ new FSTCompletion(builder.build().getFST(), true, exactMatchFirst);
...
this.higherWeightsCompletion =
- new FSTCompletion(new FST<>(readMetadata(input, NoOutputs.getSingleton()), input));
+ new FSTCompletion(
+ new FST<>(readMetadata(input, NoOutputs.getSingleton()), input), true, exactMatchFirst);Trade-offs
- The default
exactMatchFirst=truebehaves exactly as before. - Behaviour change for users who set
false: by-weight lookups now follow the documented order; worth a CHANGES line. Anyone who relied on the old order can passtrue, which gives it for distinct keys (duplicate keys were not checked). builder.build()still makes one throwawayFSTCompletion(it caches at most one root arc per bucket), negligible next to building the FST.
What the checks validated
- Stock main
a9779ef3c193(gradle, seed DEADBEEF): the repro fails 2 of 3 and the newTestFSTCompletion#testExactMatchFirstFalsefails, with assertions on and off; the control and the 13 upstreamTestFSTCompletiontests that run pass (testRandomis@Nightly). - With the fix: the repro and
TestFSTCompletionpass (18 tests, 0 failures, 1@Nightlyskipped), with assertions on and with assertions off. - With the fix, the whole
:lucene:suggest:test: 32 classes, 240 tests, 0 failures, 7 skipped (31 upstream classes, 236 upstream tests, plus the repro and the new test); this run had no-Ptests.asserts, so it ran without assertions, the default for seed DEADBEEF. ecjLintandcheckGoogleJavaFormatpass forlucene/suggestwith both tests in place; both diffs apply cleanly to a clean tree.- Through the classpath runner the fix shadowed onto the stock jars passes the repro 3 of 3; in the hunt, the finder's, the independent reproducer's, the cross-model reviewers' and the adjudication's tests passed with this change or with an independently written equivalent of it.
- Not run: other modules (nothing outside
lucene/suggestuses the class); no benchmark (the change only passes a flag).
"If there is an exact match in the automaton, it is returned first on the results list (even with by-weight sorting)" (FSTCompletionBuilder.java:77-78) describes the default FSTCompletion that build() returns; adding "by default" removes the only text that reads as the opposite contract.
Trade-offs
Doc-only.
fix-with-test.diff adds TestFSTCompletion#testExactMatchFirstFalse: build and store/load with false, assert [star wars/6, start/3, star/0]. It fails on stock main and passes with the fix.
Trade-offs
None; no upstream test builds FSTCompletionLookup with false today.
Change the javadoc to say that false only affects alphabetical lookups.
Trade-offs
Makes the argument a no-op on distinct keys and keeps the regression from FSTLookup. Note also open #11576, which proposes removing the alphabetical mode: without the fix, the flag would then reach no view at all.
By code reading only, Solr's FSTLookupFactory passes its exactMatchFirst setting into this constructor, but Solr's SuggestComponent asks for alphabetical lookups, which the bug does not reach.
A bounded-exhaustive differential check ran the suggester Lookup implementations (all except FreeTextSuggester) on the real jars against a reference written from each one's javadoc. For FSTCompletionLookup with exactMatchFirst=false it found 265,860 order mismatches each for 3 and 10 buckets and none with true; a smaller exhaustive run with the fix found none. Reading build() and load() gave the cause.
The method in full190 words
Method: bounded-exhaustive differential translation validation on the stock jars (semi-formal tier), in a hunt unit that compared the suggest module's Lookup implementations (all except FreeTextSuggester).
- The harness builds every implementation from the same dictionary and compares each lookup with an independent reference written from that class's javadoc; for
FSTCompletionLookup: bucket descending, then UTF-8 order, the exact match first only whenexactMatchFirstis true. - Scope: every set of 1-3 distinct keys over the 39 strings of length 1-3 over {a, b, c}, with weights {0, 1, 2, 2^31-1}: 596,908 dictionaries, 7,759,804 builds and 4,763,325,840 lookups, assertions off. The class "exact=false, order" appeared 265,860 times for 3 buckets and as often for 10, never for
trueor 1 bucket. With the fix shadowed onto the jar (141,904 dictionaries including 4-key sets, about 1.13 billion lookups) it appeared 0 times. - The class was reduced to a public-API JUnit (found 2026-10-27 on main
ea57360912ed, whoselucene/suggestequalsa9779ef3c193), then checked by an independent reproducer, an intended-behaviour devil's advocate, two executed reviews by a model from another family (OpenAI GPT-5.6), an adjudication, a history and prior-report follow-up and a credibility audit.
No prior report found (2026-10-27) in GitHub issues, PRs and commits, the LUCENE and SOLR JIRA archives, the Lucene and Solr lists, downstream repositories, Stack Overflow and the web, nor in the changed files of all 409 open PRs. The file is unchanged on upstream main and branch_10x.
Searches4 searches
Verdict new: no report by anyone. Keyword search cannot prove absence; GitHub search does not index every review comment, and private reports are invisible.
| Where | When | How | Found |
|---|---|---|---|
| apache/lucene issues, PRs and commits; downstream repositories; Lucene and Solr lists; JIRA archive; the web | 2026-10-27 | two runs of the hunt's prior-report tool, 76 queries | no report; related only: #11328, #4788 |
| JIRA (LUCENE, SOLR), lists, downstream GitHub, Stack Overflow | 2026-10-27 | 9 JQL queries and the comments of 14 issues; 7 list searches, 72 mails read; 11 searches in apache/solr, apache/lucene-solr, Lucene.NET, Elasticsearch, OpenSearch; 3 Stack Overflow searches | background only: SOLR-2378, SOLR-2762, SOLR-2888 (the introducing change), PR #844; solr-user@ threads about the flag set to true |
| all 409 open apache/lucene PRs | 2026-10-27 | changed-file lists | none touches FSTCompletionLookup, FSTCompletion or FSTCompletionBuilder; open issue #11576 matters for the fix design |
| registry re-check | 2026-10-27 | GitHub search for exactMatchFirst and FSTCompletionLookup, issues since 2026-10-20 | nothing new |
Born with the class in SOLR-2888 (92de7e88486, 2011-12-01; branch_3x d3e380f824a, first release 3.6.0), which replaced FSTLookup, a class that honoured the flag. Executed: 3.6.0 to 10.5.2 (23 releases) and main fail; 3.5.0 has no such class. Never fixed.
Versions tested7 rows
Introduced by SOLR-2888, "FSTSuggester refactoring: utf8 storage, external sorts (OOM prevention), code cleanups" (svn era, no PR): trunk 92de7e884868 (svn r1209265, 2011-12-01) and the branch_3x backport d3e380f824a5 (svn r1232289, 2012-01-17), which shipped first, in 3.6.0. The commit created FSTCompletionLookup; both defect sites are in its first version; its 21 JIRA comments never mention exact-match handling. The exactMatchFirst=false javadoc followed three days later in 3408defc8db ("JavaDoc clarifications"), so no release has the code without the contract.
It is a regression of that refactoring: FSTLookup, deprecated in 3.6 and removed in 4.0, applied the flag on the by-weight path. All 126 release tags with the class build the by-weight view the same faulty way (the surrounding lines differ between versions), the 9 untagged 8.x releases 8.8.2 to 8.11.4 were executed instead (135 releases counting the 4.0.0-alpha and 4.0.0-beta prereleases, 133 final releases), and FSTCompletionLookup.java is byte-identical on upstream main a95c754ee003, branch_10x and branch_10_6 (fetched 2026-10-26).
| Version or commit | Verdict | Result |
|---|---|---|
main a9779ef3c193 | affected | the compact JUnit fails 2 of 3 through gradle, assertions on and off, and through the classpath runner with -ea and -da |
| 10.5.2, 10.0.0, 9.12.3, 9.1.0 | affected | the compact JUnit fails 2 of 3 (10.5.2 also with -da); 9.1.0 is before the 9.2 stream rewrite of FSTCompletion |
| 9.0.0, 8.8.2-8.11.4, 7.7.3, 6.6.6, 6.0.0, 5.5.5, 4.10.4, 4.0.0 | affected | main() repro: build and load return [star/0, star wars/6, start/3]; the control passes |
| 3.6.0, 3.6.1, 3.6.2 | first release | FSTCompletionLookup(10, false) returns [star/0, star wars/6, start/3] |
| 3.5.0, 3.6.0, 3.6.2 FSTLookup | predecessor class | FSTLookup(10, false) returns [star wars, start, star]: the flag is honoured |
| 3.5.0 | no class | compile error: FSTCompletionLookup does not exist |
fix (scratch worktree at a9779ef3c193) | not affected | the repro and the new TestFSTCompletion test pass, assertions on and off; the whole suggest module passes (240 tests, 0 failures) |
An independent reproducer and an intended-behaviour devil's advocate reproduced it with their own tests, controls passing; two executed reviews by a model from another family (OpenAI GPT-5.6) rated it real with a weak intended argument; an adjudication re-ran it with new seeds; this doc's gradle runs reproduce it on a9779ef3c193 and validate the fix.
Checks and verdicts296 words
Every executing check wrote or re-ran its own JUnit on stock main; every control passed, and every fix check passed.
| Check | Verdict | What it found |
|---|---|---|
| finder | candidate | Exhaustive differential, then a public-API JUnit: fails 2 of 3 on main (-ea, -da), 10.5.2 and 9.12.3; 40 of 60 over 20 iterations; passes with the fix. |
| cross-model review, refute lens (OpenAI GPT-5.6) | real, high | Its own JUnit fails 2 of 4 on main (-ea, -da), its 2 controls pass, the fix passes; noted the load-path wrinkle. |
| independent reproducer | real | Own JUnit from the claim only, another dictionary: fails 3 of 7 on main, 10.5.2 and 9.12.3; a random differential finds 1,669 mismatches, 0 with true; its own fix passes 7 of 7. |
| cross-model review, intended lens (OpenAI GPT-5.6) | real, high; intended argument weak | Its own JUnit fails 2 of 5 on a9779ef3c193 (-ea, -da) and 10.5.2, 3 controls pass, passes 5 of 5 with the fix. |
| intended-behaviour devil's advocate | real (high for build, moderate-to-high for load) | Built and rejected the works-as-designed case; showed true and false give identical results on stock; executed the 3.5.0 to 3.6.0 regression. |
| adjudication | confirmed, high | Re-ran the reproducer's and the finder's tests with new seeds (3 of 7 and 2 of 3 fail) and the fix check (7 of 7 pass). |
| Follow-up and credibility audit | new; not security-sensitive; keep | History main() runs on 22 releases (23 releases executed in all, with 9.1.0 by JUnit) and the predecessor class, prior-report searches, a security screen, and fresh-seed re-runs on a9779ef3c193. |
| this doc | reproduced; fix validated | Compact JUnit fails through gradle and the classpath runner on main and 4 releases; the fix passes it, the new regression test and the suggest module. |
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 504 KB
Finding record, Lucene and upstream sources, repro files and diagram notes1,363 words
- Registry title
- FSTCompletionLookup ignores exactMatchFirst=false for by-weight lookups after build() and load(), so an exact match is always ranked first
- Area
- suggest / FST completion suggester (FSTCompletionLookup build/load exactMatchFirst wiring)
- Priority
- Low, rank 384: Ranking-only, and it needs the non-default exactMatchFirst=false in the experimental FSTCompletionLookup.
- Impact class
- silent wrong results
- Impact note
- Applications that build the opt-in, @lucene.experimental FSTCompletionLookup with the non-default exactMatchFirst=false (for example to rank suggestions purely by popularity) and then call lookup(key, true, num) after build() or after load() get the exact match first whatever its weight bucket, with no error: when num covers all completions only the order is wrong, and when num is small the exact match pushes out the lowest-ranked correct entry (top-1 can be the least popular entry); alphabetical lookups and the default exactMatchFirst=true are unaffected, no scale is needed, and every release from 3.6.0 through 10.6.0 (10.6.0 by reading) and main is affected (by code reading, Solr's FSTLookupFactory exposes the setting, but its SuggestComponent calls the alphabetical lookup(token, false, count), which the bug does not reach).
- Summary
- You build an FSTCompletionLookup(dir, "fst", 10, false) from star (weight 1), star wars (100) and start (50) and ask lookup("star", true, 3) for the most popular completions first. You expect [star wars/6, start/3, star/0], because the constructor javadoc says that with exactMatchFirst=false exact matches 'appear in the order of discretized weight and alphabetical within the bucket'. Instead you get [star/0, star wars/6, start/3], with no error, after build() and equally after store() and load(): both paths create the internal by-weight FSTCompletion with its exact-first default, and the flag reaches only the alphabetical completion, where it changes nothing.
- Introduced
- 92de7e884868f91b42689aa344c51b1b8aa8fdbb (trunk, svn r1209265, 2011-12-01) and its branch_3x backport d3e380f824a59857c3cd72e7f789e6f5682e7853 (svn r1232289, 2012-01-17; the backport shipped first, in 3.6.0), SOLR-2888 'FSTSuggester refactoring: utf8 storage, external sorts (OOM prevention), code cleanups' (an svn-era JIRA change, no pull request; its 21 comments never mention exact-match handling). The commit created FSTCompletionLookup with build() keeping higherWeightsCompletion = builder.build() and load() using the one-argument new FSTCompletion(fst), both exact-first by default, while only normalCompletion received exactMatchFirst; the pre-built-completion constructor of the same commit passes the flag to both [read: git log --follow, git log -L on the build() line (only 92de7e88486), git blame -w -M -C; both defect sites are in the class's first version, so no commit-level bisection was needed] [exec: release boundary, the predecessor FSTLookup honours exactMatchFirst=false on 3.5.0, 3.6.0 and 3.6.2, FSTCompletionLookup ignores it on 3.6.0]. The exactMatchFirst=false javadoc followed three days later in 3408defc8db (2011-12-04, 'JavaDoc clarifications'), so no release has the code without the contract (2011-12-01 (trunk); 2012-01-17 (branch_3x backport))
- First affected version
- 3.6.0 [exec], the first release that contains FSTCompletionLookup at all (3.5.0: class absent, compile error [exec]); the class it replaced, FSTLookup (deprecated in 3.6, removed in 4.0), honours exactMatchFirst=false on the same input [exec on 3.5.0, 3.6.0 and 3.6.2], so this is a regression of the refactoring, and no release of FSTCompletionLookup ever honoured the flag on this path
- Affected versions
- every release that ships FSTCompletionLookup: 3.6.0-3.6.2 and 4.0.0-alpha through 10.5.2 (135 releases, including 8.8.2-8.11.4 from branch_8x), plus main a9779ef3c193; never fixed. Executed, all failing with the control passing: 3.6.0, 3.6.1, 3.6.2, 4.0.0, 4.10.4, 5.5.5, 6.0.0, 6.6.6, 7.7.3, 8.8.2, 8.9.0, 8.10.0, 8.10.1, 8.11.0-8.11.4, 9.0.0, 9.12.3, 10.0.0 and 10.5.2 (follow-up main() repros, 10.5.2 also -da), 9.1.0 (the intended-behaviour check, before the 9.2 stream rewrite of FSTCompletion), main with -ea and -da [exec]; the other releases by reading every release tag of the history clone [read]. FSTCompletionLookup.java is byte-identical on upstream origin/main a95c754ee003, branch_10x and branch_10_6 (fetched 2026-10-26) [read], so 10.6.0 (released 2026-10-29 from branch_10_6) is affected too. Executed only on aarch64 Linux with JDK 25.0.3; the defect is pure-Java wiring, so the platform does not matter
- 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
- semi-formal: exhaustive check
- Upstream and prior reports
- none found (2026-10-27, read-only; the prior-report record, verdict new): two scripted prior-report searches (76 queries over GitHub issues, PRs and commits, downstream repositories, the Lucene and Solr mailing lists, the JIRA archive and the web), 9 JQL queries over LUCENE and SOLR, 7 list searches with 72 mails read, 11 downstream GitHub searches (apache/solr, apache/lucene-solr, Lucene.NET, Elasticsearch, OpenSearch), 3 Stack Overflow searches, the changed-file lists of all 409 open apache/lucene PRs (none touches FSTCompletionLookup.java, FSTCompletion.java or FSTCompletionBuilder.java) and the file history; the finder, the independent reproduction and the intended-behaviour check searched with other phrasings and found nothing either; re-check (2026-10-27, gh read-only): repo:apache/lucene exactMatchFirst still gives only #11328 and #4788, no open PR mentions FSTCompletionLookup, and no issue naming it was created since 2026-10-20. Not fixed upstream: origin/main a95c754ee003 and branch_10x still create the by-weight completion with the defaults. Related, none a report of this bug: #11328 (LUCENE-10292) quotes the build() lines only in a thread-safety comment; #4788 (LUCENE-3714) mentions exactMatchFirst only as a benchmark setting; SOLR-2378 added exact-match promotion for the weighted mode to the old FSTLookup; SOLR-2762 and a review remark on PR #844 were exact-match promotion bugs with the flag true in other code (fixed in 3.5 and 9.2); SOLR-2888 is the introducing change (21 comments, none on the flag); open #11576 (LUCENE-10540) proposes removing the alphabetical completion, the only one that receives the flag today; solr-user threads about exactMatchFirst concern the flag true with other suggesters or SolrCloud shard merging.
- Lucene commit tested
- main
a9779ef3c193(commit of 2026-10-26): the compact JUnit and a regression test added toTestFSTCompletion, through gradle in a clean worktree, stock and with the fix, assertions on and off, plus the wholelucene/suggestmodule with the fix (2026-10-28); through the hunt's classpath runner, the compact JUnit on main (-ea,-da, fix shadowed, 10 iterations), 10.5.2 (-ea,-da), 10.0.0, 9.12.3 and 9.1.0. The hunt's checks ran its own repros on mainea57360912ed(identicallucene/suggest), ona9779ef3c193, andmain()repros on 22 releases from 3.6.0 to 10.5.2 (23 releases executed in all, with 9.1.0 by JUnit), plus the predecessorFSTLookupon 3.5.0, 3.6.0 and 3.6.2.
- Finding record
lucene/suggest/src/java/org/apache/lucene/search/suggest/fst/FSTCompletionLookup.java:71 (at a9779ef3c193) (@lucene.experimental), :105 (no-arg constructor 'should only be used to read a previously saved suggester'), :110-118 (two-argument constructor: exact matches promoted), :120-136 (four-argument constructor; :126-128 '@param exactMatchFirst If true exact matches are promoted to the top of the suggestions list. Otherwise they appear in the order of discretized weight and alphabetical within the bucket.'), :138-154 (pre-built-completion constructor, same javadoc, passes exactMatchFirst to both completions at :152-153), :241-244 (build: higherWeightsCompletion = builder.build(); only normalCompletion gets exactMatchFirst), :262-272 (lookup uses higherWeightsCompletion when higherWeightsFirst), :303-309 (load: one-argument FSTCompletion constructor at :305-306)lucene/suggest/src/java/org/apache/lucene/search/suggest/fst/FSTCompletion.java:102-122 (at a9779ef3c193) (three-argument constructor: exactFirst 'Find and push an exact match to the first position of the result list if found'), :124-131 (one-argument constructor 'Defaults to higher weights first and exact first', this(automaton, true, true))lucene/suggest/src/java/org/apache/lucene/search/suggest/fst/FSTCompletionBuilder.java:77-78 (at a9779ef3c193) (class javadoc: an exact match 'is returned first on the results list (even with by-weight sorting)', describing the builder's default), :190-197 (build() returns new FSTCompletion(automaton))lucene/suggest/src/test/org/apache/lucene/search/suggest/fst/TestFSTCompletion.java:116-140 (at a9779ef3c193) (testRequestedCount: FSTCompletion(fst, true, false) does not promote a low-weight exact match); TestWFSTCompletion.java:154-157 (testNonExactFirst, sibling honours false); no upstream test constructs FSTCompletionLookup with exactMatchFirst=false (grep of lucene/suggest/src/test)
- The finding in 30 seconds
lucene/suggest/src/java/org/apache/lucene/search/suggest/fst/FSTCompletionLookup.java:96-100, 130-136, 152-153, 222-227, 241-244, 262-281, 303-310lucene/suggest/src/java/org/apache/lucene/search/suggest/fst/FSTCompletion.java:102-131, 197-210, 233-283; lucene/suggest/src/java/org/apache/lucene/search/suggest/fst/FSTCompletionBuilder.java:190-198; lucene/suggest/src/java/org/apache/lucene/search/suggest/Lookup.java:193-195
- Who is affected
lucene/suggest/src/java/org/apache/lucene/search/suggest/fst/FSTCompletionLookup.java:71, 116-118, 287-290
- Why it looks unintended
lucene/suggest/src/java/org/apache/lucene/search/suggest/fst/FSTCompletionLookup.java:105, 126-128, 142-144, 152-153; lucene/suggest/src/java/org/apache/lucene/search/suggest/fst/FSTCompletion.java:109-110; lucene/suggest/src/java/org/apache/lucene/search/suggest/fst/FSTCompletionBuilder.java:77-78lucene/suggest/src/test/org/apache/lucene/search/suggest/fst/TestFSTCompletion.java:116-144; lucene/suggest/src/test/org/apache/lucene/search/suggest/fst/TestWFSTCompletion.java:154-179
- Suggested fix
lucene/suggest/src/java/org/apache/lucene/search/suggest/fst/FSTCompletionLookup.java:146-154; lucene/suggest/src/java/org/apache/lucene/search/suggest/fst/FSTCompletionBuilder.java:77-78
- TestFSTCompletionLookupExactMatchFirstFalse.java (JUnit test):
repro/LFV-706/src/org/apache/lucene/search/suggest/fst/TestFSTCompletionLookupExactMatchFirstFalse.java(35 lines)
Made in parallel with this doc from a probe run on main a9779ef3c193 and 10.5.2 (stock, and main with the fix); the values on screen come from the probe, which uses the same three entries as the repro.
assets/LFV-706/bug.png(504 KB)
![Summary diagram. Example: new FSTCompletionLookup(tempDir, "fst", 10, false), build() from star (weight 1), star wars (100), start (50), then lookup("star", true, 3). The flag exactMatchFirst = false reaches normalCompletion (alphabetical lookups, exactFirst = false) but not higherWeightsCompletion (by-weight lookups), which keeps exactFirst = true. Expected, the javadoc's order by discretized weight: [star wars/6, start/3, star/0]. Actual, the exact match first: [star/0, star wars/6, start/3]; with num = 1 only [star/0]. The same after store() and load(). Control: the pre-built-completion constructor passes the flag to both views and returns the documented order. Fix: new FSTCompletion(fst, true, exactMatchFirst) in build() and load().](../assets/LFV-706/bug.png)