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 if let Some(s) = value.as_str() {
286 doc = doc.with_metadata(key, s);
287 } else {
288 doc = doc.with_metadata(key, value.to_string());
289 }
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 if let Some(s) = value.as_str() {
623 doc = doc.with_metadata(key, s);
624 } else {
625 doc = doc.with_metadata(key, value.to_string());
626 }
627 }
628 }
629 }
630
631 Ok(Some(doc))
632 }
633
634 async fn get_embedding(&self, id: &str) -> Result<Option<Vec<f32>>, VectorStoreError> {
635 let query = format!(
636 "MATCH (n:{label} {{{id_prop}: $id}}) \
637 RETURN n.{embedding_prop} AS embedding",
638 label = self.config.node_label,
639 id_prop = self.config.id_property,
640 embedding_prop = self.config.embedding_property,
641 );
642
643 let response = self.run_query(&query, json!({ "id": id })).await?;
644
645 let result = response.results.first();
646 let Some(neo4j_result) = result else {
647 return Ok(None);
648 };
649
650 let row = neo4j_result.data.first();
651 let Some(row) = row else {
652 return Ok(None);
653 };
654
655 if row.row.is_empty() {
656 return Ok(None);
657 }
658
659 let embedding: Vec<f32> = row.row[0]
660 .as_array()
661 .map(|arr| {
662 arr.iter()
663 .filter_map(|v| v.as_f64().map(|f| f as f32))
664 .collect()
665 })
666 .unwrap_or_default();
667
668 if embedding.is_empty() {
669 Ok(None)
670 } else {
671 Ok(Some(embedding))
672 }
673 }
674
675 async fn delete_document(&self, id: &str) -> Result<(), VectorStoreError> {
676 let query = format!(
677 "MATCH (n:{label} {{{id_prop}: $id}}) \
678 DETACH DELETE n",
679 label = self.config.node_label,
680 id_prop = self.config.id_property,
681 );
682
683 self.run_query(&query, json!({ "id": id })).await?;
684 Ok(())
685 }
686
687 async fn count(&self) -> usize {
688 let query = format!(
689 "MATCH (n:{label}) RETURN count(n) AS cnt",
690 label = self.config.node_label,
691 );
692
693 let result = self.run_query(&query, json!({})).await;
694 match result {
695 Ok(response) => {
696 if let Some(neo4j_result) = response.results.first() {
697 if let Some(row) = neo4j_result.data.first() {
698 if let Some(cnt) = row.row.first() {
699 return cnt.as_u64().unwrap_or(0) as usize;
700 }
701 }
702 }
703 0
704 }
705 Err(_) => 0,
706 }
707 }
708
709 async fn clear(&self) -> Result<(), VectorStoreError> {
710 let query = format!(
711 "MATCH (n:{label}) \
712 DETACH DELETE n",
713 label = self.config.node_label,
714 );
715
716 self.run_query(&query, json!({})).await?;
717 Ok(())
718 }
719}
720
721#[cfg(test)]
722mod tests {
723 use super::*;
724
725 #[test]
726 fn test_config_new() {
727 let config = Neo4jConfig::new("bolt://localhost:7687", "neo4j", "pass", "my_index");
728 assert_eq!(config.uri, "bolt://localhost:7687");
729 assert_eq!(config.username, "neo4j");
730 assert_eq!(config.password, "pass");
731 assert_eq!(config.index_name, "my_index");
732 assert_eq!(config.database, "neo4j");
733 }
734
735 #[test]
736 fn test_config_builder() {
737 let config = Neo4jConfig::new("bolt://localhost:7687", "neo4j", "pass", "idx")
738 .with_database("mydb")
739 .with_node_label("Chunk")
740 .with_embedding_property("vec")
741 .with_content_property("text");
742 assert_eq!(config.database, "mydb");
743 assert_eq!(config.node_label, "Chunk");
744 assert_eq!(config.embedding_property, "vec");
745 assert_eq!(config.content_property, "text");
746 }
747
748 #[test]
749 fn test_config_default() {
750 let config = Neo4jConfig::default();
751 assert_eq!(config.uri, "bolt://localhost:7687");
752 assert_eq!(config.node_label, "Document");
753 assert_eq!(config.embedding_property, "embedding");
754 }
755
756 #[test]
757 fn test_tx_url_bolt() {
758 let config = Neo4jConfig::new("bolt://localhost:7687", "neo4j", "pass", "idx");
759 let store = Neo4jVectorStore::new(config);
760 assert_eq!(store.tx_url(), "http://localhost:7687/db/neo4j/tx/commit");
761 }
762
763 #[test]
764 fn test_tx_url_neo4j_scheme() {
765 let config = Neo4jConfig::new("neo4j://host:7687", "neo4j", "pass", "idx");
766 let store = Neo4jVectorStore::new(config);
767 assert_eq!(store.tx_url(), "http://host:7687/db/neo4j/tx/commit");
768 }
769
770 #[test]
771 fn test_tx_url_bolt_s() {
772 let config = Neo4jConfig::new("bolt+s://host:7687", "neo4j", "pass", "idx");
773 let store = Neo4jVectorStore::new(config);
774 assert_eq!(store.tx_url(), "https://host:7687/db/neo4j/tx/commit");
775 }
776
777 #[test]
778 fn test_tx_url_custom_database() {
779 let config =
780 Neo4jConfig::new("bolt://localhost:7687", "neo4j", "pass", "idx").with_database("mydb");
781 let store = Neo4jVectorStore::new(config);
782 assert_eq!(store.tx_url(), "http://localhost:7687/db/mydb/tx/commit");
783 }
784
785 #[test]
786 fn test_base64_encode() {
787 let encoded = base64_encode("neo4j:password".to_string());
789 assert_eq!(encoded, "bmVvNGo6cGFzc3dvcmQ=");
790 }
791
792 #[test]
793 fn test_base64_encode_empty() {
794 let encoded = base64_encode(String::new());
795 assert_eq!(encoded, "");
796 }
797
798 #[test]
799 fn test_store_new() {
800 let config = Neo4jConfig::new("bolt://localhost:7687", "neo4j", "pass", "idx");
801 let _store = Neo4jVectorStore::new(config);
802 }
803
804 #[test]
805 fn test_store_debug() {
806 let config = Neo4jConfig::new("bolt://localhost:7687", "neo4j", "pass", "idx");
807 let store = Neo4jVectorStore::new(config);
808 let debug_str = format!("{:?}", store);
809 assert!(debug_str.contains("Neo4jVectorStore"));
810 assert!(debug_str.contains("idx"));
811 }
812
813 #[test]
815 fn test_filter_to_cypher_field() {
816 let f = MetadataFilter::field("lang", FilterOp::Eq, "rust");
817 let (expr, params) = filter_to_cypher(&f, "metadata").unwrap();
818 assert_eq!(expr, "node.metadata[$f0k] = $f0v");
819 assert_eq!(params["f0k"], "lang");
820 assert_eq!(params["f0v"], "rust");
821
822 let (expr, params) = filter_to_cypher(
823 &MetadataFilter::field("year", FilterOp::Gte, 2020),
824 "metadata",
825 )
826 .unwrap();
827 assert_eq!(expr, "node.metadata[$f0k] >= $f0v");
828 assert_eq!(params["f0v"], 2020);
829
830 let (expr, _) = filter_to_cypher(
832 &MetadataFilter::field("lang", FilterOp::Ne, "rust"),
833 "metadata",
834 )
835 .unwrap();
836 assert_eq!(expr, "node.metadata[$f0k] <> $f0v");
837 }
838
839 #[test]
841 fn test_filter_to_cypher_in_nin() {
842 let (expr, params) = filter_to_cypher(
843 &MetadataFilter::field("tags", FilterOp::In, vec!["a", "b"]),
844 "meta",
845 )
846 .unwrap();
847 assert_eq!(expr, "node.meta[$f0k] IN $f0v");
848 assert_eq!(params["f0v"], serde_json::json!(["a", "b"]));
849
850 let (expr, _) = filter_to_cypher(
851 &MetadataFilter::field("tags", FilterOp::Nin, vec!["x"]),
852 "meta",
853 )
854 .unwrap();
855 assert_eq!(expr, "NOT node.meta[$f0k] IN $f0v");
856
857 let err = filter_to_cypher(&MetadataFilter::field("tags", FilterOp::In, "oops"), "meta");
859 assert!(matches!(err, Err(VectorStoreError::UnsupportedFilter(_))));
860 }
861
862 #[test]
864 fn test_filter_to_cypher_and_or() {
865 let f = MetadataFilter::or(vec![
866 MetadataFilter::field("lang", FilterOp::Eq, "python"),
867 MetadataFilter::and(vec![
868 MetadataFilter::field("lang", FilterOp::Eq, "rust"),
869 MetadataFilter::field("year", FilterOp::Gt, 2020),
870 ]),
871 ]);
872 let (expr, params) = filter_to_cypher(&f, "metadata").unwrap();
873 assert_eq!(
874 expr,
875 "(node.metadata[$f0k] = $f0v) OR ((node.metadata[$f1k] = $f1v) AND (node.metadata[$f2k] > $f2v))"
876 );
877 assert_eq!(params["f0v"], "python");
878 assert_eq!(params["f1v"], "rust");
879 assert_eq!(params["f2v"], 2020);
880 }
881
882 #[test]
884 fn test_filter_to_cypher_unsupported_value() {
885 let f = MetadataFilter::field("nested", FilterOp::Eq, serde_json::json!({ "a": 1 }));
886 assert!(matches!(
887 filter_to_cypher(&f, "metadata"),
888 Err(VectorStoreError::UnsupportedFilter(_))
889 ));
890 }
891}