Skip to content

Commit 0194f3a

Browse files
Merge pull request #816 from neo4j-contrib/rc/5.3.2
Rc/5.3.2
2 parents 7c6662a + a92e491 commit 0194f3a

24 files changed

+2385
-163
lines changed

Changelog

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,7 @@
1+
Version 5.3.2 2024-06
2+
* Add support for Vector and Fulltext indexes creation
3+
* Add DateTimeNeo4jFormatProperty for Neo4j native datetime format
4+
15
Version 5.3.1 2024-05
26
* Add neomodel_generate_diagram script, which generates a graph model diagram based on your neomodel class definitions. Arrows and PlantUML dot options
37
* Fix bug in async iterator async for MyClass.nodes

doc/source/configuration.rst

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ Adjust driver configuration - these options are only available for this connecti
3232
config.MAX_TRANSACTION_RETRY_TIME = 30.0 # default
3333
config.RESOLVER = None # default
3434
config.TRUST = neo4j.TRUST_SYSTEM_CA_SIGNED_CERTIFICATES # default
35-
config.USER_AGENT = neomodel/v5.3.1 # default
35+
config.USER_AGENT = neomodel/v5.3.2 # default
3636

3737
Setting the database name, if different from the default one::
3838

doc/source/extending.rst

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,16 @@ Creating purely abstract classes is achieved using the `__abstract_node__` prope
2121
self.balance = self.balance + int(amount)
2222
self.save()
2323

24+
Custom label
25+
------------
26+
By default, neomodel uses the class name as the label for nodes. This can be overridden by setting the __label__ property on the class::
27+
28+
class PersonClass(StructuredNode):
29+
__label__ = "Person"
30+
name = StringProperty(unique_index=True)
31+
32+
Creating a PersonClass instance and saving it to the database will result in a node with the label "Person".
33+
2434

2535
Optional Labels
2636
---------------
@@ -131,11 +141,16 @@ Consider for example the following snippet of code::
131141
class PilotPerson(BasePerson):
132142
pass
133143

144+
145+
class UserClass(StructuredNode):
146+
__label__ = "User"
147+
134148
Once this script is executed, the *node-class registry* would contain the following entries: ::
135149

136150
{"BasePerson"} --> class BasePerson
137151
{"BasePerson", "TechnicalPerson"} --> class TechnicalPerson
138152
{"BasePerson", "PilotPerson"} --> class PilotPerson
153+
{"User"} --> class UserClass
139154

140155
Therefore, a ``Node`` with labels ``"BasePerson", "TechnicalPerson"`` would lead to the instantiation of a
141156
``TechnicalPerson`` object. This automatic resolution is **optional** and can be invoked automatically via
@@ -184,12 +199,52 @@ This automatic class resolution however, requires a bit of caution:
184199
``{"BasePerson", "PilotPerson"}`` to ``PilotPerson`` **in the global scope** with a mapping of the same
185200
set of labels but towards the class defined within the **local scope** of ``some_function``.
186201

202+
3. Two classes with different names but the same __label__ override will also result in a ``ClassAlreadyDefined`` exception.
203+
This can be avoided under certain circumstances, as explained in the next section on 'Database specific labels'.
204+
187205
Both ``ModelDefinitionMismatch`` and ``ClassAlreadyDefined`` produce an error message that returns the labels of the
188206
node that created the problem (either the `Node` returned from the database or the class that was attempted to be
189207
redefined) as well as the state of the current *node-class registry*. These two pieces of information can be used to
190208
debug the model mismatch further.
191209

192210

211+
Database specific labels
212+
------------------------
213+
**Only for Neo4j Enterprise Edition, with multiple databases**
214+
215+
In some cases, it is necessary to have a class with a label that is not unique across the database.
216+
This can be achieved by setting the `__target_databases__` property to a list of strings ::
217+
class PatientOne(AsyncStructuredNode):
218+
__label__ = "Patient"
219+
__target_databases__ = ["db_one"]
220+
name = StringProperty()
221+
222+
class PatientTwo(AsyncStructuredNode):
223+
__label__ = "Patient"
224+
__target_databases__ = ["db_two"]
225+
identifier = StringProperty()
226+
227+
In this example, both `PatientOne` and `PatientTwo` have the label "Patient", but these will be mapped in a database-specific *node-class registry*.
228+
229+
Now, if you fetch a node with label Patient from your database with auto resolution enabled, neomodel will try to resolve it to the correct class
230+
based on the database it was fetched from ::
231+
db.set_connection("bolt://neo4j:password@localhost:7687/db_one")
232+
patients = db.cypher_query("MATCH (n:Patient) RETURN n", resolve_objects=True) --> instance of PatientOne
233+
234+
The following will result in a ``ClassAlreadyDefined`` exception, because when retrieving from ``db_one``,
235+
neomodel would not be able to decide which model to parse into ::
236+
class GeneralPatient(AsyncStructuredNode):
237+
__label__ = "Patient"
238+
name = StringProperty()
239+
240+
class PatientOne(AsyncStructuredNode):
241+
__label__ = "Patient"
242+
__target_databases__ = ["db_one"]
243+
name = StringProperty()
244+
245+
.. warning:: This does not prevent you from saving a node to the "wrong database". So you can still save an instance of PatientTwo to database "db_one".
246+
247+
193248
``neomodel`` under multiple processes and threads
194249
-------------------------------------------------
195250
It is very important to realise that neomodel preserves a mapping of the set of labels associated with the Neo4J

doc/source/getting_started.rst

Lines changed: 16 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,8 @@ in the case of ``Relationship`` it will be possible to be queried in either dire
7373
Neomodel automatically creates a label for each ``StructuredNode`` class in the database with the corresponding indexes
7474
and constraints.
7575

76+
.. _inspect_database_doc:
77+
7678
Database Inspection - Requires APOC
7779
===================================
7880
You can inspect an existing Neo4j database to generate a neomodel definition file using the ``inspect`` command::
@@ -239,7 +241,7 @@ additional relations with a single call::
239241

240242
# The following call will generate one MATCH with traversal per
241243
# item in .fetch_relations() call
242-
results = Person.nodes.all().fetch_relations('country')
244+
results = Person.nodes.fetch_relations('country').all()
243245
for result in results:
244246
print(result[0]) # Person
245247
print(result[1]) # associated Country
@@ -248,33 +250,38 @@ You can traverse more than one hop in your relations using the
248250
following syntax::
249251

250252
# Go from person to City then Country
251-
Person.nodes.all().fetch_relations('city__country')
253+
Person.nodes.fetch_relations('city__country').all()
252254

253255
You can also force the use of an ``OPTIONAL MATCH`` statement using
254256
the following syntax::
255257

256258
from neomodel.match import Optional
257259

258-
results = Person.nodes.all().fetch_relations(Optional('country'))
260+
results = Person.nodes.fetch_relations(Optional('country')).all()
261+
262+
.. note::
263+
264+
Any relationship that you intend to traverse using this method **MUST have a model defined**, even if only the default StructuredRel, like::
265+
266+
class Person(StructuredNode):
267+
country = RelationshipTo(Country, 'IS_FROM', model=StructuredRel)
268+
269+
Otherwise, neomodel will not be able to determine which relationship model to resolve into, and will fail.
259270

260271
.. note::
261272

262273
You can fetch one or more relations within the same call
263274
to `.fetch_relations()` and you can mix optional and non-optional
264275
relations, like::
265276

266-
Person.nodes.all().fetch_relations('city__country', Optional('country'))
277+
Person.nodes.fetch_relations('city__country', Optional('country')).all()
267278

268279
.. note::
269280

