Skip to content

Commit

Permalink
Add Advanced query operations ; add interlinks
Browse files Browse the repository at this point in the history
  • Loading branch information
mariusconjeaud committed Oct 25, 2024
1 parent f66c653 commit 3da6102
Show file tree
Hide file tree
Showing 7 changed files with 144 additions and 293 deletions.
103 changes: 103 additions & 0 deletions doc/source/advanced_query_operations.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
.. _Advanced query operations:

=========================
Advanced query operations
=========================

neomodel provides ways to enhance your queries beyond filtering and traversals.

Annotate - Aliasing
-------------------

The `annotate` method allows you to add transformations to your elements. To learn more about the available transformations, keep reading this section.

Aggregations
------------

neomodel implements some of the aggregation methods available in Cypher:

- Collect
- Last

These are usable in this way::

from neomodel.sync_match import Collect, Last

This comment has been minimized.

Copy link
@tonioo

tonioo Oct 28, 2024

Collaborator

neomodel.sync_.match


# distinct is optional, and defaults to False. When true, objects are deduplicated
Supplier.nodes.traverse_relations(available_species="coffees__species")
.annotate(Collect("available_species", distinct=True))
.all()

# Last is used to get the last element of a list
Supplier.nodes.traverse_relations(available_species="coffees__species")
.annotate(Last(Collect("last_species")))
.all()

.. note::
Using the Last() method right after a Collect() without having set an ordering will return the last element in the list as it was returned by the database.

This is probably not what you want ; which means you must provide an explicit ordering. To do so, you cannot neomodel's order_by method, but need an intermediate transformation step (see below).

This comment has been minimized.

Copy link
@tonioo

tonioo Oct 28, 2024

Collaborator

you cannot use


This is because the order_by method adds ordering as the very last step of the Cypher query ; whereas in the present example, you want to first order Species, then get the last one, and then finally return your results. In other words, you need an intermediate WITH Cypher clause.

Intermediate transformations
----------------------------

The `intermediate_transform` method basically allows you to add a WITH clause to your query. This is useful when you need to perform some operations on your results before returning them.

As discussed in the note above, this is for example useful when you need to order your results before applying an aggregation method, like so::

from neomodel.sync_match import Collect, Last

This comment has been minimized.

Copy link
@tonioo

tonioo Oct 28, 2024

Collaborator

neomodel.sync_.match


# This will return all Coffee nodes, with their most expensive supplier
Coffee.nodes.traverse_relations(suppliers="suppliers")
.intermediate_transform(
{"suppliers": "suppliers"}, ordering=["suppliers.delivery_cost"]
)
.annotate(supps=Last(Collect("suppliers")))

Subqueries
----------

The `subquery` method allows you to perform a `Cypher subquery <https://neo4j.com/docs/cypher-manual/current/subqueries/call-subquery/>`_ inside your query. This allows you to perform operations in isolation to the rest of your query::

from neomodel.sync_match import Collect, Last
# This will create a CALL{} subquery
# And return a variable named supps usable in the rest of your query
Coffee.nodes.subquery(
Coffee.nodes.traverse_relations(suppliers="suppliers")
.intermediate_transform(
{"suppliers": "suppliers"}, ordering=["suppliers.delivery_cost"]
)
.annotate(supps=Last(Collect("suppliers"))),
["supps"],
)

Helpers
-------

Reading the sections above, you may have noticed that we used explicit aliasing in the examples, as in::

traverse_relations(suppliers="suppliers")

This allows you to reference the generated Cypher variables in your transformation steps, for example::

traverse_relations(suppliers="suppliers").annotate(Collect("suppliers"))

In some cases though, it is not possible to set explicit aliases, for example when using `fetch_relations`. In these cases, neomodel provides `resolver` methods, so you do not have to guess the name of the variable in the generated Cypher. Those are `NodeNameResolver` and `RelationshipNameResolver`. For example::

from neomodel.sync_match import Collect, NodeNameResolver, RelationshipNameResolver

Supplier.nodes.fetch_relations("coffees__species")
.annotate(
all_species=Collect(NodeNameResolver("coffees__species"), distinct=True),
all_species_rels=Collect(
RelationNameResolver("coffees__species"), distinct=True
),
)
.all()

