This is an automated email from the ASF dual-hosted git repository.
Jackie-Jiang pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/pinot.git
The following commit(s) were added to refs/heads/master by this push:
new 67b3bca2f2f Support star-tree index on RAW forward index with
separated dictionary with predicate queries (#19153)
67b3bca2f2f is described below
commit 67b3bca2f2fb172f33c07cb761582b8c91b12414
Author: Chaitanya Deepthi <[email protected]>
AuthorDate: Fri Aug 7 11:05:19 2026 -0700
Support star-tree index on RAW forward index with separated dictionary with
predicate queries (#19153)
---
.../apache/pinot/core/startree/StarTreeUtils.java | 17 +-
.../pinot/core/startree/StarTreeUtilsTest.java | 243 +++++++++++++++++++++
.../pinot/core/startree/v2/BaseStarTreeV2Test.java | 36 +--
.../v2/RawWithDictionaryStarTreeV2Test.java | 92 ++++++++
4 files changed, 371 insertions(+), 17 deletions(-)
diff --git
a/pinot-core/src/main/java/org/apache/pinot/core/startree/StarTreeUtils.java
b/pinot-core/src/main/java/org/apache/pinot/core/startree/StarTreeUtils.java
index d870ff6dbd8..e9352c5799f 100644
--- a/pinot-core/src/main/java/org/apache/pinot/core/startree/StarTreeUtils.java
+++ b/pinot-core/src/main/java/org/apache/pinot/core/startree/StarTreeUtils.java
@@ -18,6 +18,7 @@
*/
package org.apache.pinot.core.startree;
+import com.google.common.annotations.VisibleForTesting;
import it.unimi.dsi.fastutil.objects.ObjectBooleanPair;
import java.util.ArrayDeque;
import java.util.ArrayList;
@@ -34,6 +35,7 @@ import org.apache.pinot.common.request.context.FilterContext;
import org.apache.pinot.common.request.context.predicate.Predicate;
import org.apache.pinot.core.operator.BaseProjectOperator;
import org.apache.pinot.core.operator.filter.predicate.PredicateEvaluator;
+import
org.apache.pinot.core.operator.filter.predicate.PredicateEvaluatorProvider;
import org.apache.pinot.core.query.aggregation.function.AggregationFunction;
import
org.apache.pinot.core.query.aggregation.function.AggregationFunctionUtils;
import org.apache.pinot.core.query.request.context.QueryContext;
@@ -335,12 +337,25 @@ public class StarTreeUtils {
}
for (Pair<Predicate, PredicateEvaluator> pair :
predicatesEvaluatorMapping) {
if (pair.getKey() == predicate) {
- return pair.getValue();
+ return toDictionaryBased(pair.getValue(), predicate, dataSource);
}
}
return null;
}
+ /// Star-tree traversal reads dictionary ids; a raw-value evaluator (built
when the forward index is RAW and no
+ /// dict-consuming scan operator was available) would throw from
`getMatchingDictIds` / `applySV(int)`. Rebuild
+ /// against the segment dictionary when needed.
+ @VisibleForTesting
+ static PredicateEvaluator toDictionaryBased(PredicateEvaluator evaluator,
Predicate predicate,
+ DataSource dataSource) {
+ if (evaluator.isDictionaryBased()) {
+ return evaluator;
+ }
+ return PredicateEvaluatorProvider.getPredicateEvaluator(predicate,
dataSource.getDictionary(),
+ dataSource.getDataSourceMetadata().getDataType(), null);
+ }
+
/// Returns a [BaseProjectOperator] when the filter can be solved with
star-tree, or `null` otherwise.
@Nullable
public static BaseProjectOperator<?>
createStarTreeBasedProjectOperator(IndexSegment indexSegment,
diff --git
a/pinot-core/src/test/java/org/apache/pinot/core/startree/StarTreeUtilsTest.java
b/pinot-core/src/test/java/org/apache/pinot/core/startree/StarTreeUtilsTest.java
new file mode 100644
index 00000000000..cd18702ad88
--- /dev/null
+++
b/pinot-core/src/test/java/org/apache/pinot/core/startree/StarTreeUtilsTest.java
@@ -0,0 +1,243 @@
+/**
+ * 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.pinot.core.startree;
+
+import com.fasterxml.jackson.databind.node.ObjectNode;
+import java.io.File;
+import java.util.ArrayList;
+import java.util.List;
+import org.apache.commons.io.FileUtils;
+import org.apache.pinot.common.request.context.ExpressionContext;
+import org.apache.pinot.common.request.context.predicate.EqPredicate;
+import org.apache.pinot.core.operator.BaseProjectOperator;
+import org.apache.pinot.core.operator.blocks.ValueBlock;
+import
org.apache.pinot.core.operator.filter.predicate.BaseRawValueBasedPredicateEvaluator;
+import
org.apache.pinot.core.operator.filter.predicate.EqualsPredicateEvaluatorFactory;
+import org.apache.pinot.core.operator.filter.predicate.PredicateEvaluator;
+import org.apache.pinot.core.plan.FilterPlanNode;
+import org.apache.pinot.core.query.request.context.QueryContext;
+import
org.apache.pinot.core.query.request.context.utils.QueryContextConverterUtils;
+import
org.apache.pinot.segment.local.indexsegment.immutable.ImmutableSegmentLoader;
+import
org.apache.pinot.segment.local.segment.creator.impl.SegmentIndexCreationDriverImpl;
+import org.apache.pinot.segment.local.segment.readers.GenericRowRecordReader;
+import org.apache.pinot.segment.local.startree.v2.builder.MultipleTreesBuilder;
+import org.apache.pinot.segment.spi.IndexSegment;
+import org.apache.pinot.segment.spi.SegmentContext;
+import org.apache.pinot.segment.spi.creator.SegmentGeneratorConfig;
+import org.apache.pinot.segment.spi.datasource.DataSource;
+import org.apache.pinot.segment.spi.datasource.DataSourceMetadata;
+import org.apache.pinot.segment.spi.index.reader.Dictionary;
+import org.apache.pinot.spi.config.table.FieldConfig;
+import org.apache.pinot.spi.config.table.StarTreeIndexConfig;
+import org.apache.pinot.spi.config.table.TableConfig;
+import org.apache.pinot.spi.config.table.TableType;
+import org.apache.pinot.spi.data.FieldSpec.DataType;
+import org.apache.pinot.spi.data.Schema;
+import org.apache.pinot.spi.data.readers.GenericRow;
+import org.apache.pinot.spi.utils.JsonUtils;
+import org.apache.pinot.spi.utils.ReadMode;
+import org.apache.pinot.spi.utils.builder.TableConfigBuilder;
+import org.testng.annotations.AfterClass;
+import org.testng.annotations.BeforeClass;
+import org.testng.annotations.Test;
+
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+import static org.testng.Assert.assertEquals;
+import static org.testng.Assert.assertFalse;
+import static org.testng.Assert.assertNotNull;
+import static org.testng.Assert.assertSame;
+import static org.testng.Assert.assertTrue;
+
+
+/// Tests for [StarTreeUtils] verifying that star-tree can consume filters on
columns configured with a `RAW` forward
+/// index and a separated dictionary.
+///
+/// Before the fix,
[org.apache.pinot.core.startree.operator.StarTreeFilterOperator] invoked
`getMatchingDictIds()` on
+/// a raw-value evaluator during tree traversal and threw
`UnsupportedOperationException`. This test locks in that
+/// the star-tree planner upgrades such evaluators to dictionary-based,
yielding the same query behavior as if the
+/// column were `DICTIONARY`-encoded.
+public class StarTreeUtilsTest {
+
+ private static final String COLUMN = "raw_dim_with_dict";
+
+ //-------------------------------------------------------------------------
+ // Unit tests for the private helper
+ //-------------------------------------------------------------------------
+
+ /// Raw-value evaluator on a column with a dictionary is rebuilt as
dictionary-based.
+ @Test
+ public void testToDictionaryBasedConvertsRawEvaluator() {
+ Dictionary dictionary = mock(Dictionary.class);
+ when(dictionary.length()).thenReturn(2);
+ when(dictionary.getStringValue(0)).thenReturn("v0");
+ when(dictionary.getStringValue(1)).thenReturn("v1");
+ when(dictionary.indexOf("v1")).thenReturn(1);
+
+ DataSourceMetadata metadata = mock(DataSourceMetadata.class);
+ when(metadata.getDataType()).thenReturn(DataType.STRING);
+
+ DataSource dataSource = mock(DataSource.class);
+ when(dataSource.getDictionary()).thenReturn(dictionary);
+ when(dataSource.getDataSourceMetadata()).thenReturn(metadata);
+
+ EqPredicate predicate = new
EqPredicate(ExpressionContext.forIdentifier(COLUMN), "v1");
+ PredicateEvaluator raw =
+ EqualsPredicateEvaluatorFactory.newRawValueBasedEvaluator(predicate,
DataType.STRING);
+ assertFalse(raw.isDictionaryBased(), "Baseline evaluator must be
raw-value-based");
+ assertTrue(raw instanceof BaseRawValueBasedPredicateEvaluator);
+
+ PredicateEvaluator converted = StarTreeUtils.toDictionaryBased(raw,
predicate, dataSource);
+ assertTrue(converted.isDictionaryBased(), "Converted evaluator must be
dictionary-based");
+ assertNotNull(converted.getMatchingDictIds());
+ }
+
+ /// Dictionary-based evaluators flow through unchanged (`instanceof` no-op).
+ @Test
+ public void testToDictionaryBasedIsNoopForDictionaryBased() {
+ Dictionary dictionary = mock(Dictionary.class);
+ when(dictionary.length()).thenReturn(1);
+ when(dictionary.indexOf("v1")).thenReturn(0);
+
+ DataSource dataSource = mock(DataSource.class);
+ when(dataSource.getDictionary()).thenReturn(dictionary);
+
+ EqPredicate predicate = new
EqPredicate(ExpressionContext.forIdentifier(COLUMN), "v1");
+ PredicateEvaluator dictBased =
+ EqualsPredicateEvaluatorFactory.newDictionaryBasedEvaluator(predicate,
dictionary, DataType.STRING);
+ assertTrue(dictBased.isDictionaryBased());
+
+ PredicateEvaluator result = StarTreeUtils.toDictionaryBased(dictBased,
predicate, dataSource);
+ assertSame(result, dictBased, "Dictionary-based evaluator should pass
through unchanged");
+ }
+
+ //-------------------------------------------------------------------------
+ // Segment-level test: RAW forward + separated dictionary + star-tree
+ //-------------------------------------------------------------------------
+
+ private static final File TEMP_DIR = new File(FileUtils.getTempDirectory(),
"StarTreeUtilsTest");
+ private static final String TABLE_NAME = "starTreeUtilsTest";
+ private static final String SEGMENT_NAME = "testSegment";
+ private static final int NUM_ROWS = 1000;
+ private static final int CARDINALITY = 10;
+
+ private IndexSegment _segment;
+
+ @BeforeClass
+ public void setUp()
+ throws Exception {
+ FileUtils.deleteDirectory(TEMP_DIR);
+
+ Schema schema = new Schema.SchemaBuilder()
+ .addSingleValueDimension(COLUMN, DataType.STRING)
+ .build();
+
+ // Explicit indexes JSON:
+ // forward.encodingType = RAW → forward index stores raw values
+ // dictionary.disabled = false → keep the dictionary alongside
+ ObjectNode indexes = JsonUtils.newObjectNode();
+ ObjectNode forwardCfg = JsonUtils.newObjectNode();
+ forwardCfg.put("encodingType", "RAW");
+ indexes.set("forward", forwardCfg);
+ ObjectNode dictCfg = JsonUtils.newObjectNode();
+ dictCfg.put("disabled", false);
+ indexes.set("dictionary", dictCfg);
+
+ FieldConfig rawWithDict = new FieldConfig.Builder(COLUMN)
+ .withEncodingType(FieldConfig.EncodingType.RAW)
+ .withIndexes(indexes)
+ .build();
+
+ TableConfig tableConfig = new TableConfigBuilder(TableType.OFFLINE)
+ .setTableName(TABLE_NAME)
+ .setFieldConfigList(List.of(rawWithDict))
+ .build();
+
+ List<GenericRow> rows = new ArrayList<>(NUM_ROWS);
+ for (int i = 0; i < NUM_ROWS; i++) {
+ GenericRow row = new GenericRow();
+ row.putValue(COLUMN, "v" + (i % CARDINALITY));
+ rows.add(row);
+ }
+
+ SegmentIndexCreationDriverImpl driver = new
SegmentIndexCreationDriverImpl();
+ SegmentGeneratorConfig cfg = new SegmentGeneratorConfig(tableConfig,
schema);
+ cfg.setOutDir(TEMP_DIR.getPath());
+ cfg.setSegmentName(SEGMENT_NAME);
+ driver.init(cfg, new GenericRowRecordReader(rows));
+ driver.build();
+
+ StarTreeIndexConfig sti =
+ new StarTreeIndexConfig(List.of(COLUMN), null, List.of("COUNT__*"),
null, Integer.MAX_VALUE);
+ File indexDir = new File(TEMP_DIR, SEGMENT_NAME);
+ try (MultipleTreesBuilder builder = new MultipleTreesBuilder(List.of(sti),
false, indexDir,
+ MultipleTreesBuilder.BuildMode.OFF_HEAP)) {
+ builder.build();
+ }
+
+ _segment = ImmutableSegmentLoader.load(indexDir, ReadMode.mmap);
+ }
+
+ @AfterClass
+ public void tearDown()
+ throws Exception {
+ if (_segment != null) {
+ _segment.destroy();
+ }
+ FileUtils.deleteDirectory(TEMP_DIR);
+ }
+
+ /// Sanity check that the column is exactly the RAW-forward +
separated-dictionary configuration the fix targets.
+ @Test
+ public void testColumnHasRawForwardAndDictionary() {
+ DataSource dataSource = _segment.getDataSource(COLUMN);
+ assertNotNull(dataSource.getDictionary(), "Expected dictionary on
RAW-encoded dim column");
+ assertFalse(dataSource.getForwardIndex().isDictionaryEncoded(),
+ "Expected forward index to be RAW-encoded");
+ assertNotNull(_segment.getStarTrees(), "Star-tree must be built");
+ assertEquals(_segment.getStarTrees().size(), 1);
+ }
+
+ /// End-to-end assertion equivalent to `EXPLAIN PLAN` showing a `STAR_TREE`
node:
+ /// a filter query returning `COUNT(*)` from a RAW-forward +
separated-dictionary star-tree dimension is served
+ /// via the star-tree operator (non-null result from
`createStarTreeBasedProjectOperator`) and reads a single
+ /// aggregated record per matching path — same as a `DICTIONARY`-encoded dim
would.
+ @Test
+ public void testStarTreeAcceleratesEqualityFilterOnRawWithDictionary() {
+ QueryContext queryContext = QueryContextConverterUtils.getQueryContext(
+ String.format("SELECT COUNT(*) FROM %s WHERE %s = 'v3'", TABLE_NAME,
COLUMN));
+
+ FilterPlanNode filterPlanNode = new FilterPlanNode(new
SegmentContext(_segment), queryContext);
+ filterPlanNode.run();
+
+ BaseProjectOperator<?> operator =
StarTreeUtils.createStarTreeBasedProjectOperator(_segment, queryContext,
+ queryContext.getAggregationFunctions(), queryContext.getFilter(),
+ filterPlanNode.getPredicateEvaluators());
+ assertNotNull(operator, "Star-tree plan expected for EQ on RAW+dict
dimension");
+
+ // Traversal must not throw; before the fix,
StarTreeFilterOperator.getMatchingDictIds threw UOE here.
+ ValueBlock block = operator.nextBlock();
+ assertNotNull(block, "Star-tree traversal returned no block");
+
+ // Star-tree yields one aggregated document per matching path — same
behavior as DICTIONARY-encoded columns.
+ // (For an EQ on a single dim, matchLeafRecords=MAX_VALUE, and 1 unique
matching value, exactly one leaf matches.)
+ assertEquals(block.getNumDocs(), 1,
+ "Expected exactly one aggregated star-tree document for the EQ filter;
got " + block.getNumDocs());
+ }
+}
diff --git
a/pinot-core/src/test/java/org/apache/pinot/core/startree/v2/BaseStarTreeV2Test.java
b/pinot-core/src/test/java/org/apache/pinot/core/startree/v2/BaseStarTreeV2Test.java
index e4492bb1669..cd4bb07bbd9 100644
---
a/pinot-core/src/test/java/org/apache/pinot/core/startree/v2/BaseStarTreeV2Test.java
+++
b/pinot-core/src/test/java/org/apache/pinot/core/startree/v2/BaseStarTreeV2Test.java
@@ -43,6 +43,7 @@ import
org.apache.pinot.segment.local.aggregator.ValueAggregator;
import
org.apache.pinot.segment.local.indexsegment.immutable.ImmutableSegmentLoader;
import
org.apache.pinot.segment.local.segment.creator.impl.SegmentIndexCreationDriverImpl;
import org.apache.pinot.segment.local.segment.readers.GenericRowRecordReader;
+import org.apache.pinot.segment.local.segment.readers.PinotSegmentColumnReader;
import org.apache.pinot.segment.local.startree.v2.builder.MultipleTreesBuilder;
import org.apache.pinot.segment.spi.AggregationFunctionType;
import org.apache.pinot.segment.spi.Constants;
@@ -85,15 +86,15 @@ abstract class BaseStarTreeV2Test<R, A> {
private static final Random RANDOM = new Random();
private static final File TEMP_DIR = new File(FileUtils.getTempDirectory(),
"BaseStarTreeV2Test");
- private static final String TABLE_NAME = "testTable";
+ protected static final String TABLE_NAME = "testTable";
private static final String SEGMENT_NAME = "testSegment";
private static final int NUM_SEGMENT_RECORDS = 100_000;
private static final int MAX_LEAF_RECORDS = RANDOM.nextInt(100) + 1;
// Using column names with '__' to make sure regular table columns with '__'
in the name aren't wrongly interpreted
// as AggregationFunctionColumnPair
- private static final String DIMENSION1 = "d1__COLUMN_NAME";
- private static final String DIMENSION2 = "DISTINCTCOUNTRAWHLL__d2";
+ protected static final String DIMENSION1 = "d1__COLUMN_NAME";
+ protected static final String DIMENSION2 = "DISTINCTCOUNTRAWHLL__d2";
private static final int DIMENSION_CARDINALITY = 100;
private static final String AGG_COL = "m";
@@ -152,7 +153,7 @@ abstract class BaseStarTreeV2Test<R, A> {
dimensionFieldSpec ->
dimensionFieldSpec.setSingleValueField(isAggColSingleValueField()));
}
Schema schema = schemaBuilder.build();
- TableConfig tableConfig = new
TableConfigBuilder(TableType.OFFLINE).setTableName(TABLE_NAME).build();
+ TableConfig tableConfig = createTableConfig();
List<GenericRow> segmentRecords = new ArrayList<>(NUM_SEGMENT_RECORDS);
for (int i = 0; i < NUM_SEGMENT_RECORDS; i++) {
@@ -311,9 +312,12 @@ abstract class BaseStarTreeV2Test<R, A> {
nonStarTreeAggregationColumnDictionaries.add(dataSource.getDictionary());
}
}
- List<ForwardIndexReader> nonStarTreeGroupByColumnReaders = new
ArrayList<>(numGroupByColumns);
+ // Use PinotSegmentColumnReader so a RAW forward-index group-by column
with a separated dictionary resolves
+ // its dict ids via the dictionary — mirroring the same lookup the
star-tree builder performs at build time
+ // (see BaseSingleTreeBuilder). Behavior is identical for
dictionary-encoded columns.
+ List<PinotSegmentColumnReader> nonStarTreeGroupByColumnReaders = new
ArrayList<>(numGroupByColumns);
for (String groupByColumn : groupByColumns) {
-
nonStarTreeGroupByColumnReaders.add(_indexSegment.getDataSource(groupByColumn).getForwardIndex());
+ nonStarTreeGroupByColumnReaders.add(new
PinotSegmentColumnReader(_indexSegment, groupByColumn));
}
Map<List<Integer>, List<Object>> nonStarTreeResult =
computeNonStarTreeResult(nonStarTreeFilterPlanNode,
nonStarTreeAggregationColumnReaders,
@@ -401,14 +405,13 @@ abstract class BaseStarTreeV2Test<R, A> {
private Map<List<Integer>, List<Object>>
computeNonStarTreeResult(FilterPlanNode nonStarTreeFilterPlanNode,
List<ForwardIndexReader> aggregationColumnReaders, List<Dictionary>
aggregationColumnDictionaries,
- List<ForwardIndexReader> groupByColumnReaders)
+ List<PinotSegmentColumnReader> groupByColumnReaders)
throws IOException {
Map<List<Integer>, List<Object>> result = new HashMap<>();
int numAggregations = aggregationColumnReaders.size();
int numGroupByColumns = groupByColumnReaders.size();
List<ForwardIndexReaderContext> aggregationColumnReaderContexts = new
ArrayList<>(numAggregations);
- List<ForwardIndexReaderContext> groupByColumnReaderContexts = new
ArrayList<>(numGroupByColumns);
try {
for (ForwardIndexReader aggregationColumnReader :
aggregationColumnReaders) {
if (aggregationColumnReader != null) {
@@ -417,9 +420,6 @@ abstract class BaseStarTreeV2Test<R, A> {
aggregationColumnReaderContexts.add(null);
}
}
- for (ForwardIndexReader groupByColumnReader : groupByColumnReaders) {
- groupByColumnReaderContexts.add(groupByColumnReader.createContext());
- }
BlockDocIdIterator docIdIterator =
nonStarTreeFilterPlanNode.run().nextBlock().getBlockDocIdSet().iterator();
int docId;
@@ -427,7 +427,7 @@ abstract class BaseStarTreeV2Test<R, A> {
// Array of dictionary ids (zero-length array for non-group-by queries)
List<Integer> group = new ArrayList<>(numGroupByColumns);
for (int i = 0; i < numGroupByColumns; i++) {
- group.add(groupByColumnReaders.get(i).getDictId(docId,
groupByColumnReaderContexts.get(i)));
+ group.add(groupByColumnReaders.get(i).getDictId(docId));
}
List<Object> values = result.computeIfAbsent(group, k -> new
ArrayList<>(numAggregations));
if (values.isEmpty()) {
@@ -465,10 +465,8 @@ abstract class BaseStarTreeV2Test<R, A> {
readerContext.close();
}
}
- for (ForwardIndexReaderContext readerContext :
groupByColumnReaderContexts) {
- if (readerContext != null) {
- readerContext.close();
- }
+ for (PinotSegmentColumnReader groupByColumnReader :
groupByColumnReaders) {
+ groupByColumnReader.close();
}
}
}
@@ -507,6 +505,12 @@ abstract class BaseStarTreeV2Test<R, A> {
return true;
}
+ /// Can be overridden to customize the table config (e.g. to attach
`FieldConfig`s that force a specific
+ /// forward-index encoding or dictionary configuration for the dimension
columns).
+ protected TableConfig createTableConfig() {
+ return new
TableConfigBuilder(TableType.OFFLINE).setTableName(TABLE_NAME).build();
+ }
+
abstract ValueAggregator<R, A> getValueAggregator();
abstract DataType getRawValueType();
diff --git
a/pinot-core/src/test/java/org/apache/pinot/core/startree/v2/RawWithDictionaryStarTreeV2Test.java
b/pinot-core/src/test/java/org/apache/pinot/core/startree/v2/RawWithDictionaryStarTreeV2Test.java
new file mode 100644
index 00000000000..f0ebd508188
--- /dev/null
+++
b/pinot-core/src/test/java/org/apache/pinot/core/startree/v2/RawWithDictionaryStarTreeV2Test.java
@@ -0,0 +1,92 @@
+/**
+ * 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.pinot.core.startree.v2;
+
+import com.fasterxml.jackson.databind.node.ObjectNode;
+import java.util.List;
+import java.util.Random;
+import org.apache.pinot.segment.local.aggregator.SumValueAggregator;
+import org.apache.pinot.segment.local.aggregator.ValueAggregator;
+import org.apache.pinot.spi.config.table.FieldConfig;
+import org.apache.pinot.spi.config.table.TableConfig;
+import org.apache.pinot.spi.config.table.TableType;
+import org.apache.pinot.spi.data.FieldSpec.DataType;
+import org.apache.pinot.spi.utils.JsonUtils;
+import org.apache.pinot.spi.utils.builder.TableConfigBuilder;
+
+import static org.testng.Assert.assertEquals;
+
+
+/// Exercises the full [BaseStarTreeV2Test] query matrix against star-tree
dimensions configured with a `RAW`
+/// forward index and a separated dictionary — the exact configuration handled
by
+/// [org.apache.pinot.core.startree.StarTreeUtils#toDictionaryBased] (Apache
Pinot PR #19153). Both
+/// `DIMENSION1` and `DIMENSION2` are switched to `RAW` + separated dictionary
so every filter predicate,
+/// including the `GROUP BY DIMENSION2` case, goes through the fix.
+///
+/// Before that fix,
[org.apache.pinot.core.startree.operator.StarTreeFilterOperator] invoked
+/// `getMatchingDictIds()` on a raw-value evaluator during tree traversal and
threw
+/// `UnsupportedOperationException`. Running the inherited `testQueries()` /
`testUnsupportedFilters()`
+/// suites confirms every AND/OR/NOT/nested filter combination now produces
the same aggregated result
+/// via the star-tree as via a plain scan.
+public class RawWithDictionaryStarTreeV2Test extends
BaseStarTreeV2Test<Object, Double> {
+
+ @Override
+ ValueAggregator<Object, Double> getValueAggregator() {
+ return new SumValueAggregator();
+ }
+
+ @Override
+ DataType getRawValueType() {
+ return DataType.INT;
+ }
+
+ @Override
+ Object getRandomRawValue(Random random) {
+ return random.nextInt();
+ }
+
+ @Override
+ protected void assertAggregatedValue(Double starTreeResult, Double
nonStarTreeResult) {
+ assertEquals(starTreeResult, nonStarTreeResult, 1e-5);
+ }
+
+ @Override
+ protected TableConfig createTableConfig() {
+ return new TableConfigBuilder(TableType.OFFLINE)
+ .setTableName(TABLE_NAME)
+ .setFieldConfigList(List.of(rawWithDictionary(DIMENSION1),
rawWithDictionary(DIMENSION2)))
+ .build();
+ }
+
+ /// Builds a `FieldConfig` that stores the column as a `RAW` forward index
while keeping a dictionary
+ /// alongside — the "separated dictionary" configuration star-tree must be
able to consume.
+ private static FieldConfig rawWithDictionary(String column) {
+ ObjectNode indexes = JsonUtils.newObjectNode();
+ ObjectNode forwardCfg = JsonUtils.newObjectNode();
+ forwardCfg.put("encodingType", "RAW");
+ indexes.set("forward", forwardCfg);
+ ObjectNode dictionaryCfg = JsonUtils.newObjectNode();
+ dictionaryCfg.put("disabled", false);
+ indexes.set("dictionary", dictionaryCfg);
+ return new FieldConfig.Builder(column)
+ .withEncodingType(FieldConfig.EncodingType.RAW)
+ .withIndexes(indexes)
+ .build();
+ }
+}
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]