Neomodel — 2024 Wrap-up


Celebrating 2024

Coming off 2023, when Neomodel came back to active support, then getting increased interest after Py2neo was deleted, 2024 was a confirmation year for the Neomodel library. Would it stagnate or keep improving? Since you’re reading this, I guess you know the answer. 🙂

Let’s look at the highlights from this year’s updates and showcase some examples to help you get started with the newest functionality.

Performance Enhancements

Async Support (5.3.0)

Neomodel introduced async support, enabling non-blocking operations for more scalable applications. Example:

for i in range(n):
await Coffee(name=f"xxx_{i}", price=i).save()

async for node in Coffee.nodes:
print(node)

Support for the Neo4j Rust Driver Extension (5.4.2)

Neomodel now includes support for the Neo4j Python driver Rust extension, which usually speeds up the Python driver. Install it with:

pip install neomodel[rust-driver-ext]

Cypher Parallel Runtime (5.4.1)

With support for the Cypher parallel runtime, Neomodel lets you harness the speed of parallel query execution to handle large datasets more efficiently:

with db.parallel_read_transaction:
# Note : this is a read-only, Enterprise-edition only feature
# It works for both neomodel-generated and custom Cypher queries
parallel_count_1 = len(Coffee.nodes)
parallel_count_2 = db.cypher_query("MATCH (n:Coffee) RETURN count(n)")

Developer Tools

Graph Model Inspection and Diagrams (5.3.1)

Generate visualizations of your graph model using the neomodel_generate_diagram script:

neomodel_generate_diagram --format arrows

This can pair nicely with database inspection through neomodel_inspect_database (released in 5.2.0):

neomodel_inspect_database --db bolt://neo4j_username:neo4j_password@localhost:7687 --write-to yourapp/models.py

Static Typing With mypy (5.4.2)

The Neomodel codebase has received some much-needed housekeeping, with mypy support added to improve static typing. This change brings stronger type-checking and improved code reliability. This is not part of the release validation process yet, but more than 95 percent of the code has been reviewed and typed.

Advanced Querying Capabilities

This is the gem of 2024 for Neomodel.

That’s because this adds a ton of expressive power to Neomodel to start replacing custom Cypher queries with Pythonic code. It’s also worth noting that this was contributed back by the OpenStudyBuilder project team. Always wonderful to see open source going both ways, right?

Filtering and Ordering With Traversals (5.4.0)

If you paid attention in the past, you might have seen a fetch_relations method being added, allowing you to do multi-hop traversals.

Now, you can also filter and order directly within traversals, as well as traverse relations to return the last node instead of everything on the way. For example:

query = MyNode.nodes.filter(hop1__hop2__node_property=value).order_by('-property')

query2 = MyNode.fetch_relations(
"parents",
Optional("children__preferred_course"),
)

Raw Cypher for Ordering (5.4.0)

If you need fine-grained control, you can insert RawCypher for ordering results.

Intermediate Transformations (5.4.0 and 5.4.1)

This allows transforming data mid-query, such as to order intermediate results in a certain way before aggregating.

Variable Transformation With Aggregations (5.4.0)

New methods like Collect() and Last() make it possible to use the corresponding aggregation Cypher methods.

Subqueries With the Cypher CALL {} Clause (5.4.0)

Subqueries are officially supported, making complex query patterns easier to express (see example below).

Nested Subgraph Results (5.4.0)

Complex query results can now be resolved as nested subgraphs, offering cleaner, more intuitive data models.

Vector and Fulltext Index Support (5.3.2)

Neomodel now supports creating vector and fulltext indexes. This is particularly useful for advanced search capabilities.

Example

The example below will show how you can mix and match query operations, as described in Filtering and ordering, Path traversal, or Advanced query operations:

# These are the class definitions used for the query below
class HasCourseRel(AsyncStructuredRel):
level = StringProperty()
start_date = DateTimeProperty()
end_date = DateTimeProperty()


