blob: 74d76c5042001b2a8c2acdbe7f34201aca3a913b [file]
# 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.
"""Vector search global index result."""
from abc import abstractmethod
from typing import Callable, Dict, Optional
from pypaimon.globalindex.global_index_result import GlobalIndexResult
from pypaimon.utils.roaring_bitmap import RoaringBitmap64
# Type alias for score getter function
ScoreGetter = Callable[[int], Optional[float]]
class ScoredGlobalIndexResult(GlobalIndexResult):
"""
Vector search global index result for vector index.
This extends GlobalIndexResult with score information for each row ID.
"""
@abstractmethod
def score_getter(self) -> ScoreGetter:
"""Returns a function to get the score for a given row ID."""
pass
def offset(self, offset: int) -> 'ScoredGlobalIndexResult':
"""Returns a new result with row IDs offset by the given amount."""
if offset == 0:
return self
bitmap = self.results()
this_score_getter = self.score_getter()
offset_bitmap = RoaringBitmap64()
for row_id in bitmap:
offset_bitmap.add(row_id + offset)
return SimpleScoredGlobalIndexResult(
offset_bitmap,
lambda row_id: this_score_getter(row_id - offset)
)
def or_(self, other: GlobalIndexResult) -> GlobalIndexResult:
"""Returns the union of this result and the other result."""
if not isinstance(other, ScoredGlobalIndexResult):
return super().or_(other)
this_row_ids = self.results()
this_score_getter = self.score_getter()
other_row_ids = other.results()
other_score_getter = other.score_getter()
result_or = RoaringBitmap64.or_(this_row_ids, other_row_ids)
merged_scores = {}
for row_id in other_row_ids:
merged_scores[row_id] = other_score_getter(row_id)
for row_id in this_row_ids:
merged_scores[row_id] = this_score_getter(row_id)
return SimpleScoredGlobalIndexResult(result_or, lambda row_id: merged_scores.get(row_id))
def top_k(self, k: int) -> 'ScoredGlobalIndexResult':
"""Return the top-k results by score."""
import heapq
row_ids = self.results()
if row_ids.cardinality() <= k:
return self
score_getter_fn = self.score_getter()
# The heap head is the weakest candidate: lowest score, then largest row ID.
heap = []
for row_id in row_ids:
score = score_getter_fn(row_id)
if score is None:
score = 0.0
item = (score, -row_id)
if len(heap) < k:
heapq.heappush(heap, item)
elif item > heap[0]:
heapq.heapreplace(heap, item)
top_k_bitmap = RoaringBitmap64()
for _, neg_row_id in heap:
top_k_bitmap.add(-neg_row_id)
return SimpleScoredGlobalIndexResult(top_k_bitmap, score_getter_fn)
@staticmethod
def create_empty() -> 'ScoredGlobalIndexResult':
"""Returns an empty ScoredGlobalIndexResult."""
return SimpleScoredGlobalIndexResult(RoaringBitmap64(), lambda row_id: 0.0)
@staticmethod
def create(
bitmap: RoaringBitmap64,
score_getter: ScoreGetter
) -> 'ScoredGlobalIndexResult':
"""Creates a new ScoredGlobalIndexResult wrapping the given bitmap."""
return SimpleScoredGlobalIndexResult(bitmap, score_getter)
class SimpleScoredGlobalIndexResult(ScoredGlobalIndexResult):
"""Simple implementation of ScoredGlobalIndexResult."""
def __init__(self, bitmap: RoaringBitmap64, score_getter_fn: ScoreGetter):
self._bitmap = bitmap
self._score_getter_fn = score_getter_fn
def results(self) -> RoaringBitmap64:
return self._bitmap
def score_getter(self) -> ScoreGetter:
return self._score_getter_fn
class DictBasedScoredIndexResult(ScoredGlobalIndexResult):
"""Vector search result backed by a dictionary of row_id -> score."""
def __init__(self, id_to_scores: Dict[int, float]):
self._id_to_scores = id_to_scores
self._bitmap: Optional[RoaringBitmap64] = None
def results(self) -> RoaringBitmap64:
if self._bitmap is None:
self._bitmap = RoaringBitmap64()
for row_id in self._id_to_scores.keys():
self._bitmap.add(row_id)
return self._bitmap
def score_getter(self) -> ScoreGetter:
return lambda row_id: self._id_to_scores.get(row_id)