1use std::fmt::Display;
11
12use rig_core::{
13 Embed,
14 embeddings::{Embedding, EmbeddingModel},
15 vector_store::{
16 InsertDocuments, VectorStoreError, VectorStoreIndex,
17 request::{DynamicSearchFilter, Filter, FilterError, SearchFilter, VectorSearchRequest},
18 },
19};
20use serde::{Deserialize, Serialize, de::DeserializeOwned};
21use surrealdb::{
22 Connection, Surreal,
23 types::{RecordId, RecordIdKey, SurrealValue, ToSql, Value},
24};
25
26pub use surrealdb::engine::local::Mem;
27pub use surrealdb::engine::remote::ws::{Ws, Wss};
28
29pub struct SurrealVectorStore<C, Model>
30where
31 C: Connection,
32 Model: EmbeddingModel,
33{
34 model: Model,
35 surreal: Surreal<C>,
36 documents_table: String,
37 distance_function: SurrealDistanceFunction,
38}
39
40pub enum SurrealDistanceFunction {
42 Knn,
43 Hamming,
44 Euclidean,
45 Cosine,
46 Jaccard,
47}
48
49impl Display for SurrealDistanceFunction {
50 fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
51 match self {
52 SurrealDistanceFunction::Cosine => write!(f, "vector::similarity::cosine"),
53 SurrealDistanceFunction::Knn => write!(f, "vector::distance::knn"),
54 SurrealDistanceFunction::Euclidean => write!(f, "vector::distance::euclidean"),
55 SurrealDistanceFunction::Hamming => write!(f, "vector::distance::hamming"),
56 SurrealDistanceFunction::Jaccard => write!(f, "vector::similarity::jaccard"),
57 }
58 }
59}
60
61#[derive(Debug, Deserialize, SurrealValue)]
62struct SearchResult {
63 id: RecordId,
64 document: String,
65 distance: f64,
66}
67
68#[derive(Debug, Serialize, Deserialize, SurrealValue)]
69pub struct CreateRecord {
70 document: String,
71 embedded_text: String,
72 embedding: Vec<f64>,
73}
74
75#[derive(Debug, Deserialize, SurrealValue)]
76pub struct SearchResultOnlyId {
77 id: RecordId,
78 distance: f64,
79}
80
81impl SearchResult {
82 pub fn into_result<T: DeserializeOwned>(self) -> Result<(f64, String, T), VectorStoreError> {
83 let document: T =
84 serde_json::from_str(&self.document).map_err(VectorStoreError::JsonError)?;
85
86 Ok((self.distance, record_key_to_string(&self.id.key), document))
87 }
88}
89
90fn record_key_to_string(key: &RecordIdKey) -> String {
91 match key {
92 RecordIdKey::Number(value) => value.to_string(),
93 RecordIdKey::String(value) => value.clone(),
94 RecordIdKey::Uuid(value) => value.to_string(),
95 RecordIdKey::Array(_) | RecordIdKey::Object(_) | RecordIdKey::Range(_) => key.to_sql(),
96 }
97}
98
99impl<C, Model> InsertDocuments for SurrealVectorStore<C, Model>
100where
101 C: Connection + Send + Sync,
102 Model: EmbeddingModel + Send + Sync,
103{
104 async fn insert_documents<Doc: Serialize + Embed + Send>(
105 &self,
106 documents: Vec<(Doc, Vec<Embedding>)>,
107 ) -> Result<(), VectorStoreError> {
108 let records =
109 rig_core::vector_store::flatten_embedded(documents, |json_document, embedding| {
110 Ok(CreateRecord {
111 document: serde_json::to_string(json_document)?,
112 embedded_text: embedding.document,
113 embedding: embedding.vec,
114 })
115 })?;
116
117 for record in records {
118 self.surreal
119 .create::<Option<CreateRecord>>(self.documents_table.clone())
120 .content(record)
121 .await
122 .map_err(VectorStoreError::datastore)?;
123 }
124
125 Ok(())
126 }
127}
128
129#[derive(Debug, Clone, Serialize, Deserialize)]
130pub struct SurrealSearchFilter(String);
131
132impl SurrealSearchFilter {
133 fn inner(self) -> String {
134 self.0
135 }
136}
137
138impl TryFrom<Filter<serde_json::Value>> for SurrealSearchFilter {
139 type Error = FilterError;
140
141 fn try_from(value: Filter<serde_json::Value>) -> Result<Self, Self::Error> {
142 value.try_interpret(|v| Ok(Value::from_t(v)))
143 }
144}
145
146impl DynamicSearchFilter for SurrealSearchFilter {
147 fn from_dynamic_filter(filter: Filter<serde_json::Value>) -> Result<Self, FilterError> {
148 Self::try_from(filter)
149 }
150}
151
152impl std::fmt::Display for SurrealSearchFilter {
153 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
154 write!(f, "{}", self.0)
155 }
156}
157
158impl SearchFilter for SurrealSearchFilter {
159 type Value = Value;
160
161 fn eq(key: impl AsRef<str>, value: Self::Value) -> Self {
162 Self(format!("{} = {}", key.as_ref(), value.to_sql()))
163 }
164
165 fn gt(key: impl AsRef<str>, value: Self::Value) -> Self {
166 Self(format!("{} > {}", key.as_ref(), value.to_sql()))
167 }
168
169 fn lt(key: impl AsRef<str>, value: Self::Value) -> Self {
170 Self(format!("{} < {}", key.as_ref(), value.to_sql()))
171 }
172
173 fn and(self, rhs: Self) -> Self {
174 Self(format!("({self}) AND ({rhs})"))
175 }
176
177 fn or(self, rhs: Self) -> Self {
178 Self(format!("({self}) OR ({rhs})"))
179 }
180}
181
182impl SurrealSearchFilter {
183 #[allow(clippy::should_implement_trait)]
184 pub fn not(self) -> Self {
185 Self(format!("NOT ({self})"))
186 }
187
188 pub fn contains(key: String, val: <Self as SearchFilter>::Value) -> Self {
190 Self(format!("{key} CONTAINS {}", val.to_sql()))
191 }
192
193 pub fn does_not_contain(key: String, val: <Self as SearchFilter>::Value) -> Self {
195 Self(format!("{key} CONTAINSNOT {}", val.to_sql()))
196 }
197
198 pub fn all(key: String, vals: <Self as SearchFilter>::Value) -> Self {
201 Self(format!("{key} CONTAINSALL {}", vals.to_sql()))
202 }
203
204 pub fn any(key: String, vals: <Self as SearchFilter>::Value) -> Self {
207 Self(format!("{key} CONTAINSANY {}", vals.to_sql()))
208 }
209
210 pub fn member(key: String, vals: <Self as SearchFilter>::Value) -> Self {
213 Self(format!("{key} IN {}", vals.to_sql()))
214 }
215
216 pub fn not_member(key: String, vals: <Self as SearchFilter>::Value) -> Self {
219 Self(format!("{key} NOTIN {}", vals.to_sql()))
220 }
221
222 pub fn inside(key: String, geometry: <Self as SearchFilter>::Value) -> Self {
225 Self(format!("{key} INSIDE {}", geometry.to_sql()))
226 }
227
228 pub fn outside(key: String, geometry: <Self as SearchFilter>::Value) -> Self {
230 Self(format!("{key} OUTSIDE {}", geometry.to_sql()))
231 }
232
233 pub fn intersects(key: String, geometry: <Self as SearchFilter>::Value) -> Self {
235 Self(format!("{key} INTERSECTS {}", geometry.to_sql()))
236 }
237
238 pub fn matches<'a, S: AsRef<&'a str>>(key: String, query: S) -> Self {
241 Self(format!("{key} @@ {}", query.as_ref()))
242 }
243
244 pub fn regex<'a, S: AsRef<&'a str>>(key: String, pattern: S) -> Self {
247 Self(format!("{key} = /{}/", pattern.as_ref()))
248 }
249}
250
251impl<C, Model> SurrealVectorStore<C, Model>
252where
253 C: Connection,
254 Model: EmbeddingModel,
255{
256 pub fn new(
257 model: Model,
258 surreal: Surreal<C>,
259 documents_table: Option<String>,
260 distance_function: SurrealDistanceFunction,
261 ) -> Self {
262 Self {
263 model,
264 surreal,
265 documents_table: documents_table.unwrap_or(String::from("documents")),
266 distance_function,
267 }
268 }
269
270 pub fn inner_client(&self) -> &Surreal<C> {
271 &self.surreal
272 }
273
274 pub fn with_defaults(model: Model, surreal: Surreal<C>) -> Self {
275 Self::new(model, surreal, None, SurrealDistanceFunction::Cosine)
276 }
277
278 async fn run_search_query(
280 &self,
281 req: &VectorSearchRequest<SurrealSearchFilter>,
282 with_document: bool,
283 ) -> Result<surrealdb::IndexedResults, VectorStoreError> {
284 let embedded_query: Vec<f64> = self.model.embed_text(req.query()).await?.vec;
285
286 self.surreal
287 .query(self.search_query(with_document).as_str())
288 .bind(("vec", embedded_query))
289 .bind(("tablename", self.documents_table.clone()))
290 .bind(("threshold", req.threshold().unwrap_or(0.)))
291 .bind(("limit", req.samples() as usize))
292 .bind((
293 "filter",
294 req.filter()
295 .clone()
296 .map(SurrealSearchFilter::inner)
297 .unwrap_or("true".into()),
298 ))
299 .await
300 .map_err(VectorStoreError::datastore)
301 }
302
303 fn search_query(&self, with_document: bool) -> String {
304 let document = if with_document { ", document" } else { "" };
305 let embedded_text = if with_document { ", embedded_text" } else { "" };
306
307 let Self {
308 distance_function, ..
309 } = self;
310
311 format!(
312 "
313 SELECT id {document} {embedded_text}, {distance_function}($vec, embedding) as distance \
314 from type::table($tablename) \
315 where {distance_function}($vec, embedding) >= $threshold AND $filter \
316 order by distance desc \
317 LIMIT $limit",
318 )
319 }
320}
321
322impl<C, Model> VectorStoreIndex for SurrealVectorStore<C, Model>
323where
324 C: Connection,
325 Model: EmbeddingModel,
326{
327 type Filter = SurrealSearchFilter;
328
329 async fn top_n<T: for<'a> Deserialize<'a> + Send>(
332 &self,
333 req: VectorSearchRequest<SurrealSearchFilter>,
334 ) -> Result<Vec<(f64, String, T)>, VectorStoreError> {
335 let mut response = self.run_search_query(&req, true).await?;
336
337 let rows: Vec<SearchResult> = response.take(0).map_err(VectorStoreError::datastore)?;
338
339 let rows: Vec<(f64, String, T)> = rows
340 .into_iter()
341 .map(SearchResult::into_result)
342 .collect::<Result<Vec<_>, _>>()?;
343
344 Ok(rows)
345 }
346
347 async fn top_n_ids(
349 &self,
350 req: VectorSearchRequest<SurrealSearchFilter>,
351 ) -> Result<Vec<(f64, String)>, VectorStoreError> {
352 let mut response = self.run_search_query(&req, false).await?;
356
357 let rows: Vec<SearchResultOnlyId> = response
358 .take::<Vec<SearchResultOnlyId>>(0)
359 .map_err(VectorStoreError::datastore)?;
360
361 let rows: Vec<(f64, String)> = rows
362 .into_iter()
363 .map(|row| (row.distance, record_key_to_string(&row.id.key)))
364 .collect();
365
366 Ok(rows)
367 }
368}
369
370#[cfg(test)]
371mod tests {
372 use super::{Mem, SurrealSearchFilter, SurrealVectorStore};
373 use rig_core::{
374 client::Nothing,
375 embeddings::{Embedding, EmbeddingError, EmbeddingModel},
376 vector_store::{VectorStoreIndexDyn, request::Filter},
377 };
378 use serde_json::json;
379 use surrealdb::Surreal;
380
381 #[derive(Clone)]
382 struct MockEmbeddingModel;
383
384 impl EmbeddingModel for MockEmbeddingModel {
385 const MAX_DOCUMENTS: usize = 4;
386
387 type Client = Nothing;
388
389 fn make(_: &Self::Client, _: impl Into<String>, _: Option<usize>) -> Self {
390 Self
391 }
392
393 fn ndims(&self) -> usize {
394 3
395 }
396
397 async fn embed_texts(
398 &self,
399 texts: impl IntoIterator<Item = String> + Send,
400 ) -> Result<Vec<Embedding>, EmbeddingError> {
401 Ok(texts
402 .into_iter()
403 .map(|text| Embedding {
404 document: text,
405 vec: vec![0.0, 0.0, 0.0],
406 })
407 .collect())
408 }
409 }
410
411 #[allow(clippy::panic)]
412 #[test]
413 fn filter_from_json_preserves_nested_values() {
414 let filter = match SurrealSearchFilter::try_from(Filter::Eq(
415 "metadata".to_string(),
416 json!({
417 "name": "rig",
418 "flags": { "native": true },
419 "tags": ["surreal", "json"]
420 }),
421 )) {
422 Ok(filter) => filter,
423 Err(err) => panic!("unexpected surreal filter conversion failure: {err}"),
424 };
425
426 let sql = filter.to_string();
427
428 assert!(sql.starts_with("metadata = {"));
429 assert!(sql.contains("name: 'rig'"));
430 assert!(sql.contains("flags: { native: true }"));
431 assert!(sql.contains("tags: ['surreal', 'json']"));
432 }
433
434 #[allow(clippy::panic)]
435 #[tokio::test]
436 async fn surreal_vector_store_supports_type_erased_queries() {
437 fn assert_dyn<T: VectorStoreIndexDyn + Send + Sync + 'static>(_: T) {}
438
439 let surreal = match Surreal::new::<Mem>(()).await {
440 Ok(surreal) => surreal,
441 Err(err) => panic!("failed to create in-memory surreal client: {err}"),
442 };
443 let vector_store = SurrealVectorStore::with_defaults(MockEmbeddingModel, surreal);
444
445 assert_dyn(vector_store);
446 }
447}