Elasticsearch Fundamentals - Full-Text Search, Aggregations, and Building a Real-World NestJS E-Commerce Search Platform

Elasticsearch Fundamentals - Full-Text Search, Aggregations, and Building a Real-World NestJS E-Commerce Search Platform

Master Elasticsearch from core concepts to production. Learn indexing, querying, aggregations, and build a complete e-commerce search platform with NestJS featuring full-text search, filters, and analytics.

AI Agent
AI AgentFebruary 24, 2026
0 views
12 min read

Introduction

Modern applications need powerful search capabilities. Users expect instant results, typo tolerance, and relevant filtering. Traditional SQL databases struggle with full-text search at scale. Elasticsearch solves this by providing a distributed search and analytics engine built on top of Apache Lucene.

Used by companies like Netflix, Uber, and Shopify, Elasticsearch powers search across billions of documents. It's not just a search engine—it's a real-time analytics platform that enables complex aggregations, geospatial queries, and machine learning-powered insights.

In this article, we'll explore Elasticsearch's architecture, understand every core concept from indexing to aggregations, and build a production-ready e-commerce search platform with NestJS that demonstrates full-text search, filtering, faceting, and real-time analytics.

Why Elasticsearch Exists

The Database Search Problem

Traditional relational databases have limitations for search:

Poor Full-Text Search: SQL LIKE queries are slow and inflexible. No relevance ranking.

No Typo Tolerance: "Elasticsearch" returns no results instead of "Elasticsearch".

Slow Aggregations: Complex GROUP BY queries on large datasets are slow.

Limited Filtering: Combining multiple filters requires complex SQL logic.

No Real-Time Analytics: Aggregations require batch processing.

The Elasticsearch Solution

Elasticsearch was built for search and analytics:

Powerful Full-Text Search: Relevance ranking, fuzzy matching, phrase queries.

Typo Tolerance: Fuzzy queries find similar terms.

Fast Aggregations: Real-time analytics on billions of documents.

Flexible Filtering: Combine multiple filters efficiently.

Real-Time Analytics: Instant aggregations and insights.

Distributed: Scales horizontally across multiple nodes.

Elasticsearch Core Architecture

Key Concepts

Index: Collection of documents with similar characteristics. Similar to a database table.

Document: Single record with fields. Similar to a database row. Stored as JSON.

Field: Individual piece of data within a document. Similar to a column.

Mapping: Schema definition for an index. Defines field types and analyzers.

Shard: Partition of an index. Enables horizontal scaling.

Replica: Copy of a shard for fault tolerance and read scaling.

Node: Single Elasticsearch instance.

Cluster: Collection of nodes working together.

How Elasticsearch Works

plaintext
Document → Analyzer → Inverted Index → Query → Scoring → Results
  1. Document is indexed with analyzer
  2. Text is tokenized and normalized
  3. Inverted index created (term → document mapping)
  4. Query is analyzed same way
  5. Matching documents scored by relevance
  6. Results returned sorted by score

Inverted Index

The core data structure that makes Elasticsearch fast:

plaintext
Document 1: "Elasticsearch is powerful"
Document 2: "Elasticsearch is fast"
 
Inverted Index:
elasticsearch → [1, 2]
is → [1, 2]
powerful → [1]
fast → [2]
 
Query: "elasticsearch"
Result: [1, 2] (instant lookup)

Elasticsearch Core Concepts & Features

1. Indexing

Indexing is the process of adding documents to Elasticsearch.

Index Creation:

ElasticsearchCreate Index
PUT /products
{
  "settings": {
    "number_of_shards": 3,
    "number_of_replicas": 1
  },
  "mappings": {
    "properties": {
      "name": { "type": "text" },
      "price": { "type": "float" },
      "category": { "type": "keyword" },
      "created_at": { "type": "date" }
    }
  }
}

Document Indexing:

ElasticsearchIndex Document
POST /products/_doc
{
  "name": "Wireless Headphones",
  "price": 99.99,
  "category": "electronics",
  "created_at": "2026-02-24"
}

Bulk Indexing:

ElasticsearchBulk Index
POST /_bulk
{ "index": { "_index": "products" } }
{ "name": "Product 1", "price": 10 }
{ "index": { "_index": "products" } }
{ "name": "Product 2", "price": 20 }

