All findings · Lucene finding · LFV-351 · facetsets-adoc-matcher-counts-documents-not-sets

FacetSets.adoc says each facet-set matcher counts documents, but MatchingFacetSetsCounts counts matching facet sets

The facet-set guide says MatchingFacetSetsCounts counts how many documents each matcher matches, but the code adds 1 per matching facet set: one document with (Thriller, 2020) and (Thriller, 2021) gives a "Thriller 2020-2022" matcher 2, while the total is 1. Tests pin the per-set count; the docs need fixing.

Summary

Priority
Low, rank 1229This is a documentation-only defect: FacetSets.adoc says matchers count documents while the intended code counts sets, which can mislead facet-set users on ordinary data; it is released and plausible.
Impact
minor
Since
9.3.0 (#841, 2022): the text and the code have disagreed from the startintroduced 2022-06-25 · about 4 years
Affected
every release from 9.3.0 to 10.6.0, and main
Status
documented
Found by
non-formal: code reading while modelling facet counts, then a JUnit test

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

The finding in 30 seconds

For each hit document, MatchingFacetSetsCounts decodes every facet set and asks every matcher about it; a matcher that accepts k sets of one document gets k. The total, FacetResult.value, counts that document once. Nothing remembers that a matcher already counted the document.

Background: facet sets

  1. A facet set is a tuple of dimension values, such as (genre, year). A FacetSetsField stores a list of them in one document; the guide's motivating example is one document per actor, whose (genre, year) combinations it indexes as facet sets (FacetSets.adoc:36-38, :123-124).
  2. At search time MatchingFacetSetsCounts takes a list of matchers: an ExactFacetSetMatcher accepts one tuple, a RangeFacetSetMatcher a box of ranges, such as genre Thriller and years 2020 to 2022.
  3. It returns a FacetResult: value is the number of matching documents, plus one label value per matcher.
Summary frame titled 'MatchingFacetSetsCounts counts matching facet sets, but the guide FacetSets.adoc says documents'. One document with IntFacetSet(1, 2020) and IntFacetSet(1, 2021) and the matcher 'Thriller 2020-2022'. Four cards: the user indexes one document with two (genre, year) sets in one FacetSetsField; the guide (FacetSets.adoc:119-120) says the class counts how many documents each FacetSetMatcher matches, so expect 1; the code runs counts[j]++ for every matching set (+2) and totCount++ once per document (+1), giving label value 2 and value 1; the code is intended (class javadoc, demo test asserts 4 for 2 documents), so the fix rewords the guide and the demo javadoc. Expected 1, actual 2 with FacetResult.value 1; documentation defect, minor; since #841, first released in 9.3.0.
One actor document with two (genre, year) facet sets: the "Thriller 2020-2022" matcher counts both sets (2), while FacetResult.value counts the document once (1). Select the still to open it at full size. Generated with AI, checked against the code.

The finding, step by step

  1. You index one actor document whose FacetSetsField holds two (genre, year) facet sets, (Thriller, 2020) and (Thriller, 2021), genre encoded as 1: the guide's movie-actors example, indexed as facet sets as the guide itself assumes.
  2. You count with MatchingFacetSetsCounts and one RangeFacetSetMatcher "Thriller 2020-2022". FacetSets.adoc says it "counts how many documents are matched by each of a list of FacetSetMatcher", so you expect 1.
  3. You get 2 for the matcher, while FacetResult.value, the number of matching documents, is 1. The bundled demo does the same: 2 city documents give "Eighty to Hundred Degrees" 4.
  4. Why: for each document the code loops over its facet sets and adds 1 to every matcher that accepts the set (counts[j]++), but adds 1 to the total only once per document.
  5. The per-set count looks intended: the class javadoc says "counts for each given FacetSet" and the demo test asserts 4 for 2 documents. So the text is what should change, not the code.

Who is affected

Facet-set users whose hit documents hold two or more sets one matcher accepts: a range matcher when a document has several sets inside its ranges, as in the demo, or an exact matcher on a repeated set. Read as documents, as the guide says, the value is too high; hits and total are right. Opt-in, @lucene.experimental.

Mechanism, terms and numbers525 words

The facet-set user guide, lucene/facet/docs/FacetSets.adoc, which the package javadoc points to, says MatchingFacetSetCounts is "A Facets implementation which counts how many documents are matched by each of a list of FacetSetMatcher, in addition to counting the total number of documents that were matched", and its motivating table counts actors, one document each. The code adds 1 to a matcher's count for every facet set of a hit document that the matcher accepts, and adds 1 to the total (FacetResult.value) once per document. So one document holding (Thriller, 2020) and (Thriller, 2021) gives a "Thriller 2020-2022" range matcher 2 while FacetResult.value is 1. The bundled demo shows the same, 4 for 2 city documents, under a javadoc that says "Counting documents", and its upstream test asserts the 4. The per-set count is pinned by upstream tests and very likely intended, so this is a documentation defect: the guide and the demo javadoc should say that each matcher counts matching facet sets. It has been so since facet sets first shipped in 9.3.0.

The text (main 645d4fe14a20)

  • FacetSets.adoc:119-120: "A Facets implementation which counts how many documents are matched by each of a list of FacetSetMatcher, in addition to counting the total number of documents that were matched." The heading above it (:117) spells the class MatchingFacetSetCounts.
  • The guide's motivation counts actors: "show how many actors appeared in each year or genre" (:16) and the table "Number of Actors performing in movies by Genre and Year" (:27), one document per actor (:13-23); the guide indexes each actor's (genre, year) combinations as facet sets (:36-38, and "indexed genre and year facet sets" in that example, :123-124). package-info.java:19-20 sends users to this guide.
  • CustomFacetSetExample.java:206: "Counting documents which match a certain degrees value for any date."

The code (MatchingFacetSetsCounts.java)

for (int doc = it.nextDoc(); doc != DocIdSetIterator.NO_MORE_DOCS; doc = it.nextDoc()) {
  boolean shouldCountDoc = false;
  ...
  for (int start = Integer.BYTES; start < bytesRef.length; ) {   // every facet set of the document
    start += facetSetDecoder.decode(bytesRef, start, dimValues);
    for (int j = 0; j < facetSetMatchers.length; j++) {           // every matcher
      if (facetSetMatchers[j].matches(dimValues)) {
        counts[j]++;                                              // :128, once per matching SET
        shouldCountDoc = true;
      }
    }
  }
  if (shouldCountDoc) {
    totCount++;                                                   // :134, once per DOCUMENT
  }
}

Label values are counts[i] and FacetResult.value is totCount (:150-154, :194). A matcher sees one set at a time: matches(long[] dimValues), "the encoded dimension values of a single facet set in this field" (FacetSetMatcher.java:49-54).

The numbers

  • The repro: one document with (1, 2020) and (1, 2021), matcher "Thriller 2020-2022": label value 2, FacetResult.value 1.
  • The demo: 2 city documents with 3 temperature readings each, 2 of them in 80-100 degrees (CustomFacetSetExample.java:85-105): "Eighty to Hundred Degrees" 4, value 2, asserted by TestCustomFacetSetExample.java:54, :57.
  • An exact matcher is enough when a document repeats a set: FacetSetsField.create packs the sets as given (FacetSetsField.java:52-75), and one document holding (1, 2020) twice gives ExactFacetSetMatcher "Thriller 2020" 2 (a study verifier's run).
Exposure and likelihood228 words
  • Who: users of MatchingFacetSetsCounts with ExactFacetSetMatcher, RangeFacetSetMatcher or a custom matcher. Several sets per document is the documented shape (FacetSets.adoc:98-99: a field "which lets you index a list of FacetSet"; FacetSetsField.java:23-26). A matcher that accepts at most one set of each hit document gives the document count (the controls of the independent reproducer and of the executed cross-model review pass).
  • Default vs opt-in: opt-in, experimental module API; no codec, flag or scale condition. The smallest case is one document with two sets.
  • Effect: silent. A facet UI that labels the matcher's value as actors or documents shows a number larger than the number of documents the matcher matched whenever a hit document has two or more sets it accepts; with one matcher it is then also larger than FacetResult.value. The hit list, the total and which documents are counted are correct.
  • No stock alternative: the constructors take only the field, the hits, the decoder, an optional fastMatchQuery and the matchers (MatchingFacetSetsCounts.java:52-85), so an application that needs per-document counts must write its own Facets.
  • Likelihood: plausible but limited. Facet sets are niche; a GitHub code search for the class name found no use of MatchingFacetSetsCounts in the Elasticsearch, OpenSearch or Solr repositories (2026-10-26, repeated 2026-11-01; a control query returned hits), and the prior-report searches found no report since 9.3.0 (2022).

Minimal repro

One JUnit test, public API only: one document with the facet sets (1, 2020) and (1, 2021), one RangeFacetSetMatcher "Thriller 2020-2022", and the guide's promise as the assertion (1 document). Deterministic, single thread, the same failure with assertions on and off.

TestMatchingFacetSetsCountsPerDocument.java59 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.facet.facetset;

import org.apache.lucene.document.Document;
import org.apache.lucene.facet.FacetResult;
import org.apache.lucene.facet.FacetsCollector;
import org.apache.lucene.facet.FacetsCollectorManager;
import org.apache.lucene.index.DirectoryReader;
import org.apache.lucene.index.IndexWriter;
import org.apache.lucene.index.IndexWriterConfig;
import org.apache.lucene.search.IndexSearcher;
import org.apache.lucene.search.MatchAllDocsQuery;
import org.apache.lucene.store.Directory;
import org.apache.lucene.tests.util.LuceneTestCase;

public class TestMatchingFacetSetsCountsPerDocument extends LuceneTestCase {

  public void testOneDocumentTwoMatchingSets() throws Exception {
    try (Directory dir = newDirectory()) {
      try (IndexWriter writer = new IndexWriter(dir, new IndexWriterConfig())) {
        Document doc = new Document();
        doc.add(
            FacetSetsField.create("genreYear", new IntFacetSet(1, 2020), new IntFacetSet(1, 2021)));
        writer.addDocument(doc);
      }
      try (DirectoryReader reader = DirectoryReader.open(dir)) {
        FacetsCollector fc =
            new IndexSearcher(reader)
                .search(MatchAllDocsQuery.INSTANCE, new FacetsCollectorManager());
        FacetResult result =
            new MatchingFacetSetsCounts(
                    "genreYear",
                    fc,
                    FacetSetDecoder::decodeInts,
                    new RangeFacetSetMatcher(
                        "Thriller 2020-2022", new DimRange(1, 1), new DimRange(2020, 2022)))
                .getTopChildren(10, "genreYear");
        // FacetSets.adoc: each matcher counts the documents it matches; 1 document, counted 2
        assertEquals(
            "documents matched by 'Thriller 2020-2022'", 1, result.labelValues[0].value.intValue());
      }
    }
  }
}
Command
cp $REPRO/TestMatchingFacetSetsCountsPerDocument.java $LUCENE/lucene/facet/src/test/org/apache/lucene/facet/facetset/
cd $LUCENE
./gradlew :lucene:facet:test --tests org.apache.lucene.facet.facetset.TestMatchingFacetSetsCountsPerDocument -Ptests.seed=DEADBEEF -Ptests.asserts=true
# assertions off: -Ptests.asserts=false (same failure)
Expected
The "Thriller 2020-2022" matcher's value is 1, the one matching document, as FacetSets.adoc says.
Observed
It is 2, one per matching facet set, while FacetResult.value is 1 (main, 10.5.2, 10.6.0).

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

More files, environment, full output210 words
  • Smallest input: one document and two sets is the least that can show a value above the document count.
  • Releases: the same test fails on the 10.6.0 and 10.5.2 release jars; 9.3.0, 9.12.3 and 10.0.0 fail with a copy that uses new MatchAllDocsQuery() (they have no MatchAllDocsQuery.INSTANCE); 9.2.0 has no facet-set API.
  • Not a regression test for the doc fix: it asserts the documented promise. With the recommended text fix the code does not change, so drop the test, or keep it with its one assertion changed to the fixed text (2 sets), which this doc ran. With the per-document code change (option B) it passes unchanged.

Environment

lucenemain 645d4fe14a20 (11.0.0-SNAPSHOT); also fails on the 10.6.0 and 10.5.2 release jars
jdkOpenJDK 25.0.3
osLinux aarch64
Command (environment)
cp $REPRO/TestMatchingFacetSetsCountsPerDocument.java $LUCENE/lucene/facet/src/test/org/apache/lucene/facet/facetset/
cd $LUCENE
./gradlew :lucene:facet:test --tests org.apache.lucene.facet.facetset.TestMatchingFacetSetsCountsPerDocument -Ptests.seed=DEADBEEF -Ptests.asserts=true
# assertions off: -Ptests.asserts=false (same failure)

Observed output

# gradle, main 645d4fe14a20, seed DEADBEEF, -Ptests.asserts=true
TestMatchingFacetSetsCountsPerDocument > testOneDocumentTwoMatchingSets FAILED
    java.lang.AssertionError: documents matched by 'Thriller 2020-2022' expected:<1> but was:<2>
        at org.apache.lucene.facet.facetset.TestMatchingFacetSetsCountsPerDocument.testOneDocumentTwoMatchingSets(TestMatchingFacetSetsCountsPerDocument.java:54)
1 test completed, 1 failed
BUILD FAILED
# -Ptests.asserts=false: the same AssertionError

# upstream demo test on main (passes: the per-set count is asserted upstream)
TestCustomFacetSetExample: 4 run, 0 failed   (testRangeMatching: value 2, "Eighty to Hundred Degrees" 4)

Why it looks unintended

  • The guide states the quantity: "counts how many documents are matched by each of a list of FacetSetMatcher", and its example table counts actors.
  • The demo javadoc says "Counting documents" directly above code whose upstream test asserts 4 for 2 documents.
  • Other facet counters count a multi-valued document at most once per label (FacetResult.value, the range counters, LongValueFacetCounts).
The full argument236 words
  • The guide: the ordinary reading of FacetSets.adoc:119-120 is a count of distinct documents per matcher, which a value larger than the number of matched documents contradicts. Its motivating table counts actors (:27), one document per actor (:13-23), whose (genre, year) combinations the guide indexes as facet sets (:36-38, :123-124). The automotive-parts motivation counts parts too: aggregating on sub-fields "will lead to a wrong count of 2 (in the above example) instead of 1" (:66-69).
  • The demo: CustomFacetSetExample.java:206 says "Counting documents which match a certain degrees value for any date"; TestCustomFacetSetExample.java:54, :57 assert value 2 and label 4.
  • Other facet counters: FacetResult.value: "If a document contains multiple values for the same path, it will only be counted once in this value" (FacetResult.java:30-35); the range counters roll multi-valued counts up per document "so that counts don't get double-counted" (range/OverlappingLongRangeCounter.java:211-216); LongValueFacetCounts skips a value repeated within one document, "do not increment the count for duplicate values" (LongValueFacetCounts.java:263-276, :271; #191). Association facets aggregate weights, not counts, and are not compared here.
  • The authors described it both ways: in the #841 design discussion shaie wrote that the class "counts how many documents each matcher matched as well as how many documents matched overall" (2022-05-31); in #11664 (2022-07-12) mdmarshmallow, the PR's author, described counting matching "points". Nobody discussed several matching sets in one document.
The devil's-advocate check194 words

Strongest case (the study's intended-behaviour verifier and cross-model reviews, OpenAI GPT-5.6): the per-set count is the intended, tested semantics. The class javadoc says "Returns the counts for each given FacetSet" (MatchingFacetSetsCounts.java:36); a matcher evaluates one set at a time; TestRangeFacetSetMatcher adds 1 per matching set to its expected child counts and 1 per document to its expected value (TestRangeFacetSetMatcher.java:66-94, :123); the demo test pins 4 for 2 documents; the API is @lucene.experimental; the adoc line is a loose summary.

Why it fails: it succeeds against calling this a counting bug, which is why it is classed as a documentation defect. It does not make the documentation right: the guide names a different quantity (documents) from the one computed (sets), a matcher that accepts several of one actor's facet sets (a year range, or a repeated set) would report more than the number of actors the guide's table counts, and the demo javadoc says "documents" right above the asserted 4 for 2. The per-set semantics are strongly supported but not proven to be what the authors meant (shaie's #841 comment says documents), so the maintainers may pick either meaning.

Suggested fix

Recommended A. Say that each matcher counts facet sets (recommended)

Fix the text, not the code: FacetSets.adoc (and the demo javadoc) should say that each matcher counts the matching facet sets of the matching documents, so one document can add more than 1 and a matcher's count can exceed the total. Text only; the facet-set and demo tests pass. Per-document counting is the maintainers' alternative; it breaks 7 upstream tests.

A `Facets` implementation which counts, for each of a list of `FacetSetMatcher`, how many facet sets of the matching
documents it matches, in addition to counting the total number of documents that were matched. A document counts once
in the total but once per matching facet set for each matcher: a document with the facet sets (Thriller, 2020) and
(Thriller, 2021) adds 2 to a "Thriller 2020-2022" range matcher, so a matcher's count can be larger than the total. It
uses the `FacetSetDecoder` to decode the
All options and trade-offs2 options · 572 words

The per-set behaviour is intended and tested, so the recommended fix changes text only. Both options were applied in a scratch worktree of 645d4fe14a20 and run with gradle (OpenJDK 25.0.3, seed DEADBEEF).

A. Say that each matcher counts facet sets (recommended) (validated by the checks)

Reword FacetSets.adoc:119-120 as below, the demo javadoc CustomFacetSetExample.java:206 ("Counting temperature readings (facet sets) which match a certain degrees value for any date. A city with several matching readings is counted once per reading, so the count can be larger than the number of matching documents.") and optionally the class javadoc MatchingFacetSetsCounts.java:36.

A `Facets` implementation which counts, for each of a list of `FacetSetMatcher`, how many facet sets of the matching
documents it matches, in addition to counting the total number of documents that were matched. A document counts once
in the total but once per matching facet set for each matcher: a document with the facet sets (Thriller, 2020) and
(Thriller, 2021) adds 2 to a "Thriller 2020-2022" range matcher, so a matcher's count can be larger than the total. It
uses the `FacetSetDecoder` to decode the

Trade-offs

  • Text only: no behaviour or API change, every existing test kept.
  • Applications that want per-document counts still have no stock option (the adoc could say so).
  • The fix can also correct the heading typo MatchingFacetSetCounts (FacetSets.adoc:117). The exact-matcher demo javadocs (CustomFacetSetExample.java:122, :152) are true for the demo's data, where no document repeats a set.

What the checks validated

  • :lucene:facet:checkGoogleJavaFormat, :lucene:demo:checkGoogleJavaFormat, :lucene:facet:renderJavadoc and :lucene:demo:renderJavadoc pass.
  • The code is unchanged, so the minimal test as written still fails; with its one assertion changed to the fixed text (2 sets) it passes, assertions on and off.
  • :lucene:facet:test --tests 'org.apache.lucene.facet.facetset.*': 4 classes, 15 tests, 0 failures (assertions on; with assertions off 1 test that assumes assertions is skipped); :lucene:demo:test --tests TestCustomFacetSetExample: 4 tests, 0 failures, assertions on and off.

B. Count each document at most once per matcher (maintainers' alternative) (validated by the checks)

Keep a boolean[] per document in MatchingFacetSetsCounts.count and skip a matcher that already matched an earlier set of the same document (+5/-1 lines).

if (matchedInDoc[j] == false && facetSetMatchers[j].matches(dimValues)) {
  // count each document at most once per matcher
  matchedInDoc[j] = true;
  counts[j]++;
  shouldCountDoc = true;
}

Trade-offs

  • Makes the guide's sentence true and matches the range facets' once-per-document rule.
  • Changes the results of an API shipped since 9.3.0 and reverses semantics the tests pin: 7 upstream tests need new expected values, plus a CHANGES/MIGRATE note.
  • Per-set counts may be what some users want ("how many readings were in range"); an opt-in per-document mode (a flag or a separate class) is a third way, not written or run here.
  • As written it stops calling a matcher's matches() for the later sets of a document once that matcher has matched. The stock matchers are stateless, so results do not change, but FacetSetMatcher does not promise that a custom matcher has no side effects (FacetSetMatcher.java:49-54); calling matches() for every set and gating only counts[j]++ avoids that (not run here).

What the checks validated

  • :lucene:facet:checkGoogleJavaFormat passes; the unchanged minimal test passes, assertions on and off; TestExactFacetSetMatcher (6) and TestMatchingFacetSetsCounts (3) pass.
  • TestRangeFacetSetMatcher fails 5 of 5 (for example expected:<Ford [2010-2014] (60)> but was:<Ford [2010-2014] (48)>) and TestCustomFacetSetExample fails 2 of 4 (expected:<Eighty to Hundred Degrees (4)> but was:<Eighty to Hundred Degrees (2)>), because they assert per-set counts.

Downstream note

Applications that label a matcher's value as a number of documents or users, as the guide describes, overstate it; the hits, the total and which documents are counted are unaffected.

How it was found

Non-formal. While writing the facet-set part of a brute-force reference for facet counts, which models the per-set semantics the demo test asserts, the finder noticed that the user guide describes document counts. A public-API JUnit on the stock jars confirmed it; verifiers, cross-model reviews and an adjudication followed.

The method in full77 words
  • First signal: code reading in a hunt unit (facet taxonomy, SSDV and DrillSideways), 2026-10-26, while writing FacetCountsBX, a brute-force reference whose facet-set part counts per set as the demo test does. No model produced or witnessed the defect, so the finding is non-formal.
  • Repro: the finder's public-API JUnit failed on main with assertions on and off and on 10.5.2.
  • Minimal repro (2026-10-29): one method, one assertion, try-with-resources; fails on main 645d4fe14a20 with assertions on and off.

Prior reports, history and checks

Prior reports

No prior report found (2026-10-26, repeated 2026-11-01): GitHub issues and PRs incl. the JIRA archive, commits, mailing lists, downstream projects, the web, and all 410 open PRs' changed files. Nearest: #841, the introducing PR, and #11664, an open performance issue; neither raises the mismatch.

Searches4 searches

Keyword search only: a report that names none of the classes could be missed; apache/lucene has no GitHub Discussions; private channels cannot be searched.

What was searched, when, and what turned up
WhereWhenHowFound
GitHub issues and PRs (apache/lucene incl. the migrated JIRA archive), commits, downstream repositories (Elasticsearch, OpenSearch, Solr), dev@/java-user@/issues@/commits@, the web2026-10-26, repeated 2026-11-01three scripted prior-report searches: 42 GitHub issue/PR, 7 commit, 5 downstream, 60 mailing-list, 12 JIRA-archive and 17 web queries (class names, FacetSets.adoc, symptom phrasings)812, 1,062 and 241 distinct hits; none this bug
Open PRs2026-11-01GitHub PR listings for 9 terms; the changed-file lists of all 410 open PRs#13948 and #12862 touch the class; neither changes the counting or the wording
Threads read in full2026-11-01#841 (all comments, reviews and threads), #11664, #11310, #12862 and #13948 patches; lists.apache.org by hand (15 queries)per-document (#841) and per-point (#11664) descriptions in passing; no discussion of several matching sets per document
Commits2026-11-01GitHub commits API and git log -S for the sentence and counts[j]++both only in f6bb9d218c0 (#841); no later commit changes them

History

Introduced with the facet-set API by #841 (f6bb9d218c0, 2022-06-25; backport #981), first released in 9.3.0: the sentence, the per-set loop, the demo javadoc and the demo test value 4 came in one commit. Every release from 9.3.0 to 10.6.0 and main is affected; not fixed upstream.

Versions tested4 rows

Introduced by f6bb9d218c0 "LUCENE-10274: Add FacetSets faceting capabilities (#841)" (PR by mdmarshmallow; co-authors Shai Erera and Greg Miller per the commit trailers; 2022-06-25) and its branch_9x backport 0e1502c12ba (#981, shaie, same day). Not a regression: the documentation and the code have disagreed from the start.

Later commits on the guide, MatchingFacetSetsCounts and the demo changed neither the sentence nor the counting: #1015 (fastMatchQuery; it extended the same adoc paragraph and kept "documents"), #11764, #12655, #13328, #14761, #14832, #15499, #16255. The demo test has #1015 and one more, test-only commit, #16028 (junit4 cleanup), which keeps the asserted 4.

Not fixed upstream (a snapshot): upstream main f815e679bf6 (2026-11-01) has the same files.

Versions and branches executed, with results
Version or commitVerdictResult
main 645d4fe14a20 affectedminimal test fails, assertions on and off (gradle and standalone)
10.6.0, 10.5.2 (release jars) affectedminimal test fails (10.5.2 with assertions on and off)
9.3.0, 9.12.3, 10.0.0 (release jars) affecteda copy with new MatchAllDocsQuery() fails: expected:<1> but was:<2>
9.2.0 no APIno facet-set API: the repro does not compile

Independent checks

The finder, an independent reproducer, a devil's-advocate verifier, a reading-only cross-model review (real, medium), the adjudication and an executed cross-model refute review (real, high) find a real documentation defect. A second reading-only review gave a procedural "not real" (low): it could not run code, yet said the evidence strongly indicates a prose inconsistency. Gradle runs confirm it on main.

Checks and verdicts358 words

The executed cross-model review ran Java: its own 2-document test (3 accepted sets) fails with expected:<2> but was:<3> on main and 10.5.2, its control passes; re-run at 645d4fe14a20, 2 run, 1 failed, assertions on and off.

Each independent check and its verdict
CheckVerdictWhat it found
finder fails 1 of 1Public-API JUnit fails on main with assertions on and off and on 10.5.2.
Independent reproducer real (doc)Own 4-document JUnit, 2 of 3 fail on main, 10.5.2, 9.12.3 and 9.4.0 (a range matcher's 3 for 2 documents; an exact matcher's 2 for one document with a repeated set); the control passes.
Cross-model works-as-designed lens real (medium)OpenAI GPT-5.6, reading only: "a real documentation/contract defect, not clearly an implementation defect".
reading-only cross-model refute review not real (procedural, low)OpenAI GPT-5.6, reading only: it required a run of its own, which it could not do, so it returned not real; its notes say the static evidence "strongly indicates an actual prose inconsistency" and that the verdict is "procedural and low-confidence rather than a substantive refutation". The executed refute review below replaced it.
Intended-behaviour devil's advocate real (doc, minor)Ran the exact-matcher duplicate-set variant, the 9.x copy on 9.3.0, 9.4.2 and 9.12.3, and the upstream demo test (passes, 4 for 2 documents).
adjudication confirmed, highRe-ran the finder's and the independent reproduction's tests; category doc; asked to name the demo javadoc and to set 9.3.0 as the first release.
executed cross-model refute review real, highOpenAI GPT-5.6 running Java: own test fails on main and 10.5.2, control passes; per-set code is intended, the documentation is the defect.
this doc fails before, both fixes runGradle on main 645d4fe14a20: the minimal test fails with assertions on and off; option A passes the facet-set and demo tests; option B fixes the repro and breaks 7 upstream tests.
independent fact checks real; wording fixedOne re-ran the documented gradle command in its own worktree at 645d4fe14a20 (same output, assertions on and off) and the standalone repro; a cross-model one (OpenAI GPT-5.6) re-read the sources. Their wording corrections are in this version.

Diagram file

The diagram as a still image, generated with AI and checked against the code. It is under 10 MB, the limit for a GitHub attachment.

Sources and provenance

Finding record, Lucene and upstream sources, repro files and diagram notes1,252 words

Finding record

LFV-351, snapshot of 2026-11-06

Registry title
FacetSets.adoc says MatchingFacetSetsCounts counts the documents each FacetSetMatcher matches, but it counts matching facet sets, so one document can add more than 1 to a matcher (documentation defect; the per-set code is intended and pinned by upstream tests)
Area
facet / facet sets (MatchingFacetSetsCounts child counts vs. FacetSets.adoc and the demo javadoc)
Priority
Low, rank 1229: This is a documentation-only defect: FacetSets.adoc says matchers count documents while the intended code counts sets, which can mislead facet-set users on ordinary data; it is released and plausible.
Impact class
minor
Impact note
Documentation only: every user of the @lucene.experimental facet-set API (MatchingFacetSetsCounts with ExactFacetSetMatcher, RangeFacetSetMatcher or a custom matcher) whose documents hold several facet sets that the same matcher accepts gets per-matcher label values that count facet sets, not documents, and can exceed FacetResult.value; that is the intended, tested behaviour, so only readers who take FacetSets.adoc and the demo javadoc at their word and treat the value as a document count are misled, with the default codec and IndexWriterConfig at any scale, in every release since 9.3.0.
Summary
You index documents that each hold several facet sets (the adoc's own example: one (genre, year) set per movie of an actor) and count them with MatchingFacetSetsCounts, which the user guide FacetSets.adoc says 'counts how many documents are matched by each of a list of FacetSetMatcher'. You expect each matcher's value to be a number of documents, never larger than FacetResult.value. Instead the code adds 1 for every matching facet set, so one document with the sets (Thriller, 2020) and (Thriller, 2021) gives 2 for a 'Thriller 2020-2022' range matcher while FacetResult.value is 1; upstream tests pin this per-set count, so the conservative fix is to correct the documentation, although maintainers could instead choose per-document counts.
Introduced
f6bb9d218c0e (main, 'LUCENE-10274: Add FacetSets faceting capabilities (#841)', apache/lucene#841 by mdmarshmallow with co-authors shaie and gsmiller per the commit trailers; same-day branch_9x backport 0e1502c12ba (#981, by shaie), the one released in 9.3.0; issue LUCENE-10274 / #11310). The one squash commit added the FacetSets.adoc sentence, the demo javadoc 'Counting documents which match a certain degrees value for any date', the per-set counts[j]++ loop and the demo test that asserts 4 for 2 documents, so the documentation and the code have disagreed from the start; not a regression [read: git blame and git log -S in the history clone; exec: release level only, 9.2.0 has no facet-set API (the repro does not compile) and 9.3.0 fails; no commit-level build, because the parent commit has no facetset package] (2022-06-25)
First affected version
9.3.0 [exec] (first release with the org.apache.lucene.facet.facetset package; 9.2.0 has none [exec: compile error, API absent])
Affected versions
every release from 9.3.0 to 10.6.0 and main (ea57360912ed, 645d4fe14a20); not fixed upstream. [exec] the minimal test fails on the 10.6.0 release jars and on main 645d4fe14a20 (-ea and -da) (2026-11-01); the finder's 9.x-compatible JUnit fails on 9.3.0, 9.12.3, 10.0.0, 10.5.2 and main (-ea and -da on main) in the history step and on 9.3.0, 9.4.2 and 9.12.3; the finder's JUnit fails on 10.5.2; the independent reproduction's JUnit fails on 9.4.0, 9.12.3, 10.5.2 and main (-ea and -da). [read] every release tag from 9.3.0 to 10.5.2 contains both the adoc sentence and the per-set loop (git show tag by tag in the history clone); no commit to FacetSets.adoc, MatchingFacetSetsCounts.java or CustomFacetSetExample.java changed the wording or the loop (GitHub commits API, 2026-10-26). The finder's 'since 9.4' and the independent reproduction's '9.4.0 (the first release with facet sets)' are corrected to 9.3.0 (9.3.0 CHANGES.txt: 'LUCENE-10274: Added facetsets module').
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
non-formal: code reading
Upstream and prior reports
none found (searched 2026-10-26: a scripted prior-report search in two runs with 32 GitHub issue/PR queries on apache/lucene, 6 commit queries, 4 downstream-repo queries (Elasticsearch, OpenSearch, Solr), 40 mailing-list queries over dev@, java-user@, issues@ and commits@, 8 JIRA-archive queries and 6 web queries (702 and 1,011 distinct hits, none the same bug), 6 open-PR searches, 9 GitHub issue/PR searches, the GitHub commits API for the three files, git log -S in the history clone and the ApacheCon NA 2022 faceting slides; the finder's, the independent reproduction's and the intended-behaviour check's searches found nothing either, the independent reproduction's scripted prior-report search having timed out). Nearest items, none a report of this defect: #841 / #981 (introducing PR and backport; shaie's design comment says the class 'counts how many documents each matcher matched'), #11310 / LUCENE-10274 (feature issue, 'a generalization of LongRangeFacets / DoubleRangeFacets'), #11664 / LUCENE-10628 (open performance issue; mdmarshmallow describes per-point counting in passing), #11665 / #1001 / #1015 / #1055 (fastMatchQuery; #1015 extended the same adoc paragraph and kept the sentence), #11742 / #11764 (getTopChildren returned all children; different defect), #13948 (the only open PR touching MatchingFacetSetsCounts; decode path, unrelated); related conventions in other classes that count a multi-valued document once per label, not reports of this defect: #10985 / #127 (LUCENE-9946, range facets) and #191 (LUCENE-9964, LongValueFacetCounts). Not fixed on upstream main (2026-10-26). Gaps: term-based searches could miss a report that names none of the classes; GitHub Discussions not searched.
Lucene commit tested
main 645d4fe14a20 (2026-10-28): the minimal test fails through gradle and standalone, assertions on and off; the upstream TestCustomFacetSetExample passes (4 for 2 documents). Release jars: 10.5.2 (assertions on and off) and 10.6.0 fail with the minimal test; 9.3.0, 9.12.3 and 10.0.0 fail with a copy that uses new MatchAllDocsQuery(); 9.2.0 has no facet-set API. Both fix options were run with gradle in a scratch worktree of 645d4fe14a20.

Lucene and upstream sources

Finding record
  • lucene/facet/docs/FacetSets.adoc:27 ('Number of Actors performing in movies by Genre and Year'), :117-120 ('A `Facets` implementation which counts how many documents are matched by each of a list of `FacetSetMatcher`, in addition to counting the total number of documents that were matched')
  • lucene/facet/src/java/org/apache/lucene/facet/facetset/package-info.java:19-20 ('See FacetSets.adoc in the docs folder for more information on usage')
  • lucene/facet/src/java/org/apache/lucene/facet/facetset/MatchingFacetSetsCounts.java:36 (class javadoc 'Returns the counts for each given FacetSet'), :49 and :62 (constructor javadoc 'calculates the counts for each given facet set matcher'), :124-131 (counts[j]++ per matching facet set, :128), :133-134 (totCount++ once per document)
  • lucene/demo/src/java/org/apache/lucene/demo/facet/CustomFacetSetExample.java:206 (javadoc 'Counting documents which match a certain degrees value for any date')
  • lucene/demo/src/test/org/apache/lucene/demo/facet/TestCustomFacetSetExample.java:54, :57 (testRangeMatching: value 2, label 'Eighty to Hundred Degrees' 4)
  • lucene/facet/src/test/org/apache/lucene/facet/facetset/TestRangeFacetSetMatcher.java (expected child counts per matching set, numMatchingDocs per document; cited by the independent reproduction, the cross-model review and the intended-behaviour check)
  • lucene/facet/src/java/org/apache/lucene/facet/range/OverlappingLongRangeCounter.java:211-216, range/LongRangeCounter.java:24-38 (one count per multi-valued document and range); facet/FacetResult.java:32 ('it will only be counted once'); facetset/FacetSetsField.java:24 ('encodes a list of facet sets')
The finding in 30 seconds
  • lucene/facet/docs/FacetSets.adoc:13-23, :16, :27, :36-38, :117, :119-120, :123-124
  • lucene/facet/src/java/org/apache/lucene/facet/facetset/package-info.java:19-20
  • lucene/facet/src/java/org/apache/lucene/facet/facetset/MatchingFacetSetsCounts.java:103-135 (:128, :134), :150-154, :194
  • lucene/facet/src/java/org/apache/lucene/facet/facetset/FacetSetMatcher.java:49-54
  • lucene/facet/src/java/org/apache/lucene/facet/facetset/FacetSetsField.java:52-75
  • lucene/demo/src/java/org/apache/lucene/demo/facet/CustomFacetSetExample.java:85-105, :206
  • lucene/demo/src/test/org/apache/lucene/demo/facet/TestCustomFacetSetExample.java:49-58
Who is affected
  • lucene/facet/docs/FacetSets.adoc:98-99
  • lucene/facet/src/java/org/apache/lucene/facet/facetset/FacetSetsField.java:23-26
  • lucene/facet/src/java/org/apache/lucene/facet/facetset/MatchingFacetSetsCounts.java:52-85
  • lucene/facet/src/java/org/apache/lucene/facet/facetset/MatchingFacetSetsCounts.java:124-132 (a matcher counts only the sets it accepts)
Why it looks unintended
  • lucene/facet/docs/FacetSets.adoc:13-23, :27, :36-38, :66-69, :119-120, :123-124
  • lucene/demo/src/java/org/apache/lucene/demo/facet/CustomFacetSetExample.java:206
  • lucene/demo/src/test/org/apache/lucene/demo/facet/TestCustomFacetSetExample.java:54, :57
  • lucene/facet/src/java/org/apache/lucene/facet/FacetResult.java:30-35
  • lucene/facet/src/java/org/apache/lucene/facet/range/OverlappingLongRangeCounter.java:211-216
  • lucene/facet/src/java/org/apache/lucene/facet/facetset/MatchingFacetSetsCounts.java:36
  • lucene/facet/src/test/org/apache/lucene/facet/facetset/TestRangeFacetSetMatcher.java:66-94, :123-129
Diagrams
  • lucene/facet/src/java/org/apache/lucene/facet/facetset/MatchingFacetSetsCounts.java:128, :134 (at 645d4fe14a20)

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

Repro files

About the diagrams

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.