pub mod error;
pub mod extensions;
pub mod memory;
pub mod stub;
#[cfg(feature = "embedding-store")]
pub mod embedding_store;
#[cfg(feature = "real-pg")]
pub mod real_pg;
#[cfg(feature = "hybrid-search")]
pub mod hybrid_search;
pub use error::VectorError;
pub use extensions::{
AnnIndexDef, AnnIndexRegistry, AnnIndexType, BatchOpsExt, DimensionValidator, HnswParams,
IvfflatParams, MemoryBatchOps, SimilarityAlgorithms, VectorNormalizer, MAX_VECTOR_DIMENSION,
MIN_VECTOR_DIMENSION,
};
pub use memory::InMemoryVectorStore;
pub use stub::StubVectorStore;
#[cfg(feature = "real-pg")]
pub use real_pg::{RealPgConfig, RealPgVectorStore};
use async_trait::async_trait;
use std::collections::HashMap;
use std::str::FromStr;
pub const MAX_TOP_K: usize = 10_000;
pub fn validate_top_k(top_k: usize) -> Result<usize, VectorError> {
if top_k == 0 {
return Err(VectorError::TopKExceeded {
requested: top_k,
max: MAX_TOP_K,
});
}
if top_k > MAX_TOP_K {
return Err(VectorError::TopKExceeded {
requested: top_k,
max: MAX_TOP_K,
});
}
Ok(top_k)
}
#[derive(Debug, Clone)]
pub struct VectorRecord {
pub id: String,
pub vector: Vec<f32>,
pub score: Option<f32>,
pub metadata: Option<HashMap<String, serde_json::Value>>,
}
impl VectorRecord {
pub fn new(id: impl Into<String>, vector: Vec<f32>) -> Self {
Self {
id: id.into(),
vector,
score: None,
metadata: None,
}
}
pub fn with_score(mut self, score: f32) -> Self {
self.score = Some(score);
self
}
pub fn with_metadata(mut self, metadata: HashMap<String, serde_json::Value>) -> Self {
self.metadata = Some(metadata);
self
}
}
#[derive(Debug, Clone)]
pub struct SearchResult {
pub id: String,
pub score: f32,
pub vector: Vec<f32>,
pub text: Option<String>,
pub metadata: Option<HashMap<String, serde_json::Value>>,
}
impl SearchResult {
pub fn new(id: impl Into<String>, score: f32, vector: Vec<f32>) -> Self {
Self {
id: id.into(),
score,
vector,
text: None,
metadata: None,
}
}
pub fn with_text(mut self, text: impl Into<String>) -> Self {
self.text = Some(text.into());
self
}
pub fn with_metadata(mut self, metadata: HashMap<String, serde_json::Value>) -> Self {
self.metadata = Some(metadata);
self
}
}
#[derive(Debug, Clone, Copy, Default, PartialEq)]
pub enum VectorMetric {
#[default]
Cosine,
Euclidean,
DotProduct,
}
impl VectorMetric {
pub fn pg_operator(&self) -> &'static str {
match self {
VectorMetric::Cosine => "<=>",
VectorMetric::Euclidean => "<->",
VectorMetric::DotProduct => "<#>",
}
}
pub fn as_str(&self) -> &'static str {
match self {
VectorMetric::Cosine => "cosine",
VectorMetric::Euclidean => "euclidean",
VectorMetric::DotProduct => "dotproduct",
}
}
}
impl FromStr for VectorMetric {
type Err = String;
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s {
"cosine" => Ok(VectorMetric::Cosine),
"euclidean" => Ok(VectorMetric::Euclidean),
"dotproduct" => Ok(VectorMetric::DotProduct),
_ => Err(format!("unknown vector metric: {}", s)),
}
}
}
#[async_trait]
pub trait PgVectorStore: Send + Sync {
async fn create_collection(
&self,
name: &str,
dimension: usize,
metric: Option<VectorMetric>,
) -> Result<(), VectorError>;
async fn delete_collection(&self, name: &str) -> Result<(), VectorError>;
async fn insert(&self, collection: &str, records: Vec<VectorRecord>)
-> Result<(), VectorError>;
async fn search(
&self,
collection: &str,
query: &[f32],
top_k: usize,
) -> Result<Vec<SearchResult>, VectorError>;
async fn get(&self, collection: &str, id: &str) -> Result<Option<VectorRecord>, VectorError>;
async fn delete(&self, collection: &str, ids: Vec<String>) -> Result<u64, VectorError>;
async fn count(&self, collection: &str) -> Result<usize, VectorError>;
}
#[derive(Debug, Clone)]
pub struct AnnSearchResult {
pub records: Vec<SearchResult>,
pub recall_rate: f64,
pub latency_ms: u64,
}
#[cfg(feature = "ann-accel")]
#[async_trait]
pub trait AnnAccelerated: PgVectorStore {
async fn ann_search(
&self,
collection: &str,
query: &[f32],
top_k: usize,
tenant_id: Option<&str>,
) -> Result<AnnSearchResult, VectorError>;
}
#[cfg(feature = "ann-accel")]
#[async_trait]
impl AnnAccelerated for InMemoryVectorStore {
async fn ann_search(
&self,
collection: &str,
query: &[f32],
top_k: usize,
tenant_id: Option<&str>,
) -> Result<AnnSearchResult, VectorError> {
use std::time::Instant;
let start = Instant::now();
let mut results = self.search(collection, query, top_k).await?;
if let Some(tid) = tenant_id {
results.retain(|r| {
r.metadata
.as_ref()
.and_then(|m| m.get("tenant_id"))
.and_then(|v| v.as_str())
== Some(tid)
});
}
let latency_ms = start.elapsed().as_millis() as u64;
let recall_rate = 1.0;
Ok(AnnSearchResult {
records: results,
recall_rate,
latency_ms,
})
}
}