use std::collections::HashMap;
use daimon_core::vector_store::VectorStore;
use daimon_core::{DaimonError, Document, Result, ScoredDocument};
use opensearch::OpenSearch;
use serde_json::json;
use crate::SpaceType;
pub struct OpenSearchVectorStore {
pub(crate) client: OpenSearch,
pub(crate) index: String,
pub(crate) dimensions: usize,
pub(crate) space_type: SpaceType,
}
impl OpenSearchVectorStore {
pub fn client(&self) -> &OpenSearch {
&self.client
}
pub fn index(&self) -> &str {
&self.index
}
pub fn dimensions(&self) -> usize {
self.dimensions
}
pub fn space_type(&self) -> SpaceType {
self.space_type
}
fn map_os_error(resp: opensearch::Error) -> DaimonError {
DaimonError::Other(format!("opensearch error: {resp}"))
}
}
impl VectorStore for OpenSearchVectorStore {
async fn upsert(&self, id: &str, embedding: Vec<f32>, document: Document) -> Result<()> {
if embedding.len() != self.dimensions {
return Err(DaimonError::Other(format!(
"embedding dimension mismatch: expected {}, got {}",
self.dimensions,
embedding.len()
)));
}
let body = json!({
"embedding": embedding,
"content": document.content,
"metadata": document.metadata,
});
let response = self
.client
.index(opensearch::IndexParts::IndexId(&self.index, id))
.body(body)
.send()
.await
.map_err(Self::map_os_error)?;
let status = response.status_code();
if !status.is_success() {
let text = response
.text()
.await
.unwrap_or_else(|_| "unknown error".into());
return Err(DaimonError::Other(format!(
"opensearch upsert failed ({status}): {text}"
)));
}
Ok(())
}
async fn upsert_many(&self, items: Vec<(String, Vec<f32>, Document)>) -> Result<()> {
if items.is_empty() {
return Ok(());
}
for (_, embedding, _) in &items {
if embedding.len() != self.dimensions {
return Err(DaimonError::Other(format!(
"embedding dimension mismatch: expected {}, got {}",
self.dimensions,
embedding.len()
)));
}
}
let mut body: Vec<opensearch::http::request::JsonBody<serde_json::Value>> =
Vec::with_capacity(items.len() * 2);
for (id, embedding, document) in items {
body.push(json!({ "index": { "_id": id } }).into());
body.push(
json!({
"embedding": embedding,
"content": document.content,
"metadata": document.metadata,
})
.into(),
);
}
let response = self
.client
.bulk(opensearch::BulkParts::Index(&self.index))
.body(body)
.send()
.await
.map_err(Self::map_os_error)?;
let status = response.status_code();
if !status.is_success() {
let text = response
.text()
.await
.unwrap_or_else(|_| "unknown error".into());
return Err(DaimonError::Other(format!(
"opensearch bulk upsert failed ({status}): {text}"
)));
}
let body: serde_json::Value = response
.json()
.await
.map_err(|e| DaimonError::Other(format!("opensearch response parse error: {e}")))?;
if body["errors"].as_bool().unwrap_or(false) {
let first_error = body["items"]
.as_array()
.and_then(|items| {
items
.iter()
.find_map(|item| item["index"]["error"].as_object())
})
.map(|e| serde_json::Value::Object(e.clone()).to_string())
.unwrap_or_else(|| "unknown item error".into());
return Err(DaimonError::Other(format!(
"opensearch bulk upsert had item failures: {first_error}"
)));
}
Ok(())
}
async fn query(&self, embedding: Vec<f32>, top_k: usize) -> Result<Vec<ScoredDocument>> {
if embedding.len() != self.dimensions {
return Err(DaimonError::Other(format!(
"embedding dimension mismatch: expected {}, got {}",
self.dimensions,
embedding.len()
)));
}
let body = json!({
"size": top_k,
"query": {
"knn": {
"embedding": {
"vector": embedding,
"k": top_k
}
}
},
"_source": ["content", "metadata"]
});
let response = self
.client
.search(opensearch::SearchParts::Index(&[&self.index]))
.body(body)
.send()
.await
.map_err(Self::map_os_error)?;
let status = response.status_code();
if !status.is_success() {
let text = response
.text()
.await
.unwrap_or_else(|_| "unknown error".into());
return Err(DaimonError::Other(format!(
"opensearch query failed ({status}): {text}"
)));
}
let body: serde_json::Value = response
.json()
.await
.map_err(|e| DaimonError::Other(format!("opensearch response parse error: {e}")))?;
let hits = body["hits"]["hits"]
.as_array()
.unwrap_or(&Vec::new())
.clone();
let mut results = Vec::with_capacity(hits.len());
for hit in &hits {
let content = hit["_source"]["content"]
.as_str()
.unwrap_or_default()
.to_string();
let metadata: HashMap<String, serde_json::Value> = hit["_source"]
.get("metadata")
.and_then(|m| serde_json::from_value(m.clone()).ok())
.unwrap_or_default();
let score = hit["_score"].as_f64().unwrap_or(0.0);
let doc = Document {
content,
metadata,
score: Some(score),
};
results.push(ScoredDocument::new(doc, score));
}
Ok(results)
}
async fn delete(&self, id: &str) -> Result<bool> {
let response = self
.client
.delete(opensearch::DeleteParts::IndexId(&self.index, id))
.send()
.await
.map_err(Self::map_os_error)?;
let status = response.status_code();
if status == opensearch::http::StatusCode::NOT_FOUND {
return Ok(false);
}
if !status.is_success() {
let text = response
.text()
.await
.unwrap_or_else(|_| "unknown error".into());
return Err(DaimonError::Other(format!(
"opensearch delete failed ({status}): {text}"
)));
}
Ok(true)
}
async fn count(&self) -> Result<usize> {
let response = self
.client
.count(opensearch::CountParts::Index(&[&self.index]))
.send()
.await
.map_err(Self::map_os_error)?;
let status = response.status_code();
if !status.is_success() {
let text = response
.text()
.await
.unwrap_or_else(|_| "unknown error".into());
return Err(DaimonError::Other(format!(
"opensearch count failed ({status}): {text}"
)));
}
let body: serde_json::Value = response
.json()
.await
.map_err(|e| DaimonError::Other(format!("opensearch response parse error: {e}")))?;
let count = body["count"].as_u64().unwrap_or(0) as usize;
Ok(count)
}
}