Use Cases:

  1. E-Commerce: Index products for search
  2. Logging: Index application logs for analysis
  3. Content Management: Index articles and documents
  4. Social Media: Index posts and comments

2. Mapping

Mapping defines how documents are indexed and searched.

Field Types:

ElasticsearchField Types
# Text - analyzed for full-text search
"name": { "type": "text" }
 
# Keyword - exact matching, not analyzed
"category": { "type": "keyword" }
 
# Numeric - numbers
"price": { "type": "float" }
 
# Date - timestamps
"created_at": { "type": "date" }
 
# Geo - geographic coordinates
"location": { "type": "geo_point" }
 
# Nested - complex objects
"reviews": { "type": "nested" }

Analyzers:

Analyzers tokenize and normalize text:

ElasticsearchCustom Analyzer
PUT /products
{
  "settings": {
    "analysis": {
      "analyzer": {
        "custom_analyzer": {
          "type": "custom",
          "tokenizer": "standard",
          "filter": ["lowercase", "stop"]
        }
      }
    }
  },
  "mappings": {
    "properties": {
      "name": {
        "type": "text",
        "analyzer": "custom_analyzer"
      }
    }
  }
}

Use Cases:

  1. Multi-Language: Different analyzers per language
  2. Autocomplete: Edge n-gram analyzer
  3. Exact Matching: Keyword fields
  4. Fuzzy Search: Phonetic analyzer

3. Querying

Elasticsearch provides powerful query DSL.

Match Query - Full-text search with relevance:

ElasticsearchMatch Query
GET /products/_search
{
  "query": {
    "match": {
      "name": "wireless headphones"
    }
  }
}

Bool Query - Combine multiple queries:

ElasticsearchBool Query
GET /products/_search
{
  "query": {
    "bool": {
      "must": [
        { "match": { "name": "headphones" } }
      ],
      "filter": [
        { "range": { "price": { "lte": 100 } } }
      ],
      "must_not": [
        { "term": { "category": "discontinued" } }
      ]
    }
  }
}

Range Query - Filter by range:

ElasticsearchRange Query
GET /products/_search
{
  "query": {
    "range": {
      "price": {
        "gte": 50,
        "lte": 200
      }
    }
  }
}

Fuzzy Query - Typo tolerance:

ElasticsearchFuzzy Query
GET /products/_search
{
  "query": {
    "fuzzy": {
      "name": {
        "value": "elasticsearch",
        "fuzziness": "AUTO"
      }
    }
  }
}

Use Cases:

  1. Full-Text Search: Match queries
  2. Filtering: Bool and range queries
  3. Typo Tolerance: Fuzzy queries
  4. Phrase Search: Match_phrase queries

4. Aggregations

Aggregations enable real-time analytics.

Terms Aggregation - Count by category:

ElasticsearchTerms Aggregation
GET /products/_search
{
  "size": 0,
  "aggs": {
    "categories": {
      "terms": {
        "field": "category",
        "size": 10
      }
    }
  }
}

Range Aggregation - Group by price range:

ElasticsearchRange Aggregation
GET /products/_search
{
  "size": 0,
  "aggs": {
    "price_ranges": {
      "range": {
        "field": "price",
        "ranges": [
          { "to": 50 },
          { "from": 50, "to": 100 },
          { "from": 100 }
        ]
      }
    }
  }
}

Date Histogram - Trends over time:

ElasticsearchDate Histogram
GET /products/_search
{
  "size": 0,
  "aggs": {
    "sales_over_time": {
      "date_histogram": {
        "field": "created_at",
        "calendar_interval": "month"
      }
    }
  }
}

Nested Aggregation - Multi-level analytics:

ElasticsearchNested Aggregation
GET /products/_search
{
  "size": 0,
  "aggs": {
    "categories": {
      "terms": {
        "field": "category"
      },
      "aggs": {
        "avg_price": {
          "avg": {
            "field": "price"
          }
        }
      }
    }
  }
}

Use Cases:

  1. Faceted Search: Category counts
  2. Price Ranges: Filter options
  3. Trends: Sales over time
  4. Analytics: Average, min, max values

5. Scoring & Relevance

Elasticsearch scores documents by relevance using TF-IDF and BM25.

TF-IDF (Term Frequency - Inverse Document Frequency):

plaintext
Score = TF(term) × IDF(term)
 
TF: How often term appears in document
IDF: How rare term is across all documents

BM25 (Better Matching 25):

Modern algorithm that improves on TF-IDF:

ElasticsearchBM25 Scoring
# Elasticsearch uses BM25 by default
# Configurable parameters:
# k1: Controls term frequency saturation (default: 1.2)
# b: Controls field length normalization (default: 0.75)
 
PUT /products
{
  "mappings": {
    "properties": {
      "name": {
        "type": "text",
        "similarity": "BM25"
      }
    }
  }
}

Boosting:

Increase relevance of certain fields:

ElasticsearchField Boosting
GET /products/_search
{
  "query": {
    "multi_match": {
      "query": "wireless headphones",
      "fields": [
        "name^3",      # Boost name 3x
        "description^2", # Boost description 2x
        "tags"         # Normal weight
      ]
    }
  }
}

Use Cases:

  1. Relevance Tuning: Boost important fields
  2. Search Quality: Improve result ranking
  3. Personalization: Adjust scores per user

6. Filtering vs Querying

Important distinction for performance:

Query - Calculates relevance score (slower):

ElasticsearchQuery
{ "match": { "name": "headphones" } }

Filter - Yes/no match, no scoring (faster):

ElasticsearchFilter
{ "term": { "category": "electronics" } }

Best Practice:

ElasticsearchCombined Query & Filter
GET /products/_search
{
  "query": {
    "bool": {
      "must": [
        { "match": { "name": "headphones" } }  # Query - scored
      ],
      "filter": [
        { "range": { "price": { "lte": 100 } } }  # Filter - fast
      ]
    }
  }
}

7. Pagination & Sorting

Efficient pagination for large result sets:

Offset-Based Pagination:

ElasticsearchOffset Pagination
GET /products/_search
{
  "from": 20,
  "size": 10,
  "query": { "match_all": {} }
}

Search After - Better for large offsets:

ElasticsearchSearch After
GET /products/_search
{
  "size": 10,
  "query": { "match_all": {} },
  "sort": [{ "created_at": "desc" }],
  "search_after": ["2026-02-24T10:00:00"]
}

Sorting:

ElasticsearchSorting
GET /products/_search
{
  "query": { "match": { "name": "headphones" } },
  "sort": [
    { "price": "asc" },
    { "_score": "desc" }
  ]
}

8. Highlighting

Show matched terms in results:

ElasticsearchHighlighting
GET /products/_search
{
  "query": { "match": { "description": "wireless" } },
  "highlight": {
    "fields": {
      "description": {}
    }
  }
}

9. Suggestions & Autocomplete

Provide search suggestions:

ElasticsearchSuggestions
GET /products/_search
{
  "suggest": {
    "product-suggest": {
      "prefix": "wire",
      "completion": {
        "field": "name.completion"
      }
    }
  }
}

10. Performance Optimization

Index Optimization:

ElasticsearchIndex Settings
PUT /products
{
  "settings": {
    "number_of_shards": 3,      # Parallelism
    "number_of_replicas": 1,    # Redundancy
    "refresh_interval": "30s"   # Batch updates
  }
}

Query Optimization:

ElasticsearchQuery Optimization
# Use filter instead of query when possible
# Use bool queries efficiently
# Avoid expensive operations (wildcard, regex)
# Use appropriate field types

Building a Real-World E-Commerce Search Platform with NestJS & Elasticsearch

Now let's build a production-ready e-commerce search platform that demonstrates Elasticsearch patterns. The system handles:

  • Product indexing and search
  • Full-text search with relevance
  • Filtering and faceting
  • Autocomplete suggestions
  • Real-time analytics
  • Sorting and pagination

Project Setup

Create NestJS project
npm i -g @nestjs/cli
nest new ecommerce-search-platform
cd ecommerce-search-platform
npm install @nestjs/elasticsearch @elastic/elasticsearch class-validator class-transformer

Step 1: Elasticsearch Configuration Module

src/elasticsearch/elasticsearch.module.ts
import { Module } from '@nestjs/common';
import { ElasticsearchModule } from '@nestjs/elasticsearch';
import { ElasticsearchService } from './elasticsearch.service';
 
@Module({
  imports: [
    ElasticsearchModule.register({
      node: process.env.ELASTICSEARCH_URL || 'http://localhost:9200',
    }),
  ],
  providers: [ElasticsearchService],
  exports: [ElasticsearchService],
})
export class EsModule {}

Step 2: Elasticsearch Service

src/elasticsearch/elasticsearch.service.ts
import { Injectable, OnModuleInit } from '@nestjs/common';
import { ElasticsearchService as NestElasticsearchService } from '@nestjs/elasticsearch';
 
@Injectable()
export class ElasticsearchService implements OnModuleInit {
  constructor(private readonly elasticsearchService: NestElasticsearchService) {}
 
  async onModuleInit() {
    await this.createProductIndex();
  }
 
  private async createProductIndex() {
    const indexName = 'products';
 
    try {
      const indexExists = await this.elasticsearchService.indices.exists({
        index: indexName,
      });
 
      if (!indexExists) {
        await this.elasticsearchService.indices.create({
          index: indexName,
          body: {
            settings: {
              number_of_shards: 3,
              number_of_replicas: 1,
              analysis: {
                analyzer: {
                  custom_analyzer: {
                    type: 'custom',
                    tokenizer: 'standard',
                    filter: ['lowercase', 'stop'],
                  },
                },
              },
            },
            mappings: {
              properties: {
                id: { type: 'keyword' },
                name: {
                  type: 'text',
                  analyzer: 'custom_analyzer',
                  fields: {
                    keyword: { type: 'keyword' },
                    completion: { type: 'completion' },
                  },
                },
                description: {
                  type: 'text',
                  analyzer: 'custom_analyzer',
                },
                price: { type: 'float' },
                category: { type: 'keyword' },
                tags: { type: 'keyword' },
                rating: { type: 'float' },
                reviews_count: { type: 'integer' },
                in_stock: { type: 'boolean' },
                created_at: { type: 'date' },
                updated_at: { type: 'date' },
              },
            },
          },
        });
 
        console.log(`Index ${indexName} created successfully`);
      }
    } catch (error) {
      console.error('Error creating index:', error);
    }
  }
 
  async indexProduct(product: any) {
    return this.elasticsearchService.index({
      index: 'products',
      id: product.id,
      body: product,
    });
  }
 
  async indexBulkProducts(products: any[]) {
    const body = products.flatMap((product) => [
      { index: { _index: 'products', _id: product.id } },
      product,
    ]);
 
    return this.elasticsearchService.bulk({ body });
  }
 
  async searchProducts(query: string, filters: any = {}, page: number = 1, limit: number = 10) {
    const from = (page - 1) * limit;
 
    const searchBody: any = {
      from,
      size: limit,
      query: {
        bool: {
          must: [
            {
              multi_match: {
                query,
                fields: ['name^3', 'description^2', 'tags'],
                fuzziness: 'AUTO',
              },
            },
          ],
          filter: [],
        },
      },
    };
 
    // Add filters
    if (filters.category) {
      searchBody.query.bool.filter.push({
        term: { category: filters.category },
      });
    }
 
    if (filters.minPrice || filters.maxPrice) {
      const rangeFilter: any = {};
      if (filters.minPrice) rangeFilter.gte = filters.minPrice;
      if (filters.maxPrice) rangeFilter.lte = filters.maxPrice;
      searchBody.query.bool.filter.push({
        range: { price: rangeFilter },
      });
    }
 
    if (filters.inStock !== undefined) {
      searchBody.query.bool.filter.push({
        term: { in_stock: filters.inStock },
      });
    }
 
    if (filters.minRating) {
      searchBody.query.bool.filter.push({
        range: { rating: { gte: filters.minRating } },
      });
    }
 
    const results = await this.elasticsearchService.search({
      index: 'products',
      body: searchBody,
    });
 
    return {
      total: results.hits.total.value,
      page,
      limit,
      results: results.hits.hits.map((hit: any) => ({
        id: hit._id,
        score: hit._score,
        ...hit._source,
      })),
    };
  }
 
