1use async_trait::async_trait;
19use serde::Deserialize;
20use serde_json::json;
21
22use crate::{Document, FilterOp, MetadataFilter, SearchResult, VectorStore, VectorStoreError};
23
24#[derive(Debug, Clone)]
26pub struct Neo4jConfig {
27 pub uri: String,
29 pub username: String,
31 pub password: String,
33 pub database: String,
35 pub node_label: String,
37 pub index_name: String,
39 pub embedding_property: String,
41 pub content_property: String,
43 pub metadata_property: String,
45 pub id_property: String,
47}
48
49impl Neo4jConfig {
50 pub fn new(
52 uri: impl Into<String>,
53 username: impl Into<String>,
54 password: impl Into<String>,
55 index_name: impl Into<String>,
56 ) -> Self {
57 Self {
58 uri: uri.into(),
59 username: username.into(),
60 password: password.into(),
61 database: "neo4j".to_string(),
62 node_label: "Document".to_string(),
63 index_name: index_name.into(),
64 embedding_property: "embedding".to_string(),
65 content_property: "content".to_string(),
66 metadata_property: "metadata".to_string(),
67 id_property: "id".to_string(),
68 }
69 }
70
71 pub fn from_env_result() -> Result<Self, VectorStoreError> {
73 let uri = std::env::var("NEO4J_URI").map_err(|_| {
74 VectorStoreError::ConfigError("NEO4J_URI environment variable not set".to_string())
75 })?;
76 let username = std::env::var("NEO4J_USERNAME").map_err(|_| {
77 VectorStoreError::ConfigError("NEO4J_USERNAME environment variable not set".to_string())
78 })?;
79 let password = std::env::var("NEO4J_PASSWORD").map_err(|_| {
80 VectorStoreError::ConfigError("NEO4J_PASSWORD environment variable not set".to_string())
81 })?;
82 let index_name = std::env::var("NEO4J_VECTOR_INDEX_NAME").map_err(|_| {
83 VectorStoreError::ConfigError(
84 "NEO4J_VECTOR_INDEX_NAME environment variable not set".to_string(),
85 )
86 })?;
87 let database = std::env::var("NEO4J_DATABASE").unwrap_or_else(|_| "neo4j".to_string());
88 Ok(Self {
89 uri,
90 username,
91 password,
92 database,
93 index_name,
94 ..Default::default()
95 })
96 }
97
98 pub fn with_database(mut self, database: impl Into<String>) -> Self {
100 self.database = database.into();
101 self
102 }
103
104 pub fn with_node_label(mut self, label: impl Into<String>) -> Self {
106 self.node_label = label.into();
107 self
108 }
109
110 pub fn with_embedding_property(mut self, prop: impl Into<String>) -> Self {
112 self.embedding_property = prop.into();
113 self
114 }
115
116 pub fn with_content_property(mut self, prop: impl Into<String>) -> Self {
118 self.content_property = prop.into();
119 self
120 }
121}
122
123impl Default for Neo4jConfig {
124 fn default() -> Self {
125 Self {
126 uri: "bolt://localhost:7687".to_string(),
127 username: "neo4j".to_string(),
128 password: String::new(),
129 database: "neo4j".to_string(),
130 node_label: "Document".to_string(),
131 index_name: "vector_index".to_string(),
132 embedding_property: "embedding".to_string(),
133 content_property: "content".to_string(),
134 metadata_property: "metadata".to_string(),
135 id_property: "id".to_string(),
136 }
137 }
138}
139
140pub struct Neo4jVectorStore {
144 config: Neo4jConfig,
145 client: reqwest::Client,
146}
147
148impl std::fmt::Debug for Neo4jVectorStore {
149 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
150 f.debug_struct("Neo4jVectorStore")
151 .field("uri", &self.config.uri)
152 .field("index", &self.config.index_name)
153 .finish()
154 }
155}
156
157impl Neo4jVectorStore {
158 pub fn new(config: Neo4jConfig) -> Self {
160 Self {
161 config,
162 client: reqwest::Client::new(),
163 }
164 }
165
166 pub fn from_env_result() -> Result<Self, VectorStoreError> {
168 Ok(Self::new(Neo4jConfig::from_env_result()?))
169 }
170
171 fn tx_url(&self) -> String {
173 let http_uri = self
175 .config
176 .uri
177 .replace("bolt://", "http://")
178 .replace("neo4j://", "http://")
179 .replace("bolt+s://", "https://")
180 .replace("neo4j+s://", "https://");
181 format!(
182 "{}/db/{}/tx/commit",
183 http_uri.trim_end_matches('/'),
184 self.config.database
185 )
186 }
187
188 async fn run_query(
190 &self,
191 query: &str,
192 params: serde_json::Value,
193 ) -> Result<Neo4jResponse, VectorStoreError> {
194 let body = json!({
195 "statements": [{
196 "statement": query,
197 "parameters": params,
198 }]
199 });
200
201 let response = self
202 .client
203 .post(self.tx_url())
204 .header("Content-Type", "application/json")
205 .header(
206 "Authorization",
207 format!(
208 "Basic {}",
209 base64_encode(format!("{}:{}", self.config.username, self.config.password))
210 ),
211 )
212 .json(&body)
213 .send()
214 .await
215 .map_err(|e| VectorStoreError::ConnectionError(e.to_string()))?;
216
217 let status = response.status();
218 if !status.is_success() {
219 let error_text = response.text().await.unwrap_or_default();
220 return Err(VectorStoreError::ConnectionError(format!(
221 "HTTP {}: {}",
222 status, error_text
223 )));
224 }
225
226 let neo4j_response: Neo4jResponse = response
227 .json()
228 .await
229 .map_err(|e| VectorStoreError::StorageError(e.to_string()))?;
230
231 if let Some(errors) = &neo4j_response.errors {
233 if !errors.is_empty() {
234 let msg = errors
235 .iter()
236 .map(|e| e.message.clone())
237 .collect::<Vec<_>>()
238 .join("; ");
239 return Err(VectorStoreError::StorageError(msg));
240 }
241 }
242
243 Ok(neo4j_response)
244 }
245
246 fn search_query_prefix(&self) -> String {
248 "CALL db.index.vector.queryNodes($index_name, $k, $query_vector) \
249 YIELD node, score"
250 .to_string()
251 }
252
253 fn search_query_suffix(&self) -> String {
255 format!(
256 " RETURN node.{id_prop} AS id, \
257 node.{content_prop} AS content, \
258 node.{metadata_prop} AS metadata, \
259 score \
260 ORDER BY score DESC",
261 id_prop = self.config.id_property,
262 content_prop = self.config.content_property,
263 metadata_prop = self.config.metadata_property,
264 )
265 }
266
267 fn parse_search_results(&self, response: Neo4jResponse) -> Vec<SearchResult> {
269 let Some(neo4j_result) = response.results.first() else {
270 return Vec::new();
271 };
272
273 let mut search_results = Vec::new();
274 for row in &neo4j_result.data {
275 if row.row.len() >= 4 {
276 let id = row.row[0].as_str().unwrap_or_default().to_string();
277 let content = row.row[1].as_str().unwrap_or_default().to_string();
278 let score = row.row[3].as_f64().unwrap_or(0.0) as f32;
279
280 let mut doc = Document::new(content).with_id(id);
281
282 if let Some(meta_obj) = row.row[2].as_object() {
284 for (key, value) in meta_obj {
285 doc = doc.with_metadata(key, value.clone());
290 }
291 }
292
293 search_results.push(SearchResult {
294 document: doc,
295 score,
296 });
297 }
298 }
299
300 search_results
301 }
302}
303
304pub fn filter_to_cypher(
312 filter: &MetadataFilter,
313 metadata_prop: &str,
314) -> Result<(String, serde_json::Value), VectorStoreError> {
315 let mut state = CypherState { next: 0 };
316 let mut params = serde_json::Map::new();
317 let expr = state.expr(filter, metadata_prop, &mut params)?;
318 Ok((expr, serde_json::Value::Object(params)))
319}
320
321struct CypherState {
323 next: usize,
324}
325
326impl CypherState {
327 fn expr(
328 &mut self,
329 filter: &MetadataFilter,
330 metadata_prop: &str,
331 params: &mut serde_json::Map<String, serde_json::Value>,
332 ) -> Result<String, VectorStoreError> {
333 match filter {
334 MetadataFilter::Field { key, op, value } => {
335 let kp = format!("f{}k", self.next);
336 let vp = format!("f{}v", self.next);
337 self.next += 1;
338
339 params.insert(kp.clone(), serde_json::Value::String(key.clone()));
340 let target = format!("node.{}[${}]", metadata_prop, kp);
342
343 match op {
344 FilterOp::In | FilterOp::Nin => {
345 let arr = value.as_array().ok_or_else(|| {
346 VectorStoreError::UnsupportedFilter(format!(
347 "IN/NIN requires an array value for field `{}`, got {}",
348 key,
349 value_type_name(value)
350 ))
351 })?;
352 params.insert(vp.clone(), serde_json::Value::Array(arr.clone()));
353 if matches!(op, FilterOp::In) {
354 Ok(format!("{} IN ${}", target, vp))
355 } else {
356 Ok(format!("NOT {} IN ${}", target, vp))
357 }
358 }
359 _ => {
360 if !matches!(
361 value,
362 serde_json::Value::String(_)
363 | serde_json::Value::Number(_)
364 | serde_json::Value::Bool(_)
365 ) {
366 return Err(VectorStoreError::UnsupportedFilter(format!(
367 "cannot translate value of type {} to a Cypher comparison for field `{}`",
368 value_type_name(value),
369 key
370 )));
371 }
372 params.insert(vp.clone(), value.clone());
373 Ok(format!("{} {} ${}", target, cypher_op(op), vp))
374 }
375 }
376 }
377 MetadataFilter::And(filters) => self.join(filters, metadata_prop, params, "AND"),
378 MetadataFilter::Or(filters) => self.join(filters, metadata_prop, params, "OR"),
379 }
380 }
381
382 fn join(
383 &mut self,
384 filters: &[MetadataFilter],
385 metadata_prop: &str,
386 params: &mut serde_json::Map<String, serde_json::Value>,
387 keyword: &str,
388 ) -> Result<String, VectorStoreError> {
389 let parts = filters
390 .iter()
391 .map(|f| self.expr(f, metadata_prop, params))
392 .collect::<Result<Vec<String>, _>>()?;
393 if parts.is_empty() {
394 return Ok("TRUE".to_string());
395 }
396 Ok(parts
397 .iter()
398 .map(|p| format!("({})", p))
399 .collect::<Vec<_>>()
400 .join(&format!(" {} ", keyword)))
401 }
402}
403
404fn cypher_op(op: &FilterOp) -> &'static str {
405 match op {
406 FilterOp::Eq => "=",
407 FilterOp::Ne => "<>",
408 FilterOp::Gt => ">",
409 FilterOp::Gte => ">=",
410 FilterOp::Lt => "<",
411 FilterOp::Lte => "<=",
412 FilterOp::In | FilterOp::Nin => unreachable!("handled by filter_to_cypher"),
413 }
414}
415
416fn value_type_name(value: &serde_json::Value) -> &'static str {
417 match value {
418 serde_json::Value::Null => "null",
419 serde_json::Value::Bool(_) => "boolean",
420 serde_json::Value::Number(_) => "number",
421 serde_json::Value::String(_) => "string",
422 serde_json::Value::Array(_) => "array",
423 serde_json::Value::Object(_) => "object",
424 }
425}
426
427fn base64_encode(input: String) -> String {
429 const CHARSET: &[u8] = b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
430 let bytes = input.as_bytes();
431 let mut result = String::new();
432 let mut i = 0;
433 while i < bytes.len() {
434 let b0 = bytes[i];
435 let b1 = if i + 1 < bytes.len() { bytes[i + 1] } else { 0 };
436 let b2 = if i + 2 < bytes.len() { bytes[i + 2] } else { 0 };
437
438 result.push(CHARSET[((b0 >> 2) & 0x3F) as usize] as char);
439 result.push(CHARSET[(((b0 << 4) | (b1 >> 4)) & 0x3F) as usize] as char);
440 result.push(if i + 1 < bytes.len() {
441 CHARSET[(((b1 << 2) | (b2 >> 6)) & 0x3F) as usize] as char
442 } else {
443 '='
444 });
445 result.push(if i + 2 < bytes.len() {
446 CHARSET[(b2 & 0x3F) as usize] as char
447 } else {
448 '='
449 });
450
451 i += 3;
452 }
453 result
454}
455
456#[derive(Debug, Deserialize)]
462struct Neo4jResponse {
463 results: Vec<Neo4jResult>,
464 errors: Option<Vec<Neo4jError>>,
465}
466
467#[derive(Debug, Deserialize)]
468struct Neo4jResult {
469 data: Vec<Neo4jRow>,
470}
471
472#[derive(Debug, Deserialize)]
473struct Neo4jRow {
474 row: Vec<serde_json::Value>,
475}
476
477#[derive(Debug, Deserialize)]
478struct Neo4jError {
479 message: String,
480}
481
482#[async_trait]
483impl VectorStore for Neo4jVectorStore {
484 async fn add_documents(
485 &self,
486 documents: Vec<Document>,
487 embeddings: Vec<Vec<f32>>,
488 ) -> Result<Vec<String>, VectorStoreError> {
489 if documents.len() != embeddings.len() {
490 return Err(VectorStoreError::EmbeddingError(
491 "Number of documents and embeddings must match".to_string(),
492 ));
493 }
494
495 let ids: Vec<String> = documents
496 .iter()
497 .map(|doc| {
498 doc.id
499 .clone()
500 .unwrap_or_else(|| uuid::Uuid::new_v4().to_string())
501 })
502 .collect();
503
504 let rows: Vec<serde_json::Value> = documents
506 .into_iter()
507 .zip(embeddings)
508 .zip(ids.iter())
509 .map(|((doc, vec), id)| {
510 let metadata: serde_json::Value = doc
511 .metadata
512 .iter()
513 .map(|(k, v)| (k.clone(), json!(v)))
514 .collect();
515 json!({
516 "id": id,
517 "content": doc.content,
518 "embedding": vec,
519 "metadata": metadata,
520 })
521 })
522 .collect();
523
524 let query = format!(
525 "UNWIND $rows AS row \
526 MERGE (n:{label} {{{id_prop}: row.id}}) \
527 SET n.{content_prop} = row.content, \
528 n.{embedding_prop} = row.embedding, \
529 n.{metadata_prop} = row.metadata",
530 label = self.config.node_label,
531 id_prop = self.config.id_property,
532 content_prop = self.config.content_property,
533 embedding_prop = self.config.embedding_property,
534 metadata_prop = self.config.metadata_property,
535 );
536
537 self.run_query(&query, json!({ "rows": rows })).await?;
538
539 Ok(ids)
540 }
541
542 async fn similarity_search(
543 &self,
544 query_embedding: &[f32],
545 k: usize,
546 ) -> Result<Vec<SearchResult>, VectorStoreError> {
547 let query = format!(
548 "{}{}",
549 self.search_query_prefix(),
550 self.search_query_suffix()
551 );
552
553 let params = json!({
554 "index_name": self.config.index_name,
555 "k": k,
556 "query_vector": query_embedding,
557 });
558
559 let response = self.run_query(&query, params).await?;
560 Ok(self.parse_search_results(response))
561 }
562
563 async fn similarity_search_with_filter(
566 &self,
567 query_embedding: &[f32],
568 k: usize,
569 filter: Option<&MetadataFilter>,
570 ) -> Result<Vec<SearchResult>, VectorStoreError> {
571 let mut query = self.search_query_prefix();
572 let mut params = json!({
573 "index_name": self.config.index_name,
574 "k": k,
575 "query_vector": query_embedding,
576 });
577 if let Some(f) = filter {
578 let (where_clause, extra_params) = filter_to_cypher(f, &self.config.metadata_property)?;
579 query.push_str(&format!(" WHERE {}", where_clause));
580 if let (Some(base), Some(extra)) = (params.as_object_mut(), extra_params.as_object()) {
581 base.extend(extra.clone());
582 }
583 }
584 query.push_str(&self.search_query_suffix());
585
586 let response = self.run_query(&query, params).await?;
587 Ok(self.parse_search_results(response))
588 }
589
590 async fn get_document(&self, id: &str) -> Result<Option<Document>, VectorStoreError> {
591 let query = format!(
592 "MATCH (n:{label} {{{id_prop}: $id}}) \
593 RETURN n.{content_prop} AS content, n.{metadata_prop} AS metadata",
594 label = self.config.node_label,
595 id_prop = self.config.id_property,
596 content_prop = self.config.content_property,
597 metadata_prop = self.config.metadata_property,
598 );
599
600 let response = self.run_query(&query, json!({ "id": id })).await?;
601
602 let result = response.results.first();
603 let Some(neo4j_result) = result else {
604 return Ok(None);
605 };
606
607 let row = neo4j_result.data.first();
608 let Some(row) = row else {
609 return Ok(None);
610 };
611
612 if row.row.is_empty() {
613 return Ok(None);
614 }
615
616 let content = row.row[0].as_str().unwrap_or_default().to_string();
617 let mut doc = Document::new(content).with_id(id);
618
619 if row.row.len() > 1 {
620 if let Some(meta_obj) = row.row[1].as_object() {
621 for (key, value) in meta_obj {
622 doc = doc.with_metadata(key, value.clone());
624 }
625 }
626 }
627
628 Ok(Some(doc))
629 }
630
631 async fn get_embedding(&self, id: &str) -> Result<Option<Vec<f32>>, VectorStoreError> {
632 let query = format!(
633 "MATCH (n:{label} {{{id_prop}: $id}}) \
634 RETURN n.{embedding_prop} AS embedding",
635 label = self.config.node_label,
636 id_prop = self.config.id_property,
637 embedding_prop = self.config.embedding_property,
638 );
639
640 let response = self.run_query(&query, json!({ "id": id })).await?;
641
642 let result = response.results.first();
643 let Some(neo4j_result) = result else {
644 return Ok(None);
645 };
646
647 let row = neo4j_result.data.first();
648 let Some(row) = row else {
649 return Ok(None);
650 };
651
652 if row.row.is_empty() {
653 return Ok(None);
654 }
655
656 let embedding: Vec<f32> = row.row[0]
657 .as_array()
658 .map(|arr| {
659 arr.iter()
660 .filter_map(|v| v.as_f64().map(|f| f as f32))
661 .collect()
662 })
663 .unwrap_or_default();
664
665 if embedding.is_empty() {
666 Ok(None)
667 } else {
668 Ok(Some(embedding))
669 }
670 }
671
672 async fn delete_document(&self, id: &str) -> Result<(), VectorStoreError> {
673 let query = format!(
674 "MATCH (n:{label} {{{id_prop}: $id}}) \
675 DETACH DELETE n",
676 label = self.config.node_label,
677 id_prop = self.config.id_property,
678 );
679
680 self.run_query(&query, json!({ "id": id })).await?;
681 Ok(())
682 }
683
684 async fn count(&self) -> usize {
685 let query = format!(
686 "MATCH (n:{label}) RETURN count(n) AS cnt",
687 label = self.config.node_label,
688 );
689
690 let result = self.run_query(&query, json!({})).await;
691 match result {
692 Ok(response) => {
693 if let Some(neo4j_result) = response.results.first() {
694 if let Some(row) = neo4j_result.data.first() {
695 if let Some(cnt) = row.row.first() {
696 return cnt.as_u64().unwrap_or(0) as usize;
697 }
698 }
699 }
700 0
701 }
702 Err(_) => 0,
703 }
704 }
705
706 async fn clear(&self) -> Result<(), VectorStoreError> {
707 let query = format!(
708 "MATCH (n:{label}) \
709 DETACH DELETE n",
710 label = self.config.node_label,
711 );
712
713 self.run_query(&query, json!({})).await?;
714 Ok(())
715 }
716}
717
718#[cfg(test)]
719mod tests {
720 use super::*;
721
722 #[test]
723 fn test_config_new() {
724 let config = Neo4jConfig::new("bolt://localhost:7687", "neo4j", "pass", "my_index");
725 assert_eq!(config.uri, "bolt://localhost:7687");
726 assert_eq!(config.username, "neo4j");
727 assert_eq!(config.password, "pass");
728 assert_eq!(config.index_name, "my_index");
729 assert_eq!(config.database, "neo4j");
730 }
731
732 #[test]
733 fn test_config_builder() {
734 let config = Neo4jConfig::new("bolt://localhost:7687", "neo4j", "pass", "idx")
735 .with_database("mydb")
736 .with_node_label("Chunk")
737 .with_embedding_property("vec")
738 .with_content_property("text");
739 assert_eq!(config.database, "mydb");
740 assert_eq!(config.node_label, "Chunk");
741 assert_eq!(config.embedding_property, "vec");
742 assert_eq!(config.content_property, "text");
743 }
744
745 #[test]
746 fn test_config_default() {
747 let config = Neo4jConfig::default();
748 assert_eq!(config.uri, "bolt://localhost:7687");
749 assert_eq!(config.node_label, "Document");
750 assert_eq!(config.embedding_property, "embedding");
751 }
752
753 #[test]
754 fn test_tx_url_bolt() {
755 let config = Neo4jConfig::new("bolt://localhost:7687", "neo4j", "pass", "idx");
756 let store = Neo4jVectorStore::new(config);
757 assert_eq!(store.tx_url(), "http://localhost:7687/db/neo4j/tx/commit");
758 }
759
760 #[test]
761 fn test_tx_url_neo4j_scheme() {
762 let config = Neo4jConfig::new("neo4j://host:7687", "neo4j", "pass", "idx");
763 let store = Neo4jVectorStore::new(config);
764 assert_eq!(store.tx_url(), "http://host:7687/db/neo4j/tx/commit");
765 }
766
767 #[test]
768 fn test_tx_url_bolt_s() {
769 let config = Neo4jConfig::new("bolt+s://host:7687", "neo4j", "pass", "idx");
770 let store = Neo4jVectorStore::new(config);
771 assert_eq!(store.tx_url(), "https://host:7687/db/neo4j/tx/commit");
772 }
773
774 #[test]
775 fn test_tx_url_custom_database() {
776 let config =
777 Neo4jConfig::new("bolt://localhost:7687", "neo4j", "pass", "idx").with_database("mydb");
778 let store = Neo4jVectorStore::new(config);
779 assert_eq!(store.tx_url(), "http://localhost:7687/db/mydb/tx/commit");
780 }
781
782 #[test]
783 fn test_base64_encode() {
784 let encoded = base64_encode("neo4j:password".to_string());
786 assert_eq!(encoded, "bmVvNGo6cGFzc3dvcmQ=");
787 }
788
789 #[test]
790 fn test_base64_encode_empty() {
791 let encoded = base64_encode(String::new());
792 assert_eq!(encoded, "");
793 }
794
795 #[test]
796 fn test_store_new() {
797 let config = Neo4jConfig::new("bolt://localhost:7687", "neo4j", "pass", "idx");
798 let _store = Neo4jVectorStore::new(config);
799 }
800
801 #[test]
802 fn test_store_debug() {
803 let config = Neo4jConfig::new("bolt://localhost:7687", "neo4j", "pass", "idx");
804 let store = Neo4jVectorStore::new(config);
805 let debug_str = format!("{:?}", store);
806 assert!(debug_str.contains("Neo4jVectorStore"));
807 assert!(debug_str.contains("idx"));
808 }
809
810 #[test]
812 fn test_filter_to_cypher_field() {
813 let f = MetadataFilter::field("lang", FilterOp::Eq, "rust");
814 let (expr, params) = filter_to_cypher(&f, "metadata").unwrap();
815 assert_eq!(expr, "node.metadata[$f0k] = $f0v");
816 assert_eq!(params["f0k"], "lang");
817 assert_eq!(params["f0v"], "rust");
818
819 let (expr, params) = filter_to_cypher(
820 &MetadataFilter::field("year", FilterOp::Gte, 2020),
821 "metadata",
822 )
823 .unwrap();
824 assert_eq!(expr, "node.metadata[$f0k] >= $f0v");
825 assert_eq!(params["f0v"], 2020);
826
827 let (expr, _) = filter_to_cypher(
829 &MetadataFilter::field("lang", FilterOp::Ne, "rust"),
830 "metadata",
831 )
832 .unwrap();
833 assert_eq!(expr, "node.metadata[$f0k] <> $f0v");
834 }
835
836 #[test]
838 fn test_filter_to_cypher_in_nin() {
839 let (expr, params) = filter_to_cypher(
840 &MetadataFilter::field("tags", FilterOp::In, vec!["a", "b"]),
841 "meta",
842 )
843 .unwrap();
844 assert_eq!(expr, "node.meta[$f0k] IN $f0v");
845 assert_eq!(params["f0v"], serde_json::json!(["a", "b"]));
846
847 let (expr, _) = filter_to_cypher(
848 &MetadataFilter::field("tags", FilterOp::Nin, vec!["x"]),
849 "meta",
850 )
851 .unwrap();
852 assert_eq!(expr, "NOT node.meta[$f0k] IN $f0v");
853
854 let err = filter_to_cypher(&MetadataFilter::field("tags", FilterOp::In, "oops"), "meta");
856 assert!(matches!(err, Err(VectorStoreError::UnsupportedFilter(_))));
857 }
858
859 #[test]
861 fn test_filter_to_cypher_and_or() {
862 let f = MetadataFilter::or(vec![
863 MetadataFilter::field("lang", FilterOp::Eq, "python"),
864 MetadataFilter::and(vec![
865 MetadataFilter::field("lang", FilterOp::Eq, "rust"),
866 MetadataFilter::field("year", FilterOp::Gt, 2020),
867 ]),
868 ]);
869 let (expr, params) = filter_to_cypher(&f, "metadata").unwrap();
870 assert_eq!(
871 expr,
872 "(node.metadata[$f0k] = $f0v) OR ((node.metadata[$f1k] = $f1v) AND (node.metadata[$f2k] > $f2v))"
873 );
874 assert_eq!(params["f0v"], "python");
875 assert_eq!(params["f1v"], "rust");
876 assert_eq!(params["f2v"], 2020);
877 }
878
879 #[test]
881 fn test_filter_to_cypher_unsupported_value() {
882 let f = MetadataFilter::field("nested", FilterOp::Eq, serde_json::json!({ "a": 1 }));
883 assert!(matches!(
884 filter_to_cypher(&f, "metadata"),
885 Err(VectorStoreError::UnsupportedFilter(_))
886 ));
887 }
888}