1use rig_core::{
11 Embed,
12 embeddings::{Embedding, EmbeddingModel},
13 vector_store::{
14 InsertDocuments, VectorStoreError, VectorStoreIndex,
15 request::{
16 DynamicSearchFilter, Filter, FilterError, SearchFilter, SqlCondition,
17 VectorSearchRequest,
18 },
19 },
20};
21use scylla::{
22 client::{Compression, session::Session, session_builder::SessionBuilder},
23 statement::prepared::PreparedStatement,
24 value::CqlValue,
25};
26use serde::{Deserialize, Serialize};
27use std::{
28 collections::HashMap,
29 hash::{DefaultHasher, Hash, Hasher},
30 sync::{Arc, RwLock},
31};
32use uuid::Uuid;
33
34pub struct ScyllaDbVectorStore<M: EmbeddingModel> {
39 model: M,
41 pub session: Arc<Session>,
43 keyspace: String,
45 table: String,
46 dimensions: usize,
48 insert_stmt: PreparedStatement,
50 search_stmt: PreparedStatement,
51 get_by_id_stmt: PreparedStatement,
52 cache: Arc<RwLock<HashMap<u64, PreparedStatement>>>,
54}
55
56fn cql_value_from_json(value: serde_json::Value) -> Result<CqlValue, FilterError> {
58 use scylla::value::CqlVarint;
59 use serde_json::Value;
60
61 match value {
62 Value::Bool(b) => Ok(CqlValue::Boolean(b)),
63 Value::Number(n) => {
64 if let Some(i) = n.as_i64() {
65 Ok(CqlValue::BigInt(i))
66 } else if let Some(u) = n.as_u64() {
67 let mut bytes = vec![0u8];
70 bytes.extend_from_slice(&u.to_be_bytes());
71 Ok(CqlValue::Varint(CqlVarint::from_signed_bytes_be(bytes)))
72 } else if let Some(f) = n.as_f64() {
73 Ok(CqlValue::Double(f))
74 } else {
75 Err(FilterError::Expected {
76 expected: "Valid number".into(),
77 got: "Invalid number".into(),
78 })
79 }
80 }
81 Value::String(s) => Ok(CqlValue::Text(s)),
82 Value::Array(arr) => Ok(CqlValue::List(
83 arr.into_iter()
84 .map(cql_value_from_json)
85 .collect::<Result<_, _>>()?,
86 )),
87 Value::Object(map) => {
88 let pairs = map
89 .into_iter()
90 .map(|(k, v)| Ok((CqlValue::Text(k), cql_value_from_json(v)?)))
91 .collect::<Result<Vec<_>, FilterError>>()?;
92 Ok(CqlValue::Map(pairs))
93 }
94 Value::Null => Ok(CqlValue::Empty),
95 }
96}
97
98const PLACEHOLDER: &str = "?";
100
101#[derive(Clone, Debug)]
103pub struct ScyllaSearchFilter(SqlCondition<CqlValue>);
104
105impl std::hash::Hash for ScyllaSearchFilter {
108 fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
109 self.0.condition().hash(state)
110 }
111}
112
113impl SearchFilter for ScyllaSearchFilter {
114 type Value = CqlValue;
115
116 fn eq(key: impl AsRef<str>, value: Self::Value) -> Self {
117 Self(SqlCondition::binary(key, "=", PLACEHOLDER, value))
118 }
119
120 fn gt(key: impl AsRef<str>, value: Self::Value) -> Self {
121 Self(SqlCondition::binary(key, ">", PLACEHOLDER, value))
122 }
123
124 fn lt(key: impl AsRef<str>, value: Self::Value) -> Self {
125 Self(SqlCondition::binary(key, "<", PLACEHOLDER, value))
126 }
127
128 fn and(self, rhs: Self) -> Self {
129 Self(self.0.and(rhs.0))
130 }
131
132 fn or(self, rhs: Self) -> Self {
133 Self(self.0.or(rhs.0))
134 }
135}
136
137impl ScyllaSearchFilter {
138 fn condition(&self) -> &str {
139 self.0.condition()
140 }
141
142 fn params(&self) -> &[CqlValue] {
143 self.0.params()
144 }
145
146 #[allow(clippy::should_implement_trait)]
147 pub fn not(self) -> Self {
148 Self(self.0.not())
149 }
150
151 pub fn gte(key: String, value: <Self as SearchFilter>::Value) -> Self {
152 Self(SqlCondition::binary(key, ">=", PLACEHOLDER, value))
153 }
154
155 pub fn lte(key: String, value: <Self as SearchFilter>::Value) -> Self {
156 Self(SqlCondition::binary(key, "<=", PLACEHOLDER, value))
157 }
158
159 pub fn ne(key: String, value: <Self as SearchFilter>::Value) -> Self {
160 Self(SqlCondition::binary(key, "!=", PLACEHOLDER, value))
161 }
162
163 pub fn member(key: String, values: Vec<<Self as SearchFilter>::Value>) -> Self {
164 Self(SqlCondition::list(key, "IN", PLACEHOLDER, values))
165 }
166}
167
168impl TryFrom<Filter<serde_json::Value>> for ScyllaSearchFilter {
169 type Error = FilterError;
170
171 fn try_from(value: Filter<serde_json::Value>) -> Result<Self, Self::Error> {
172 value.try_interpret(cql_value_from_json)
173 }
174}
175
176impl DynamicSearchFilter for ScyllaSearchFilter {
177 fn from_dynamic_filter(filter: Filter<serde_json::Value>) -> Result<Self, FilterError> {
178 Self::try_from(filter)
179 }
180}
181
182impl<M> ScyllaDbVectorStore<M>
183where
184 M: EmbeddingModel,
185{
186 pub async fn new(
195 model: M,
196 session: Session,
197 keyspace: &str,
198 table: &str,
199 dimensions: usize,
200 ) -> Result<Self, VectorStoreError> {
201 let session = Arc::new(session);
202
203 let create_keyspace_cql = format!(
205 "CREATE KEYSPACE IF NOT EXISTS {keyspace} WITH REPLICATION = {{
206 'class': 'SimpleStrategy',
207 'replication_factor': 1
208 }}"
209 );
210 session
211 .query_unpaged(create_keyspace_cql, &[])
212 .await
213 .map_err(VectorStoreError::datastore)?;
214
215 let create_table_cql = format!(
219 "CREATE TABLE IF NOT EXISTS {keyspace}.{table} (
220 id UUID PRIMARY KEY,
221 vector LIST<FLOAT>,
222 metadata TEXT,
223 created_at BIGINT
224 )"
225 );
226 session
227 .query_unpaged(create_table_cql, &[])
228 .await
229 .map_err(VectorStoreError::datastore)?;
230
231 let insert_stmt = session
233 .prepare(format!(
234 "INSERT INTO {keyspace}.{table} (id, vector, metadata, created_at) VALUES (?, ?, ?, ?)"
235 ))
236 .await
237 .map_err(VectorStoreError::datastore)?;
238
239 let search_stmt = session
240 .prepare(format!(
241 "SELECT id, vector, metadata, created_at FROM {keyspace}.{table}"
242 ))
243 .await
244 .map_err(VectorStoreError::datastore)?;
245
246 let get_by_id_stmt = session
247 .prepare(format!(
248 "SELECT id, vector, metadata, created_at FROM {keyspace}.{table} WHERE id = ?"
249 ))
250 .await
251 .map_err(VectorStoreError::datastore)?;
252
253 Ok(Self {
254 model,
255 session,
256 keyspace: keyspace.to_string(),
257 table: table.to_string(),
258 dimensions,
259 insert_stmt,
260 search_stmt,
261 get_by_id_stmt,
262 cache: Default::default(),
263 })
264 }
265
266 pub fn session(&self) -> &Arc<Session> {
268 &self.session
269 }
270
271 pub fn keyspace(&self) -> &str {
273 &self.keyspace
274 }
275
276 pub fn table(&self) -> &str {
278 &self.table
279 }
280
281 pub async fn get_by_id<T: for<'a> Deserialize<'a> + Send>(
283 &self,
284 id: &str,
285 ) -> Result<Option<T>, VectorStoreError> {
286 let uuid = Uuid::parse_str(id).map_err(VectorStoreError::datastore)?;
287
288 let result = self
289 .session
290 .execute_unpaged(&self.get_by_id_stmt, (uuid,))
291 .await
292 .map_err(VectorStoreError::datastore)?;
293
294 let rows_result = result
295 .into_rows_result()
296 .map_err(VectorStoreError::datastore)?;
297
298 if let Some(first_row) = rows_result
299 .rows::<(Uuid, Vec<f32>, String, i64)>()
300 .map_err(VectorStoreError::datastore)?
301 .next()
302 {
303 let (_, _, metadata, _) = first_row.map_err(VectorStoreError::datastore)?;
304
305 let payload: T = serde_json::from_str(&metadata)?;
306 return Ok(Some(payload));
307 }
308
309 Ok(None)
310 }
311
312 fn cosine_similarity(vec1: &[f32], vec2: &[f32]) -> f32 {
314 let dot_product: f32 = vec1.iter().zip(vec2.iter()).map(|(a, b)| a * b).sum();
315 let norm1: f32 = vec1.iter().map(|x| x * x).sum::<f32>().sqrt();
316 let norm2: f32 = vec2.iter().map(|x| x * x).sum::<f32>().sqrt();
317
318 if norm1 == 0.0 || norm2 == 0.0 {
319 0.0
320 } else {
321 dot_product / (norm1 * norm2)
322 }
323 }
324
325 async fn generate_query_vector(&self, query: &str) -> Result<Vec<f32>, VectorStoreError> {
327 let embedding = self.model.embed_text(query).await?;
328 Ok(embedding.vec.iter().map(|&x| x as f32).collect())
329 }
330
331 async fn get_filter_statement_or_default(
332 &self,
333 req: &VectorSearchRequest<ScyllaSearchFilter>,
334 ) -> Result<PreparedStatement, VectorStoreError> {
335 if let Some(filter) = req.filter() {
336 let mut hasher = DefaultHasher::new();
337 filter.hash(&mut hasher);
338 let filter_hash = hasher.finish();
339
340 let statement = if let Some(cached) = self
341 .cache
342 .read()
343 .ok()
344 .and_then(|cache| cache.get(&filter_hash).cloned())
345 {
346 cached
347 } else {
348 let query = format!(
349 "SELECT id, vector, metadata, created_at FROM {}.{} WHERE {} ALLOW FILTERING",
350 self.keyspace,
351 self.table,
352 filter.condition()
353 );
354
355 let prepared = self
356 .session
357 .prepare(query)
358 .await
359 .map_err(VectorStoreError::datastore)?;
360
361 let mut cache = self.cache.write().map_err(|e| {
362 VectorStoreError::DatastoreError(
363 format!("Error writing statement cache: {e}").into(),
364 )
365 })?;
366 cache.insert(filter_hash, prepared.clone());
367 prepared
368 };
369
370 Ok(statement)
371 } else {
372 Ok(self.search_stmt.clone())
373 }
374 }
375
376 async fn search_candidates(
379 &self,
380 req: &VectorSearchRequest<ScyllaSearchFilter>,
381 ) -> Result<Vec<(f64, String, String)>, VectorStoreError> {
382 let query_vector = self.generate_query_vector(req.query()).await?;
383
384 let statement = self.get_filter_statement_or_default(req).await?;
385 let params = req
386 .filter()
387 .as_ref()
388 .map(ScyllaSearchFilter::params)
389 .unwrap_or_default();
390
391 let results = self
393 .session
394 .execute_unpaged(&statement, params)
395 .await
396 .map_err(VectorStoreError::datastore)?;
397
398 let rows_result = results
399 .into_rows_result()
400 .map_err(VectorStoreError::datastore)?;
401
402 let mut candidates = Vec::new();
403
404 for row_result in rows_result
405 .rows::<(Uuid, Vec<f32>, String, i64)>()
406 .map_err(VectorStoreError::datastore)?
407 {
408 let (id, vector, metadata, _) = row_result.map_err(VectorStoreError::datastore)?;
409
410 let score = Self::cosine_similarity(&query_vector, &vector) as f64;
411
412 if req.threshold().is_some_and(|threshold| score < threshold) {
413 continue;
414 }
415
416 candidates.push((score, id.to_string(), metadata));
417 }
418
419 candidates.sort_by(|a, b| b.0.total_cmp(&a.0));
421 candidates.truncate(req.samples() as usize);
422
423 Ok(candidates)
424 }
425}
426
427impl<Model> InsertDocuments for ScyllaDbVectorStore<Model>
428where
429 Model: EmbeddingModel + Send + Sync,
430{
431 async fn insert_documents<Doc: Serialize + Embed + Send>(
432 &self,
433 documents: Vec<(Doc, Vec<Embedding>)>,
434 ) -> Result<(), VectorStoreError> {
435 for (document, embeddings) in documents {
436 let metadata = serde_json::to_string(&document)?;
437 let now = chrono::Utc::now().timestamp();
438
439 for embedding in embeddings.into_iter() {
440 let vector: Vec<f32> = embedding.vec.into_iter().map(|x| x as f32).collect();
441
442 if vector.len() != self.dimensions {
443 return Err(VectorStoreError::DatastoreError(
444 format!(
445 "Vector dimension mismatch: expected {}, got {}",
446 self.dimensions,
447 vector.len()
448 )
449 .into(),
450 ));
451 }
452
453 let id = Uuid::new_v4();
454
455 self.session
456 .execute_unpaged(&self.insert_stmt, (id, vector, &metadata, now))
457 .await
458 .map_err(VectorStoreError::datastore)?;
459 }
460 }
461
462 Ok(())
463 }
464}
465
466impl<M> VectorStoreIndex for ScyllaDbVectorStore<M>
467where
468 M: EmbeddingModel + std::marker::Sync + Send,
469{
470 type Filter = ScyllaSearchFilter;
471
472 async fn top_n<T: for<'a> Deserialize<'a> + Send>(
479 &self,
480 req: VectorSearchRequest<ScyllaSearchFilter>,
481 ) -> Result<Vec<(f64, String, T)>, VectorStoreError> {
482 self.search_candidates(&req)
483 .await?
484 .into_iter()
485 .map(|(score, id, metadata)| Ok((score, id, serde_json::from_str(&metadata)?)))
486 .collect()
487 }
488
489 async fn top_n_ids(
492 &self,
493 req: VectorSearchRequest<ScyllaSearchFilter>,
494 ) -> Result<Vec<(f64, String)>, VectorStoreError> {
495 Ok(self
496 .search_candidates(&req)
497 .await?
498 .into_iter()
499 .map(|(score, id, _)| (score, id))
500 .collect())
501 }
502}
503
504pub async fn create_session(uri: &str) -> Result<Session, VectorStoreError> {
506 SessionBuilder::new()
507 .known_node(uri)
508 .compression(Some(Compression::Lz4))
509 .build()
510 .await
511 .map_err(VectorStoreError::datastore)
512}
513
514#[cfg(test)]
515mod tests {
516 use super::{CqlValue, ScyllaSearchFilter, SearchFilter};
517
518 #[test]
521 fn every_parameterised_operator_uses_question_mark_placeholders() {
522 let filter = ScyllaSearchFilter::gte("price".into(), CqlValue::BigInt(5))
523 .and(ScyllaSearchFilter::member(
524 "id".into(),
525 vec![CqlValue::BigInt(1), CqlValue::BigInt(2)],
526 ))
527 .or(ScyllaSearchFilter::ne("kind".into(), CqlValue::Text("veg".into())).not());
528
529 assert_eq!(
530 filter.condition(),
531 "((price >= ?) AND (id IN (?, ?))) OR (NOT (kind != ?))"
532 );
533 assert_eq!(filter.condition().matches('?').count(), 4);
534 assert_eq!(filter.params().len(), 4);
535 }
536}