Filtering & faceting

This section documents the current Apache Lucene 10.x line — Lucene 10 requires Java 21 — as published at the Apache Lucene documentation and Javadoc, which is the reference these pages are written and verified against. No specific patch version is pinned; examples target lucene-core 10.x and the companion modules. Some areas (the Panama foreign-memory / Vector API internals, codec file-format internals, and the nightly benchmark harness) are linked, not documented in depth.

This content was generated with the assistance of AI and should be verified against the official documentation before being relied on in production, as Lucene iterates quickly.

This section’s bibliography lists the reference material consulted while preparing these pages.

Two related jobs live on this page: restricting a result set without affecting scores (a filter), and turning a result set into grouped counts (a facet). Modern Lucene has no Filter class — a filter is just a Query added with Occur.FILTER — and faceting is a separate lucene-facet module built on doc-values or a sidecar taxonomy index.

Filtering: Occur.FILTER, ConstantScoreQuery, TermInSetQuery

A clause added to a BooleanQuery with BooleanClause.Occur.FILTER must match, contributes nothing to the score, and is eligible for caching in the IndexSearcher’s `LRUQueryCache (on by default). Pre-5.x code wrapped a Filter object around a query or passed it as a separate search argument; that class is gone — express the same intent as a FILTER clause.

// https://lucene.apache.org/core/10_0_0/core/org/apache/lucene/search/BooleanClause.Occur.html
import org.apache.lucene.document.IntPoint;
import org.apache.lucene.index.Term;
import org.apache.lucene.search.*;
import org.apache.lucene.search.BooleanClause.Occur;

Query q = new BooleanQuery.Builder()
    .add(new TermQuery(new Term("body", "lucene")), Occur.MUST)     // scored
    .add(new TermQuery(new Term("status", "published")), Occur.FILTER) // unscored, cacheable
    .add(IntPoint.newRangeQuery("year", 2015, 2025), Occur.FILTER)     // unscored, cacheable
    .build();

ConstantScoreQuery wraps any query so it runs as an unscored match with a fixed score of 1.0 — useful when a query must contribute a hit list to a scored SHOULD/MUST context but its own relevance is irrelevant.

// https://lucene.apache.org/core/10_0_0/core/org/apache/lucene/search/ConstantScoreQuery.html
Query constant = new ConstantScoreQuery(
        IntPoint.newRangeQuery("year", 2015, 2025));

TermInSetQuery matches a document whose field holds any of a set of terms — a single query that is far cheaper than a BooleanQuery of many SHOULD TermQuery clauses when the set is large. It scores as a constant and caches well as a FILTER.

// https://lucene.apache.org/core/10_0_0/core/org/apache/lucene/search/TermInSetQuery.html
import java.util.List;
import org.apache.lucene.util.BytesRef;

Query tags = new TermInSetQuery("tag",
        List.of(new BytesRef("java"), new BytesRef("search"), new BytesRef("lucene")));

The searcher’s cache is configured with IndexSearcher.setQueryCache / setQueryCachingPolicy; see LRUQueryCache. For scored alternatives to a plain filter (boosting rather than restricting), see Function & custom scoring.

The lucene-facet module

lucene-facet computes facet counts as a second pass over the hits of a query. It needs a FacetsConfig describing which dimensions are hierarchical, multi-valued, or stored with an indexFieldName, and the same config must be used at index and search time.

<dependency>
  <groupId>org.apache.lucene</groupId>
  <artifactId>lucene-facet</artifactId>
  <version>10.0.0</version>
</dependency>
// https://lucene.apache.org/core/10_0_0/facet/org/apache/lucene/facet/FacetsConfig.html
import org.apache.lucene.facet.FacetsConfig;

FacetsConfig config = new FacetsConfig();
config.setHierarchical("Category", true);
config.setMultiValued("Author", true);

Taxonomy index vs. SortedSetDocValues

There are two storage strategies for facet fields, and the choice is made per index:

Taxonomy sidecar index versus SortedSetDocValues faceting

Taxonomy index — a second Directory written by DirectoryTaxonomyWriter maps each facet label to an ordinal; FacetsConfig.build adds the ordinals to each document, and FastTaxonomyFacetCounts counts them at search time via a TaxonomyReader. It supports deep hierarchies and association facets efficiently, at the cost of maintaining the sidecar directory alongside the main index.

// https://lucene.apache.org/core/10_0_0/facet/org/apache/lucene/facet/taxonomy/FastTaxonomyFacetCounts.html
import org.apache.lucene.facet.*;
import org.apache.lucene.facet.taxonomy.*;
import org.apache.lucene.facet.taxonomy.directory.DirectoryTaxonomyWriter;
import org.apache.lucene.facet.taxonomy.directory.DirectoryTaxonomyReader;

// indexing
DirectoryTaxonomyWriter taxoWriter = new DirectoryTaxonomyWriter(taxoDir);
Document doc = new Document();
doc.add(new FacetField("Category", "books", "computers"));
doc.add(new FacetField("Author", "Doug"));
indexWriter.addDocument(config.build(taxoWriter, doc));
taxoWriter.close();

// searching
TaxonomyReader taxoReader = new DirectoryTaxonomyReader(taxoDir);
FacetsCollector fc = new FacetsCollector();
FacetsCollector.search(searcher, new MatchAllDocsQuery(), 10, fc);
Facets facets = new FastTaxonomyFacetCounts(taxoReader, config, fc);
FacetResult top = facets.getTopChildren(10, "Category");

SortedSetDocValues — SortedSetDocValuesFacetField stores the labels directly as a doc-values field, and SortedSetDocValuesFacetCounts (or the segment-state variant) counts them with no sidecar index. Simpler to operate; best for flat or shallow dimensions.

// https://lucene.apache.org/core/10_0_0/facet/org/apache/lucene/facet/sortedset/SortedSetDocValuesFacetCounts.html
import org.apache.lucene.facet.sortedset.*;

// indexing -- no taxonomy writer
doc.add(new SortedSetDocValuesFacetField("Author", "Doug"));
indexWriter.addDocument(config.build(doc));

// searching
SortedSetDocValuesReaderState state =
    new DefaultSortedSetDocValuesReaderState(reader, config);
Facets facets = new SortedSetDocValuesFacetCounts(state, fc);

Value and range facets

LongValueFacetCounts buckets by the exact value of a NumericDocValuesField / SortedNumericDocValuesField — one bucket per distinct number, no config entry needed. LongRangeFacetCounts and DoubleRangeFacetCounts count into explicit LongRange / DoubleRange bands defined at query time (the counterpart of a Solr facet.range or an Elasticsearch range aggregation).

// https://lucene.apache.org/core/10_0_0/facet/org/apache/lucene/facet/range/LongRangeFacetCounts.html
import org.apache.lucene.facet.range.*;
import org.apache.lucene.facet.LongValueFacetCounts;

Facets exact = new LongValueFacetCounts("year", fc);

LongRange[] decades = {
    new LongRange("1990s", 1990, true, 1999, true),
    new LongRange("2000s", 2000, true, 2009, true),
    new LongRange("2010s", 2010, true, 2019, true),
};
Facets ranges = new LongRangeFacetCounts("year", fc, decades);
FacetResult byDecade = ranges.getTopChildren(10, "year");

Drill-down and drill-sideways

DrillDownQuery adds one or more facet-value constraints onto a base query — the click-through of a faceted UI. DrillSideways runs the base query plus, for each drill-down dimension, a variant that omits that one constraint, so every facet still shows counts for the values the user did not pick (multi-select faceting).

// https://lucene.apache.org/core/10_0_0/facet/org/apache/lucene/facet/DrillSideways.html
import org.apache.lucene.facet.DrillDownQuery;
import org.apache.lucene.facet.DrillSideways;

DrillDownQuery ddq = new DrillDownQuery(config, new MatchAllDocsQuery());
ddq.add("Category", "books");
ddq.add("Author", "Doug");

DrillSideways ds = new DrillSideways(searcher, config, taxoReader);
DrillSideways.DrillSidewaysResult r = ds.search(ddq, 10);
// r.facets -> counts where each dimension ignores its own drill-down

Association facets

Instead of counting occurrences, association facets aggregate a per-document numeric payload attached to a facet label: IntAssociationFacetField / FloatAssociationFacetField at index time, TaxonomyFacetIntAssociations / TaxonomyFacetFloatAssociations with a AssociationAggregationFunction (SUM, MAX) at search time — e.g. total revenue per category rather than document count.

// https://lucene.apache.org/core/10_0_0/facet/org/apache/lucene/facet/taxonomy/TaxonomyFacetFloatAssociations.html
import org.apache.lucene.facet.taxonomy.*;

doc.add(new FloatAssociationFacetField(19.99f, "Category", "books"));
// ... at search time:
Facets revenue = new TaxonomyFacetFloatAssociations(
        taxoReader, config, fc, AssociationAggregationFunction.SUM);

See the lucene-facet package Javadoc for the full type list and the FacetsCollectorManager for concurrent collection.