Skip to main content

rig_neo4j/
lib.rs

1//! A Rig vector store for Neo4j.
2//!
3//! This crate is a companion crate to the [rig-core crate](https://github.com/0xPlaygrounds/rig).
4//! It provides a vector store implementation that uses Neo4j as the underlying datastore.
5//!
6//! See the [README](https://github.com/0xPlaygrounds/rig/tree/main/crates/rig-neo4j) for more information.
7//!
8//! ## Prerequisites
9//!
10//! ### GenAI Plugin
11//! The GenAI plugin is enabled by default in Neo4j Aura.
12//!
13//! The plugin needs to be installed on self-managed instances. This is done by moving the neo4j-genai.jar
14//! file from /products to /plugins in the Neo4j home directory, or, if you are using Docker, by starting
15//! the Docker container with the extra parameter `--env NEO4J_PLUGINS='["genai"]'`.
16//!
17//! For more information, see [Operations Manual → Configure plugins](https://neo4j.com/docs/upgrade-migration-guide/current/version-5/migration/install-and-configure/#_plugins).
18//!
19//! ### Pre-existing Vector Index
20//!
21//! The [Neo4jVectorStoreIndex](Neo4jVectorIndex) struct is designed to work with a pre-existing
22//! Neo4j vector index. You can create the index using the Neo4j browser, a raw Cypher query, or the
23//! [Neo4jClient::create_vector_index] method.
24//! See the [Neo4j documentation](https://neo4j.com/docs/genai/tutorials/embeddings-vector-indexes/setup/vector-index/)
25//! for more information.
26//!
27//! The index name must be unique among both indexes and constraints.
28//! ❗A newly created index is not immediately available but is created in the background.
29//!
30//! ```text
31//! CREATE VECTOR INDEX moviePlots
32//!     FOR (m:Movie)
33//!     ON m.embedding
34//!     OPTIONS {indexConfig: {
35//!         `vector.dimensions`: 1536,
36//!         `vector.similarity_function`: 'cosine'
37//!     }}
38//! ```
39//!
40//! ## Simple example:
41//! More examples can be found in the [/examples](https://github.com/0xPlaygrounds/rig/tree/main/crates/rig-neo4j/examples) folder.
42//! ```ignore
43//! use rig_neo4j::{vector_index::*, Neo4jClient};
44//! use neo4rs::ConfigBuilder;
45//! use rig_core::{providers::openai::*, vector_store::VectorStoreIndex};
46//! use serde::Deserialize;
47//! use std::env;
48//!
49//! #[tokio::main]
50//! async fn main() {
51//!     let openai_api_key = env::var("OPENAI_API_KEY").expect("OPENAI_API_KEY not set");
52//!     let openai_client = Client::new(&openai_api_key);
53//!     let model = openai_client.embedding_model(TEXT_EMBEDDING_ADA_002);
54//!
55//!
56//!     const NEO4J_URI: &str = "neo4j+s://demo.neo4jlabs.com:7687";
57//!     const NEO4J_DB: &str = "recommendations";
58//!     const NEO4J_USERNAME: &str = "recommendations";
59//!     const NEO4J_PASSWORD: &str = "recommendations";
60//!
61//!     let client = Neo4jClient::from_config(
62//!         ConfigBuilder::default()
63//!             .uri(NEO4J_URI)
64//!             .db(NEO4J_DB)
65//!             .user(NEO4J_USERNAME)
66//!             .password(NEO4J_PASSWORD)
67//!             .build()
68//!             .unwrap(),
69//!     )
70//!    .await
71//!    .unwrap();
72//!
73//!     let index = client.get_index(
74//!         model,
75//!         "moviePlotsEmbedding"
76//!     ).await.unwrap();
77//!
78//!     #[derive(Debug, Deserialize)]
79//!     struct Movie {
80//!         title: String,
81//!         plot: String,
82//!     }
83//!     let results = index.top_n::<Movie>("Batman", 3).await.unwrap();
84//!     println!("{:#?}", results);
85//! }
86//! ```
87pub 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    // String matching
156
157    /// Tests whether the value at `key` contains the pattern
158    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    /// Tests whether the value at `key` starts with the pattern
169    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    /// Tests whether the value at `key` ends with the pattern
180    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) // Handle unexpected number type
250                    }
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) // Handle serialization error
270            }
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    /// Returns a `Neo4jVectorIndex` that mirrors an existing Neo4j Vector Index.
320    ///
321    /// An index (of type "vector") of the same name as `index_name` must already exist for the Neo4j database.
322    /// See the Neo4j [documentation (Create vector index)](https://neo4j.com/docs/genai/tutorials/embeddings-vector-indexes/setup/vector-index/) for more information on creating indexes.
323    ///
324    /// ❗IMPORTANT: The index must be created with the same embedding model that will be used to query the index.
325    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            // Preserve the node label the index is attached to so `insert_documents`
380            // writes to the same label.
381            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    /// Calls the `CREATE VECTOR INDEX` Neo4j query and waits for the index to be created.
406    /// A newly created index is not immediately fully available but is created (i.e. data is indexed) in the background.
407    ///
408    /// ❗ If there is already an index targeting the same node label and property, the new index creation will fail.
409    ///
410    /// ### Arguments
411    /// * `index_name` - The name of the index to create.
412    /// * `node_label` - The label of the nodes to which the index will be applied. For example, if your nodes have
413    ///   the label `:Movie`, pass "Movie" as the `node_label` parameter.
414    /// * `embedding_prop_name` (optional) - The name of the property that contains the embedding vectors. Defaults to "embedding".
415    ///
416    pub async fn create_vector_index(
417        &self,
418        index_config: IndexConfig,
419        node_label: &str,
420        model: &impl EmbeddingModel,
421    ) -> Result<(), VectorStoreError> {
422        // Create a vector index on our vector store
423        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        // Check if the index exists with db.awaitIndex(), the call timeouts if the index is not ready
453        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}