Elasticsearch with Node.js and TypeScript — Full-Text Search Guide 2026
Advertisement
Introduction
Why Elasticsearch for Node.js Backends
Elasticsearch delivers sub-100ms full-text search on billions of documents. It handles relevance scoring, fuzzy matching, autocomplete, and log analytics out of the box — tasks that would cripple a relational database. The official @elastic/elasticsearch client ships TypeScript definitions, giving you compile-time safety on complex query DSL objects.
Use Elasticsearch alongside your primary database: write to Postgres or MongoDB, index into Elasticsearch, and query via search.
Installation and Connection
npm install @elastic/elasticsearchimport { Client } from '@elastic/elasticsearch';
const client = new Client({
node: process.env.ELASTICSEARCH_URL ?? 'http://localhost:9200',
auth: {
username: process.env.ES_USER ?? 'elastic',
password: process.env.ES_PASSWORD ?? '',
},
});
// Health check at startup
const health = await client.cluster.health({});
console.log('Cluster status:', health.status); // green | yellow | redCreating an Index with Mappings
await client.indices.create({
index: 'products',
mappings: {
properties: {
name: { type: 'text', analyzer: 'english' },
description: { type: 'text', analyzer: 'english' },
price: { type: 'float' },
category: { type: 'keyword' }, // exact match, not analyzed
inStock: { type: 'boolean' },
createdAt: { type: 'date' },
tags: { type: 'keyword' },
},
},
settings: {
number_of_shards: 1,
number_of_replicas: 1,
},
});Indexing Documents
// Single document
await client.index({
index: 'products',
id: 'prod-001',
document: {
name: 'TypeScript Handbook',
description: 'Complete guide to TypeScript for backend developers',
price: 29.99,
category: 'books',
inStock: true,
createdAt: new Date(),
tags: ['typescript', 'programming'],
},
});
// Bulk indexing for high throughput
const body = products.flatMap(p => [
{ index: { _index: 'products', _id: p.id } },
p,
]);
await client.bulk({ operations: body });
await client.indices.refresh({ index: 'products' });Search Queries
// Basic match query
const simple = await client.search({
index: 'products',
query: { match: { name: 'typescript handbook' } },
});
// Multi-field search with boosting
const multiField = await client.search({
index: 'products',
query: {
multi_match: {
query: 'typescript backend',
fields: ['name^3', 'description', 'tags^2'], // name has 3x boost
fuzziness: 'AUTO',
},
},
});
// Boolean query — combine must, should, filter
const advanced = await client.search({
index: 'products',
query: {
bool: {
must: [{ match: { description: 'typescript' } }],
filter: [
{ term: { category: 'books' } },
{ term: { inStock: true } },
{ range: { price: { gte: 10, lte: 50 } } },
],
should: [{ term: { tags: 'programming' } }],
minimum_should_match: 0,
},
},
from: 0,
size: 20,
sort: [{ price: 'asc' }],
});
// Extract hits
const hits = advanced.hits.hits.map(h => h._source);Aggregations
// Category breakdown + price stats
const aggs = await client.search({
index: 'products',
size: 0, // no documents, only aggregations
aggs: {
by_category: {
terms: { field: 'category', size: 10 },
aggs: {
avg_price: { avg: { field: 'price' } },
},
},
price_histogram: {
histogram: { field: 'price', interval: 10 },
},
},
});Custom Analyzers
await client.indices.create({
index: 'articles',
settings: {
analysis: {
filter: {
autocomplete_filter: {
type: 'edge_ngram',
min_gram: 2,
max_gram: 20,
},
},
analyzer: {
autocomplete: {
type: 'custom',
tokenizer: 'standard',
filter: ['lowercase', 'autocomplete_filter'],
},
},
},
},
mappings: {
properties: {
title: {
type: 'text',
analyzer: 'autocomplete',
search_analyzer: 'standard',
},
},
},
});Service Pattern
class SearchService {
constructor(private readonly es: Client) {}
async indexDocument(index: string, id: string, doc: Record<string, unknown>) {
await this.es.index({ index, id, document: doc });
}
async search(index: string, query: string, from = 0, size = 10) {
const res = await this.es.search({
index,
from,
size,
query: {
multi_match: { query, fields: ['name^2', 'description'], fuzziness: 'AUTO' },
},
highlight: { fields: { description: {} } },
});
return {
total: res.hits.total,
hits: res.hits.hits.map(h => ({
id: h._id,
score: h._score,
source: h._source,
highlights: h.highlight,
})),
};
}
async delete(index: string, id: string) {
await this.es.delete({ index, id });
}
}Common Mistakes
- Using
texttype for exact-match filters — usekeywordfor filtering and aggregating - Not calling
refreshafter bulk indexing during tests, causing empty search results - Ignoring
fuzziness— users make typos; setfuzziness: 'AUTO'for better recall - Running queries with huge
sizevalues — paginate withfrom/sizeor use scroll API - Storing large binary blobs or full HTML in Elasticsearch — store metadata only
Best Practices
- Define explicit mappings before indexing — never rely on dynamic mapping in production
- Use
filtercontext for exact conditions (faster, cacheable) andmustfor scored full-text - Keep shard size between 10–50 GB; avoid thousands of tiny shards
- Index writes to your primary DB first, then to Elasticsearch — treat ES as a read replica
- Use index aliases so you can reindex without downtime by swapping aliases
- Monitor query latency and rejected requests via Kibana or Grafana
Key Takeaways
- Elasticsearch uses inverted indexes to achieve sub-100ms search on millions of documents
- The
@elastic/elasticsearchclient ships TypeScript types for safe query construction keywordfields are for exact matching and aggregations;textfields are for full-text search- Boolean queries combine
must,filter,should, andmust_notclauses for precision - Aggregations replace complex GROUP BY SQL — they run server-side with no data transfer
- Custom analyzers with
edge_ngramenable autocomplete and prefix search - Always define production mappings explicitly — dynamic mapping can create unintended field types
- Write to your primary database first; sync to Elasticsearch asynchronously for resilience
Advertisement
Related reading
Prisma ORM Guide 2026 — Type-Safe Database Access with PostgreSQL5 min readRedis Caching Guide 2026 — Improve API Performance 10x5 min readPostgreSQL Guide 2026 — Performance, JSON, Full-Text Search, and Scaling5 min readAPI-First Development in 2026 — Design, Mock, Validate, Then Build6 min readbetter-auth — The Open-Source Auth Library That Replaces NextAuth6 min readData Corruption from Bad Serialization — When Your Data Silently Changes6 min read