270-
This feature is still a work in progress for extending path traversal and fecthing.
271-
It currently stops at returning the resolved objects as they are returned in Cypher.
272-
So for instance, if your path looks like ``(startNode:Person)-[r1]->(middleNode:City)<-[r2]-(endNode:Country)``,
281+
If your path looks like ``(startNode:Person)-[r1]->(middleNode:City)<-[r2]-(endNode:Country)``,
273282
then you will get a list of results, where each result is a list of ``(startNode, r1, middleNode, r2, endNode)``.
274283
These will be resolved by neomodel, so ``startNode`` will be a ``Person`` class as defined in neomodel for example.
275284

276-
If you want to go further in the resolution process, you have to develop your own parser (for now).
277-
278285

279286
Async neomodel
280287
==============

doc/source/index.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,7 @@ Contents
7373
relationships
7474
properties
7575
spatial_properties
76+
schema_management
7677
queries
7778
cypher
7879
transactions

doc/source/properties.rst

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -6,15 +6,15 @@ Property types
66

77
The following properties are available on nodes and relationships:
88

9-
==================================================== ===========================================================
10-
:class:`~neomodel.properties.AliasProperty` :class:`~neomodel.properties.IntegerProperty`
11-
:class:`~neomodel.properties.ArrayProperty` :class:`~neomodel.properties.JSONProperty`
12-
:class:`~neomodel.properties.BooleanProperty` :class:`~neomodel.properties.RegexProperty`
13-
:class:`~neomodel.properties.DateProperty` :class:`~neomodel.properties.StringProperty` (:ref:`Notes <properties_notes>`)
14-
:class:`~neomodel.properties.DateTimeProperty` :class:`~neomodel.properties.UniqueIdProperty`
15-
:class:`~neomodel.properties.DateTimeFormatProperty` :class:`~neomodel.contrib.spatial_properties.PointProperty`
16-
:class:`~neomodel.properties.FloatProperty` \
17-
==================================================== ===========================================================
9+
========================================================= ===========================================================
10+
:class:`~neomodel.properties.AliasProperty` :class:`~neomodel.properties.FloatProperty`
11+
:class:`~neomodel.properties.ArrayProperty` :class:`~neomodel.properties.IntegerProperty`
12+
:class:`~neomodel.properties.BooleanProperty` :class:`~neomodel.properties.JSONProperty`
13+
:class:`~neomodel.properties.DateProperty` :class:`~neomodel.properties.RegexProperty`
14+
:class:`~neomodel.properties.DateTimeProperty` :class:`~neomodel.properties.StringProperty` (:ref:`Notes <properties_notes>`)
15+
:class:`~neomodel.properties.DateTimeFormatProperty` :class:`~neomodel.properties.UniqueIdProperty`
16+
:class:`~neomodel.properties.DateTimeNeo4jFormatProperty` :class:`~neomodel.contrib.spatial_properties.PointProperty`
17+
========================================================= ===========================================================
1818

1919

2020
Naming Convention

