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
- Query Optimization - Optimize graph queries
- Nodes - Working with graph nodes
- Relationships - Managing relationships
- Traversals - Graph pathfinding
See Also
- Graph Overview - Full API reference
- Best Practices - Performance optimization