Skip to content

Commit d53524a

Browse files
committed
feat: add batch insert and search
Signed-off-by: Arshdeep54 <balarsh535@gmail.com>
1 parent d28707a commit d53524a

11 files changed

Lines changed: 296 additions & 56 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: 69 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
use defs::{Dimension, IndexedVector, Similarity};
1+
use defs::{Dimension, IndexedVector, SearchQueryInput, Similarity};
22

33
use defs::{DenseVector, Payload, Point, PointId};
44
use index::hnsw::HnswIndex;
@@ -68,6 +68,28 @@ impl VectorDb {
6868
Ok(point_id)
6969
}
7070

71+
pub fn insert_batch(&self, points: Vec<Point>) -> Result<Vec<PointId>> {
72+
let mut ids = Vec::with_capacity(points.len());
73+
74+
for point in points {
75+
let id = point.id.unwrap_or_else(Uuid::new_v4);
76+
let vector = point.vector;
77+
let payload = point.payload;
78+
79+
self.storage.insert_point(id, vector.clone(), payload)?;
80+
81+
if let Some(v) = vector {
82+
let indexed = IndexedVector { id, vector: v };
83+
let mut index = self.index.write().map_err(|_| ApiError::LockError)?;
84+
index.insert(indexed)?;
85+
}
86+
87+
ids.push(id);
88+
}
89+
90+
Ok(ids)
91+
}
92+
7193
//TODO: Make this an atomic operation
7294
pub fn delete(&self, id: PointId) -> Result<bool> {
7395
// Remove from storage
@@ -84,7 +106,7 @@ impl VectorDb {
84106
let vector = self.storage.get_vector(id)?;
85107
if payload.is_some() || vector.is_some() {
86108
Ok(Some(Point {
87-
id,
109+
id: Some(id),
88110
payload,
89111
vector,
90112
}))
@@ -93,34 +115,41 @@ impl VectorDb {
93115
}
94116
}
95117

96-
pub fn search(
97-
&self,
98-
query: DenseVector,
99-
similarity: Similarity,
100-
limit: usize,
101-
) -> Result<Vec<PointId>> {
118+
pub fn search(&self, query: SearchQueryInput) -> Result<Vec<PointId>> {
102119
// Validate search limit
103-
if limit == 0 {
104-
return Err(ApiError::InvalidSearchLimit { limit });
120+
if query.limit == 0 {
121+
return Err(ApiError::InvalidSearchLimit { limit: query.limit });
105122
}
106123

107124
// Validate query dimension
108-
if query.len() != self.dimension {
125+
if query.vector.len() != self.dimension {
109126
return Err(ApiError::DimensionMismatch {
110127
expected: self.dimension,
111-
got: query.len(),
128+
got: query.vector.len(),
112129
});
113130
}
114131

115132
// Use vector index to find similar vectors
116133
let index = self.index.read().map_err(|_| ApiError::LockError)?;
117134

118135
//TODO: Add feat of returning similarity scores in the search
119-
let vectors = index.search(query, similarity, limit)?;
136+
let vectors = index.search(query.vector, query.similarity, query.limit)?;
120137

121138
Ok(vectors)
122139
}
123140

141+
pub fn search_batch(&self, queries: Vec<SearchQueryInput>) -> Result<Vec<Vec<PointId>>> {
142+
let mut results = Vec::with_capacity(queries.len());
143+
let index = self.index.read().unwrap();
144+
145+
for query in queries {
146+
let found = index.search(query.vector, query.similarity, query.limit)?;
147+
results.push(found);
148+
}
149+
150+
Ok(results)
151+
}
152+
124153
pub fn list(&self, offset: PointId, limit: usize) -> Result<Option<VectorPage>> {
125154
let page = self.storage.list_vectors(offset, limit)?;
126155
Ok(page)
@@ -224,7 +253,7 @@ mod tests {
224253

225254
// Test get
226255
let point = db.get(id).unwrap().unwrap();
227-
assert_eq!(point.id, id);
256+
assert_eq!(point.id, Some(id));
228257
assert_eq!(point.vector.as_ref().unwrap(), &vector);
229258
assert_eq!(point.payload.as_ref().unwrap(), &payload);
230259
assert_eq!(
@@ -305,7 +334,13 @@ mod tests {
305334

306335
// Search for the closest vector to [1.0, 0.1, 0.1]
307336
let query = vec![1.0, 0.1, 0.1];
308-
let results = db.search(query, Similarity::Cosine, 1).unwrap();
337+
let results = db
338+
.search(SearchQueryInput {
339+
vector: query,
340+
similarity: Similarity::Cosine,
341+
limit: 1,
342+
})
343+
.unwrap();
309344

310345
assert_eq!(results.len(), 1);
311346
assert_eq!(results[0], ids[0]); // The first vector should be closest
@@ -333,7 +368,13 @@ mod tests {
333368

334369
// Search with limit 3
335370
let query = vec![0.0, 0.0, 0.0];
336-
let results = db.search(query, Similarity::Euclidean, 3).unwrap();
371+
let results = db
372+
.search(SearchQueryInput {
373+
vector: query,
374+
similarity: Similarity::Euclidean,
375+
limit: 3,
376+
})
377+
.unwrap();
337378

338379
assert_eq!(results.len(), 3);
339380
}
@@ -343,7 +384,11 @@ mod tests {
343384
let (db, _temp_dir) = create_test_db();
344385

345386
let query = vec![1.0, 2.0, 3.0];
346-
let result = db.search(query, Similarity::Cosine, 0);
387+
let result = db.search(SearchQueryInput {
388+
vector: query,
389+
similarity: Similarity::Cosine,
390+
limit: 0,
391+
});
347392

348393
assert!(result.is_err());
349394
match result.unwrap_err() {
@@ -362,7 +407,13 @@ mod tests {
362407
assert!(db.get(Uuid::new_v4()).unwrap().is_none());
363408

364409
let query = vec![1.0, 2.0, 3.0];
365-
let results = db.search(query, Similarity::Cosine, 10).unwrap();
410+
let results = db
411+
.search(SearchQueryInput {
412+
vector: query,
413+
similarity: Similarity::Cosine,
414+
limit: 10,
415+
})
416+
.unwrap();
366417
assert_eq!(results.len(), 0);
367418
}
368419

‎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
@@ -1,15 +1,31 @@
11
use std::io;
22

3-
#[derive(Debug)]
3+
use axum::{http::StatusCode, response::IntoResponse};
4+
use snafu::Snafu;
5+
6+
#[derive(Debug, Snafu)]
47
pub enum ServerError {
5-
Bind(io::Error),
6-
Serve(io::Error),
8+
#[snafu(display("Failed to bind: {source}"))]
9+
Bind { source: io::Error },
10+
11+
#[snafu(display("Failed to serve: {source}"))]
12+
Serve { source: io::Error },
713
}
814

915
#[derive(Debug)]
1016
pub enum AppError {
1117
ServerError(ServerError),
18+
Api(String),
1219
}
1320

21+
impl IntoResponse for AppError {
22+
fn into_response(self) -> axum::response::Response {
23+
let (status, message) = match self {
24+
AppError::Api(msg) => (StatusCode::BAD_REQUEST, msg),
25+
AppError::ServerError(e) => (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()),
26+
};
27+
(status, message).into_response()
28+
}
29+
}
1430
// Error type for server
1531
pub type BoxError = Box<dyn std::error::Error + Send + Sync>;

‎crates/defs/src/types.rs‎

Lines changed: 39 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -19,21 +19,22 @@ pub enum StoredVector {
1919
Dense(DenseVector),
2020
}
2121

22-
#[derive(Serialize, Deserialize, Clone, Copy, Debug, PartialEq)]
22+
#[derive(Serialize, Deserialize, Clone, Copy, Debug, Default, PartialEq)]
2323
pub enum ContentType {
24+
#[default]
2425
Text,
2526
Image,
2627
}
2728

28-
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq)]
29+
#[derive(Serialize, Deserialize, Clone, Debug, Default, PartialEq)]
2930
pub struct Payload {
3031
pub content_type: ContentType,
3132
pub content: String,
3233
}
3334

3435
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq)]
3536
pub struct Point {
36-
pub id: PointId,
37+
pub id: Option<PointId>,
3738
pub vector: Option<DenseVector>,
3839
pub payload: Option<Payload>,
3940
}
@@ -45,14 +46,48 @@ pub struct IndexedVector {
4546
pub vector: DenseVector,
4647
}
4748

48-
#[derive(Debug, Deserialize, Copy, Clone)]
49+
#[derive(Debug, Serialize, Deserialize, Copy, Clone)]
4950
pub enum Similarity {
5051
Euclidean,
5152
Manhattan,
5253
Hamming,
5354
Cosine,
5455
}
5556

57+
#[derive(Debug, Clone, Serialize, Deserialize)]
58+
pub struct BatchInsertRequest {
59+
pub points: Vec<Point>,
60+
}
61+
62+
#[derive(Debug, Clone, Serialize, Deserialize)]
63+
pub struct BatchInsertResponse {
64+
pub inserted: usize,
65+
pub ids: Vec<PointId>,
66+
}
67+
68+
// For batch search
69+
#[derive(Debug, Clone, Serialize, Deserialize)]
70+
pub struct BatchSearchRequest {
71+
pub queries: Vec<SearchQueryInput>,
72+
}
73+
74+
#[derive(Debug, Clone, Serialize, Deserialize)]
75+
pub struct SearchQueryInput {
76+
pub vector: DenseVector,
77+
pub similarity: Similarity,
78+
pub limit: usize,
79+
}
80+
81+
#[derive(Clone, Serialize, Deserialize, Debug)]
82+
pub struct SearchResponse {
83+
pub results: Vec<PointId>,
84+
}
85+
86+
#[derive(Debug, Clone, Serialize, Deserialize)]
87+
pub struct BatchSearchResponse {
88+
pub results: Vec<SearchResponse>,
89+
}
90+
5691
// Struct which stores the distance between a vector and query vector and implements ordering traits
5792
#[derive(Copy, Clone)]
5893
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)