All findings · Lucene finding · LFV-956 · synonym-file-utf8-bom-breaks-first-rule

A synonyms file saved as UTF-8 with a byte order mark silently loses its first rule

If a synonyms file starts with a UTF-8 byte order mark, the invisible mark sticks to the first word: with couch, sofa on line 1, a search for couch misses a document about a sofa, and a commented-out first rule becomes live. Every release from 3.4.0 to 10.6.0 and main.

Summary

Priority
Low, rank 499This is the same kept-BOM class as LFV-718 and LFV-719, but the synonym factories are the most widely used of the affected file loaders and the first rule silently stops working, so it leads the BOM rows right after LFV-857.
Impact
silent wrong results
Since
3.4.0 (LUCENE-3233, 2011)introduced 2011-07-11 · over 15 years
Affected
3.4.0 to 10.6.0 and main
Status
documented
Found by
bounded-exhaustive metamorphic check of the synonym loaders

AI-assisted page: this finding was first signalled by a semi-formal check (an exact check of the real jar, or a modelling hypothesis that a tool then witnessed) 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

The synonym factories read the synonyms file themselves. The word-list factories (stop words and the like) read their files through a shared line loader that drops a leading byte order mark; the synonym path keeps it on line 1.

Two things to know first

  1. A byte order mark (BOM) is the three bytes EF BB BF that some editors write at the start of a UTF-8 file. Java decodes them into one invisible character, U+FEFF (ZERO WIDTH NO-BREAK SPACE). It is not whitespace, so trim() keeps it.
  2. A synonyms file in the default Solr format has one rule per line: couch, sofa makes two words equivalent, tv => television maps one to the other, and lines starting with # are comments.
Summary card: a synonyms file that starts with a UTF-8 BOM silently loses its first rule. Left: synonyms.txt = BOM + 'couch, sofa' + 'tv, television', executed on main; 'couch' gets no synonym, 'sofa' gains an invisible U+FEFF-prefixed synonym, and a search for 'couch' over the one document 'a red sofa' gives 0 hits; line 2 still works. Right: why, from the code at a9779ef3c193: the synonym factory decodes the file with its own UTF-8 reader and the decoder keeps EF BB BF as U+FEFF; SolrSynonymParser's charAt(0) == '#' test and trim() keep it; WordlistLoader.getLines, used by the word-list factories such as StopFilterFactory, skips it. Below: a commented-out first line becomes a live rule, a first mapping never fires, WordNet's first word leaves its synset; every release from 3.4.0 to 10.5.2 and main; the recommended fix (fix F) skips one leading U+FEFF where both factories decode each file; the alternative, fix P, skips it in the two parsers.
One synonyms file, EF BB BF then couch, sofa and tv, television: the decoder keeps the mark as U+FEFF on line 1, so the rule's key is \uFEFFcouch, the token couch matches nothing, and a search for couch over a red sofa finds 0 hits. Select the still to open it at full size. Generated with AI, checked against the code.

The finding, step by step

  1. You save synonyms.txt with two rules, couch, sofa and tv, television, in an editor that writes UTF-8 with a byte order mark: 30 bytes instead of 27, starting EF BB BF, which Java decodes to the invisible character U+FEFF.
  2. You load it with SynonymGraphFilterFactory and its defaults, and expect what the same file without the mark gives: couch expands to [sofa, couch], and a search for couch finds the one document a red sofa.
  3. You get [couch]: the first rule is stored as \uFEFFcouch, sofa, so the token couch never matches it, and the search finds 0 of 1 documents. Line 2 still works (tv gives [television, tv]). No error, and printed tokens look right because U+FEFF is invisible.
  4. A commented-out first line is hit too: #couch, sofa now starts with U+FEFF, not #, so it becomes a live rule and sofa expands to \uFEFF#couch. The Apache license header that opens Lucene's own test synonyms files has a comma, so with a mark it becomes a rule as well.
  5. Why: the synonym factories decode the file with their own reader. The word-list factories (stop words, keep words, stemmer overrides, char-filter mappings, type files) read through WordlistLoader.getLines, which drops a leading mark ("skip initial bom marker"), and so did the synonym factory until its 3.4.0 rewrite.

Who is affected

Anyone who loads a synonyms file saved as UTF-8 with a byte order mark through SynonymGraphFilterFactory or the deprecated SynonymFilterFactory; the default settings are affected. One rule per file goes wrong, silently. The synonym filter is opt-in (no stock analyzer uses it); indexes keep wrong expansions until reindexed.

Mechanism, terms and numbers796 words

SynonymGraphFilterFactory and the deprecated SynonymFilterFactory decode the synonyms file with their own strict UTF-8 reader and pass it to SolrSynonymParser (the default format) or WordnetSynonymParser. Some editors save UTF-8 with a byte order mark, the bytes EF BB BF, which Java decodes to the invisible character U+FEFF. Nothing on the synonym path drops it: the Solr parser treats a line as a comment only if its first character is #, and trim() and the default whitespace rule tokenizer keep U+FEFF, so it stays glued to the first term of the first line. With couch, sofa on line 1, couch gets no synonym and sofa gains the invisible synonym \uFEFFcouch; a commented-out first line #couch, sofa becomes a live rule; in WordNet format the first word leaves its synset. With the default settings only the first line of each file is affected, and no error is raised. The word-list factories (stop words, keep words, stemmer overrides, char-filter mappings, type files) read through WordlistLoader.getLines, which skips a leading mark (SOLR-1865, 2010), and the synonym factory did too in 3.1 to 3.3; the 3.4.0 rewrite (LUCENE-3233) gave it its own reader without the skip. Every release from 3.4.0 to 10.5.2, 10.6.0 (branch_10_6) and main is affected (10 releases executed, the rest inferred from the unchanged code). Skipping one leading U+FEFF where the factories decode each file fixes it.

Terms

  • Synonyms file: the synonyms parameter of SynonymGraphFilterFactory and the deprecated SynonymFilterFactory (one file or a comma-separated list), parsed by SolrSynonymParser (format=solr, the default) or WordnetSynonymParser (format=wordnet).
  • Byte order mark (BOM): the bytes EF BB BF at the start of a UTF-8 file. Java's UTF-8 decoder keeps them as the character U+FEFF; Character.isWhitespace(0xFEFF) is false and "\uFEFF".trim() has length 1.

