This document describes the performance optimization features available in feathers-elasticsearch.
The following performance optimizations are available:
- Content-Based Query Caching - Caches parsed queries based on content
- Lean Mode - Skips fetching full documents after bulk operations
- Configurable Refresh - Per-operation control of index refresh
- Query Complexity Budgeting - Limits expensive queries to protect cluster performance
Parsed queries are cached based on their content (using SHA256 hashing) rather than object references. This significantly improves cache hit rates when the same query structure is used multiple times.
- Before: ~5-10% cache hit rate (WeakMap based on object references)
- After: ~50-90% cache hit rate (content-based hashing)
- Memory: Max 1000 cached entries, 5-minute TTL
// These two queries will hit the cache even though they're different objects
service.find({ query: { name: 'John' } })
service.find({ query: { name: 'John' } }) // Cache hit!No configuration needed - enabled automatically. Cache parameters:
- Max size: 1000 entries
- TTL: 5 minutes
- Automatic cleanup on size/age limits
Skips the round-trip to fetch full documents after bulk create, patch, or remove operations. Useful when you don't need the full document data back.
- Reduction: Eliminates 1 network round-trip (mget call)
- Speedup: ~40-60% faster for bulk operations
- Best for: High-throughput imports, batch updates where response data isn't needed
// Create bulk without fetching full documents
await service.create([
{ name: 'John' },
{ name: 'Jane' }
], {
lean: true // Returns minimal response (just IDs and status)
})
// Patch bulk in lean mode
await service.patch(null, { status: 'active' }, {
query: { type: 'user' },
lean: true
})
// Remove bulk in lean mode
await service.remove(null, {
query: { archived: true },
lean: true
})Without lean mode (default):
[
{ id: '1', name: 'John', email: 'john@example.com', _meta: {...} },
{ id: '2', name: 'Jane', email: 'jane@example.com', _meta: {...} }
]With lean mode:
// create-bulk
[
{ id: '1', _meta: { status: 201, _id: '1', ... } },
{ id: '2', _meta: { status: 201, _id: '2', ... } }
]
// remove-bulk
[
{ id: '1' },
{ id: '2' }
]Allows per-operation control of when Elasticsearch refreshes its indices, overriding the global default.
refresh: false: Fastest (default) - changes visible after refresh interval (~1s)refresh: 'wait_for': Medium - waits for refresh before returningrefresh: true: Slowest - forces immediate refresh
// Service-level default (set once)
const service = new Service({
Model: esClient,
esParams: {
refresh: false // Default for all operations
}
})
// Per-operation override for immediate visibility
await service.create({
name: 'Important Document'
}, {
refresh: 'wait_for' // Override: wait for refresh
})
// Bulk import without refresh (fastest)
await service.create(largeDataset, {
refresh: false // Explicit: don't wait for refresh
})
// Critical update that must be immediately visible
await service.patch(id, { status: 'published' }, {
refresh: true // Force immediate refresh
})| Option | Use Case | Performance |
|---|---|---|
false |
Bulk imports, batch updates, background jobs | Fastest |
'wait_for' |
User-facing updates that should be visible immediately | Medium |
true |
Critical updates requiring immediate consistency | Slowest |
// ✅ Good: Fast bulk import
await service.create(1000records, {
lean: true, // Don't fetch back
refresh: false // Don't wait for refresh
})
// ✅ Good: User update with visibility
await service.patch(userId, updates, {
refresh: 'wait_for' // Wait for next refresh
})
// ❌ Avoid: Forcing refresh on every operation
await service.create(data, {
refresh: true // Forces immediate refresh - slow!
})Calculates a complexity score for queries and rejects overly complex queries that could impact cluster performance.
- Protection: Prevents expensive queries from overwhelming the cluster
- Default limit: 100 complexity points
- Configurable: Adjust based on your cluster capacity
Different query types have different costs:
| Query Type | Cost | Reason |
|---|---|---|
| Script queries | 15 | Very expensive - avoid in production |
| Nested queries | 10 | Expensive due to document joins |
| Regex queries | 8 | Pattern matching is CPU-intensive |
| Fuzzy queries | 6 | Levenshtein distance calculation |
| Wildcard queries | 5 | Requires term enumeration |
| Prefix queries | 3 | Moderate - uses prefix tree |
| Match queries | 2 | Standard text search |
| Range queries | 2 | Index scan required |
| Bool clauses | 1 | Minimal overhead |
| Term queries | 1 | Cheapest - exact match |
const service = new Service({
Model: esClient,
security: {
maxQueryComplexity: 100 // Default
}
})
// For more powerful clusters
const service = new Service({
Model: esClient,
security: {
maxQueryComplexity: 200 // Allow more complex queries
}
})
// For resource-constrained environments
const service = new Service({
Model: esClient,
security: {
maxQueryComplexity: 50 // Stricter limits
}
})// Simple query (cost: ~3)
service.find({
query: {
name: 'John', // +1
status: 'active' // +1
}
})
// Complex query (cost: ~45)
service.find({
query: {
$or: [ // +1, children x2
{
$wildcard: { // +5
name: 'Jo*'
}
},
{
$nested: { // +10, children x10
path: 'addresses',
query: {
city: 'Boston' // +1 (x10 = 10)
}
}
}
]
}
})
// Query too complex (cost: >100) - will be rejected
service.find({
query: {
$or: [ // Multiple nested OR clauses
{ $regexp: { ... } }, // +8 each
{ $regexp: { ... } },
{ $regexp: { ... } },
// ... many more
]
}
})
// Error: Query complexity (150) exceeds maximum allowed (100)try {
await service.find({
query: veryComplexQuery
})
} catch (error) {
if (error.name === 'BadRequest' && error.message.includes('complexity')) {
// Query too complex - simplify it
console.log('Query too complex, simplifying...')
await service.find({
query: simplifiedQuery
})
}
}These features work together for maximum performance:
// Example: High-performance bulk import
await service.create(largeDataset, {
lean: true, // Don't fetch documents back
refresh: false // Don't wait for refresh
})
// Result: 60-80% faster than default
// Example: Complex search with safeguards
const service = new Service({
Model: esClient,
security: {
maxQueryComplexity: 75 // Limit expensive queries
}
})
// Queries are automatically validated
await service.find({
query: complexButSafeQuery // Automatically checked
})
// Example: User-facing update
await service.patch(userId, updates, {
refresh: 'wait_for' // Visible to user immediately
// lean: false (default) - return full updated document
})Based on typical workloads:
| Operation | Default | Optimized | Improvement |
|---|---|---|---|
| Bulk create (1000 docs) | 2500ms | 950ms | 62% faster |
| Bulk patch (500 docs) | 1800ms | 720ms | 60% faster |
| Bulk remove (200 docs) | 450ms | 180ms | 60% faster |
| Repeated queries | 100% | 50-10% | 50-90% faster (cache hits) |
| Complex queries | Varies | Rejected if > limit | Cluster protected |
Monitor cache hit rates by tracking query response times. If you see consistent slow queries for the same patterns, the cache is working.
Start with default (100) and adjust based on:
- Cluster size and capacity
- Query patterns in your application
- Performance monitoring data
Choose based on your use case:
- Analytics dashboard:
refresh: false(eventual consistency OK) - User profile updates:
refresh: 'wait_for'(user expects to see changes) - Critical system updates:
refresh: true(immediate consistency required)
All new features are opt-in and backward compatible:
// Existing code works unchanged
await service.create(data)
// Opt into optimizations gradually
await service.create(data, { lean: true })
// Adjust complexity limits if needed
const service = new Service({
Model: esClient,
security: {
maxQueryComplexity: 150 // Increase if you need complex queries
}
})- Default behavior unchanged
- All parameters optional
- Existing code continues to work
- PERFORMANCE.md - Detailed performance analysis
- SECURITY.md - Security features including query depth limits
- README.md - General usage documentation