class Course(AsyncStructuredNode):
name = StringProperty()


class Building(AsyncStructuredNode):
name = StringProperty()


class Student(AsyncStructuredNode):
name = StringProperty()

parents = AsyncRelationshipTo("Student", "HAS_PARENT", model=AsyncStructuredRel)
children = AsyncRelationshipFrom("Student", "HAS_PARENT", model=AsyncStructuredRel)
lives_in = AsyncRelationshipTo(Building, "LIVES_IN", model=AsyncStructuredRel)
courses = AsyncRelationshipTo(Course, "HAS_COURSE", model=HasCourseRel)
preferred_course = AsyncRelationshipTo(
Course,
"HAS_PREFERRED_COURSE",
model=AsyncStructuredRel,
cardinality=AsyncZeroOrOne,
)

# This is the query
full_nodeset = (
await Student.nodes.filter(name__istartswith="m", lives_in__name="Eiffel Tower") # Combine filters
.order_by("name")
.fetch_relations(
"parents",
Optional("children__preferred_course"),
) # Combine fetch_relations
.subquery(
Student.nodes.fetch_relations("courses") # Root variable student will be auto-injected here
.intermediate_transform(
{"rel": RelationNameResolver("courses")},
ordering=[
RawCypher("toInteger(split(rel.level, '.')[0])"),
RawCypher("toInteger(split(rel.level, '.')[1])"),
"rel.end_date",
"rel.start_date",
], # Intermediate ordering
)
.annotate(
latest_course=Last(Collect("rel")),
),
["latest_course"],
)
)

# Using async, we need to do 2 await
# One is for subquery, the other is for resolve_subgraph
# It only runs a single Cypher query though
subgraph = await full_nodeset.annotate(
children=Collect(NodeNameResolver("children"), distinct=True),
children_preferred_course=Collect(
NodeNameResolver("children__preferred_course"), distinct=True
),
).resolve_subgraph()

# The generated Cypher query looks like this
query = """
MATCH (student:Student)-[r1:`HAS_PARENT`]->(student_parents:Student)
MATCH (student)-[r4:`LIVES_IN`]->(building_lives_in:Building)
OPTIONAL MATCH (student)<-[r2:`HAS_PARENT`]-(student_children:Student)-[r3:`HAS_PREFERRED_COURSE`]->(course_children__preferred_course:Course)
WITH *
# building_lives_in_name_1 = "Eiffel Tower"
# student_name_1 = "(?i)m.*"
WHERE building_lives_in.name = $building_lives_in_name_1 AND student.name =~ $student_name_1
CALL {
WITH student
MATCH (student)-[r1:`HAS_COURSE`]->(course_courses:Course)
WITH r1 AS rel
ORDER BY toInteger(split(rel.level, '.')[0]),toInteger(split(rel.level, '.')[1]),rel.end_date,rel.start_date
RETURN last(collect(rel)) AS latest_course
}
RETURN latest_course, student, student_parents, r1, student_children, r2, course_children__preferred_course, r3, building_lives_in, r4, collect(DISTINCT student_children) AS children, collect(DISTINCT course_children__preferred_course) AS children_preferred_course
ORDER BY student.name
"""

Why These Updates Matter

From the addition of expressive power for queries to async support and tools to get started faster from a pre-existing database, these updates bring Neomodel closer to the recent new capabilities of Neo4j, making it easier for developers to write clean, efficient, and powerful graph queries.

Get Started With the Latest Version

To upgrade to the latest version of Neomodel, simply run:

pip install --upgrade neomodel

Dive into the documentation to explore these features in detail. And don’t forget to share your thoughts and contributions in the GitHub repository.

What’s your favorite new feature from the 2024 updates? Let us know in the comments!


Neomodel — 2024 Wrap-up was originally published in Neo4j Developer Blog on Medium, where people are continuing the conversation by highlighting and responding to this story.