- Installation
- Quick Start
- Configuration
- Service Methods
- Query Operators
- Special Features
- Error Handling
- TypeScript Support
npm install feathers-elasticsearch @elastic/elasticsearchimport { Client } from '@elastic/elasticsearch'
import service from 'feathers-elasticsearch'
// Initialize Elasticsearch client
const client = new Client({
node: 'http://localhost:9200'
})
// Create service
const peopleService = service({
Model: client,
index: 'people',
id: 'id',
paginate: {
default: 10,
max: 100
}
})
// Use in Feathers app
app.use('/people', peopleService)| Option | Type | Required | Description |
|---|---|---|---|
Model |
Client |
Yes | Elasticsearch client instance |
index |
string |
No | Default index name |
id |
string |
No | ID field name (default: '_id') |
parent |
string |
No | Parent field name for parent-child relationships |
routing |
string |
No | Routing field name |
join |
string |
No | Join field name for parent-child relationships |
meta |
string |
No | Metadata field name (default: '_meta') |
esVersion |
string |
No | Elasticsearch version (e.g., '8.0') |
esParams |
object |
No | Default Elasticsearch parameters |
paginate |
object |
No | Pagination configuration |
whitelist |
string[] |
No | Allowed query operators |
multi |
boolean|string[] |
No | Allow multi operations |
const service = service({
Model: client,
index: 'products',
id: 'productId',
esVersion: '8.0',
esParams: {
refresh: true,
timeout: '30s'
},
paginate: {
default: 20,
max: 50
},
multi: true,
whitelist: ['$match', '$phrase', '$prefix']
})Find multiple documents matching the query.
// Basic find
const results = await service.find({
query: {
status: 'active',
category: 'electronics'
}
})
// With pagination
const page = await service.find({
query: {
status: 'active'
},
paginate: {
default: 10,
max: 50
}
})
// Returns: { total, limit, skip, data }
// Without pagination
const all = await service.find({
query: {
status: 'active'
},
paginate: false
})Get a single document by ID.
const doc = await service.get('doc123')
// With selected fields
const doc = await service.get('doc123', {
query: {
$select: ['name', 'email']
}
})Create one or more documents.
// Single document
const created = await service.create({
name: 'John Doe',
email: 'john@example.com'
})
// With specific ID
const created = await service.create({
id: 'user123',
name: 'John Doe',
email: 'john@example.com'
})
// Bulk creation
const items = await service.create([
{ name: 'John', age: 30 },
{ name: 'Jane', age: 25 }
])
// With upsert
const doc = await service.create({ id: 'doc123', name: 'Updated' }, { upsert: true })Replace a document entirely.
const updated = await service.update('doc123', {
name: 'Jane Doe',
email: 'jane@example.com',
age: 28
})
// With upsert
const doc = await service.update('doc123', { name: 'New Document' }, { upsert: true })Partially update one or more documents.
// Single document
const patched = await service.patch('doc123', {
status: 'inactive'
})
// Bulk patch
const results = await service.patch(
null,
{ archived: true },
{
query: {
createdAt: { $lt: '2023-01-01' }
}
}
)Remove one or more documents.
// Single document
const removed = await service.remove('doc123')
// Bulk removal
const results = await service.remove(null, {
query: {
status: 'deleted'
}
})Execute raw Elasticsearch API methods.
// Direct search
const results = await service.raw('search', {
body: {
query: {
match_all: {}
},
aggs: {
categories: {
terms: { field: 'category' }
}
}
}
})
// Index operations
const mapping = await service.raw('indices.getMapping') | Operator | Description | Example |
|---|---|---|
$gt |
Greater than | { age: { $gt: 18 } } |
$gte |
Greater than or equal | { age: { $gte: 18 } } |
$lt |
Less than | { age: { $lt: 65 } } |
$lte |
Less than or equal | { age: { $lte: 65 } } |
$ne |
Not equal | { status: { $ne: 'deleted' } } |
$in |
In array | { status: { $in: ['active', 'pending'] } } |
$nin |
Not in array | { status: { $nin: ['deleted', 'archived'] } } |
| Operator | Description | Example |
|---|---|---|
$match |
Full-text match | { title: { $match: 'elasticsearch' } } |
$phrase |
Phrase match | { title: { $phrase: 'quick brown fox' } } |
$phrase_prefix |
Phrase prefix | { title: { $phrase_prefix: 'quick br' } } |
$prefix |
Term prefix | { username: { $prefix: 'john' } } |
$wildcard |
Wildcard pattern | { email: { $wildcard: '*@example.com' } } |
$regexp |
Regular expression | { phone: { $regexp: '^\\+1.*' } } |
// $or
{
$or: [
{ status: 'active' },
{ priority: 'high' }
]
}
// $and
{
$and: [
{ status: 'active' },
{ category: 'electronics' }
]
}
// Combined
{
status: 'active',
$or: [
{ priority: 'high' },
{ deadline: { $lt: '2024-01-01' } }
]
}{
$all: true
} // Returns all documents{
$sqs: {
$query: 'nodejs elasticsearch',
$fields: ['title', 'description'],
$operator: 'and' // Optional: 'and' or 'or'
}
}{
$exists: ['email', 'phone']
} // Documents with these fields
{
$missing: ['deletedAt']
} // Documents without these fields{
$nested: {
$path: 'comments',
'comments.author': 'John',
'comments.rating': { $gte: 4 }
}
}// Find child documents
{
$child: {
$type: 'comment',
author: 'John'
}
}
// Find parent documents
{
$parent: {
$type: 'post',
status: 'published'
}
}// Default pagination
const page1 = await service.find({
query: { status: 'active' }
})
// Custom pagination
const page2 = await service.find({
query: {
status: 'active',
$limit: 20,
$skip: 20
}
})
// Disable pagination
const all = await service.find({
query: { status: 'active' },
paginate: false
}){
query: {
$sort: {
createdAt: -1, // Descending
name: 1 // Ascending
}
}
}{
query: {
$select: ['name', 'email', 'status']
}
}// Query specific index
{
query: {
$index: 'products-2024'
}
}
// With routing
{
query: {
$routing: 'user123'
}
}// Bulk create
const docs = await service.create([{ name: 'Doc1' }, { name: 'Doc2' }, { name: 'Doc3' }])
// Bulk patch
const updated = await service.patch(
null,
{ status: 'archived' },
{ query: { createdAt: { $lt: '2023-01-01' } } }
)
// Bulk remove
const removed = await service.remove(null, {
query: { status: 'deleted' }
})The service throws Feathers errors that can be caught and handled:
try {
const doc = await service.get('nonexistent')
} catch (error) {
if (error.name === 'NotFound') {
// Handle not found
}
}
// Error types:
// - BadRequest (400): Invalid query or parameters
// - NotFound (404): Document not found
// - Conflict (409): Document already exists
// - GeneralError (500): Elasticsearch errorsThe service exports comprehensive TypeScript types:
import service, {
ElasticsearchServiceOptions,
ElasticsearchServiceParams,
ElasticsearchDocument,
ESSearchResponse,
QueryOperators,
ServiceResult,
PaginatedResult
} from 'feathers-elasticsearch'
// Typed service creation
const typedService = service<User>({
Model: client,
index: 'users'
})
// Typed queries
const users: User[] = await typedService.find({
query: {
age: { $gte: 18 },
status: 'active'
}
})
// Custom document type
interface User extends ElasticsearchDocument {
name: string
email: string
age: number
status: 'active' | 'inactive'
}const results = await service.raw('search', {
body: {
query: {
bool: {
must: [{ term: { status: 'active' } }],
filter: [{ range: { age: { gte: 18 } } }]
}
},
aggs: {
age_groups: {
histogram: {
field: 'age',
interval: 10
}
}
}
}
})// Setup service with join
const service = service({
Model: client,
index: 'blog',
join: 'post_comment',
parent: 'post_id'
})
// Create parent document
const post = await service.create({
id: 'post1',
title: 'My Post',
join: 'post'
})
// Create child document
const comment = await service.create({
content: 'Great post!',
parent: 'post1',
join: {
name: 'comment',
parent: 'post1'
}
})import { createRetryWrapper } from 'feathers-elasticsearch/utils'
// Wrap client with retry logic
const retryClient = createRetryWrapper(client, {
maxRetries: 3,
initialDelay: 100,
backoffMultiplier: 2
})
const service = service({
Model: retryClient,
index: 'products'
})- Update to Feathers v5 (Dove)
- Use new TypeScript types
- Update error handling (errors are now properly thrown)
- Use new query operators format
// Old (v2)
service.find({
query: {
$search: 'text'
}
})
// New (v3)
service.find({
query: {
$match: 'text'
}
})-
Use field selection to reduce data transfer:
{ query: { $select: ['id', 'name'] } }
-
Enable refresh only when needed:
esParams: { refresh: false } // Default
-
Use bulk operations for multiple documents:
service.create([...documents]) // Instead of multiple create calls
-
Leverage Elasticsearch caching:
service.raw('search', { request_cache: true, body: { ... } })
-
Use appropriate pagination limits:
paginate: { default: 20, max: 100 }
- GitHub Issues: Report bugs
- Documentation: Full documentation
- Feathers Discord: Community support