Skip to main content

Indexes & Constraints

Learn how to create and manage indexes and constraints to optimize query performance and ensure data integrity in your graph database.

Quick Example

// Create a unique index on user emails
await ductape.graph.createNodeIndex({
name: 'idx_user_email',
type: NodeIndexType.BTREE,
label: 'User',
properties: ['email'],
});

// Create a uniqueness constraint
await ductape.graph.createNodeConstraint({
name: 'unique_user_email',
type: NodeConstraintType.UNIQUE,
label: 'User',
properties: ['email'],
});

// List all indexes
const indexes = await ductape.graph.listIndexes();
console.log('Indexes:', indexes.indexes.length);

Why Indexes Matter

Indexes dramatically improve query performance:

Without Index:

// Scans all nodes in database (slow)
const user = await ductape.graph.findNodes({
labels: ['User'],
where: { email: 'alice@example.com' },
});
// Could take seconds with millions of nodes

With Index:

// Uses index for instant lookup (fast)
const user = await ductape.graph.findNodes({
labels: ['User'],
where: { email: 'alice@example.com' },
});
// Milliseconds even with millions of nodes

Node Indexes

Create Node Index

import { NodeIndexType } from '@ductape/sdk';

const result = await ductape.graph.createNodeIndex({
name: 'idx_user_email',
type: NodeIndexType.BTREE,
label: 'User',
properties: ['email'],
});

console.log('Index created:', result.created);

Index Types

BTREE (Default)

Best for most queries with equality and range conditions:

await ductape.graph.createNodeIndex({
name: 'idx_product_price',
type: NodeIndexType.BTREE,
label: 'Product',
properties: ['price'],
});

// Efficiently supports:
// - Equality: { price: 99.99 }
// - Range: { price: { $GTE: 50, $LTE: 100 } }
// - Sorting: orderBy price

FULLTEXT

For text search capabilities:

await ductape.graph.createNodeIndex({
name: 'idx_article_content',
type: NodeIndexType.FULLTEXT,
label: 'Article',
properties: ['title', 'content'],
});

// Use with full-text search
const results = await ductape.graph.fullTextSearch({
label: 'Article',
query: 'graph databases',
properties: ['title', 'content'],
});

RANGE

Optimized for range queries (Neo4j):

await ductape.graph.createNodeIndex({
name: 'idx_user_age',
type: NodeIndexType.RANGE,
label: 'User',
properties: ['age'],
});

// Optimized for range conditions
const adults = await ductape.graph.findNodes({
labels: ['User'],
where: { age: { $GTE: 18, $LTE: 65 } },
});

TEXT

For string pattern matching:

await ductape.graph.createNodeIndex({
name: 'idx_user_name',
type: NodeIndexType.TEXT,
label: 'User',
properties: ['name'],
});

// Efficient for CONTAINS, STARTS_WITH, ENDS_WITH
const users = await ductape.graph.findNodes({
labels: ['User'],
where: {
name: { $CONTAINS: 'John' },
},
});

POINT (Spatial)

For geospatial queries:

await ductape.graph.createNodeIndex({
name: 'idx_store_location',
type: NodeIndexType.POINT,
label: 'Store',
properties: ['location'],
});

// Use for spatial queries
const nearbyStores = await ductape.graph.findNodes({
labels: ['Store'],
where: {
location: {
$NEAR: { lat: 37.7749, lon: -122.4194, distance: 5000 },
},
},
});

Composite Indexes

Index multiple properties together:

await ductape.graph.createNodeIndex({
name: 'idx_user_city_age',
type: NodeIndexType.BTREE,
label: 'User',
properties: ['city', 'age'],
});

// Efficient for queries filtering both properties
const users = await ductape.graph.findNodes({
labels: ['User'],
where: {
city: 'San Francisco',
age: { $GTE: 25 },
},
});

Index Options

await ductape.graph.createNodeIndex({
name: 'idx_custom',
type: NodeIndexType.BTREE,
label: 'Product',
properties: ['category'],
options: {
// Database-specific options
indexProvider: 'native-btree-1.0',
indexConfig: {
'spatial.cartesian.min': [-100.0, -100.0],
'spatial.cartesian.max': [100.0, 100.0],
},
},
});

