xiangfu0 commented on code in PR #19799:
URL: https://github.com/apache/pinot/pull/19799#discussion_r4224839587


##########
pinot-core/src/main/java/org/apache/pinot/core/startree/operator/StarTreeFilterOperator.java:
##########
@@ -342,6 +352,9 @@ private StarTreeResult traverseStarTree() {
       }
     }
 
+    if (heapMatchingDocIds != null) {
+      matchingDocIds = heapMatchingDocIds.toMutableRoaringBitmap();

Review Comment:
   **[efficiency, plausible]** In RoaringBitmap 1.6.23 this is `new 
MutableRoaringBitmap(RoaringBitmap)` — a full container-by-container 
re-materialization (per-container `CharBuffer`/`LongBuffer` allocate + copy) of 
the entire result on every range-path query. For the shape the PR optimizes 
(many tiny ranges) this is a clear net win, but for a query matching a few leaf 
nodes with very wide ranges the old code did 2-3 cheap range adds and zero 
whole-bitmap copies, while the new code pays one full heap→buffer copy 
(hundreds of KB for a few-million-doc match) — allocation can regress for that 
shape, consistent with the “dense/wider ranges still pay conversion overhead” 
caveat in the description. Two cheaper options worth considering: serialize 
once and wrap zero-copy (`new 
ImmutableRoaringBitmap(ByteBuffer.wrap(RoaringBitmapUtils.serialize(heapMatchingDocIds)))`
 — one contiguous allocation; `ImmutableSegmentImpl` already uses the 
wrap-serialized-bytes pattern), or only take
  the heap path after N range additions so the few-wide-ranges shape keeps the 
old single-bitmap behavior.



##########
pinot-core/src/main/java/org/apache/pinot/core/startree/operator/StarTreeFilterOperator.java:
##########
@@ -233,15 +235,23 @@ private StarTreeResult traverseStarTree() {
 
       // If all predicate columns and group-by columns are matched, we can use 
aggregated document
       if (remainingPredicateColumns.isEmpty() && 
remainingGroupByColumns.isEmpty()) {
-        matchingDocIds.add(starTreeNode.getAggregatedDocId());
+        if (heapMatchingDocIds == null) {
+          matchingDocIds.add(starTreeNode.getAggregatedDocId());
+        } else {
+          heapMatchingDocIds.add(starTreeNode.getAggregatedDocId());
+        }
         continue;
       }
 
       // For leaf node, because we haven't exhausted all predicate columns and 
group-by columns, we cannot use
       // the aggregated document. Add the range of documents for this node to 
the bitmap, and keep track of the
       // remaining predicate columns for this node
       if (starTreeNode.isLeaf()) {
-        matchingDocIds.add((long) starTreeNode.getStartDocId(), 
starTreeNode.getEndDocId());
+        if (heapMatchingDocIds == null) {
+          // Range additions grow heap array containers geometrically. Keep 
aggregate-only matches in the buffer bitmap.
+          heapMatchingDocIds = matchingDocIds.toRoaringBitmap();
+        }
+        heapMatchingDocIds.add((long) starTreeNode.getStartDocId(), 
starTreeNode.getEndDocId());

Review Comment:
   **[altitude, plausible]** The root cause being worked around here is not 
star-tree-specific: buffer `MappeableArrayContainer.iadd(begin, end)` allocates 
an exact-size `CharBuffer` on every extending range add (no geometric growth), 
while heap `ArrayContainer.iadd` grows via `calculateCapacity` — any repeated 
range-add on a `MutableRoaringBitmap` hits the same quadratic re-allocation. 
`SortedIndexBasedFilterOperator.getBitmaps()` does the identical per-range 
`bitmap.add(left, right + 1L)` loop and would benefit equally. If the 
heap-then-convert trick is worth keeping, consider extracting it into a small 
shared helper (e.g. next to `RoaringBitmapUtils` in pinot-common) rather than 
an inline one-off, and naming the upstream cause 
(`MappeableArrayContainer.iadd`) in a comment so the workaround can be retired 
if a future RoaringBitmap upgrade fixes the buffer container growth.



##########
pinot-core/src/test/java/org/apache/pinot/core/startree/operator/StarTreeFilterOperatorTest.java:
##########
@@ -0,0 +1,130 @@
+/**
+ * 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.operator;
+
+import it.unimi.dsi.fastutil.objects.ObjectBooleanPair;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import org.apache.pinot.common.request.context.predicate.Predicate;
+import org.apache.pinot.core.operator.filter.TestUtils;
+import org.apache.pinot.core.operator.filter.predicate.PredicateEvaluator;
+import org.apache.pinot.core.query.request.context.QueryContext;
+import org.apache.pinot.core.startree.CompositePredicateEvaluator;
+import org.apache.pinot.segment.spi.datasource.DataSource;
+import org.apache.pinot.segment.spi.datasource.DataSourceMetadata;
+import org.apache.pinot.segment.spi.index.reader.InvertedIndexReader;
+import org.apache.pinot.segment.spi.index.reader.NullValueVectorReader;
+import org.apache.pinot.segment.spi.index.startree.StarTree;
+import org.apache.pinot.segment.spi.index.startree.StarTreeNode;
+import org.apache.pinot.segment.spi.index.startree.StarTreeV2;
+import org.apache.pinot.segment.spi.index.startree.StarTreeV2Metadata;
+import org.roaringbitmap.buffer.MutableRoaringBitmap;
+import org.testng.annotations.DataProvider;
+import org.testng.annotations.Test;
+
+import static org.mockito.Mockito.doReturn;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+import static org.testng.Assert.assertEquals;
+
+
+/// Tests star-tree document selection through the public filter result. Each 
invocation owns its fixture.
+public class StarTreeFilterOperatorTest {
+  @DataProvider(name = "nullHandling")
+  public Object[][] nullHandling() {
+    return new Object[][]{{false}, {true}};
+  }
+
+  @Test(dataProvider = "nullHandling")
+  public void testRangesAndAggregatedDocsWithResidualPredicate(boolean 
nullHandlingEnabled) {
+    // Early leaves retain the dim1 predicate, while deeper dim1 nodes 
contribute aggregate document ids.
+    // Keep the range order deliberately unsorted, overlap two ranges, and 
cross the 16-bit container boundary.
+    StarTreeNode firstAggregate = leaf(1, 0, 20, 21, 0);
+    StarTreeNode lastAggregate = leaf(1, 1, 30, 31, 70000);
+    StarTreeNode branch = branch(0, 4, List.of(firstAggregate, lastAggregate));
+    StarTreeNode root = branch(-1, StarTreeNode.ALL, List.of(
+        leaf(0, 0, 65534, 65538, 40), leaf(0, 1, 3, 4, 41), leaf(0, 2, 2, 4, 
42),
+        leaf(0, 3, 10, 10, 43), branch));
+    StarTree starTree = mock(StarTree.class);
+    when(starTree.getRoot()).thenReturn(root);
+    when(starTree.getDimensionNames()).thenReturn(List.of("dim0", "dim1"));
+    StarTreeV2Metadata metadata = mock(StarTreeV2Metadata.class);
+    when(metadata.getNumDocs()).thenReturn(70001);
+    StarTreeV2 starTreeV2 = mock(StarTreeV2.class);
+    when(starTreeV2.getStarTree()).thenReturn(starTree);
+    when(starTreeV2.getMetadata()).thenReturn(metadata);
+    QueryContext queryContext = new QueryContext.Builder().build();
+    queryContext.setNullHandlingEnabled(nullHandlingEnabled);
+
+    // A group-by on dim1 takes the same range/singleton paths without 
applying the residual predicate.
+    StarTreeFilterOperator unfiltered = new 
StarTreeFilterOperator(queryContext, starTreeV2, Map.of(), Set.of("dim1"));
+    
assertEquals(TestUtils.getDocIds(unfiltered.nextBlock().getBlockDocIdSet()),
+        List.of(0, 2, 3, 65534, 65535, 65536, 65537, 70000));
+
+    when(root.getAggregatedDocId()).thenReturn(70000);

Review Comment:
   **[test robustness, plausible]** This mid-test re-stub of the shared `root` 
mock is only safe because the `unfiltered` assertion above has already eagerly 
evaluated its traversal via `nextBlock()`, and that traversal never reads 
root's aggregated doc id. If the assertions are ever reordered, or the first 
assertion is moved below this line, the fixture semantics silently change 
across cases instead of failing clearly. Building a fresh root mock (or 
stubbing the aggregated id up front in `branch()`) per case would remove the 
ordering dependency.



##########
pinot-core/src/test/java/org/apache/pinot/core/startree/operator/StarTreeFilterOperatorTest.java:
##########
@@ -0,0 +1,130 @@
+/**
+ * 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.operator;
+
+import it.unimi.dsi.fastutil.objects.ObjectBooleanPair;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import org.apache.pinot.common.request.context.predicate.Predicate;
+import org.apache.pinot.core.operator.filter.TestUtils;
+import org.apache.pinot.core.operator.filter.predicate.PredicateEvaluator;
+import org.apache.pinot.core.query.request.context.QueryContext;
+import org.apache.pinot.core.startree.CompositePredicateEvaluator;
+import org.apache.pinot.segment.spi.datasource.DataSource;
+import org.apache.pinot.segment.spi.datasource.DataSourceMetadata;
+import org.apache.pinot.segment.spi.index.reader.InvertedIndexReader;
+import org.apache.pinot.segment.spi.index.reader.NullValueVectorReader;
+import org.apache.pinot.segment.spi.index.startree.StarTree;
+import org.apache.pinot.segment.spi.index.startree.StarTreeNode;
+import org.apache.pinot.segment.spi.index.startree.StarTreeV2;
+import org.apache.pinot.segment.spi.index.startree.StarTreeV2Metadata;
+import org.roaringbitmap.buffer.MutableRoaringBitmap;
+import org.testng.annotations.DataProvider;
+import org.testng.annotations.Test;
+
+import static org.mockito.Mockito.doReturn;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+import static org.testng.Assert.assertEquals;
+
+
+/// Tests star-tree document selection through the public filter result. Each 
invocation owns its fixture.
+public class StarTreeFilterOperatorTest {
+  @DataProvider(name = "nullHandling")
+  public Object[][] nullHandling() {
+    return new Object[][]{{false}, {true}};
+  }
+
+  @Test(dataProvider = "nullHandling")
+  public void testRangesAndAggregatedDocsWithResidualPredicate(boolean 
nullHandlingEnabled) {
+    // Early leaves retain the dim1 predicate, while deeper dim1 nodes 
contribute aggregate document ids.
+    // Keep the range order deliberately unsorted, overlap two ranges, and 
cross the 16-bit container boundary.
+    StarTreeNode firstAggregate = leaf(1, 0, 20, 21, 0);
+    StarTreeNode lastAggregate = leaf(1, 1, 30, 31, 70000);
+    StarTreeNode branch = branch(0, 4, List.of(firstAggregate, lastAggregate));
+    StarTreeNode root = branch(-1, StarTreeNode.ALL, List.of(
+        leaf(0, 0, 65534, 65538, 40), leaf(0, 1, 3, 4, 41), leaf(0, 2, 2, 4, 
42),
+        leaf(0, 3, 10, 10, 43), branch));
+    StarTree starTree = mock(StarTree.class);
+    when(starTree.getRoot()).thenReturn(root);
+    when(starTree.getDimensionNames()).thenReturn(List.of("dim0", "dim1"));
+    StarTreeV2Metadata metadata = mock(StarTreeV2Metadata.class);
+    when(metadata.getNumDocs()).thenReturn(70001);
+    StarTreeV2 starTreeV2 = mock(StarTreeV2.class);
+    when(starTreeV2.getStarTree()).thenReturn(starTree);
+    when(starTreeV2.getMetadata()).thenReturn(metadata);
+    QueryContext queryContext = new QueryContext.Builder().build();
+    queryContext.setNullHandlingEnabled(nullHandlingEnabled);
+
+    // A group-by on dim1 takes the same range/singleton paths without 
applying the residual predicate.
+    StarTreeFilterOperator unfiltered = new 
StarTreeFilterOperator(queryContext, starTreeV2, Map.of(), Set.of("dim1"));
+    
assertEquals(TestUtils.getDocIds(unfiltered.nextBlock().getBlockDocIdSet()),
+        List.of(0, 2, 3, 65534, 65535, 65536, 65537, 70000));
+
+    when(root.getAggregatedDocId()).thenReturn(70000);
+    StarTreeFilterOperator aggregateOnly = new 
StarTreeFilterOperator(queryContext, starTreeV2, Map.of(), null);
+    
assertEquals(TestUtils.getDocIds(aggregateOnly.nextBlock().getBlockDocIdSet()), 
List.of(70000));
+
+    PredicateEvaluator predicateEvaluator = mock(PredicateEvaluator.class);
+    when(predicateEvaluator.getPredicateType()).thenReturn(Predicate.Type.IN);
+    when(predicateEvaluator.getMatchingDictIds()).thenReturn(new int[]{0, 1});
+    CompositePredicateEvaluator compositePredicateEvaluator =
+        new 
CompositePredicateEvaluator(List.of(ObjectBooleanPair.of(predicateEvaluator, 
false)));
+    InvertedIndexReader<?> invertedIndex = mock(InvertedIndexReader.class);
+    // Include a matching indexed document outside the selected ranges to 
prove the residual is conjoined with them.
+    doReturn(MutableRoaringBitmap.bitmapOf(0, 2, 999, 65535, 
65536)).when(invertedIndex).getDocIds(0);
+    
doReturn(MutableRoaringBitmap.bitmapOf(70000)).when(invertedIndex).getDocIds(1);
+    DataSourceMetadata dataSourceMetadata = mock(DataSourceMetadata.class);
+    when(dataSourceMetadata.isSingleValue()).thenReturn(true);
+    NullValueVectorReader nullValueVector = mock(NullValueVectorReader.class);
+    
when(nullValueVector.getNullBitmap()).thenReturn(MutableRoaringBitmap.bitmapOf(65536));
+    DataSource dataSource = mock(DataSource.class);

Review Comment:
   **[test robustness, plausible]** The residual-predicate path routing to an 
inverted-index-based operator relies entirely on Mockito defaults for unstubbed 
methods (`isSorted()` = false, `getDictionary()` = null, `getColumnName()` = 
null — the last tolerated only because the default `QueryContext` has no 
skip-indexes), and the test never asserts which leaf operator 
`FilterOperatorUtils` actually selected. If the dispatch precedence changes, 
the test either dies in `ScanBasedFilterOperator` with a confusing `checkState` 
failure instead of an assertion diff, or keeps passing through a different 
operator than the fixture was designed to exercise. Stubbing the 
routing-relevant methods explicitly (and/or asserting the selected operator 
type) would make the intent explicit.



##########
pinot-core/src/test/java/org/apache/pinot/core/startree/operator/StarTreeFilterOperatorTest.java:
##########
@@ -0,0 +1,130 @@
+/**
+ * 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.operator;
+
+import it.unimi.dsi.fastutil.objects.ObjectBooleanPair;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import org.apache.pinot.common.request.context.predicate.Predicate;
+import org.apache.pinot.core.operator.filter.TestUtils;
+import org.apache.pinot.core.operator.filter.predicate.PredicateEvaluator;
+import org.apache.pinot.core.query.request.context.QueryContext;
+import org.apache.pinot.core.startree.CompositePredicateEvaluator;
+import org.apache.pinot.segment.spi.datasource.DataSource;
+import org.apache.pinot.segment.spi.datasource.DataSourceMetadata;
+import org.apache.pinot.segment.spi.index.reader.InvertedIndexReader;
+import org.apache.pinot.segment.spi.index.reader.NullValueVectorReader;
+import org.apache.pinot.segment.spi.index.startree.StarTree;
+import org.apache.pinot.segment.spi.index.startree.StarTreeNode;
+import org.apache.pinot.segment.spi.index.startree.StarTreeV2;
+import org.apache.pinot.segment.spi.index.startree.StarTreeV2Metadata;
+import org.roaringbitmap.buffer.MutableRoaringBitmap;
+import org.testng.annotations.DataProvider;
+import org.testng.annotations.Test;
+
+import static org.mockito.Mockito.doReturn;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+import static org.testng.Assert.assertEquals;
+
+
+/// Tests star-tree document selection through the public filter result. Each 
invocation owns its fixture.
+public class StarTreeFilterOperatorTest {
+  @DataProvider(name = "nullHandling")
+  public Object[][] nullHandling() {
+    return new Object[][]{{false}, {true}};
+  }
+
+  @Test(dataProvider = "nullHandling")
+  public void testRangesAndAggregatedDocsWithResidualPredicate(boolean 
nullHandlingEnabled) {
+    // Early leaves retain the dim1 predicate, while deeper dim1 nodes 
contribute aggregate document ids.
+    // Keep the range order deliberately unsorted, overlap two ranges, and 
cross the 16-bit container boundary.
+    StarTreeNode firstAggregate = leaf(1, 0, 20, 21, 0);

Review Comment:
   **[test clarity, minor]** `firstAggregate`'s aggregated doc id of 0 
coincides with Mockito's default int return (so this stub verifies nothing if 
lost) and doc 0 also appears in the dictId-0 inverted-index bitmap, giving it 
dual provenance in the filtered assertion. A unique non-zero id (e.g. 50, with 
the expected lists updated) would make each asserted doc id's provenance 
unambiguous.



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: [email protected]

For queries about this service, please contact Infrastructure at:
[email protected]


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to