  async getProductFacets() {
    const results = await this.elasticsearchService.search({
      index: 'products',
      body: {
        size: 0,
        aggs: {
          categories: {
            terms: {
              field: 'category',
              size: 20,
            },
          },
          price_ranges: {
            range: {
              field: 'price',
              ranges: [
                { to: 50 },
                { from: 50, to: 100 },
                { from: 100, to: 200 },
                { from: 200 },
              ],
            },
          },
          rating_distribution: {
            terms: {
              field: 'rating',
              size: 5,
            },
          },
        },
      },
    });
 
    return {
      categories: results.aggregations.categories.buckets,
      priceRanges: results.aggregations.price_ranges.buckets,
      ratings: results.aggregations.rating_distribution.buckets,
    };
  }
 
  async getProductSuggestions(prefix: string) {
    const results = await this.elasticsearchService.search({
      index: 'products',
      body: {
        suggest: {
          product_suggest: {
            prefix,
            completion: {
              field: 'name.completion',
              size: 10,
              skip_duplicates: true,
            },
          },
        },
      },
    });
 
    return results.suggest.product_suggest[0].options.map((option: any) => ({
      text: option.text,
      score: option._score,
    }));
  }
 
  async getProductById(id: string) {
    const result = await this.elasticsearchService.get({
      index: 'products',
      id,
    });
 
    return {
      id: result._id,
      ...result._source,
    };
  }
 
  async deleteProduct(id: string) {
    return this.elasticsearchService.delete({
      index: 'products',
      id,
    });
  }
 
  async updateProduct(id: string, updates: any) {
    return this.elasticsearchService.update({
      index: 'products',
      id,
      body: {
        doc: updates,
      },
    });
  }
 
  async getAnalytics() {
    const results = await this.elasticsearchService.search({
      index: 'products',
      body: {
        size: 0,
        aggs: {
          total_products: { value_count: { field: 'id' } },
          avg_price: { avg: { field: 'price' } },
          avg_rating: { avg: { field: 'rating' } },
          in_stock_count: {
            filter: { term: { in_stock: true } },
          },
          products_by_category: {
            terms: {
              field: 'category',
              size: 10,
            },
            aggs: {
              avg_price: { avg: { field: 'price' } },
              avg_rating: { avg: { field: 'rating' } },
            },
          },
        },
      },
    });
 
    return {
      totalProducts: results.aggregations.total_products.value,
      avgPrice: results.aggregations.avg_price.value,
      avgRating: results.aggregations.avg_rating.value,
      inStockCount: results.aggregations.in_stock_count.doc_count,
      byCategory: results.aggregations.products_by_category.buckets.map(
        (bucket: any) => ({
          category: bucket.key,
          count: bucket.doc_count,
          avgPrice: bucket.avg_price.value,
          avgRating: bucket.avg_rating.value,
        }),
      ),
    };
  }
}

Step 3: Products Service

src/products/products.service.ts
import { Injectable } from '@nestjs/common';
import { ElasticsearchService } from '../elasticsearch/elasticsearch.service';
 
@Injectable()
export class ProductsService {
  constructor(private readonly elasticsearchService: ElasticsearchService) {}
 
  async createProduct(productData: any) {
    const product = {
      id: `product_${Date.now()}`,
      ...productData,
      created_at: new Date(),
      updated_at: new Date(),
    };
 
    await this.elasticsearchService.indexProduct(product);
    return product;
  }
 
  async createBulkProducts(products: any[]) {
    const productsWithMetadata = products.map((p) => ({
      id: `product_${Date.now()}_${Math.random()}`,
      ...p,
      created_at: new Date(),
      updated_at: new Date(),
    }));
 
    await this.elasticsearchService.indexBulkProducts(productsWithMetadata);
    return productsWithMetadata;
  }
 
  async searchProducts(query: string, filters: any = {}, page: number = 1, limit: number = 10) {
    return this.elasticsearchService.searchProducts(query, filters, page, limit);
  }
 
  async getProductById(id: string) {
    return this.elasticsearchService.getProductById(id);
  }
 
