Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Filters on related Nodes are applied to each related node individually, not across all related nodes #857

Open
rsyoung96 opened this issue Jan 30, 2025 · 2 comments

Comments

@rsyoung96
Copy link

rsyoung96 commented Jan 30, 2025

Expected Behavior (Mandatory)

When I filter on the properties of a node class related to the target node class, I'd expect that if any of the related nodes match those filters, then it would return the target node.

E.g. I'm looking for dog owners who both own a dog that's >=10 years old AND own a dog that's a Terrier. They don't need to own a Terrier >= 10 years old, they could own two dogs that together meet the criteria.

If there is an alternative syntax (I tried various combinations of subquery, traverse_relations, and filters with Q's), then I'm also happy to use that.

Actual Behavior (Mandatory)

The neomodel filters give me "dog owners who own a dog that is >= 10 years old and is a Terrier", i.e. it enforces that a single node of the related class must meet all the filter conditions.

How to Reproduce the Problem

Run the script below.

Simple Example

import neomodel
from neomodel import (
    StructuredNode, 
    StringProperty, 
    IntegerProperty, 
    RelationshipTo,
    StructuredRel,
)

class Dog(StructuredNode):
    age = IntegerProperty(required=True)
    breed = StringProperty(required=True)

class Human(StructuredNode):
    name = StringProperty(required=True)
    owns_dog = RelationshipTo(Dog, 'OWNS', model=StructuredRel)

neomodel.config.DATABASE_URL = f"bolt://{NEO4J_USER}:{NEO4J_PASSWORD}@{NEO4J_CLUSTER}"
neomodel.config.DATABASE_NAME = "temp"
neomodel.db.set_connection(neomodel.config.DATABASE_URL)

neomodel.db.clear_neo4j_database(clear_constraints=True, clear_indexes=True)
neomodel.install_all_labels()

# Create instances
human1 = Human(name='Alice').save()
dog1 = Dog(age=12, breed='Corgi').save()
dog2 = Dog(age=8, breed='Terrier').save()
human1.owns_dog.connect(dog1)
human1.owns_dog.connect(dog2)

human2 = Human(name='Bob').save()
dog3 = Dog(age=13, breed='Terrier').save()
human2.owns_dog.connect(dog3)

human3 = Human(name='Charlie').save()
dog4 = Dog(age=7, breed='Corgi').save()
human3.owns_dog.connect(dog4)

results = Human.nodes.filter(
    owns_dog__age__gte=10, 
    owns_dog__breed="Terrier"
).all()
print(results)

neomodel.db.close_connection()

It prints:

[[<Human: {'name': 'Bob', 'element_id_property': '4:f0d8eb6f-77af-415b-aa4b-9aeed41a0ca2:36'}>, <Dog: {'age': 13, 'breed': 'Terrier', 'element_id_property': '4:f0d8eb6f-77af-415b-aa4b-9aeed41a0ca2:37'}>, <neomodel.sync_.relationship.StructuredRel object at 0x10a25ffd0>]]

But I want it to additionally return Alice, who owns a Terrier and owns a dog >= 10 years old, as specified by the filter.

To get what I expect, I would need to run:

results = neomodel.db.cypher_query("""
    MATCH (h:Human)-[:OWNS]->(d1:Dog)
    WHERE d1.age >= 10
    MATCH (h)-[:OWNS]->(d2:Dog)
    WHERE d2.breed = "Terrier"
    RETURN DISTINCT h
""", resolve_objects=True)[0]
print(results)

(but in order to support more complicated examples, I want to use neomodel's NodeSet functionality rather than custom cypher)

Specifications (Mandatory)

Currently used versions

Versions

  • OS: MacOS
  • Library: neomodel 5.4.1
  • Neo4j: 5.27.0
@rsyoung96
Copy link
Author

One thing that surprisingly works is to add a second relationship on Human owns_dog2 = RelationshipTo(Dog, 'OWNS', model=StructuredRel), then make the filter:

results = Human.nodes.filter(
    owns_dog__age__gte=10, 
    owns_dog2__breed="Terrier"
).all()

This returns both Alice and Bob, as I wanted the original query to.

This is definitely a hack though, it's not feasible to add extra relationships up to the total number of filters I might want to apply on a relationship.

@mariusconjeaud
Copy link
Collaborator

Hello, I understand your problem, and I see why you thought filter() would work the way you thought, I admit this can lead to confusion.
But it is how I think it is intended : combining two filters inside a single filter() creates an AND, and your case is more subtle than this.

Adding a second relationship definition will make neomodel consider it's two different paths so that makes sense that it works. It DOES feel a bit hacky ; but at the same time, models are not only designed for write operations, but also retrieval, so maybe that can work for you - look at how GraphQL makes some "virtual" properties that are actually traversals.

In any case, I can only think of that option and the custom Cypher option to do what you are trying to do.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants