1717
1818package org .apache .doris .datasource .lance .source ;
1919
20+ import org .apache .doris .analysis .ArrayLiteral ;
2021import org .apache .doris .analysis .BinaryPredicate ;
2122import org .apache .doris .analysis .CompoundPredicate ;
2223import org .apache .doris .analysis .Expr ;
24+ import org .apache .doris .analysis .FunctionCallExpr ;
2325import org .apache .doris .analysis .InPredicate ;
2426import org .apache .doris .analysis .IsNullPredicate ;
27+ import org .apache .doris .analysis .LikePredicate ;
28+ import org .apache .doris .analysis .LiteralExpr ;
2529import org .apache .doris .analysis .SlotRef ;
30+ import org .apache .doris .analysis .StringLiteral ;
2631import org .apache .doris .datasource .lance .index .LanceIndexSegmentGroup ;
2732import org .apache .doris .datasource .lance .index .LanceIndexSegmentInfo ;
2833import org .apache .doris .datasource .lance .metadata .LanceFragmentInfo ;
3136import org .lance .index .IndexType ;
3237
3338import java .util .ArrayList ;
39+ import java .util .HashMap ;
3440import java .util .HashSet ;
3541import java .util .List ;
3642import java .util .Map ;
3743import java .util .Set ;
3844
3945/** Assigns one BTree/Bitmap/LabelList segment and a disjoint fragment domain to each ordinary scan task. */
4046final class LanceScalarIndexPlanner {
47+ // Match the pinned lance-c scoped-expression bounds. Exceeding either bound makes
48+ // native planning scan the whole domain, so do not coalesce fragments in that case.
49+ private static final int MAX_EXPRESSION_NODES = 128 ;
50+ private static final int MAX_EXPRESSION_DEPTH = 32 ;
51+
4152 static final class Plan {
4253 final String indexName ;
4354 final LanceSplitBuilder splits ;
@@ -56,85 +67,164 @@ static Plan plan(LanceTableMetadata metadata, List<Expr> pushedConjuncts,
5667 || !metadata .getIndexMetadataState ().canPlanIndexSegments ()) {
5768 return null ;
5869 }
59- Set <Integer > filterFields = collectFilterFields (metadata , pushedConjuncts );
60- if (filterFields .isEmpty ()) {
61- return null ;
70+ int nodes = Math .max (0 , pushedConjuncts .size () - 1 );
71+ for (Expr expr : pushedConjuncts ) {
72+ nodes += expressionNodes (expr , Math .max (0 , pushedConjuncts .size () - 1 ));
73+ if (nodes > MAX_EXPRESSION_NODES ) {
74+ return null ;
75+ }
6276 }
6377 // Metadata already groups physical segments by logical index. Name order
6478 // provides a stable winner when multiple indices cover the same number of rows.
6579 List <LanceIndexSegmentGroup > indices = new ArrayList <>(metadata .getIndexes ());
6680 indices .sort (java .util .Comparator .comparing (LanceIndexSegmentGroup ::getName ));
67- boolean splitComplementByFragment = pushedConjuncts .stream ()
68- .anyMatch (LanceScalarIndexPlanner ::containsComplement );
6981 Plan selected = null ;
82+ Map <IndexType , Set <Integer >> fieldsByIndexType = new HashMap <>();
7083 for (LanceIndexSegmentGroup logicalIndex : indices ) {
7184 List <LanceIndexSegmentInfo > segments = logicalIndex .getSegments ();
72- // Segment-scoped scans support one top-level key in BTree/Bitmap/LabelList indices. Lance
73- // performs the final typed driver selection and falls back within the same domain.
85+ // Only coalesce a segment when a positive, indexable necessary condition
86+ // exists. Broad complements keep fragment parallelism without repeating a
87+ // segment-wide index search once per fragment.
7488 LanceIndexSegmentInfo index = segments .get (0 );
7589 if ((index .getIndexType () != IndexType .BTREE && index .getIndexType () != IndexType .BITMAP
7690 && index .getIndexType () != IndexType .LABEL_LIST )
77- || index .getFieldIds ().size () != 1 || !filterFields .contains (index .getFieldIds ().get (0 ))) {
91+ || index .getFieldIds ().size () != 1
92+ || !fieldsByIndexType .computeIfAbsent (index .getIndexType (),
93+ type -> collectFilterFields (metadata , pushedConjuncts , type ))
94+ .contains (index .getFieldIds ().get (0 ))) {
7895 continue ;
7996 }
80- Plan candidate = groupFragments (metadata , segments , visibleFragments ,
81- splitComplementByFragment );
97+ Plan candidate = groupFragments (metadata , segments , visibleFragments );
8298 if (candidate != null && (selected == null || candidate .coveredRows > selected .coveredRows )) {
8399 selected = candidate ;
84100 }
85101 }
86102 return selected ;
87103 }
88104
89- private static Set <Integer > collectFilterFields (LanceTableMetadata metadata , List <Expr > pushedConjuncts ) {
105+ private static Set <Integer > collectFilterFields (LanceTableMetadata metadata ,
106+ List <Expr > pushedConjuncts , IndexType indexType ) {
90107 Set <Integer > fields = new HashSet <>();
91108 for (Expr expr : pushedConjuncts ) {
92- fields .addAll (collectDriverFields (metadata , expr , false ));
109+ fields .addAll (collectDriverFields (metadata , expr , indexType ));
93110 }
94111 return fields ;
95112 }
96113
97- private static Set <Integer > collectDriverFields (LanceTableMetadata metadata , Expr expr , boolean complete ) {
114+ private static Set <Integer > collectDriverFields (LanceTableMetadata metadata , Expr expr , IndexType indexType ) {
115+ Set <Integer > fields = new HashSet <>();
98116 if (expr instanceof CompoundPredicate ) {
99117 CompoundPredicate .Operator op = ((CompoundPredicate ) expr ).getOp ();
100118 if (op == CompoundPredicate .Operator .NOT ) {
101- // NOT cannot complement a pruned AND: that would discard valid matches.
102- return collectDriverFields (metadata , expr .getChild (0 ), true );
119+ return fields ;
103120 }
104- Set <Integer > left = collectDriverFields (metadata , expr .getChild (0 ), complete );
105- Set <Integer > right = collectDriverFields (metadata , expr .getChild (1 ), complete );
106- if (op == CompoundPredicate .Operator .AND && ! complete ) {
121+ Set <Integer > left = collectDriverFields (metadata , expr .getChild (0 ), indexType );
122+ Set <Integer > right = collectDriverFields (metadata , expr .getChild (1 ), indexType );
123+ if (op == CompoundPredicate .Operator .AND ) {
107124 left .addAll (right );
108125 } else {
109- // One selected index must supply candidates for both OR branches, and for
110- // every leaf of a negated subtree. lance-c rechecks the complete predicate .
126+ // Sharing a slot is insufficient: both OR branches must actually be
127+ // indexable. A suffix LIKE, for example, cannot supply BTree candidates .
111128 left .retainAll (right );
112129 }
113130 return left ;
114131 }
132+ if (!isPositiveIndexLeaf (expr , indexType )) {
133+ return fields ;
134+ }
115135 Set <SlotRef > slots = new HashSet <>();
116136 expr .collect (SlotRef .class , slots );
117- Set <Integer > fields = new HashSet <>();
118137 for (SlotRef slot : slots ) {
119138 metadata .getLanceFieldId (slot .getColumnName ()).ifPresent (fields ::add );
120139 }
121140 return fields ;
122141 }
123142
124- private static boolean containsComplement (Expr expr ) {
125- if ((expr instanceof CompoundPredicate
126- && ((CompoundPredicate ) expr ).getOp () == CompoundPredicate .Operator .NOT )
127- || (expr instanceof InPredicate && ((InPredicate ) expr ).isNotIn ())
128- || (expr instanceof BinaryPredicate
129- && ((BinaryPredicate ) expr ).getOp () == BinaryPredicate .Operator .NE )
130- || (expr instanceof IsNullPredicate && ((IsNullPredicate ) expr ).isNotNull ())) {
131- return true ;
132- }
133- return expr .getChildren ().stream ().anyMatch (LanceScalarIndexPlanner ::containsComplement );
143+ private static boolean isPositiveIndexLeaf (Expr expr , IndexType indexType ) {
144+ if (indexType == IndexType .LABEL_LIST ) {
145+ if (!(expr instanceof FunctionCallExpr ) || expr .getChildren ().size () != 2 ) {
146+ return false ;
147+ }
148+ String name = ((FunctionCallExpr ) expr ).getFnName ().getFunction ();
149+ if ("array_contains" .equalsIgnoreCase (name )) {
150+ return expr .getChild (0 ) instanceof SlotRef && expr .getChild (1 ) instanceof LiteralExpr ;
151+ }
152+ return "arrays_overlap" .equalsIgnoreCase (name ) && overlapSize (expr ) > 0 ;
153+ }
154+ if (expr instanceof BinaryPredicate ) {
155+ BinaryPredicate .Operator op = ((BinaryPredicate ) expr ).getOp ();
156+ return (op == BinaryPredicate .Operator .EQ || op == BinaryPredicate .Operator .GT
157+ || op == BinaryPredicate .Operator .GE || op == BinaryPredicate .Operator .LT
158+ || op == BinaryPredicate .Operator .LE )
159+ && ((expr .getChild (0 ) instanceof SlotRef && expr .getChild (1 ) instanceof LiteralExpr )
160+ || (expr .getChild (1 ) instanceof SlotRef && expr .getChild (0 ) instanceof LiteralExpr ));
161+ }
162+ if (expr instanceof InPredicate ) {
163+ return !((InPredicate ) expr ).isNotIn () && expr .getChild (0 ) instanceof SlotRef ;
164+ }
165+ if (expr instanceof IsNullPredicate ) {
166+ return !((IsNullPredicate ) expr ).isNotNull () && expr .getChild (0 ) instanceof SlotRef ;
167+ }
168+ // Bitmap has no prefix-query support. Keep only unambiguous BTree prefixes;
169+ // escaped and other LIKE shapes remain pushed filters in fragment scans.
170+ if (indexType != IndexType .BTREE || expr .getChildren ().size () != 2
171+ || !(expr .getChild (0 ) instanceof SlotRef ) || !(expr .getChild (1 ) instanceof StringLiteral )) {
172+ return false ;
173+ }
174+ String name = expr instanceof FunctionCallExpr
175+ ? ((FunctionCallExpr ) expr ).getFnName ().getFunction () : "" ;
176+ boolean like = (expr instanceof LikePredicate && ((LikePredicate ) expr ).getOp () == LikePredicate .Operator .LIKE )
177+ || "like" .equalsIgnoreCase (name );
178+ String prefix = ((StringLiteral ) expr .getChild (1 )).getStringValue ();
179+ if (like && prefix .endsWith ("%" )) {
180+ prefix = prefix .substring (0 , prefix .length () - 1 );
181+ } else if (!"starts_with" .equalsIgnoreCase (name )) {
182+ return false ;
183+ }
184+ return !prefix .isEmpty () && prefix .indexOf ('%' ) < 0 && prefix .indexOf ('_' ) < 0 && prefix .indexOf ('\\' ) < 0 ;
185+ }
186+
187+ private static int overlapSize (Expr expr ) {
188+ if (!(expr instanceof FunctionCallExpr ) || expr .getChildren ().size () != 2
189+ || !"arrays_overlap" .equalsIgnoreCase (((FunctionCallExpr ) expr ).getFnName ().getFunction ())) {
190+ return 0 ;
191+ }
192+ for (int i = 0 ; i < 2 ; i ++) {
193+ if (expr .getChild (i ) instanceof ArrayLiteral && expr .getChild (1 - i ) instanceof SlotRef ) {
194+ return expr .getChild (i ).getChildren ().size ();
195+ }
196+ }
197+ return 0 ;
198+ }
199+
200+ private static int expressionNodes (Expr expr , int depth ) {
201+ if (depth > MAX_EXPRESSION_DEPTH ) {
202+ return MAX_EXPRESSION_NODES + 1 ;
203+ }
204+ if (expr instanceof CompoundPredicate ) {
205+ int nodes = 1 ;
206+ for (Expr child : expr .getChildren ()) {
207+ nodes += expressionNodes (child , depth + 1 );
208+ if (nodes > MAX_EXPRESSION_NODES ) {
209+ break ;
210+ }
211+ }
212+ return nodes ;
213+ }
214+ int labels = overlapSize (expr );
215+ // The converter emits a balanced OR tree: N memberships require 2*N-1 nodes.
216+ if (labels > 0 ) {
217+ int levels = 32 - Integer .numberOfLeadingZeros (labels - 1 );
218+ if (labels > MAX_EXPRESSION_NODES / 2 || depth + levels > MAX_EXPRESSION_DEPTH ) {
219+ return MAX_EXPRESSION_NODES + 1 ;
220+ }
221+ return 2 * labels - 1 ;
222+ }
223+ return 1 ;
134224 }
135225
136226 private static Plan groupFragments (LanceTableMetadata metadata , List <LanceIndexSegmentInfo > segments ,
137- Map <Long , LanceFragmentInfo > visibleFragments , boolean splitComplementByFragment ) {
227+ Map <Long , LanceFragmentInfo > visibleFragments ) {
138228 LanceSplitBuilder splits = new LanceSplitBuilder (
139229 metadata .getDatasetUri (), metadata .getVersion (), segments .size ());
140230 Set <Long > coveredFragments = new HashSet <>();
@@ -162,17 +252,7 @@ private static Plan groupFragments(LanceTableMetadata metadata, List<LanceIndexS
162252 coveredRows += Math .max (fragment .getPhysicalRows (), 0 );
163253 }
164254 if (!fragments .isEmpty ()) {
165- // Complement selectivity is unknown. Preserve fragment-level scheduling so a
166- // broad NOT cannot funnel all covered data through one BE. Each task retains
167- // the same segment UUID but owns a disjoint domain, including on fallback.
168- if (splitComplementByFragment ) {
169- for (Long fragmentId : fragments ) {
170- splits .addIndexSegmentSplit (segment .getUuid (), java .util .Collections .singletonList (fragmentId ),
171- visibleFragments .get (fragmentId ).getSchedulingWeight ());
172- }
173- } else {
174- splits .addIndexSegmentSplit (segment .getUuid (), fragments , physicalRows );
175- }
255+ splits .addIndexSegmentSplit (segment .getUuid (), fragments , physicalRows );
176256 }
177257 }
178258 return splits .isEmpty () ? null : new Plan (segments .get (0 ).getIndexName (), splits , coveredRows );
0 commit comments