doc/source/schema_management.rst

Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
=================
2+
Schema management
3+
=================
4+
5+
Neo4j allows a flexible schema management, where you can define indexes and constraints on the properties of nodes and relationships.
6+
To learn more, please refer to the `Neo4j schema documentation <https://neo4j.com/docs/getting-started/cypher-intro/schema/>`_.
7+
8+
Defining your model
9+
-------------------
10+
11+
neomodel allows you to define indexes and constraints in your node and relationship classes, like so: ::
12+
13+
from neomodel import (StructuredNode, StructuredRel, StringProperty,
14+
IntegerProperty, RelationshipTo)
15+
16+
class LocatedIn(StructuredRel):
17+
since = IntegerProperty(index=True)
18+
19+
class Country(StructuredNode):
20+
code = StringProperty(unique_index=True)
21+
22+
class City(StructuredNode):
23+
name = StringProperty(index=True)
24+
country = RelationshipTo(Country, 'FROM_COUNTRY', model=LocatedIn)
25+
26+
27+
Applying constraints and indexes
28+
--------------------------------
29+
After creating your model, any constraints or indexes must be applied to Neo4j and ``neomodel`` provides a
30+
script (:ref:`neomodel_install_labels`) to automate this: ::
31+
32+
$ neomodel_install_labels yourapp.py someapp.models --db bolt://neo4j_username:neo4j_password@localhost:7687
33+
34+
It is important to execute this after altering the schema and observe the number of classes it reports.
35+
36+
Ommitting the ``--db`` argument will default to the ``NEO4J_BOLT_URL`` environment variable. This is useful for masking
37+
your credentials.
38+
39+
.. note::
40+
The script will only create indexes and constraints that are defined in your model classes. It will not remove any
41+
existing indexes or constraints that are not defined in your model classes.
42+
43+
Indexes
44+
=======
45+
46+
The following indexes are supported:
47+
48+
- ``index=True``: This will create the default Neo4j index on the property (currently RANGE).
49+
- ``fulltext_index=FulltextIndex()``: This will create a FULLTEXT index on the property. Only available for Neo4j version 5.16 or higher. With this one, you can define the following options:
50+
- ``analyzer``: The analyzer to use. The default is ``standard-no-stop-words``.
51+
- ``eventually_consistent``: Whether the index should be eventually consistent. The default is ``False``.
52+
53+
Please refer to the `Neo4j documentation <https://neo4j.com/docs/cypher-manual/current/indexes/semantic-indexes/full-text-indexes/#configuration-settings>`_. for more information on fulltext indexes.
54+
55+
- ``vector_index=VectorIndex()``: This will create a VECTOR index on the property. Only available for Neo4j version 5.15 (node) and 5.18 (relationship) or higher. With this one, you can define the following options:
56+
- ``dimensions``: The dimension of the vector. The default is 1536.
57+
- ``similarity_function``: The similarity algorithm to use. The default is ``cosine``.
58+
59+
Those indexes are available for both node- and relationship properties.
60+
61+
.. note::
62+
Yes, you can create multiple indexes of a different type on the same property. For example, a default index and a fulltext index.
63+
64+
.. note::
65+
For the semantic indexes (fulltext and vector), this allows you to create indexes, but searching those indexes require using Cypher queries.
66+
This is because Cypher only supports querying those indexes through a specific procedure for now.
67+
68+
Full example: ::
69+
70+
from neomodel import StructuredNode, StringProperty, FulltextIndex, VectorIndex
71+
class VeryIndexedNode(StructuredNode):
72+
name = StringProperty(
73+
index=True,
74+
fulltext_index=FulltextIndex(analyzer='english', eventually_consistent=True)
75+
vector_index=VectorIndex(dimensions=512, similarity_function='euclidean')
76+
)
77+
78+
Constraints
79+
===========
80+
81+
The following constraints are supported:
82+
83+
- ``unique_index=True``: This will create a uniqueness constraint on the property. Available for both nodes and relationships (Neo4j version 5.7 or higher).
84+
85+
.. note::
86+
The uniquess constraint of Neo4j is not supported as such, but using ``required=True`` on a property serves the same purpose.
87+
88+
89+
Extracting the schema from a database
90+
=====================================
91+
92+
You can extract the schema from an existing database using the ``neomodel_inspect_database`` script (:ref:`inspect_database_doc`).
93+
This script will output the schema in the neomodel format, including indexes and constraints.

neomodel/__init__.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,15 +25,18 @@
2525
BooleanProperty,
2626
DateProperty,
2727
DateTimeFormatProperty,
28+
DateTimeNeo4jFormatProperty,
2829
DateTimeProperty,
2930
EmailProperty,
3031
FloatProperty,
32+
FulltextIndex,
3133
IntegerProperty,
3234
JSONProperty,
3335
NormalizedProperty,
3436
RegexProperty,
3537
StringProperty,
3638
UniqueIdProperty,
39+
VectorIndex,
3740
)
3841
from neomodel.sync_.cardinality import One, OneOrMore, ZeroOrMore, ZeroOrOne
3942
from neomodel.sync_.core import (

neomodel/_version.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
__version__ = "5.3.1"
1+
__version__ = "5.3.2"

0 commit comments

Comments
 (0)