Prerequisites
- Basic understanding of JavaScript ๐
- TypeScript installation โก
- VS Code or preferred IDE ๐ป
What you'll learn
- Understand the concept fundamentals ๐ฏ
- Apply the concept in real projects ๐๏ธ
- Debug common issues ๐
- Write type-safe code โจ
๐ฏ Introduction
Welcome to the exciting world of Elasticsearch with TypeScript! ๐ In this guide, weโll explore how to build powerful search capabilities into your applications using Elasticsearch and TypeScript together.
Youโll discover how Elasticsearch can transform your applicationโs search experience. Whether youโre building an e-commerce platform ๐, a content management system ๐, or a data analytics dashboard ๐, understanding Elasticsearch integration is essential for delivering lightning-fast, relevant search results.
By the end of this tutorial, youโll feel confident implementing full-text search, faceted navigation, and real-time analytics in your TypeScript projects! Letโs dive in! ๐โโ๏ธ
๐ Understanding Elasticsearch
๐ค What is Elasticsearch?
Elasticsearch is like having a super-smart librarian ๐ who can instantly find any book in a massive library, understand what you meant even if you misspelled the title, and suggest related books you might enjoy!
In technical terms, Elasticsearch is a distributed, RESTful search and analytics engine built on Apache Lucene. With TypeScript, you get:
- โจ Type-safe queries and responses
- ๐ Autocomplete for query building
- ๐ก๏ธ Compile-time validation of search parameters
๐ก Why Use Elasticsearch with TypeScript?
Hereโs why developers love this combination:
- Lightning-Fast Search โก: Millisecond response times even with millions of documents
- Intelligent Querying ๐ง : Fuzzy matching, synonyms, and relevance scoring
- Real-Time Analytics ๐: Aggregate and analyze data on the fly
- Type Safety ๐: Catch query errors before runtime
Real-world example: Imagine building a recipe search engine ๐ณ. With Elasticsearch, users can search for โchiken parmisanโ (misspelled!) and still find โChicken Parmesanโ recipes, plus get suggestions for similar Italian dishes!
๐ง Basic Syntax and Usage
๐ Setting Up the Connection
Letโs start with connecting to Elasticsearch:
// ๐ Hello, Elasticsearch!
import { Client } from '@elastic/elasticsearch';
// ๐จ Define our document type
interface Recipe {
id: string;
title: string;
ingredients: string[];
cookingTime: number;
difficulty: 'easy' | 'medium' | 'hard';
emoji: string; // Every recipe needs an emoji!
}
// ๐ Create the client
const client = new Client({
node: 'http://localhost:9200',
auth: {
username: 'elastic',
password: 'your-password'
}
});
// ๐๏ธ Create an index with mappings
const createRecipeIndex = async () => {
await client.indices.create({
index: 'recipes',
body: {
mappings: {
properties: {
title: { type: 'text' },
ingredients: { type: 'text' },
cookingTime: { type: 'integer' },
difficulty: { type: 'keyword' },
emoji: { type: 'keyword' }
}
}
}
});
console.log('โ
Recipe index created!');
};
๐ก Explanation: We define TypeScript interfaces that match our Elasticsearch mappings, ensuring type safety throughout our application!
๐ฏ Common Operations
Here are the essential operations youโll use daily:
// ๐๏ธ Indexing documents
const indexRecipe = async (recipe: Recipe) => {
const response = await client.index({
index: 'recipes',
id: recipe.id,
body: recipe
});
console.log(`โ
Indexed recipe: ${recipe.emoji} ${recipe.title}`);
return response;
};
// ๐ Basic search
const searchRecipes = async (query: string) => {
const response = await client.search<Recipe>({
index: 'recipes',
body: {
query: {
multi_match: {
query,
fields: ['title^2', 'ingredients'], // Title is 2x more important!
fuzziness: 'AUTO'
}
}
}
});
return response.body.hits.hits.map(hit => hit._source!);
};
// ๐ Aggregations for analytics
const getRecipeStats = async () => {
const response = await client.search({
index: 'recipes',
body: {
size: 0, // We only want aggregations
aggs: {
by_difficulty: {
terms: { field: 'difficulty' }
},
avg_cooking_time: {
avg: { field: 'cookingTime' }
}
}
}
});
return response.body.aggregations;
};
๐ก Practical Examples
๐ Example 1: E-Commerce Product Search
Letโs build a real product search system:
// ๐๏ธ Define our product type
interface Product {
id: string;
name: string;
description: string;
price: number;
category: string;
tags: string[];
inStock: boolean;
rating: number;
emoji: string;
}
// ๐ Advanced search with filters
class ProductSearchEngine {
private client: Client;
constructor(client: Client) {
this.client = client;
}
// ๐ฏ Search with multiple criteria
async searchProducts(params: {
query?: string;
category?: string;
minPrice?: number;
maxPrice?: number;
inStock?: boolean;
minRating?: number;
}) {
const must: any[] = [];
const filter: any[] = [];
// ๐ Full-text search
if (params.query) {
must.push({
multi_match: {
query: params.query,
fields: ['name^3', 'description', 'tags'],
fuzziness: 'AUTO'
}
});
}
// ๐ท๏ธ Category filter
if (params.category) {
filter.push({ term: { category: params.category } });
}
// ๐ฐ Price range
if (params.minPrice || params.maxPrice) {
filter.push({
range: {
price: {
...(params.minPrice && { gte: params.minPrice }),
...(params.maxPrice && { lte: params.maxPrice })
}
}
});
}
// ๐ฆ Stock status
if (params.inStock !== undefined) {
filter.push({ term: { inStock: params.inStock } });
}
// โญ Rating filter
if (params.minRating) {
filter.push({ range: { rating: { gte: params.minRating } } });
}
const response = await this.client.search<Product>({
index: 'products',
body: {
query: {
bool: {
must: must.length > 0 ? must : [{ match_all: {} }],
filter
}
},
sort: [
{ _score: 'desc' },
{ rating: 'desc' }
]
}
});
return response.body.hits.hits.map(hit => ({
...hit._source!,
score: hit._score
}));
}
// ๐จ Get facets for filtering UI
async getFacets(query?: string) {
const baseQuery = query ? {
multi_match: {
query,
fields: ['name^3', 'description', 'tags']
}
} : { match_all: {} };
const response = await this.client.search({
index: 'products',
body: {
size: 0,
query: baseQuery,
aggs: {
categories: {
terms: { field: 'category', size: 20 }
},
price_ranges: {
range: {
field: 'price',
ranges: [
{ key: '๐ท๏ธ Under $25', to: 25 },
{ key: '๐ต $25-$50', from: 25, to: 50 },
{ key: '๐ฐ $50-$100', from: 50, to: 100 },
{ key: '๐ Over $100', from: 100 }
]
}
},
avg_rating: {
avg: { field: 'rating' }
}
}
}
});
return response.body.aggregations;
}
}
// ๐ฎ Let's use it!
const searchEngine = new ProductSearchEngine(client);
// Search for gaming keyboards under $100
const results = await searchEngine.searchProducts({
query: 'gaming keyboard RGB',
maxPrice: 100,
inStock: true,
minRating: 4
});
console.log(`๐ฎ Found ${results.length} gaming keyboards!`);
results.forEach(product => {
console.log(` ${product.emoji} ${product.name} - $${product.price} โญ${product.rating}`);
});
๐ฏ Try it yourself: Add autocomplete functionality and search suggestions!
๐ฎ Example 2: Real-Time Log Analysis
Letโs build a log analysis system:
// ๐ Log entry type
interface LogEntry {
timestamp: Date;
level: 'debug' | 'info' | 'warn' | 'error';
service: string;
message: string;
userId?: string;
requestId?: string;
duration?: number;
emoji: string;
}
class LogAnalyzer {
private client: Client;
constructor(client: Client) {
this.client = client;
}
// ๐ Index a log entry
async log(entry: Omit<LogEntry, 'timestamp' | 'emoji'> & { level: LogEntry['level'] }) {
const emojiMap = {
debug: '๐',
info: '๐',
warn: 'โ ๏ธ',
error: '๐จ'
};
const logEntry: LogEntry = {
...entry,
timestamp: new Date(),
emoji: emojiMap[entry.level]
};
await this.client.index({
index: `logs-${new Date().toISOString().slice(0, 10)}`, // Daily indices
body: logEntry
});
console.log(`${logEntry.emoji} Logged: ${logEntry.message}`);
}
// ๐ Search logs with complex queries
async searchLogs(params: {
startTime?: Date;
endTime?: Date;
levels?: LogEntry['level'][];
services?: string[];
searchText?: string;
userId?: string;
}) {
const must: any[] = [];
// โฐ Time range
if (params.startTime || params.endTime) {
must.push({
range: {
timestamp: {
...(params.startTime && { gte: params.startTime }),
...(params.endTime && { lte: params.endTime })
}
}
});
}
// ๐ฏ Level filter
if (params.levels && params.levels.length > 0) {
must.push({ terms: { level: params.levels } });
}
// ๐ข Service filter
if (params.services && params.services.length > 0) {
must.push({ terms: { service: params.services } });
}
// ๐ค User filter
if (params.userId) {
must.push({ term: { userId: params.userId } });
}
// ๐ Text search
if (params.searchText) {
must.push({
match: {
message: {
query: params.searchText,
operator: 'and'
}
}
});
}
const response = await this.client.search<LogEntry>({
index: 'logs-*',
body: {
query: { bool: { must } },
sort: [{ timestamp: 'desc' }],
size: 100
}
});
return response.body.hits.hits.map(hit => hit._source!);
}
// ๐ Get error statistics
async getErrorStats(hours: number = 24) {
const response = await this.client.search({
index: 'logs-*',
body: {
size: 0,
query: {
bool: {
must: [
{ term: { level: 'error' } },
{
range: {
timestamp: {
gte: new Date(Date.now() - hours * 60 * 60 * 1000)
}
}
}
]
}
},
aggs: {
errors_over_time: {
date_histogram: {
field: 'timestamp',
interval: 'hour'
}
},
by_service: {
terms: { field: 'service' }
},
avg_duration: {
avg: { field: 'duration' }
}
}
}
});
return response.body.aggregations;
}
}
// ๐ฎ Example usage
const logger = new LogAnalyzer(client);
// Log some events
await logger.log({
level: 'info',
service: 'auth-service',
message: 'User login successful',
userId: 'user123'
});
await logger.log({
level: 'error',
service: 'payment-service',
message: 'Payment processing failed',
userId: 'user456',
duration: 1500
});
// Search for errors
const errors = await logger.searchLogs({
levels: ['error'],
startTime: new Date(Date.now() - 60 * 60 * 1000) // Last hour
});
console.log(`๐จ Found ${errors.length} errors in the last hour!`);
๐ Advanced Concepts
๐งโโ๏ธ Advanced Topic 1: Custom Analyzers and Tokenizers
When youโre ready to level up, create custom text analysis:
// ๐ฏ Custom analyzer for product names
const createCustomAnalyzer = async () => {
await client.indices.create({
index: 'products_advanced',
body: {
settings: {
analysis: {
analyzer: {
product_analyzer: {
type: 'custom',
tokenizer: 'standard',
filter: ['lowercase', 'synonym_filter', 'stop']
}
},
filter: {
synonym_filter: {
type: 'synonym',
synonyms: [
'laptop,notebook,computer',
'phone,mobile,smartphone',
'tv,television,telly'
]
}
}
}
},
mappings: {
properties: {
name: {
type: 'text',
analyzer: 'product_analyzer'
}
}
}
}
});
console.log('โจ Custom analyzer created!');
};
// ๐ช Using the custom analyzer
const searchWithSynonyms = async (query: string) => {
const response = await client.search({
index: 'products_advanced',
body: {
query: {
match: {
name: {
query,
analyzer: 'product_analyzer'
}
}
}
}
});
// Searching for "laptop" will also find "notebook" and "computer"!
return response.body.hits.hits;
};
๐๏ธ Advanced Topic 2: Percolator Queries for Real-Time Alerts
For the brave developers, implement reverse search:
// ๐ Alert system using percolator
interface Alert {
id: string;
name: string;
userId: string;
query: any;
emoji: string;
}
class AlertSystem {
private client: Client;
constructor(client: Client) {
this.client = client;
}
// ๐ Register an alert
async createAlert(alert: Alert) {
await this.client.index({
index: 'alerts',
id: alert.id,
body: {
name: alert.name,
userId: alert.userId,
emoji: alert.emoji,
query: alert.query // This is the percolator query
}
});
console.log(`๐ Alert created: ${alert.emoji} ${alert.name}`);
}
// ๐ฏ Check which alerts match a document
async checkAlerts(doc: any) {
const response = await this.client.search({
index: 'alerts',
body: {
query: {
percolate: {
field: 'query',
document: doc
}
}
}
});
const matchedAlerts = response.body.hits.hits.map(hit => hit._source);
matchedAlerts.forEach(alert => {
console.log(`๐จ Alert triggered: ${alert.emoji} ${alert.name}`);
// Send notification to user
});
return matchedAlerts;
}
}
// Example: Price drop alerts
const alertSystem = new AlertSystem(client);
// User wants alert when gaming laptops under $1000 appear
await alertSystem.createAlert({
id: 'alert-1',
name: 'Gaming Laptop Deal',
userId: 'user123',
emoji: '๐ธ',
query: {
bool: {
must: [
{ match: { category: 'laptops' } },
{ match: { tags: 'gaming' } }
],
filter: [
{ range: { price: { lte: 1000 } } }
]
}
}
});
โ ๏ธ Common Pitfalls and Solutions
๐ฑ Pitfall 1: Mapping Conflicts
// โ Wrong way - changing field types causes conflicts!
await client.index({
index: 'products',
body: {
price: "29.99" // String instead of number! ๐ฅ
}
});
// โ
Correct way - consistent types!
interface StrictProduct {
price: number; // Always a number
}
const indexProduct = async (product: StrictProduct) => {
// TypeScript ensures price is always a number
await client.index({
index: 'products',
body: product
});
};
๐คฏ Pitfall 2: Deep Pagination Performance
// โ Dangerous - deep pagination kills performance!
const getAllResults = async () => {
const results = [];
let from = 0;
while (true) {
const response = await client.search({
index: 'products',
from: from, // Gets slower as from increases! ๐ฅ
size: 100
});
results.push(...response.body.hits.hits);
if (results.length >= response.body.hits.total.value) break;
from += 100;
}
return results;
};
// โ
Safe - use search_after for efficient pagination!
const getAllResultsEfficiently = async () => {
const results = [];
let searchAfter: any[] | undefined;
while (true) {
const response = await client.search({
index: 'products',
body: {
size: 100,
sort: [{ _id: 'asc' }],
...(searchAfter && { search_after: searchAfter })
}
});
const hits = response.body.hits.hits;
if (hits.length === 0) break;
results.push(...hits);
searchAfter = hits[hits.length - 1].sort;
}
return results;
};
๐ ๏ธ Best Practices
- ๐ฏ Use TypeScript Interfaces: Define interfaces for all document types
- ๐ Version Your Mappings: Use index aliases for zero-downtime updates
- ๐ก๏ธ Handle Errors Gracefully: Elasticsearch operations can fail
- ๐จ Design for Scale: Use time-based indices for logs
- โจ Optimize Queries: Use filters instead of queries when possible
๐งช Hands-On Exercise
๐ฏ Challenge: Build a Movie Search Engine
Create a type-safe movie search system:
๐ Requirements:
- โ Index movies with title, genre, year, rating, and actors
- ๐ Full-text search across title and actor names
- ๐ญ Filter by genre and year range
- โญ Sort by rating or relevance
- ๐ Show genre distribution and average ratings
- ๐จ Each movie needs an emoji based on genre!
๐ Bonus Points:
- Add โMore Like Thisโ recommendations
- Implement autocomplete suggestions
- Create saved searches for users
๐ก Solution
๐ Click to see solution
// ๐ฌ Our type-safe movie search system!
interface Movie {
id: string;
title: string;
genre: string[];
year: number;
rating: number;
actors: string[];
director: string;
plot: string;
emoji: string;
}
class MovieSearchEngine {
private client: Client;
private genreEmojis = {
action: '๐ฅ',
comedy: '๐',
drama: '๐ญ',
horror: '๐ป',
romance: '๐',
scifi: '๐',
thriller: '๐ช',
animation: '๐จ'
};
constructor(client: Client) {
this.client = client;
}
// ๐๏ธ Create index with proper mappings
async createIndex() {
await this.client.indices.create({
index: 'movies',
body: {
mappings: {
properties: {
title: {
type: 'text',
fields: {
suggest: {
type: 'search_as_you_type'
}
}
},
genre: { type: 'keyword' },
year: { type: 'integer' },
rating: { type: 'float' },
actors: { type: 'text' },
director: { type: 'text' },
plot: { type: 'text' },
emoji: { type: 'keyword' }
}
}
}
});
console.log('๐ฌ Movie index created!');
}
// โ Index a movie
async indexMovie(movie: Omit<Movie, 'emoji'>) {
const emoji = this.genreEmojis[movie.genre[0]] || '๐ฌ';
const fullMovie: Movie = { ...movie, emoji };
await this.client.index({
index: 'movies',
id: movie.id,
body: fullMovie
});
console.log(`โ
Indexed: ${emoji} ${movie.title}`);
}
// ๐ Search movies with filters
async searchMovies(params: {
query?: string;
genres?: string[];
yearFrom?: number;
yearTo?: number;
minRating?: number;
sortBy?: 'rating' | 'year' | 'relevance';
}) {
const must: any[] = [];
const filter: any[] = [];
if (params.query) {
must.push({
multi_match: {
query: params.query,
fields: ['title^3', 'actors^2', 'director', 'plot'],
type: 'best_fields',
fuzziness: 'AUTO'
}
});
}
if (params.genres && params.genres.length > 0) {
filter.push({ terms: { genre: params.genres } });
}
if (params.yearFrom || params.yearTo) {
filter.push({
range: {
year: {
...(params.yearFrom && { gte: params.yearFrom }),
...(params.yearTo && { lte: params.yearTo })
}
}
});
}
if (params.minRating) {
filter.push({ range: { rating: { gte: params.minRating } } });
}
const sort: any[] = [];
if (params.sortBy === 'rating') {
sort.push({ rating: 'desc' });
} else if (params.sortBy === 'year') {
sort.push({ year: 'desc' });
}
sort.push('_score'); // Always include relevance
const response = await this.client.search<Movie>({
index: 'movies',
body: {
query: {
bool: {
must: must.length > 0 ? must : [{ match_all: {} }],
filter
}
},
sort
}
});
return response.body.hits.hits.map(hit => hit._source!);
}
// ๐ฏ Autocomplete suggestions
async getSuggestions(prefix: string) {
const response = await this.client.search({
index: 'movies',
body: {
size: 5,
query: {
multi_match: {
query: prefix,
type: 'bool_prefix',
fields: ['title.suggest', 'title.suggest._2gram', 'title.suggest._3gram']
}
},
_source: ['title', 'year', 'emoji']
}
});
return response.body.hits.hits.map(hit => hit._source);
}
// ๐ Get statistics
async getStats() {
const response = await this.client.search({
index: 'movies',
body: {
size: 0,
aggs: {
genre_distribution: {
terms: { field: 'genre', size: 20 }
},
rating_stats: {
stats: { field: 'rating' }
},
movies_by_decade: {
histogram: {
field: 'year',
interval: 10
}
}
}
}
});
return response.body.aggregations;
}
// ๐ฌ More like this
async getRecommendations(movieId: string) {
const response = await this.client.search<Movie>({
index: 'movies',
body: {
query: {
more_like_this: {
fields: ['genre', 'actors', 'director'],
like: [{
_index: 'movies',
_id: movieId
}],
min_term_freq: 1,
min_doc_freq: 1
}
}
}
});
return response.body.hits.hits.map(hit => hit._source!);
}
}
// ๐ฎ Test it out!
const movieEngine = new MovieSearchEngine(client);
// Search for sci-fi movies
const scifiMovies = await movieEngine.searchMovies({
query: 'space',
genres: ['scifi'],
minRating: 7,
sortBy: 'rating'
});
console.log('๐ Top sci-fi movies:');
scifiMovies.forEach(movie => {
console.log(` ${movie.emoji} ${movie.title} (${movie.year}) - โญ${movie.rating}`);
});
๐ Key Takeaways
Youโve learned so much! Hereโs what you can now do:
- โ Build powerful search engines with Elasticsearch and TypeScript ๐ช
- โ Create type-safe queries that catch errors at compile time ๐ก๏ธ
- โ Implement advanced features like faceted search and analytics ๐ฏ
- โ Handle real-time data with efficient indexing strategies ๐
- โ Scale your search to millions of documents! ๐
Remember: Elasticsearch + TypeScript = Search superpowers! The combination gives you speed, relevance, and type safety. ๐ค
๐ค Next Steps
Congratulations! ๐ Youโve mastered Elasticsearch integration with TypeScript!
Hereโs what to do next:
- ๐ป Build the movie search engine from the exercise
- ๐๏ธ Add Elasticsearch to an existing project
- ๐ Explore advanced features like machine learning
- ๐ Learn about Elasticsearch cluster management
Remember: Every search giant started with a single query. Keep building, keep searching, and most importantly, have fun! ๐
Happy searching! ๐๐โจ