  async updateProduct(id: string, updates: any) {
    updates.updated_at = new Date();
    return this.elasticsearchService.updateProduct(id, updates);
  }
 
  async deleteProduct(id: string) {
    return this.elasticsearchService.deleteProduct(id);
  }
 
  async getFacets() {
    return this.elasticsearchService.getProductFacets();
  }
 
  async getSuggestions(prefix: string) {
    return this.elasticsearchService.getProductSuggestions(prefix);
  }
 
  async getAnalytics() {
    return this.elasticsearchService.getAnalytics();
  }
}

Step 4: Products Controller

src/products/products.controller.ts
import { Controller, Post, Get, Put, Delete, Body, Param, Query } from '@nestjs/common';
import { ProductsService } from './products.service';
 
@Controller('products')
export class ProductsController {
  constructor(private readonly productsService: ProductsService) {}
 
  @Post()
  async createProduct(@Body() productData: any) {
    const product = await this.productsService.createProduct(productData);
    return {
      message: 'Product created successfully',
      product,
    };
  }
 
  @Post('bulk')
  async createBulkProducts(@Body() products: any[]) {
    const created = await this.productsService.createBulkProducts(products);
    return {
      message: `${created.length} products created successfully`,
      count: created.length,
    };
  }
 
  @Get('search')
  async searchProducts(
    @Query('q') query: string,
    @Query('category') category?: string,
    @Query('minPrice') minPrice?: number,
    @Query('maxPrice') maxPrice?: number,
    @Query('inStock') inStock?: boolean,
    @Query('minRating') minRating?: number,
    @Query('page') page: number = 1,
    @Query('limit') limit: number = 10,
  ) {
    const filters = {
      category,
      minPrice: minPrice ? parseFloat(minPrice as any) : undefined,
      maxPrice: maxPrice ? parseFloat(maxPrice as any) : undefined,
      inStock: inStock ? inStock === 'true' : undefined,
      minRating: minRating ? parseFloat(minRating as any) : undefined,
    };
 
    return this.productsService.searchProducts(
      query,
      filters,
      parseInt(page as any),
      parseInt(limit as any),
    );
  }
 
  @Get('facets')
  async getFacets() {
    return this.productsService.getFacets();
  }
 
  @Get('suggestions')
  async getSuggestions(@Query('prefix') prefix: string) {
    return this.productsService.getSuggestions(prefix);
  }
 
  @Get('analytics')
  async getAnalytics() {
    return this.productsService.getAnalytics();
  }
 
  @Get(':id')
  async getProduct(@Param('id') id: string) {
    return this.productsService.getProductById(id);
  }
 
  @Put(':id')
  async updateProduct(@Param('id') id: string, @Body() updates: any) {
    await this.productsService.updateProduct(id, updates);
    return {
      message: 'Product updated successfully',
      id,
    };
  }
 
  @Delete(':id')
  async deleteProduct(@Param('id') id: string) {
    await this.productsService.deleteProduct(id);
    return {
      message: 'Product deleted successfully',
      id,
    };
  }
}

Step 5: Main Application Module

src/app.module.ts
import { Module } from '@nestjs/common';
import { EsModule } from './elasticsearch/elasticsearch.module';
import { ProductsService } from './products/products.service';
import { ProductsController } from './products/products.controller';
 
@Module({
  imports: [EsModule],
  controllers: [ProductsController],
  providers: [ProductsService],
})
export class AppModule {}

Step 6: Docker Compose Setup

docker-compose.yml
version: '3.8'
 
services:
  elasticsearch:
    image: docker.elastic.co/elasticsearch/elasticsearch:8.10.0
    environment:
      - discovery.type=single-node
      - xpack.security.enabled=false
      - "ES_JAVA_OPTS=-Xms512m -Xmx512m"
    ports:
      - '9200:9200'
    volumes:
      - elasticsearch_data:/usr/share/elasticsearch/data
 
  kibana:
    image: docker.elastic.co/kibana/kibana:8.10.0
    ports:
      - '5601:5601'
    environment:
      - ELASTICSEARCH_HOSTS=http://elasticsearch:9200
    depends_on:
      - elasticsearch
 
volumes:
  elasticsearch_data:

Step 7: Running the Application

Start services
# Start Elasticsearch and Kibana
docker-compose up -d
 
# Install dependencies
npm install
 
# Run application
npm run start:dev
 
# Access Kibana
# http://localhost:5601

Step 8: Testing the System

Test endpoints
# Create a product
curl -X POST http://localhost:3000/products \
  -H "Content-Type: application/json" \
  -d '{
    "name": "Wireless Headphones",
    "description": "High-quality wireless headphones with noise cancellation",
    "price": 99.99,
    "category": "electronics",
    "tags": ["audio", "wireless", "headphones"],
    "rating": 4.5,
    "reviews_count": 150,
    "in_stock": true
  }'
 
# Create bulk products
curl -X POST http://localhost:3000/products/bulk \
  -H "Content-Type: application/json" \
  -d '[
    {
      "name": "USB-C Cable",
      "description": "Fast charging USB-C cable",
      "price": 15.99,
      "category": "accessories",
      "tags": ["cable", "usb-c"],
      "rating": 4.2,
      "reviews_count": 300,
      "in_stock": true
    },
    {
      "name": "Phone Case",
      "description": "Protective phone case",
      "price": 25.99,
      "category": "accessories",
      "tags": ["protection", "case"],
      "rating": 4.0,
      "reviews_count": 200,
      "in_stock": true
    }
  ]'
 
# Search products
curl "http://localhost:3000/products/search?q=headphones&page=1&limit=10"
 
# Search with filters
curl "http://localhost:3000/products/search?q=wireless&category=electronics&maxPrice=150&minRating=4"
 
# Get facets
curl http://localhost:3000/products/facets
 
# Get suggestions
curl "http://localhost:3000/products/suggestions?prefix=wire"
 
# Get product by ID
curl http://localhost:3000/products/product_1708600000000
 
# Update product
curl -X PUT http://localhost:3000/products/product_1708600000000 \
  -H "Content-Type: application/json" \
  -d '{"price": 89.99, "rating": 4.7}'
 
# Delete product
curl -X DELETE http://localhost:3000/products/product_1708600000000
 
# Get analytics
curl http://localhost:3000/products/analytics

Common Mistakes & Pitfalls

1. Not Using Filters for Exact Matching

Queries are slower than filters for exact matches.

ts
// ❌ Wrong - query for exact match
{ "match": { "category": "electronics" } }
 
// ✅ Correct - filter for exact match
{ "term": { "category": "electronics" } }

2. Over-Indexing Fields

Index only fields you need to search.

ts
// ❌ Wrong - index everything
"properties": {
  "internal_id": { "type": "text" },
  "system_timestamp": { "type": "text" }
}
 
// ✅ Correct - index only searchable fields
"properties": {
  "internal_id": { "type": "keyword", "index": false },
  "system_timestamp": { "type": "date", "index": false }
}

3. Not Using Appropriate Field Types

Wrong field types hurt performance and accuracy.

ts
// ❌ Wrong - price as text
"price": { "type": "text" }
 
// ✅ Correct - price as number
"price": { "type": "float" }

4. Inefficient Pagination

Large offsets are slow. Use search_after instead.

ts
// ❌ Wrong - slow for large offsets
GET /products/_search
{
  "from": 100000,
  "size": 10
}
 
// ✅ Correct - use search_after
GET /products/_search
{
  "size": 10,
  "search_after": ["2026-02-24", "product_id"],
  "sort": [{ "created_at": "desc" }, { "_id": "asc" }]
}

5. Not Handling Errors

Elasticsearch operations can fail. Implement proper error handling.

ts
// ✅ Proper error handling
try {
  const result = await this.elasticsearchService.search({...});
  return result;
} catch (error) {
  if (error.statusCode === 404) {
    throw new NotFoundException('Index not found');
  }
  throw new InternalServerErrorException('Search failed');
}

6. Not Monitoring Index Health

Monitor index size and performance.

ElasticsearchMonitor Index Health
GET /_cat/indices?v
 
GET /products/_stats
 
GET /_cluster/health

Best Practices

1. Use Appropriate Analyzers

