Elasticsearch Practical Guide | Search· Aggregations
이 글의 핵심
Elasticsearch enables sub-second full-text search across millions of documents. This guide covers index design, search queries, aggregations, autocomplete, and practical Node.js integration patterns.
What Elasticsearch Does
Elasticsearch is a distributed search and analytics engine built on Apache Lucene:
PostgreSQL query: SELECT * FROM products WHERE name LIKE '%wireless headphones%'
??Scans every row, case-sensitive, no ranking
Elasticsearch: search "wireless headphones" in products
??Inverted index lookup (O(1)), relevance scoring, fuzzy matching,
handles "headphone" ??"headphones", returns best matches first
Use cases:
- Full-text product/content search
- Log analytics (ELK Stack: Elasticsearch + Logstash + Kibana)
- Autocomplete and suggestions
- Faceted search (filter by category, price range, brand)
- Real-time dashboards from event streams
Setup
# Docker ??easiest for development
docker run -d \
--name elasticsearch \
-p 9200:9200 \
-e "discovery.type=single-node" \
-e "xpack.security.enabled=false" \
elasticsearch:8.12.0
# Verify
curl http://localhost:9200
# { "name": "...", "version": { "number": "8.12.0", ... } }
# Node.js client
npm install @elastic/elasticsearch
// lib/elasticsearch.ts
import { Client } from '@elastic/elasticsearch';
export const es = new Client({
node: process.env.ELASTICSEARCH_URL || 'http://localhost:9200',
});
Index and Mappings
Define the schema before indexing documents:
// Create index with explicit mapping
await es.indices.create({
index: 'products',
body: {
settings: {
number_of_shards: 1,
number_of_replicas: 1,
analysis: {
analyzer: {
// Custom analyzer for product search
product_search: {
type: 'custom',
tokenizer: 'standard',
filter: ['lowercase', 'asciifolding', 'stop'],
},
},
},
},
mappings: {
properties: {
name: {
type: 'text',
analyzer: 'product_search',
fields: {
keyword: { type: 'keyword' }, // Exact matching and sorting
suggest: { type: 'completion' }, // Autocomplete
},
},
description: {
type: 'text',
analyzer: 'product_search',
},
price: { type: 'float' },
category: { type: 'keyword' }, // Exact match, aggregations
brand: { type: 'keyword' },
rating: { type: 'float' },
inStock: { type: 'boolean' },
tags: { type: 'keyword' },
createdAt: { type: 'date' },
},
},
},
});
Field type guide:
text??analyzed, for full-text search (tokenized, lowercased)keyword??not analyzed, for exact matching, sorting, aggregationsfloat,integer,long??numeric fieldsdate??date/datetime fieldsboolean??true/falsecompletion??for autocomplete/suggestions
CRUD Operations
// Index (create) a document
await es.index({
index: 'products',
id: '1', // Optional ??auto-generated if omitted
document: {
name: 'Sony WH-1000XM5 Wireless Headphones',
description: 'Industry-leading noise canceling with outstanding call quality',
price: 349.99,
category: 'Electronics',
brand: 'Sony',
rating: 4.8,
inStock: true,
tags: ['wireless', 'noise-canceling', 'headphones'],
createdAt: new Date().toISOString(),
},
});
// Bulk index ??much faster for large imports
const operations = products.flatMap(product => [
{ index: { _index: 'products', _id: product.id } },
product,
]);
await es.bulk({ operations });
// Get by ID
const doc = await es.get({ index: 'products', id: '1' });
console.log(doc._source); // The document
// Update (partial)
await es.update({
index: 'products',
id: '1',
doc: { price: 299.99, inStock: false },
});
// Delete
await es.delete({ index: 'products', id: '1' });
Search Queries
Full-Text Search
// Simple match query
const results = await es.search({
index: 'products',
query: {
match: {
name: 'wireless headphones', // Tokenized: ["wireless", "headphones"]
},
},
});
// Multi-field search with boosting
const results = await es.search({
index: 'products',
query: {
multi_match: {
query: 'wireless headphones',
fields: ['name^3', 'description^1', 'tags^2'], // name is 3x more important
type: 'best_fields',
fuzziness: 'AUTO', // Handle typos: "headphoens" ??"headphones"
},
},
});
Bool Query ??Combine Conditions
const results = await es.search({
index: 'products',
query: {
bool: {
must: [ // All must match (AND)
{ match: { name: 'headphones' } },
],
filter: [ // Must match, no scoring impact
{ term: { inStock: true } },
{ term: { category: 'Electronics' } },
{
range: {
price: { gte: 100, lte: 500 },
},
},
],
should: [ // Nice to have (boost score)
{ term: { brand: 'Sony' } },
{ range: { rating: { gte: 4.5 } } },
],
must_not: [ // Must NOT match
{ term: { brand: 'Unknown' } },
],
},
},
});
Pagination and Sorting
const results = await es.search({
index: 'products',
query: { match: { name: 'headphones' } },
from: 0, // Offset (page - 1) * size
size: 10, // Page size
sort: [
{ _score: 'desc' }, // Primary: relevance score
{ rating: 'desc' }, // Secondary: highest rated first
{ price: 'asc' }, // Tertiary: cheapest first
],
_source: ['name', 'price', 'rating', 'category'], // Only these fields
});
const hits = results.hits.hits;
const total = results.hits.total.value;
Aggregations
Aggregations compute summaries and analytics:
const results = await es.search({
index: 'products',
query: {
bool: {
filter: [{ term: { inStock: true } }],
},
},
size: 0, // Only return aggregations, no documents
aggs: {
// Category breakdown
by_category: {
terms: { field: 'category', size: 10 },
aggs: {
avg_price: { avg: { field: 'price' } },
avg_rating: { avg: { field: 'rating' } },
},
},
// Price histogram
price_ranges: {
histogram: {
field: 'price',
interval: 100, // Buckets: 0-100, 100-200, 200-300...
},
},
// Statistics
price_stats: {
stats: { field: 'price' }, // min, max, avg, count, sum
},
// Top brands
top_brands: {
terms: { field: 'brand', size: 5 },
},
},
});
// Access results
const categories = results.aggregations.by_category.buckets;
// [{ key: 'Electronics', doc_count: 245, avg_price: { value: 187.5 } }, ...]
Autocomplete / Suggestions
// Using completion suggester (defined in mapping as 'completion' type)
const suggestions = await es.search({
index: 'products',
suggest: {
product_suggest: {
prefix: 'wirel', // What the user has typed so far
completion: {
field: 'name.suggest', // The completion field
size: 5,
skip_duplicates: true,
fuzzy: { fuzziness: 1 }, // Allow 1 character difference
},
},
},
});
const options = suggestions.suggest.product_suggest[0].options;
// [{ text: 'Wireless Headphones', _score: 1 }, ...]
Node.js Integration Pattern
// services/search.ts
import { es } from '../lib/elasticsearch';
interface SearchParams {
query: string;
category?: string;
minPrice?: number;
maxPrice?: number;
inStock?: boolean;
page?: number;
pageSize?: number;
sortBy?: 'relevance' | 'price_asc' | 'price_desc' | 'rating';
}
export async function searchProducts(params: SearchParams) {
const {
query,
category,
minPrice,
maxPrice,
inStock,
page = 1,
pageSize = 20,
sortBy = 'relevance',
} = params;
const sort = {
relevance: [{ _score: 'desc' }],
price_asc: [{ price: 'asc' }],
price_desc: [{ price: 'desc' }],
rating: [{ rating: 'desc' }],
}[sortBy];
const filters: any[] = [];
if (category) filters.push({ term: { category } });
if (inStock !== undefined) filters.push({ term: { inStock } });
if (minPrice !== undefined || maxPrice !== undefined) {
filters.push({
range: {
price: {
...(minPrice !== undefined && { gte: minPrice }),
...(maxPrice !== undefined && { lte: maxPrice }),
},
},
});
}
const results = await es.search({
index: 'products',
query: {
bool: {
must: query
? [{
multi_match: {
query,
fields: ['name^3', 'description^1', 'tags^2'],
fuzziness: 'AUTO',
},
}]
: [{ match_all: {} }],
filter: filters,
},
},
sort,
from: (page - 1) * pageSize,
size: pageSize,
aggs: {
by_category: { terms: { field: 'category', size: 20 } },
price_stats: { stats: { field: 'price' } },
},
});
return {
products: results.hits.hits.map(hit => ({ id: hit._id, ...hit._source })),
total: results.hits.total.value,
totalPages: Math.ceil(results.hits.total.value / pageSize),
facets: {
categories: results.aggregations.by_category.buckets,
priceStats: results.aggregations.price_stats,
},
};
}
Syncing with a Primary Database
// Keep Elasticsearch in sync when the primary DB changes
import { prisma } from '../lib/prisma';
import { es } from '../lib/elasticsearch';
async function indexProduct(productId: number) {
const product = await prisma.product.findUnique({
where: { id: productId },
include: { category: true, tags: true },
});
if (!product) return;
await es.index({
index: 'products',
id: product.id.toString(),
document: {
name: product.name,
description: product.description,
price: product.price,
category: product.category.name,
brand: product.brand,
rating: product.rating,
inStock: product.stock > 0,
tags: product.tags.map(t => t.name),
createdAt: product.createdAt.toISOString(),
},
});
}
async function deleteProductIndex(productId: number) {
await es.delete({ index: 'products', id: productId.toString() });
}
// Call after DB operations
await prisma.product.update({ where: { id }, data: updateData });
await indexProduct(id); // Sync to Elasticsearch
Quick Reference
| Operation | Code |
|---|---|
| Create index | es.indices.create({ index, body: { settings, mappings } }) |
| Index document | es.index({ index, id, document }) |
| Bulk index | es.bulk({ operations }) |
| Full-text search | { match: { field: 'query' } } |
| Exact match | { term: { field: 'value' } } |
| Range filter | { range: { price: { gte: 10, lte: 100 } } } |
| Multiple conditions | { bool: { must, filter, should, must_not } } |
| Aggregate | { aggs: { name: { terms: { field: 'category' } } } } |
Related posts:
Frequently Asked Questions (FAQ)
Q. When would I use this in practice?
A. Elasticsearch enables sub-second full-text search across millions of documents. This guide covers index design, search queries, aggregations, autocomplete, and practical Node.js integration patterns.
Q. What should I read before this?
A. Follow the previous article or related articles links at the bottom of each post to learn in sequence.
Q. Where can I study this more deeply?
A. Check cppreference and the relevant library’s official documentation. The reference links at the end of the article are also worth using.
Related Articles (Internal Links)
Other articles related to this topic.
- SQL vs NoSQL Database Guide | MySQL· PostgreSQL
- Redis Complete Guide | Caching· Pub/Sub
- Docker Compose Complete Guide | Multi-Container Apps
Keywords Covered in This Article (Related Search Terms)
This article covers Elasticsearch, Search, Database, Node.js, TypeScript, Backend, Performance.