You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@lucene.apache.org by vo...@apache.org on 2025/03/03 15:51:07 UTC
(lucene) branch branch_10x updated: Utility classes to make it easier to use sandbox facet API for most common cases (#14237)
This is an automated email from the ASF dual-hosted git repository.
voditas pushed a commit to branch branch_10x
in repository https://gitbox.apache.org/repos/asf/lucene.git
The following commit(s) were added to refs/heads/branch_10x by this push:
new 45fe4ac75b2 Utility classes to make it easier to use sandbox facet API for most common cases (#14237)
45fe4ac75b2 is described below
commit 45fe4ac75b2a553277bf476fb34659f4a5ab5e52
Author: Egor Potemkin <eg...@gmail.com>
AuthorDate: Mon Mar 3 15:03:46 2025 +0000
Utility classes to make it easier to use sandbox facet API for most common cases (#14237)
---
lucene/CHANGES.txt | 2 +
.../lucene/demo/facet/SandboxFacetsExample.java | 119 +++++-
.../org/apache/lucene/demo/facet/package-info.java | 11 +
lucene/sandbox/src/java/module-info.java | 1 +
.../facet/cutters/TaxonomyFacetsCutter.java | 3 +-
.../facet/cutters/ranges/LongRangeFacetCutter.java | 3 +
.../facet/iterators/LengthOrdinalIterator.java | 57 +++
.../apache/lucene/sandbox/facet/package-info.java | 23 +-
.../sandbox/facet/utils/BaseFacetBuilder.java | 157 +++++++
.../sandbox/facet/utils/CommonFacetBuilder.java | 60 +++
.../sandbox/facet/{ => utils}/ComparableUtils.java | 2 +-
.../utils/DrillSidewaysFacetOrchestrator.java | 131 ++++++
.../lucene/sandbox/facet/utils/FacetBuilder.java | 64 +++
.../sandbox/facet/utils/FacetOrchestrator.java | 99 +++++
.../sandbox/facet/utils/LongValueFacetBuilder.java | 82 ++++
.../facet/utils/RangeFacetBuilderFactory.java | 81 ++++
.../sandbox/facet/utils/TaxonomyFacetBuilder.java | 117 ++++++
.../lucene/sandbox/facet/utils/package-info.java | 55 +++
.../lucene/sandbox/facet/SandboxFacetTestCase.java | 13 +
.../lucene/sandbox/facet/TestFacetRecorders.java | 1 +
.../lucene/sandbox/facet/TestLongValueFacet.java | 1 +
.../lucene/sandbox/facet/TestRangeFacet.java | 4 +-
.../facet/utils/TestFacetOrchestrators.java | 456 +++++++++++++++++++++
23 files changed, 1528 insertions(+), 14 deletions(-)
diff --git a/lucene/CHANGES.txt b/lucene/CHANGES.txt
index 4aac0ee48be..400fb7abab7 100644
--- a/lucene/CHANGES.txt
+++ b/lucene/CHANGES.txt
@@ -27,6 +27,8 @@ API Changes
* GITHUB#14292: The redundant DocIdSet#bits method has been deprecated. (Luca Cavanna)
+* GITHUB#14237: Add utils for sandbox facets module for the most common tasks. (Egor Potemkin)
+
New Features
---------------------
diff --git a/lucene/demo/src/java/org/apache/lucene/demo/facet/SandboxFacetsExample.java b/lucene/demo/src/java/org/apache/lucene/demo/facet/SandboxFacetsExample.java
index bddc4114646..7d1aa8221f4 100644
--- a/lucene/demo/src/java/org/apache/lucene/demo/facet/SandboxFacetsExample.java
+++ b/lucene/demo/src/java/org/apache/lucene/demo/facet/SandboxFacetsExample.java
@@ -17,7 +17,7 @@
package org.apache.lucene.demo.facet;
import static org.apache.lucene.facet.FacetsConfig.DEFAULT_INDEX_FIELD_NAME;
-import static org.apache.lucene.sandbox.facet.ComparableUtils.byAggregatedValue;
+import static org.apache.lucene.sandbox.facet.utils.ComparableUtils.byAggregatedValue;
import java.io.IOException;
import java.util.ArrayList;
@@ -42,7 +42,6 @@ import org.apache.lucene.index.DirectoryReader;
import org.apache.lucene.index.IndexWriter;
import org.apache.lucene.index.IndexWriterConfig;
import org.apache.lucene.index.IndexWriterConfig.OpenMode;
-import org.apache.lucene.sandbox.facet.ComparableUtils;
import org.apache.lucene.sandbox.facet.FacetFieldCollectorManager;
import org.apache.lucene.sandbox.facet.cutters.TaxonomyFacetsCutter;
import org.apache.lucene.sandbox.facet.cutters.ranges.LongRangeFacetCutter;
@@ -56,6 +55,12 @@ import org.apache.lucene.sandbox.facet.recorders.CountFacetRecorder;
import org.apache.lucene.sandbox.facet.recorders.LongAggregationsFacetRecorder;
import org.apache.lucene.sandbox.facet.recorders.MultiFacetsRecorder;
import org.apache.lucene.sandbox.facet.recorders.Reducer;
+import org.apache.lucene.sandbox.facet.utils.ComparableUtils;
+import org.apache.lucene.sandbox.facet.utils.DrillSidewaysFacetOrchestrator;
+import org.apache.lucene.sandbox.facet.utils.FacetBuilder;
+import org.apache.lucene.sandbox.facet.utils.FacetOrchestrator;
+import org.apache.lucene.sandbox.facet.utils.RangeFacetBuilderFactory;
+import org.apache.lucene.sandbox.facet.utils.TaxonomyFacetBuilder;
import org.apache.lucene.search.DoubleValuesSource;
import org.apache.lucene.search.IndexSearcher;
import org.apache.lucene.search.LongValuesSource;
@@ -76,6 +81,7 @@ public class SandboxFacetsExample {
private SandboxFacetsExample() {
config.setHierarchical("Publish Date", true);
+ config.setHierarchical("Author", false);
}
/** Build the example index. */
@@ -130,6 +136,88 @@ public class SandboxFacetsExample {
IOUtils.close(indexWriter, taxoWriter);
}
+ /**
+ * Example for {@link FacetBuilder} usage - simple API that provides results in a format very
+ * similar to classic facets module. It doesn't give all flexibility available with {@link
+ * org.apache.lucene.sandbox.facet.cutters.FacetCutter} and {@link
+ * org.apache.lucene.sandbox.facet.recorders.FacetRecorder} though, see below for lower level API
+ * usage examples.
+ */
+ private List<FacetResult> simpleFacetsWithSearch() throws IOException {
+ //// init readers and searcher
+ DirectoryReader indexReader = DirectoryReader.open(indexDir);
+ IndexSearcher searcher = new IndexSearcher(indexReader);
+ TaxonomyReader taxoReader = new DirectoryTaxonomyReader(taxoDir);
+
+ //// build facets requests
+ FacetBuilder authorFacetBuilder =
+ new TaxonomyFacetBuilder(config, taxoReader, "Author").withTopN(10);
+ FacetBuilder priceFacetBuilder =
+ RangeFacetBuilderFactory.forLongRanges(
+ "Price",
+ new LongRange("0-10", 0, true, 10, true),
+ new LongRange("10-20", 10, true, 20, true));
+
+ //// Main hits collector
+ TopScoreDocCollectorManager hitsCollectorManager =
+ new TopScoreDocCollectorManager(2, Integer.MAX_VALUE);
+
+ //// Search and collect
+ TopDocs topDocs =
+ new FacetOrchestrator()
+ .addBuilder(authorFacetBuilder)
+ .addBuilder(priceFacetBuilder)
+ .collect(new MatchAllDocsQuery(), searcher, hitsCollectorManager);
+ System.out.println(
+ "Search results: totalHits: "
+ + topDocs.totalHits
+ + ", collected hits: "
+ + topDocs.scoreDocs.length);
+
+ //// Results
+ FacetResult authorResults = authorFacetBuilder.getResult();
+ FacetResult rangeResults = priceFacetBuilder.getResult();
+
+ IOUtils.close(indexReader, taxoReader);
+
+ return List.of(authorResults, rangeResults);
+ }
+
+ /** Example for {@link FacetBuilder} usage with {@link DrillSideways}. */
+ private List<FacetResult> simpleFacetsWithDrillSideways() throws IOException {
+ //// init readers and searcher
+ DirectoryReader indexReader = DirectoryReader.open(indexDir);
+ IndexSearcher searcher = new IndexSearcher(indexReader);
+ TaxonomyReader taxoReader = new DirectoryTaxonomyReader(taxoDir);
+ DrillSideways ds = new DrillSideways(searcher, config, taxoReader);
+
+ //// build facets requests
+ FacetBuilder authorFacetBuilder =
+ new TaxonomyFacetBuilder(config, taxoReader, "Author").withTopN(10);
+ FacetBuilder priceFacetBuilder =
+ RangeFacetBuilderFactory.forLongRanges(
+ "Price",
+ new LongRange("0-10", 0, true, 10, true),
+ new LongRange("10-20", 10, true, 20, true));
+
+ //// Build query and collect
+ DrillDownQuery query = new DrillDownQuery(config);
+ query.add("Author", "Lisa");
+
+ new DrillSidewaysFacetOrchestrator()
+ .addDrillDownBuilder(priceFacetBuilder)
+ .addDrillSidewaysBuilder("Author", authorFacetBuilder)
+ .collect(query, ds);
+
+ //// Results
+ FacetResult authorResults = authorFacetBuilder.getResult();
+ FacetResult rangeResults = priceFacetBuilder.getResult();
+
+ IOUtils.close(indexReader, taxoReader);
+
+ return List.of(authorResults, rangeResults);
+ }
+
/** User runs a query and counts facets only without collecting the matching documents. */
List<FacetResult> facetsOnly() throws IOException {
//// (1) init readers and searcher
@@ -643,6 +731,18 @@ public class SandboxFacetsExample {
return facetResults;
}
+ /** Runs the simple search example. */
+ public List<FacetResult> runSimpleFacetsWithSearch() throws IOException {
+ index();
+ return simpleFacetsWithSearch();
+ }
+
+ /** Runs the simple drill sideways example. */
+ public List<FacetResult> runSimpleFacetsWithDrillSideways() throws IOException {
+ index();
+ return simpleFacetsWithDrillSideways();
+ }
+
/** Runs the search example. */
public List<FacetResult> runFacetOnly() throws IOException {
index();
@@ -687,9 +787,22 @@ public class SandboxFacetsExample {
/** Runs the search and drill-down examples and prints the results. */
public static void main(String[] args) throws Exception {
+ SandboxFacetsExample example = new SandboxFacetsExample();
+
+ System.out.println("Simple facet counting example:");
+ System.out.println("---------------------------------------------");
+ for (FacetResult result : example.runSimpleFacetsWithSearch()) {
+ System.out.println(result);
+ }
+
+ System.out.println("Simple facet counting for drill sideways example:");
+ System.out.println("---------------------------------------------");
+ for (FacetResult result : example.runSimpleFacetsWithDrillSideways()) {
+ System.out.println(result);
+ }
+
System.out.println("Facet counting example:");
System.out.println("-----------------------");
- SandboxFacetsExample example = new SandboxFacetsExample();
List<FacetResult> results1 = example.runFacetOnly();
System.out.println("Author: " + results1.get(0));
System.out.println("Publish Date: " + results1.get(1));
diff --git a/lucene/demo/src/java/org/apache/lucene/demo/facet/package-info.java b/lucene/demo/src/java/org/apache/lucene/demo/facet/package-info.java
index 2fbf29999ce..317d944915a 100644
--- a/lucene/demo/src/java/org/apache/lucene/demo/facet/package-info.java
+++ b/lucene/demo/src/java/org/apache/lucene/demo/facet/package-info.java
@@ -41,6 +41,8 @@
* <li><a href="#drill_down">Drill-Down with Regular Facets</a>
* <li><a href="#multi-category_list">Multiple Category Lists</a>
* <li><a href="#sampling">Sampling</a>
+ * <li><a href="#drf">Dynamic Range Facets</a>
+ * <li><a href="#faceting_during_collection">Faceting During Collection</a>
* </ol>
* <li><a href="#concurrent_indexing_search">Concurrent Indexing and Search</a>
* <li><a href="#demo">All demo packages and classes</a>
@@ -391,6 +393,15 @@
* values can be weighted and the number of desired ranges can be specified. To see an example,
* check {@link org.apache.lucene.demo.facet.DynamicRangeFacetsExample}.
*
+ * <h3 id="faceting_during_collection">Faceting During Collection</h3>
+ *
+ * Facets collection described in the document requires two stages. During first stage matching
+ * document IDs are collected by FacetCollector. During second stage, Facets go through matching doc
+ * IDs to aggregate results. There is an experimental module that does it in a single stage by
+ * computing aggregations during collection. The module also enables different aggregation types
+ * (e.g. multiple Long aggregations per path) for all facet types (e.g. taxonomy or range facets).
+ * To see examples, check {@link org.apache.lucene.demo.facet.SandboxFacetsExample}.
+ *
* <h2 id="concurrent_indexing_search">Concurrent Indexing and Search</h2>
*
* <p>Sometimes, indexing is done once, and when the index is fully prepared, searching starts.
diff --git a/lucene/sandbox/src/java/module-info.java b/lucene/sandbox/src/java/module-info.java
index f3fd6199ba3..d79a150fea3 100644
--- a/lucene/sandbox/src/java/module-info.java
+++ b/lucene/sandbox/src/java/module-info.java
@@ -35,6 +35,7 @@ module org.apache.lucene.sandbox {
exports org.apache.lucene.sandbox.facet.cutters;
exports org.apache.lucene.sandbox.facet.labels;
exports org.apache.lucene.sandbox.facet.plain.histograms;
+ exports org.apache.lucene.sandbox.facet.utils;
provides org.apache.lucene.codecs.PostingsFormat with
org.apache.lucene.sandbox.codecs.idversion.IDVersionPostingsFormat;
diff --git a/lucene/sandbox/src/java/org/apache/lucene/sandbox/facet/cutters/TaxonomyFacetsCutter.java b/lucene/sandbox/src/java/org/apache/lucene/sandbox/facet/cutters/TaxonomyFacetsCutter.java
index cbefdd8feeb..6ec684c3a48 100644
--- a/lucene/sandbox/src/java/org/apache/lucene/sandbox/facet/cutters/TaxonomyFacetsCutter.java
+++ b/lucene/sandbox/src/java/org/apache/lucene/sandbox/facet/cutters/TaxonomyFacetsCutter.java
@@ -124,7 +124,8 @@ public final class TaxonomyFacetsCutter implements FacetCutter {
Map.Entry<String, FacetsConfig.DimConfig> ent = dimensions.next();
String dim = ent.getKey();
FacetsConfig.DimConfig ft = ent.getValue();
- if (ft.hierarchical && ft.multiValued == false && ft.indexFieldName.equals(indexFieldName)) {
+ if (ft.multiValued == false && ft.indexFieldName.equals(indexFieldName)) {
+ // ft.hierarchical is ignored - rollup even if path length is only two
dimsToRollup.add(new FacetLabel(dim));
}
}
diff --git a/lucene/sandbox/src/java/org/apache/lucene/sandbox/facet/cutters/ranges/LongRangeFacetCutter.java b/lucene/sandbox/src/java/org/apache/lucene/sandbox/facet/cutters/ranges/LongRangeFacetCutter.java
index 49fd4b1317f..b9518bfca15 100644
--- a/lucene/sandbox/src/java/org/apache/lucene/sandbox/facet/cutters/ranges/LongRangeFacetCutter.java
+++ b/lucene/sandbox/src/java/org/apache/lucene/sandbox/facet/cutters/ranges/LongRangeFacetCutter.java
@@ -31,6 +31,9 @@ import org.apache.lucene.search.LongValuesSource;
/**
* {@link FacetCutter} for ranges of long values. It's based on LongRangeCounter class.
*
+ * <p>TODO: support "total count" facet ordinal - to be able to return {@link
+ * org.apache.lucene.facet.FacetResult#value}
+ *
* @lucene.experimental
*/
public abstract class LongRangeFacetCutter implements FacetCutter {
diff --git a/lucene/sandbox/src/java/org/apache/lucene/sandbox/facet/iterators/LengthOrdinalIterator.java b/lucene/sandbox/src/java/org/apache/lucene/sandbox/facet/iterators/LengthOrdinalIterator.java
new file mode 100644
index 00000000000..af3114969a2
--- /dev/null
+++ b/lucene/sandbox/src/java/org/apache/lucene/sandbox/facet/iterators/LengthOrdinalIterator.java
@@ -0,0 +1,57 @@
+/*
+ * 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.sandbox.facet.iterators;
+
+import java.io.IOException;
+
+/**
+ * {@link OrdinalIterator} passes through all ordinals as is and counts them.
+ *
+ * @lucene.experimental
+ */
+public final class LengthOrdinalIterator implements OrdinalIterator {
+
+ private final OrdinalIterator delegate;
+ private boolean complete;
+ private int length;
+
+ /** Constructor. */
+ public LengthOrdinalIterator(OrdinalIterator delegate) throws IOException {
+ this.delegate = delegate;
+ }
+
+ /** Number of ordinals in the delegate. */
+ public int length() {
+ if (complete) {
+ return length;
+ } else {
+ throw new IllegalStateException(
+ "You can't request count before the ordinal iterator was consumed");
+ }
+ }
+
+ @Override
+ public int nextOrd() throws IOException {
+ int ord = delegate.nextOrd();
+ if (ord == NO_MORE_ORDS) {
+ complete = true;
+ } else {
+ length++;
+ }
+ return ord;
+ }
+}
diff --git a/lucene/sandbox/src/java/org/apache/lucene/sandbox/facet/package-info.java b/lucene/sandbox/src/java/org/apache/lucene/sandbox/facet/package-info.java
index f0e116bf68c..53836b5e034 100644
--- a/lucene/sandbox/src/java/org/apache/lucene/sandbox/facet/package-info.java
+++ b/lucene/sandbox/src/java/org/apache/lucene/sandbox/facet/package-info.java
@@ -15,13 +15,22 @@
* limitations under the License.
*/
/**
- * Sandbox faceting - Collectors that compute facets. Facet Ordinals/Ids: Each doc may have
- * different facets and therefore, different facet ordinals. For example a book can have Author,
- * Publish Date, Page Count etc. as facets. The specific value for each of these Facets for a book
- * can be mapped to an ordinal. Facet ordinals may be common across different book documents.
- * FacetCutter: Can interpret Facets of a specific type for a doc type and output all the Facet
- * Ordinals for the type for the doc. Facet Recorders: record data per ordinal. Some recorders may
- * compute aggregations and record per ordinal data aggregated across an index.
+ * Sandbox faceting - Collectors that compute facets.
+ *
+ * <p>See {@link org.apache.lucene.sandbox.facet.utils} for simple API for most common use cases.
+ *
+ * <p>Lower level API is based on following concepts:
+ *
+ * <ul>
+ * <li>Facet Ordinals/Ids: Each doc may have different facets and therefore, different facet
+ * ordinals. For example a book can have Author, Publish Date, Page Count etc. as facets. The
+ * specific value for each of these Facets for a book can be mapped to an ordinal. Facet
+ * ordinals may be common across different book documents.
+ * <li>Facet Cutter: Can interpret Facets of a specific type for a doc type and output all the
+ * Facet Ordinals for the type for the doc.
+ * <li>Facet Recorders: record data per ordinal. Some recorders may compute aggregations and
+ * record per ordinal data aggregated across an index.
+ * </ul>
*
* <p>See SandboxFacetsExample for examples.
*
diff --git a/lucene/sandbox/src/java/org/apache/lucene/sandbox/facet/utils/BaseFacetBuilder.java b/lucene/sandbox/src/java/org/apache/lucene/sandbox/facet/utils/BaseFacetBuilder.java
new file mode 100644
index 00000000000..ec6cd2b9a3d
--- /dev/null
+++ b/lucene/sandbox/src/java/org/apache/lucene/sandbox/facet/utils/BaseFacetBuilder.java
@@ -0,0 +1,157 @@
+/*
+ * 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.sandbox.facet.utils;
+
+import java.io.IOException;
+import java.util.function.Supplier;
+import org.apache.lucene.facet.FacetResult;
+import org.apache.lucene.facet.LabelAndValue;
+import org.apache.lucene.facet.taxonomy.FacetLabel;
+import org.apache.lucene.sandbox.facet.FacetFieldCollectorManager;
+import org.apache.lucene.sandbox.facet.cutters.FacetCutter;
+import org.apache.lucene.sandbox.facet.iterators.ComparableSupplier;
+import org.apache.lucene.sandbox.facet.iterators.LengthOrdinalIterator;
+import org.apache.lucene.sandbox.facet.iterators.OrdinalIterator;
+import org.apache.lucene.sandbox.facet.iterators.TopnOrdinalIterator;
+import org.apache.lucene.sandbox.facet.labels.OrdToLabel;
+import org.apache.lucene.sandbox.facet.recorders.CountFacetRecorder;
+
+/**
+ * Base implementation for {@link FacetBuilder}, provides common functionality that can be shared
+ * between different facet types.
+ *
+ * @lucene.experimental
+ */
+abstract class BaseFacetBuilder<C extends BaseFacetBuilder<C>> extends FacetBuilder {
+ final String dimension;
+ final String[] path;
+ CountFacetRecorder countRecorder;
+ private FacetFieldCollectorManager<CountFacetRecorder> collectorManager;
+ private int topN = -1;
+ // Sort results by count by default
+ Supplier<ComparableSupplier<?>> sortOrderSupplier = () -> ComparableUtils.byCount(countRecorder);
+
+ BaseFacetBuilder(String dimension, String... path) {
+ this.dimension = dimension;
+ this.path = path;
+ }
+
+ /** Request top N results only. If not set, return all results. */
+ public final C withTopN(int n) {
+ this.topN = n;
+ return self();
+ }
+
+ /** Sort results by count. */
+ public final C withSortByCount() {
+ this.sortOrderSupplier = () -> ComparableUtils.byCount(countRecorder);
+ return self();
+ }
+
+ /**
+ * Expert: sort results by facet ordinal. Facet ordinal meaning depends on {@link FacetCutter}
+ * implementation.
+ */
+ final C withSortByOrdinal() {
+ this.sortOrderSupplier = ComparableUtils::byOrdinal;
+ return self();
+ }
+
+ /** Get {@link FacetCutter} to be used for this facet builder. */
+ abstract FacetCutter getFacetCutter();
+
+ /** Get {@link OrdToLabel} to be used for this facet builder. */
+ abstract OrdToLabel getOrdToLabel();
+
+ /**
+ * Child classes should override this method to return only face ordinals that match the request
+ * before topN cutoff.
+ */
+ OrdinalIterator getMatchingOrdinalIterator() throws IOException {
+ return countRecorder.recordedOrds();
+ }
+
+ /** Get value for {@link LabelAndValue}. */
+ final Number getValue(int facetOrd) {
+ // TODO: support other aggregations
+ return countRecorder.getCount(facetOrd);
+ }
+
+ /** Get value for {@link FacetResult#value} */
+ abstract Number getOverallValue() throws IOException;
+
+ @Override
+ FacetBuilder initOrReuseCollector(FacetBuilder similar) {
+ // share recorders between FacetBuilders that share CollectorManager
+ // TODO: add support for other aggregation types, e.g. float/int associations
+ // and long aggregations
+ if (similar instanceof BaseFacetBuilder<?> castedSimilar) {
+ this.countRecorder = castedSimilar.countRecorder;
+ return similar;
+ } else {
+ this.countRecorder = new CountFacetRecorder();
+ this.collectorManager = new FacetFieldCollectorManager<>(getFacetCutter(), countRecorder);
+ return this;
+ }
+ }
+
+ @Override
+ final FacetFieldCollectorManager<?> getCollectorManager() {
+ return this.collectorManager;
+ }
+
+ @Override
+ public final FacetResult getResult() {
+ assert countRecorder != null : "must not be called before collect";
+ OrdinalIterator ordinalIterator;
+ try {
+ LengthOrdinalIterator lengthOrdinalIterator =
+ new LengthOrdinalIterator(getMatchingOrdinalIterator());
+ ordinalIterator = lengthOrdinalIterator;
+
+ ComparableSupplier<?> comparableSupplier = sortOrderSupplier.get();
+ int[] ordinalsArray;
+ if (topN != -1) {
+ ordinalsArray =
+ new TopnOrdinalIterator<>(ordinalIterator, comparableSupplier, topN).toArray();
+ } else {
+ ordinalsArray = ordinalIterator.toArray();
+ ComparableUtils.sort(ordinalsArray, comparableSupplier);
+ }
+
+ OrdToLabel ordToLabel = getOrdToLabel();
+ FacetLabel[] labels = ordToLabel.getLabels(ordinalsArray);
+
+ LabelAndValue[] labelsAndValues = new LabelAndValue[labels.length];
+ for (int i = 0; i < ordinalsArray.length; i++) {
+ labelsAndValues[i] =
+ new LabelAndValue(
+ labels[i].lastComponent(),
+ getValue(ordinalsArray[i]),
+ countRecorder.getCount(ordinalsArray[i]));
+ }
+ return new FacetResult(
+ dimension, path, getOverallValue(), labelsAndValues, lengthOrdinalIterator.length());
+
+ } catch (IOException e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ /** Util method to be able to extend this class. */
+ abstract C self();
+}
diff --git a/lucene/sandbox/src/java/org/apache/lucene/sandbox/facet/utils/CommonFacetBuilder.java b/lucene/sandbox/src/java/org/apache/lucene/sandbox/facet/utils/CommonFacetBuilder.java
new file mode 100644
index 00000000000..4b126b38efc
--- /dev/null
+++ b/lucene/sandbox/src/java/org/apache/lucene/sandbox/facet/utils/CommonFacetBuilder.java
@@ -0,0 +1,60 @@
+/*
+ * 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.sandbox.facet.utils;
+
+import org.apache.lucene.sandbox.facet.cutters.FacetCutter;
+import org.apache.lucene.sandbox.facet.labels.OrdToLabel;
+
+/**
+ * Common {@link FacetBuilder} that works with any {@link FacetCutter} and {@link OrdToLabel}
+ * provided by user.
+ *
+ * @lucene.experimental
+ */
+public final class CommonFacetBuilder extends BaseFacetBuilder<CommonFacetBuilder> {
+
+ private final FacetCutter cutter;
+ private final OrdToLabel ordToLabel;
+
+ public CommonFacetBuilder(String dimension, FacetCutter cutter, OrdToLabel ordToLabel) {
+ super(dimension);
+ this.cutter = cutter;
+ this.ordToLabel = ordToLabel;
+ }
+
+ @Override
+ FacetCutter getFacetCutter() {
+ return cutter;
+ }
+
+ @Override
+ Number getOverallValue() {
+ // There is no common way to compute overall value for all facet types
+ return -1;
+ }
+
+ @Override
+ OrdToLabel getOrdToLabel() {
+ return ordToLabel;
+ }
+
+ @Override
+ CommonFacetBuilder self() {
+ return this;
+ }
+}
diff --git a/lucene/sandbox/src/java/org/apache/lucene/sandbox/facet/ComparableUtils.java b/lucene/sandbox/src/java/org/apache/lucene/sandbox/facet/utils/ComparableUtils.java
similarity index 99%
rename from lucene/sandbox/src/java/org/apache/lucene/sandbox/facet/ComparableUtils.java
rename to lucene/sandbox/src/java/org/apache/lucene/sandbox/facet/utils/ComparableUtils.java
index aa6e8fd0775..e5de9ddad9d 100644
--- a/lucene/sandbox/src/java/org/apache/lucene/sandbox/facet/ComparableUtils.java
+++ b/lucene/sandbox/src/java/org/apache/lucene/sandbox/facet/utils/ComparableUtils.java
@@ -14,7 +14,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-package org.apache.lucene.sandbox.facet;
+package org.apache.lucene.sandbox.facet.utils;
import java.io.IOException;
import java.util.ArrayList;
diff --git a/lucene/sandbox/src/java/org/apache/lucene/sandbox/facet/utils/DrillSidewaysFacetOrchestrator.java b/lucene/sandbox/src/java/org/apache/lucene/sandbox/facet/utils/DrillSidewaysFacetOrchestrator.java
new file mode 100644
index 00000000000..fd67f87b3e1
--- /dev/null
+++ b/lucene/sandbox/src/java/org/apache/lucene/sandbox/facet/utils/DrillSidewaysFacetOrchestrator.java
@@ -0,0 +1,131 @@
+/*
+ * 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.sandbox.facet.utils;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import org.apache.lucene.facet.DrillDownQuery;
+import org.apache.lucene.facet.DrillSideways;
+import org.apache.lucene.sandbox.facet.FacetFieldCollector;
+import org.apache.lucene.sandbox.facet.FacetFieldCollectorManager;
+import org.apache.lucene.search.Collector;
+import org.apache.lucene.search.CollectorManager;
+import org.apache.lucene.search.MultiCollectorManager;
+
+/**
+ * Utility class to orchestrate {@link FacetBuilder}s to collect facets using {@link DrillSideways}.
+ *
+ * @lucene.experimental
+ */
+public final class DrillSidewaysFacetOrchestrator {
+ private List<FacetBuilder> drillDownFacetBuilders = new ArrayList<>();
+ private Map<String, Integer> dimToIndex = new HashMap<>();
+ private Map<Integer, List<FacetBuilder>> drillSidewaysFacetBuilders = new HashMap<>();
+
+ public DrillSidewaysFacetOrchestrator() {}
+
+ public DrillSidewaysFacetOrchestrator addDrillSidewaysBuilder(
+ String dim, FacetBuilder facetBuilder) {
+ // TODO: this looks fragile as it duplicates index assignment logic from DrillDownQuery.
+ // Instead we can change DrillSideways API to accept a dimension (String) to collector
+ // manager map instead of a list so that we can be sure that we use the right
+ // collector for the right dimension.
+ // but I think we should do it in a separate PR as it requires changing existing API.
+ int dimIndex = dimToIndex.computeIfAbsent(dim, (x) -> dimToIndex.size());
+ drillSidewaysFacetBuilders
+ .computeIfAbsent(dimIndex, (x) -> new ArrayList<>())
+ .add(facetBuilder);
+ return this;
+ }
+
+ public DrillSidewaysFacetOrchestrator addDrillDownBuilder(FacetBuilder facetBuilder) {
+ drillDownFacetBuilders.add(facetBuilder);
+ return this;
+ }
+
+ public void collect(DrillDownQuery query, DrillSideways drillSideways) throws IOException {
+ collect(query, drillSideways, null);
+ }
+
+ @SuppressWarnings({"unchecked"})
+ public <T> T collect(
+ DrillDownQuery query,
+ DrillSideways drillSideways,
+ CollectorManager<? extends Collector, T> mainCollector)
+ throws IOException {
+ // drill down
+ MultiCollectorManager drillDownManager =
+ FacetOrchestrator.createMainCollector(drillDownFacetBuilders, mainCollector);
+
+ // drill sideways
+ List<VoidFacetFieldCollectorManager> drillSidewaysManagers = new ArrayList<>();
+ for (int i = 0; i < drillSidewaysFacetBuilders.size(); i++) {
+ List<FacetFieldCollectorManager<?>> managers =
+ FacetOrchestrator.collectorManagerForBuilders(drillSidewaysFacetBuilders.get(i));
+ if (managers.size() != 1) {
+ throw new IllegalArgumentException(
+ "Expected exactly one collector manager per dimension but got " + managers.size());
+ }
+ drillSidewaysManagers.add(new VoidFacetFieldCollectorManager(managers.getFirst()));
+ }
+
+ DrillSideways.Result<Object[], ?> result =
+ drillSideways.search(query, drillDownManager, drillSidewaysManagers);
+
+ if (mainCollector != null) {
+ return (T) result.drillDownResult();
+ } else {
+ return null;
+ }
+ }
+
+ /**
+ * Class to hide {@link FacetFieldCollectorManager} return type.
+ *
+ * <p>The reason we need it is that {@link DrillSideways} requires drill sideways dimension
+ * collectors to be of the same type. As a result, we can't use wildcard (?) in the list of
+ * collectors in this class, and consequently, we can't use different {@link
+ * org.apache.lucene.sandbox.facet.recorders.FacetRecorder}s for different dimensions.
+ *
+ * <p>We can do it because each {@link FacetBuilder} keeps references to its {@link
+ * org.apache.lucene.sandbox.facet.recorders.FacetRecorder}(s) so we don't need DrillSideways to
+ * return them.
+ */
+ private static final class VoidFacetFieldCollectorManager
+ implements CollectorManager<FacetFieldCollector, Void> {
+ private final FacetFieldCollectorManager<?> delegate;
+
+ public VoidFacetFieldCollectorManager(FacetFieldCollectorManager<?> delegate) {
+ this.delegate = delegate;
+ }
+
+ @Override
+ public FacetFieldCollector newCollector() throws IOException {
+ return delegate.newCollector();
+ }
+
+ @Override
+ public Void reduce(Collection<FacetFieldCollector> collection) throws IOException {
+ delegate.reduce(collection);
+ return null;
+ }
+ }
+}
diff --git a/lucene/sandbox/src/java/org/apache/lucene/sandbox/facet/utils/FacetBuilder.java b/lucene/sandbox/src/java/org/apache/lucene/sandbox/facet/utils/FacetBuilder.java
new file mode 100644
index 00000000000..cb98ce88035
--- /dev/null
+++ b/lucene/sandbox/src/java/org/apache/lucene/sandbox/facet/utils/FacetBuilder.java
@@ -0,0 +1,64 @@
+/*
+ * 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.sandbox.facet.utils;
+
+import org.apache.lucene.facet.FacetResult;
+import org.apache.lucene.sandbox.facet.FacetFieldCollectorManager;
+
+/**
+ * End-to-end (request, collection and results) management of a single facet request.
+ *
+ * <p>Use {@link CommonFacetBuilder} unless there is a facet type specific implementation, such as
+ * {@link TaxonomyFacetBuilder}. Use with {@link FacetOrchestrator} or {@link
+ * DrillSidewaysFacetOrchestrator}.
+ *
+ * <p>Note that you need separate {@link FacetBuilder} instances even for very similar requests,
+ * e.g. when sort order is the only difference. {@link FacetOrchestrator} and {@link
+ * DrillSidewaysFacetOrchestrator}
+ *
+ * <p>It's an abstract class, not an interface, to define some methods as package private as they
+ * should only be called by {@link FacetOrchestrator} or {@link DrillSidewaysFacetOrchestrator}.
+ *
+ * @lucene.experimental
+ */
+public abstract class FacetBuilder {
+
+ /**
+ * If parameter is null, init attrs required for collection phase. Otherwise, reuse the attributes
+ * from {@link FacetBuilder}, so that we can share {@link
+ * org.apache.lucene.search.CollectorManager} with it.
+ *
+ * @return similar if it is not null, otherwise this.
+ */
+ abstract FacetBuilder initOrReuseCollector(FacetBuilder similar);
+
+ /** Create {@link org.apache.lucene.search.CollectorManager} for this facet request. */
+ abstract FacetFieldCollectorManager<?> getCollectorManager();
+
+ /**
+ * Unique key for collection time. Multiple facet requests can use the same collector, e.g. when
+ * they require counting matches for the same field.
+ *
+ * <p>Default: this, to use unique collector for this request.
+ */
+ Object collectionKey() {
+ return this;
+ }
+
+ /** Get results for this request. */
+ public abstract FacetResult getResult();
+}
diff --git a/lucene/sandbox/src/java/org/apache/lucene/sandbox/facet/utils/FacetOrchestrator.java b/lucene/sandbox/src/java/org/apache/lucene/sandbox/facet/utils/FacetOrchestrator.java
new file mode 100644
index 00000000000..e9da084eab0
--- /dev/null
+++ b/lucene/sandbox/src/java/org/apache/lucene/sandbox/facet/utils/FacetOrchestrator.java
@@ -0,0 +1,99 @@
+/*
+ * 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.sandbox.facet.utils;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import org.apache.lucene.sandbox.facet.FacetFieldCollectorManager;
+import org.apache.lucene.search.Collector;
+import org.apache.lucene.search.CollectorManager;
+import org.apache.lucene.search.IndexSearcher;
+import org.apache.lucene.search.MultiCollectorManager;
+import org.apache.lucene.search.Query;
+
+/**
+ * Utility class to orchestrate {@link FacetBuilder}s to setup and exec collection for multiple
+ * facets.
+ *
+ * @lucene.experimental
+ */
+public final class FacetOrchestrator {
+ private List<FacetBuilder> facetBuilders = new ArrayList<>();
+
+ public FacetOrchestrator() {}
+
+ public FacetOrchestrator addBuilder(FacetBuilder request) {
+ facetBuilders.add(request);
+ return this;
+ }
+
+ public void collect(Query query, IndexSearcher searcher) throws IOException {
+ collect(query, searcher, null);
+ }
+
+ // public FacetBuilder addCollector(CollectorManager<C, T>)
+ @SuppressWarnings({"unchecked"})
+ public <C extends Collector, T> T collect(
+ Query query, IndexSearcher searcher, CollectorManager<C, T> mainCollector)
+ throws IOException {
+ MultiCollectorManager mcm = createMainCollector(facetBuilders, mainCollector);
+ Object[] res = searcher.search(query, mcm);
+ if (mainCollector != null) {
+ return (T) res[0];
+ } else {
+ return null;
+ }
+ }
+
+ @SuppressWarnings({"rawtypes", "unchecked"})
+ static <C extends Collector, T> MultiCollectorManager createMainCollector(
+ List<FacetBuilder> facetBuilders, CollectorManager<C, T> additionalCollectorManager) {
+ // drill down
+ List<FacetFieldCollectorManager<?>> drillDownManagers =
+ collectorManagerForBuilders(facetBuilders);
+ int i = 0;
+ CollectorManager[] managersArray;
+ if (additionalCollectorManager != null) {
+ managersArray = new CollectorManager[drillDownManagers.size() + 1];
+ managersArray[i++] = additionalCollectorManager;
+ } else {
+ managersArray = new CollectorManager[drillDownManagers.size()];
+ }
+ for (FacetFieldCollectorManager<?> m : drillDownManagers) {
+ managersArray[i++] = m;
+ }
+ return new MultiCollectorManager(managersArray);
+ }
+
+ static List<FacetFieldCollectorManager<?>> collectorManagerForBuilders(
+ List<FacetBuilder> facetBuilders) {
+ // Check if we can reuse collectors for some FacetBuilders
+ Map<Object, FacetBuilder> buildersUniqueForCollection = new HashMap<>();
+ for (FacetBuilder builder : facetBuilders) {
+ buildersUniqueForCollection.compute(
+ builder.collectionKey(), (x, v) -> builder.initOrReuseCollector(v));
+ }
+ List<FacetFieldCollectorManager<?>> managers = new ArrayList<>();
+ for (FacetBuilder c : buildersUniqueForCollection.values()) {
+ managers.add(c.getCollectorManager());
+ }
+ return managers;
+ }
+}
diff --git a/lucene/sandbox/src/java/org/apache/lucene/sandbox/facet/utils/LongValueFacetBuilder.java b/lucene/sandbox/src/java/org/apache/lucene/sandbox/facet/utils/LongValueFacetBuilder.java
new file mode 100644
index 00000000000..4ae0edfc4f7
--- /dev/null
+++ b/lucene/sandbox/src/java/org/apache/lucene/sandbox/facet/utils/LongValueFacetBuilder.java
@@ -0,0 +1,82 @@
+/*
+ * 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.sandbox.facet.utils;
+
+import org.apache.lucene.sandbox.facet.cutters.LongValueFacetCutter;
+import org.apache.lucene.sandbox.facet.labels.OrdToLabel;
+
+/**
+ * {@link FacetBuilder} for distinct numeric field values.
+ *
+ * @lucene.experimental
+ */
+public final class LongValueFacetBuilder extends BaseFacetBuilder<LongValueFacetBuilder> {
+
+ private LongValueFacetCutter cutter;
+ private final CollectionKey collectionKey;
+
+ public LongValueFacetBuilder(String field) {
+ super(field);
+ // We can reuse collector manager if it is for the same field
+ collectionKey = new CollectionKey(field);
+ // Sort by field value by default
+ sortOrderSupplier = () -> ComparableUtils.byLongValue(getFacetCutter());
+ }
+
+ @Override
+ LongValueFacetCutter getFacetCutter() {
+ return cutter;
+ }
+
+ @Override
+ OrdToLabel getOrdToLabel() {
+ return cutter;
+ }
+
+ @Override
+ Number getOverallValue() {
+ // TODO: we don't collect overall value anywhere, we should either change cutter to return ord
+ // that corresponse to "and value",
+ // or change CountRecorder itself to collect overall value when needed.
+ return -1;
+ }
+
+ @Override
+ FacetBuilder initOrReuseCollector(FacetBuilder similar) {
+ if (similar == null) {
+ cutter = new LongValueFacetCutter(dimension);
+ } else if (similar instanceof LongValueFacetBuilder castedSimilar) {
+ cutter = castedSimilar.cutter;
+ } else {
+ assert false;
+ }
+
+ return super.initOrReuseCollector(similar);
+ }
+
+ @Override
+ LongValueFacetBuilder self() {
+ return this;
+ }
+
+ private record CollectionKey(String indexFieldName) {}
+
+ @Override
+ Object collectionKey() {
+ return collectionKey;
+ }
+}
diff --git a/lucene/sandbox/src/java/org/apache/lucene/sandbox/facet/utils/RangeFacetBuilderFactory.java b/lucene/sandbox/src/java/org/apache/lucene/sandbox/facet/utils/RangeFacetBuilderFactory.java
new file mode 100644
index 00000000000..8d69acfdc33
--- /dev/null
+++ b/lucene/sandbox/src/java/org/apache/lucene/sandbox/facet/utils/RangeFacetBuilderFactory.java
@@ -0,0 +1,81 @@
+/*
+ * 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.sandbox.facet.utils;
+
+import org.apache.lucene.facet.MultiDoubleValuesSource;
+import org.apache.lucene.facet.MultiLongValuesSource;
+import org.apache.lucene.facet.range.DoubleRange;
+import org.apache.lucene.facet.range.LongRange;
+import org.apache.lucene.sandbox.facet.cutters.ranges.DoubleRangeFacetCutter;
+import org.apache.lucene.sandbox.facet.cutters.ranges.LongRangeFacetCutter;
+import org.apache.lucene.sandbox.facet.labels.RangeOrdToLabel;
+
+/**
+ * {@link FacetBuilder} factory for faceting types base on numeric fields.
+ *
+ * @lucene.experimental
+ */
+public final class RangeFacetBuilderFactory {
+
+ private RangeFacetBuilderFactory() {}
+
+ /** Request long range facets for numeric field by name. */
+ public static CommonFacetBuilder forLongRanges(String field, LongRange... ranges) {
+ return forLongRanges(field, MultiLongValuesSource.fromLongField(field), ranges);
+ }
+
+ /**
+ * Request long range facets for provided {@link MultiLongValuesSource} by default sorted in
+ * original ranges order.
+ *
+ * @param dimension dimension to return in results to match {@link
+ * org.apache.lucene.facet.range.LongRangeFacetCounts#getTopChildren} results
+ * @param valuesSource value source
+ * @param ranges ranges
+ */
+ public static CommonFacetBuilder forLongRanges(
+ String dimension, MultiLongValuesSource valuesSource, LongRange... ranges) {
+ return new CommonFacetBuilder(
+ dimension,
+ LongRangeFacetCutter.create(valuesSource, ranges),
+ new RangeOrdToLabel(ranges))
+ .withSortByOrdinal();
+ }
+
+ /** Request double range facets for numeric field by name. */
+ public static CommonFacetBuilder forDoubleRanges(String field, DoubleRange... ranges) {
+ return forDoubleRanges(field, MultiDoubleValuesSource.fromDoubleField(field), ranges);
+ }
+
+ /**
+ * Request double range facets for provided {@link MultiDoubleValuesSource} by default sorted in
+ * original ranges order.
+ *
+ * @param dimension dimension to return in results to match {@link
+ * org.apache.lucene.facet.range.DoubleRangeFacetCounts#getTopChildren} results
+ * @param valuesSource value source
+ * @param ranges ranges
+ */
+ public static CommonFacetBuilder forDoubleRanges(
+ String dimension, MultiDoubleValuesSource valuesSource, DoubleRange... ranges) {
+ return new CommonFacetBuilder(
+ dimension,
+ new DoubleRangeFacetCutter(valuesSource, ranges),
+ new RangeOrdToLabel(ranges))
+ .withSortByOrdinal();
+ }
+}
diff --git a/lucene/sandbox/src/java/org/apache/lucene/sandbox/facet/utils/TaxonomyFacetBuilder.java b/lucene/sandbox/src/java/org/apache/lucene/sandbox/facet/utils/TaxonomyFacetBuilder.java
new file mode 100644
index 00000000000..beed2b27e6b
--- /dev/null
+++ b/lucene/sandbox/src/java/org/apache/lucene/sandbox/facet/utils/TaxonomyFacetBuilder.java
@@ -0,0 +1,117 @@
+/*
+ * 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.sandbox.facet.utils;
+
+import java.io.IOException;
+import org.apache.lucene.facet.FacetsConfig;
+import org.apache.lucene.facet.taxonomy.FacetLabel;
+import org.apache.lucene.facet.taxonomy.TaxonomyReader;
+import org.apache.lucene.sandbox.facet.cutters.FacetCutter;
+import org.apache.lucene.sandbox.facet.cutters.TaxonomyFacetsCutter;
+import org.apache.lucene.sandbox.facet.iterators.OrdinalIterator;
+import org.apache.lucene.sandbox.facet.iterators.TaxonomyChildrenOrdinalIterator;
+import org.apache.lucene.sandbox.facet.labels.TaxonomyOrdLabelBiMap;
+
+/**
+ * {@link FacetBuilder} for taxonomy facets.
+ *
+ * @lucene.experimental
+ */
+public final class TaxonomyFacetBuilder extends BaseFacetBuilder<TaxonomyFacetBuilder> {
+ private final FacetsConfig facetsConfig;
+ private final FacetsConfig.DimConfig dimConfig;
+ private final TaxonomyReader taxonomyReader;
+ private final String indexFieldName;
+ private final CollectionKey collectionKey;
+
+ // Post-collection vars
+ private TaxonomyOrdLabelBiMap taxoOrdLabels;
+ private int parentOrd = -1;
+
+ public TaxonomyFacetBuilder(
+ FacetsConfig facetsConfig, TaxonomyReader taxonomyReader, String dimension, String... path) {
+ super(dimension, path);
+ if (facetsConfig.isDimConfigured(dimension) == false) {
+ // The reason the dimension config is required is that we want to be able to compute total
+ // value for single value fields that use default config, and to do that we need to rollup
+ // value for the dimension, but TaxonomyFacetsCutter uses FacetConfig#getDimConfigs
+ // to find dimensions that need to be rolled up, hence it needs the dimension to be
+ // configured explicitly.
+ throw new IllegalArgumentException(
+ "Dimension config for "
+ + dimension
+ + " is required."
+ + " Call one of the FacetsConfig's setter with a default value.");
+ }
+ this.facetsConfig = facetsConfig;
+ this.taxonomyReader = taxonomyReader;
+ this.dimConfig = facetsConfig.getDimConfig(dimension);
+ this.indexFieldName = dimConfig.indexFieldName;
+ this.collectionKey = new CollectionKey(indexFieldName);
+ // For taxo facets we sort by count by default
+ this.withSortByCount();
+ }
+
+ private record CollectionKey(String indexFieldName) {}
+
+ @Override
+ Object collectionKey() {
+ return collectionKey;
+ }
+
+ @Override
+ FacetCutter getFacetCutter() {
+ return new TaxonomyFacetsCutter(indexFieldName, facetsConfig, taxonomyReader);
+ }
+
+ private int getParentOrd() throws IOException {
+ if (this.parentOrd < 0) {
+ FacetLabel parentLabel = new FacetLabel(dimension, path);
+ this.parentOrd = getOrdToLabel().getOrd(parentLabel);
+ }
+ return this.parentOrd;
+ }
+
+ @Override
+ OrdinalIterator getMatchingOrdinalIterator() throws IOException {
+ return new TaxonomyChildrenOrdinalIterator(
+ super.getMatchingOrdinalIterator(),
+ taxonomyReader.getParallelTaxonomyArrays().parents(),
+ getParentOrd());
+ }
+
+ @Override
+ Number getOverallValue() throws IOException {
+ if (dimConfig.multiValued == false || dimConfig.requireDimCount) {
+ return getValue(getParentOrd());
+ }
+ return -1; // Can't compute
+ }
+
+ @Override
+ TaxonomyOrdLabelBiMap getOrdToLabel() {
+ if (taxoOrdLabels == null) {
+ taxoOrdLabels = new TaxonomyOrdLabelBiMap(taxonomyReader);
+ }
+ return taxoOrdLabels;
+ }
+
+ @Override
+ TaxonomyFacetBuilder self() {
+ return this;
+ }
+}
diff --git a/lucene/sandbox/src/java/org/apache/lucene/sandbox/facet/utils/package-info.java b/lucene/sandbox/src/java/org/apache/lucene/sandbox/facet/utils/package-info.java
new file mode 100644
index 00000000000..438d8f4148b
--- /dev/null
+++ b/lucene/sandbox/src/java/org/apache/lucene/sandbox/facet/utils/package-info.java
@@ -0,0 +1,55 @@
+/*
+ * 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.
+ */
+/**
+ * Sandbox faceting: utility classes to make it easier to use the new API for standard use cases.
+ *
+ * <p>Create a {@link org.apache.lucene.sandbox.facet.utils.FacetBuilder} for each facet request you
+ * have, use {@link org.apache.lucene.sandbox.facet.utils.FacetOrchestrator} or {@link
+ * org.apache.lucene.sandbox.facet.utils.DrillSidewaysFacetOrchestrator} to complete facet requests
+ * for a query, and call {@link org.apache.lucene.sandbox.facet.utils.FacetBuilder#getResult()} to
+ * get final results.
+ *
+ * <p>Which {@link org.apache.lucene.sandbox.facet.utils.FacetBuilder} to use?
+ *
+ * <ul>
+ * <li>Use implementation specific to your use case, e.g. {@link
+ * org.apache.lucene.sandbox.facet.utils.TaxonomyFacetBuilder} for taxonomy fields ({@link
+ * org.apache.lucene.facet.FacetField}), {@link
+ * org.apache.lucene.sandbox.facet.utils.RangeFacetBuilderFactory} for range facets, {@link
+ * org.apache.lucene.sandbox.facet.utils.LongValueFacetBuilder} for numeric fields.
+ * <li>Use {@link org.apache.lucene.sandbox.facet.utils.CommonFacetBuilder} for other cases where
+ * you can provide {@link org.apache.lucene.sandbox.facet.cutters.FacetCutter} and {@link
+ * org.apache.lucene.sandbox.facet.labels.OrdToLabel}.
+ * </ul>
+ *
+ * <p>There is no implementation for your use case?
+ *
+ * <ul>
+ * <li>Implement {@link org.apache.lucene.sandbox.facet.cutters.FacetCutter}, {@link
+ * org.apache.lucene.sandbox.facet.labels.OrdToLabel} and/or {@link
+ * org.apache.lucene.sandbox.facet.recorders.FacetRecorder} for your use case. See {@link
+ * org.apache.lucene.sandbox.facet} for low level API overview.
+ * <li>Implement {@link org.apache.lucene.sandbox.facet.utils.FacetBuilder} for your use case
+ * unless {@link org.apache.lucene.sandbox.facet.utils.CommonFacetBuilder} is sufficient, or
+ * use low level API directly.
+ * </ul>
+ *
+ * <p>See SandboxFacetsExample for examples.
+ *
+ * @lucene.experimental
+ */
+package org.apache.lucene.sandbox.facet.utils;
diff --git a/lucene/sandbox/src/test/org/apache/lucene/sandbox/facet/SandboxFacetTestCase.java b/lucene/sandbox/src/test/org/apache/lucene/sandbox/facet/SandboxFacetTestCase.java
index 560a3d4bc30..a72264bef5f 100644
--- a/lucene/sandbox/src/test/org/apache/lucene/sandbox/facet/SandboxFacetTestCase.java
+++ b/lucene/sandbox/src/test/org/apache/lucene/sandbox/facet/SandboxFacetTestCase.java
@@ -24,6 +24,7 @@ import org.apache.lucene.facet.FacetResult;
import org.apache.lucene.facet.LabelAndValue;
import org.apache.lucene.facet.taxonomy.FacetLabel;
import org.apache.lucene.facet.taxonomy.TaxonomyReader;
+import org.apache.lucene.index.IndexReader;
import org.apache.lucene.sandbox.facet.iterators.CandidateSetOrdinalIterator;
import org.apache.lucene.sandbox.facet.iterators.ComparableSupplier;
import org.apache.lucene.sandbox.facet.iterators.OrdinalIterator;
@@ -32,6 +33,8 @@ import org.apache.lucene.sandbox.facet.iterators.TopnOrdinalIterator;
import org.apache.lucene.sandbox.facet.labels.OrdToLabel;
import org.apache.lucene.sandbox.facet.labels.TaxonomyOrdLabelBiMap;
import org.apache.lucene.sandbox.facet.recorders.CountFacetRecorder;
+import org.apache.lucene.sandbox.facet.utils.ComparableUtils;
+import org.apache.lucene.search.IndexSearcher;
import org.apache.lucene.tests.util.LuceneTestCase;
public abstract class SandboxFacetTestCase extends LuceneTestCase {
@@ -195,4 +198,14 @@ public abstract class SandboxFacetTestCase extends LuceneTestCase {
}
return counts;
}
+
+ protected IndexSearcher getNewSearcherForDrillSideways(IndexReader reader) {
+ // Do not wrap with an asserting searcher, since DrillSidewaysQuery doesn't
+ // implement all the required components like Weight#scorer.
+ IndexSearcher searcher = newSearcher(reader, true, false, Concurrency.INTER_SEGMENT);
+ // DrillSideways requires the entire range of docs to be scored at once, so it doesn't support
+ // timeouts whose implementation scores one window of doc IDs at a time.
+ searcher.setTimeout(null);
+ return searcher;
+ }
}
diff --git a/lucene/sandbox/src/test/org/apache/lucene/sandbox/facet/TestFacetRecorders.java b/lucene/sandbox/src/test/org/apache/lucene/sandbox/facet/TestFacetRecorders.java
index 41d73e16f98..b6262707a92 100644
--- a/lucene/sandbox/src/test/org/apache/lucene/sandbox/facet/TestFacetRecorders.java
+++ b/lucene/sandbox/src/test/org/apache/lucene/sandbox/facet/TestFacetRecorders.java
@@ -45,6 +45,7 @@ import org.apache.lucene.sandbox.facet.recorders.FacetRecorder;
import org.apache.lucene.sandbox.facet.recorders.LongAggregationsFacetRecorder;
import org.apache.lucene.sandbox.facet.recorders.MultiFacetsRecorder;
import org.apache.lucene.sandbox.facet.recorders.Reducer;
+import org.apache.lucene.sandbox.facet.utils.ComparableUtils;
import org.apache.lucene.search.DoubleValuesSource;
import org.apache.lucene.search.IndexSearcher;
import org.apache.lucene.search.LongValuesSource;
diff --git a/lucene/sandbox/src/test/org/apache/lucene/sandbox/facet/TestLongValueFacet.java b/lucene/sandbox/src/test/org/apache/lucene/sandbox/facet/TestLongValueFacet.java
index 2c2f82fc9e6..929036f16b0 100644
--- a/lucene/sandbox/src/test/org/apache/lucene/sandbox/facet/TestLongValueFacet.java
+++ b/lucene/sandbox/src/test/org/apache/lucene/sandbox/facet/TestLongValueFacet.java
@@ -38,6 +38,7 @@ import org.apache.lucene.sandbox.facet.iterators.ComparableSupplier;
import org.apache.lucene.sandbox.facet.iterators.OrdinalIterator;
import org.apache.lucene.sandbox.facet.iterators.TopnOrdinalIterator;
import org.apache.lucene.sandbox.facet.recorders.CountFacetRecorder;
+import org.apache.lucene.sandbox.facet.utils.ComparableUtils;
import org.apache.lucene.search.IndexSearcher;
import org.apache.lucene.search.MatchAllDocsQuery;
import org.apache.lucene.store.Directory;
diff --git a/lucene/sandbox/src/test/org/apache/lucene/sandbox/facet/TestRangeFacet.java b/lucene/sandbox/src/test/org/apache/lucene/sandbox/facet/TestRangeFacet.java
index 94b3281847e..3a91ef3af84 100644
--- a/lucene/sandbox/src/test/org/apache/lucene/sandbox/facet/TestRangeFacet.java
+++ b/lucene/sandbox/src/test/org/apache/lucene/sandbox/facet/TestRangeFacet.java
@@ -493,7 +493,7 @@ public class TestRangeFacet extends SandboxFacetTestCase {
final IndexReader r = w.getReader();
final TaxonomyReader tr = new DirectoryTaxonomyReader(tw);
- IndexSearcher s = newSearcher(r, false, false, Concurrency.INTER_SEGMENT);
+ IndexSearcher s = getNewSearcherForDrillSideways(r);
// DrillSideways requires the entire range of docs to be scored at once, so it doesn't support
// timeouts whose implementation scores one window of doc IDs at a time.
s.setTimeout(null);
@@ -1555,7 +1555,7 @@ public class TestRangeFacet extends SandboxFacetTestCase {
IndexReader r = writer.getReader();
- IndexSearcher s = newSearcher(r, false, false, Concurrency.INTER_SEGMENT);
+ IndexSearcher s = getNewSearcherForDrillSideways(r);
// DrillSideways requires the entire range of docs to be scored at once, so it doesn't support
// timeouts whose implementation scores one window of doc IDs at a time.
s.setTimeout(null);
diff --git a/lucene/sandbox/src/test/org/apache/lucene/sandbox/facet/utils/TestFacetOrchestrators.java b/lucene/sandbox/src/test/org/apache/lucene/sandbox/facet/utils/TestFacetOrchestrators.java
new file mode 100644
index 00000000000..1467a880983
--- /dev/null
+++ b/lucene/sandbox/src/test/org/apache/lucene/sandbox/facet/utils/TestFacetOrchestrators.java
@@ -0,0 +1,456 @@
+/*
+ * 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.sandbox.facet.utils;
+
+import java.io.IOException;
+import java.util.List;
+import org.apache.lucene.document.Document;
+import org.apache.lucene.document.DoubleDocValuesField;
+import org.apache.lucene.document.LongPoint;
+import org.apache.lucene.document.NumericDocValuesField;
+import org.apache.lucene.facet.*;
+import org.apache.lucene.facet.range.DoubleRange;
+import org.apache.lucene.facet.range.LongRange;
+import org.apache.lucene.facet.taxonomy.TaxonomyReader;
+import org.apache.lucene.facet.taxonomy.directory.DirectoryTaxonomyReader;
+import org.apache.lucene.facet.taxonomy.directory.DirectoryTaxonomyWriter;
+import org.apache.lucene.index.IndexReader;
+import org.apache.lucene.index.IndexWriterConfig;
+import org.apache.lucene.sandbox.facet.SandboxFacetTestCase;
+import org.apache.lucene.search.IndexSearcher;
+import org.apache.lucene.search.MatchAllDocsQuery;
+import org.apache.lucene.search.Query;
+import org.apache.lucene.store.Directory;
+import org.apache.lucene.tests.index.RandomIndexWriter;
+import org.apache.lucene.util.IOUtils;
+
+public class TestFacetOrchestrators extends SandboxFacetTestCase {
+ public void testTaxonomyFacets() throws Exception {
+ Directory dir = newDirectory();
+ Directory taxoDir = newDirectory();
+
+ // Writes facet ords to a separate directory from the
+ // main index:
+ DirectoryTaxonomyWriter taxoWriter =
+ new DirectoryTaxonomyWriter(taxoDir, IndexWriterConfig.OpenMode.CREATE);
+
+ FacetsConfig config = new FacetsConfig();
+ config.setHierarchical("Publish Date", true);
+ config.setHierarchical("Author", false);
+
+ RandomIndexWriter writer = new RandomIndexWriter(random(), dir);
+
+ Document doc = new Document();
+ doc.add(new FacetField("Author", "Bob"));
+ doc.add(new FacetField("Publish Date", "2010", "10", "15"));
+ writer.addDocument(config.build(taxoWriter, doc));
+
+ doc = new Document();
+ doc.add(new FacetField("Author", "Lisa"));
+ doc.add(new FacetField("Publish Date", "2010", "10", "20"));
+ writer.addDocument(config.build(taxoWriter, doc));
+
+ doc = new Document();
+ doc.add(new FacetField("Author", "Lisa"));
+ doc.add(new FacetField("Publish Date", "2012", "1", "1"));
+ writer.addDocument(config.build(taxoWriter, doc));
+
+ doc = new Document();
+ doc.add(new FacetField("Author", "Susan"));
+ doc.add(new FacetField("Publish Date", "2012", "1", "7"));
+ writer.addDocument(config.build(taxoWriter, doc));
+
+ doc = new Document();
+ doc.add(new FacetField("Author", "Frank"));
+ doc.add(new FacetField("Publish Date", "1999", "5", "5"));
+ writer.addDocument(config.build(taxoWriter, doc));
+
+ // NRT open
+ IndexSearcher searcher = getNewSearcherForDrillSideways(writer.getReader());
+
+ // NRT open
+ TaxonomyReader taxoReader = new DirectoryTaxonomyReader(taxoWriter);
+
+ Query query = new MatchAllDocsQuery();
+
+ FacetBuilder authorTop0Builder =
+ new TaxonomyFacetBuilder(config, taxoReader, "Author").withTopN(0);
+ FacetBuilder authorTop10Builder =
+ new TaxonomyFacetBuilder(config, taxoReader, "Author").withTopN(10);
+ FacetBuilder authorAllBuilder =
+ new TaxonomyFacetBuilder(config, taxoReader, "Author").withTopN(10);
+ FacetBuilder publishDateTop10Builder =
+ new TaxonomyFacetBuilder(config, taxoReader, "Publish Date").withTopN(10);
+ FacetBuilder publishDateAllBuilder =
+ new TaxonomyFacetBuilder(config, taxoReader, "Publish Date");
+ expectThrows(
+ IllegalArgumentException.class,
+ () -> {
+ new TaxonomyFacetBuilder(config, taxoReader, "Not configured dimension");
+ });
+
+ assertEquals(
+ "There must be only one collector because all TaxonomyFacetBuilder use the same index field",
+ 1,
+ FacetOrchestrator.collectorManagerForBuilders(
+ List.of(
+ authorTop0Builder,
+ authorTop10Builder,
+ authorAllBuilder,
+ publishDateTop10Builder,
+ publishDateAllBuilder))
+ .size());
+
+ new FacetOrchestrator()
+ .addBuilder(authorTop0Builder)
+ .addBuilder(authorTop10Builder)
+ .addBuilder(authorAllBuilder)
+ .addBuilder(publishDateTop10Builder)
+ .addBuilder(publishDateAllBuilder)
+ .collect(query, searcher);
+
+ expectThrows(
+ IllegalArgumentException.class,
+ () -> {
+ authorTop0Builder.getResult();
+ });
+
+ // Retrieve & verify results:
+ assertEquals(
+ "dim=Publish Date path=[] value=5 childCount=3\n 2010 (2)\n 2012 (2)\n 1999 (1)\n",
+ publishDateTop10Builder.getResult().toString());
+ assertEquals(
+ "dim=Author path=[] value=5 childCount=4\n Lisa (2)\n Bob (1)\n Susan (1)\n Frank (1)\n",
+ authorTop10Builder.getResult().toString());
+
+ assertFacetResult(
+ publishDateAllBuilder.getResult(),
+ "Publish Date",
+ new String[0],
+ 3,
+ 5,
+ new LabelAndValue("1999", 1),
+ new LabelAndValue("2010", 2),
+ new LabelAndValue("2012", 2));
+
+ assertFacetResult(
+ authorAllBuilder.getResult(),
+ "Author",
+ new String[0],
+ 4,
+ 5,
+ new LabelAndValue("Bob", 1),
+ new LabelAndValue("Frank", 1),
+ new LabelAndValue("Lisa", 2),
+ new LabelAndValue("Susan", 1));
+
+ // Now use drill sideways
+ authorTop10Builder = new TaxonomyFacetBuilder(config, taxoReader, "Author").withTopN(10);
+ publishDateTop10Builder =
+ new TaxonomyFacetBuilder(config, taxoReader, "Publish Date").withTopN(10);
+ DrillSidewaysFacetOrchestrator drillSidewaysFacetOrchestrator =
+ new DrillSidewaysFacetOrchestrator();
+ drillSidewaysFacetOrchestrator.addDrillDownBuilder(authorTop10Builder);
+ DrillDownQuery q2 = new DrillDownQuery(config);
+ q2.add("Publish Date", "2010");
+ drillSidewaysFacetOrchestrator.addDrillSidewaysBuilder("Publish Date", publishDateTop10Builder);
+
+ DrillSideways ds =
+ new DrillSideways(searcher, config, taxoReader) {
+ @Override
+ protected boolean scoreSubDocsAtOnce() {
+ return random().nextBoolean();
+ }
+ };
+ drillSidewaysFacetOrchestrator.collect(q2, ds);
+
+ // For Author it is drill down
+ assertEquals(
+ "dim=Author path=[] value=2 childCount=2\n" + " Bob (1)\n" + " Lisa (1)\n",
+ authorTop10Builder.getResult().toString());
+ // For Publish Date it is drill sideways
+ assertEquals(
+ "dim=Publish Date path=[] value=5 childCount=3\n"
+ + " 2010 (2)\n"
+ + " 2012 (2)\n"
+ + " 1999 (1)\n",
+ publishDateTop10Builder.getResult().toString());
+
+ writer.close();
+ IOUtils.close(taxoWriter, searcher.getIndexReader(), taxoReader, taxoDir, dir);
+ }
+
+ /** Tests mix of long range, double range and taxonomy facets. */
+ public void testMixedRangeAndNonRangeTaxonomy() throws IOException {
+ Directory d = newDirectory();
+ RandomIndexWriter w = new RandomIndexWriter(random(), d);
+ Directory td = newDirectory();
+ DirectoryTaxonomyWriter tw = new DirectoryTaxonomyWriter(td, IndexWriterConfig.OpenMode.CREATE);
+
+ FacetsConfig config = new FacetsConfig();
+ config.setRequireDimCount("dim", false);
+
+ for (long l = 0; l < 100; l++) {
+ Document doc = new Document();
+ // For computing range facet counts:
+ doc.add(new NumericDocValuesField("longField", l));
+ // For drill down by numeric range:
+ doc.add(new LongPoint("longField", l));
+ // For double range facets
+ doc.add(new DoubleDocValuesField("doubleField", (double) l / 10));
+
+ if ((l & 3) == 0) {
+ doc.add(new FacetField("dim", "a"));
+ } else {
+ doc.add(new FacetField("dim", "b"));
+ }
+ w.addDocument(config.build(tw, doc));
+ }
+
+ final IndexReader r = w.getReader();
+ final TaxonomyReader tr = new DirectoryTaxonomyReader(tw);
+
+ IndexSearcher s = getNewSearcherForDrillSideways(r);
+ // DrillSideways requires the entire range of docs to be scored at once, so it doesn't support
+ // timeouts whose implementation scores one window of doc IDs at a time.
+ s.setTimeout(null);
+
+ if (VERBOSE) {
+ System.out.println("TEST: searcher=" + s);
+ }
+
+ DrillSideways ds =
+ new DrillSideways(s, config, tr) {
+ @Override
+ protected boolean scoreSubDocsAtOnce() {
+ return random().nextBoolean();
+ }
+ };
+
+ // Data for range facets
+ LongRange[] longRanges =
+ new LongRange[] {
+ new LongRange("less than 10", 0L, true, 10L, false),
+ new LongRange("less than or equal to 10", 0L, true, 10L, true),
+ new LongRange("over 90", 90L, false, 100L, false),
+ new LongRange("90 or above", 90L, true, 100L, false),
+ new LongRange("over 1000", 1000L, false, Long.MAX_VALUE, false)
+ };
+ DoubleRange[] doubleRanges =
+ new DoubleRange[] {
+ new DoubleRange("less than 1.0", 0.0, true, 1.0, false),
+ new DoubleRange("less than or equal to 1.0", 0.0, true, 1.0, true),
+ new DoubleRange("over 9.0", 9.0, false, 10.0, false),
+ new DoubleRange("9.0 or above", 9.0, true, 10.0, false),
+ new DoubleRange("over 10.0", 10.0, false, Double.POSITIVE_INFINITY, false)
+ };
+
+ // Search with IndexSearcher
+ FacetBuilder longRangeFacetBuilder =
+ RangeFacetBuilderFactory.forLongRanges("longField", longRanges);
+ FacetBuilder doubleRangeFacetBuilder =
+ RangeFacetBuilderFactory.forDoubleRanges("doubleField", doubleRanges)
+ .withSortByCount()
+ .withTopN(3);
+ FacetBuilder taxonomyFacetBuilder = new TaxonomyFacetBuilder(config, tr, "dim").withTopN(10);
+
+ new FacetOrchestrator()
+ .addBuilder(taxonomyFacetBuilder)
+ .addBuilder(longRangeFacetBuilder)
+ .addBuilder(doubleRangeFacetBuilder)
+ .collect(new MatchAllDocsQuery(), s);
+ assertEquals(
+ "dim=dim path=[] value=100 childCount=2\n b (75)\n a (25)\n",
+ taxonomyFacetBuilder.getResult().toString());
+ assertEquals(
+ "dim=longField path=[] value=-1 childCount=4\n"
+ + " less than 10 (10)\n"
+ + " less than or equal to 10 (11)\n"
+ + " over 90 (9)\n"
+ + " 90 or above (10)\n",
+ longRangeFacetBuilder.getResult().toString());
+ assertEquals(
+ "dim=doubleField path=[] value=-1 childCount=4\n"
+ + " less than or equal to 1.0 (11)\n"
+ + " less than 1.0 (10)\n"
+ + " 9.0 or above (10)\n",
+ doubleRangeFacetBuilder.getResult().toString());
+
+ // Now search with DrillDownQuery
+ longRangeFacetBuilder = RangeFacetBuilderFactory.forLongRanges("longField", longRanges);
+ doubleRangeFacetBuilder =
+ RangeFacetBuilderFactory.forDoubleRanges("doubleField", doubleRanges)
+ .withSortByCount()
+ .withTopN(3);
+ taxonomyFacetBuilder = new TaxonomyFacetBuilder(config, tr, "dim").withTopN(10);
+
+ new DrillSidewaysFacetOrchestrator()
+ .addDrillDownBuilder(taxonomyFacetBuilder)
+ .addDrillDownBuilder(longRangeFacetBuilder)
+ .addDrillDownBuilder(doubleRangeFacetBuilder)
+ .collect(new DrillDownQuery(config), ds);
+ assertEquals(
+ "dim=dim path=[] value=100 childCount=2\n b (75)\n a (25)\n",
+ taxonomyFacetBuilder.getResult().toString());
+ assertEquals(
+ "dim=longField path=[] value=-1 childCount=4\n"
+ + " less than 10 (10)\n"
+ + " less than or equal to 10 (11)\n"
+ + " over 90 (9)\n"
+ + " 90 or above (10)\n",
+ longRangeFacetBuilder.getResult().toString());
+ assertEquals(
+ "dim=doubleField path=[] value=-1 childCount=4\n"
+ + " less than or equal to 1.0 (11)\n"
+ + " less than 1.0 (10)\n"
+ + " 9.0 or above (10)\n",
+ doubleRangeFacetBuilder.getResult().toString());
+
+ // Now search with DrillDownQuery, with drill down on dim=b
+ longRangeFacetBuilder = RangeFacetBuilderFactory.forLongRanges("longField", longRanges);
+ doubleRangeFacetBuilder =
+ RangeFacetBuilderFactory.forDoubleRanges("doubleField", doubleRanges)
+ .withSortByCount()
+ .withTopN(3);
+ taxonomyFacetBuilder = new TaxonomyFacetBuilder(config, tr, "dim").withTopN(10);
+
+ DrillDownQuery query = new DrillDownQuery(config);
+ query.add("dim", "b");
+ new DrillSidewaysFacetOrchestrator()
+ .addDrillDownBuilder(longRangeFacetBuilder)
+ .addDrillDownBuilder(doubleRangeFacetBuilder)
+ .addDrillSidewaysBuilder("dim", taxonomyFacetBuilder)
+ .collect(query, ds);
+ assertEquals(
+ "dim=dim path=[] value=100 childCount=2\n b (75)\n a (25)\n",
+ taxonomyFacetBuilder.getResult().toString());
+ assertEquals(
+ "dim=longField path=[] value=-1 childCount=4\n"
+ + " less than 10 (7)\n"
+ + " less than or equal to 10 (8)\n"
+ + " over 90 (7)\n"
+ + " 90 or above (8)\n",
+ longRangeFacetBuilder.getResult().toString());
+ assertEquals(
+ "dim=doubleField path=[] value=-1 childCount=4\n"
+ + " less than or equal to 1.0 (8)\n"
+ + " 9.0 or above (8)\n"
+ + " less than 1.0 (7)\n",
+ doubleRangeFacetBuilder.getResult().toString());
+
+ // Now search with DrillDownQuery, with drill down on longField range "less than or equal to
+ // 10":
+ longRangeFacetBuilder = RangeFacetBuilderFactory.forLongRanges("longField", longRanges);
+ doubleRangeFacetBuilder =
+ RangeFacetBuilderFactory.forDoubleRanges("doubleField", doubleRanges)
+ .withSortByCount()
+ .withTopN(3);
+ taxonomyFacetBuilder = new TaxonomyFacetBuilder(config, tr, "dim").withTopN(10);
+
+ query = new DrillDownQuery(config);
+ query.add("longField", LongPoint.newRangeQuery("longField", 0L, 10L));
+ new DrillSidewaysFacetOrchestrator()
+ .addDrillDownBuilder(taxonomyFacetBuilder)
+ .addDrillDownBuilder(doubleRangeFacetBuilder)
+ .addDrillSidewaysBuilder("longField", longRangeFacetBuilder)
+ .collect(query, ds);
+ assertEquals(
+ "dim=dim path=[] value=11 childCount=2\n" + " b (8)\n" + " a (3)\n",
+ taxonomyFacetBuilder.getResult().toString());
+ assertEquals(
+ "dim=longField path=[] value=-1 childCount=4\n"
+ + " less than 10 (10)\n"
+ + " less than or equal to 10 (11)\n"
+ + " over 90 (9)\n"
+ + " 90 or above (10)\n",
+ longRangeFacetBuilder.getResult().toString());
+ assertEquals(
+ "dim=doubleField path=[] value=-1 childCount=2\n"
+ + " less than or equal to 1.0 (11)\n"
+ + " less than 1.0 (10)\n",
+ doubleRangeFacetBuilder.getResult().toString());
+
+ w.close();
+ IOUtils.close(tw, tr, td, r, d);
+ }
+
+ public void testLongValues() throws Exception {
+ Directory d = newDirectory();
+ RandomIndexWriter w = new RandomIndexWriter(random(), d);
+ for (long l = 0; l < 5; l++) {
+ for (long ll = 0; ll < l + 10; ll++) {
+ Document doc = new Document();
+ doc.add(new NumericDocValuesField("field", l));
+ w.addDocument(doc);
+ }
+ }
+
+ // Also add Long.MAX_VALUE
+ Document doc = new Document();
+ doc.add(new NumericDocValuesField("field", Long.MAX_VALUE));
+ w.addDocument(doc);
+
+ IndexReader r = w.getReader();
+ w.close();
+
+ IndexSearcher s = newSearcher(r);
+ FacetBuilder allChildrenSortByValue = new LongValueFacetBuilder("field");
+ FacetBuilder allChildrenSortByCount = new LongValueFacetBuilder("field").withSortByCount();
+ FacetBuilder topChildrenSortByCount =
+ new LongValueFacetBuilder("field").withSortByCount().withTopN(2);
+
+ assertEquals(
+ "There must be only one collector for all builders since they all use the same field",
+ 1,
+ FacetOrchestrator.collectorManagerForBuilders(
+ List.of(allChildrenSortByCount, allChildrenSortByValue, topChildrenSortByCount))
+ .size());
+
+ new FacetOrchestrator()
+ .addBuilder(allChildrenSortByValue)
+ .addBuilder(allChildrenSortByCount)
+ .addBuilder(topChildrenSortByCount)
+ .collect(new MatchAllDocsQuery(), s);
+
+ assertEquals(
+ "dim=field path=[] value=-1 childCount=6\n"
+ + " 0 (10)\n"
+ + " 1 (11)\n"
+ + " 2 (12)\n"
+ + " 3 (13)\n"
+ + " 4 (14)\n"
+ + " 9223372036854775807 (1)\n",
+ allChildrenSortByValue.getResult().toString());
+
+ assertEquals(
+ "dim=field path=[] value=-1 childCount=6\n" + " 4 (14)\n" + " 3 (13)\n",
+ topChildrenSortByCount.getResult().toString());
+
+ assertEquals(
+ "dim=field path=[] value=-1 childCount=6\n"
+ + " 4 (14)\n"
+ + " 3 (13)\n"
+ + " 2 (12)\n"
+ + " 1 (11)\n"
+ + " 0 (10)\n"
+ + " 9223372036854775807 (1)\n",
+ allChildrenSortByCount.getResult().toString());
+
+ r.close();
+ d.close();
+ }
+}