1use crate::encoding::{named_node_id, rdf_type_id, DEFAULT_GRAPH_ID};
17use crate::error::{Error, Result};
18use crate::registry::NS;
19use crate::sql::{sql_str, Capabilities, Statement};
20use oxrdf::vocab::{rdf, xsd};
21use oxrdf::{GraphName, Literal, NamedNode, Quad, Term};
22use std::collections::BTreeMap;
23
24pub const VECTORS_GRAPH: &str = "oxilite:vectors";
26
27pub const INDEX_PREFIX: &str = "oxilite:vector/";
30
31const META_PREFIX: &str = "vector:";
33
34const GUARD_TABLE: &str = "CREATE TABLE IF NOT EXISTS oxilite_vector_guard (\
37 vector_dimensions_mismatch TEXT CHECK (vector_dimensions_mismatch IS NULL))";
38
39pub const MAX_K: u64 = 10_000;
41
42pub const DEFAULT_K: u64 = 10;
44
45pub mod vocab {
47 pub const VECTOR_INDEX: &str = "https://oxilite.dev/ns#VectorIndex";
48 pub const INDEX_NAME: &str = "https://oxilite.dev/ns#indexName";
49 pub const PROPERTY: &str = "https://oxilite.dev/ns#property";
50 pub const DIMENSIONS: &str = "https://oxilite.dev/ns#dimensions";
51 pub const METRIC: &str = "https://oxilite.dev/ns#metric";
52 pub const ELEMENT_TYPE: &str = "https://oxilite.dev/ns#elementType";
53 pub const CLASS: &str = "https://oxilite.dev/ns#class";
54 pub const QUERY: &str = "https://oxilite.dev/ns#query";
56 pub const K: &str = "https://oxilite.dev/ns#k";
57 pub const NODE: &str = "https://oxilite.dev/ns#node";
58 pub const DISTANCE: &str = "https://oxilite.dev/ns#distance";
59 pub const SCORE: &str = "https://oxilite.dev/ns#score";
60}
61
62#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Default)]
64pub enum Metric {
65 #[default]
66 Cosine,
67 Euclidean,
68 DotProduct,
69 Jaccard,
71}
72
73impl Metric {
74 pub const ALL: [Self; 4] = [
75 Self::Cosine,
76 Self::Euclidean,
77 Self::DotProduct,
78 Self::Jaccard,
79 ];
80
81 pub fn name(self) -> &'static str {
83 match self {
84 Self::Cosine => "cosine",
85 Self::Euclidean => "euclidean",
86 Self::DotProduct => "dot",
87 Self::Jaccard => "jaccard",
88 }
89 }
90
91 pub fn iri(self) -> String {
92 format!(
93 "{NS}{}",
94 match self {
95 Self::Cosine => "Cosine",
96 Self::Euclidean => "Euclidean",
97 Self::DotProduct => "DotProduct",
98 Self::Jaccard => "Jaccard",
99 }
100 )
101 }
102
103 pub fn parse(s: &str) -> Option<Self> {
105 Some(match s.to_ascii_lowercase().as_str() {
106 "cosine" | "cos" => Self::Cosine,
107 "euclidean" | "l2" => Self::Euclidean,
108 "dot" | "dot_product" | "dotproduct" => Self::DotProduct,
109 "jaccard" => Self::Jaccard,
110 _ => return None,
111 })
112 }
113
114 pub fn from_iri(iri: &str) -> Option<Self> {
115 Self::ALL.into_iter().find(|m| m.iri() == iri)
116 }
117
118 pub fn distance_fn(self) -> &'static str {
120 match self {
121 Self::Cosine => "vector_distance_cos",
122 Self::Euclidean => "vector_distance_l2",
123 Self::DotProduct => "vector_distance_dot",
124 Self::Jaccard => "vector_distance_jaccard",
125 }
126 }
127
128 pub fn score_sql(self, d: &str) -> String {
130 match self {
131 Self::Cosine => format!("(1.0 - ({d}) / 2.0)"),
132 Self::Euclidean => format!("(1.0 / (1.0 + ({d}) * ({d})))"),
133 Self::DotProduct => format!("(-({d}))"),
135 Self::Jaccard => format!("(1.0 - ({d}))"),
136 }
137 }
138
139 pub fn score(self, d: f64) -> f64 {
141 match self {
142 Self::Cosine => 1.0 - d / 2.0,
143 Self::Euclidean => 1.0 / (1.0 + d * d),
144 Self::DotProduct => -d,
145 Self::Jaccard => 1.0 - d,
146 }
147 }
148}
149
150#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Default)]
152pub enum ElementType {
153 #[default]
154 Float32,
155 Float64,
156 Int8,
158 Bit1,
160 SparseFloat32,
162}
163
164impl ElementType {
165 pub const ALL: [Self; 5] = [
166 Self::Float32,
167 Self::Float64,
168 Self::Int8,
169 Self::Bit1,
170 Self::SparseFloat32,
171 ];
172
173 pub fn name(self) -> &'static str {
174 match self {
175 Self::Float32 => "float32",
176 Self::Float64 => "float64",
177 Self::Int8 => "int8",
178 Self::Bit1 => "bit1",
179 Self::SparseFloat32 => "sparse",
180 }
181 }
182
183 pub fn iri(self) -> String {
184 format!(
185 "{NS}{}",
186 match self {
187 Self::Float32 => "Float32",
188 Self::Float64 => "Float64",
189 Self::Int8 => "Int8",
190 Self::Bit1 => "Bit1",
191 Self::SparseFloat32 => "SparseFloat32",
192 }
193 )
194 }
195
196 pub fn parse(s: &str) -> Option<Self> {
197 Some(match s.to_ascii_lowercase().as_str() {
198 "float32" | "f32" => Self::Float32,
199 "float64" | "f64" => Self::Float64,
200 "int8" | "i8" | "float8" => Self::Int8,
201 "bit1" | "1bit" | "binary" => Self::Bit1,
202 "sparse" | "sparse_float32" | "sparsefloat32" => Self::SparseFloat32,
203 _ => return None,
204 })
205 }
206
207 pub fn from_iri(iri: &str) -> Option<Self> {
208 Self::ALL.into_iter().find(|t| t.iri() == iri)
209 }
210
211 pub fn convert_fn(self) -> &'static str {
213 match self {
214 Self::Float32 => "vector32",
215 Self::Float64 => "vector64",
216 Self::Int8 => "vector8",
217 Self::Bit1 => "vector1bit",
218 Self::SparseFloat32 => "vector32_sparse",
219 }
220 }
221}
222
223#[derive(Debug, Clone, PartialEq, Eq)]
225pub struct VectorIndex {
226 pub name: String,
227 pub property: NamedNode,
229 pub dimensions: u32,
230 pub metric: Metric,
231 pub element_type: ElementType,
232 pub class: Option<NamedNode>,
234}
235
236impl VectorIndex {
237 pub fn new(name: impl Into<String>, property: NamedNode, dimensions: u32) -> Self {
239 Self {
240 name: name.into(),
241 property,
242 dimensions,
243 metric: Metric::Cosine,
244 element_type: ElementType::Float32,
245 class: None,
246 }
247 }
248
249 pub fn metric(mut self, metric: Metric) -> Self {
250 self.metric = metric;
251 self
252 }
253
254 pub fn element_type(mut self, element_type: ElementType) -> Self {
255 self.element_type = element_type;
256 self
257 }
258
259 pub fn class(mut self, class: NamedNode) -> Self {
260 self.class = Some(class);
261 self
262 }
263
264 pub fn validate(&self) -> Result<()> {
266 let mut chars = self.name.chars();
267 let ok = self.name.len() <= 64
268 && chars.next().is_some_and(|c| c.is_ascii_alphabetic())
269 && chars.all(|c| c.is_ascii_alphanumeric() || c == '_');
270 if !ok {
271 return Err(Error::Other(format!(
272 "vector index name {:?} must match [A-Za-z][A-Za-z0-9_]{{0,63}}",
273 self.name
274 )));
275 }
276 if self.dimensions == 0 || self.dimensions > 65_536 {
277 return Err(Error::Other(format!(
278 "vector index {}: dimensions must be between 1 and 65536, not {}",
279 self.name, self.dimensions
280 )));
281 }
282 let sparse = self.element_type == ElementType::SparseFloat32;
283 if (self.metric == Metric::Jaccard) != sparse {
284 return Err(Error::Other(format!(
285 "vector index {}: the Jaccard metric goes with sparse vectors, and sparse vectors with Jaccard",
286 self.name
287 )));
288 }
289 Ok(())
290 }
291
292 pub fn iri(&self) -> NamedNode {
294 index_iri(&self.name)
295 }
296
297 pub fn table(&self) -> String {
299 table_name(&self.name)
300 }
301
302 pub fn fingerprint(&self) -> String {
304 format!(
305 "{}|{}|{}|{}|{}",
306 self.property.as_str(),
307 self.dimensions,
308 self.metric.name(),
309 self.element_type.name(),
310 self.class.as_ref().map_or("", NamedNode::as_str)
311 )
312 }
313
314 pub fn to_quads(&self) -> Vec<Quad> {
316 let g = GraphName::NamedNode(NamedNode::new_unchecked(VECTORS_GRAPH));
317 let s = self.iri();
318 let n = |i: &str| NamedNode::new_unchecked(i);
319 let mut out = vec![
320 Quad::new(s.clone(), rdf::TYPE, n(vocab::VECTOR_INDEX), g.clone()),
321 Quad::new(
322 s.clone(),
323 n(vocab::INDEX_NAME),
324 Literal::new_simple_literal(&self.name),
325 g.clone(),
326 ),
327 Quad::new(
328 s.clone(),
329 n(vocab::PROPERTY),
330 self.property.clone(),
331 g.clone(),
332 ),
333 Quad::new(
334 s.clone(),
335 n(vocab::DIMENSIONS),
336 Literal::from(i64::from(self.dimensions)),
337 g.clone(),
338 ),
339 Quad::new(
340 s.clone(),
341 n(vocab::METRIC),
342 n(&self.metric.iri()),
343 g.clone(),
344 ),
345 Quad::new(
346 s.clone(),
347 n(vocab::ELEMENT_TYPE),
348 n(&self.element_type.iri()),
349 g.clone(),
350 ),
351 ];
352 if let Some(c) = &self.class {
353 out.push(Quad::new(s, n(vocab::CLASS), c.clone(), g));
354 }
355 out
356 }
357
358 pub fn create_statements(&self, caps: &Capabilities) -> Result<Vec<Statement>> {
362 require(caps)?;
363 self.validate()?;
364 let t = self.table();
365 let pid = named_node_id(self.property.as_str());
366 let dims = self.dimensions;
367 let conv = self.element_type.convert_fn();
368 let msg = sql_str(&format!(
369 "oxilite: vector index {} expects {dims} dimensions",
370 self.name
371 ));
372 let check = |o: &str| {
373 format!(
374 "COALESCE(json_array_length((SELECT lex FROM terms WHERE id = {o})), -1) <> {dims}"
375 )
376 };
377 let mut s = vec![
378 Statement::new(format!(
379 "CREATE TABLE IF NOT EXISTS {t} (s INTEGER NOT NULL, o INTEGER NOT NULL, g INTEGER NOT NULL, \
380 e BLOB NOT NULL, PRIMARY KEY (s, o, g))"
381 )),
382 Statement::new(format!(
383 "CREATE TRIGGER IF NOT EXISTS {t}_ins AFTER INSERT ON quads WHEN NEW.p = {pid} BEGIN \
384 SELECT RAISE(ABORT, {msg}) WHERE {}; \
385 INSERT OR IGNORE INTO {t}(s, o, g, e) SELECT NEW.s, NEW.o, NEW.g, {conv}(lex) FROM terms WHERE id = NEW.o; \
386 END",
387 check("NEW.o")
388 )),
389 Statement::new(format!(
390 "CREATE TRIGGER IF NOT EXISTS {t}_del AFTER DELETE ON quads WHEN OLD.p = {pid} BEGIN \
391 DELETE FROM {t} WHERE s = OLD.s AND o = OLD.o AND g = OLD.g; END"
392 )),
393 Statement::new(GUARD_TABLE),
396 Statement::new(format!(
397 "INSERT INTO oxilite_vector_guard(vector_dimensions_mismatch) \
398 SELECT {} FROM quads q WHERE q.p = {pid} AND {} LIMIT 1",
399 sql_str(&self.name),
400 check("q.o")
401 )),
402 Statement::new(format!(
403 "INSERT OR IGNORE INTO {t}(s, o, g, e) SELECT q.s, q.o, q.g, {conv}(t.lex) \
404 FROM quads q JOIN terms t ON t.id = q.o WHERE q.p = {pid}"
405 )),
406 ];
407 if self.element_type == ElementType::SparseFloat32 && caps.vector_index_methods {
408 s.push(Statement::new(format!(
409 "CREATE INDEX IF NOT EXISTS {t}_ivf ON {t} USING toy_vector_sparse_ivf (e)"
410 )));
411 }
412 s.push(Statement::new(format!(
413 "INSERT OR REPLACE INTO oxilite_meta(key, value) VALUES ({}, {})",
414 sql_str(&format!("{META_PREFIX}{}", self.name.to_lowercase())),
415 sql_str(&self.fingerprint())
416 )));
417 Ok(s)
418 }
419
420 pub fn knn_sql(&self, query: &QueryVector, k: u64) -> Result<String> {
422 knn_sql(self, query, k)
423 }
424}
425
426pub fn index_iri(name: &str) -> NamedNode {
428 NamedNode::new_unchecked(format!("{INDEX_PREFIX}{name}"))
429}
430
431fn table_name(name: &str) -> String {
432 format!("vec_{}", name.to_lowercase())
433}
434
435fn require(caps: &Capabilities) -> Result<()> {
436 if caps.vectors {
437 Ok(())
438 } else {
439 Err(Error::unsupported(format!(
440 "vector indexes need a backend with vector functions, such as Turso (oxilite-turso); {} has none",
441 caps.name
442 )))
443 }
444}
445
446pub fn drop_statements(name: &str) -> Vec<Statement> {
448 let t = table_name(name);
449 vec![
450 Statement::new(format!("DROP TRIGGER IF EXISTS {t}_ins")),
451 Statement::new(format!("DROP TRIGGER IF EXISTS {t}_del")),
452 Statement::new(format!("DROP INDEX IF EXISTS {t}_ivf")),
453 Statement::new(format!("DROP TABLE IF EXISTS {t}")),
454 Statement::new(format!(
455 "DELETE FROM oxilite_meta WHERE key = {}",
456 sql_str(&format!("{META_PREFIX}{}", name.to_lowercase()))
457 )),
458 ]
459}
460
461#[derive(Debug, Clone, PartialEq)]
463pub enum QueryVector {
464 Vector(String),
466 Node(Term),
468}
469
470impl QueryVector {
471 pub fn vector(values: &[f64]) -> Self {
473 Self::Vector(format!(
474 "[{}]",
475 values
476 .iter()
477 .map(|v| v.to_string())
478 .collect::<Vec<_>>()
479 .join(",")
480 ))
481 }
482
483 pub fn node(node: impl Into<Term>) -> Self {
485 Self::Node(node.into())
486 }
487}
488
489pub fn parse_vector(text: &str) -> Option<Vec<f64>> {
491 let inner = text.trim().strip_prefix('[')?.strip_suffix(']')?.trim();
492 if inner.is_empty() {
493 return Some(Vec::new());
494 }
495 inner
496 .split(',')
497 .map(|x| x.trim().parse::<f64>().ok().filter(|v| v.is_finite()))
498 .collect()
499}
500
501pub fn knn_sql(index: &VectorIndex, query: &QueryVector, k: u64) -> Result<String> {
504 if k == 0 || k > MAX_K {
505 return Err(Error::Other(format!(
506 "vector search: k must be between 1 and {MAX_K}, not {k}"
507 )));
508 }
509 let t = index.table();
510 let q = match query {
511 QueryVector::Vector(text) => {
512 let v = parse_vector(text).ok_or_else(|| {
513 Error::Other(format!(
514 "vector search on {}: the query {text:?} is not a JSON array of numbers",
515 index.name
516 ))
517 })?;
518 if v.len() != index.dimensions as usize {
519 return Err(Error::Other(format!(
520 "vector search on {}: the query has {} dimensions, the index {}",
521 index.name,
522 v.len(),
523 index.dimensions
524 )));
525 }
526 format!("{}({})", index.element_type.convert_fn(), sql_str(text))
527 }
528 QueryVector::Node(node) => format!(
529 "(SELECT e FROM {t} WHERE s = {} LIMIT 1)",
530 crate::encoding::term_id(node.as_ref())
531 ),
532 };
533 let dist = index.metric.distance_fn();
534 let inner = match &index.class {
535 None if index.element_type == ElementType::SparseFloat32 => {
536 format!("SELECT s, {dist}(e, {q}) AS d FROM {t} ORDER BY d LIMIT {k}")
538 }
539 None => format!("SELECT v.s AS s, {dist}(v.e, {q}) AS d FROM {t} v"),
540 Some(class) => format!(
541 "SELECT v.s AS s, {dist}(v.e, {q}) AS d FROM {t} v WHERE EXISTS (SELECT 1 FROM quads c \
542 WHERE c.s = v.s AND c.p = {} AND c.o = {})",
543 rdf_type_id(),
544 named_node_id(class.as_str())
545 ),
546 };
547 Ok(format!(
548 "SELECT s, MIN(d) AS d FROM ({inner}) GROUP BY s ORDER BY d, s LIMIT {k}"
549 ))
550}
551
552pub fn knn_ranked_sql(index: &VectorIndex, query: &QueryVector, k: u64) -> Result<String> {
555 let knn = knn_sql(index, query, k)?;
556 Ok(format!(
557 "WITH knn AS ({knn}) SELECT knn.s AS s, knn.d AS d, \
558 (SELECT COUNT(*) FROM knn k2 WHERE k2.d < knn.d OR (k2.d = knn.d AND k2.s < knn.s)) + 1 AS r FROM knn"
559 ))
560}
561
562pub fn definitions_statement(id_col: impl Fn(&str) -> String) -> Statement {
565 Statement::new(format!(
566 "SELECT {}, {}, {}, ts.lex, tp.lex, t.lex, t.dt, t.lang, t.dir FROM quads q \
567 LEFT JOIN terms ts ON ts.id = q.s LEFT JOIN terms tp ON tp.id = q.p LEFT JOIN terms t ON t.id = q.o \
568 WHERE q.g = {}",
569 id_col("q.s"),
570 id_col("q.p"),
571 id_col("q.o"),
572 named_node_id(VECTORS_GRAPH)
573 ))
574}
575
576pub fn definitions_from_rows(
579 rows: &[Vec<crate::sql::SqlValue>],
580) -> (Vec<VectorIndex>, Vec<String>) {
581 let mut by_subject: BTreeMap<String, Vec<(String, Term)>> = BTreeMap::new();
582 for row in rows {
583 let get = |i: usize| row.get(i).cloned().unwrap_or(crate::sql::SqlValue::Null);
584 let (Some(_s), Some(_p), Some(o)) = (get(0).as_i64(), get(1).as_i64(), get(2).as_i64())
585 else {
586 continue;
587 };
588 let (Some(s), Some(p)) = (get(3).into_string(), get(4).into_string()) else {
589 continue;
590 };
591 let term = match crate::encoding::decode_inline(o) {
592 Some(t) => t,
593 None => {
594 let Some(lex) = get(5).into_string() else {
595 continue;
596 };
597 match crate::encoding::decode_row(
598 o,
599 lex,
600 get(6).into_string(),
601 get(7).into_string(),
602 get(8).as_i64(),
603 ) {
604 Ok(t) => t,
605 Err(_) => continue,
606 }
607 }
608 };
609 by_subject.entry(s).or_default().push((p, term));
610 }
611 definitions_from_triples(by_subject)
612}
613
614pub fn definitions_from_quads(quads: &[Quad]) -> (Vec<VectorIndex>, Vec<String>) {
616 let mut by_subject: BTreeMap<String, Vec<(String, Term)>> = BTreeMap::new();
617 for q in quads {
618 if !matches!(&q.graph_name, GraphName::NamedNode(g) if g.as_str() == VECTORS_GRAPH) {
619 continue;
620 }
621 if let oxrdf::NamedOrBlankNode::NamedNode(s) = &q.subject {
622 by_subject
623 .entry(s.as_str().to_owned())
624 .or_default()
625 .push((q.predicate.as_str().to_owned(), q.object.clone()));
626 }
627 }
628 definitions_from_triples(by_subject)
629}
630
631fn definitions_from_triples(
632 by_subject: BTreeMap<String, Vec<(String, Term)>>,
633) -> (Vec<VectorIndex>, Vec<String>) {
634 let mut defs: Vec<VectorIndex> = Vec::new();
635 let mut problems = Vec::new();
636 for (s, props) in by_subject {
637 let is_index = props.iter().any(|(p, o)| {
638 p == rdf::TYPE.as_str()
639 && matches!(o, Term::NamedNode(n) if n.as_str() == vocab::VECTOR_INDEX)
640 });
641 if !is_index {
642 continue;
643 }
644 match definition(&s, &props) {
645 Ok(d) => {
646 if let Some(other) = defs.iter().find(|x| x.table() == d.table()) {
647 problems.push(format!(
648 "<{s}>: index name {} clashes with {} (names are compared ignoring case)",
649 d.name, other.name
650 ));
651 } else {
652 defs.push(d);
653 }
654 }
655 Err(e) => problems.push(format!("<{s}>: {e}")),
656 }
657 }
658 (defs, problems)
659}
660
661fn definition(s: &str, props: &[(String, Term)]) -> std::result::Result<VectorIndex, String> {
662 let one = |p: &str| -> std::result::Result<Option<&Term>, String> {
663 let mut it = props.iter().filter(|(k, _)| k == p).map(|(_, v)| v);
664 let first = it.next();
665 if it.next().is_some() {
666 return Err(format!("more than one {}", crate::functions::local_name(p)));
667 }
668 Ok(first)
669 };
670 let from_iri = s
671 .strip_prefix(INDEX_PREFIX)
672 .ok_or_else(|| format!("an index must be named <{INDEX_PREFIX}NAME>"))?;
673 let name = match one(vocab::INDEX_NAME)? {
674 Some(Term::Literal(l)) => l.value().to_owned(),
675 Some(_) => return Err("indexName must be a string".into()),
676 None => from_iri.to_owned(),
677 };
678 if name != from_iri {
679 return Err(format!(
680 "indexName {name:?} differs from the name in the IRI ({from_iri:?})"
681 ));
682 }
683 let property = match one(vocab::PROPERTY)? {
684 Some(Term::NamedNode(n)) => n.clone(),
685 Some(_) => return Err("property must be an IRI".into()),
686 None => return Err("property is missing".into()),
687 };
688 let dimensions = match one(vocab::DIMENSIONS)? {
689 Some(Term::Literal(l))
690 if crate::encoding::numeric_rank(l.datatype().as_str()) == Some(1)
691 || l.datatype() == xsd::INTEGER =>
692 {
693 l.value()
694 .parse::<u32>()
695 .map_err(|_| format!("dimensions {} is not a positive integer", l.value()))?
696 }
697 Some(_) => return Err("dimensions must be an integer".into()),
698 None => return Err("dimensions is missing".into()),
699 };
700 let metric = match one(vocab::METRIC)? {
701 Some(Term::NamedNode(n)) => {
702 Metric::from_iri(n.as_str()).ok_or_else(|| format!("unknown metric <{n}>"))?
703 }
704 Some(_) => return Err("metric must be an IRI".into()),
705 None => Metric::Cosine,
706 };
707 let element_type = match one(vocab::ELEMENT_TYPE)? {
708 Some(Term::NamedNode(n)) => ElementType::from_iri(n.as_str())
709 .ok_or_else(|| format!("unknown element type <{n}>"))?,
710 Some(_) => return Err("elementType must be an IRI".into()),
711 None if metric == Metric::Jaccard => ElementType::SparseFloat32,
712 None => ElementType::Float32,
713 };
714 let class = match one(vocab::CLASS)? {
715 Some(Term::NamedNode(n)) => Some(n.clone()),
716 Some(_) => return Err("class must be an IRI".into()),
717 None => None,
718 };
719 let d = VectorIndex {
720 name,
721 property,
722 dimensions,
723 metric,
724 element_type,
725 class,
726 };
727 d.validate().map_err(|e| e.to_string())?;
728 Ok(d)
729}
730
731pub fn built_from_meta<'a>(
733 rows: impl IntoIterator<Item = (&'a str, &'a str)>,
734) -> BTreeMap<String, String> {
735 rows.into_iter()
736 .filter_map(|(k, v)| Some((k.strip_prefix(META_PREFIX)?.to_owned(), v.to_owned())))
737 .collect()
738}
739
740pub fn is_built(index: &VectorIndex, built: &BTreeMap<String, String>) -> bool {
742 built.get(&index.name.to_lowercase()) == Some(&index.fingerprint())
743}
744
745pub fn sync_statements(
748 defs: &[VectorIndex],
749 built: &BTreeMap<String, String>,
750 caps: &Capabilities,
751) -> Result<Vec<Statement>> {
752 let mut s = Vec::new();
753 for name in built.keys() {
754 if !defs.iter().any(|d| d.name.to_lowercase() == *name) {
755 s.extend(drop_statements(name));
756 }
757 }
758 for d in defs {
759 if is_built(d, built) {
760 continue;
761 }
762 if built.contains_key(&d.name.to_lowercase()) {
763 s.extend(drop_statements(&d.name));
764 }
765 s.extend(d.create_statements(caps)?);
766 }
767 Ok(s)
768}
769
770pub fn update_touches_vectors(update: &spargebra::Update) -> bool {
775 use spargebra::term::{GraphName as G, GraphNamePattern, NamedNodePattern, TermPattern};
776 use spargebra::GraphUpdateOperation as Op;
777 let definition_triple = |p: &NamedNodePattern, o: &TermPattern| match p {
778 NamedNodePattern::Variable(_) => true,
779 NamedNodePattern::NamedNode(n) if n.as_ref() == rdf::TYPE => match o {
780 TermPattern::NamedNode(c) => c.as_str().starts_with(NS),
781 TermPattern::Variable(_) => true,
782 _ => false,
783 },
784 NamedNodePattern::NamedNode(n) => n.as_str().starts_with(NS),
785 };
786 let counts = |g: &GraphNamePattern, p: &NamedNodePattern, o: &TermPattern| match g {
787 GraphNamePattern::NamedNode(n) => n.as_str() == VECTORS_GRAPH,
788 GraphNamePattern::DefaultGraph => false,
789 GraphNamePattern::Variable(_) => definition_triple(p, o),
790 };
791 let data = |g: &G| matches!(g, G::NamedNode(n) if n.as_str() == VECTORS_GRAPH);
792 update.operations.iter().any(|op| match op {
793 Op::InsertData { data: d } => d.iter().any(|q| data(&q.graph_name)),
794 Op::DeleteData { data: d } => d.iter().any(|q| {
795 matches!(&q.graph_name, spargebra::term::GraphName::NamedNode(n) if n.as_str() == VECTORS_GRAPH)
796 }),
797 Op::DeleteInsert { delete, insert, .. } => {
798 delete.iter().any(|q| {
799 let o: TermPattern = q.object.clone().into();
800 counts(&q.graph_name, &q.predicate, &o)
801 }) || insert
802 .iter()
803 .any(|q| counts(&q.graph_name, &q.predicate, &q.object))
804 }
805 Op::Create { .. } => false,
806 Op::Load { .. } | Op::Clear { .. } | Op::Drop { .. } => true,
807 })
808}
809
810pub fn service_index(iri: &str) -> Option<&str> {
812 iri.strip_prefix(INDEX_PREFIX)
813}
814
815pub fn find<'a>(defs: &'a [VectorIndex], name: &str) -> Option<&'a VectorIndex> {
817 defs.iter()
818 .find(|d| d.name == name)
819 .or_else(|| defs.iter().find(|d| d.name.eq_ignore_ascii_case(name)))
820}
821
822pub const DEFAULT_GRAPH: i64 = DEFAULT_GRAPH_ID;
824
825#[cfg(test)]
826mod tests {
827 use super::*;
828
829 fn ex(l: &str) -> NamedNode {
830 NamedNode::new_unchecked(format!("http://example.com/{l}"))
831 }
832
833 #[test]
834 fn definitions_round_trip_through_rdf() {
835 let d = VectorIndex::new("Docs", ex("embedding"), 3)
836 .metric(Metric::Euclidean)
837 .element_type(ElementType::Float64)
838 .class(ex("Doc"));
839 let (defs, problems) = definitions_from_quads(&d.to_quads());
840 assert!(problems.is_empty(), "{problems:?}");
841 assert_eq!(defs, vec![d]);
842 }
843
844 #[test]
845 fn invalid_definitions() {
846 assert!(VectorIndex::new("1x", ex("e"), 3).validate().is_err());
847 assert!(VectorIndex::new("x", ex("e"), 0).validate().is_err());
848 assert!(VectorIndex::new("x", ex("e"), 3)
849 .metric(Metric::Jaccard)
850 .validate()
851 .is_err());
852 let mut quads = VectorIndex::new("x", ex("e"), 3).to_quads();
853 quads.retain(|q| q.predicate.as_str() != vocab::DIMENSIONS);
854 let (defs, problems) = definitions_from_quads(&quads);
855 assert!(defs.is_empty());
856 assert!(
857 problems[0].contains("dimensions is missing"),
858 "{problems:?}"
859 );
860 }
861
862 #[test]
863 fn knn_checks_the_query() {
864 let d = VectorIndex::new("x", ex("e"), 3);
865 assert!(knn_sql(&d, &QueryVector::vector(&[1.0, 2.0]), 5)
866 .unwrap_err()
867 .to_string()
868 .contains("2 dimensions"));
869 assert!(knn_sql(&d, &QueryVector::Vector("nope".into()), 5).is_err());
870 assert!(knn_sql(&d, &QueryVector::vector(&[1.0, 2.0, 3.0]), 0).is_err());
871 let sql = knn_sql(&d, &QueryVector::vector(&[1.0, 2.0, 3.0]), 5).unwrap();
872 assert!(
873 sql.contains("vector_distance_cos(v.e, vector32('[1,2,3]'))"),
874 "{sql}"
875 );
876 }
877
878 #[test]
879 fn sync_plans() {
880 let caps = Capabilities {
881 vectors: true,
882 ..Capabilities::native()
883 };
884 let d = VectorIndex::new("x", ex("e"), 3);
885 let mut built = BTreeMap::new();
886 assert!(!sync_statements(std::slice::from_ref(&d), &built, &caps)
887 .unwrap()
888 .is_empty());
889 built.insert("x".to_owned(), d.fingerprint());
890 assert!(sync_statements(std::slice::from_ref(&d), &built, &caps)
891 .unwrap()
892 .is_empty());
893 let drop = sync_statements(&[], &built, &caps).unwrap();
894 assert!(drop.iter().any(|s| s.sql == "DROP TABLE IF EXISTS vec_x"));
895 assert!(d.create_statements(&Capabilities::native()).is_err());
896 }
897}