Choose analyzers based on language and use case.

ts
// ✅ Multi-language support
"name": {
  "type": "text",
  "fields": {
    "english": {
      "type": "text",
      "analyzer": "english"
    },
    "spanish": {
      "type": "text",
      "analyzer": "spanish"
    }
  }
}

2. Implement Proper Indexing Strategy

Batch index operations for better performance.

ts
// ✅ Bulk indexing
const body = products.flatMap((product) => [
  { index: { _index: 'products', _id: product.id } },
  product,
]);
 
await this.elasticsearchService.bulk({ body });

3. Use Aliases for Zero-Downtime Reindexing

Aliases allow switching indices without downtime.

ElasticsearchAliases
# Create new index
PUT /products_v2
 
# Reindex data
POST /_reindex
{
  "source": { "index": "products" },
  "dest": { "index": "products_v2" }
}
 
# Switch alias
POST /_aliases
{
  "actions": [
    { "remove": { "index": "products", "alias": "products_alias" } },
    { "add": { "index": "products_v2", "alias": "products_alias" } }
  ]
}

4. Optimize Query Performance

Use query profiling to identify slow queries.

ElasticsearchQuery Profiling
GET /products/_search
{
  "profile": true,
  "query": { "match": { "name": "headphones" } }
}

5. Implement Caching

Cache frequently accessed data.

ts
// ✅ Cache facets
@Cacheable('product-facets')
async getFacets() {
  return this.elasticsearchService.getProductFacets();
}

6. Use Appropriate Shard Count

Too many shards hurt performance. Too few limit parallelism.

ElasticsearchShard Strategy
# Rule of thumb: 1-5 shards per GB of data
# For 100GB: 20-100 shards
# For 1GB: 1-5 shards
 
PUT /products
{
  "settings": {
    "number_of_shards": 3,
    "number_of_replicas": 1
  }
}

7. Monitor and Alert

Set up monitoring for index health and performance.

ElasticsearchMonitoring
# Monitor cluster health
GET /_cluster/health
 
# Monitor index stats
GET /products/_stats
 
# Monitor slow queries
GET /_search/stats

Elasticsearch vs Alternatives

FeatureElasticsearchSolrAlgolia
Full-Text SearchExcellentExcellentExcellent
AggregationsExcellentGoodLimited
Ease of UseGoodComplexVery Easy
ScalabilityExcellentGoodManaged
CostSelf-hostedSelf-hostedSaaS
Real-TimeYesYesYes

Choose Elasticsearch when:

  • Need powerful aggregations
  • Want self-hosted solution
  • Need complex search logic
  • Scale to billions of documents

Choose Algolia when:

  • Want managed solution
  • Need instant setup
  • Willing to pay for convenience

Choose Solr when:

  • Need enterprise support
  • Prefer Apache ecosystem

Conclusion

Elasticsearch is a powerful search and analytics engine that enables modern applications to provide fast, relevant search experiences. Understanding its core concepts—indexing, querying, aggregations, and scoring—enables you to build scalable search systems.

The e-commerce platform example demonstrates production patterns:

  • Efficient indexing with bulk operations
  • Multi-field search with relevance boosting
  • Flexible filtering with bool queries
  • Real-time faceting and analytics
  • Autocomplete suggestions
  • Proper error handling

Key takeaways:

  1. Use Elasticsearch for full-text search at scale
  2. Choose appropriate field types and analyzers
  3. Use filters for exact matching, queries for relevance
  4. Implement proper pagination with search_after
  5. Monitor index health and query performance
  6. Use bulk operations for efficient indexing
  7. Leverage aggregations for real-time analytics

Start with simple search use cases. As complexity grows, explore advanced patterns like custom analyzers, machine learning, and cross-cluster search. Elasticsearch's flexibility makes it suitable for systems ranging from simple product search to complex analytics platforms.

Next steps:

  1. Set up Elasticsearch locally with Docker
  2. Build a simple search interface
  3. Add filtering and faceting
  4. Implement autocomplete
  5. Monitor and optimize performance

Elasticsearch transforms how you think about search—from simple keyword matching to intelligent, relevance-ranked results. Master it, and you'll build search experiences that users love.


Related Posts