neomodel

repository·master·Indexed 22 days ago

https://github.com/neo4j-contrib/neomodel

An Object Graph Mapper (OGM) for Neo4j built on the official neo4j-python-driver. It provides class-based model definitions, schema enforcement, and a query API for managing graph data in Python. Features include support for native semantic indexes (Vector and Full-text) in version 6.0.0, batch node creation, and atomic operations like create_or_update and get_or_create.

Tokens
41.6K
Snippets
140
Records
184
Agent score
76%

What's inside neomodel

  1. New features in neomodel 6.0

    master

    Version 6.0 introduces several significant improvements:

    • SemVer: neomodel now uses Semantic Versioning (major.minor.patch).
    • Modern Configuration: A new configuration system using dataclasses with typing, runtime/update validation, and environment variable support.
    • Batch Operations: The merge_by parameter is now available for batch operations to customize merge behavior (label and property keys).
  2. New features in neomodel 6.0.0

    master

    Version 6.0.0 introduced several major changes:

    • SemVer Versioning: neomodel now follows Semantic Versioning (major.minor.patch).
    • Modern Configuration System: Uses a dataclass with typing, runtime and update validation rules, and environment variable support.
    • Native Semantic Indexes: Supports Vector and Full-text semantic indexes natively without requiring custom Cypher queries.
  3. Enforce cardinality constraints on relationships

    master

    You can enforce cardinality (how many connections are allowed) on your relationships. Note: Cardinality must be declared on both sides of the relationship definition to be effective.

    Available constraints:

    • ZeroOrOne
    • One
    • ZeroOrMore (default)
    • OneOrMore

    If a constraint is violated by existing data, a neomodel.exception.CardinalityViolation is raised.

    Soft Checks: For development, you can enable config.soft_cardinality_check = True. This will print a warning to the console instead of raising an exception, allowing the relationship to be created anyway.

    from neomodel import StructuredNode, RelationshipTo, RelationshipFrom, One
    
    class Person(StructuredNode):
        car = RelationshipTo('Car', 'OWNS', cardinality=One)
    
    class Car(StructuredNode):
        owner = RelationshipFrom('Person', 'OWNS', cardinality=One)
  4. Define Node Entities and Relationships

    master

    Nodes are defined by subclassing StructuredNode. Data members intended for storage must be defined using neomodel property objects (e.g., StringProperty, IntegerProperty, UniqueIdProperty).

    Relationships are defined using RelationshipTo, RelationshipFrom, or Relationship objects.

    • RelationshipTo and RelationshipFrom specify a direction for traversal.
    • Use Relationship for bi-directional relationships to avoid defining two complementary relationships in Python.

    neomodel automatically creates a label for each StructuredNode class in the database along with any specified indexes and constraints.

    from neomodel import (get_config, StructuredNode, StringProperty, IntegerProperty, 
        UniqueIdProperty, RelationshipTo)
    
    config = get_config()
    config.database_url = 'bolt://neo4j_username:neo4j_password@localhost:7687'
    
    class Country(StructuredNode):
        code = StringProperty(unique_index=True, required=True)
    
    class City(StructuredNode):
        name = StringProperty(required=True)
        country = RelationshipTo(Country, 'FROM_COUNTRY')
    
    class Person(StructuredNode):
        uid = UniqueIdProperty()
        name = StringProperty(unique_index=True)
        age = IntegerProperty(index=True, default=0)
    
        country = RelationshipTo(Country, 'IS_FROM')
        city = RelationshipTo(City, 'LIVES_IN')
  5. Limitations of Semantic Indexing in neomodel

    master

    As of the current version, there are limitations regarding semantic indexes in the OGM:

    1. Relationship Querying: There is currently no OGM method to query Full Text or Vector indexes on relationships. To query relationship indexes, you must use db.cypher_query with manual Cypher syntax.
    2. Filter Combinations: When combining FulltextFilter or VectorFilter with standard filters, the result set is the intersection. This means you might receive fewer than the requested topk results if the other filters are restrictive.
    3. Score Return Values: When using semantic filters in conjunction with relationship filters, the similarity score is not returned alongside the relationship; only the topk nodes and their relationships are returned.
  6. Configure custom merge keys with merge_by

    master

    You can override the default matching behavior of create_or_update() and get_or_create() using the merge_by parameter. This is useful when you want to match nodes based on specific properties (even non-required ones) or a specific label.

    The merge_by parameter accepts a dictionary with:

    • label: The Neo4j label to match against (optional; defaults to the node's inherited labels).
    • keys: A list of property names to use as the merge key(s).

    Example Configurations:

    • Single key: merge_by={'keys': ['email']}
    • Multiple keys: merge_by={'keys': ['username', 'email']}
    • Specific label and keys: merge_by={'label': 'User', 'keys': ['username']}
    # Custom merge by email only
    users = User.create_or_update({
        'username': 'johndoe',
        'email': 'john@example.com',
        'age': 31
    }, merge_by={'keys': ['email']})
    
    # Custom merge by username only with a specific label
    users = User.create_or_update({
        'username': 'johndoe',
        'email': 'john.doe@newcompany.com',
        'age': 32
    }, merge_by={'label': 'User', 'keys': ['username']})
  7. Understand neomodel behavior in multi-threaded and multi-process environments

    master

    neomodel manages a mapping between Neo4j labels and Python classes (the node-class registry) and transaction information. Understanding how this behaves across threads and processes is critical for cluster environments and parallel testing.

    Multi-threading (Single Process)

    • Registry: All threads within the same process share the same node-class registry.
    • Sessions: All threads in a single process share the same session.
    • Transactions:
      • Multiple calls to transaction functions within the same thread will re-use an existing transaction.
      • Separate threads can start different transactions, but they all execute within the same session.
    • Concurrency: Because they share a session, parallel WRITE transactions in multiple threads do not provide performance gains.

    Multi-processing (Multiple Processes)

    • Isolation: Each process is independent. A new process must re-initialize the neomodel.db object and re-import application-specific models.
    • Sessions: Each process starts its own unique session with the Neo4j DBMS.
    • Concurrency: To achieve parallel WRITE performance, use multiple processes. However, because transactions across different sessions are unaware of each other, you must manually handle potential conflicts (e.g., uniqueness constraint violations).

    Summary Table for Parallelism

    ScenarioShared Registry?Shared Session?Best Use Case
    Multiple ThreadsYesYesParallel READ queries (using bolt+routing:)
    Multiple ProcessesNoNoParallel WRITE operations (requires conflict handling)
  8. How Node Inheritance works with relationships

    master

    Neomodel can resolve derived (subclassed) nodes at the endpoints of a relationship. If a relationship is defined to point to a BaseClass, and you connect a DerivedClass instance, neomodel will correctly instantiate the object as a DerivedClass when traversing the relationship.

    This allows for polymorphic relationships where a single relationship type can connect various specialized node types that share a common base.

    class BasePerson(StructuredNode):
        name = StringProperty(required=True, unique_index=True)
        friends_with = RelationshipTo("BasePerson", "FRIENDS_WITH", model=PersonalRelationship)
    
    class TechnicalPerson(BasePerson):
        expertise = StringProperty(required=True)
    
    class PilotPerson(BasePerson):
        airplane = StringProperty(required=True)
    
    # A TechnicalPerson can befriend a PilotPerson
    a = TechnicalPerson(name="Grumpy", expertise="Grumpiness").save()
    d = PilotPerson(name="Porco Rosso", airplane="Savoia").save()
    a.friends_with.connect(d)
    
    # Traversing will return the correct specialized type
    for friend in a.friends_with:
        print(type(friend)) # <class '...PilotPerson'>
  9. Resolve Cypher variables with NodeNameResolver and RelationshipNameResolver

    master

    When you cannot set explicit aliases (for example, when using fetch_relations), you can use Resolver objects to reference the generated Cypher variables.

    • NodeNameResolver(name): Resolves to the variable name of the node in the traversal. If used in a traversal, it resolves to the last node in that traversal.
    • RelationshipNameResolver(name): Resolves to the variable name of the relationship in the traversal.
    • NodeNameResolver("self"): Resolves to the root node of the query.

    This is particularly useful for annotate() calls where you need to reference variables created by fetch_relations or complex traversals.

    from neomodel.sync_match import Collect, NodeNameResolver, RelationshipNameResolver
    
    # Using resolvers to reference variables from fetch_relations
    Supplier.nodes.fetch_relations("coffees__species")\n    .annotate(\n        all_species=Collect(NodeNameResolver("coffees__species"), distinct=True),\n        all_species_rels=Collect(\n            RelationNameResolver("coffees__species"), distinct=True\n        ),\n    )\n    .all()
  10. Understand Automatic Class Resolution and the Node-Class Registry

    master

    Neomodel uses a node-class registry (a dictionary mapping sets of labels to classes) to automatically transform database nodes into native Python objects.

    How to use it

    Automatic resolution is triggered when calling neomodel.Database.cypher_query with the parameter resolve_objects=True (which is the default behavior for certain query methods).

    Common Errors

    1. neomodel.exceptions.ModelDefinitionMismatch: Occurs if a query returns a node whose class definition has not yet been imported into the current execution context. Ensure all classes in a hierarchy are imported before running queries.
    2. neomodel.exceptions.NodeClassAlreadyDefined: Occurs if a class is redefined. This prevents the registry from having ambiguous mappings. This can be bypassed in development using config.ALLOW_RELOAD = True.
  11. Manage asynchronous relationships and cardinality

    master

    Neomodel provides an asynchronous API for handling graph relationships. You can use the following modules to manage how nodes connect and the constraints on those connections:

    • neomodel.async_.relationship: Defines and manages relationship types.
    • neomodel.async_.relationship_manager: Handles the logic for creating and traversing relationships.
    • neomodel.async_.cardinality: Defines constraints on the number of relationships (e.g., One-to-One, One-to-Many).
  12. Filter by relationship properties and traversals

    master

    You can filter nodes based on properties of connected nodes or the relationships themselves.

    Traversing to remote nodes

    Use the relationship name followed by double underscores to reach properties on connected nodes: relationship__property. Example: Coffee.nodes.filter(suppliers__country__name='Brazil')

    Filtering on relationship properties

    To filter on a property belonging to the relationship itself (rather than the target node), use a pipe | in the key. Because the pipe character is invalid in standard Python keyword arguments, you must pass these filters as a dictionary using **kwargs.

    Syntax: **{"{relationship_name}|{rel_property_name}": value}

    # Find coffee 'Java' where the relationship 'suppliers' has a 'since' property < 2007
    # and the connected Supplier has a 'delivery_cost' > 5
    since_date = datetime(2007, 1, 1)
    java_old_timers = Coffee.nodes.filter(
        name='Java',
        suppliers__delivery_cost__gt=5,
        **{"suppliers|since__lt": since_date}
    )