.. note::

When using the resolvers in combination with a traversal as in the example above, it will resolve the variable name of the last element in the traversal - the Species node for NodeNameResolver, and Coffee--Species relationship for RelationshipNameResolver.
12 changes: 12 additions & 0 deletions doc/source/cypher.rst
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,18 @@ Outside of a `StructuredNode`::

The ``resolve_objects`` parameter automatically inflates the returned nodes to their defined classes (this is turned **off** by default). See :ref:`automatic_class_resolution` for details and possible pitfalls.

You canalso retrieve a whole path of already instantiated objects corresponding to

This comment has been minimized.

Copy link
@tonioo

tonioo Oct 28, 2024

Collaborator

You can also (missing space)

the nodes and relationship classes with a single query::

q = db.cypher_query("MATCH p=(:CityOfResidence)<-[:LIVES_IN]-(:PersonOfInterest)-[:IS_FROM]->(:CountryOfOrigin) RETURN p LIMIT 1",
resolve_objects = True)

Notice here that ``resolve_objects`` is set to ``True``. This results in ``q`` being a
list of ``result, result_name`` and ``q[0][0][0]`` being a ``NeomodelPath`` object.

``NeomodelPath`` ``nodes, relationships`` attributes contain already instantiated objects of the
nodes and relationships in the query, *in order of appearance*.

Integrations
============

Expand Down
2 changes: 1 addition & 1 deletion doc/source/filtering_ordering.rst
Original file line number Diff line number Diff line change
Expand Up @@ -120,7 +120,7 @@ The `has` method checks for existence of (one or more) relationships, in this ca

This can be negated by setting `suppliers=False`, to find `Coffee` nodes without `suppliers`.

You can also filter on the existence of more complex traversals by using the `traverse_relations` method. (ADD LINK TO DOC)
You can also filter on the existence of more complex traversals by using the `traverse_relations` method. See :ref:`Path traversal`.

Ordering
========
Expand Down
54 changes: 22 additions & 32 deletions doc/source/getting_started.rst
Original file line number Diff line number Diff line change
Expand Up @@ -193,6 +193,28 @@ simply returning the node IDs rather than every attribute associated with that N
# Return set of nodes
people = Person.nodes.filter(age__gt=3)

Iteration, slicing and more
---------------------------

Iteration, slicing and counting is also supported::

# Iterable
for coffee in Coffee.nodes:
print coffee.name

# Sliceable using python slice syntax
coffee = Coffee.nodes.filter(price__gt=2)[2:]

The slice syntax returns a NodeSet object which can in turn be chained.

Length and boolean methods do not return NodeSet objects and cannot be chained further::

# Count with __len__
print len(Coffee.nodes.filter(price__gt=2))

if Coffee.nodes:
print "We have coffee nodes!"

Relationships
=============

Expand Down Expand Up @@ -236,38 +258,6 @@ Working with relationships::
Retrieving additional relations
===============================

To avoid queries multiplication, you have the possibility to retrieve
additional relations with a single call::

# The following call will generate one MATCH with traversal per
# item in .fetch_relations() call
results = Person.nodes.fetch_relations('country').all()
for result in results:
print(result[0]) # Person
print(result[1]) # associated Country

You can traverse more than one hop in your relations using the
following syntax::

# Go from person to City then Country
Person.nodes.fetch_relations('city__country').all()

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

from neomodel.match import Optional

results = Person.nodes.fetch_relations(Optional('country')).all()

.. note::

Any relationship that you intend to traverse using this method **MUST have a model defined**, even if only the default StructuredRel, like::
class Person(StructuredNode):
country = RelationshipTo(Country, 'IS_FROM', model=StructuredRel)

Otherwise, neomodel will not be able to determine which relationship model to resolve into, and will fail.

.. note::

You can fetch one or more relations within the same call
Expand Down
4 changes: 3 additions & 1 deletion doc/source/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,9 @@ Contents
properties
spatial_properties
schema_management
queries
filtering_ordering
traversal
advanced_query_operations
cypher
transactions
hooks
Expand Down
Loading

0 comments on commit 3da6102

Please sign in to comment.