How to Create Unique Indexes and Constraints on Properties in neomodel

Use unique_index=True when defining a Property in your StructuredNode or StructuredRel class, then run db.install_labels() to generate the corresponding CREATE CONSTRAINT ... IS UNIQUE statement in Neo4j.

Neomodel, the Python Object-Graph Mapper (OGM) for Neo4j maintained by the neo4j-contrib organization, provides a declarative way to enforce data integrity through unique constraints. By setting unique_index=True on a property definition, you instruct neomodel to create a database-level unique constraint rather than a standard index, ensuring no two nodes or relationships share the same value for that property.

Understanding Unique Constraints vs Indexes in neomodel

In neomodel, the terms "unique index" and "unique constraint" refer to the same database artifact. When you declare a property with unique_index=True, neomodel generates a Neo4j IS UNIQUE constraint, which both enforces uniqueness and creates an index for fast lookups.

Important: The unique_index and index parameters are mutually exclusive. In neomodel/properties.py, the Property base class validates this during initialization:


# neomodel/properties.py

if unique_index and index:
    raise ValueError(
        "The arguments `unique_index` and `index` are mutually exclusive."
    )

You must choose one or the other: use index=True for non-unique indexes and unique_index=True for unique constraints.

Creating Unique Constraints on Node Properties

Defining Unique Properties in Your Model

To enforce uniqueness on a node property, subclass StructuredNode and set unique_index=True on the desired Property:

from neomodel import StructuredNode, StringProperty, UniqueIdProperty

class User(StructuredNode):
    # Automatically generates a unique constraint on the uid property

    uid = UniqueIdProperty()
    
    # Explicit unique constraint on email

    email = StringProperty(unique_index=True, required=True)
    username = StringProperty()

The UniqueIdProperty is a convenience class that automatically sets unique_index=True and generates a UUID, but you can apply unique_index=True to any property type including StringProperty, IntegerProperty, etc.

Installing Constraints to Neo4j

Defining the model only updates the Python class; you must explicitly install the schema to the database. Neomodel provides three ways to trigger constraint creation:

Method 1: Install specific labels

from neomodel import db

# Connect to Neo4j first

db.set_connection('bolt://neo4j:password@localhost:7687')

# Install constraints for the User model only

db.install_labels(User)

Method 2: Install all labels


# Discover and install all StructuredNode subclasses

db.install_all_labels()

Method 3: Command-line interface

python -m neomodel.scripts.neomodel_install_labels \
    path/to/models.py path/to/other_models.py

When you run any of these commands, neomodel executes Cypher statements like:

CREATE CONSTRAINT constraint_unique_User_email
FOR (n:User) REQUIRE n.email IS UNIQUE

If the constraint already exists, neomodel catches the ClientError and continues silently, making the operation idempotent.

Creating Unique Constraints on Relationship Properties (Neo4j 5.7+)

Neo4j only supports unique constraints on relationship properties starting from version 5.7. Neomodel checks the server version during schema installation and raises FeatureNotSupported if you attempt to create a unique constraint on a relationship property in earlier versions.

To define a unique constraint on a relationship property, pass unique_index=True to the property definition within the RelationshipTo or RelationshipFrom declaration:

from neomodel import StructuredNode, RelationshipTo, StringProperty

class City(StructuredNode):
    name = StringProperty()

class Country(StructuredNode):
    name = StringProperty(unique_index=True)
    # Unique constraint on the 'code' property of the LOCATED_IN relationship

    cities = RelationshipTo(City, "LOCATED_IN", 
                           code=StringProperty(unique_index=True))

When you run db.install_all_labels() on Neo4j 5.7+, neomodel executes:

CREATE CONSTRAINT constraint_unique_LOCATED_IN_code
FOR ()-[r:LOCATED_IN]-() REQUIRE r.code IS UNIQUE

On older Neo4j versions, the installation fails with a clear error message indicating that relationship property uniqueness constraints require Neo4j 5.7 or later.

How Unique Constraints Work Under the Hood

Understanding the implementation details helps debug schema installation issues and optimize your data model.

Property Definition Validation

In neomodel/properties.py, the Property class constructor (lines 91-99 and 146-148) validates that you cannot combine index and unique_index:


# From neomodel/properties.py

class Property:
    def __init__(self, unique_index=False, index=False, **kwargs):
        if unique_index and index:
            raise ValueError(
                "The arguments `unique_index` and `index` are mutually exclusive."
            )
        self.unique_index = unique_index
        # ... additional initialization

Schema Installation Logic

The Database class in neomodel/sync_/database.py (and its async counterpart in neomodel/async_/database.py) handles the actual constraint creation:

  1. _install_node() (around line 1322) iterates through node properties and calls _create_node_constraint() for any property with unique_index=True.

  2. _create_node_constraint() generates the Cypher:

    label = target_cls.__label__
    constraint_name = f"constraint_unique_{label}_{property_name}"
    self.cypher_query(
        f"""CREATE CONSTRAINT {constraint_name}
                    FOR (n:{label}) REQUIRE n.{property_name} IS UNIQUE"""
    )
  3. Error handling: The method catches ClientError to handle cases where the constraint already exists, making the operation safe to run multiple times.

For relationships, _install_relationship() performs a version check before calling _create_relationship_constraint(), ensuring compatibility with Neo4j 5.7+.

Summary

  • Use unique_index=True on any Property subclass to enforce uniqueness at the database level.
  • Constraints are not created automatically; you must run db.install_labels(Model) or db.install_all_labels() to apply them to Neo4j.
  • Mutual exclusivity: You cannot use unique_index=True and index=True on the same property.
  • Relationship constraints require Neo4j 5.7 or later; neomodel raises FeatureNotSupported on older versions.
  • Idempotent operation: Running the installation script multiple times is safe—existing constraints are silently skipped.

Frequently Asked Questions

What is the difference between index=True and unique_index=True in neomodel?

The index=True parameter creates a standard Neo4j index that speeds up lookups but allows duplicate values. The unique_index=True parameter creates a unique constraint that both indexes the property and enforces that no two nodes or relationships can share the same value. According to the source code in neomodel/properties.py, these two options are mutually exclusive—you must choose one or the other.

How do I apply unique constraints to an existing Neo4j database?

After defining or modifying your neomodel classes to include unique_index=True on the desired properties, connect to your database using db.set_connection() and run db.install_labels(YourModel) for specific models or db.install_all_labels() to apply all constraints. This executes the necessary CREATE CONSTRAINT Cypher statements. The operation is idempotent, so running it multiple times will not cause errors if the constraints already exist.

Can I create unique constraints on relationship properties in neomodel?

Yes, but only if you are running Neo4j version 5.7 or later. To define a unique constraint on a relationship property, pass unique_index=True to the property definition within your RelationshipTo or RelationshipFrom declaration. When you run the schema installation, neomodel checks the server version in neomodel/sync_/database.py and raises FeatureNotSupported if the database is older than 5.7. For supported versions, it creates the constraint using CREATE CONSTRAINT ... FOR ()-[r:REL_TYPE]-() REQUIRE r.property IS UNIQUE.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →