Skip to content

Commit 66d331f

Browse files
authored
feat: add batch insert and search (#74)
Signed-off-by: Arshdeep54 <balarsh535@gmail.com>
1 parent b3d492f commit 66d331f

12 files changed

Lines changed: 306 additions & 50 deletions

File tree

‎Cargo.lock‎

Lines changed: 2 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

‎crates/api/src/lib.rs‎

Lines changed: 77 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
1-
use defs::{DbError, Dimension, IndexedVector, Similarity, SnapshottableDb};
2-
use defs::{DenseVector, Payload, Point, PointId};
1+
use defs::{DbError, Dimension, IndexedVector, SearchQueryInput, Similarity, SnapshottableDb};
2+
use defs::{DenseVector, Payload, Point, PointId, PointInput};
33
use index::hnsw::HnswIndex;
44
use index::kd_tree::index::KDTree;
55
use std::path::{Path, PathBuf};
@@ -70,6 +70,37 @@ impl VectorDb {
7070
Ok(point_id)
7171
}
7272

73+
pub fn insert_batch(&self, points: Vec<PointInput>) -> Result<Vec<PointId>> {
74+
let mut ids = Vec::with_capacity(points.len());
75+
76+
for point in points {
77+
let id = point.id.unwrap_or_else(Uuid::new_v4);
78+
let vector = point.vector;
79+
let payload = point.payload;
80+
81+
if let Some(ref v) = vector
82+
&& v.len() != self.dimension
83+
{
84+
return Err(ApiError::DimensionMismatch {
85+
expected: self.dimension,
86+
got: v.len(),
87+
});
88+
}
89+
90+
self.storage.insert_point(id, vector.clone(), payload)?;
91+
92+
if let Some(v) = vector {
93+
let indexed = IndexedVector { id, vector: v };
94+
let mut index = self.index.write().map_err(|_| ApiError::LockError)?;
95+
index.insert(indexed)?;
96+
}
97+
98+
ids.push(id);
99+
}
100+
101+
Ok(ids)
102+
}
103+
73104
//TODO: Make this an atomic operation
74105
pub fn delete(&self, id: PointId) -> Result<bool> {
75106
// Remove from storage
@@ -95,34 +126,41 @@ impl VectorDb {
95126
}
96127
}
97128

98-
pub fn search(
99-
&self,
100-
query: DenseVector,
101-
similarity: Similarity,
102-
limit: usize,
103-
) -> Result<Vec<PointId>> {
129+
pub fn search(&self, query: SearchQueryInput) -> Result<Vec<PointId>> {
104130
// Validate search limit
105-
if limit == 0 {
106-
return Err(ApiError::InvalidSearchLimit { limit });
131+
if query.limit == 0 {
132+
return Err(ApiError::InvalidSearchLimit { limit: query.limit });
107133
}
108134

109135
// Validate query dimension
110-
if query.len() != self.dimension {
136+
if query.vector.len() != self.dimension {
111137
return Err(ApiError::DimensionMismatch {
112138
expected: self.dimension,
113-
got: query.len(),
139+
got: query.vector.len(),
114140
});
115141
}
116142

117143
// Use vector index to find similar vectors
118144
let index = self.index.read().map_err(|_| ApiError::LockError)?;
119145

120146
//TODO: Add feat of returning similarity scores in the search
121-
let vectors = index.search(query, similarity, limit)?;
147+
let vectors = index.search(query.vector, query.similarity, query.limit)?;
122148

123149
Ok(vectors)
124150
}
125151

152+
pub fn search_batch(&self, queries: Vec<SearchQueryInput>) -> Result<Vec<Vec<PointId>>> {
153+
let mut results = Vec::with_capacity(queries.len());
154+
let index = self.index.read().unwrap();
155+
156+
for query in queries {
157+
let found = index.search(query.vector, query.similarity, query.limit)?;
158+
results.push(found);
159+
}
160+
161+
Ok(results)
162+
}
163+
126164
pub fn list(&self, offset: PointId, limit: usize) -> Result<Option<VectorPage>> {
127165
let page = self.storage.list_vectors(offset, limit)?;
128166
Ok(page)
@@ -359,7 +397,13 @@ mod tests {
359397

360398
// Search for the closest vector to [1.0, 0.1, 0.1]
361399
let query = vec![1.0, 0.1, 0.1];
362-
let results = db.search(query, Similarity::Cosine, 1).unwrap();
400+
let results = db
401+
.search(SearchQueryInput {
402+
vector: query,
403+
similarity: Similarity::Cosine,
404+
limit: 1,
405+
})
406+
.unwrap();
363407

364408
assert_eq!(results.len(), 1);
365409
assert_eq!(results[0], ids[0]); // The first vector should be closest
@@ -387,7 +431,13 @@ mod tests {
387431

388432
// Search with limit 3
389433
let query = vec![0.0, 0.0, 0.0];
390-
let results = db.search(query, Similarity::Euclidean, 3).unwrap();
434+
let results = db
435+
.search(SearchQueryInput {
436+
vector: query,
437+
similarity: Similarity::Euclidean,
438+
limit: 3,
439+
})
440+
.unwrap();
391441

392442
assert_eq!(results.len(), 3);
393443
}
@@ -397,7 +447,11 @@ mod tests {
397447
let (db, _temp_dir) = create_test_db();
398448

399449
let query = vec![1.0, 2.0, 3.0];
400-
let result = db.search(query, Similarity::Cosine, 0);
450+
let result = db.search(SearchQueryInput {
451+
vector: query,
452+
similarity: Similarity::Cosine,
453+
limit: 0,
454+
});
401455

402456
assert!(result.is_err());
403457
match result.unwrap_err() {
@@ -416,7 +470,13 @@ mod tests {
416470
assert!(db.get(Uuid::new_v4()).unwrap().is_none());
417471

418472
let query = vec![1.0, 2.0, 3.0];
419-
let results = db.search(query, Similarity::Cosine, 10).unwrap();
473+
let results = db
474+
.search(SearchQueryInput {
475+
vector: query,
476+
similarity: Similarity::Cosine,
477+
limit: 10,
478+
})
479+
.unwrap();
420480
assert_eq!(results.len(), 0);
421481
}
422482

‎crates/defs/Cargo.toml‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,5 +7,7 @@ edition.workspace = true
77
license.workspace = true
88

99
[dependencies]
10+
axum.workspace = true
1011
serde.workspace = true
12+
snafu.workspace = true
1113
uuid.workspace = true

‎crates/defs/src/error.rs‎

Lines changed: 19 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -24,16 +24,32 @@ pub enum DbError {
2424
PointNotFound { id: PointId },
2525
}
2626

27-
#[derive(Debug)]
27+
use axum::{http::StatusCode, response::IntoResponse};
28+
use snafu::Snafu;
29+
30+
#[derive(Debug, Snafu)]
2831
pub enum ServerError {
29-
Bind(io::Error),
30-
Serve(io::Error),
32+
#[snafu(display("Failed to bind: {source}"))]
33+
Bind { source: io::Error },
34+
35+
#[snafu(display("Failed to serve: {source}"))]
36+
Serve { source: io::Error },
3137
}
3238

3339
#[derive(Debug)]
3440
pub enum AppError {
3541
ServerError(ServerError),
42+
Api(String),
3643
}
3744

45+
impl IntoResponse for AppError {
46+
fn into_response(self) -> axum::response::Response {
47+
let (status, message) = match self {
48+
AppError::Api(msg) => (StatusCode::BAD_REQUEST, msg),
49+
AppError::ServerError(e) => (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()),
50+
};
51+
(status, message).into_response()
52+
}
53+
}
3854
// Error type for server
3955
pub type BoxError = Box<dyn std::error::Error + Send + Sync>;

‎crates/defs/src/types.rs‎

Lines changed: 44 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,18 +21,26 @@ pub enum StoredVector {
2121
Dense(DenseVector),
2222
}
2323

24-
#[derive(Serialize, Deserialize, Clone, Copy, Debug, PartialEq)]
24+
#[derive(Serialize, Deserialize, Clone, Copy, Debug, Default, PartialEq)]
2525
pub enum ContentType {
26+
#[default]
2627
Text,
2728
Image,
2829
}
2930

30-
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq)]
31+
#[derive(Serialize, Deserialize, Clone, Debug, Default, PartialEq)]
3132
pub struct Payload {
3233
pub content_type: ContentType,
3334
pub content: String,
3435
}
3536

37+
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq)]
38+
pub struct PointInput {
39+
pub id: Option<PointId>,
40+
pub vector: Option<DenseVector>,
41+
pub payload: Option<Payload>,
42+
}
43+
3644
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq)]
3745
pub struct Point {
3846
pub id: PointId,
@@ -55,6 +63,40 @@ pub enum Similarity {
5563
Cosine,
5664
}
5765

66+
#[derive(Debug, Clone, Serialize, Deserialize)]
67+
pub struct BatchInsertRequest {
68+
pub points: Vec<PointInput>,
69+
}
70+
71+
#[derive(Debug, Clone, Serialize, Deserialize)]
72+
pub struct BatchInsertResponse {
73+
pub inserted: usize,
74+
pub ids: Vec<PointId>,
75+
}
76+
77+
// For batch search
78+
#[derive(Debug, Clone, Serialize, Deserialize)]
79+
pub struct BatchSearchRequest {
80+
pub queries: Vec<SearchQueryInput>,
81+
}
82+
83+
#[derive(Debug, Clone, Serialize, Deserialize)]
84+
pub struct SearchQueryInput {
85+
pub vector: DenseVector,
86+
pub similarity: Similarity,
87+
pub limit: usize,
88+
}
89+
90+
#[derive(Clone, Serialize, Deserialize, Debug)]
91+
pub struct SearchResponse {
92+
pub results: Vec<PointId>,
93+
}
94+
95+
#[derive(Debug, Clone, Serialize, Deserialize)]
96+
pub struct BatchSearchResponse {
97+
pub results: Vec<SearchResponse>,
98+
}
99+
58100
// Struct which stores the distance between a vector and query vector and implements ordering traits
59101
#[derive(Copy, Clone)]
60102
pub struct DistanceOrderedVector<'q> {

‎crates/grpc/proto/vector-db.proto‎

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,9 @@ service VectorDB {
2020

2121
//Search for the k nearest vectors to a target vector given a distance function
2222
rpc SearchPoints(SearchRequest) returns (SearchResponse) {}
23+
24+
rpc InsertVectorsBatch(InsertVectorsBatchRequest) returns (InsertVectorsBatchResponse) {}
25+
rpc SearchPointsBatch(SearchPointsBatchRequest) returns (SearchPointsBatchResponse) {}
2326
}
2427

2528

@@ -71,3 +74,18 @@ message Payload {
7174
string content = 2;
7275
}
7376

77+
message InsertVectorsBatchRequest {
78+
repeated InsertVectorRequest vectors = 1;
79+
}
80+
81+
message InsertVectorsBatchResponse {
82+
repeated PointID ids = 1;
83+
}
84+
85+
message SearchPointsBatchRequest {
86+
repeated SearchRequest queries = 1;
87+
}
88+
89+
message SearchPointsBatchResponse {
90+
repeated SearchResponse results = 1;
91+
}

0 commit comments

Comments
 (0)