|
| 1 | +.. _Filtering and ordering: |
| 2 | + |
| 3 | +====================== |
| 4 | +Filtering and ordering |
| 5 | +====================== |
| 6 | + |
| 7 | +For the examples in this section, we will be using the following model:: |
| 8 | + |
| 9 | + class SupplierRel(StructuredRel): |
| 10 | + since = DateTimeProperty(default=datetime.now) |
| 11 | + |
| 12 | + |
| 13 | + class Supplier(StructuredNode): |
| 14 | + name = StringProperty() |
| 15 | + delivery_cost = IntegerProperty() |
| 16 | + |
| 17 | + |
| 18 | + class Coffee(StructuredNode): |
| 19 | + name = StringProperty(unique_index=True) |
| 20 | + price = IntegerProperty() |
| 21 | + suppliers = RelationshipFrom(Supplier, 'SUPPLIES', model=SupplierRel) |
| 22 | + |
| 23 | +Filtering |
| 24 | +========= |
| 25 | + |
| 26 | +neomodel allows filtering on nodes' and relationships' properties. Filters can be combined using Django's Q syntax. It also allows multi-hop relationship traversals to filter on "remote" elements. |
| 27 | + |
| 28 | +Filter methods |
| 29 | +-------------- |
| 30 | + |
| 31 | +The ``.nodes`` property of a class returns all nodes of that type from the database. |
| 32 | + |
| 33 | +This set (called `NodeSet`) can be iterated over and filtered on, using the `.filter` method:: |
| 34 | + |
| 35 | + # nodes with label Coffee whose price is greater than 2 |
| 36 | + high_end_coffees = Coffee.nodes.filter(price__gt=2) |
| 37 | + |
| 38 | + try: |
| 39 | + java = Coffee.nodes.get(name='Java') |
| 40 | + except DoesNotExist: |
| 41 | + # .filter will not throw an exception if no results are found |
| 42 | + # but .get will |
| 43 | + print("Couldn't find coffee 'Java'") |
| 44 | + |
| 45 | +The filter method borrows the same Django filter format with double underscore prefixed operators: |
| 46 | + |
| 47 | +- lt - less than |
| 48 | +- gt - greater than |
| 49 | +- lte - less than or equal to |
| 50 | +- gte - greater than or equal to |
| 51 | +- ne - not equal |
| 52 | +- in - item in list |
| 53 | +- isnull - `True` IS NULL, `False` IS NOT NULL |
| 54 | +- exact - string equals |
| 55 | +- iexact - string equals, case insensitive |
| 56 | +- contains - contains string value |
| 57 | +- icontains - contains string value, case insensitive |
| 58 | +- startswith - starts with string value |
| 59 | +- istartswith - starts with string value, case insensitive |
| 60 | +- endswith - ends with string value |
| 61 | +- iendswith - ends with string value, case insensitive |
| 62 | +- regex - matches a regex expression |
| 63 | +- iregex - matches a regex expression, case insensitive |
| 64 | + |
| 65 | +These operators work with both `.get` and `.filter` methods. |
| 66 | + |
| 67 | +Combining filters |
| 68 | +----------------- |
| 69 | + |
| 70 | +The filter method allows you to combine multiple filters:: |
| 71 | + |
| 72 | + cheap_arabicas = Coffee.nodes.filter(price__lt=5, name__icontains='arabica') |
| 73 | + |
| 74 | +These filters are combined using the logical AND operator. To execute more complex logic (for example, queries with OR statements), `Q objects <neomodel.Q>` can be used. This is borrowed from Django. |
| 75 | + |
| 76 | +``Q`` objects can be combined using the ``&`` and ``|`` operators. Statements of arbitrary complexity can be composed by combining ``Q`` objects |
| 77 | +with the ``&`` and ``|`` operators and use parenthetical grouping. Also, ``Q`` |
| 78 | +objects can be negated using the ``~`` operator, allowing for combined lookups |
| 79 | +that combine both a normal query and a negated (``NOT``) query:: |
| 80 | + |
| 81 | + Q(name__icontains='arabica') | ~Q(name__endswith='blend') |
| 82 | + |
| 83 | +Chaining ``Q`` objects will join them as an AND clause:: |
| 84 | + |
| 85 | + not_middle_priced_arabicas = Coffee.nodes.filter( |
| 86 | + Q(name__icontains='arabica'), |
| 87 | + Q(price__lt=5) | Q(price__gt=10) |
| 88 | + ) |
| 89 | + |
| 90 | +Traversals and filtering |
| 91 | +------------------------ |
| 92 | + |
| 93 | +Sometimes you need to filter nodes based on other nodes they are connected to. This can be done by including a traversal in the `filter` method. :: |
| 94 | + |
| 95 | + # Find all suppliers of coffee 'Java' who have been supplying since 2007 |
| 96 | + # But whose prices are greater than 5 |
| 97 | + since_date = datetime(2007, 1, 1) |
| 98 | + java_old_timers = Coffee.nodes.filter( |
| 99 | + name='Java', |
| 100 | + suppliers__delivery_cost__gt=5, |
| 101 | + **{"suppliers|since__lt": since_date} |
| 102 | + ) |
| 103 | + |
| 104 | +In the example above, note the following syntax elements: |
| 105 | + |
| 106 | +- The name of relationships as defined in the `StructuredNode` class is used to traverse relationships. `suppliers` in this example. |
| 107 | +- Double underscore `__` is used to target a property of a node. `delivery_cost` in this example. |
| 108 | +- A pipe `|` is used to separate the relationship traversal from the property filter. The filter also has to included in a `**kwargs` dictionary, because the pipe character would break the syntax. This is a special syntax to indicate that the filter is on the relationship itself, not on the node at the end of the relationship. |
| 109 | +- The filter operators like lt, gt, etc. can be used on the filtered property. |
| 110 | + |
| 111 | +Traversals can be of any length, with each relationships separated by a double underscore `__`, for example:: |
| 112 | + |
| 113 | + # country is here a relationship between Supplier and Country |
| 114 | + Coffee.nodes.filter(suppliers__country__name='Brazil') |
| 115 | + |
| 116 | +Enforcing relationship/path existence |
| 117 | +------------------------------------- |
| 118 | + |
| 119 | +The `has` method checks for existence of (one or more) relationships, in this case it returns a set of `Coffee` nodes which have a supplier:: |
| 120 | + |
| 121 | + Coffee.nodes.has(suppliers=True) |
| 122 | + |
| 123 | +This can be negated by setting `suppliers=False`, to find `Coffee` nodes without `suppliers`. |
| 124 | + |
| 125 | +You can also filter on the existence of more complex traversals by using the `traverse_relations` method. See :ref:`Path traversal`. |
| 126 | + |
| 127 | +Ordering |
| 128 | +======== |
| 129 | + |
| 130 | +neomodel allows ordering by nodes' and relationships' properties. Order can be ascending or descending. Is also allows multi-hop relationship traversals to order on "remote" elements. Finally, you can inject raw Cypher clauses to have full control over ordering when necessary. |
| 131 | + |
| 132 | +order_by |
| 133 | +-------- |
| 134 | + |
| 135 | +Ordering results by a particular property is done via the `order_by` method:: |
| 136 | + |
| 137 | + # Ascending sort |
| 138 | + for coffee in Coffee.nodes.order_by('price'): |
| 139 | + print(coffee, coffee.price) |
| 140 | + |
| 141 | + # Descending sort |
| 142 | + for supplier in Supplier.nodes.order_by('-delivery_cost'): |
| 143 | + print(supplier, supplier.delivery_cost) |
| 144 | + |
| 145 | + |
| 146 | +Removing the ordering from a previously defined query, is done by passing `None` to `order_by`:: |
| 147 | + |
| 148 | + # Sort in descending order |
| 149 | + suppliers = Supplier.nodes.order_by('-delivery_cost') |
| 150 | + |
| 151 | + # Don't order; yield nodes in the order neo4j returns them |
| 152 | + suppliers = suppliers.order_by(None) |
| 153 | + |
| 154 | +For random ordering simply pass '?' to the order_by method:: |
| 155 | + |
| 156 | + Coffee.nodes.order_by('?') |
| 157 | + |
| 158 | +Traversals and ordering |
| 159 | +----------------------- |
| 160 | + |
| 161 | +Sometimes you need to order results based on properties situated on different nodes or relationships. This can be done by including a traversal in the `order_by` method. :: |
| 162 | + |
| 163 | + # Find the most expensive coffee to deliver |
| 164 | + # Then order by the date the supplier started supplying |
| 165 | + Coffee.nodes.order_by( |
| 166 | + '-suppliers__delivery_cost', |
| 167 | + 'suppliers|since', |
| 168 | + ) |
| 169 | + |
| 170 | +In the example above, note the following syntax elements: |
| 171 | + |
| 172 | +- The name of relationships as defined in the `StructuredNode` class is used to traverse relationships. `suppliers` in this example. |
| 173 | +- Double underscore `__` is used to target a property of a node. `delivery_cost` in this example. |
| 174 | +- A pipe `|` is used to separate the relationship traversal from the property filter. This is a special syntax to indicate that the filter is on the relationship itself, not on the node at the end of the relationship. |
| 175 | + |
| 176 | +Traversals can be of any length, with each relationships separated by a double underscore `__`, for example:: |
| 177 | + |
| 178 | + # country is here a relationship between Supplier and Country |
| 179 | + Coffee.nodes.order_by('suppliers__country__latitude') |
| 180 | + |
| 181 | +RawCypher |
| 182 | +--------- |
| 183 | + |
| 184 | +When you need more advanced ordering capabilities, for example to apply order to a transformed property, you can use the `RawCypher` method, like so:: |
| 185 | + |
| 186 | + from neomodel.sync_.match import RawCypher |
| 187 | + |
| 188 | + class SoftwareDependency(AsyncStructuredNode): |
| 189 | + name = StringProperty() |
| 190 | + version = StringProperty() |
| 191 | + |
| 192 | + SoftwareDependency(name="Package2", version="1.4.0").save() |
| 193 | + SoftwareDependency(name="Package3", version="2.5.5").save() |
| 194 | + |
| 195 | + latest_dep = SoftwareDependency.nodes.order_by( |
| 196 | + RawCypher("toInteger(split($n.version, '.')[0]) DESC"), |
| 197 | + ) |
| 198 | + |
| 199 | +In the example above, note the `$n` placeholder in the `RawCypher` clause. This is a placeholder for the node being ordered (`SoftwareDependency` in this case). |
0 commit comments