Skip to content

Commit c8e6255

Browse files
committed
[core] Optimize vector refine raw reads
1 parent d9383ae commit c8e6255

6 files changed

Lines changed: 573 additions & 79 deletions

File tree

paimon-core/src/main/java/org/apache/paimon/table/source/AbstractVectorRead.java

Lines changed: 187 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@
3838
import org.apache.paimon.partition.PartitionPredicate;
3939
import org.apache.paimon.predicate.BatchVectorSearch;
4040
import org.apache.paimon.predicate.Predicate;
41+
import org.apache.paimon.predicate.PredicateVisitor;
4142
import org.apache.paimon.predicate.VectorSearch;
4243
import org.apache.paimon.reader.RecordReader;
4344
import org.apache.paimon.table.FileStoreTable;
@@ -60,6 +61,7 @@
6061
import java.util.List;
6162
import java.util.Map;
6263
import java.util.Optional;
64+
import java.util.PriorityQueue;
6365
import java.util.Set;
6466
import java.util.TreeSet;
6567
import 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);

paimon-core/src/main/java/org/apache/paimon/table/source/BatchVectorReadImpl.java

Lines changed: 28 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -138,9 +138,35 @@ protected ScoredGlobalIndexResult[] readIndexedBatch(
138138
}
139139
}
140140
}
141+
return maybeRerankIndexedBatchResults(merged, indexType, globalIndexer);
142+
}
143+
144+
protected ScoredGlobalIndexResult[] maybeRerankIndexedBatchResults(
145+
ScoredGlobalIndexResult[] results, String indexType, GlobalIndexer globalIndexer) {
146+
if (configuredRefineFactor(indexType) == 0) {
147+
return results;
148+
}
149+
150+
int n = results.length;
151+
int searchLimit = indexedSearchLimit(indexType);
152+
ScoredGlobalIndexResult[] candidates = new ScoredGlobalIndexResult[n];
153+
RoaringNavigableMap64 unionCandidates = new RoaringNavigableMap64();
154+
for (int i = 0; i < n; i++) {
155+
candidates[i] = results[i].topK(searchLimit);
156+
unionCandidates.or(candidates[i].results());
157+
}
158+
159+
if (unionCandidates.isEmpty()) {
160+
return candidates;
161+
}
162+
163+
Map<Long, float[]> rawVectors = readRawVectors(unionCandidates, false);
164+
String metric = rawSearchMetric(globalIndexer);
165+
ScoredGlobalIndexResult[] reranked = new ScoredGlobalIndexResult[n];
141166
for (int i = 0; i < n; i++) {
142-
merged[i] = maybeRerankIndexedResult(merged[i], indexType, globalIndexer, vectors[i]);
167+
reranked[i] =
168+
scoreRawVectors(candidates[i].results(), rawVectors, vectors[i], metric, limit);
143169
}
144-
return merged;
170+
return reranked;
145171
}
146172
}

0 commit comments

Comments
 (0)