The bug

  1. SynonymGraphFilterFactory.loadSynonyms (SynonymGraphFilterFactory.java:168-194) builds a strict UTF-8 decoder and, for each file, calls parser.parse(new InputStreamReader(loader.openResource(file), decoder)) (:191). The deprecated SynonymFilterFactory.loadSynonyms (SynonymFilterFactory.java:171-199) does the same (:194-196). The decoded first line of the example is \uFEFFcouch, sofa, 12 characters.
  2. SolrSynonymParser.addInternal skips a line only if it is empty or its charAt(0) is # (SolrSynonymParser.java:85; javadoc :33: "Blank lines and lines starting with '#' are comments"). It splits the line on => and ,, and each piece goes through unescape(...).trim() and the rule analyzer (:96-118), by default a WhitespaceTokenizer (SynonymGraphFilterFactory.java:146), which does not split on U+FEFF. So the first term is \uFEFFcouch, and a # first line is parsed as a rule; it only has a visible effect when it contains , or =>.
  3. WordnetSynonymParser.parse reads the synset id from fixed columns, line.substring(2, 11) (WordnetSynonymParser.java:53-54): with the mark, line 1 gives (10000000 instead of 100000001, so the first word starts a synset of its own.
  4. The factories that read word lists and line files through AbstractAnalysisFactory.getLines / getWordSet go through WordlistLoader.getLines, which drops a leading U+FEFF before the # test: // skip initial bom marker (WordlistLoader.java:302-304). Stop words, keep words and common-grams words in the default wordset format, stemmer overrides, char-filter mappings and type lists get the skip. Not every resource loader does: the snowball words format (AbstractWordsFileFilterFactory.java:89-90 -> AbstractAnalysisFactory.getSnowballWordSet, its own reader), ICU tokenizer rule files and the Japanese and Korean user dictionaries keep the mark too (separate reports).

What the synonym filter does (main a9779ef3c193, file = EF BB BF + rules, default settings)

first line of the filetextwithout the markwith the mark
couch, sofacouch[sofa, couch][couch]; the map key is \uFEFFcouch
couch, sofasofa[couch, sofa][\uFEFFcouch, sofa]
couch, sofa; search couch over a red sofaSynonym(f:couch f:sofa), 1 hitf:couch, 0 hits
tv => televisiontv[television][tv]: the mapping never fires
#couch, sofa (commented out)sofa[sofa][\uFEFF#couch, sofa]: the comment is a rule
the Apache license header, # The ASF licenses ... Apache License, Version 2.0Version 2.0[Version, 2.0]the 12 header tokens from \uFEFF# to License added as a synonym
# my synonyms (no , or =>)couch[sofa, couch] (the diagram check's probe)[sofa, couch]: harmless
WordNet: couch and sofa in synset 100000001couch[sofa, couch][couch]
deprecated SynonymFilterFactory, couch, sofacouch[couch, sofa][couch]

Line 2 and later lines are unaffected; with a comma-separated list, each file that starts with a mark loses its own first line. With the non-default tokenizerFactory=org.apache.lucene.analysis.standard.StandardTokenizerFactory, which drops a leading U+FEFF, the first rule survives, but a commented-out first rule still becomes active, and a mark followed by a blank first line fails to load (term: \uFEFF was completely eliminated by analyzer) while the same file without the mark loads.

Exposure and likelihood334 words
  • Who is exposed. Applications that configure SynonymGraphFilterFactory or the deprecated SynonymFilterFactory with a synonyms file: directly, through CustomAnalyzer, or through a search server's schema (the factory javadoc example is a schema snippet). The factories' defaults are affected: format=solr, the whitespace rule tokenizer, expand=true. The component is opt-in: no stock Lucene analyzer uses it; SynonymGraphFilterFactory and both parsers are @lucene.experimental, and SynonymFilterFactory is deprecated.
  • What it takes. A synonyms file saved as UTF-8 with a byte order mark, which the 2010 CHANGES entry for the shared loader's skip describes as "often created by editors such as Windows Notepad", whose first line is a rule, or a # line with , or =>. A re-saved copy of a file that starts with the Apache license header qualifies: that header line has a comma.
  • What happens. The first rule silently stops matching its first term (missed expansions: the couch query finds 0 of 1 documents), and its other terms gain an invisible synonym that matches nothing; a first explicit mapping never fires; a commented-out first rule becomes active and adds expansions the user disabled; in WordNet format the first word loses its synonyms. With the default settings, no error or warning (with the non-default StandardTokenizerFactory as the rule tokenizer, a mark before a blank first line makes loading fail).
  • Scale. Deterministic and independent of index size: one line per file. If the filter runs at index time, terms indexed with the wrong rules stay until reindexing.
  • Versions. Every release from 3.4.0 to 10.5.2, 10.6.0 (branch_10_6) and main (10 releases executed, the rest inferred from the code).
  • How likely. Rare: a file saved with a mark is needed, only its first line goes wrong, and no public report was found in 15 years. The WordNet half is the weaker one (WordNet prolog files are machine-generated), so the Solr-format symptoms lead.
  • Workarounds. Save the file without a mark, or start it with a blank line or a comment line without , or =>.

Minimal repro

The test shows that SynonymGraphFilterFactory with a synonyms file that starts with a UTF-8 byte order mark ("\uFEFFfoo, bar\n") silently drops the first rule.

TestSynonymGraphFilterFactoryUtf8Bom.java (JUnit test)38 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.analysis.synonym;

import java.io.StringReader;
import java.util.HashMap;
import java.util.Map;
import org.apache.lucene.analysis.Tokenizer;
import org.apache.lucene.analysis.core.WhitespaceTokenizer;
import org.apache.lucene.tests.analysis.BaseTokenStreamTestCase;
import org.apache.lucene.tests.util.StringMockResourceLoader;

public class TestSynonymGraphFilterFactoryUtf8Bom extends BaseTokenStreamTestCase {

  public void testBomBeforeFirstRule() throws Exception {
    SynonymGraphFilterFactory factory =
        new SynonymGraphFilterFactory(new HashMap<>(Map.of("synonyms", "synonyms.txt")));
    factory.inform(new StringMockResourceLoader("\uFEFFfoo, bar\n"));
    Tokenizer tokenizer = new WhitespaceTokenizer();
    tokenizer.setReader(new StringReader("foo"));
    // expected: foo expands to [bar, foo], as without the BOM; actual: [foo]
    assertTokenStreamContents(factory.create(tokenizer), new String[] {"bar", "foo"});
  }
}
Command
# in a copy of this finding's repro folder (repro/LFV-956/, 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: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.analysis.synonym.TestSynonymGraphFilterFactoryUtf8Bom
Expected vs actual
Expected "foo" to expand to [bar, foo], as without the mark; actual: [foo] (term 0 expected:<[bar]> but was:<[foo]>), with -ea and -da.

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

More files, environment, full output38 words

Environment

luceneLucene main e1faeba08bde (2026-11-02)
jdk25.0.3+9-LTS

Observed output

# Lucene main 645d4fe14a20, assertions on (-ea), seed DEADBEEF: 1 test(s) run, 1 failed
    => org.junit.ComparisonFailure: term 0 expected:<[bar]> but was:<[foo]>
       org.junit.Assert.assertEquals(Assert.java:117)
       org.apache.lucene.tests.analysis.BaseTokenStreamTestCase.assertTokenStreamContents(BaseTokenStreamTestCase.java:262)
       org.apache.lucene.tests.analysis.BaseTokenStreamTestCase.assertTokenStreamContents(BaseTokenStreamTestCase.java:513)
       org.apache.lucene.tests.analysis.BaseTokenStreamTestCase.assertTokenStreamContents(BaseTokenStreamTestCase.java:598)
       org.apache.lucene.tests.analysis.BaseTokenStreamTestCase.assertTokenStreamContents(BaseTokenStreamTestCase.java:622)
       org.apache.lucene.tests.analysis.BaseTokenStreamTestCase.assertTokenStreamContents(BaseTokenStreamTestCase.java:689)
       org.apache.lucene.analysis.synonym.TestSynonymGraphFilterFactoryUtf8Bom.testBomBeforeFirstRule(TestSynonymGraphFilterFactoryUtf8Bom.java:36)

Why it looks unintended

  • The format says "lines starting with '#' are comments", yet a commented-out first rule becomes live.
  • Lucene's shared line loader has dropped a leading mark on purpose since 2010; the synonym factory did too until its 3.4.0 rewrite removed it without a word.
  • The equivalent hunspell change was filed under Bug fixes; nothing says synonyms files must be BOM-free.
The full argument269 words
  • The file's own comment rule. "Blank lines and lines starting with '#' are comments" (SolrSynonymParser.java:33). A user who writes #couch, sofa on line 1 sees a line that starts with #; with a mark, Lucene makes it a rule.
  • Lucene's shared line loader drops the mark on purpose. SOLR-1865 (2010) added the skip with the CHANGES entry "SolrResourceLoader.getLines ignores Byte Order Markers (BOMs) at the beginning of input files, these are often created by editors such as Windows Notepad"; LUCENE-4257 moved it into WordlistLoader.getLines (WordlistLoader.java:302-304). Every factory that reads its file through getLines or getWordSet gets it: the same resource loader and the same bytes give a different result for synonyms.
  • The synonym factory had it and lost it silently. In 3.1 to 3.3, SynonymFilterFactory read its rules with SolrResourceLoader.getLines, skip included. LUCENE-3233 (3.4.0) replaced that with a plain InputStreamReader. Its issue (41 comments), its CHANGES entries ("Improved memory usage, build time, and performance of SynonymFilterFactory"; "Added SynonymFilter ... with parsers for wordnet and solr formats") and the upgrade notes say nothing about BOM or encoding handling. Executed: 3.3.0 clean, 3.4.0 affected, 3.4.0 with luceneMatchVersion 3.3 (the old loader) clean.
  • Hunspell's equivalent was a bug fix. "LUCENE-5505: HunspellStemFilter ignores BOM markers in dictionaries" is listed under "Bug fixes" (lucene/CHANGES.txt:12543, :12554); Dictionary.java:351-352: "ignore any BOM marker on first line".
  • Valid input, no documented exclusion. A file with a mark is valid UTF-8, and the factories' strict decoder accepts it. No javadoc, test, CHANGES or MIGRATE entry says synonyms files must be BOM-free, and no synonym test uses a mark.
The devil's-advocate check274 words

The checkers built the strongest case for "intended": (a) SynonymMap.Parser.parse takes a Reader, i.e. characters, and the Solr grammar exempts only lines whose first character is #; U+FEFF is a real character (ZERO WIDTH NO-BREAK SPACE). (b) SynonymGraphFilterFactory and the parsers are @lucene.experimental. (c) Committers have said Reader-level parsers do not deal with encodings: "If you use Lucene directly, the stopword file parser does not care about encodings at all, it simply takes a java.io.Reader" (uschindler, SOLR-1860, 2010), and "if you have a byte order mark in your character input (reader) then your conversion from bytes is screwed up somewhere before" (dweiss, LUCENE-7785, 2017, about a char-filter mapping: an analogy). (d) In the 2010 user-list thread behind the skip, rmuir answered "No." to whether a file with a mark is a bug, then proposed skipping it "to be more user-friendly"; SOLR-1865 is typed Improvement. (e) Nobody reported it in 15 years.

Why we think it fails: (a) to (c) describe the Reader-level parser; users configure the factory with a file name, and the factory itself turns the bytes into characters, so by the reasoning of (c) the factory is where the mark should go (the recommended fix). (d) was about whether to handle marks at all; the project then did, in the very loader the synonym factory used until 3.4.0, and the rewrite dropped it without a word. (e) shows rarity, not intent. The cross-model reviews rated the works-as-designed case moderate (the intended-lens review: "not ultimately persuasive"; the refute-lens review: "not conclusive"), and the devil's advocate rated it weak to moderate. The literal reading (a) stays arguable: expect "improvement, not bug".

Suggested fix

Recommended F. Skip the mark where the factories decode each file (SynonymGraphFilterFactory and SynonymFilterFactory)

Skip one leading U+FEFF where both factories decode each synonyms file (loadSynonyms), as the shared line loader does: read the first character through a PushbackReader and push it back unless it is U+FEFF. Validated: the repro, an on-disk test, all upstream synonym suites and the whole analysis-common module pass, and so does the module's format and lint check.

     for (String file : files) {
       decoder.reset();
-      parser.parse(new InputStreamReader(loader.openResource(file), decoder));
+      try (PushbackReader reader =
+          new PushbackReader(new InputStreamReader(loader.openResource(file), decoder), 1)) {
+        // skip initial bom marker, as WordlistLoader.getLines does for other resource files
+        int first = reader.read();
+        if (first != -1 && first != '\uFEFF') {
+          reader.unread(first);
+        }
+        parser.parse(reader);
+      }
     }
All options and trade-offs3 options · 846 words

Options F and P were each applied in a scratch worktree of main a9779ef3c193 and run through gradle with assertions on and off; the worktree was removed afterwards. Option C was not run. Pick one of F and P: together they would strip two leading U+FEFF characters.

F. Skip the mark where the factories decode each file (SynonymGraphFilterFactory and SynonymFilterFactory) (validated by the checks) (recommended)

In both loadSynonyms methods (SynonymGraphFilterFactory.java:191, SynonymFilterFactory.java:194-196), wrap the decoding reader in a PushbackReader of size 1, read one character and push it back unless it is U+FEFF, then parse as before, inside a try-with-resources. +19/-4 lines over the two files (one import each; the deprecated factory drops its unused java.io.Reader import).

What it changes. Only the factory path: one U+FEFF at the very start of each synonyms file is dropped before the parser sees it, the case getLines covers for the word-list factories. It covers every parser the factory loads, including a custom SynonymMap.Parser named in format. Files without a mark are read unchanged. SynonymGraphFilterFactory now also closes each reader itself, as the deprecated factory already did; the parser's own close() makes the second close a no-op. A decoding error in the first character is now raised by the factory's read() rather than inside parse: the same MalformedInputException, an IOException, either way (read, not run).

     for (String file : files) {
       decoder.reset();
-      parser.parse(new InputStreamReader(loader.openResource(file), decoder));
+      try (PushbackReader reader =
+          new PushbackReader(new InputStreamReader(loader.openResource(file), decoder), 1)) {
+        // skip initial bom marker, as WordlistLoader.getLines does for other resource files
+        int first = reader.read();
+        if (first != -1 && first != '\uFEFF') {
+          reader.unread(first);
+        }
+        parser.parse(reader);
+      }
     }

Trade-offs

  • A direct caller of SolrSynonymParser.parse(Reader) or WordnetSynonymParser.parse(Reader) that builds its own Reader from a marked file still gets the glued term. That matches the committers' view that Reader-level parsers are encoding-agnostic, but such callers exist downstream (engines that read synonyms files with their own UTF-8 reader; checked by reading only) and would need their own skip.
  • Like getLines, it cannot tell a mark from a file whose first character is meant as a ZERO WIDTH NO-BREAK SPACE (not a realistic synonym).
  • Open PR #13054 (synonym map off-heap) rewraps loadSynonyms, so the two may conflict.
  • Indexes built with the wrong rules keep their terms until reindexed. No CHANGES entry is drafted.

What the checks validated

  • Gradle in a scratch worktree of main a9779ef3c193, seed DEADBEEF, assertions on and off: the 6-test repro passes (stock: 5 of 6 fail), the independent on-disk test passes 6 of 6 (stock: 3 of 6 fail), and all upstream synonym suites plus TestConditionalTokenFilter pass: 14 classes, 144 tests, 0 failures, each way.
  • All of :lucene:analysis:common:test with assertions on: 341 classes, 1,921 tests (the two BOM test classes included), 18 skipped, 0 failures.
  • :lucene:analysis:common:check -x test passes with the repro added (google-java-format, ecjLint, forbiddenApis).

P. Skip the mark on the first line in the two parsers (SolrSynonymParser and WordnetSynonymParser) (validated by the checks)

In SolrSynonymParser.addInternal (before the comment test, SolrSynonymParser.java:85) and WordnetSynonymParser.parse (before substring(2, 11), WordnetSynonymParser.java:54), drop a leading U+FEFF from the first line of each parse() call, the idiom of WordlistLoader.getLines. +6 lines per parser. This is the hunt's fix; the finder, a checker and the registration check ran it in the repro harness.

+    boolean first = true;
     while ((line = in.readLine()) != null) {
+      if (first) {
+        first = false;
+        // skip initial bom marker, as WordlistLoader.getLines does
+        if (line.length() > 0 && line.charAt(0) == '\uFEFF') line = line.substring(1);
+      }
       if (line.length() == 0 || line.charAt(0) == '#') {

Trade-offs

Also covers downstream callers that build their own Reader and call the parsers directly. But it changes @lucene.experimental Reader-level parsers that committers have called encoding-agnostic, a Reader whose text really starts with U+FEFF loses it, and a custom SynonymMap.Parser named in format is not covered.

What the checks validated

  • The same gradle runs, assertions on and off: the repro, the on-disk test and the upstream synonym suites pass (14 classes, 144 tests, 0 failures, each way); :lucene:analysis:common:check -x test passes with the repro added. The whole module was not re-run for this option.
  • Earlier, shadowed into the repro harness by the finder and the checkers: the repro 6 of 6, 94 upstream synonym tests, an independent on-disk test 6 of 6, a root-cause variant 8 of 8; the bounded-exhaustive check drops to 0 mismatches.

C. One BOM-skipping reader for every factory resource file (not run)

A shared helper (for example in AbstractAnalysisFactory, next to getLines) that opens a resource as strict UTF-8 and drops one leading U+FEFF, used wherever a factory decodes a resource itself.

Trade-offs

Would also fix the same pattern in other loaders (snowball-format words files and ICU tokenizer rule files are separate confirmed reports, and the Japanese and Korean user dictionaries a sibling one), so it suits a single "BOM in resource files" issue; it touches several modules and needs its own tests. Not drafted.

Downstream note

Search servers that load synonyms files through these factories inherit it whenever an operator saves the file in an editor that writes a byte order mark; engines that pass their own UTF-8 reader to the parsers see the same (by reading their code, not run).

How it was found

A bounded-exhaustive metamorphic check of the real factories: every synonyms file of 1 to 3 lines over a small alphabet must analyze the same with and without a leading byte order mark. Solr format: 456 of 620 checks differed per factory; WordNet: 112 of 336; three loaders that use the shared line loader: 0.

The method in full221 words
  • First signal. On 2026-10-27 a probe in the hunt's unit on factory resource-file formats (the unit that found the snowball words-file case) printed <BOM>foo,bar : foo -> [foo] for the synonym factory and recorded it as a lead for another unit; a planning probe re-ran it (8 checks, 6 failing).
  • Method (semi-formal): bounded-exhaustive metamorphic check on the stock main jars, in the hunt's unit on synonym and user-dictionary loaders. Law: loading F and loading BOM+F give the same analysis of every probe text (terms, position increments, position lengths) or the same exception class. Inputs: every file of 1 to 3 lines over a 5-line Solr alphabet (equivalence, explicit mapping, comment with a comma, blank, multi-word) and a 4-line WordNet alphabet, with LF and CRLF line ends and expand true and false, through both synonym factories, plus three controls that read through the shared line loader (stop filter, stemmer override, mapping char filter).
  • Signal. Solr format: 456 of 620 checks mismatch for each factory (372 with a term glued to U+FEFF, 84 where a first mapping never fires); WordNet: 112 of 336 for each factory; the three controls: 0. With the parser fix shadowed, every family: 0.
  • The finder then traced it to the factories' own reader, checked the 3.4.0 history and wrote the public-API JUnit; independent checkers followed (see "Independent checks").

Prior reports, history and checks

Prior reports

No prior report found (searched 2026-10-28: GitHub issues, PRs and commits, JIRA, the Lucene and Solr lists, downstream trackers, the web and the changed files of all 414 open PRs). Closest: SOLR-1865 (2010), which made the shared line loader skip the mark.

Searches6 searches

A 2018 issue in a downstream plugin repository ends with the reporter saying that saving synonyms.txt as "UTF-8 without BOM" solved their problem, but the error they reported (an analyze-request error) does not depend on file content, and no symptom of this bug and no Lucene code is named; it was judged not a report and is listed for transparency. Tracking issue #16937 and the dev@ thread do not mention it.

What was searched, when, and what turned up
WhereWhenHowFound
scripted prior-report search (three new runs, plus the finder's and a checker's)2026-10-28a loader, a symptom and a WordNet/regression angle: 937, 737 and 156 distinct hits over GitHub issues, PRs and commits, downstream repositories, the Lucene and Solr lists and the JIRA archive; earlier runs 191 and 52 hits; every hit scanned for BOM, byte order mark and FEFFno report
GitHub (manual)2026-10-28about 50 searches over apache/lucene (including items created since 2026-10-20 and 2026-10-26), apache/solr, apache/lucene-solr, downstream search-engine repositories and all of GitHub; the changed-file lists of all 414 open apache/lucene PRs; a refreshno report; only open PR #13054 (synonym map off-heap) touches loadSynonyms, with no BOM handling
JIRA (LUCENE, SOLR)2026-10-2810 queries; SOLR-1865, SOLR-1860 and SOLR-12731 read in fullonly SOLR-1865 (the shared loader, stop-word lists) and SOLR-1860 (stop-word lists)
Mailing lists2026-10-2835 queries over 7 Lucene and Solr lists; threads readonly the 2010 solr-user thread behind SOLR-1865
Web and Stack Overflow2026-10-2826 web queries; 7 "synonyms not working" questions readno report; one downstream plugin issue (2018) with BOM folklore, not this bug
Closest items (read)2026-10-28full threadsSOLR-1865 (precedent), #4306 / LUCENE-3233 (the 3.4.0 rewrite; 41 comments, no BOM discussion), #6568 / LUCENE-5505 (hunspell ignores BOMs, a bug fix), SOLR-1860, #13054 (open PR)

History

A regression in 3.4.0: LUCENE-3233 (015ecfa0a02, 2011-07-11) gave the synonym factory its own reader and bypassed the BOM-skipping line loader it used in 3.1 to 3.3. Executed: 3.3.0 clean, every tested release from 3.4.0 to 10.6.0 and main affected. Never fixed.

Versions tested8 rows

Introduced by LUCENE-3233 "HuperDuperSynonymsFilter" (#4306), by rmuir: trunk 015ecfa0a02 and the branch_3x backport a52015db05f, both 2011-07-11 ("LUCENE-3233: improve ram/perf of SynonymFilter, add wordnet parsing, nuke contrib/wordnet"), no PR. It added FSTSynonymFilterFactory, which passes new InputStreamReader(loader.openResource(file), decoder) straight to the new SolrSynonymParser and WordnetSynonymParser, and made SynonymFilterFactory use it for luceneMatchVersion 3.4 and later. Before, SynonymFilterFactory.loadRules read the rules with SolrResourceLoader.getLines, which had skipped a leading U+FEFF since SOLR-1865 (2010-05-08).

Reach over time. 3.4.0 to 3.6.2: Solr's factory with luceneMatchVersion 3.4 or later (the value the 3.4.0 example config ships); a lower value kept the old loader until 5.0.0 removed it. The factories moved into Lucene's own jars in 4.0.0-beta (LUCENE-4044); SynonymGraphFilterFactory (same loader) exists since 6.4.0. The WordNet format came with the introducing commit, so it was never unaffected.

Never fixed. No commit on any branch adds BOM handling to the synonym code. The reader line was edited (LUCENE-5219 turned parser.add into parser.parse, LUCENE-5945 restructured the file-list loop), but through those and LUCENE-4095 and LUCENE-6664 the factories kept passing an unmodified InputStreamReader to the parser. Upstream main 645d4fe14a20 (2026-10-28) has no later change to the synonym package or WordlistLoader.

Versions and branches executed, with results
Version or commitVerdictResult
main a9779ef3c193 affectedthe JUnit fails 5 of 6 (assertions on and off); the on-disk tests 3 of 6 and 6 of 8 (assertions on and off)
branch_10_6 4ae946e5a9b9 affectedthe JUnit fails 5 of 6 (assertions on)
10.5.2, 10.0.0, 9.12.3, 9.1.0 affectedthe JUnit fails 5 of 6 (assertions on); the on-disk tests on 10.5.2 and 9.12.3 too
8.11.4, 6.4.0, 5.0.0, 4.0.0 affectedmain() checks through the public factories: every BOM file differs from its twin (both factories from 6.4.0)
3.6.2, 3.4.0 affectedmain() check through the released Solr factory and resource loader: 3 of 3 differ (first rule, mapping, commented-out rule)
3.4.0 and 4.0.0 with luceneMatchVersion 3.3 controlthe old getLines-based loader: no difference
3.3.0 not affectedthe last release whose synonym factory read through getLines: no difference
fix F on main a9779ef3c193 not affectedthe repro, the on-disk test, all upstream synonym suites (144 tests) and all 1,921 analysis-common tests pass; fix P gives the same on the suites

Independent checks

An independent reproducer, reviews by a reviewer from another model family, a devil's advocate with real files on disk, an adjudication and a credibility audit all re-ran or re-read it; every executed check reproduced it. This doc re-ran it through gradle and validated two fixes.

Checks and verdicts365 words

The cross-model reviews were reading-only (they read ea57360912ed, whose synonym package and WordlistLoader equal a9779ef3c193, and could not run code); every claim they read was executed by the other checks. Everything else ran on a9779ef3c193.

Each independent check and its verdict
CheckVerdictWhat it found
Cross-model review, intended lens real (medium)An independent reviewer from another model family (OpenAI GPT-5.6), reading only: root cause agrees, inputs in contract; the intended-behaviour case is moderate and "not ultimately persuasive". A separate re-run failed 5 of 6.
Independent reproducer realOwn test with real files on disk through CustomAnalyzer.builder(Path): 6 of 8 fail on main (assertions on and off), 10.5.2 and 9.12.3; both controls pass; 8 of 8 pass with a root-cause shadow.
Cross-model review, refute lens not real (low), proceduralThe same OpenAI GPT-5.6 reviewer, reading only: "not real" because it could not run its own test; it agreed with the root cause and the inputs, and rated the intended-behaviour case moderate, "not conclusive". A separate main() with other words (red, crimson, scarlet => vermilion) found 6 of 6 cases different, assertions on and off.
Intended-behaviour devil's advocate real (high)Intended argument weak to moderate. Its on-disk test fails 3 of 6 on main, 10.5.2 and 9.12.3 and passes with the fix; it found the license-header case.
Adjudication confirmed (high)Re-ran the independent reproduction's test (6 of 8 fail, assertions on) and the finder's (5 of 6, assertions off) with new seeds.
Credibility audit downgrade (impact only)An independent on-disk main() check: 6 of 8 cases differ, both controls agree; the bug stands, impact re-scored for an opt-in, experimental component.
Fix validation (this doc) fixes F and P passGradle in a scratch worktree of a9779ef3c193, seed DEADBEEF: stock, the repro fails 5 of 6 and the on-disk test 3 of 6 with assertions on and off; with fix F and, separately, fix P both pass, as do the upstream synonym suites (144 tests with the two BOM classes); with fix F all 1,921 analysis-common tests pass; the module check passes with the repro added, which needed two javadoc paragraphs rewrapped.

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 notes1,727 words

Finding record

LFV-956, snapshot of 2026-11-06

Registry title
SynonymGraphFilterFactory and SynonymFilterFactory keep a UTF-8 byte order mark at the start of a synonyms file, so the file's first rule silently stops working and a commented-out first line that contains a comma becomes a live rule
Area
analysis-common / synonym file loading (SynonymGraphFilterFactory and the deprecated SynonymFilterFactory loadSynonyms -> SolrSynonymParser, WordnetSynonymParser)
Priority
Low, rank 499: This is the same kept-BOM class as LFV-718 and LFV-719, but the synonym factories are the most widely used of the affected file loaders and the first rule silently stops working, so it leads the BOM rows right after LFV-857.
Impact class
silent wrong results
Impact note
Every user of SynonymGraphFilterFactory or the deprecated SynonymFilterFactory whose synonyms file was saved as UTF-8 with a byte order mark, in every release since 3.4.0 and with the factory's default settings (solr format, whitespace rule tokenizer): only the first line of each such file is affected, but there the first rule silently stops matching because its first term carries an invisible U+FEFF, a first line that starts with '#' and contains ',' or '=>' (such as the Apache license header of the example synonyms files) becomes a rule, and in WordNet format the first word leaves its synset; no error is raised, the index is not damaged and no scale is needed.
Summary
You configure SynonymGraphFilterFactory (or SynonymFilterFactory) with a synonyms file that an editor saved as UTF-8 with a byte order mark (bytes EF BB BF) and expect the same rules as from the file without the mark, because Lucene's shared resource-line loader skips a leading BOM for the word-list factories that read through it (WordlistLoader.getLines, 'skip initial bom marker', SOLR-1865) and the synonym factory itself skipped it until 3.4.0. Instead the factory decodes the file with its own UTF-8 reader and SolrSynonymParser keeps U+FEFF glued to the first line: with the first rule 'foo, bar' the text 'foo' is no longer expanded (and 'bar' gains the invisible synonym '\uFEFFfoo'), a first rule 'baz => qux' never fires, and a first line '#couch, sofa' is no longer a comment, so 'sofa' expands to '\uFEFF#couch'. In WordNet format the fixed-column synset id (substring(2, 11)) is shifted by one character, so the first word ends up in a synset of its own; nothing reports an error.
Introduced
015ecfa0a0255f053682bb2436b2b8dec5cb8283 (LUCENE-3233 'HuperDuperSynonymsFilter', apache/lucene#4306; trunk svn r1145158, author rmuir, message 'LUCENE-3233: improve ram/perf of SynonymFilter, add wordnet parsing, nuke contrib/wordnet'; no pull request, Subversion era) with the branch_3x backport a52015db05f790613906a9858531ff16e3d78b8a (r1145178), the one released in 3.4.0. The commit added FSTSynonymFilterFactory, which passes new InputStreamReader(loader.openResource(file), decoder) straight to the new SolrSynonymParser and WordnetSynonymParser, and made SynonymFilterFactory use it for luceneMatchVersion >= 3.4; before, SynonymFilterFactory.loadRules read the rules with SolrResourceLoader.getLines, which had skipped a leading U+FEFF before the '#' comment test since SOLR-1865 (f3c25f02d587, 3.x backport 7aaf7ed9a20, 2010-05-08). So this is a regression; no issue comment (body and 41 comments read), CHANGES entry or upgrade note mentions BOM or encoding handling, so the drop looks unintended (not provable). Never fixed: git log --all -G 'FEFF|[Bb][Oo][Mm]' over the synonym paths finds no commit on any branch, and the reader line was carried unchanged through LUCENE-4095 (bc3a3dc5d47), LUCENE-5219 (9d16038391b), LUCENE-5945 (3eb66fb19ca) and LUCENE-6664 (c0467bb9291, which added SynonymGraphFilterFactory). [read: git history in the full-history clone; the boundary is executed: 3.3.0 clean, 3.4.0 BUG, 3.4.0 and 4.0.0 with luceneMatchVersion 3.3 (old getLines-based SlowSynonymFilterFactory) clean] (2011-07-11 (trunk and branch_3x; author and commit dates identical, Subversion import))
First affected version
3.4.0 [exec] (Solr SynonymFilterFactory with luceneMatchVersion >= 3.4, the value the 3.4.0 example solrconfig.xml ships; the parsers ship in Lucene contrib analyzers from 3.4.0 and the Lucene-module factory exists from 4.0.0-beta, LUCENE-4044). Last unaffected release: 3.3.0 [exec]. luceneMatchVersion < 3.4 kept the BOM-safe SlowSynonymFilterFactory through 4.10.4 [exec controls on 3.4.0 and 4.0.0 pass]; 5.0.0 removed it. format=wordnet was added by the introducing commit, so for that format no unaffected release exists.
Affected versions
Every release from 3.4.0 to 10.6.0 (branch_10_6 head 4ae946e5a9b9) and main a9779ef3c193 [exec: main() differential checks through the public factory (BOM file vs the same file without it) on 3.4.0, 3.6.2, 4.0.0, 5.0.0, 6.4.0 and 8.11.4 (both factories from 6.4.0); the finder's JUnit fails 5 of 6 on 9.1.0, 9.12.3, 10.0.0, 10.5.2, branch_10_6 and main (-ea; main also -da); the independent reproduction's and the intended-behaviour check's own tests fail on main (-ea and -da), 10.5.2 and 9.12.3 (-ea); releases in between read]. SynonymGraphFilterFactory is affected since it was added in 6.4.0 [exec]. Not affected: 3.1-3.3, which read synonyms through getLines [3.3.0 exec]. Upstream main 645d4fe14a20 (2026-10-28) has no synonym-package or WordlistLoader commit after a9779ef3c193.
Status
documented (brief, checks and a per-finding doc exist)
Verification
Confirmed by AI-run checks: an independent repro, a cross-model review that could only read the code (its low-confidence “not real” is procedural), an intended-behaviour check, a prior-report search and a skeptical audit; not yet triaged by a person.
Found by
semi-formal; also reached by exhaustive check
Upstream and prior reports
none found (searched 2026-10-28, read-only; the prior-report record, verdict new): three scripted prior-report searches (937, 737 and 156 distinct hits over GitHub issues, PRs and commits, downstream repositories, the Lucene and Solr lists and the JIRA archive; every stored hit scanned for BOM, byte order mark and FEFF) plus the finder's (191 hits) and the intended-behaviour check's (42 queries, 52 hits); manual GitHub searches over apache/lucene, apache/lucene-solr, apache/solr, Elasticsearch, OpenSearch and all of GitHub, including items created or updated in the last 48 h; 10 JQL queries on LUCENE and SOLR (SOLR-1865, SOLR-1860 and SOLR-12731 read in full); 35 queries over the Lucene and Solr list archives; Stack Exchange; 26 web queries; the changed-file lists of all 414 open apache/lucene PRs (only #13054, synonym map off-heap, open since 2024, touches loadSynonyms, and it adds no BOM handling). No issue, PR, JIRA entry, list post or downstream report describes a BOM at the start of a synonyms file breaking its first rule. Related only: SOLR-1865 (2010, BOM skip in getLines, typed Improvement, about stopword lists; the precedent), #4306 / LUCENE-3233 (the 3.4.0 rewrite that bypassed getLines; 41 comments, no BOM discussion), #6568 / LUCENE-5505 (hunspell ignores BOMs, filed as a bug fix), SOLR-1860 (stopwords), #10073 / LUCENE-9030 and #9148 / LUCENE-8100 (other WordNet-parser issues), and a 2018 downstream plugin issue (sing1ee/elasticsearch-jieba-plugin#17) whose reporter later wrote that their problem was solved by saving synonyms.txt as 'UTF-8 without BOM', although the error they reported ('request body or source parameter is required' from _analyze) does not depend on file content and no symptom of this defect is described. Not fixed: the four synonym files are unchanged on upstream main (head 645d4fe14a20, 2026-10-28) and branch_10_6 since 2026-04-02 (author dates). Refresh 2026-10-28 (GitHub search and REST API): apache/lucene items mentioning synonym created since 2026-10-26 or updated since 2026-10-27: 0; BOM updated since 2026-10-26: 0; 'byte order mark' updated since 2026-10-20: 0; 'FEFF synonym': 0; SolrSynonymParser: the same 5 old, unrelated items; of the 20 PRs created since 2026-10-26, none lists a synonym file or WordlistLoader.java in its changed files (first page; #16911 deletes all of branch_10x); the only synonym-package commit on main since 2026-10-01 is 2eb462541b1c (#16834, author 2026-10-13, unrelated, already in a9779ef3c193); elastic/elasticsearch 'synonyms BOM', opensearch-project/OpenSearch 'synonym BOM', apache/solr 'synonyms BOM' and all-of-GitHub 'synonyms.txt "byte order mark"': 0.apache.org stores only the start of each mail, the web and Stack Exchange searches were throttled at times, and private reports are not searchable.
Lucene commit tested
main a9779ef3c193 (2026-10-26) through gradle in a scratch worktree (removed afterwards), seed DEADBEEF, assertions on and off: stock, the 6-test JUnit fails 5 of 6 and an independent on-disk test 3 of 6; with fix F (factories) and, separately, fix P (parsers), both pass together with all upstream synonym suites (144 tests); with fix F all of :lucene:analysis:common:test passes and so does :lucene:analysis:common:check -x test with the repro added. Through the repro harness: the JUnit on main (assertions on and off), branch_10_6 4ae946e5a9b9, 10.5.2, 10.0.0, 9.12.3 and 9.1.0; two independent on-disk tests on main, 10.5.2 and 9.12.3; main() checks through the released factories on 3.3.0, 3.4.0 (and with luceneMatchVersion 3.3), 3.6.2, 4.0.0 (and with 3.3), 5.0.0, 6.4.0 and 8.11.4; a bounded-exhaustive metamorphic check on main; the diagram's probe on main, stock and with each fix. The cross-model reviews read ea57360912ed, whose synonym package and WordlistLoader equal a9779ef3c193. OpenJDK 25.0.3, Linux aarch64.

Lucene and upstream sources

Finding record
  • a Lucene main checkout lucene/analysis/common/src/java/org/apache/lucene/analysis/synonym/SynonymGraphFilterFactory.java:167-194 (loadSynonyms: strict UTF-8 decoder, new InputStreamReader(loader.openResource(file), decoder) passed straight to parser.parse); SynonymFilterFactory.java:171-197 (same loader, try-with-resources); SolrSynonymParser.java:33 ('Blank lines and lines starting with '#' are comments'), :82-87 (addInternal: readLine, comment test on charAt(0)); WordnetSynonymParser.java:53-54 (synset id = line.substring(2, 11)); lucene/core/src/java/org/apache/lucene/analysis/WordlistLoader.java:302-304 ('// skip initial bom marker' in getLines); lucene/analysis/common/src/test/org/apache/lucene/analysis/synonym/synonyms.txt line 1 (license header with a comma); re-read in the registration check; identical at ea57360912ed (git diff empty)
The finding in 30 seconds
  • lucene/analysis/common/src/java/org/apache/lucene/analysis/synonym/SynonymGraphFilterFactory.java:146 (default WhitespaceTokenizer), :168-194 (loadSynonyms; decoder :171-175; files :188; InputStreamReader + parse :191) @ a9779ef3c193
  • lucene/analysis/common/src/java/org/apache/lucene/analysis/synonym/SynonymFilterFactory.java:171-199 (loadSynonyms; try-with-resources :194-196) @ a9779ef3c193
  • lucene/analysis/common/src/java/org/apache/lucene/analysis/synonym/SolrSynonymParser.java:33 (comment rule), :82-139 (addInternal; charAt(0) test :85; split/unescape/trim/analyze :90-118) @ a9779ef3c193
  • lucene/analysis/common/src/java/org/apache/lucene/analysis/synonym/WordnetSynonymParser.java:53-54 (substring(2, 11)) @ a9779ef3c193
  • lucene/core/src/java/org/apache/lucene/analysis/WordlistLoader.java:297-314 (getLines), :302-304 (skip initial bom marker) @ a9779ef3c193
  • lucene/core/src/java/org/apache/lucene/analysis/AbstractAnalysisFactory.java:279-302 (getWordSet, getLines) @ a9779ef3c193; getLines callers in analysis-common: StemmerOverrideFilterFactory.java:74, MappingCharFilterFactory.java:73, TypeTokenFilterFactory.java:74, WordDelimiterFilterFactory.java:140, WordDelimiterGraphFilterFactory.java:143, PatternTypingFilterFactory.java:94; snowball format: lucene/analysis/common/src/java/org/apache/lucene/analysis/en/AbstractWordsFileFilterFactory.java:87-90, AbstractAnalysisFactory.java:308-328 (own InputStreamReader :322, no BOM skip; registry LFV-718)
Who is affected
  • lucene/analysis/common/src/java/org/apache/lucene/analysis/synonym/SynonymGraphFilterFactory.java:43-52 (schema example), :72 (@lucene.experimental), :93-96 (defaults: ignoreCase false, format, expand true) @ a9779ef3c193
  • lucene/analysis/common/src/java/org/apache/lucene/analysis/synonym/SolrSynonymParser.java:57, WordnetSynonymParser.java:33 (@lucene.experimental) @ a9779ef3c193
  • lucene/analysis/common/src/java/org/apache/lucene/analysis/synonym/SynonymFilterFactory.java:73-78 (@deprecated, @Deprecated) @ a9779ef3c193
  • lucene/analysis/common/src/test/org/apache/lucene/analysis/synonym/synonyms.txt, synonyms2.txt line 1 (license header with a comma) @ a9779ef3c193
Why it looks unintended
  • lucene/analysis/common/src/java/org/apache/lucene/analysis/synonym/SolrSynonymParser.java:33, :85 @ a9779ef3c193; lucene/core/src/java/org/apache/lucene/analysis/WordlistLoader.java:302-304 @ a9779ef3c193
  • lucene/CHANGES.txt:12543 (Bug fixes), :12554 (LUCENE-5505) @ a9779ef3c193; lucene/analysis/common/src/java/org/apache/lucene/analysis/hunspell/Dictionary.java:351-352
Suggested fix
  • lucene/analysis/common/src/java/org/apache/lucene/analysis/synonym/SynonymGraphFilterFactory.java:188-192, SynonymFilterFactory.java:191-197, SolrSynonymParser.java:67-87, WordnetSynonymParser.java:45-54 @ a9779ef3c193
History
  • a Lucene clone: git log a9779ef3c193..645d4fe14a20 over the synonym package and WordlistLoader.java: no commit (6 commits in range; checked 2026-10-28)

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

Repro files

About the diagrams

Every number on screen comes from a probe executed for the diagram on main a9779ef3c193 (stock, with fix F and with fix P), and every code line from the code at that commit.

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.