Skip to main content

sz_orm_vector/
lib.rs

1//! SZ-ORM pgvector Extension
2//!
3//! Provides PostgreSQL pgvector vector similarity search capabilities, supporting three implementations:
4//!
5//! - **In-memory implementation** (`InMemoryVectorStore`): Pure Rust vector computation, no database connection, suitable for testing and benchmarking
6//! - **Stub implementation** (`StubVectorStore`): All methods return Unsupported, suitable for debug placeholder
7//! - **Real implementation** (`RealPgVectorStore`, requires `real-pg` feature): Connects to PostgreSQL + pgvector via tokio-postgres
8//!
9//! # Quick Start
10//!
11//! ```rust
12//! use sz_orm_vector::{InMemoryVectorStore, PgVectorStore, VectorRecord, VectorMetric};
13//!
14//! # #[tokio::main]
15//! # async fn main() -> Result<(), Box<dyn std::error::Error>> {
16//! let store = InMemoryVectorStore::new();
17//! store.create_collection("docs", 3, None).await?;
18//!
19//! let record = VectorRecord::new("doc1", vec![1.0, 0.0, 0.0]);
20//! store.insert("docs", vec![record]).await?;
21//!
22//! let results = store.search("docs", &[1.0, 0.0, 0.0], 5).await?;
23//! println!("found {} results", results.len());
24//! # Ok(())
25//! # }
26//! ```
27
28pub mod error;
29pub mod extensions;
30pub mod memory;
31pub mod stub;
32
33#[cfg(feature = "real-pg")]
34pub mod real_pg;
35
36// v4.0.0 M3:混合搜索(hybrid-search feature gate 隔离)
37#[cfg(feature = "hybrid-search")]
38pub mod hybrid_search;
39
40pub use error::VectorError;
41pub use extensions::{
42    AnnIndexDef, AnnIndexRegistry, AnnIndexType, BatchOpsExt, DimensionValidator, HnswParams,
43    IvfflatParams, MemoryBatchOps, SimilarityAlgorithms, VectorNormalizer, MAX_VECTOR_DIMENSION,
44    MIN_VECTOR_DIMENSION,
45};
46pub use memory::InMemoryVectorStore;
47pub use stub::StubVectorStore;
48
49#[cfg(feature = "real-pg")]
50pub use real_pg::{RealPgConfig, RealPgVectorStore};
51
52use async_trait::async_trait;
53use std::collections::HashMap;
54use std::str::FromStr;
55
56/// M-16 fix: top_k maximum limit
57///
58/// Limit top_k upper bound to prevent:
59/// - Large k values causing memory explosion (each SearchResult contains full vector)
60/// - Performance issues with database/vector engine executing huge k queries
61/// - Malicious caller triggering OOM via top_k=usize::MAX
62pub const MAX_TOP_K: usize = 10_000;
63
64/// M-16 fix: Validate whether top_k is within reasonable range
65///
66/// - `top_k = 0`: Returns `TopKExceeded` error (meaningless query)
67/// - `top_k > MAX_TOP_K`: Returns `TopKExceeded` error
68/// - `1 <= top_k <= MAX_TOP_K`: Returns Ok
69pub fn validate_top_k(top_k: usize) -> Result<usize, VectorError> {
70    if top_k == 0 {
71        return Err(VectorError::TopKExceeded {
72            requested: top_k,
73            max: MAX_TOP_K,
74        });
75    }
76    if top_k > MAX_TOP_K {
77        return Err(VectorError::TopKExceeded {
78            requested: top_k,
79            max: MAX_TOP_K,
80        });
81    }
82    Ok(top_k)
83}
84
85/// Vector record
86#[derive(Debug, Clone)]
87pub struct VectorRecord {
88    pub id: String,
89    pub vector: Vec<f32>,
90    pub score: Option<f32>,
91    pub metadata: Option<HashMap<String, serde_json::Value>>,
92}
93
94impl VectorRecord {
95    pub fn new(id: impl Into<String>, vector: Vec<f32>) -> Self {
96        Self {
97            id: id.into(),
98            vector,
99            score: None,
100            metadata: None,
101        }
102    }
103
104    pub fn with_score(mut self, score: f32) -> Self {
105        self.score = Some(score);
106        self
107    }
108
109    pub fn with_metadata(mut self, metadata: HashMap<String, serde_json::Value>) -> Self {
110        self.metadata = Some(metadata);
111        self
112    }
113}
114
115/// Search result
116#[derive(Debug, Clone)]
117pub struct SearchResult {
118    pub id: String,
119    pub score: f32,
120    pub vector: Vec<f32>,
121    pub text: Option<String>,
122    pub metadata: Option<HashMap<String, serde_json::Value>>,
123}
124
125impl SearchResult {
126    pub fn new(id: impl Into<String>, score: f32, vector: Vec<f32>) -> Self {
127        Self {
128            id: id.into(),
129            score,
130            vector,
131            text: None,
132            metadata: None,
133        }
134    }
135
136    pub fn with_text(mut self, text: impl Into<String>) -> Self {
137        self.text = Some(text.into());
138        self
139    }
140
141    pub fn with_metadata(mut self, metadata: HashMap<String, serde_json::Value>) -> Self {
142        self.metadata = Some(metadata);
143        self
144    }
145}
146
147/// Vector distance metric
148#[derive(Debug, Clone, Copy, Default, PartialEq)]
149pub enum VectorMetric {
150    #[default]
151    Cosine,
152    Euclidean,
153    DotProduct,
154}
155
156impl VectorMetric {
157    /// pgvector operator mapping
158    pub fn pg_operator(&self) -> &'static str {
159        match self {
160            VectorMetric::Cosine => "<=>",
161            VectorMetric::Euclidean => "<->",
162            VectorMetric::DotProduct => "<#>",
163        }
164    }
165
166    pub fn as_str(&self) -> &'static str {
167        match self {
168            VectorMetric::Cosine => "cosine",
169            VectorMetric::Euclidean => "euclidean",
170            VectorMetric::DotProduct => "dotproduct",
171        }
172    }
173}
174
175impl FromStr for VectorMetric {
176    type Err = String;
177
178    fn from_str(s: &str) -> Result<Self, Self::Err> {
179        match s {
180            "cosine" => Ok(VectorMetric::Cosine),
181            "euclidean" => Ok(VectorMetric::Euclidean),
182            "dotproduct" => Ok(VectorMetric::DotProduct),
183            _ => Err(format!("unknown vector metric: {}", s)),
184        }
185    }
186}
187
188/// Vector Store core trait
189///
190/// Provides CRUD and similarity search capabilities for vector collections.
191/// All methods are async, suitable for real database I/O.
192#[async_trait]
193pub trait PgVectorStore: Send + Sync {
194    /// Create collection
195    async fn create_collection(
196        &self,
197        name: &str,
198        dimension: usize,
199        metric: Option<VectorMetric>,
200    ) -> Result<(), VectorError>;
201
202    /// Delete collection
203    async fn delete_collection(&self, name: &str) -> Result<(), VectorError>;
204
205    /// Insert vector record (upsert semantics: same id overwrites)
206    async fn insert(&self, collection: &str, records: Vec<VectorRecord>)
207        -> Result<(), VectorError>;
208
209    /// Similarity search
210    ///
211    /// M-16 fix: `top_k` must be in `[1, MAX_TOP_K]` range.
212    /// Implementations should call `validate_top_k(top_k)?` before executing search.
213    async fn search(
214        &self,
215        collection: &str,
216        query: &[f32],
217        top_k: usize,
218    ) -> Result<Vec<SearchResult>, VectorError>;
219
220    /// Get single record
221    async fn get(&self, collection: &str, id: &str) -> Result<Option<VectorRecord>, VectorError>;
222
223    /// Delete record
224    async fn delete(&self, collection: &str, ids: Vec<String>) -> Result<u64, VectorError>;
225
226    /// Count records
227    async fn count(&self, collection: &str) -> Result<usize, VectorError>;
228}