Relationship Indexes

Create Relationship Index

import { RelationshipIndexType } from '@ductape/sdk';

await ductape.graph.createRelationshipIndex({
name: 'idx_friendship_since',
type: RelationshipIndexType.BTREE,
relationshipType: 'FRIENDS_WITH',
properties: ['since'],
});

// Now efficient to query by relationship properties
const recentFriends = await ductape.graph.findRelationships({
type: 'FRIENDS_WITH',
where: {
since: { $GTE: new Date('2024-01-01') },
},
});

Relationship Index Types

// BTREE for general queries
await ductape.graph.createRelationshipIndex({
name: 'idx_order_total',
type: RelationshipIndexType.BTREE,
relationshipType: 'PURCHASED',
properties: ['total', 'date'],
});

// RANGE for range queries
await ductape.graph.createRelationshipIndex({
name: 'idx_weight',
type: RelationshipIndexType.RANGE,
relationshipType: 'INFLUENCES',
properties: ['weight'],
});

Node Constraints

Constraints enforce data integrity and automatically create indexes.

Unique Constraint

Ensures property values are unique:

import { NodeConstraintType } from '@ductape/sdk';

await ductape.graph.createNodeConstraint({
name: 'unique_user_email',
type: NodeConstraintType.UNIQUE,
label: 'User',
properties: ['email'],
});

// Prevents duplicate emails
try {
await ductape.graph.createNode({
labels: ['User'],
properties: { email: 'alice@example.com' },
});

// This will fail - email already exists
await ductape.graph.createNode({
labels: ['User'],
properties: { email: 'alice@example.com' },
});
} catch (error) {
console.log('Constraint violation:', error.message);
}

Existence Constraint

Ensures properties must exist:

await ductape.graph.createNodeConstraint({
name: 'user_email_exists',
type: NodeConstraintType.EXISTS,
label: 'User',
properties: ['email'],
});

// This will fail - email is required
try {
await ductape.graph.createNode({
labels: ['User'],
properties: { name: 'Bob' }, // Missing email
});
} catch (error) {
console.log('Email is required');
}

Node Key Constraint

Composite uniqueness across multiple properties:

await ductape.graph.createNodeConstraint({
name: 'unique_product_sku_vendor',
type: NodeConstraintType.NODE_KEY,
label: 'Product',
properties: ['sku', 'vendor'],
});

// SKU must be unique per vendor
// Same SKU can exist for different vendors

Relationship Constraints

Unique Relationship Constraint

import { RelationshipConstraintType } from '@ductape/sdk';

await ductape.graph.createRelationshipConstraint({
name: 'unique_follows',
type: RelationshipConstraintType.UNIQUE,
relationshipType: 'FOLLOWS',
properties: ['userId', 'followedId'],
});

// Prevents duplicate follow relationships

Relationship Property Existence

await ductape.graph.createRelationshipConstraint({
name: 'purchase_requires_date',
type: RelationshipConstraintType.EXISTS,
relationshipType: 'PURCHASED',
properties: ['date'],
});

// All PURCHASED relationships must have a date property

Managing Indexes

List All Indexes

const result = await ductape.graph.listIndexes();

console.log(`Total indexes: ${result.indexes.length}`);

result.indexes.forEach(index => {
console.log(`${index.name} on ${index.label || index.relationshipType}`);
console.log(` Type: ${index.type}`);
console.log(` Properties: ${index.properties.join(', ')}`);
console.log(` State: ${index.state}`);
});

Drop Index

const result = await ductape.graph.dropIndex('idx_user_email');

if (result.dropped) {
console.log('Index dropped successfully');
}

List All Constraints

const result = await ductape.graph.listConstraints();

console.log(`Total constraints: ${result.constraints.length}`);

