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.
- A facet set is a tuple of dimension values, such as (genre, year). A
FacetSetsFieldstores 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). - At search time
MatchingFacetSetsCountstakes a list of matchers: anExactFacetSetMatcheraccepts one tuple, aRangeFacetSetMatchera box of ranges, such as genre Thriller and years 2020 to 2022. - It returns a
FacetResult:valueis the number of matching documents, plus one label value per matcher.
FacetResult.value counts the document once (1). Select the still to open it at full size. Generated with AI, checked against the code.- You index one actor document whose
FacetSetsFieldholds 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. - You count with
MatchingFacetSetsCountsand oneRangeFacetSetMatcher"Thriller 2020-2022". FacetSets.adoc says it "counts how many documents are matched by each of a list ofFacetSetMatcher", so you expect 1. - 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. - 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. - 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.
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.
FacetSets.adoc:119-120: "AFacetsimplementation which counts how many documents are matched by each of a list ofFacetSetMatcher, in addition to counting the total number of documents that were matched." The heading above it (:117) spells the classMatchingFacetSetCounts.- 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 "indexedgenreandyearfacet sets" in that example,:123-124).package-info.java:19-20sends users to this guide. CustomFacetSetExample.java:206: "Counting documents which match a certain degrees value for any date."
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 repro: one document with (1, 2020) and (1, 2021), matcher "Thriller 2020-2022": label value 2,
FacetResult.value1. - 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,value2, asserted byTestCustomFacetSetExample.java:54,:57. - An exact matcher is enough when a document repeats a set:
FacetSetsField.createpacks the sets as given (FacetSetsField.java:52-75), and one document holding (1, 2020) twice givesExactFacetSetMatcher"Thriller 2020" 2 (a study verifier's run).
Exposure and likelihood228 words
- Who: users of
MatchingFacetSetsCountswithExactFacetSetMatcher,RangeFacetSetMatcheror a custom matcher. Several sets per document is the documented shape (FacetSets.adoc:98-99: a field "which lets you index a list ofFacetSet";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
fastMatchQueryand the matchers (MatchingFacetSetsCounts.java:52-85), so an application that needs per-document counts must write its ownFacets. - Likelihood: plausible but limited. Facet sets are niche; a GitHub code search for the class name found no use of
MatchingFacetSetsCountsin 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).
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.
/*
* 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());
}
}
}
}
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.valueis 1 (main, 10.5.2, 10.6.0).
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 noMatchAllDocsQuery.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.
| lucene | main 645d4fe14a20 (11.0.0-SNAPSHOT); also fails on the 10.6.0 and 10.5.2 release jars |
|---|---|
| jdk | OpenJDK 25.0.3 |
| os | Linux aarch64 |
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)
# 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)- 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-120is 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:206says "Counting documents which match a certain degrees value for any date";TestCustomFacetSetExample.java:54,:57assert 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);LongValueFacetCountsskips 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.
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 theAll 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).
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 theTrade-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:renderJavadocand:lucene:demo:renderJavadocpass.- 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, butFacetSetMatcherdoes not promise that a custom matcher has no side effects (FacetSetMatcher.java:49-54); callingmatches()for every set and gating onlycounts[j]++avoids that (not run here).
What the checks validated
:lucene:facet:checkGoogleJavaFormatpasses; the unchanged minimal test passes, assertions on and off;TestExactFacetSetMatcher(6) andTestMatchingFacetSetsCounts(3) pass.TestRangeFacetSetMatcherfails 5 of 5 (for exampleexpected:<Ford [2010-2014] (60)> but was:<Ford [2010-2014] (48)>) andTestCustomFacetSetExamplefails 2 of 4 (expected:<Eighty to Hundred Degrees (4)> but was:<Eighty to Hundred Degrees (2)>), because they assert per-set counts.
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.
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
645d4fe14a20with assertions on and off.
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.
| Where | When | How | Found |
|---|---|---|---|
| GitHub issues and PRs (apache/lucene incl. the migrated JIRA archive), commits, downstream repositories (Elasticsearch, OpenSearch, Solr), dev@/java-user@/issues@/commits@, the web | 2026-10-26, repeated 2026-11-01 | three 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 PRs | 2026-11-01 | GitHub 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 full | 2026-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 |
| Commits | 2026-11-01 | GitHub commits API and git log -S for the sentence and counts[j]++ | both only in f6bb9d218c0 (#841); no later commit changes them |
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.
| Version or commit | Verdict | Result |
|---|---|---|
main 645d4fe14a20 | affected | minimal test fails, assertions on and off (gradle and standalone) |
| 10.6.0, 10.5.2 (release jars) | affected | minimal test fails (10.5.2 with assertions on and off) |
| 9.3.0, 9.12.3, 10.0.0 (release jars) | affected | a copy with new MatchAllDocsQuery() fails: expected:<1> but was:<2> |
| 9.2.0 | no API | no facet-set API: the repro does not compile |
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.
| Check | Verdict | What it found |
|---|---|---|
| finder | fails 1 of 1 | Public-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, high | Re-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, high | OpenAI 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 run | Gradle 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 fixed | One 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. |
The diagram as a still image, generated with AI and checked against the code. It is under 10 MB, the limit for a GitHub attachment.
- PNG 411 KB
Finding record, Lucene and upstream sources, repro files and diagram notes1,252 words
- 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 upstreamTestCustomFacetSetExamplepasses (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 usesnew MatchAllDocsQuery(); 9.2.0 has no facet-set API. Both fix options were run with gradle in a scratch worktree of645d4fe14a20.
- 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-124lucene/facet/src/java/org/apache/lucene/facet/facetset/package-info.java:19-20lucene/facet/src/java/org/apache/lucene/facet/facetset/MatchingFacetSetsCounts.java:103-135 (:128, :134), :150-154, :194lucene/facet/src/java/org/apache/lucene/facet/facetset/FacetSetMatcher.java:49-54lucene/facet/src/java/org/apache/lucene/facet/facetset/FacetSetsField.java:52-75lucene/demo/src/java/org/apache/lucene/demo/facet/CustomFacetSetExample.java:85-105, :206lucene/demo/src/test/org/apache/lucene/demo/facet/TestCustomFacetSetExample.java:49-58
- Who is affected
lucene/facet/docs/FacetSets.adoc:98-99lucene/facet/src/java/org/apache/lucene/facet/facetset/FacetSetsField.java:23-26lucene/facet/src/java/org/apache/lucene/facet/facetset/MatchingFacetSetsCounts.java:52-85lucene/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-124lucene/demo/src/java/org/apache/lucene/demo/facet/CustomFacetSetExample.java:206lucene/demo/src/test/org/apache/lucene/demo/facet/TestCustomFacetSetExample.java:54, :57lucene/facet/src/java/org/apache/lucene/facet/FacetResult.java:30-35lucene/facet/src/java/org/apache/lucene/facet/range/OverlappingLongRangeCounter.java:211-216lucene/facet/src/java/org/apache/lucene/facet/facetset/MatchingFacetSetsCounts.java:36lucene/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)
- TestMatchingFacetSetsCountsPerDocument.java:
repro/LFV-351/src/org/apache/lucene/facet/facetset/TestMatchingFacetSetsCountsPerDocument.java(59 lines)
assets/LFV-351/bug.png(411 KB)
![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.](../assets/LFV-351/bug.png)