3838import org .apache .paimon .partition .PartitionPredicate ;
3939import org .apache .paimon .predicate .BatchVectorSearch ;
4040import org .apache .paimon .predicate .Predicate ;
41+ import org .apache .paimon .predicate .PredicateVisitor ;
4142import org .apache .paimon .predicate .VectorSearch ;
4243import org .apache .paimon .reader .RecordReader ;
4344import org .apache .paimon .table .FileStoreTable ;
6061import java .util .List ;
6162import java .util .Map ;
6263import java .util .Optional ;
64+ import java .util .PriorityQueue ;
6365import java .util .Set ;
6466import java .util .TreeSet ;
6567import java .util .concurrent .CompletableFuture ;
@@ -79,6 +81,10 @@ public abstract class AbstractVectorRead implements Serializable {
7981 protected final DataField vectorColumn ;
8082 protected final Map <String , String > options ;
8183
84+ private static final Comparator <long []> WEAKEST_SCORE_FIRST =
85+ Comparator .<long []>comparingDouble (a -> Float .intBitsToFloat ((int ) a [1 ]))
86+ .thenComparing ((a , b ) -> Long .compare (b [0 ], a [0 ]));
87+
8288 protected AbstractVectorRead (
8389 FileStoreTable table ,
8490 @ Nullable PartitionPredicate partitionFilter ,
@@ -316,11 +322,7 @@ protected ScoredGlobalIndexResult maybeRerankIndexedResult(
316322 return result ;
317323 }
318324 ScoredGlobalIndexResult candidates = result .topK (indexedSearchLimit (indexType ));
319- return readRawSearch (
320- candidates .results ().toRangeList (),
321- candidates .results (),
322- globalIndexer ,
323- queryVector );
325+ return readRawRefineSearch (candidates .results (), globalIndexer , queryVector );
324326 }
325327
326328 protected String vectorIndexType (List <IndexVectorSearchSplit > splits ) {
@@ -376,48 +378,208 @@ protected ScoredGlobalIndexResult readRawSearch(
376378 @ Nullable RoaringNavigableMap64 preFilter ,
377379 String metric ,
378380 float [] queryVector ) {
379- RowType readType = SpecialFields .rowTypeWithRowId (table .rowType ());
380- if (preFilter != null ) {
381- rawRowRanges =
382- Range .and (
383- Range .sortAndMergeOverlap (rawRowRanges , true ),
384- Range .sortAndMergeOverlap (preFilter .toRangeList (), true ));
385- }
381+ return readRawSearch (rawRowRanges , preFilter , null , metric , queryVector , true );
382+ }
383+
384+ protected ScoredGlobalIndexResult readRawRefineSearch (
385+ RoaringNavigableMap64 candidates ,
386+ @ Nullable GlobalIndexer globalIndexer ,
387+ float [] queryVector ) {
388+ return readRawCandidateSearch (
389+ candidates .toRangeList (),
390+ candidates ,
391+ rawSearchMetric (globalIndexer ),
392+ queryVector ,
393+ false );
394+ }
395+
396+ @ Deprecated
397+ protected ScoredGlobalIndexResult readRawRefineSearch (
398+ List <Range > rawRowRanges ,
399+ @ Nullable RoaringNavigableMap64 candidates ,
400+ @ Nullable GlobalIndexer globalIndexer ,
401+ float [] queryVector ) {
402+ return readRawSearch (
403+ rawRowRanges , null , candidates , rawSearchMetric (globalIndexer ), queryVector , false );
404+ }
405+
406+ protected ScoredGlobalIndexResult readRawCandidateSearch (
407+ List <Range > rawRowRanges ,
408+ RoaringNavigableMap64 candidates ,
409+ String metric ,
410+ float [] queryVector ,
411+ boolean includeFilter ) {
412+ return readRawSearch (rawRowRanges , null , candidates , metric , queryVector , includeFilter );
413+ }
414+
415+ private ScoredGlobalIndexResult readRawSearch (
416+ List <Range > rawRowRanges ,
417+ @ Nullable RoaringNavigableMap64 preFilter ,
418+ @ Nullable RoaringNavigableMap64 scoreCandidates ,
419+ String metric ,
420+ float [] queryVector ,
421+ boolean includeFilter ) {
422+ RowType readType = rawSearchReadType (includeFilter );
423+ rawRowRanges = filteredRawRowRanges (rawRowRanges , preFilter );
386424 if (rawRowRanges .isEmpty ()) {
387425 return ScoredGlobalIndexResult .createEmpty ();
388426 }
389427
390428 TableScan .Plan plan =
391429 newRawReadBuilder (readType , false ).withRowRanges (rawRowRanges ).newScan ().plan ();
392- ReadBuilder readBuilder = newRawReadBuilder (readType , true );
393- RoaringNavigableMap64 resultBitmap = new RoaringNavigableMap64 ();
394- Map <Long , Float > scoreMap = new HashMap <>();
430+ ReadBuilder readBuilder = newRawReadBuilder (readType , includeFilter );
395431 int vectorIndex = readType .getFieldIndex (vectorColumn .name ());
396432 int rowIdIndex = readType .getFieldIndex (SpecialFields .ROW_ID .name ());
433+ PriorityQueue <long []> topKHeap =
434+ new PriorityQueue <>(Math .max (1 , limit + 1 ), WEAKEST_SCORE_FIRST );
397435
398436 try (RecordReader <InternalRow > reader =
399437 readBuilder .newRead ().executeFilter ().createReader (plan )) {
400438 reader .forEachRemaining (
401439 row -> {
440+ long rowId = row .getLong (rowIdIndex );
441+ if (scoreCandidates != null && !scoreCandidates .contains (rowId )) {
442+ return ;
443+ }
402444 if (row .isNullAt (vectorIndex )) {
403445 return ;
404446 }
405447 float [] stored = getVector (row , vectorIndex );
406- if (stored .length != queryVector .length ) {
407- throw new IllegalArgumentException (
408- String .format (
409- "Query vector dimension mismatch: expected %d, got %d" ,
410- stored .length , queryVector .length ));
411- }
448+ checkVectorDimension (queryVector , stored );
449+ offerScore (
450+ topKHeap , limit , rowId , computeScore (queryVector , stored , metric ));
451+ });
452+ } catch (IOException e ) {
453+ throw new RuntimeException ("Failed to read raw vectors for vector search." , e );
454+ }
455+
456+ return scoredResult (topKHeap );
457+ }
458+
459+ protected Map <Long , float []> readRawVectors (
460+ RoaringNavigableMap64 candidates , boolean includeFilter ) {
461+ return readRawVectors (candidates .toRangeList (), candidates , includeFilter );
462+ }
463+
464+ protected Map <Long , float []> readRawVectors (
465+ List <Range > rawRowRanges ,
466+ @ Nullable RoaringNavigableMap64 candidates ,
467+ boolean includeFilter ) {
468+ RowType readType = rawSearchReadType (includeFilter );
469+ rawRowRanges = filteredRawRowRanges (rawRowRanges , null );
470+ if (rawRowRanges .isEmpty ()) {
471+ return Collections .emptyMap ();
472+ }
473+
474+ TableScan .Plan plan =
475+ newRawReadBuilder (readType , false ).withRowRanges (rawRowRanges ).newScan ().plan ();
476+ ReadBuilder readBuilder = newRawReadBuilder (readType , includeFilter );
477+ Map <Long , float []> rawVectors = new HashMap <>();
478+ int vectorIndex = readType .getFieldIndex (vectorColumn .name ());
479+ int rowIdIndex = readType .getFieldIndex (SpecialFields .ROW_ID .name ());
480+
481+ try (RecordReader <InternalRow > reader =
482+ readBuilder .newRead ().executeFilter ().createReader (plan )) {
483+ reader .forEachRemaining (
484+ row -> {
412485 long rowId = row .getLong (rowIdIndex );
413- resultBitmap .add (rowId );
414- scoreMap .put (rowId , computeScore (queryVector , stored , metric ));
486+ if (candidates != null && !candidates .contains (rowId )) {
487+ return ;
488+ }
489+ if (row .isNullAt (vectorIndex )) {
490+ return ;
491+ }
492+ float [] stored = getVector (row , vectorIndex );
493+ rawVectors .put (rowId , stored );
415494 });
416495 } catch (IOException e ) {
417496 throw new RuntimeException ("Failed to read raw vectors for vector search." , e );
418497 }
419498
420- return ScoredGlobalIndexResult .create (resultBitmap , scoreMap ::get ).topK (limit );
499+ return rawVectors ;
500+ }
501+
502+ private List <Range > filteredRawRowRanges (
503+ List <Range > rawRowRanges , @ Nullable RoaringNavigableMap64 preFilter ) {
504+ if (preFilter == null ) {
505+ return rawRowRanges ;
506+ }
507+ return Range .and (
508+ Range .sortAndMergeOverlap (rawRowRanges , true ),
509+ Range .sortAndMergeOverlap (preFilter .toRangeList (), true ));
510+ }
511+
512+ protected ScoredGlobalIndexResult scoreRawVectors (
513+ RoaringNavigableMap64 candidates ,
514+ Map <Long , float []> rawVectors ,
515+ float [] queryVector ,
516+ String metric ,
517+ int topK ) {
518+ PriorityQueue <long []> topKHeap =
519+ new PriorityQueue <>(Math .max (1 , topK + 1 ), WEAKEST_SCORE_FIRST );
520+ for (long rowId : candidates ) {
521+ float [] stored = rawVectors .get (rowId );
522+ if (stored == null ) {
523+ continue ;
524+ }
525+ checkVectorDimension (queryVector , stored );
526+ offerScore (topKHeap , topK , rowId , computeScore (queryVector , stored , metric ));
527+ }
528+ return scoredResult (topKHeap );
529+ }
530+
531+ private static void offerScore (
532+ PriorityQueue <long []> topKHeap , int topK , long rowId , float score ) {
533+ if (topK <= 0 ) {
534+ return ;
535+ }
536+ long [] entry = new long [] {rowId , Float .floatToRawIntBits (score )};
537+ if (topKHeap .size () < topK ) {
538+ topKHeap .offer (entry );
539+ } else if (WEAKEST_SCORE_FIRST .compare (entry , topKHeap .peek ()) > 0 ) {
540+ topKHeap .poll ();
541+ topKHeap .offer (entry );
542+ }
543+ }
544+
545+ private static ScoredGlobalIndexResult scoredResult (PriorityQueue <long []> topKHeap ) {
546+ if (topKHeap .isEmpty ()) {
547+ return ScoredGlobalIndexResult .createEmpty ();
548+ }
549+ RoaringNavigableMap64 resultBitmap = new RoaringNavigableMap64 ();
550+ Map <Long , Float > scoreMap = new HashMap <>();
551+ for (long [] entry : topKHeap ) {
552+ long rowId = entry [0 ];
553+ resultBitmap .add (rowId );
554+ scoreMap .put (rowId , Float .intBitsToFloat ((int ) entry [1 ]));
555+ }
556+ return ScoredGlobalIndexResult .create (resultBitmap , scoreMap ::get );
557+ }
558+
559+ private static void checkVectorDimension (float [] queryVector , float [] stored ) {
560+ if (stored .length != queryVector .length ) {
561+ throw new IllegalArgumentException (
562+ String .format (
563+ "Query vector dimension mismatch: expected %d, got %d" ,
564+ stored .length , queryVector .length ));
565+ }
566+ }
567+
568+ protected RowType rawSearchReadType (boolean includeFilter ) {
569+ RowType tableRowType = table .rowType ();
570+ List <String > readFields = new ArrayList <>();
571+ readFields .add (vectorColumn .name ());
572+
573+ if (includeFilter && filter != null ) {
574+ Set <String > filterFields = PredicateVisitor .collectFieldNames (filter );
575+ for (String field : tableRowType .getFieldNames ()) {
576+ if (filterFields .contains (field ) && !readFields .contains (field )) {
577+ readFields .add (field );
578+ }
579+ }
580+ }
581+
582+ return SpecialFields .rowTypeWithRowId (tableRowType .project (readFields ));
421583 }
422584
423585 private ReadBuilder newRawReadBuilder (RowType readType , boolean includeFilter ) {
@@ -554,7 +716,7 @@ private static String normalizeMetric(String metric) {
554716 return metric .toLowerCase ().replace ('-' , '_' );
555717 }
556718
557- private int configuredRefineFactor (String indexType ) {
719+ protected int configuredRefineFactor (String indexType ) {
558720 String value = configuredRefineFactor (options , indexType );
559721 if (value == null ) {
560722 value = configuredRefineFactor (table .options (), indexType );
0 commit comments