daimon_plugin_opensearch/
store.rs1use std::collections::HashMap;
4
5use daimon_core::vector_store::VectorStore;
6use daimon_core::{DaimonError, Document, Result, ScoredDocument};
7use opensearch::OpenSearch;
8use serde_json::json;
9
10use crate::SpaceType;
11
12pub struct OpenSearchVectorStore {
16 pub(crate) client: OpenSearch,
17 pub(crate) index: String,
18 pub(crate) dimensions: usize,
19 pub(crate) space_type: SpaceType,
20}
21
22impl OpenSearchVectorStore {
23 pub fn client(&self) -> &OpenSearch {
25 &self.client
26 }
27
28 pub fn index(&self) -> &str {
30 &self.index
31 }
32
33 pub fn dimensions(&self) -> usize {
35 self.dimensions
36 }
37
38 pub fn space_type(&self) -> SpaceType {
43 self.space_type
44 }
45
46 fn map_os_error(resp: opensearch::Error) -> DaimonError {
47 DaimonError::Other(format!("opensearch error: {resp}"))
48 }
49}
50
51impl VectorStore for OpenSearchVectorStore {
52 async fn upsert(&self, id: &str, embedding: Vec<f32>, document: Document) -> Result<()> {
53 if embedding.len() != self.dimensions {
54 return Err(DaimonError::Other(format!(
55 "embedding dimension mismatch: expected {}, got {}",
56 self.dimensions,
57 embedding.len()
58 )));
59 }
60
61 let body = json!({
62 "embedding": embedding,
63 "content": document.content,
64 "metadata": document.metadata,
65 });
66
67 let response = self
68 .client
69 .index(opensearch::IndexParts::IndexId(&self.index, id))
70 .body(body)
71 .send()
72 .await
73 .map_err(Self::map_os_error)?;
74
75 let status = response.status_code();
76 if !status.is_success() {
77 let text = response
78 .text()
79 .await
80 .unwrap_or_else(|_| "unknown error".into());
81 return Err(DaimonError::Other(format!(
82 "opensearch upsert failed ({status}): {text}"
83 )));
84 }
85
86 Ok(())
87 }
88
89 async fn upsert_many(&self, items: Vec<(String, Vec<f32>, Document)>) -> Result<()> {
90 if items.is_empty() {
91 return Ok(());
92 }
93 for (_, embedding, _) in &items {
94 if embedding.len() != self.dimensions {
95 return Err(DaimonError::Other(format!(
96 "embedding dimension mismatch: expected {}, got {}",
97 self.dimensions,
98 embedding.len()
99 )));
100 }
101 }
102
103 let mut body: Vec<opensearch::http::request::JsonBody<serde_json::Value>> =
105 Vec::with_capacity(items.len() * 2);
106 for (id, embedding, document) in items {
107 body.push(json!({ "index": { "_id": id } }).into());
108 body.push(
109 json!({
110 "embedding": embedding,
111 "content": document.content,
112 "metadata": document.metadata,
113 })
114 .into(),
115 );
116 }
117
118 let response = self
119 .client
120 .bulk(opensearch::BulkParts::Index(&self.index))
121 .body(body)
122 .send()
123 .await
124 .map_err(Self::map_os_error)?;
125
126 let status = response.status_code();
127 if !status.is_success() {
128 let text = response
129 .text()
130 .await
131 .unwrap_or_else(|_| "unknown error".into());
132 return Err(DaimonError::Other(format!(
133 "opensearch bulk upsert failed ({status}): {text}"
134 )));
135 }
136
137 let body: serde_json::Value = response
139 .json()
140 .await
141 .map_err(|e| DaimonError::Other(format!("opensearch response parse error: {e}")))?;
142 if body["errors"].as_bool().unwrap_or(false) {
143 let first_error = body["items"]
144 .as_array()
145 .and_then(|items| {
146 items
147 .iter()
148 .find_map(|item| item["index"]["error"].as_object())
149 })
150 .map(|e| serde_json::Value::Object(e.clone()).to_string())
151 .unwrap_or_else(|| "unknown item error".into());
152 return Err(DaimonError::Other(format!(
153 "opensearch bulk upsert had item failures: {first_error}"
154 )));
155 }
156
157 Ok(())
158 }
159
160 async fn query(&self, embedding: Vec<f32>, top_k: usize) -> Result<Vec<ScoredDocument>> {
161 if embedding.len() != self.dimensions {
162 return Err(DaimonError::Other(format!(
163 "embedding dimension mismatch: expected {}, got {}",
164 self.dimensions,
165 embedding.len()
166 )));
167 }
168
169 let body = json!({
170 "size": top_k,
171 "query": {
172 "knn": {
173 "embedding": {
174 "vector": embedding,
175 "k": top_k
176 }
177 }
178 },
179 "_source": ["content", "metadata"]
180 });
181
182 let response = self
183 .client
184 .search(opensearch::SearchParts::Index(&[&self.index]))
185 .body(body)
186 .send()
187 .await
188 .map_err(Self::map_os_error)?;
189
190 let status = response.status_code();
191 if !status.is_success() {
192 let text = response
193 .text()
194 .await
195 .unwrap_or_else(|_| "unknown error".into());
196 return Err(DaimonError::Other(format!(
197 "opensearch query failed ({status}): {text}"
198 )));
199 }
200
201 let body: serde_json::Value = response
202 .json()
203 .await
204 .map_err(|e| DaimonError::Other(format!("opensearch response parse error: {e}")))?;
205
206 let hits = body["hits"]["hits"]
207 .as_array()
208 .unwrap_or(&Vec::new())
209 .clone();
210
211 let mut results = Vec::with_capacity(hits.len());
212 for hit in &hits {
213 let content = hit["_source"]["content"]
214 .as_str()
215 .unwrap_or_default()
216 .to_string();
217
218 let metadata: HashMap<String, serde_json::Value> = hit["_source"]
219 .get("metadata")
220 .and_then(|m| serde_json::from_value(m.clone()).ok())
221 .unwrap_or_default();
222
223 let score = hit["_score"].as_f64().unwrap_or(0.0);
232
233 let doc = Document {
234 content,
235 metadata,
236 score: Some(score),
237 };
238 results.push(ScoredDocument::new(doc, score));
239 }
240
241 Ok(results)
242 }
243
244 async fn delete(&self, id: &str) -> Result<bool> {
245 let response = self
246 .client
247 .delete(opensearch::DeleteParts::IndexId(&self.index, id))
248 .send()
249 .await
250 .map_err(Self::map_os_error)?;
251
252 let status = response.status_code();
253 if status == opensearch::http::StatusCode::NOT_FOUND {
254 return Ok(false);
255 }
256 if !status.is_success() {
257 let text = response
258 .text()
259 .await
260 .unwrap_or_else(|_| "unknown error".into());
261 return Err(DaimonError::Other(format!(
262 "opensearch delete failed ({status}): {text}"
263 )));
264 }
265
266 Ok(true)
267 }
268
269 async fn count(&self) -> Result<usize> {
270 let response = self
271 .client
272 .count(opensearch::CountParts::Index(&[&self.index]))
273 .send()
274 .await
275 .map_err(Self::map_os_error)?;
276
277 let status = response.status_code();
278 if !status.is_success() {
279 let text = response
280 .text()
281 .await
282 .unwrap_or_else(|_| "unknown error".into());
283 return Err(DaimonError::Other(format!(
284 "opensearch count failed ({status}): {text}"
285 )));
286 }
287
288 let body: serde_json::Value = response
289 .json()
290 .await
291 .map_err(|e| DaimonError::Other(format!("opensearch response parse error: {e}")))?;
292
293 let count = body["count"].as_u64().unwrap_or(0) as usize;
294 Ok(count)
295 }
296}