LuciferYang commented on issue #9883:
URL: https://github.com/apache/paimon/issues/9883#issuecomment-5715345869

   ## Follow-up design for the partially-evaluable case
   
   #9909 fixed the fully-unevaluable case: when the scalar global index cannot 
evaluate the predicate at all, `scalarMatchedRows` returns `null` and the 
ranges are routed to the raw search where the exact filter decides. This issue 
stays open for the partially-evaluable case.
   
   ### The remaining problem
   
   When a WHERE filter has one conjunct the index can evaluate and one it 
cannot, `GlobalIndexEvaluator` silently drops the unevaluable conjunct and 
returns a non-null result that is a superset of the true matches. The vector 
index path trusts that bitmap as the exact include set (it has no final-read 
filter), so the top-K is computed over the superset: rows that fail the dropped 
conjunct can take top-K slots, and truly-matching rows ranked below them are 
lost.
   
   Two shapes, both reproducible:
   - different fields: `a = 1 AND tags IS NOT NULL`, where `IS NOT NULL` on a 
multivalue-indexed array is unsupported;
   - same field: `id >= 0 AND CAST(id AS BIGINT) > 0`, where the CAST conjunct 
is unsupported.
   
   ### Field coverage is not sufficient
   
   An earlier attempt gated "keep the index result" on whether every field the 
filter references contributed (`contributingFieldIds`). That is not enough: the 
same-field example has `contributingFieldIds = {id}` covering every referenced 
field, yet the result is still a superset because a second conjunct on `id` was 
dropped. Exactness is a property of the whole predicate evaluation, not of 
field coverage.
   
   ### Where it lives (per engine)
   
   The gap is at the evaluator level and is engine-agnostic. Anything that 
reaches the vector-search `filter` is affected, and both example predicates do 
reach it:
   - Spark pushes convertible predicates into the scan; 
`SparkV2FilterConverter` converts both `CAST(id AS BIGINT) > 0` (a cast 
transform) and `array IS NOT NULL`, so they reach the evaluator.
   - Flink's `vector_search` procedure parses the whole `where` string into the 
Paimon predicate; supported functions reach the evaluator, and an inexpressible 
one such as CAST fails the call rather than silently dropping.
   - Local and batch reads pass the full predicate directly.
   
   One case is out of scope here: a genuinely non-convertible Spark predicate 
(a UDF, a column-to-column comparison, an unresolvable cast) stays as a Spark 
residual, never reaches Paimon, and Spark post-filters the already-truncated 
top-K, which cannot refill displaced rows. That is a separate Spark-boundary 
concern, not the "scalar index cannot evaluate" problem tracked here; I will 
file it separately.
   
   ### Proposed design
   
   1. **Exactness signal in the evaluator.** Add an `exact` flag sourced from 
the reader/result (default exact; an approximate reader reports inexact), 
propagated bottom-up in `combineResults`: an AND is exact only if no child was 
dropped and every contributing child is exact; an OR is exact if all children 
are exact (it already requires all present); an empty result is exact. A 
boolean is sufficient. Sourcing it from the reader, rather than inferring it 
from "the reader returned a present result", keeps the guarantee under future 
approximate or ANN-style readers.
   
   2. **Remediation, gated on the flag.** When the result is exact, use it as 
today. When it is inexact, keep the index (ANN) path but re-check: apply the 
supported part as a pre-filter, over-retrieve more than K candidates (the 
existing refine-factor path already over-retrieves), re-apply the exact filter 
on the candidate rows (the candidate read path can already read filter columns 
and run the exact predicate), take the top-K, and iteratively retrieve more if 
fewer than K survive. Fall back to the raw search when the residual is 
selective enough that refilling approaches a full scan. This keeps the index 
fast path for a selective supported conjunct and stays correct.
   
   All four read paths (local, batch, Spark, Flink) inherit this from the 
shared base, so it is a single fix.
   
   ### Alternatives considered
   
   - Route every inexact predicate to a full raw search, like the 
fully-unevaluable case. Simplest, but it loses the index acceleration for a 
selective supported conjunct, so it is kept only as the safety valve above.
   - Gate on `contributingFieldIds`. Insufficient, as shown above.
   
   Feedback welcome, especially on the `exact` flag versus a residual 
predicate, and on candidate re-check versus raw fallback as the default.
   


-- 
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]

Reply via email to