Skip to content

Commit 491c1ff

Browse files
authored
Adds an upsert vector function (neo4j#60)
* Added an upsert_vector function * Added test_upsert_vector unit tests * Added mypy type hints back in * Removed node_label from upsert_vector * Added Neo4jInsertionError to api.rst docs * Updated CHANGELOG.md
1 parent 607e667 commit 491c1ff

File tree

5 files changed

+96
-8
lines changed

5 files changed

+96
-8
lines changed

CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@
44

55
### Added
66
- Stopped embeddings from being returned when searching with `VectorRetriever`. Added `nodeLabels` and `id` to the metadata of `VectorRetriever` results.
7+
- Added `upsert_vector` utility function for attaching vectors to node properties.
8+
- Introduced `Neo4jInsertionError` for handling insertion failures in Neo4j.
79

810
## 0.2.0
911

docs/source/api.rst

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -139,6 +139,13 @@ Neo4jIndexError
139139
:show-inheritance:
140140

141141

142+
Neo4jInsertionError
143+
===============
144+
145+
.. autoclass:: neo4j_genai.exceptions.Neo4jInsertionError
146+
:show-inheritance:
147+
148+
142149
Neo4jVersionError
143150
=================
144151

src/neo4j_genai/exceptions.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,12 @@ class Neo4jIndexError(Neo4jGenAiError):
6161
pass
6262

6363

64+
class Neo4jInsertionError(Neo4jGenAiError):
65+
"""Exception raised when inserting data into the Neo4j database fails."""
66+
67+
pass
68+
69+
6470
class Neo4jVersionError(Neo4jGenAiError):
6571
"""Exception raised when Neo4j version does not meet minimum requirements."""
6672

src/neo4j_genai/indexes.py

Lines changed: 41 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -13,13 +13,14 @@
1313
# See the License for the specific language governing permissions and
1414
# limitations under the License.
1515

16+
import logging
17+
from typing import Literal
18+
1619
import neo4j
1720
from pydantic import ValidationError
1821

19-
from .exceptions import Neo4jIndexError
20-
from .types import VectorIndexModel, FulltextIndexModel
21-
import logging
22-
from typing import Literal
22+
from .exceptions import Neo4jIndexError, Neo4jInsertionError
23+
from .types import FulltextIndexModel, VectorIndexModel
2324

2425
logger = logging.getLogger(__name__)
2526

@@ -143,3 +144,39 @@ def drop_index_if_exists(driver: neo4j.Driver, name: str) -> None:
143144
driver.execute_query(query, parameters)
144145
except neo4j.exceptions.ClientError as e:
145146
raise Neo4jIndexError(f"Dropping Neo4j index failed: {e}")
147+
148+
149+
def upsert_vector(
150+
driver: neo4j.Driver,
151+
node_id: int,
152+
vector_prop: str,
153+
vector: list[float],
154+
) -> None:
155+
"""
156+
This method constructs a Cypher query and executes it to upsert (insert or update) a vector property on a specific node.
157+
158+
Args:
159+
driver (neo4j.Driver): Neo4j Python driver instance.
160+
node_id (int): The id of the node.
161+
vector_prop (str): The name of the property to store the vector in.
162+
vector (list[float]): The vector to store.
163+
164+
Raises:
165+
Neo4jInsertError: If upserting of the vector fails.
166+
"""
167+
try:
168+
query = """
169+
MATCH (n)
170+
WHERE elementId(n) = $id
171+
WITH n
172+
CALL db.create.setNodeVectorProperty(n, $vector_prop, $vector)
173+
RETURN n
174+
"""
175+
parameters = {
176+
"id": node_id,
177+
"vector_prop": vector_prop,
178+
"vector": vector,
179+
}
180+
driver.execute_query(query, parameters)
181+
except neo4j.exceptions.ClientError as e:
182+
raise Neo4jInsertionError(f"Upserting vector to Neo4j failed: {e}")

tests/unit/test_indexes.py

Lines changed: 40 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -12,15 +12,16 @@
1212
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
1313
# See the License for the specific language governing permissions and
1414
# limitations under the License.
15+
from unittest.mock import MagicMock
16+
1517
import neo4j.exceptions
1618
import pytest
17-
18-
from neo4j_genai.exceptions import Neo4jIndexError
19-
from unittest.mock import MagicMock
19+
from neo4j_genai.exceptions import Neo4jIndexError, Neo4jInsertionError
2020
from neo4j_genai.indexes import (
21+
create_fulltext_index,
2122
create_vector_index,
2223
drop_index_if_exists,
23-
create_fulltext_index,
24+
upsert_vector,
2425
)
2526

2627

@@ -161,3 +162,38 @@ def test_create_fulltext_index_ensure_escaping(driver: MagicMock) -> None:
161162
create_query,
162163
{"name": "my-complicated-`-index"},
163164
)
165+
166+
167+
def test_upsert_vector_happy_path(driver: MagicMock) -> None:
168+
id = 1
169+
vector_prop = "embedding"
170+
vector = [1.0, 2.0, 3.0]
171+
172+
upsert_vector(driver, id, vector_prop, vector)
173+
174+
upsert_query = """
175+
MATCH (n)
176+
WHERE elementId(n) = $id
177+
WITH n
178+
CALL db.create.setNodeVectorProperty(n, $vector_prop, $vector)
179+
RETURN n
180+
"""
181+
182+
driver.execute_query.assert_called_once_with(
183+
upsert_query,
184+
{"id": id, "vector_prop": vector_prop, "vector": vector},
185+
)
186+
187+
188+
def test_upsert_vector_raises_error_with_neo4j_insertion_error(
189+
driver: MagicMock,
190+
) -> None:
191+
id = 1
192+
vector_prop = "embedding"
193+
vector = [1.0, 2.0, 3.0]
194+
driver.execute_query.side_effect = neo4j.exceptions.ClientError
195+
196+
with pytest.raises(Neo4jInsertionError) as excinfo:
197+
upsert_vector(driver, id, vector_prop, vector)
198+
199+
assert "Upserting vector to Neo4j failed" in str(excinfo)

0 commit comments

Comments
 (0)