1use async_trait::async_trait;
20use serde::{Deserialize, Serialize};
21use serde_json::json;
22
23use crate::{Document, SearchResult, VectorStore, VectorStoreError};
24
25#[derive(Debug, Clone)]
27pub struct LanceDBConfig {
28 pub uri: String,
30 pub table_name: String,
32 pub api_key: Option<String>,
34 pub region: Option<String>,
36}
37
38impl LanceDBConfig {
39 pub fn new(uri: impl Into<String>, table_name: impl Into<String>) -> Self {
41 Self {
42 uri: uri.into(),
43 table_name: table_name.into(),
44 api_key: None,
45 region: None,
46 }
47 }
48
49 pub fn from_env_result() -> Result<Self, String> {
51 let uri = std::env::var("LANCEDB_URI")
52 .map_err(|_| "LANCEDB_URI environment variable not set".to_string())?;
53 let table_name = std::env::var("LANCEDB_TABLE_NAME")
54 .map_err(|_| "LANCEDB_TABLE_NAME environment variable not set".to_string())?;
55 let api_key = std::env::var("LANCEDB_API_KEY").ok();
56 let region = std::env::var("LANCEDB_REGION").ok();
57 Ok(Self {
58 uri,
59 table_name,
60 api_key,
61 region,
62 })
63 }
64
65 pub fn with_api_key(mut self, key: impl Into<String>) -> Self {
67 self.api_key = Some(key.into());
68 self
69 }
70
71 pub fn with_region(mut self, region: impl Into<String>) -> Self {
73 self.region = Some(region.into());
74 self
75 }
76}
77
78pub struct LanceDBVectorStore {
82 config: LanceDBConfig,
83 client: reqwest::Client,
84}
85
86impl std::fmt::Debug for LanceDBVectorStore {
87 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
88 f.debug_struct("LanceDBVectorStore")
89 .field("table", &self.config.table_name)
90 .finish()
91 }
92}
93
94impl LanceDBVectorStore {
95 pub fn new(config: LanceDBConfig) -> Self {
97 Self {
98 config,
99 client: reqwest::Client::new(),
100 }
101 }
102
103 pub fn from_env_result() -> Result<Self, String> {
105 Ok(Self::new(LanceDBConfig::from_env_result()?))
106 }
107
108 fn table_url(&self) -> String {
110 format!(
111 "{}/v1/table/{}",
112 self.config.uri.trim_end_matches('/'),
113 self.config.table_name
114 )
115 }
116
117 fn add_auth(&self, req: reqwest::RequestBuilder) -> reqwest::RequestBuilder {
119 let mut req = req;
120 if let Some(ref api_key) = self.config.api_key {
121 req = req.header("x-api-key", api_key);
122 }
123 if let Some(ref region) = self.config.region {
124 req = req.header("x-region", region);
125 }
126 req
127 }
128}
129
130#[derive(Debug, Serialize, Deserialize)]
132struct LanceDBDocument {
133 id: String,
134 vector: Vec<f32>,
135 content: String,
136 #[serde(default, skip_serializing_if = "hash_map_is_empty")]
137 metadata: std::collections::HashMap<String, String>,
138}
139
140fn hash_map_is_empty(map: &std::collections::HashMap<String, String>) -> bool {
141 map.is_empty()
142}
143
144#[derive(Debug, Deserialize)]
146struct LanceDBSearchResponse {
147 data: Vec<LanceDBSearchItem>,
148}
149
150#[derive(Debug, Deserialize)]
151struct LanceDBSearchItem {
152 id: String,
153 vector: Vec<f32>,
154 content: String,
155 #[serde(default)]
156 metadata: std::collections::HashMap<String, String>,
157 #[serde(default)]
158 score: Option<f32>,
159}
160
161#[async_trait]
162impl VectorStore for LanceDBVectorStore {
163 async fn add_documents(
164 &self,
165 documents: Vec<Document>,
166 embeddings: Vec<Vec<f32>>,
167 ) -> Result<Vec<String>, VectorStoreError> {
168 if documents.len() != embeddings.len() {
169 return Err(VectorStoreError::EmbeddingError(
170 "Number of documents and embeddings must match".to_string(),
171 ));
172 }
173
174 let lancedb_docs: Vec<LanceDBDocument> = documents
175 .into_iter()
176 .zip(embeddings)
177 .map(|(doc, vec)| {
178 let id = doc.id.clone().unwrap_or_else(|| uuid::Uuid::new_v4().to_string());
179 LanceDBDocument {
180 id: id.clone(),
181 vector: vec,
182 content: doc.content,
183 metadata: doc.metadata,
184 }
185 })
186 .collect();
187
188 let ids: Vec<String> = lancedb_docs.iter().map(|d| d.id.clone()).collect();
189
190 let url = format!("{}/insert", self.table_url());
191 let body = json!({
192 "data": lancedb_docs,
193 });
194
195 let req = self.client.post(&url);
196 let req = self.add_auth(req);
197 let response = req
198 .header("Content-Type", "application/json")
199 .json(&body)
200 .send()
201 .await
202 .map_err(|e| VectorStoreError::ConnectionError(e.to_string()))?;
203
204 let status = response.status();
205 if !status.is_success() {
206 let error_text = response.text().await.unwrap_or_default();
207 return Err(VectorStoreError::StorageError(format!(
208 "HTTP {}: {}",
209 status, error_text
210 )));
211 }
212
213 Ok(ids)
214 }
215
216 async fn similarity_search(
217 &self,
218 query_embedding: &[f32],
219 k: usize,
220 ) -> Result<Vec<SearchResult>, VectorStoreError> {
221 let url = format!("{}/search", self.table_url());
222 let body = json!({
223 "vector": query_embedding,
224 "k": k,
225 });
226
227 let req = self.client.post(&url);
228 let req = self.add_auth(req);
229 let response = req
230 .header("Content-Type", "application/json")
231 .json(&body)
232 .send()
233 .await
234 .map_err(|e| VectorStoreError::ConnectionError(e.to_string()))?;
235
236 let status = response.status();
237 if !status.is_success() {
238 let error_text = response.text().await.unwrap_or_default();
239 return Err(VectorStoreError::StorageError(format!(
240 "HTTP {}: {}",
241 status, error_text
242 )));
243 }
244
245 let search_response: LanceDBSearchResponse = response
246 .json()
247 .await
248 .map_err(|e| VectorStoreError::StorageError(e.to_string()))?;
249
250 Ok(search_response
251 .data
252 .into_iter()
253 .map(|item| {
254 let mut doc = Document::new(item.content).with_id(item.id);
255 for (key, value) in item.metadata {
256 doc = doc.with_metadata(key, value);
257 }
258 SearchResult {
259 document: doc,
260 score: item.score.unwrap_or(0.0),
261 }
262 })
263 .collect())
264 }
265
266 async fn get_document(&self, id: &str) -> Result<Option<Document>, VectorStoreError> {
267 let url = format!("{}/get/{}", self.table_url(), id);
268
269 let req = self.client.get(&url);
270 let req = self.add_auth(req);
271 let response = req
272 .send()
273 .await
274 .map_err(|e| VectorStoreError::ConnectionError(e.to_string()))?;
275
276 let status = response.status();
277 if status == reqwest::StatusCode::NOT_FOUND {
278 return Ok(None);
279 }
280 if !status.is_success() {
281 let error_text = response.text().await.unwrap_or_default();
282 return Err(VectorStoreError::StorageError(format!(
283 "HTTP {}: {}",
284 status, error_text
285 )));
286 }
287
288 let item: LanceDBSearchItem = response
289 .json()
290 .await
291 .map_err(|e| VectorStoreError::StorageError(e.to_string()))?;
292
293 let mut doc = Document::new(item.content).with_id(item.id);
294 for (key, value) in item.metadata {
295 doc = doc.with_metadata(key, value);
296 }
297 Ok(Some(doc))
298 }
299
300 async fn get_embedding(&self, id: &str) -> Result<Option<Vec<f32>>, VectorStoreError> {
301 let url = format!("{}/get/{}", self.table_url(), id);
302
303 let req = self.client.get(&url);
304 let req = self.add_auth(req);
305 let response = req
306 .send()
307 .await
308 .map_err(|e| VectorStoreError::ConnectionError(e.to_string()))?;
309
310 let status = response.status();
311 if status == reqwest::StatusCode::NOT_FOUND {
312 return Ok(None);
313 }
314 if !status.is_success() {
315 let error_text = response.text().await.unwrap_or_default();
316 return Err(VectorStoreError::StorageError(format!(
317 "HTTP {}: {}",
318 status, error_text
319 )));
320 }
321
322 let item: LanceDBSearchItem = response
323 .json()
324 .await
325 .map_err(|e| VectorStoreError::StorageError(e.to_string()))?;
326
327 Ok(Some(item.vector))
328 }
329
330 async fn delete_document(&self, id: &str) -> Result<(), VectorStoreError> {
331 let url = format!("{}/delete/{}", self.table_url(), id);
332
333 let req = self.client.delete(&url);
334 let req = self.add_auth(req);
335 let response = req
336 .send()
337 .await
338 .map_err(|e| VectorStoreError::ConnectionError(e.to_string()))?;
339
340 let status = response.status();
341 if !status.is_success() {
342 let error_text = response.text().await.unwrap_or_default();
343 return Err(VectorStoreError::StorageError(format!(
344 "HTTP {}: {}",
345 status, error_text
346 )));
347 }
348
349 Ok(())
350 }
351
352 async fn count(&self) -> usize {
353 let url = format!("{}/count", self.table_url());
354
355 let req = self.client.get(&url);
356 let req = self.add_auth(req);
357 let result = req.send().await;
358
359 match result {
360 Ok(response) if response.status().is_success() => {
361 let body: serde_json::Value = response.json().await.unwrap_or_default();
362 body["count"].as_u64().unwrap_or(0) as usize
363 }
364 _ => 0,
365 }
366 }
367
368 async fn clear(&self) -> Result<(), VectorStoreError> {
369 let url = format!("{}/clear", self.table_url());
370
371 let req = self.client.post(&url);
372 let req = self.add_auth(req);
373 let response = req
374 .send()
375 .await
376 .map_err(|e| VectorStoreError::ConnectionError(e.to_string()))?;
377
378 let status = response.status();
379 if !status.is_success() {
380 let error_text = response.text().await.unwrap_or_default();
381 return Err(VectorStoreError::StorageError(format!(
382 "HTTP {}: {}",
383 status, error_text
384 )));
385 }
386
387 Ok(())
388 }
389}
390
391#[cfg(test)]
392mod tests {
393 use super::*;
394
395 #[test]
396 fn test_config_new() {
397 let config = LanceDBConfig::new("http://localhost:1337", "my_table");
398 assert_eq!(config.uri, "http://localhost:1337");
399 assert_eq!(config.table_name, "my_table");
400 assert!(config.api_key.is_none());
401 }
402
403 #[test]
404 fn test_config_builder() {
405 let config = LanceDBConfig::new("http://localhost:1337", "test")
406 .with_api_key("secret")
407 .with_region("us-east-1");
408 assert_eq!(config.api_key, Some("secret".to_string()));
409 assert_eq!(config.region, Some("us-east-1".to_string()));
410 }
411
412 #[test]
413 fn test_table_url() {
414 let config = LanceDBConfig::new("http://localhost:1337", "my_table");
415 let store = LanceDBVectorStore::new(config);
416 assert_eq!(
417 store.table_url(),
418 "http://localhost:1337/v1/table/my_table"
419 );
420 }
421
422 #[test]
423 fn test_table_url_trailing_slash() {
424 let config = LanceDBConfig::new("http://localhost:1337/", "my_table");
425 let store = LanceDBVectorStore::new(config);
426 assert_eq!(
427 store.table_url(),
428 "http://localhost:1337/v1/table/my_table"
429 );
430 }
431
432 #[test]
433 fn test_store_new() {
434 let config = LanceDBConfig::new("http://localhost:1337", "test");
435 let _store = LanceDBVectorStore::new(config);
436 }
437
438 #[test]
439 fn test_lancedb_document_serialization() {
440 let doc = LanceDBDocument {
441 id: "test-1".to_string(),
442 vector: vec![0.1, 0.2, 0.3],
443 content: "hello world".to_string(),
444 metadata: std::collections::HashMap::new(),
445 };
446 let json = serde_json::to_value(&doc).unwrap();
447 assert_eq!(json["id"], "test-1");
448 assert!(json["vector"].is_array());
449 assert_eq!(json["content"], "hello world");
450 }
451}