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 query(&self, embedding: Vec<f32>, top_k: usize) -> Result<Vec<ScoredDocument>> {
90 if embedding.len() != self.dimensions {
91 return Err(DaimonError::Other(format!(
92 "embedding dimension mismatch: expected {}, got {}",
93 self.dimensions,
94 embedding.len()
95 )));
96 }
97
98 let body = json!({
99 "size": top_k,
100 "query": {
101 "knn": {
102 "embedding": {
103 "vector": embedding,
104 "k": top_k
105 }
106 }
107 },
108 "_source": ["content", "metadata"]
109 });
110
111 let response = self
112 .client
113 .search(opensearch::SearchParts::Index(&[&self.index]))
114 .body(body)
115 .send()
116 .await
117 .map_err(Self::map_os_error)?;
118
119 let status = response.status_code();
120 if !status.is_success() {
121 let text = response
122 .text()
123 .await
124 .unwrap_or_else(|_| "unknown error".into());
125 return Err(DaimonError::Other(format!(
126 "opensearch query failed ({status}): {text}"
127 )));
128 }
129
130 let body: serde_json::Value = response
131 .json()
132 .await
133 .map_err(|e| DaimonError::Other(format!("opensearch response parse error: {e}")))?;
134
135 let hits = body["hits"]["hits"]
136 .as_array()
137 .unwrap_or(&Vec::new())
138 .clone();
139
140 let mut results = Vec::with_capacity(hits.len());
141 for hit in &hits {
142 let content = hit["_source"]["content"]
143 .as_str()
144 .unwrap_or_default()
145 .to_string();
146
147 let metadata: HashMap<String, serde_json::Value> = hit["_source"]
148 .get("metadata")
149 .and_then(|m| serde_json::from_value(m.clone()).ok())
150 .unwrap_or_default();
151
152 let score = hit["_score"].as_f64().unwrap_or(0.0);
161
162 let doc = Document {
163 content,
164 metadata,
165 score: Some(score),
166 };
167 results.push(ScoredDocument::new(doc, score));
168 }
169
170 Ok(results)
171 }
172
173 async fn delete(&self, id: &str) -> Result<bool> {
174 let response = self
175 .client
176 .delete(opensearch::DeleteParts::IndexId(&self.index, id))
177 .send()
178 .await
179 .map_err(Self::map_os_error)?;
180
181 let status = response.status_code();
182 if status == opensearch::http::StatusCode::NOT_FOUND {
183 return Ok(false);
184 }
185 if !status.is_success() {
186 let text = response
187 .text()
188 .await
189 .unwrap_or_else(|_| "unknown error".into());
190 return Err(DaimonError::Other(format!(
191 "opensearch delete failed ({status}): {text}"
192 )));
193 }
194
195 Ok(true)
196 }
197
198 async fn count(&self) -> Result<usize> {
199 let response = self
200 .client
201 .count(opensearch::CountParts::Index(&[&self.index]))
202 .send()
203 .await
204 .map_err(Self::map_os_error)?;
205
206 let status = response.status_code();
207 if !status.is_success() {
208 let text = response
209 .text()
210 .await
211 .unwrap_or_else(|_| "unknown error".into());
212 return Err(DaimonError::Other(format!(
213 "opensearch count failed ({status}): {text}"
214 )));
215 }
216
217 let body: serde_json::Value = response
218 .json()
219 .await
220 .map_err(|e| DaimonError::Other(format!("opensearch response parse error: {e}")))?;
221
222 let count = body["count"].as_u64().unwrap_or(0) as usize;
223 Ok(count)
224 }
225}