result.constraints.forEach(constraint => {
console.log(`${constraint.name}: ${constraint.type}`);
console.log(` On: ${constraint.label || constraint.relationshipType}`);
console.log(` Properties: ${constraint.properties.join(', ')}`);
});

Drop Constraint

const result = await ductape.graph.dropConstraint('unique_user_email');

if (result.dropped) {
console.log('Constraint dropped successfully');
}

Best Practices

1. Index Frequently Queried Properties

// Good - index properties used in WHERE clauses
await ductape.graph.createNodeIndex({
name: 'idx_user_email',
type: NodeIndexType.BTREE,
label: 'User',
properties: ['email'],
});

// Also index properties used in JOINs/traversals
await ductape.graph.createNodeIndex({
name: 'idx_user_id',
type: NodeIndexType.BTREE,
label: 'User',
properties: ['userId'],
});

2. Use Constraints for Data Integrity

// Prevent duplicate emails at database level
await ductape.graph.createNodeConstraint({
name: 'unique_user_email',
type: NodeConstraintType.UNIQUE,
label: 'User',
properties: ['email'],
});

// Ensure critical properties always exist
await ductape.graph.createNodeConstraint({
name: 'user_required_fields',
type: NodeConstraintType.EXISTS,
label: 'User',
properties: ['email', 'createdAt'],
});

3. Composite Indexes for Multiple Filters

// If you often query by city AND status together
await ductape.graph.createNodeIndex({
name: 'idx_user_city_status',
type: NodeIndexType.BTREE,
label: 'User',
properties: ['city', 'status'],
});

// Efficient for:
const users = await ductape.graph.findNodes({
labels: ['User'],
where: {
city: 'New York',
status: 'active',
},
});

4. Choose the Right Index Type

// BTREE for equality and ranges
await ductape.graph.createNodeIndex({
name: 'idx_price',
type: NodeIndexType.BTREE,
label: 'Product',
properties: ['price'],
});

// FULLTEXT for text search
await ductape.graph.createNodeIndex({
name: 'idx_content',
type: NodeIndexType.FULLTEXT,
label: 'Article',
properties: ['content'],
});

// TEXT for pattern matching
await ductape.graph.createNodeIndex({
name: 'idx_name',
type: NodeIndexType.TEXT,
label: 'User',
properties: ['name'],
});

5. Don't Over-Index

// Bad - too many indexes slow down writes
// Only index what you actually query

// Good - strategic indexing
// Index high-cardinality, frequently queried properties
await ductape.graph.createNodeIndex({
name: 'idx_user_email',
type: NodeIndexType.BTREE,
label: 'User',
properties: ['email'], // High cardinality, frequently queried
});

// Skip indexing low-cardinality properties (unless required)
// Don't index: gender (only 2-3 values), isActive (boolean)

6. Index Property Order Matters

// Property order in composite indexes matters!

// Good - most selective property first
await ductape.graph.createNodeIndex({
name: 'idx_user_email_city',
type: NodeIndexType.BTREE,
label: 'User',
properties: ['email', 'city'], // email is more selective
});

// This efficiently supports:
// - { email: 'x@y.com' }
// - { email: 'x@y.com', city: 'NYC' }

// But NOT efficient for:
// - { city: 'NYC' } alone (first property not in filter)

7. Monitor Index Usage

// Regularly review your indexes
const indexes = await ductape.graph.listIndexes();

// Check which are actually being used
// Drop unused indexes to improve write performance
for (const index of indexes.indexes) {
if (index.state !== 'ONLINE') {
console.log(`Index ${index.name} is not online`);
}
}

Common Patterns

User System

// Unique email for login
await ductape.graph.createNodeConstraint({
name: 'unique_user_email',
type: NodeConstraintType.UNIQUE,
label: 'User',
properties: ['email'],
});

// Index for user lookups
await ductape.graph.createNodeIndex({
name: 'idx_user_username',
type: NodeIndexType.BTREE,
label: 'User',
properties: ['username'],
});

// Index for status filtering
await ductape.graph.createNodeIndex({
name: 'idx_user_status_created',
type: NodeIndexType.BTREE,
label: 'User',
properties: ['status', 'createdAt'],
});

