blob: 491b65f132b08a1ebfcde30799a99ecf1e8b8fdf [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.
use std::collections::HashMap;
#[derive(Clone)]
pub struct VectorSearch {
pub vector: Vec<f32>,
pub limit: usize,
pub field_name: String,
pub include_row_ids: Option<roaring::RoaringTreemap>,
}
impl VectorSearch {
pub fn new(vector: Vec<f32>, limit: usize, field_name: String) -> crate::Result<Self> {
if vector.is_empty() {
return Err(crate::Error::DataInvalid {
message: "Search vector cannot be empty".to_string(),
source: None,
});
}
if limit == 0 || limit > i32::MAX as usize {
return Err(crate::Error::DataInvalid {
message: format!("Limit must be between 1 and {}, got: {}", i32::MAX, limit),
source: None,
});
}
if field_name.is_empty() {
return Err(crate::Error::DataInvalid {
message: "Field name cannot be null or empty".to_string(),
source: None,
});
}
Ok(Self {
vector,
limit,
field_name,
include_row_ids: None,
})
}
pub fn with_include_row_ids(mut self, include_row_ids: roaring::RoaringTreemap) -> Self {
self.include_row_ids = Some(include_row_ids);
self
}
}
impl std::fmt::Display for VectorSearch {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(
f,
"VectorSearch(field_name={}, limit={})",
self.field_name, self.limit
)
}
}
pub struct GlobalIndexIOMeta {
pub file_path: String,
pub file_size: u64,
pub metadata: Vec<u8>,
}
impl GlobalIndexIOMeta {
pub fn new(file_path: String, file_size: u64, metadata: Vec<u8>) -> Self {
Self {
file_path,
file_size,
metadata,
}
}
}
#[derive(Debug, Clone)]
pub struct SearchResult {
pub row_ids: Vec<u64>,
pub scores: Vec<f32>,
}
impl SearchResult {
pub fn new(row_ids: Vec<u64>, scores: Vec<f32>) -> Self {
assert_eq!(row_ids.len(), scores.len());
Self { row_ids, scores }
}
pub fn empty() -> Self {
Self {
row_ids: Vec::new(),
scores: Vec::new(),
}
}
pub fn from_scored_map(map: HashMap<u64, f32>) -> Self {
let mut row_ids = Vec::with_capacity(map.len());
let mut scores = Vec::with_capacity(map.len());
for (id, score) in map {
row_ids.push(id);
scores.push(score);
}
Self { row_ids, scores }
}
pub fn len(&self) -> usize {
self.row_ids.len()
}
pub fn is_empty(&self) -> bool {
self.row_ids.is_empty()
}
pub fn offset(&self, offset: i64) -> Self {
if offset == 0 {
return self.clone();
}
let row_ids = self
.row_ids
.iter()
.map(|&id| {
if offset >= 0 {
id.saturating_add(offset as u64)
} else {
id.saturating_sub(offset.unsigned_abs())
}
})
.collect();
Self {
row_ids,
scores: self.scores.clone(),
}
}
pub fn or(&self, other: &SearchResult) -> Self {
let mut row_ids = self.row_ids.clone();
let mut scores = self.scores.clone();
row_ids.extend_from_slice(&other.row_ids);
scores.extend_from_slice(&other.scores);
Self { row_ids, scores }
}
pub fn top_k(&self, k: usize) -> Self {
if self.row_ids.len() <= k {
return self.clone();
}
let mut indices: Vec<usize> = (0..self.row_ids.len()).collect();
indices.sort_by(|&a, &b| {
self.scores[b]
.partial_cmp(&self.scores[a])
.unwrap_or(std::cmp::Ordering::Equal)
});
indices.truncate(k);
let row_ids = indices.iter().map(|&i| self.row_ids[i]).collect();
let scores = indices.iter().map(|&i| self.scores[i]).collect();
Self { row_ids, scores }
}
pub fn to_row_ranges(&self) -> crate::Result<Vec<crate::table::RowRange>> {
if self.row_ids.is_empty() {
return Ok(Vec::new());
}
let mut sorted = self
.row_ids
.iter()
.copied()
.map(|id| {
i64::try_from(id).map_err(|_| crate::Error::DataInvalid {
message: format!(
"Vector search row id {id} exceeds i64::MAX and cannot be converted to RowRange"
),
source: None,
})
})
.collect::<crate::Result<Vec<_>>>()?;
sorted.sort_unstable();
sorted.dedup();
let mut ranges = Vec::new();
let mut start = sorted[0];
let mut end = start;
for &id in &sorted[1..] {
if end.checked_add(1) == Some(id) {
end = id;
} else {
ranges.push(crate::table::RowRange::new(start, end));
start = id;
end = id;
}
}
ranges.push(crate::table::RowRange::new(start, end));
Ok(ranges)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_vector_search_clone_preserves_include_row_ids() {
let mut include_row_ids = roaring::RoaringTreemap::new();
include_row_ids.insert(1);
include_row_ids.insert(3);
let vector_search = VectorSearch::new(vec![1.0, 2.0], 10, "embedding".to_string())
.unwrap()
.with_include_row_ids(include_row_ids.clone());
let cloned = vector_search.clone();
assert_eq!(cloned.vector, vector_search.vector);
assert_eq!(cloned.limit, vector_search.limit);
assert_eq!(cloned.field_name, vector_search.field_name);
assert_eq!(cloned.include_row_ids.as_ref(), Some(&include_row_ids));
}
#[test]
fn test_search_result_from_scored_map() {
let mut map = HashMap::new();
map.insert(1u64, 0.9f32);
map.insert(2, 0.5);
let result = SearchResult::from_scored_map(map);
assert_eq!(result.len(), 2);
}
#[test]
fn test_search_result_top_k() {
let result = SearchResult::new(vec![1, 2, 3, 4, 5], vec![0.1, 0.9, 0.5, 0.8, 0.3]);
let top = result.top_k(2);
assert_eq!(top.len(), 2);
assert!(top.row_ids.contains(&2));
assert!(top.row_ids.contains(&4));
}
#[test]
fn test_search_result_offset() {
let result = SearchResult::new(vec![0, 1], vec![0.5, 0.6]);
let offset = result.offset(100);
assert_eq!(offset.row_ids, vec![100, 101]);
assert_eq!(offset.scores, vec![0.5, 0.6]);
}
#[test]
fn test_search_result_or() {
let a = SearchResult::new(vec![1, 2], vec![0.5, 0.6]);
let b = SearchResult::new(vec![3], vec![0.7]);
let merged = a.or(&b);
assert_eq!(merged.len(), 3);
}
#[test]
fn test_search_result_to_row_ranges() {
let result = SearchResult::new(vec![5, 1, 2, 3, 10], vec![0.1; 5]);
let ranges = result.to_row_ranges().unwrap();
assert_eq!(ranges.len(), 3);
assert_eq!(ranges[0].from(), 1);
assert_eq!(ranges[0].to(), 3);
assert_eq!(ranges[1].from(), 5);
assert_eq!(ranges[1].to(), 5);
assert_eq!(ranges[2].from(), 10);
assert_eq!(ranges[2].to(), 10);
}
#[test]
fn test_search_result_to_row_ranges_rejects_i64_overflow() {
let result = SearchResult::new(vec![i64::MAX as u64 + 1], vec![0.1]);
let err = result.to_row_ranges().unwrap_err();
assert!(
err.to_string().contains("exceeds i64::MAX"),
"unexpected error: {err}"
);
}
}