All findings · Lucene finding · LFV-665 · arabic-stemmer-waw-then-article-not-stripped

ArabicStemmer strips only one prefix: after the conjunction و ("and") the article stays, unlike the light10 algorithm ArabicAnalyzer cites

ArabicAnalyzer cites the light10 stemmer, which strips a leading و ("and") and then the article, but Lucene stops after the و: وبالكتاب ("and with the book") is indexed as بالكتاب, not كتاب, so a search for الكتاب ("the book") finds 0 of 1 documents. Every release since 2.9.0 has it.

Summary

Priority
Low, rank 293A one-language stemming-quality gap unreported since 2.9.0, whose fix changes indexed terms and has an unmeasured net effect, so it is not urgent.
Impact
silent wrong results
Since
2.9.0 (LUCENE-1406, committed 2008-10-20; light10 contract since LUCENE-1758, 2009)introduced 2008-10-20 · over 18 years
Affected
2.9.0 to 10.5.2 and main
Status
documented
Found by
bounded-exhaustive differential check against the paper's published steps

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

Arabic attaches "and" (و) and "with the" or "for the" (بال, لل) to the front of a word. A light stemmer removes them so that every form of a word shares one index term. Lucene's stemmer removes only the first of these pieces it finds.

Summary card: Lucene ArabicStemmer strips only one prefix: after و (and) the article stays. Left panel, وبالكتاب two ways: expected (light10, the paper's section 2.2) وبالكتاب, step 1 removes و, giving بالكتاب, step 2 removes بال, giving كتاب, the term of الكتاب; actual on main: step 1 removes و, giving بالكتاب, and the method returns, so no article is tried again (ArabicStemmer.java:92). Right panel, the search with ArabicAnalyzer: the document وبالكتاب is stored as بالكتاب, the query الكتاب becomes body:كتاب, count = 0 where 1 hit is expected. The same for و + بال, كال, فال, لل; not affected: و + ال and one prefix alone.
The word وبالكتاب ("and with the book") two ways: light10 removes و and then بال and gets كتاب; Lucene's stemPrefix returns after removing و and indexes بالكتاب, so a search for الكتاب ("the book") finds 0 of 1 documents. Select the still to open it at full size. Generated with AI, checked against the code.
  1. Index the document وبالكتاب, "and with the book" (و "and" + بال "with the" + كتاب "book"), with the stock new ArabicAnalyzer(). Its javadoc says it stems "as specified by" the light10 paper.
  2. Light10 runs ordered steps: remove a leading و if 3 or more letters remain, then one article (ال, وال, بال, كال, فال, لل) if 2 or more remain, then suffixes. You expect كتاب, the same term as الكتاب ("the book") and بالكتاب.
  3. You get بالكتاب. ArabicStemmer.stemPrefix keeps the six articles and the و in one list, tried in order, and returns after the first match: it removes the و and never tries بال on the rest.
  4. A TermQuery for the analyzed الكتاب (body:كتاب) finds 0 of 1 documents, with no error. The same happens for و + لل, كال and فال, and for real words such as وبالتالي ("and therefore") becomes بالتال instead of تال.
  5. Only و + ال works, because وال is in the list itself. The code is the same in every release from 2.9.0 (2009) to 10.5.2 and on main.

Who is affected

Everyone who indexes or searches Arabic text with the stock ArabicAnalyzer, ArabicStemFilter or the arabicStem factory. Real words such as وبالتالي, وبالنسبة and وللمرة always get a different term than light10 gives; a hit is missed when query and document use different forms of the word. There is no error; the net effect on ranking quality was not measured.

Mechanism, terms and numbers789 words

ArabicAnalyzer's javadoc says it "implements light-stemming as specified by: Light Stemming for Arabic Information Retrieval" (ArabicAnalyzer.java:38-39). Section 2.2 of that paper runs ordered steps: 1) remove a leading و ("and") if 3 or more characters remain, 2) remove one definite article (ال, وال, بال, كال, فال, لل) if 2 or more remain, 3) go through the suffixes once. ArabicStemmer.stemPrefix instead scans one array, the six articles followed by و (ArabicStemmer.java:46-54), and returns after the first match (:90-94); stem() calls it once (:76-81). So once it removes the و it never tries the articles on the rest: وبالكتاب ("and with the book") is indexed as بالكتاب instead of كتاب, the term of الكتاب and بالكتاب, and the same for و + لل, كال and فال (وللطلاب becomes للطلاب, وبالتالي becomes بالتال). A TermQuery for the analyzed الكتاب then finds 0 of 1 documents that contain only وبالكتاب, with no error. و + ال is unaffected because وال is in the array itself. The single-return loop came with the class (LUCENE-1406, 2008) and was kept when it moved to light10 (LUCENE-1758, 2009), so every release from 2.9.0 to 10.5.2 and main has it. The fix tries the articles again after removing the one-letter و; it changes indexed terms (reindex note needed) and also brings light10's own overstemming for a few words, so no retrieval gain is claimed.

Terms

  • Light stemming: removing a few frequent prefixes and suffixes without a dictionary, so that الكتاب ("the book"), بالكتاب ("with the book") and وبالكتاب ("and with the book") all get the index term كتاب ("book").
  • The prefixes: the conjunction و (wa-, "and") and the definite articles ال (al-, "the"), وال (wa + al-), بال (bi-l-, "with the"), كال (ka-l-, "like the"), فال (fa-l-, "so the"), لل (li-l-, "for the"). They are written joined to the word, and و can come before one of the others.
  • light10: the stemmer of Larkey, Ballesteros and Connell, "Light Stemming for Arabic Information Retrieval", which ArabicAnalyzer cites. Section 2.2 (page 13 of the paper): "1. Remove و (\"and\") ... if the remainder of the word is 3 or more characters long. ... 2. Remove any of the definite articles if this leaves 2 or more characters. 3. Go through the list of suffixes once ...".

The bug

  1. ArabicStemmer.prefixes is one array: ال, وال, بال, كال, فال, لل, و, with و last (ArabicStemmer.java:46-54).
  2. stemPrefix tries the entries in order and returns deleteN(...) on the first that matches (:90-94). stem() calls it once, then the suffixes (:76-81).
  3. For وبالكتاب none of the six articles matches (the word starts with و and then ب, so even وال fails at its second letter); و matches and is removed, and the method returns بالكتاب. بال would match now (7 characters, it needs 5), but nothing tries it again.
  4. The length rules (:118-132: و needs 4 or more characters, the others their length + 2) are exactly the paper's 3 and 2 remaining characters; only the order of steps 1 and 2 was lost when they were folded into one list.
  5. ArabicStemFilter applies the stemmer to every non-keyword token (ArabicStemFilter.java:44-55); ArabicAnalyzer ends its chain with it (ArabicAnalyzer.java:131-142); the SPI factory arabicStem creates the same filter.
  6. A double و before وال (ووال...) is hit the same way, because وال no longer matches at position 0. Words too short for light10's length rules (for example و + بال + one letter) come out the same either way; in the follow-up's probe, 70 of 148,795 waw+article-shaped words are unchanged by the fix.

The numbers

inputlight10 (and main with the fix)main
الكتاب, بالكتاب, والكتاب, وكتاب (controls)كتابكتاب
وبالكتاب (و + بال, "and with the book")كتاببالكتاب
وللطلاب (و + لل, "and for the students")طلابللطلاب
وكالبحر (و + كال, "and like the sea")بحركالبحر
وبالتالي ("and therefore")تالبالتال
وبالنسبةنسببالنسب
وللمرةمرللمر
ArabicAnalyzer("الكتاب بالكتاب وبالكتاب")[كتاب, كتاب, كتاب][كتاب, كتاب, بالكتاب]
one document وبالكتاب, TermQuery on the analyzed الكتاب1 hit0 hits

The rows for وبالتالي, وبالنسبة and وللمرة come from a probe through the default ArabicAnalyzer, and the fact check's probe through ArabicStemFilter alone gives the same terms; the other word rows ran through ArabicStemFilter.

When the query text goes through the same analyzer (for example a query parser over ArabicAnalyzer), the miss goes both ways: the query وبالكتاب gives بالكتاب and misses documents that say الكتاب or بالكتاب. A TermQuery built from a raw string is not analyzed, and ArabicAnalyzer.normalize() does not stem.

Exposure and likelihood362 words
  • Who is exposed. Applications that analyze Arabic text with the stock ArabicAnalyzer (the default constructor included, ArabicAnalyzer.java:96-98), ArabicStemFilter directly, or the arabicStem factory (for example in a CustomAnalyzer). No option turns the behaviour on or off. Tokens marked as keywords (for example by the analyzer's stem exclusion set) are not stemmed, and stopwords are removed before the stemmer.
  • Not affected. و + ال (والكتاب), any single prefix alone, words without a leading و, words too short for light10's length rules, and the separate Snowball Arabic stemmer.
  • What triggers it. Real words of the shape و + بال, كال, فال or لل ("and with the", "and like the", "and so the", "and for the"), for example وبالتالي ("and therefore"), وبالنسبة, وللمرة, and a double و before وال. It is deterministic, at index time and, when the query is analyzed the same way, at query time, and needs no size or setting. How often these words occur in real text was not measured.
  • What the damage looks like. Mostly missed matches between a waw+article form and the other forms of the same word, inside each document's own field: the document وبالكتاب is not found by الكتاب or بالكتاب, and the query وبالكتاب misses them; much less often, extra conflations that light10 would not make (see the next point). No exception and no term longer than its input: a probe over 411,089 words on main (assertions on and off) found neither. The work per token stays a fixed handful of comparisons (from the code), and the index is not damaged.
  • Scale, honestly. Over that bounded, mostly constructed word set the stock stemmer loses 19,859,718 same-stem pairs that light10 would make and keeps 240,799 that light10 would not; over 29 real words it loses 18 pairs and keeps 2 (one right, وكالة/وكالات; one spurious, وبالون/بال). These are counts over a constructed set, not corpus frequencies, and the net retrieval effect of a fix was not measured.
  • Existing indexes keep the old terms until reindexed; after a fix, new query terms and old documents disagree for these words until the field is reindexed.

Minimal repro

The test shows that ArabicStemFilter removes the conjunction waw from وبالكتاب ("and with the book") but then not the article بال, unlike light10 (waw first, then the article).

TestArabicWawThenArticle.java (JUnit test)16 lines
package org.apache.lucene.analysis.ar;

import java.io.StringReader;
import org.apache.lucene.analysis.Tokenizer;
import org.apache.lucene.analysis.core.KeywordTokenizer;
import org.apache.lucene.tests.analysis.BaseTokenStreamTestCase;

public class TestArabicWawThenArticle extends BaseTokenStreamTestCase {

  public void testWawThenArticle() throws Exception {
    Tokenizer tokenizer = new KeywordTokenizer();
    tokenizer.setReader(new StringReader("وبالكتاب"));
    // light10 removes the waw, then the article: expected كتاب, actual بالكتاب
    assertTokenStreamContents(new ArabicStemFilter(tokenizer), new String[] {"كتاب"});
  }
}
Command
# in a copy of this finding's repro folder (repro/LFV-665/, 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.ar.TestArabicWawThenArticle
Expected vs actual
Expected كتاب; actual بالكتاب (term 0 expected:<[]كتاب> but was:<[بال]كتاب>), with -ea and -da.

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

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:<[]كتاب> but was:<[بال]كتاب>
       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.ar.TestArabicWawThenArticle.testWawThenArticle(TestArabicWawThenArticle.java:14)

Why it looks unintended

  • ArabicAnalyzer says it stems "as specified by" the light10 paper, whose steps remove و first, then an article.
  • The light10 code the authors' lab distributed does the same with Lucene's tables: a 10-million-word diff finds 6,072 differences, all و + article, and 0 with the fix.
  • No test, comment or discussion calls stopping after و deliberate.
The full argument443 words
  • The analyzer's contract. ArabicAnalyzer.java:38-39: "This analyzer implements light-stemming as specified by: <i> Light Stemming for Arabic Information Retrieval </i>" (the PDF URL after it is dead).
  • CHANGES of the light10 change (LUCENE-1758, 820620f3a72, 2009, the whole entry): "ArabicAnalyzer now uses the light10 algorithm, has a refined default stopword list, and lowercases non-Arabic text. You'll need to fully re-index any previously created indexes. This isn't a break in back-compatibility because ArabicAnalyzer has not yet been released. (Robert Muir)"
  • The stemmer's javadoc defines stemming as "Removal of attached definite article, conjunction, and prepositions" (ArabicStemmer.java:26-29).
  • The paper, section 2.2 (page 13): "We tried several versions of light stemming, all of which followed the same steps: 1. Remove و (\"and\") for light2, light3, and light8, and light10 if the remainder of the word is 3 or more characters long. ... 2. Remove any of the definite articles if this leaves 2 or more characters. 3. Go through the list of suffixes once ...". Lucene's length rules are exactly these 3 and 2 characters. Its Table 1 (page 14) lists the light10 "prefixes" as ال, وال, بال, كال, فال, لل, و, the same strings in the same order as ArabicStemmer.java:46-54.
  • The lab's distributed code. The Lemur Toolkit's arabic_light10 (University of Massachusetts) does "Remove waw (advance pointer) if remainder >= 3 chars", then "Remove definite article if remainder >=2 chars", then the suffixes; its article and suffix tables equal Lucene's. A diff of Lucene against a Java port of its stemming steps (Lemur's normalization and stopword steps left out, as ArabicStemFilter does neither), every word up to 6 letters over 13 letters plus 5 million random longer words (10,229,043 words), finds 6,072 differences, every one و followed by an article, and 0 with the fix. It is the light10 code the lab distributed, not necessarily the code of the paper's experiments (which ran on an in-house engine).
  • The original author's aim (LUCENE-1406, #2480): "the major motivation for implementing it exactly the way it is presented in the paper is that the algorithm is TREC-tested".
  • Tests. TestArabicStemFilter checks each prefix alone (والحسن, بالحسن, كالحسن, فالحسن, للاخر, وحسن), و plus suffixes and two suffixes; none has و followed by an article other than وال, and none asserts that such an article is kept.
  • History. Neither LUCENE-1406 nor LUCENE-1758 discusses stopping after one prefix; no CHANGES or MIGRATE entry mentions it. (The 2008 light8 code cited an earlier paper by the same authors, "Improving Stemming for Arabic Information Retrieval: Light Stemming and Co-occurrence Analysis" (2002), which was not checked here; the light10 paper says light8 followed the same steps.)
The devil's-advocate check242 words

The intended-behaviour checks built the strongest case for "works as designed" (rated moderate by the cross-model reviewer): the helper's javadoc says "Stem a prefix off" (singular) while the suffix helper says "Stem suffix(es) off"; the array copies the paper's Table 1 column, which lists و next to the articles and also وال, so "remove one Table 1 prefix" is a possible reading; the output has been stable since 2009 with no report; a fix forces a reindex; on LUCENE-8028 a committer asked to be conservative about changing the default; ArabicStemmer is package-private and ArabicStemFilter only promises to "apply" it; and the fix brings light10's own overstemming (وكالات, "agencies", becomes ات instead of كال).

Why it fails: the public contract is the analyzer's "as specified by" and the CHANGES "now uses the light10 algorithm", and the paper's steps are explicitly separate and ordered. The lab's own light10 code keeps وال in its article table and still removes و first, so the Table 1 reading does not hold. The original author said they followed the paper "exactly", and the length rules do. Nothing records stopping after و as a decision. The reindex cost and the overstemming are real, but they are a compatibility and quality decision for maintainers, not evidence that the current output is the documented one; the overstemming is the paper's own behaviour, and stock already overstems the same way without the و (بالون gives ون).

Suggested fix

Recommended After removing و, try the articles once more

After stemPrefix removes the one-letter و, try the six articles once more (+15/-2 in ArabicStemmer.java). With it the repro passes and the module's full check passes (1,915 tests). Indexed terms change, so the CHANGES entry needs a reindex note, and a few words overstem as light10 does (وكالات becomes ات).

All options and trade-offs4 options · 777 words

Two equivalent code fixes were validated (A through gradle and the module's check, B with the standalone runner); C and D are the alternatives a maintainer may prefer.

After removing و, try the articles once more (validated by the checks) (recommended)

In ArabicStemmer.stemPrefix, when the matched prefix is the one-letter و, try the six articles on the rest before returning (+15/-2 as formatted by tidy; the same logic as the finder's +9/-1 sketch). A regression test: the repro's class, or its cases added to TestArabicStemFilter.

int stemPrefix(char[] s, int len) {
  for (int i = 0; i < prefixes.length; i++) {
    if (startsWithCheckLength(s, len, prefixes[i])) {
      len = deleteN(s, 0, len, prefixes[i].length);
      // light10 removes the conjunction waw (step 1) before a definite article (step 2):
      // after removing waw, try the articles once more
      if (prefixes[i].length == 1) {
        for (int j = 0; j < prefixes.length - 1; j++) {
          if (startsWithCheckLength(s, len, prefixes[j])) {
            return deleteN(s, 0, len, prefixes[j].length);
          }
        }
      }
      return len;
    }
  }
  return len;
}

Trade-offs

  • Changes indexed terms for waw+article words long enough for light10's length rules: the CHANGES entry should advise reindexing fields analyzed with ArabicAnalyzer, ArabicStemFilter or arabicStem; it suits main (11.0). (LUCENE-1758's reindex note of 2009 is no precedent: it came before the analyzer's first release.)
  • Brings light10's own overstemming where the و is a root letter followed by letters that look like an article: وكالات ("agencies") becomes ات instead of كال, so it no longer conflates with وكالة ("agency", كال on both); وبالون becomes ون, as بالون already does on stock, instead of بال. That is the paper's behaviour; no retrieval gain is claimed.
  • Relies on و being the only one-letter entry and the last one in prefixes, and on every entry before it being a light10 article (true today, ArabicStemmer.java:46-53).
  • Cost: at most 6 more 2-3-character comparisons and one more in-place shift for a token that starts with و.

What the checks validated

  • Scratch worktree at a9779ef3c193, gradle, JDK 25: the new test fails 5 of 6 on stock and passes 6 of 6 with the fix; the 5 Arabic suites pass 56 of 56 with assertions on and off (TestArabicStemFilter 23, TestArabicNormalizationFilter 15, TestArabicAnalyzer 8, TestArabicFilters 4, the new 6); :lucene:analysis:common:check passes: checkGoogleJavaFormat, ecjLint, forbiddenApis and the whole module's tests (340 suites, 1,915 tests, 0 failed, 18 skipped).
  • An exhaustive diff against a light10 reference written from the paper (every word up to 6 letters over 13 letters, 5,229,042 words): 0 mismatches with the fix, 3,042 on stock. The diff against the lab's code port (10.2 million words) gave 0 with the finder's equivalent sketch.

Write the two light10 steps out (validated with the standalone runner)

Step 1: if the word starts with و and has 4 or more characters, remove it. Step 2: try the six articles once (+10/-2).

int stemPrefix(char[] s, int len) {
  // light10 step 1: remove the conjunction waw if 3 or more characters remain
  if (len >= 4 && s[0] == WAW) {
    len = deleteN(s, 0, len, 1);
  }
  // light10 step 2: remove one definite article if 2 or more characters remain
  for (int i = 0; i < prefixes.length - 1; i++) {
    if (startsWithCheckLength(s, len, prefixes[i])) {
      return deleteN(s, 0, len, prefixes[i].length);
    }
  }
  return len;
}

Trade-offs

Reads like the paper and gives the same output as A on the tested space (for a word that starts with وال, removing و and then ال removes the same 3 characters as وال alone, under the same length rules). It still loops to prefixes.length - 1, so it relies on و staying the last entry; moving و out of the array would be cleaner but touches more lines. Same reindex and overstemming notes as A.

What the checks validated

The doc's test 6 of 6 and the upstream TestArabicStemFilter, TestArabicAnalyzer and TestArabicFilters 35 of 35 with it on the classpath; the exhaustive diff (5,229,042 words) finds 0 mismatches. Not run through gradle tidy or check.

Document the current behaviour

Say in the ArabicAnalyzer and ArabicStemmer javadocs that at most one prefix is removed, so و + بال, كال, فال, لل keep the article.

Trade-offs

No reindex, but the analyzer would no longer implement the algorithm it cites, and the missed matches stay.

Make the light10 order opt-in

Keep the default and add a switch (a constructor flag on ArabicStemFilter, a factory parameter). Not built.

Trade-offs

Avoids silent term changes for existing indexes, but adds API surface for what the docs already promise, and the default stays different from light10.

How it was found

A bounded-exhaustive check ran the stock ArabicStemFilter and an independent reference written from section 2.2 of the cited paper over 408,800 constructed words. They differed on 119,138, every one a و before an article, and on none with the fix. The finder then reduced it to a public-API test and read the code and its history.

The method in full178 words

Method: bounded-exhaustive differential checking against a reference written from the published rules (the study's "semi-formal" tier). Hunt unit "light stemmers against their published rules", 2026-10-27, main a9779ef3c193.

  • The reference implements light10 steps 1-3 from section 2.2 of the paper that ArabicAnalyzer cites, independently of Lucene's code.
  • Word space: 14 prefix chains x 2,380 stems (length 0 to 3 over 13 letters: the 11 letters of the affix tables plus 2 neutral letters, د and س) x 13 suffix chains = 433,160 combinations, which the harness deduplicates to 408,800 distinct words, each run through the stock ArabicStemFilter and the reference.
  • Result: 119,138 mismatches, every one with a و before an article (و + بال, كال, فال, لل: 4 x 29,744; a double و before وال: 162); 0 mismatches with the fix sketch on the classpath.
  • An AI finder then wrote the public-API JUnit and traced the cause in stemPrefix and the git history; independent AI tools reproduced it, built their own exhaustive diffs (one against the light10 code the authors' lab distributed) and judged it (see the checks).

Prior reports, history and checks

Prior reports

No prior report: 134 scripted prior-report search queries in two runs (GitHub, mailing lists, JIRA, the web in English and Arabic) plus manual searches (2026-10-27); the final run's 565 hits include none of this bug. Related: LUCENE-8028 / lucene-solr PR #274, an unmerged opt-in stemmer whose own table covers و + article; it never calls the default stemmer wrong.

Searches5 searches

No report of this defect was found. The closest item is related work: a maintainer may answer "use or revive that stemmer".

What was searched, when, and what turned up
WhereWhenHowFound
scripted prior-report search (two runs)2026-10-2751 and 83 queries: GitHub issues, PRs and commits, downstream projects, the dev, java-user, issues and commits lists, JIRA and 8 web queries in English and Arabicfinal run: 565 distinct hits, 0 high, 20 medium; none reports this bug
GitHub (manual)2026-10-27apache/lucene "arabic" (all 59 titles read), ArabicStemmer, ArabicStemFilter, ArabicAnalyzer, open and closed PRs; apache/lucene-solr; Elasticsearch, OpenSearch, Solr, Lucene.NET; all of GitHub; the trackers of two portsno report; no open PR changes stemPrefix
JIRA archive and mailing lists2026-10-27LUCENE and SOLR (arabic, waw, prefix, ArabicStemmer); java-user@, dev@, solr-user@, users@solr (four Arabic threads read in full)no report
Related items (read)2026-10-27LUCENE-8028 / #9076 and lucene-solr PR #274; LUCENE-1406 / #2480; LUCENE-1758 / #2832; tomoya55/ar-stemmer#4a separate opt-in stemmer whose table covers و + article, never calling the default wrong; a 2008 remark that light8 misses "many common 'prefix'/suffix combinations", naming no waw+article case; the light10 change; another contributor's port that stopped looping over prefixes because it overstemmed
History2026-10-27git log on ArabicStemmer.java and TestArabicStemFilter.java; lucene/CHANGES.txt on mainno behaviour change to stemPrefix since 2009

History

Introduced with the class by LUCENE-1406 (8dfe073760c, 2008-10-20) and kept by the light10 change LUCENE-1758 (820620f3a72, 2009-08-05), both before the first release, 2.9.0; never changed since. It fails on 16 releases from 2.9.0 to 10.5.2 and on main; 2.4.1 has no Arabic package.

Versions tested7 rows

Introduced by 8dfe073760c (LUCENE-1406, #2480; patch by rmuir, committed by gsingers as svn r706342, 2008-10-20), light8: stem() calls stemPrefix() once and it returns after the first match; the loop is already in the contributor's patch. From then on و + بال, كال and فال keep their article. 820620f3a72 (LUCENE-1758, #2832, rmuir, 2009-08-05) moved to light10: it added the لل entry just before و, the light10 javadoc and the CHANGES line, and left the loop alone, so و + لل is affected too.

Both landed before 2.9.0 (2009-09-24), the first release with the Arabic package, so no release ever shipped a different variant. Later commits on these lines (LUCENE-2503 rename, LUCENE-9570 reformat, LUCENE-10003 array syntax, LUCENE-10561 visibility) keep the behaviour. All 136 release tags in the history clone that contain the light10 commit have the same code; upstream main a95c754ee003 (read 2026-10-27) still has it.

Versions and branches executed, with results
Version or commitVerdictResult
main a9779ef3c193 affectedthis doc's test 5 of 6 fail (gradle and the standalone runner, assertions on and off); the finder's test 2 of 3; the stemmer and filter probes 4 of 4
10.5.2, 9.12.3 affectedthis doc's test 5 of 6 fail; the finder's test 2 of 3; the probes 4 of 4
9.1.0, 10.0.0 affectedthe finder's test 2 of 3 fail (10.0.0 also the stemmer probe)
3.0.3, 3.1.0, 3.6.2, 4.0.0, 4.10.4, 5.5.5, 6.6.6, 7.7.3, 8.11.4, 9.0.0 affectedthe stemmer probe (4 controls; not on 3.1.0) and/or the public filter probe (3 controls; 3.1.0, 3.6.2, 4.10.4 to 8.11.4): 4 of 4 waw+article words keep the article in each run, the controls stem
2.9.0, 2.9.4 affectedthe stemmer probe: 4 of 4; the 2.9 filter probe (2 controls, 2 waw+article words): 2 of 2, and ArabicAnalyzer gives [كتاب, كتاب, بالكتاب]
2.4.1 no APIno Arabic package: the probe does not compile
main with the fix fixthis doc's test 6 of 6 and the probes pass

Independent checks

An independent reproducer, a devil's advocate and an adjudication found it real and re-ran it on main with assertions on and off; two executed reviews by another model family agreed (real, high). A later audit and this doc re-ran it, the doc through gradle with the fix and the module's full check.

Checks and verdicts517 words

Each check wrote or ran its own tests against the stock jars at a9779ef3c193 (the cross-model reviews read ea57360912ed, whose analysis sources are identical, and their test was re-run on a9779ef3c193).

Each independent check and its verdict
CheckVerdictWhat it found
finder candidateExhaustive diff against the paper's steps (408,800 words, 119,138 mismatches, all و + article); JUnit fails 2 of 3 on main (on and off), 10.5.2 and 9.12.3; passes with the fix; upstream Arabic tests 31 of 31 with it.
Independent reproducer realOwn JUnit written from the claim only: 5 of 6 fail on main (on and off), 10.5.2 and 9.12.3, including the 0-hit search; 6 of 6 with the fix. Own exhaustive diff (5.2 million words): 3,042 mismatches, all و + article, 0 with the fix. This doc's test is based on it.
cross-model review, refute lens real (high)An independent reviewer from another model family (OpenAI GPT-5.6, executed mode) wrote its own test (وبالصقر gives بالصقر): fails 2 of 3 with assertions on and off and over 3 iterations, passes with the fix; root cause agreed.
cross-model review, intended lens real (high)Built the works-as-designed case (singular javadoc, one-at-a-time tests, stable output, reindex cost) and rated it moderate; rejected because the analyzer's javadoc and CHANGES promise the paper's light10.
The cross-model review re-runs reproducedThe reviewer's test re-run on a9779ef3c193 (assertions on and off), 10.5.2 and 9.12.3: 2 of 3 fail; 3 of 3 pass with the fix.
Devil's advocate real (high)Read the lab's light10 code: و first, then the article, with Lucene's tables; a diff against a port of it (10.2 million words) finds 6,072 mismatches, all و + article, 0 with the fix. Flagged the overstemming and LUCENE-8028.
adjudication confirmed (high)Re-ran the independent reproduction's test: 5 of 6 fail with assertions on and off, 6 of 6 with the fix; read section 2.2 and the lab's code; duplicate of none; framing and reindex caveats.
Follow-up new, security noneHistory runs on 16 releases plus main, prior-report search (new), security probe (411,089 words, no exception, no term longer than its input).
audit 4 keepRe-ran the finder's test with a fresh seed: 2 of 3 fail with assertions on and off; code, paper and the lab's code re-read; impact kept.
this doc reproduced, fix validatedThe doc's test through gradle on stock (5 of 6 fail, on and off) and with fix A (6 of 6); the module's full check with fix A (1,915 tests pass); the standalone runner on main, 10.5.2, 9.12.3 and both fixes.
fact checks of this doc reproduced; corrections appliedAn independent fact check re-ran the doc's gradle command (5 of 6 fail, assertions on and off), fix A with the module's check (1,915 tests pass) and the standalone runner on main, 10.5.2 and 9.12.3 with assertions on and off. A second one, by another model family (OpenAI GPT-5.6, read-only), re-read the sources and logs. Both found the bug as described; their counting, quoting and wording corrections are applied.

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,409 words

Finding record

LFV-665, snapshot of 2026-11-06

Registry title
ArabicStemmer removes at most one prefix, so after it strips the conjunction waw it never strips the article that follows (wa+bi-al-, wa+li-l-, wa+ka-al-, wa+fa-al-), unlike the light10 algorithm ArabicAnalyzer documents
Area
analysis-common / Arabic light stemmer (ArabicStemmer.stemPrefix; ArabicStemFilter, ArabicAnalyzer and the 'arabicStem' factory)
Priority
Low, rank 293: A one-language stemming-quality gap unreported since 2.9.0, whose fix changes indexed terms and has an unmeasured net effect, so it is not urgent.
Impact class
silent wrong results
Impact note
Missed matches (plus a few accidental extra conflations), with no error: every application that indexes or queries Arabic text through ArabicAnalyzer (default constructor included), ArabicStemFilter or the 'arabicStem' factory, on every release from 2.9.0 through 10.5.2 and on main, gets the article form instead of the stem for words that start with the conjunction waw followed by bi-al-, li-l-, ka-al- or fa-al- (for example 'and with the book' is indexed as 'with-the-book', not 'book'), so a search for 'the book' misses a document that only contains 'and with the book'; wa+al- is not affected, no scale is needed, the result is the same with -ea and -da, the net retrieval effect of a fix was not measured (the paper-faithful fix also overstems a few words), and because a fix changes indexed terms, affected fields need reindexing after it.
Summary
You index Arabic text with the stock ArabicAnalyzer, whose javadoc says it implements light stemming 'as specified by' the light10 paper; that paper removes a leading waw ('and') first and then a definite article, so 'وبالكتاب' ('and with the book') should stem to 'كتاب' ('book'), like 'الكتاب' and 'بالكتاب'. Instead ArabicStemmer.stemPrefix scans one list (the six articles, then waw) and returns after the first match, so once it has removed the waw it never tries the articles: 'وبالكتاب' becomes 'بالكتاب' and 'وللطلاب' becomes 'للطلاب'. A TermQuery for the analyzed form of 'الكتاب' then finds 0 of 1 documents that contain only 'وبالكتاب', with no error.
Introduced
8dfe073760c5f97a980390ae5daad3a881fd7730 (LUCENE-1406 'new Arabic Analyzer (Apache license)', GitHub #2480; patch by rmuir, committed by gsingers as svn r706342, 2008-10-20) [read: history clone, git log -L on stemPrefix, and the contributor's LUCENE-1406.patch, which already has the single-return loop]: stem() calls stemPrefix() once and stemPrefix() returns after the first match in one list {al, wal, bal, kal, fal, waw}, so wa+bal-, wa+kal- and wa+fal- keep their article from this commit on. 820620f3a7224c6416d001984570897682112676 (LUCENE-1758 'improve arabic analyzer: light8 -> light10', GitHub #2832, rmuir, 2009-08-05) added the lil- entry just before waw, the light10 javadoc citation and the CHANGES line 'ArabicAnalyzer now uses the light10 algorithm', without changing the loop, so wa+lil- is affected from then on. Not a regression: later commits on these lines (3241eb9291c LUCENE-2503 rename, 6faa4f98e04 LUCENE-9570 reformat, 8ac26737913 LUCENE-10003 array syntax, 694d797526a LUCENE-10561 visibility) preserve behaviour. (2008-10-20 (light10 contract and the lil- entry 2009-08-05; first released 2009-09-24 in 2.9.0))
First affected version
2.9.0 [exec] (tag releases/lucene/2.9.0, 2009-09-24, the first release with the org.apache.lucene.analysis.ar package; it contains both commits). The last release before it, 2.4.1, has no Arabic package [exec: the probe does not compile, class ArabicStemmer not found], so no release ever shipped a different variant.
Affected versions
Every release that ships the Arabic analysis API, 2.9.0 through 10.5.2, and main a9779ef3c193; not fixed. [exec] a same-package stemmer probe, a public ArabicStemFilter/ArabicAnalyzer probe, or both (4 of 4 waw+article words keep the article, 4 controls stemmed) fail on 2.9.0, 2.9.4, 3.0.3, 3.1.0, 3.6.2, 4.0.0, 4.10.4, 5.5.5, 6.6.6, 7.7.3, 8.11.4, 9.0.0, 9.12.3, 10.0.0, 10.5.2 and main, and the stemmer probe passes on main with the fix shadowed; the finder's JUnit fails 2 of 3 on 9.1.0, 9.12.3, 10.0.0, 10.5.2 (-ea and -da) and main (-ea and -da). [read] all 136 release tags in the history clone that contain 820620f3a72 have the same prefix list, stem() and stemPrefix() bodies and length rules (the 8.9-8.11.x tags are missing from the clone; 8.11.4 was executed); upstream main a95c754ee003 still has the single-return stemPrefix (read 2026-10-27).
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 (searched 2026-10-27, read-only; the prior-report record, verdict new): two scripted prior-report searches (the finder's 51 queries and the follow-up's 83 queries over GitHub issues and PRs, commits, downstream repositories, the dev, java-user, issues and commits lists, JIRA and 8 web queries in English and Arabic; 565 distinct hits, 0 high, 20 medium), manual GitHub searches (apache/lucene 'arabic' with all 59 titles read, ArabicStemmer, ArabicStemFilter, ArabicAnalyzer; apache/lucene-solr; Elasticsearch, OpenSearch, Solr, Lucene.NET; all of GitHub; the trackers of two ports), JIRA (LUCENE, SOLR), the Lucene and Solr user lists, and the file's full git history. No issue, PR, JIRA ticket, list thread, commit or web page reports that ArabicStemmer stops after the waw and keeps the following article, or that it departs from the light10 step order; no open PR changes stemPrefix. Related, not the same bug: LUCENE-8028 / #9076 (open, 2017) and lucene-solr PR #274 (open, never merged) propose a separate, opt-in dictionary-assisted stemmer from other papers whose compound-prefix table includes وبال, وكال, وفال and ولل; neither says the default stemmer departs from light10 (a maintainer asked to keep the default unchanged and add the new stemmer as an option). LUCENE-1406 / #2480 has a 2008 remark by its contributor (rmuir) that 'many common prefix/suffix combinations are not stemmed by light8 algorithm', a coverage remark that names no waw+article case. An outside Ruby port (tomoya55/ar-stemmer#4, 2016) dropped looping over all prefixes because it overstemmed; not about Lucene. Gaps: academic papers that evaluate Lucene's analyzer were not searched exhaustively; some Stack Overflow and blog pages could not be fetched.
Lucene commit tested
main a9779ef3c193 (commit of 2026-10-26; tested 2026-10-27) through gradle in a scratch worktree and through the standalone JUnit runner, with assertions on and off; the release jars 10.5.2 and 9.12.3 (this doc's test); 2.9.0, 2.9.4, 3.0.3, 3.1.0, 3.6.2, 4.0.0, 4.10.4, 5.5.5, 6.6.6, 7.7.3, 8.11.4, 9.0.0, 9.1.0 and 10.0.0 with the follow-up's probes or the finder's test; 2.4.1 (no Arabic package). Fix A was validated in the scratch worktree with the module's check task (1,915 tests); fix B with the standalone runner.

Lucene and upstream sources

Finding record
  • lucene/analysis/common/src/java/org/apache/lucene/analysis/ar/ArabicStemmer.java:21-32 (at a9779ef3c193) (class javadoc: 'Removal of attached definite article, conjunction, and prepositions'), :46-54 (prefixes {ال, وال, بال, كال, فال, لل, و}, waw last), :76-81 (stem(): stemPrefix once, then stemSuffix), :83-94 (stemPrefix: 'Stem a prefix off'; returns deleteN(...) on the first match), :96-108 (stemSuffix goes through the suffix list once), :118-132 (startsWithCheckLength: waw needs len >= 4, the others prefix length + 2)
  • lucene/analysis/common/src/java/org/apache/lucene/analysis/ar/ArabicAnalyzer.java:35-39 (at a9779ef3c193) (javadoc: 'implements light-stemming as specified by: Light Stemming for Arabic Information Retrieval'), :95-98 (default constructor), :130-143 (createComponents: StandardTokenizer, LowerCaseFilter, DecimalDigitFilter, StopFilter, ArabicNormalizationFilter, ArabicStemFilter); lucene/analysis/common/src/java/org/apache/lucene/analysis/ar/ArabicStemFilter.java:26-27, :44-55 (applies ArabicStemmer to every non-keyword token); lucene/analysis/common/src/java/org/apache/lucene/analysis/ar/ArabicStemFilterFactory.java:41 (SPI name 'arabicStem')
  • GitHub, read-only (2026-10-27): apache/lucene #2480 (LUCENE-1406 comments), #9076 (LUCENE-8028 body and comments), apache/lucene-solr PR #274 (description), apache/lucene main a95c754ee003 ArabicStemmer.java (stemPrefix unchanged; last commits 51b3a5baa73e #14804, 694d797526ae LUCENE-10561)
The finding in 30 seconds
  • lucene/analysis/common/src/java/org/apache/lucene/analysis/ar/ArabicStemmer.java:46-54 (at a9779ef3c193) (prefixes), :76-81 (stem), :90-94 (stemPrefix), :103-108 (stemSuffix), :118-132 (startsWithCheckLength)
  • lucene/analysis/common/src/java/org/apache/lucene/analysis/ar/ArabicAnalyzer.java:38-39 (at a9779ef3c193) (javadoc), :131-142 (createComponents), :146-150 (normalize, no stemming); ArabicStemFilter.java:44-55; ArabicStemFilterFactory.java:41, :57-58
Who is affected
  • lucene/analysis/common/src/java/org/apache/lucene/analysis/ar/ArabicAnalyzer.java:96-98 (at a9779ef3c193) (default constructor), :109-119 (stem exclusion set), :131-142 (:136 StopFilter before the stemmer, :139-141 SetKeywordMarkerFilter); ArabicStemFilter.java:47 (keyword tokens skipped); ArabicStemmer.java:90-94 and StemmerUtil.deleteN (work per token)
  • lucene/analysis/common/src/java/org/apache/lucene/analysis/ar/ArabicStemFilterFactory.java:41 (at a9779ef3c193) (SPI name arabicStem); lucene/analysis/common/src/java/org/tartarus/snowball/ext/ArabicStemmer.java (at a9779ef3c193) (separate Snowball stemmer)
Why it looks unintended
  • lucene/analysis/common/src/java/org/apache/lucene/analysis/ar/ArabicAnalyzer.java:38-39; (at a9779ef3c193) ArabicStemmer.java:26-29, :83-94 ('Stem a prefix off'), :96-108 ('Stem suffix(es) off'), :118-132
  • lucene/analysis/common/src/test/org/apache/lucene/analysis/ar/TestArabicStemFilter.java:31-57, (at a9779ef3c193) :99-105

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

Repro files

About the diagrams

A light, step-by-step picture of one concrete example (the document وبالكتاب, the query الكتاب), made in parallel with this doc. Every term, scan result and hit count on screen comes from an executed probe on main a9779ef3c193 (stock and with the fix sketch), and every numbered code line is checked against the code at that commit. The PNG is the final summary card.

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.