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