E-Commerce

// Unique product SKUs
await ductape.graph.createNodeConstraint({
name: 'unique_product_sku',
type: NodeConstraintType.UNIQUE,
label: 'Product',
properties: ['sku'],
});

// Index for product search
await ductape.graph.createNodeIndex({
name: 'idx_product_name',
type: NodeIndexType.TEXT,
label: 'Product',
properties: ['name'],
});

// Index for category browsing
await ductape.graph.createNodeIndex({
name: 'idx_product_category_price',
type: NodeIndexType.BTREE,
label: 'Product',
properties: ['category', 'price'],
});

// Full-text search for products
await ductape.graph.createNodeIndex({
name: 'idx_product_description',
type: NodeIndexType.FULLTEXT,
label: 'Product',
properties: ['name', 'description'],
});

Social Network

// Unique usernames
await ductape.graph.createNodeConstraint({
name: 'unique_username',
type: NodeConstraintType.UNIQUE,
label: 'User',
properties: ['username'],
});

// Index friendship dates
await ductape.graph.createRelationshipIndex({
name: 'idx_friendship_since',
type: RelationshipIndexType.BTREE,
relationshipType: 'FRIENDS_WITH',
properties: ['since'],
});

// Index for feed queries
await ductape.graph.createNodeIndex({
name: 'idx_post_created',
type: NodeIndexType.BTREE,
label: 'Post',
properties: ['createdAt'],
});

Content Management

// Unique slugs for URLs
await ductape.graph.createNodeConstraint({
name: 'unique_article_slug',
type: NodeConstraintType.UNIQUE,
label: 'Article',
properties: ['slug'],
});

// Full-text search
await ductape.graph.createNodeIndex({
name: 'idx_article_search',
type: NodeIndexType.FULLTEXT,
label: 'Article',
properties: ['title', 'content', 'tags'],
});

// Index for filtering
await ductape.graph.createNodeIndex({
name: 'idx_article_status_published',
type: NodeIndexType.BTREE,
label: 'Article',
properties: ['status', 'publishedAt'],
});

Performance Impact

Write Performance

Indexes slow down writes slightly:

// No indexes: Fast writes, slow reads
// With 1 index: Slightly slower writes, fast reads
// With 5 indexes: Noticeably slower writes, fast reads

// Balance is key - only index what you query

Query Performance

Indexes can provide 100-1000x speedup:

// Without index: O(n) - scans all nodes
const users = await ductape.graph.findNodes({
labels: ['User'],
where: { email: 'alice@example.com' },
});
// 1M nodes = 1000ms

// With index: O(log n) - uses index
const users = await ductape.graph.findNodes({
labels: ['User'],
where: { email: 'alice@example.com' },
});
// 1M nodes = 1ms

Database-Specific Features

Neo4j

// Neo4j supports all index types
await ductape.graph.createNodeIndex({
name: 'idx_user_location',
type: NodeIndexType.POINT,
label: 'User',
properties: ['location'],
});

// Vector indexes (Neo4j 5.11+)
await ductape.graph.createNodeIndex({
name: 'idx_embedding',
type: NodeIndexType.VECTOR,
label: 'Document',
properties: ['embedding'],
options: {
vectorDimensions: 1536,
vectorSimilarityFunction: 'cosine',
},
});

AWS Neptune

// Neptune has limited constraint support
// Focus on indexes for performance
await ductape.graph.createNodeIndex({
name: 'idx_user_email',
type: NodeIndexType.BTREE,
label: 'User',
properties: ['email'],
});

ArangoDB

// ArangoDB persistent indexes
await ductape.graph.createNodeIndex({
name: 'idx_user_email',
type: NodeIndexType.BTREE,
label: 'User',
properties: ['email'],
options: {
sparse: false,
unique: true,
},
});

Memgraph

// Memgraph label-property indexes
await ductape.graph.createNodeIndex({
name: 'idx_user_email',
type: NodeIndexType.BTREE,
label: 'User',
properties: ['email'],
});

Next Steps

See Also