1pub mod vector_index;
88use std::str::FromStr;
89
90use futures::TryStreamExt;
91use neo4rs::*;
92use rig_core::{
93 embeddings::EmbeddingModel,
94 vector_store::{VectorStoreError, request::SearchFilter},
95};
96use serde::{Deserialize, Serialize};
97use vector_index::{IndexConfig, Neo4jVectorIndex, VectorSimilarityFunction};
98
99pub struct Neo4jClient {
100 pub graph: Graph,
101}
102
103#[derive(Clone, Debug, Serialize, Deserialize)]
104pub struct Neo4jSearchFilter(String);
105
106impl SearchFilter for Neo4jSearchFilter {
107 type Value = serde_json::Value;
108
109 fn eq(key: impl AsRef<str>, value: Self::Value) -> Self {
110 Self(format!("n.{} = {}", key.as_ref(), serialize_cypher(value)))
111 }
112
113 fn gt(key: impl AsRef<str>, value: Self::Value) -> Self {
114 Self(format!("n.{} > {}", key.as_ref(), serialize_cypher(value)))
115 }
116
117 fn lt(key: impl AsRef<str>, value: Self::Value) -> Self {
118 Self(format!("n.{} < {}", key.as_ref(), serialize_cypher(value)))
119 }
120
121 fn and(self, rhs: Self) -> Self {
122 Self(format!("({}) AND ({})", self.0, rhs.0))
123 }
124
125 fn or(self, rhs: Self) -> Self {
126 Self(format!("({}) OR ({})", self.0, rhs.0))
127 }
128}
129
130impl Neo4jSearchFilter {
131 pub fn render(self) -> String {
132 format!("WHERE {}", self.0)
133 }
134
135 #[allow(clippy::should_implement_trait)]
136 pub fn not(self) -> Self {
137 Self(format!("NOT ({})", self.0))
138 }
139
140 pub fn gte(key: String, value: <Self as SearchFilter>::Value) -> Self {
141 Self(format!("n.{key} >= {}", serialize_cypher(value)))
142 }
143
144 pub fn lte(key: String, value: <Self as SearchFilter>::Value) -> Self {
145 Self(format!("n.{key} <= {}", serialize_cypher(value)))
146 }
147
148 pub fn member(key: String, values: Vec<<Self as SearchFilter>::Value>) -> Self {
149 Self(format!(
150 "n.{key} IN {}",
151 serialize_cypher(serde_json::Value::Array(values))
152 ))
153 }
154
155 pub fn contains<S>(key: String, pattern: S) -> Self
159 where
160 S: AsRef<str>,
161 {
162 Self(format!(
163 "n.{key} CONTAINS {}",
164 serialize_cypher(serde_json::Value::String(pattern.as_ref().into()))
165 ))
166 }
167
168 pub fn starts_with<S>(key: String, pattern: S) -> Self
170 where
171 S: AsRef<str>,
172 {
173 Self(format!(
174 "n.{key} STARTS WITH {}",
175 serialize_cypher(serde_json::Value::String(pattern.as_ref().into()))
176 ))
177 }
178
179 pub fn ends_with<S>(key: String, pattern: S) -> Self
181 where
182 S: AsRef<str>,
183 {
184 Self(format!(
185 "n.{key} ENDS WITH {}",
186 serialize_cypher(serde_json::Value::String(pattern.as_ref().into()))
187 ))
188 }
189
190 pub fn matches<S>(key: String, pattern: S) -> Self
191 where
192 S: AsRef<str>,
193 {
194 Self(format!(
195 "n.{key} =~ {}",
196 serialize_cypher(serde_json::Value::String(pattern.as_ref().into()))
197 ))
198 }
199}
200
201fn serialize_cypher(value: serde_json::Value) -> String {
202 use serde_json::Value::*;
203 match value {
204 Null => "null".into(),
205 Bool(b) => b.to_string(),
206 Number(n) => n.to_string(),
207 String(s) => format!("'{}'", s.replace('\'', "\\'")),
208 Array(arr) => {
209 format!(
210 "[{}]",
211 arr.into_iter()
212 .map(serialize_cypher)
213 .collect::<Vec<std::string::String>>()
214 .join(", ")
215 )
216 }
217 Object(obj) => {
218 format!(
219 "{{{}}}",
220 obj.into_iter()
221 .map(|(k, v)| format!("{k}: {}", serialize_cypher(v)))
222 .collect::<Vec<std::string::String>>()
223 .join(", ")
224 )
225 }
226 }
227}
228
229pub trait ToBoltType {
230 fn to_bolt_type(&self) -> BoltType;
231}
232
233impl<T> ToBoltType for T
234where
235 T: serde::Serialize,
236{
237 fn to_bolt_type(&self) -> BoltType {
238 match serde_json::to_value(self) {
239 Ok(json_value) => match json_value {
240 serde_json::Value::Null => BoltType::Null(BoltNull),
241 serde_json::Value::Bool(b) => BoltType::Boolean(BoltBoolean::new(b)),
242 serde_json::Value::Number(num) => {
243 if let Some(i) = num.as_i64() {
244 BoltType::Integer(BoltInteger::new(i))
245 } else if let Some(f) = num.as_f64() {
246 BoltType::Float(BoltFloat::new(f))
247 } else {
248 println!("Couldn't map to BoltType, will ignore.");
249 BoltType::Null(BoltNull) }
251 }
252 serde_json::Value::String(s) => BoltType::String(BoltString::new(&s)),
253 serde_json::Value::Array(arr) => BoltType::List(
254 arr.iter()
255 .map(|v| v.to_bolt_type())
256 .collect::<Vec<BoltType>>()
257 .into(),
258 ),
259 serde_json::Value::Object(obj) => {
260 let mut bolt_map = BoltMap::new();
261 for (k, v) in obj {
262 bolt_map.put(BoltString::new(&k), v.to_bolt_type());
263 }
264 BoltType::Map(bolt_map)
265 }
266 },
267 Err(_) => {
268 println!("Couldn't serialize to JSON, will ignore.");
269 BoltType::Null(BoltNull) }
271 }
272 }
273}
274
275impl Neo4jClient {
276 const GET_INDEX_QUERY: &'static str = "
277 SHOW VECTOR INDEXES
278 YIELD name, labelsOrTypes, properties, options
279 WHERE name=$index_name
280 RETURN name, labelsOrTypes, properties, options
281 ";
282
283 const SHOW_INDEXES_QUERY: &'static str = "SHOW VECTOR INDEXES YIELD name RETURN name";
284
285 pub fn new(graph: Graph) -> Self {
286 Self { graph }
287 }
288
289 pub async fn connect(uri: &str, user: &str, password: &str) -> Result<Self, VectorStoreError> {
290 tracing::info!("Connecting to Neo4j DB at {} ...", uri);
291 let graph = Graph::new(uri, user, password)
292 .await
293 .map_err(VectorStoreError::datastore)?;
294 tracing::info!("Connected to Neo4j");
295 Ok(Self { graph })
296 }
297
298 pub async fn from_config(config: Config) -> Result<Self, VectorStoreError> {
299 let graph = Graph::connect(config)
300 .await
301 .map_err(VectorStoreError::datastore)?;
302 Ok(Self { graph })
303 }
304
305 pub async fn execute_and_collect<T: for<'a> Deserialize<'a>>(
306 graph: &Graph,
307 query: Query,
308 ) -> Result<Vec<T>, VectorStoreError> {
309 graph
310 .execute(query)
311 .await
312 .map_err(VectorStoreError::datastore)?
313 .into_stream_as::<T>()
314 .try_collect::<Vec<T>>()
315 .await
316 .map_err(VectorStoreError::datastore)
317 }
318
319 pub async fn get_index<M: EmbeddingModel>(
326 &self,
327 model: M,
328 index_name: &str,
329 ) -> Result<Neo4jVectorIndex<M>, VectorStoreError> {
330 #[derive(Deserialize)]
331 #[serde(rename_all = "camelCase")]
332 struct IndexInfo {
333 name: String,
334 labels_or_types: Vec<String>,
335 properties: Vec<String>,
336 options: IndexOptions,
337 }
338
339 #[derive(Deserialize)]
340 #[serde(rename_all = "camelCase")]
341 struct IndexOptions {
342 #[allow(dead_code)]
343 index_provider: Option<String>,
344 index_config: IndexConfigDetails,
345 }
346
347 #[derive(Deserialize)]
348 struct IndexConfigDetails {
349 #[serde(rename = "vector.dimensions")]
350 vector_dimensions: i64,
351 #[serde(rename = "vector.similarity_function")]
352 vector_similarity_function: String,
353 }
354
355 let index_info = Self::execute_and_collect::<IndexInfo>(
356 &self.graph,
357 neo4rs::query(Self::GET_INDEX_QUERY).param("index_name", index_name),
358 )
359 .await?;
360
361 let index_config = if let Some(index) = index_info.first() {
362 if index.options.index_config.vector_dimensions != model.ndims() as i64 {
363 tracing::warn!(
364 "The embedding vector dimensions of the existing Neo4j DB index ({}) do not match the provided model dimensions ({}). This may affect search performance.",
365 index.options.index_config.vector_dimensions,
366 model.ndims()
367 );
368 }
369 let embedding_property = index.properties.first().ok_or_else(|| {
370 VectorStoreError::DatastoreError(
371 "Neo4j index is missing an embedding property".into(),
372 )
373 })?;
374 let mut config = IndexConfig::new(index.name.clone())
375 .embedding_property(embedding_property)
376 .similarity_function(VectorSimilarityFunction::from_str(
377 &index.options.index_config.vector_similarity_function,
378 )?);
379 if let Some(label) = index.labels_or_types.first() {
382 config = config.node_label(label);
383 }
384 config
385 } else {
386 let indexes = Self::execute_and_collect::<String>(
387 &self.graph,
388 neo4rs::query(Self::SHOW_INDEXES_QUERY),
389 )
390 .await?;
391 return Err(VectorStoreError::datastore(std::io::Error::new(
392 std::io::ErrorKind::NotFound,
393 format!(
394 "Index `{index_name}` not found in database. Available indexes: {indexes:?}"
395 ),
396 )));
397 };
398 Ok(Neo4jVectorIndex::new(
399 self.graph.clone(),
400 model,
401 index_config,
402 ))
403 }
404
405 pub async fn create_vector_index(
417 &self,
418 index_config: IndexConfig,
419 node_label: &str,
420 model: &impl EmbeddingModel,
421 ) -> Result<(), VectorStoreError> {
422 tracing::info!("Creating vector index {} ...", index_config.index_name);
424
425 let create_vector_index_query = format!(
426 "
427 CREATE VECTOR INDEX $index_name IF NOT EXISTS
428 FOR (m:{})
429 ON m.{}
430 OPTIONS {{
431 indexConfig: {{
432 `vector.dimensions`: $dimensions,
433 `vector.similarity_function`: $similarity_function
434 }}
435 }}",
436 node_label, index_config.embedding_property
437 );
438
439 self.graph
440 .run(
441 neo4rs::query(&create_vector_index_query)
442 .param("index_name", index_config.index_name.clone())
443 .param(
444 "similarity_function",
445 index_config.similarity_function.clone().to_bolt_type(),
446 )
447 .param("dimensions", model.ndims() as i64),
448 )
449 .await
450 .map_err(VectorStoreError::datastore)?;
451
452 let index_exists = self
454 .graph
455 .run(
456 neo4rs::query("CALL db.awaitIndex($index_name, 10000)")
457 .param("index_name", index_config.index_name.clone()),
458 )
459 .await;
460
461 if index_exists.is_err() {
462 tracing::warn!(
463 "Index with name `{}` is not ready or could not be created.",
464 index_config.index_name.clone()
465 );
466 }
467
468 tracing::info!(
469 "Index created successfully with name: {}",
470 index_config.index_name
471 );
472 Ok(())
473 }
474}