Skip to content

Elasticsearch từ Cơ Bản đến Nâng Cao ​

Mục Lục ​

Phần 1: Nền Tảng ​

Phần 2: Thao Tác Cơ Bản ​

Phần 3: Tìm Kiếm ​

Phần 4: Phân Tích Văn Bản ​

  • Chương 8: Text Analysis
    • Analyzer pipeline
    • Built-in analyzers
    • Custom analyzers
    • Tokenizers và Token filters
    • Xử lý tiếng Việt

Phần 5: Aggregations ​

  • Chương 9: Aggregations
    • Metric aggregations
    • Bucket aggregations
    • Pipeline aggregations
    • Nested aggregations
    • Thực hành với dữ liệu thực tế

Phần 6: Hiệu Năng và Sản Xuất ​

Phần 7: Thực Chiến ​


Chương 1: Giới thiệu Elasticsearch ​

1.1 Elasticsearch là gì? ​

Elasticsearch (ES) là một search engine phân tán, mã nguồn mở, được xây dựng trên nền tảng Apache Lucene. Nó được phát triển bởi Shay Banon và lần đầu release vào năm 2010. Elasticsearch cho phép bạn lưu trữ, tìm kiếm và phân tích lượng lớn dữ liệu một cách nhanh chóng và theo thời gian thực (near real-time).

Định nghĩa kỹ thuật ​

Elasticsearch là:

  • Search Engine: Tối ưu hóa cho việc tìm kiếm full-text với độ liên quan (relevance scoring)
  • Distributed Database: Tự phân tán dữ liệu trên nhiều node
  • NoSQL Document Store: Lưu trữ JSON documents, không cần schema cứng
  • Analytics Engine: Cung cấp aggregation framework mạnh mẽ
  • REST API First: Toàn bộ tương tác qua HTTP/JSON API

Elasticsearch trong Elastic Stack ​

Elasticsearch thường được sử dụng cùng với các công cụ khác trong Elastic Stack (trước đây gọi là ELK Stack):

┌─────────────────────────────────────────────────────────────┐
│                        ELASTIC STACK                        │
│                                                             │
│  ┌──────────┐    ┌──────────┐    ┌──────────────────────┐  │
│  │  Beats   │    │ Logstash │    │    Kibana (UI)        │  │
│  │(Agents)  │───>│(Pipeline)│───>│  Visualization &     │  │
│  └──────────┘    └──────────┘    │  Dashboards          │  │
│                                  └──────────────────────┘  │
│                         │                    │              │
│                         ▼                    ▼              │
│                  ┌──────────────────────────────────────┐  │
│                  │         ELASTICSEARCH                 │  │
│                  │   Search + Storage + Analytics       │  │
│                  └──────────────────────────────────────┘  │
└─────────────────────────────────────────────────────────────┘
  • Beats: Lightweight data shippers (Filebeat, Metricbeat, Packetbeat...)
  • Logstash: Data processing pipeline (ETL)
  • Elasticsearch: Core search và storage engine
  • Kibana: Visualization và management UI

1.2 Tại sao cần Elasticsearch? ​

Vấn đề với SQL LIKE và Full-Text Search truyền thống ​

Hầu hết backend developers bắt đầu với MySQL/PostgreSQL và sử dụng LIKE operator:

sql
-- Tìm sản phẩm có tên chứa "điện thoại"
SELECT * FROM products WHERE name LIKE '%điện thoại%';

-- Hoặc PostgreSQL full-text
SELECT * FROM products 
WHERE to_tsvector('english', description) @@ to_tsquery('smartphone');

Vấn đề:

Vấn đềGiải thích
Hiệu năng kémLIKE '%keyword%' không dùng index, full table scan
Không có rankingKhông biết document nào "liên quan hơn"
Ngôn ngữ hạn chếKhó xử lý tiếng Việt có dấu, từ đồng nghĩa
Không scalable10M+ rows thì rất chậm
Không có analyticsKhó làm báo cáo phức tạp
Không real-timeĐặc biệt với MySQL fulltext index

Elasticsearch giải quyết các vấn đề này ​

Tìm kiếm: "điện thoại samsung màn hình lớn pin trâu"
                          │
                    Elasticsearch
                          │
              ┌───────────┴───────────┐
              │                       │
       Phân tích query          Inverted Index
    (tokenize, analyze)         (pre-built)
              │                       │
              └───────────┬───────────┘
                          │
                  Kết quả có ranking
                  (relevance score)
                          │
              ┌───────────┴───────────┐
              │ Score: 0.95           │ Score: 0.87
              │ Samsung S24 Ultra     │ Samsung A55
              │ "điện thoại màn lớn   │ "màn hình 6.4 inch
              │  pin 5000mAh"         │  pin 5000mAh"
              └───────────────────────┘

Lợi ích cụ thể:

  1. Tốc độ: Tìm kiếm trong hàng triệu documents trong < 100ms
  2. Relevance Scoring: Tự động tính điểm liên quan (TF-IDF, BM25)
  3. Text Analysis: Hiểu ngôn ngữ tự nhiên, dấu câu, từ đồng nghĩa
  4. Distributed: Tự phân tán, scale horizontally
  5. Near Real-Time: Document được index trong ~1 giây
  6. Aggregations: Faceted search, analytics dashboard

1.3 Các Use Case Phù Hợp ​

Bài toán: Website bán hàng như Shopee/Tiki với 10 triệu sản phẩm. Người dùng search "điện thoại samsung 5G dưới 10 triệu".

Yêu cầu:

  • Full-text search với typo tolerance
  • Filter theo giá, brand, category
  • Faceted search (hiển thị số lượng theo category)
  • Spell correction: "samsumg" → "samsung"
  • Autocomplete khi người dùng gõ

Tại sao MySQL không đủ:

sql
-- Query này sẽ rất chậm và không có ranking
SELECT * FROM products 
WHERE (name LIKE '%samsung%' OR description LIKE '%samsung%')
  AND (name LIKE '%5G%' OR description LIKE '%5G%')
  AND price < 10000000
ORDER BY ??? -- Không biết sắp xếp theo gì
LIMIT 20;

Elasticsearch giải quyết:

json
GET /products/_search
{
  "query": {
    "bool": {
      "must": [
        {
          "multi_match": {
            "query": "điện thoại samsung 5G",
            "fields": ["name^3", "description", "brand^2"],
            "fuzziness": "AUTO"
          }
        }
      ],
      "filter": [
        { "range": { "price": { "lte": 10000000 } } },
        { "term": { "status": "active" } }
      ]
    }
  },
  "aggs": {
    "by_brand": {
      "terms": { "field": "brand.keyword", "size": 10 }
    },
    "price_ranges": {
      "range": {
        "field": "price",
        "ranges": [
          { "to": 3000000, "key": "Dưới 3 triệu" },
          { "from": 3000000, "to": 7000000, "key": "3-7 triệu" },
          { "from": 7000000, "to": 10000000, "key": "7-10 triệu" }
        ]
      }
    }
  }
}

Use Case 2: Log Analytics (ELK Stack) ​

Bài toán: Hệ thống microservices với 50 services, mỗi giây generate 100,000 log lines. Cần debug production issue.

Vòng đời log:

Application Logs (JSON)
        │
        ▼
    Filebeat (collect)
        │
        ▼
    Logstash (parse, enrich)
        │
        ▼
  Elasticsearch (store, index)
        │
        ▼
    Kibana (visualize, alert)

Ví dụ log được index:

json
{
  "@timestamp": "2024-01-15T10:30:45.123Z",
  "service": "payment-service",
  "level": "ERROR",
  "message": "Payment failed: insufficient funds",
  "trace_id": "abc123def456",
  "user_id": "user_789",
  "amount": 500000,
  "error_code": "INSUFFICIENT_FUNDS",
  "response_time_ms": 45,
  "host": "prod-microservice-03"
}

Query tìm lỗi trong 1 giờ qua:

json
GET /logs-*/_search
{
  "query": {
    "bool": {
      "must": [
        { "match": { "level": "ERROR" } },
        { "match": { "service": "payment-service" } }
      ],
      "filter": [
        {
          "range": {
            "@timestamp": {
              "gte": "now-1h",
              "lte": "now"
            }
          }
        }
      ]
    }
  },
  "aggs": {
    "errors_over_time": {
      "date_histogram": {
        "field": "@timestamp",
        "calendar_interval": "5m"
      }
    },
    "top_errors": {
      "terms": {
        "field": "error_code.keyword",
        "size": 10
      }
    }
  },
  "sort": [{ "@timestamp": "desc" }]
}

Use Case 3: Content/Article Search ​

Bài toán: Nền tảng tin tức như VnExpress với 5 triệu bài viết. Người dùng tìm "bão số 3 thiệt hại".

Đặc điểm:

  • Tiếng Việt với dấu ~ khó xử lý
  • Nội dung dài (10,000+ chữ mỗi bài)
  • Cần highlight kết quả tìm kiếm
  • Phân loại theo chủ đề (news categories)
json
GET /articles/_search
{
  "query": {
    "multi_match": {
      "query": "bão số 3 thiệt hại",
      "fields": ["title^5", "summary^3", "content"],
      "type": "best_fields"
    }
  },
  "highlight": {
    "fields": {
      "title": {},
      "content": {
        "fragment_size": 200,
        "number_of_fragments": 3
      }
    }
  },
  "aggs": {
    "by_category": {
      "terms": { "field": "category.keyword" }
    },
    "published_date": {
      "date_histogram": {
        "field": "published_at",
        "calendar_interval": "day"
      }
    }
  }
}

Use Case 4: Real-time Analytics & Monitoring ​

Bài toán: Dashboard theo dõi metrics của hệ thống: CPU, memory, response time, error rate theo thời gian thực.

Data flow:

Servers → Metricbeat → Elasticsearch → Kibana Dashboard
                                           │
                                    [Real-time refresh: 5s]
                                    - CPU usage per host
                                    - Memory trends
                                    - Request rate
                                    - Error rate alerting

Use Case 5: Autocomplete & Suggestions ​

Bài toán: Search box với autocomplete như Google - khi user gõ "iph" thì hiện ra "iPhone 15", "iPhone 14 Pro", "iPhone charger"...

Elasticsearch có Completion Suggester được tối ưu cho use case này với cấu trúc FST (Finite State Transducer) trong memory, cho response time < 5ms.


1.4 So Sánh Elasticsearch với Các Giải Pháp Khác ​

Tiêu chíMySQL FTSElasticsearch
Tốc độ tìm kiếmChậm với data lớnNhanh (< 100ms với triệu docs)
Relevance ScoringCơ bảnBM25, có thể tùy chỉnh
ScalabilityVertical scalingHorizontal scaling (thêm node)
Text AnalysisHạn chếCực kỳ linh hoạt
AggregationsHạn chế (GROUP BY)Rất mạnh
Near Real-timeIndex chậm~1 giây
Tiếng ViệtKémHỗ trợ tốt (custom analyzer)
TransactionsACIDKhông có
JoinsDễPhức tạp (nested/parent-child)
Chi phí vận hànhĐơn giảnPhức tạp hơn
Tiêu chíMongoDB Atlas SearchElasticsearch
Tích hợp DBTích hợp sẵn với MongoDBCần sync riêng
Phức tạp setupĐơn giảnPhức tạp hơn
PerformanceTốtTốt hơn cho search-heavy
AggregationsTốtRất mạnh hơn
EcosystemSmallerRất lớn (ELK)
CostTheo usageSelf-managed hoặc Elastic Cloud

Elasticsearch vs Apache Solr ​

Cả hai đều dùng Lucene làm core, nhưng:

Tiêu chíSolrElasticsearch
APIXML/JSONJSON REST API
DistributedZooKeeperBuilt-in native
Ease of usePhức tạp hơnDeveloper-friendly hơn
AnalyticsHạn chếAggregation framework mạnh
CommunityNhỏ hơnLớn hơn
CloudKhông có cloud service riêngElastic Cloud
Use caseLegacy enterprise searchModern applications

Khi nào KHÔNG dùng Elasticsearch ​

Elasticsearch không phù hợp cho:

  1. Primary database: Không có ACID transactions, có thể mất data
  2. Relational data với nhiều joins: ES không tối ưu cho joins
  3. Small data (< 1000 records): Overhead không đáng
  4. Write-heavy workloads: ES tối ưu cho read, write có latency cao hơn SQL
  5. Sensitive financial data: Khi cần transaction guarantees

1.5 Kiến Trúc Tổng Quan ​

Kiến Trúc Phân Tán ​

                    CLIENT (Application)
                           │
                    Load Balancer
                           │
                ┌──────────┼──────────┐
                │          │          │
           ┌────▼────┐ ┌───▼────┐ ┌──▼─────┐
           │ Node 1  │ │ Node 2 │ │ Node 3 │
           │(Master) │ │(Data)  │ │(Data)  │
           └────┬────┘ └───┬────┘ └──┬─────┘
                │          │         │
                └──────────┴─────────┘
                        Cluster

Luồng Xử Lý Query ​

Client gửi query
       │
       ▼
Coordinating Node (nhận request)
       │
       ▼
Broadcast query đến các shards liên quan
       │
    ┌──┴──┐
    │     │
 Shard 0  Shard 1
 (Node 1) (Node 2)
    │     │
    └──┬──┘
       │
Gather & merge kết quả
(sort, rank, paginate)
       │
       ▼
Return kết quả cho client

Luồng Index Document ​

Client gửi document mới
         │
         ▼
Routing: hash(document_id) % num_shards
         │
         ▼
Primary Shard nhận document
         │
    ┌────┴────┐
    │         │
Write to     Replicate to
Lucene       Replica Shards
segment      (asynchronous)
    │
    ▼
Translog (durability)
    │
    ▼
Refresh (default 1s) → searchable

1.6 Lịch Sử và Phiên Bản ​

NămSự kiện
2004Shay Banon bắt đầu Compass (tiền thân của ES)
2010Elasticsearch 0.4 - lần đầu public release
2012Elastic (công ty) được thành lập
2013ElasticSearch 1.0
2014Logstash và Kibana gia nhập Elastic Stack
2015Elasticsearch 2.0
2017Elasticsearch 5.0 (thống nhất version với Kibana, Logstash)
2018Elasticsearch 6.0
2019Elasticsearch 7.0 (loại bỏ mapping types)
2021Elasticsearch 7.10 - License thay đổi (SSPL)
2022Elasticsearch 8.0 (security by default, TSDB)
2023Elasticsearch 8.x với vector search (kNN)

Phiên bản hiện tại (2024): Elasticsearch 8.x

Major Changes ở 8.x cần biết: ​

  1. Security by default: HTTPS, authentication bật mặc định
  2. Loại bỏ mapping types: Không còn _type field
  3. Stack-based licensing: Nhiều tính năng miễn phí hơn
  4. Vector search (kNN): Search theo embeddings cho AI/ML
  5. TSDB (Time Series Data Stream): Tối ưu cho time series data

1.7 Cách Elasticsearch Hoạt Động - Cơ Bản ​

Inverted Index là gì? ​

Đây là khái niệm quan trọng nhất. Hãy hiểu qua ví dụ:

Dữ liệu:

Doc 1: "Điện thoại Samsung Galaxy S24"
Doc 2: "Điện thoại iPhone 15 Pro"
Doc 3: "Laptop Samsung Galaxy Book"

Inverted Index được xây dựng:

Token         → Documents
─────────────────────────
"điện"        → [Doc1, Doc2]
"thoại"       → [Doc1, Doc2]
"samsung"     → [Doc1, Doc3]
"galaxy"      → [Doc1, Doc3]
"s24"         → [Doc1]
"iphone"      → [Doc2]
"15"          → [Doc2]
"pro"         → [Doc2]
"laptop"      → [Doc3]
"book"        → [Doc3]

Khi query "samsung":

  1. Lookup "samsung" → [Doc1, Doc3]
  2. Tính relevance score cho mỗi doc
  3. Trả về kết quả đã được sort

So với table scan của SQL:

  • SQL: Duyệt qua từng row → O(n)
  • Elasticsearch: Lookup trong inverted index → O(1) ~ O(log n)

BM25 - Thuật Toán Tính Điểm Relevance ​

Elasticsearch dùng BM25 (Best Match 25) để tính điểm relevance:

$$\text{score}(D, Q) = \sum_{i=1}^{n} IDF(q_i) \cdot \frac{f(q_i, D) \cdot (k_1 + 1)}{f(q_i, D) + k_1 \cdot (1 - b + b \cdot \frac{|D|}{avgdl})}$$

Trong đó:

  • $f(q_i, D)$ = tần suất của term $q_i$ trong document $D$ (TF)
  • $|D|$ = độ dài document
  • $avgdl$ = độ dài trung bình của tất cả documents
  • $k_1$ = tham số kiểm soát ảnh hưởng của TF (default: 1.2)
  • $b$ = tham số normalization theo độ dài (default: 0.75)
  • $IDF(q_i)$ = Inverse Document Frequency

IDF được tính: $$IDF(q_i) = \ln\left(1 + \frac{N - n(q_i) + 0.5}{n(q_i) + 0.5}\right)$$

Giải thích intuition:

  • Term xuất hiện nhiều hơn trong document → score cao hơn (TF)
  • Term xuất hiện ít trong toàn bộ corpus → quý hơn → score cao hơn (IDF)
  • Document ngắn mà chứa term → relevant hơn document dài (length normalization)

1.8 Các Khái Niệm Cần Nhớ Ngay ​

Khái niệm ESTương đương SQL
IndexDatabase/Table
DocumentRow/Record
FieldColumn
MappingSchema
ShardPartition
NodeServer
ClusterDatabase Cluster

Lưu ý quan trọng: Trước ES 7.0, có khái niệm "Type" tương đương với Table trong một Database. Từ ES 7.0+, mỗi Index chỉ có một type (_doc), nên Index ≈ Table.


Tóm Tắt Chương 1 ​

  • Elasticsearch là search engine phân tán, xây dựng trên Lucene
  • Dùng ES khi cần: full-text search, log analytics, real-time analytics, autocomplete
  • Không dùng ES làm primary database, không dùng khi cần ACID transactions
  • Inverted Index là lý do ES tìm kiếm nhanh
  • BM25 là thuật toán tính điểm relevance mặc định
  • ES hoạt động tốt nhất khi kết hợp với SQL DB (SQL cho primary storage, ES cho search)

Bước Tiếp Theo ​

→ Chương 2: Cài đặt và Cấu hình - Thiết lập môi trường để bắt đầu thực hành


Chương 2: Cài Đặt và Cấu Hình Elasticsearch ​

2.1 Yêu Cầu Hệ Thống ​

Yêu cầu phần cứng tối thiểu (Development) ​

Tài nguyênMinimumRecommended
RAM2GB8GB+
CPU2 cores4+ cores
Disk10GBSSD 50GB+
OSLinux/macOS/WindowsLinux (Ubuntu 22.04+)
JVMJava 17+Java 17 (bundled)

Yêu cầu hệ thống tối thiểu (Production) ​

Tài nguyênHot NodesWarm/Cold Nodes
RAM32-64GB16-32GB
CPU8-16 cores4-8 cores
DiskNVMe SSDSSD/HDD
Network10Gbps1Gbps

Quan trọng: Elasticsearch sử dụng JVM heap + OS page cache. Quy tắc: JVM heap = 50% RAM, không vượt 30GB (compressed object pointers)


2.2 Cài Đặt với Docker (Khuyến nghị cho Development) ​

Option 1: Single Node với Docker Compose ​

Tạo file docker-compose.yml:

yaml
version: '3.8'

services:
  elasticsearch:
    image: docker.elastic.co/elasticsearch/elasticsearch:8.12.0
    container_name: elasticsearch
    environment:
      - node.name=es01
      - cluster.name=my-elasticsearch
      - discovery.type=single-node
      - bootstrap.memory_lock=true
      - "ES_JAVA_OPTS=-Xms1g -Xmx1g"
      # Tắt security cho development (KHÔNG làm trên production!)
      - xpack.security.enabled=false
      - xpack.security.http.ssl.enabled=false
    ulimits:
      memlock:
        soft: -1
        hard: -1
    volumes:
      - es_data:/usr/share/elasticsearch/data
    ports:
      - "9200:9200"
      - "9300:9300"
    networks:
      - elastic
    healthcheck:
      test: ["CMD-SHELL", "curl -f http://localhost:9200/_health || exit 1"]
      interval: 30s
      timeout: 10s
      retries: 3

  kibana:
    image: docker.elastic.co/kibana/kibana:8.12.0
    container_name: kibana
    environment:
      - ELASTICSEARCH_HOSTS=http://elasticsearch:9200
    ports:
      - "5601:5601"
    networks:
      - elastic
    depends_on:
      elasticsearch:
        condition: service_healthy

volumes:
  es_data:
    driver: local

networks:
  elastic:
    driver: bridge

Khởi động:

bash
docker-compose up -d

# Kiểm tra trạng thái
docker-compose ps

# Xem logs
docker-compose logs -f elasticsearch

Kiểm tra ES đã chạy:

bash
curl http://localhost:9200

# Kết quả mong đợi:
{
  "name" : "es01",
  "cluster_name" : "my-elasticsearch",
  "cluster_uuid" : "abc123...",
  "version" : {
    "number" : "8.12.0",
    ...
  },
  "tagline" : "You Know, for Search"
}

Option 2: Multi-Node Cluster với Docker Compose ​

Đây là setup thực tế hơn, giả lập cluster production:

yaml
version: '3.8'

services:
  es01:
    image: docker.elastic.co/elasticsearch/elasticsearch:8.12.0
    container_name: es01
    environment:
      - node.name=es01
      - cluster.name=my-cluster
      - node.roles=master,data,ingest
      - discovery.seed_hosts=es02,es03
      - cluster.initial_master_nodes=es01,es02,es03
      - bootstrap.memory_lock=true
      - "ES_JAVA_OPTS=-Xms512m -Xmx512m"
      - xpack.security.enabled=false
    ulimits:
      memlock:
        soft: -1
        hard: -1
    volumes:
      - es01_data:/usr/share/elasticsearch/data
    ports:
      - "9200:9200"
    networks:
      - elastic

  es02:
    image: docker.elastic.co/elasticsearch/elasticsearch:8.12.0
    container_name: es02
    environment:
      - node.name=es02
      - cluster.name=my-cluster
      - node.roles=master,data,ingest
      - discovery.seed_hosts=es01,es03
      - cluster.initial_master_nodes=es01,es02,es03
      - bootstrap.memory_lock=true
      - "ES_JAVA_OPTS=-Xms512m -Xmx512m"
      - xpack.security.enabled=false
    ulimits:
      memlock:
        soft: -1
        hard: -1
    volumes:
      - es02_data:/usr/share/elasticsearch/data
    networks:
      - elastic

  es03:
    image: docker.elastic.co/elasticsearch/elasticsearch:8.12.0
    container_name: es03
    environment:
      - node.name=es03
      - cluster.name=my-cluster
      - node.roles=master,data,ingest
      - discovery.seed_hosts=es01,es02
      - cluster.initial_master_nodes=es01,es02,es03
      - bootstrap.memory_lock=true
      - "ES_JAVA_OPTS=-Xms512m -Xmx512m"
      - xpack.security.enabled=false
    ulimits:
      memlock:
        soft: -1
        hard: -1
    volumes:
      - es03_data:/usr/share/elasticsearch/data
    networks:
      - elastic

  kibana:
    image: docker.elastic.co/kibana/kibana:8.12.0
    container_name: kibana
    environment:
      - ELASTICSEARCH_HOSTS=http://es01:9200
    ports:
      - "5601:5601"
    networks:
      - elastic
    depends_on:
      - es01

volumes:
  es01_data:
  es02_data:
  es03_data:

networks:
  elastic:
    driver: bridge

Các lệnh quản lý:

bash
# Khởi động cluster
docker-compose up -d

# Xem cluster health
curl http://localhost:9200/_cluster/health?pretty

# Xem các nodes
curl http://localhost:9200/_cat/nodes?v

# Output mong đợi:
# ip         heap.percent ram.percent cpu load_1m load_5m load_15m node.role  name
# 172.18.0.2           12          95   3    0.02    0.07     0.13 cdfhilmrstw es01
# 172.18.0.3           11          95   2    0.02    0.07     0.13 cdfhilmrstw es02
# 172.18.0.4           13          95   2    0.02    0.07     0.13 cdfhilmrstw es03

2.3 Cài Đặt với Security (Khuyến Nghị cho Production) ​

Từ ES 8.0, security bật mặc định. Đây là cách setup đúng:

yaml
version: '3.8'

services:
  setup:
    image: docker.elastic.co/elasticsearch/elasticsearch:8.12.0
    volumes:
      - certs:/usr/share/elasticsearch/config/certs
    user: "0"
    command: >
      bash -c '
        if [ x${ELASTIC_PASSWORD} == x ]; then
          echo "Set the ELASTIC_PASSWORD environment variable in the .env file";
          exit 1;
        fi;
        if [ ! -f config/certs/ca.zip ]; then
          echo "Creating CA";
          bin/elasticsearch-certutil ca --silent --pem -out config/certs/ca.zip;
          unzip config/certs/ca.zip -d config/certs;
        fi;
        if [ ! -f config/certs/certs.zip ]; then
          echo "Creating certs";
          cat > config/certs/instances.yml << EOF
          instances:
            - name: es01
              dns:
                - es01
                - localhost
              ip:
                - 127.0.0.1
          EOF
          bin/elasticsearch-certutil cert --silent --pem -out config/certs/certs.zip \
            --in config/certs/instances.yml --ca-cert config/certs/ca/ca.crt \
            --ca-key config/certs/ca/ca.key;
          unzip config/certs/certs.zip -d config/certs;
        fi;
        echo "Setting file permissions"
        chown -R root:root config/certs;
        find . -type d -exec chmod 750 \{\} \;;
        find . -type f -exec chmod 640 \{\} \;;
        echo "Waiting for Elasticsearch availability";
        until curl -s --cacert config/certs/ca/ca.crt https://es01:9200 | \
          grep -q "missing authentication credentials"; do sleep 30; done;
        echo "Setting elastic password";
        until curl -s -X POST --cacert config/certs/ca/ca.crt \
          -u "elastic:changeme" -H "Content-Type: application/json" \
          https://es01:9200/_security/user/elastic/_password \
          -d "{\"password\":\"${ELASTIC_PASSWORD}\"}" | grep -q "^{}"; do sleep 10; done;
        echo "All done!";
      '

  es01:
    image: docker.elastic.co/elasticsearch/elasticsearch:8.12.0
    container_name: es01
    environment:
      - node.name=es01
      - cluster.name=my-secure-cluster
      - discovery.type=single-node
      - ELASTIC_PASSWORD=${ELASTIC_PASSWORD}
      - bootstrap.memory_lock=true
      - "ES_JAVA_OPTS=-Xms1g -Xmx1g"
      - xpack.security.enabled=true
      - xpack.security.http.ssl.enabled=true
      - xpack.security.http.ssl.key=certs/es01/es01.key
      - xpack.security.http.ssl.certificate=certs/es01/es01.crt
      - xpack.security.http.ssl.certificate_authorities=certs/ca/ca.crt
      - xpack.security.transport.ssl.enabled=true
      - xpack.security.transport.ssl.key=certs/es01/es01.key
      - xpack.security.transport.ssl.certificate=certs/es01/es01.crt
      - xpack.security.transport.ssl.certificate_authorities=certs/ca/ca.crt
      - xpack.security.transport.ssl.verification_mode=certificate
    volumes:
      - certs:/usr/share/elasticsearch/config/certs
      - es01_data:/usr/share/elasticsearch/data
    ports:
      - "9200:9200"
    ulimits:
      memlock:
        soft: -1
        hard: -1

volumes:
  certs:
  es01_data:

File .env:

ELASTIC_PASSWORD=MySecureP@ssw0rd123

Kết nối với security:

bash
# Với SSL và authentication
curl -u elastic:MySecureP@ssw0rd123 \
  --cacert /path/to/ca.crt \
  https://localhost:9200

# Hoặc tắt SSL verification (không dùng production!)
curl -k -u elastic:MySecureP@ssw0rd123 https://localhost:9200

2.4 Cài Đặt trên Ubuntu Server (Production-like) ​

Step 1: Import GPG Key ​

bash
wget -qO - https://artifacts.elastic.co/GPG-KEY-elasticsearch | \
  sudo gpg --dearmor -o /usr/share/keyrings/elasticsearch-keyring.gpg

Step 2: Add Repository ​

bash
sudo apt-get install apt-transport-https
echo "deb [signed-by=/usr/share/keyrings/elasticsearch-keyring.gpg] \
  https://artifacts.elastic.co/packages/8.x/apt stable main" | \
  sudo tee /etc/apt/sources.list.d/elastic-8.x.list

Step 3: Install ​

bash
sudo apt-get update && sudo apt-get install elasticsearch

Step 4: Configure (elasticsearch.yml) ​

bash
sudo nano /etc/elasticsearch/elasticsearch.yml
yaml
# /etc/elasticsearch/elasticsearch.yml

# Cluster
cluster.name: production-cluster
node.name: node-1

# Network
network.host: 0.0.0.0
http.port: 9200
transport.port: 9300

# Discovery
discovery.seed_hosts: ["node-1-ip", "node-2-ip", "node-3-ip"]
cluster.initial_master_nodes: ["node-1", "node-2", "node-3"]

# Paths
path.data: /var/lib/elasticsearch
path.logs: /var/log/elasticsearch

# Memory
bootstrap.memory_lock: true

# Security (production)
xpack.security.enabled: true
xpack.security.transport.ssl.enabled: true

# Slow log thresholds
index.search.slowlog.threshold.query.warn: 10s
index.search.slowlog.threshold.query.info: 5s
index.search.slowlog.threshold.fetch.warn: 1s
index.indexing.slowlog.threshold.index.warn: 10s

Step 5: Configure JVM ​

bash
sudo nano /etc/elasticsearch/jvm.options.d/heap.options
# Set heap to 50% of RAM, max 30GB
-Xms8g
-Xmx8g

Step 6: System Configuration ​

bash
# Disable swap
sudo swapoff -a
# Permanent: comment out swap in /etc/fstab

# Increase virtual memory map count
sudo sysctl -w vm.max_map_count=262144
# Permanent:
echo "vm.max_map_count=262144" | sudo tee -a /etc/sysctl.conf

# Increase file descriptor limits
sudo nano /etc/security/limits.conf
# Add:
# elasticsearch soft nofile 65536
# elasticsearch hard nofile 65536
# elasticsearch soft memlock unlimited
# elasticsearch hard memlock unlimited

Step 7: Start và Enable ​

bash
sudo systemctl daemon-reload
sudo systemctl enable elasticsearch
sudo systemctl start elasticsearch

# Check status
sudo systemctl status elasticsearch
sudo journalctl -u elasticsearch -f

2.5 Kibana - Giao Diện Quản Lý ​

Kibana với Docker ​

Đã có trong docker-compose ở trên. Truy cập: http://localhost:5601

Kibana Dev Tools ​

Đây là công cụ quan trọng nhất để học ES. Đường dẫn: Kibana → Dev Tools (Management)

Giao diện Dev Tools:

┌─────────────────────────────────────────────────────────────┐
│  Dev Tools                              [History] [Settings]│
├───────────────────────┬─────────────────────────────────────┤
│                       │                                     │
│  GET _cluster/health  │  {                                  │
│  ▶ (Ctrl+Enter)       │    "cluster_name": "my-cluster",   │
│                       │    "status": "green",               │
│  POST /products/_doc  │    "timed_out": false,              │
│  {                    │    "number_of_nodes": 3,            │
│    "name": "iPhone"   │    "number_of_data_nodes": 3,       │
│  }                    │    "active_shards": 15              │
│                       │  }                                  │
│  [Request panel]      │  [Response panel]                   │
└───────────────────────┴─────────────────────────────────────┘

Shortcuts hữu ích:

  • Ctrl+Enter hoặc Cmd+Enter: Chạy query hiện tại
  • Ctrl+Space: Auto-complete
  • Ctrl+/: Toggle comment
  • Click ▶ button cạnh query

2.6 Cấu Hình quan trọng cho Production ​

elasticsearch.yml Production Template ​

yaml
# ======================== Elasticsearch Configuration =========================
# Cluster
cluster.name: prod-cluster
node.name: ${HOSTNAME}

# Node roles - tách biệt master và data nodes cho cluster lớn
node.roles: [data, ingest]
# Master-only nodes:
# node.roles: [master]

# ---------------------------------- Paths ------------------------------------
path.data:
  - /mnt/data/elasticsearch
path.logs: /var/log/elasticsearch

# ---------------------------------- Memory -----------------------------------
bootstrap.memory_lock: true

# ---------------------------------- Network ----------------------------------
network.host: _eth0_
http.port: 9200
transport.port: 9300
http.compression: true
http.max_content_length: 200mb

# --------------------------------- Discovery ---------------------------------
discovery.seed_hosts:
  - "master-1:9300"
  - "master-2:9300"
  - "master-3:9300"

cluster.initial_master_nodes:
  - "master-1"
  - "master-2"
  - "master-3"

# ---------------------------------- Various -----------------------------------
# Hạn chế dynamic scripting (security)
script.allowed_types: stored
script.allowed_contexts: search, update

# Slow logs
index.search.slowlog.threshold.query.warn: 10s
index.search.slowlog.threshold.query.info: 5s
index.search.slowlog.threshold.query.debug: 2s
index.search.slowlog.threshold.fetch.warn: 1s
index.indexing.slowlog.threshold.index.warn: 10s
index.indexing.slowlog.source: 1000

# Circuit breakers
indices.breaker.total.use_real_memory: true
indices.breaker.total.limit: 95%
indices.breaker.request.limit: 60%
indices.breaker.fielddata.limit: 40%

# --------------------------------- Security ----------------------------------
xpack.security.enabled: true
xpack.security.transport.ssl.enabled: true
xpack.security.transport.ssl.verification_mode: certificate
xpack.security.transport.ssl.client_authentication: required
xpack.security.transport.ssl.keystore.path: /etc/elasticsearch/certs/elastic-certificates.p12
xpack.security.transport.ssl.truststore.path: /etc/elasticsearch/certs/elastic-certificates.p12

xpack.security.http.ssl.enabled: true
xpack.security.http.ssl.keystore.path: /etc/elasticsearch/certs/http.p12

# --------------------------------- Monitoring -----------------------------------
xpack.monitoring.collection.enabled: true

Quan Trọng: vm.max_map_count ​

Elasticsearch cần vm.max_map_count >= 262144. Nếu không set:

[1]: max virtual memory areas vm.max_map_count [65530] is too low, 
     increase to at least [262144]

Fix:

bash
# Temporary
sudo sysctl -w vm.max_map_count=262144

# Permanent
echo 'vm.max_map_count=262144' | sudo tee -a /etc/sysctl.d/elasticsearch.conf
sudo sysctl --system

2.7 Kiểm Tra Cluster và APIs Cơ Bản ​

Cluster Health API ​

bash
GET /_cluster/health

# Response:
{
  "cluster_name": "my-cluster",
  "status": "green",        # green/yellow/red
  "timed_out": false,
  "number_of_nodes": 3,
  "number_of_data_nodes": 3,
  "active_primary_shards": 5,
  "active_shards": 15,
  "relocating_shards": 0,
  "initializing_shards": 0,
  "unassigned_shards": 0,
  "delayed_unassigned_shards": 0,
  "number_of_pending_tasks": 0,
  "number_of_in_flight_fetch": 0,
  "task_max_waiting_in_queue_millis": 0,
  "active_shards_percent_as_number": 100.0
}

Ý nghĩa cluster status:

  • 🟢 Green: Tất cả primary và replica shards hoạt động
  • 🟡 Yellow: Primary shards hoạt động, nhưng một số replica chưa được assign (thường với single node)
  • 🔴 Red: Một số primary shards không hoạt động → data loss có thể xảy ra

Cat APIs - Human Readable ​

bash
# Xem tất cả nodes
GET /_cat/nodes?v

# Xem tất cả indices
GET /_cat/indices?v&s=index

# Xem shards
GET /_cat/shards?v

# Xem aliases
GET /_cat/aliases?v

# Xem cluster health ngắn gọn
GET /_cat/health?v

# Output cat/nodes:
# ip         heap.percent ram.percent cpu load_1m node.role name
# 172.18.0.2           45          67   5    0.15  master,data  es01
# 172.18.0.3           38          67   3    0.10  data         es02

Node Info APIs ​

bash
# Thông tin chi tiết về các nodes
GET /_nodes

# Stats của nodes (quan trọng cho monitoring)
GET /_nodes/stats

# JVM stats
GET /_nodes/stats/jvm

# Indices stats
GET /_nodes/stats/indices

# OS stats
GET /_nodes/stats/os

Cluster Settings ​

bash
# Xem tất cả settings hiện tại
GET /_cluster/settings?include_defaults=true

# Thay đổi settings động (không cần restart)
PUT /_cluster/settings
{
  "persistent": {
    "cluster.routing.allocation.enable": "all"
  },
  "transient": {
    "logger.level": "DEBUG"
  }
}

Phân biệt persistent và transient:

  • persistent: Lưu vào cluster state, survive restart
  • transient: Mất khi restart cluster

2.8 Kết Nối từ ứng dụng Backend ​

Node.js với @elastic/elasticsearch ​

bash
npm install @elastic/elasticsearch
javascript
const { Client } = require('@elastic/elasticsearch');

// Development (no security)
const client = new Client({
  node: 'http://localhost:9200'
});

// Production (with security)
const client = new Client({
  node: 'https://my-es-cluster:9200',
  auth: {
    username: 'elastic',
    password: 'MySecurePassword'
  },
  tls: {
    ca: fs.readFileSync('/path/to/ca.crt'),
    rejectUnauthorized: true
  }
});

// Test kết nối
async function ping() {
  const result = await client.ping();
  console.log('Connected:', result);
}

ping().catch(console.error);

Java với Spring Boot ​

xml
<!-- pom.xml -->
<dependency>
  <groupId>co.elastic.clients</groupId>
  <artifactId>elasticsearch-java</artifactId>
  <version>8.12.0</version>
</dependency>
<dependency>
  <groupId>com.fasterxml.jackson.core</groupId>
  <artifactId>jackson-databind</artifactId>
  <version>2.17.0</version>
</dependency>
java
// ElasticSearchConfig.java
@Configuration
public class ElasticSearchConfig {
    
    @Value("${elasticsearch.host}")
    private String host;
    
    @Value("${elasticsearch.port}")
    private int port;
    
    @Bean
    public ElasticsearchClient elasticsearchClient() {
        RestClient restClient = RestClient.builder(
            new HttpHost(host, port, "http")
        ).build();
        
        ElasticsearchTransport transport = new RestClientTransport(
            restClient, new JacksonJsonpMapper()
        );
        
        return new ElasticsearchClient(transport);
    }
}

Python với elasticsearch-py ​

bash
pip install elasticsearch
python
from elasticsearch import Elasticsearch

# Development
es = Elasticsearch("http://localhost:9200")

# Production
es = Elasticsearch(
    "https://my-es-cluster:9200",
    http_auth=("elastic", "password"),
    verify_certs=True,
    ca_certs="/path/to/ca.crt"
)

# Test connection
if es.ping():
    print("Connected to Elasticsearch!")
    info = es.info()
    print(f"Version: {info['version']['number']}")

Go với olivere/elastic hoặc elastic/go-elasticsearch ​

bash
go get github.com/elastic/go-elasticsearch/v8
go
package main

import (
    "github.com/elastic/go-elasticsearch/v8"
    "log"
)

func main() {
    cfg := elasticsearch.Config{
        Addresses: []string{
            "http://localhost:9200",
        },
    }
    
    es, err := elasticsearch.NewClient(cfg)
    if err != nil {
        log.Fatalf("Error creating client: %s", err)
    }
    
    // Test connection
    res, err := es.Info()
    if err != nil {
        log.Fatalf("Error getting response: %s", err)
    }
    defer res.Body.Close()
    
    log.Println("Connected to Elasticsearch!")
}

2.9 Troubleshooting Cài Đặt Thường Gặp ​

Lỗi: "max virtual memory areas vm.max_map_count too low" ​

bash
sudo sysctl -w vm.max_map_count=262144

Lỗi: "max file descriptors too low" ​

bash
# Check hiện tại
ulimit -n

# Tăng lên
ulimit -n 65536

# Permanent: /etc/security/limits.conf
elasticsearch soft nofile 65536
elasticsearch hard nofile 65536

Lỗi: cluster status RED sau khi restart ​

bash
# Check unassigned shards
GET /_cat/shards?v&h=index,shard,prirep,state,unassigned.reason

# Retry shard allocation
POST /_cluster/reroute?retry_failed=true

# Nếu vẫn RED, check specific index
GET /_cluster/allocation/explain

Lỗi: Out of Memory / GC pressure ​

bash
# Check heap usage
GET /_nodes/stats/jvm

# Nếu heap usage > 75% thường xuyên:
# 1. Tăng JVM heap (tối đa 50% RAM, không quá 30GB)
# 2. Giảm field data cache
# 3. Xem xét thêm nodes

# Emergency: clear field data cache
POST /_cache/clear?fielddata=true

Lỗi: "This node is not master eligible" ​

Khi cluster mới hình thành cần ít nhất 1 master-eligible node. Check:

bash
GET /_cluster/settings

# Set voting configuration exclusions nếu cần
POST /_cluster/voting_config_exclusions?node_names=old-master-node

Tóm Tắt Chương 2 ​

  1. Docker là cách nhanh nhất để bắt đầu với ES development
  2. Luôn dùng xpack.security.enabled=false chỉ cho development/learning
  3. Production cần: security, SSL, proper JVM heap, vm.max_map_count
  4. Kibana Dev Tools là công cụ tốt nhất để thực hành ES queries
  5. Cluster status green = tốt, yellow = cần chú ý, red = khẩn cấp
  6. ES cung cấp drivers cho tất cả ngôn ngữ backend phổ biến

Bước Tiếp Theo ​

→ Chương 3: Khái niệm Cốt lõi - Hiểu sâu về Index, Shard, Document và Inverted Index


Chương 3: Khái Niệm Cốt Lõi của Elasticsearch ​

3.1 Kiến Trúc Dữ Liệu ​

Index ​

Index là đơn vị lưu trữ logic cao nhất trong Elasticsearch. Nó tương đương với một bảng (table) trong SQL hoặc một collection trong MongoDB.

RELATIONAL DB          ELASTICSEARCH
─────────────────      ─────────────────
Database        ≈      (cluster)
Table           ≈      Index
Row             ≈      Document  
Column          ≈      Field
Schema          ≈      Mapping

Đặt tên Index:

  • Chỉ dùng chữ thường (lowercase)
  • Không có dấu cách, dấu phẩy, colon, asterisk
  • Không bắt đầu bằng -, _, +
  • Không dùng . hoặc ..
  • Tối đa 255 bytes
  • Convention tốt: {app}-{resource}-{env} → myapp-products-prod

Tạo index:

bash
# Tạo index đơn giản
PUT /products

# Tạo index với settings và mappings
PUT /products
{
  "settings": {
    "number_of_shards": 3,
    "number_of_replicas": 1,
    "index": {
      "refresh_interval": "1s",
      "analysis": {
        "analyzer": {
          "my_analyzer": {
            "type": "custom",
            "tokenizer": "standard",
            "filter": ["lowercase", "stop"]
          }
        }
      }
    }
  },
  "mappings": {
    "properties": {
      "name": {
        "type": "text",
        "analyzer": "my_analyzer"
      },
      "price": {
        "type": "double"
      },
      "created_at": {
        "type": "date"
      }
    }
  }
}

Document ​

Document là đơn vị lưu trữ cơ bản trong ES. Mỗi document là một JSON object.

json
{
  "_index": "products",
  "_id": "1",
  "_version": 1,
  "_seq_no": 0,
  "_primary_term": 1,
  "found": true,
  "_source": {
    "id": 1,
    "name": "iPhone 15 Pro Max",
    "brand": "Apple",
    "category": "smartphones",
    "price": 34990000,
    "specs": {
      "ram": "8GB",
      "storage": "256GB",
      "screen_size": 6.7,
      "battery": 4422
    },
    "tags": ["flagship", "5G", "iOS"],
    "images": [
      "https://cdn.apple.com/iphone-15-pro-1.jpg",
      "https://cdn.apple.com/iphone-15-pro-2.jpg"
    ],
    "in_stock": true,
    "created_at": "2023-09-22T10:00:00Z",
    "updated_at": "2024-01-15T08:30:00Z"
  }
}

Các metadata fields của Document:

FieldMô tả
_indexIndex chứa document
_idUnique identifier của document
_versionVersion number, tăng mỗi lần update
_seq_noSequence number cho optimistic concurrency
_primary_termPrimary term cho optimistic concurrency
_sourceJSON document gốc được index
_scoreRelevance score (chỉ khi search)

Field và Data Types ​

Mỗi field trong document có một data type xác định cách ES lưu trữ và index nó.

Ví dụ mapping với nhiều loại field:

json
{
  "mappings": {
    "properties": {
      "product_id": { "type": "keyword" },
      "name": { "type": "text" },
      "description": { "type": "text" },
      "price": { "type": "double" },
      "quantity": { "type": "integer" },
      "discount_rate": { "type": "float" },
      "in_stock": { "type": "boolean" },
      "created_at": { "type": "date" },
      "last_updated": { 
        "type": "date",
        "format": "yyyy-MM-dd HH:mm:ss||yyyy-MM-dd||epoch_millis"
      },
      "location": { "type": "geo_point" },
      "tags": { "type": "keyword" },
      "category_path": { "type": "text" },
      "specs": {
        "type": "object",
        "properties": {
          "weight": { "type": "float" },
          "dimensions": { "type": "keyword" }
        }
      },
      "reviews": {
        "type": "nested",
        "properties": {
          "user_id": { "type": "keyword" },
          "rating": { "type": "integer" },
          "comment": { "type": "text" }
        }
      }
    }
  }
}

Phân biệt text và keyword - cực kỳ quan trọng:

textkeyword
Mục đíchFull-text searchExact match, aggregations
AnalyzedCó (tokenize, lowercase...)Không
Ví dụ"iPhone 15 Pro Max" → ["iphone", "15", "pro", "max"]"iPhone 15 Pro Max"
Dùng chomatch queryterm query, sort, aggregation
Case sensitiveKhông (lowercase)Có
bash
# TEXT field - full-text search
GET /products/_search
{
  "query": {
    "match": {
      "name": "iphone pro"  # Tìm được "iPhone 15 Pro Max"
    }
  }
}

# KEYWORD field - exact match
GET /products/_search
{
  "query": {
    "term": {
      "brand.keyword": "Apple"  # Phải match chính xác "Apple"
    }
  }
}

3.2 Shards và Replicas ​

Primary Shards ​

Shard là đơn vị phân tán cơ bản. Mỗi index được chia thành nhiều shards, mỗi shard là một Lucene index độc lập.

INDEX: products (3 shards, 1 replica)
─────────────────────────────────────
Node 1                Node 2               Node 3
┌─────────────┐   ┌─────────────┐   ┌─────────────┐
│  Shard P0   │   │  Shard P1   │   │  Shard P2   │
│ (Primary)   │   │ (Primary)   │   │ (Primary)   │
│             │   │             │   │             │
│  Shard R1   │   │  Shard R2   │   │  Shard R0   │
│ (Replica)   │   │ (Replica)   │   │ (Replica)   │
└─────────────┘   └─────────────┘   └─────────────┘

Quy tắc quan trọng:

  • Primary và Replica không bao giờ nằm trên cùng một node
  • Số primary shards được quyết định khi tạo index và không thay đổi được
  • Số replica shards có thể thay đổi sau khi tạo

Cách tính số shards:

Công thức tham khảo:

Số primary shards = Tổng dung lượng dữ liệu / Target shard size  
Target shard size = 20-40GB (guidelines từ Elastic)

Ví dụ: 
- Dữ liệu: 300GB
- Target shard: 30GB
- Số shards = 300GB / 30GB = 10 shards

Tác hại của quá nhiều shards (over-sharding):

  • Mỗi shard tốn RAM (~1.5MB metadata/shard trong heap)
  • Query overhead: scatter-gather across nhiều shards
  • Segment merging phức tạp hơn

Ví dụ cấu hình:

bash
# Index nhỏ (< 10GB)
PUT /small-index
{
  "settings": {
    "number_of_shards": 1,
    "number_of_replicas": 1
  }
}

# Index vừa (10-100GB)  
PUT /medium-index
{
  "settings": {
    "number_of_shards": 3,
    "number_of_replicas": 1
  }
}

# Index lớn (100GB+)
PUT /large-index
{
  "settings": {
    "number_of_shards": 10,
    "number_of_replicas": 2
  }
}

Routing - Document đến Shard nào? ​

Khi index một document, ES cần quyết định primary shard nào sẽ chứa document đó:

routing = hash(document_id) % number_of_primary_shards

Ví dụ:

document_id = "product-123"
hash("product-123") = 12345678
number_of_primary_shards = 3

routing = 12345678 % 3 = 0  → Shard 0

Đây là lý do không thể thay đổi số primary shards sau khi tạo index - vì công thức routing sẽ sai.

Custom routing:

bash
# Index document vào shard cụ thể
PUT /products/_doc/1?routing=category-electronics
{
  "name": "iPhone 15",
  "category": "electronics"
}

# Search với routing (chỉ search relevant shards)
GET /products/_search?routing=category-electronics
{
  "query": {
    "term": { "category.keyword": "electronics" }
  }
}

Replica Shards ​

Replica là bản sao của primary shard, phục vụ 2 mục đích:

  1. High Availability: Nếu node chứa primary bị chết, replica sẽ được promoted thành primary
  2. Read Throughput: Search request có thể được gửi đến cả primary và replica
bash
# Thay đổi số replicas (có thể thay đổi sau khi tạo)
PUT /products/_settings
{
  "number_of_replicas": 2
}

# 0 replica - Tốt nhất cho bulk indexing
PUT /products/_settings
{
  "number_of_replicas": 0
}
# Sau khi bulk xong:
PUT /products/_settings
{
  "number_of_replicas": 1
}

Trade-off của replicas:

  • Nhiều replicas = High availability + Read throughput cao hơn
  • Nhiều replicas = Tốn thêm disk, RAM, network cho replication
  • Với single node: replicas không được assign (cluster status yellow)

3.3 Inverted Index - Trái Tim của Elasticsearch ​

Lucene Segment ​

Mỗi Elasticsearch shard = một Lucene index. Mỗi Lucene index gồm nhiều segments (immutable).

Elasticsearch Shard
        │
        ▼
   Lucene Index
        │
   ┌────┼────┐
   │    │    │
Seg0  Seg1  Seg2  (immutable files)
   │
   ├── .fdt  (stored fields - _source)
   ├── .fdx  (field index)
   ├── .tim  (term index)  
   ├── .tip  (term index pointer)
   ├── .doc  (frequencies và positions)
   ├── .pos  (positions)
   ├── .pay  (payloads)
   ├── .nvd  (norms data)
   ├── .nvm  (norms metadata)
   └── .dvm  (doc values metadata)

Quá Trình Index Document ​

Bước 1: Document được nhận
{
  "title": "Elasticsearch Guide",
  "content": "Learn Elasticsearch from scratch"
}
                    │
                    ▼
Bước 2: Analyzer xử lý text fields
"Elasticsearch Guide" 
  → tokenize: ["Elasticsearch", "Guide"]
  → lowercase: ["elasticsearch", "guide"]
  → stemming/stop words nếu có

"Learn Elasticsearch from scratch"
  → ["learn", "elasticsearch", "from", "scratch"]
  → (stop words): ["learn", "elasticsearch", "scratch"]
                    │
                    ▼
Bước 3: Ghi vào in-memory buffer
(chờ refresh)
                    │
          ┌─────────┴─────────┐
          │                   │
          ▼                   ▼
  Translog            Refresh (mỗi 1s)
  (WAL for           Tạo mới Segment
  durability)        từ buffer
                          │
                          ▼
                  Document searchable
                   (near real-time)

Inverted Index Chi Tiết ​

Sau khi analyze, ES xây dựng inverted index:

Term Dictionary:
┌────────────────┬────────────────────────────────────┐
│ Term           │ Posting List                       │
├────────────────┼────────────────────────────────────┤
│ elasticsearch  │ Doc1(pos:0), Doc2(pos:1), Doc5(pos:0) │
│ guide          │ Doc1(pos:1), Doc3(pos:2)            │
│ learn          │ Doc2(pos:0)                         │
│ scratch        │ Doc2(pos:3)                         │
│ tutorial       │ Doc3(pos:0), Doc4(pos:0)            │
└────────────────┴────────────────────────────────────┘

Posting List chi tiết (với positions và frequencies):
"elasticsearch":
  - Doc1: freq=1, positions=[0]
  - Doc2: freq=2, positions=[1, 5]  (xuất hiện 2 lần)
  - Doc5: freq=1, positions=[0]

Positions cho phép phrase queries:

bash
# Phrase query - tìm "elasticsearch guide" liên tiếp
GET /docs/_search
{
  "query": {
    "match_phrase": {
      "title": "elasticsearch guide"
    }
  }
}

Segment Merge ​

Lucene segments là immutable. Khi delete/update document:

  • Delete: Đánh dấu "deleted" trong .liv file, không xóa khỏi segment
  • Update: Delete old + Insert new

Theo thời gian, nhiều small segments được merge thành ít large segments hơn:

Before merge:
Seg0 (100 docs, 20 deleted)
Seg1 (50 docs, 5 deleted)
Seg2 (30 docs, 10 deleted)
         │
    Merge bởi Lucene
         │
After merge:
Seg3 (145 docs, 0 deleted)  ← docs deleted thật sự ở đây

Force merge (dùng cẩn thận, chỉ cho read-only indices):

bash
POST /products/_forcemerge?max_num_segments=1

3.4 Node Roles trong Cluster ​

Các Loại Node ​

yaml
# elasticsearch.yml - Cấu hình node roles

# Master-eligible node (quản lý cluster)
node.roles: [master]

# Data node (lưu trữ và xử lý data)
node.roles: [data]

# Data nodes theo tier (Elasticsearch 7.10+)
node.roles: [data_hot]   # SSD, frequent access
node.roles: [data_warm]  # HDD, less frequent
node.roles: [data_cold]  # Archived data
node.roles: [data_frozen]# Very rarely accessed

# Ingest node (pre-processing)
node.roles: [ingest]

# Coordinating-only node (route requests)
node.roles: []  # Empty = coordinating only

# ML node
node.roles: [ml]

# Transform node
node.roles: [transform]

Master Node ​

Master node chịu trách nhiệm:

  1. Theo dõi trạng thái của tất cả nodes
  2. Quyết định shard allocation
  3. Quản lý cluster state changes (tạo/xóa index)
  4. Cluster-wide settings

Master election: Elasticsearch dùng Raft-based consensus algorithm:

Cluster cần minimum (N/2 + 1) master-eligible nodes để vote
→ Tối thiểu 3 master-eligible nodes để có fault tolerance

Nếu chỉ có 2 nodes:
- 1 node fail → không đủ quorum → cluster stop nhận writes
→ Luôn dùng số lẻ (3, 5, 7)

Dedicated master (production):

yaml
# Master node - không lưu data
node.roles: [master]
node.master: true

# Tắt data trên master để giảm load
# Chỉ làm cluster management

Data Node ​

Data nodes lưu trữ shards và xử lý CRUD + search operations.

Data tiers (ILM - Index Lifecycle Management):

Hot Tier (SSD)          Warm Tier (SSD/HDD)    Cold Tier (HDD)     Frozen Tier (S3)
─────────────           ───────────────────    ────────────        ─────────────────
Indices hiện tại        Indices ít truy cập    Archive data        Very old data
Write + Read Heavy      Read-only              Read-only           Search on demand
Fast indexing           Slower queries OK      Slow queries OK     Very slow
Replicas = 1+           Replicas có thể = 0    Replicas = 0        Mount on demand

Coordinating Node ​

Mỗi node có thể là coordinating node. Khi nhận search request:

Client → Coordinating Node
              │
    ┌─────────┼─────────┐
    │         │         │
  Shard 0   Shard 1   Shard 2
(Node 1)  (Node 2)  (Node 3)
    │         │         │
    └─────────┼─────────┘
              │
    Merge results
    Sort & Rank globally
              │
           Response

Dedicated coordinating nodes thường dùng khi:

  • Cluster có heavy search load
  • Muốn tách biệt coordination overhead khỏi data nodes
  • API gateway pattern

3.5 Cluster State và Cluster Metadata ​

Cluster state chứa:

  • Index metadata (settings, mappings, aliases)
  • Shard allocation (shard nào ở node nào)
  • Nodes trong cluster

Cluster state được đồng bộ trên tất cả nodes và được quản lý bởi master node.

bash
# Xem cluster state (rất lớn, cẩn thận)
GET /_cluster/state/metadata/products

# Xem cluster state ngắn gọn
GET /_cluster/state/metadata?filter_path=metadata.indices.*.settings

Elasticsearch không phải real-time 100% - nó near real-time (NRT).

Refresh Cycle ​

Document indexed
       │
       ▼
In-memory buffer      Translog
(not searchable yet)  (durability)
       │
    [Refresh - default: 1 giây]
       │  
       ▼
   Lucene Segment  ← Document NOW searchable
(immutable file)

Tùy chỉnh refresh interval:

bash
# Tắt auto-refresh (cho bulk indexing)
PUT /products/_settings
{
  "refresh_interval": "-1"
}

# Sau khi bulk xong:
PUT /products/_settings
{
  "refresh_interval": "1s"
}

# Force refresh ngay lập tức
POST /products/_refresh

# Index document và refresh ngay
PUT /products/_doc/1?refresh=true
{
  "name": "iPhone 15"
}

# Index document và chờ refresh
PUT /products/_doc/1?refresh=wait_for
{
  "name": "iPhone 15"
}

Translog - Durability ​

Translog là Write-Ahead Log (WAL) như trong databases:

Document write
      │
      ├──→ Memory buffer (sẽ refresh)
      │
      └──→ Translog (survives crash)
                │
           [Flush - default: 5 phút hoặc khi translog đạt 512MB]
                │
                ▼
          Lucene commit
          (fsync to disk)
          Translog cleared

Flush vs Refresh:

  • Refresh: Buffer → Segment (searchable), mỗi 1 giây
  • Flush: Lucene commit + clear translog, mỗi 5 phút

3.7 Aliases - Quản Lý Index Linh Hoạt ​

Alias là tên tham chiếu đến một hoặc nhiều indices. Rất quan trọng trong production.

Tại sao dùng Alias? ​

Bài toán: Bạn có index products-v1 với 3 shards. Giờ cần tái cấu trúc (reindex) thành products-v2 với 6 shards. Nếu application code dùng index name trực tiếp, bạn phải deploy code mới. Nhưng nếu dùng alias products, chỉ cần: swap alias.

bash
# Tạo alias trỏ đến index cụ thể
POST /_aliases
{
  "actions": [
    {
      "add": {
        "index": "products-v1",
        "alias": "products"
      }
    }
  ]
}

# Application đọc/ghi qua alias
GET /products/_search {...}
POST /products/_doc {...}

# Zero-downtime reindex
# Bước 1: Reindex sang index mới
POST /_reindex
{
  "source": { "index": "products-v1" },
  "dest": { "index": "products-v2" }
}

# Bước 2: Swap alias atomically
POST /_aliases
{
  "actions": [
    { "remove": { "index": "products-v1", "alias": "products" } },
    { "add": { "index": "products-v2", "alias": "products" } }
  ]
}

Alias với Filter (Virtual Index) ​

bash
# Alias với filter - chỉ thấy active products
POST /_aliases
{
  "actions": [
    {
      "add": {
        "index": "products",
        "alias": "active_products",
        "filter": {
          "term": { "status": "active" }
        }
      }
    }
  ]
}

# Query qua alias - tự động filter
GET /active_products/_search
{
  "query": { "match_all": {} }
  # Tự động thêm: status = active
}

Alias với Write Index ​

Khi alias trỏ đến nhiều indices:

bash
POST /_aliases
{
  "actions": [
    {
      "add": {
        "index": "products-2024-01",
        "alias": "products",
        "is_write_index": true  # Writes go here
      }
    },
    {
      "add": {
        "index": "products-2023",
        "alias": "products"
        # Reads come from both, writes only to 2024-01
      }
    }
  ]
}

3.8 Index Templates ​

Templates cho phép tự động áp dụng settings/mappings khi tạo index mới khớp với pattern.

bash
# Index Template cơ bản
PUT /_index_template/logs-template
{
  "index_patterns": ["logs-*", "events-*"],
  "priority": 1,
  "template": {
    "settings": {
      "number_of_shards": 1,
      "number_of_replicas": 1,
      "index.lifecycle.name": "logs-ilm-policy",
      "index.lifecycle.rollover_alias": "logs"
    },
    "mappings": {
      "properties": {
        "@timestamp": {
          "type": "date"
        },
        "level": {
          "type": "keyword"
        },
        "message": {
          "type": "text"
        },
        "service": {
          "type": "keyword"
        },
        "trace_id": {
          "type": "keyword"
        }
      }
    }
  }
}

# Khi tạo index "logs-2024-01-15" tự động áp dụng template
PUT /logs-2024-01-15
# Không cần specify settings - lấy từ template

3.9 Data Streams ​

Data Streams là abstraction mới (ES 7.9+) cho time-series data (logs, metrics, traces).

bash
# Tạo index template cho data stream
PUT /_index_template/logs-ds-template
{
  "index_patterns": ["logs-app-*"],
  "data_stream": {},
  "template": {
    "settings": {
      "number_of_shards": 1
    },
    "mappings": {
      "properties": {
        "@timestamp": {
          "type": "date"
        },
        "message": { "type": "text" }
      }
    }
  }
}

# Tạo data stream
PUT /_data_stream/logs-app-prod

# Index document vào data stream
POST /logs-app-prod/_doc
{
  "@timestamp": "2024-01-15T10:30:00Z",
  "message": "Application started",
  "level": "INFO"
}

Data stream tự động quản lý:

  • Rollover: Tạo index mới khi index hiện tại quá lớn/cũ
  • Backing indices: logs-app-prod-000001, logs-app-prod-000002...
  • Write luôn vào index mới nhất
  • Read từ tất cả backing indices

3.10 Cluster Coordination và Fault Tolerance ​

Split Brain Problem ​

Split brain xảy ra khi network partition chia cluster thành 2 phần, cả 2 đều nghĩ mình là primary:

Trước khi partition:
Node1(Master) ─────── Node2 ─────── Node3

Sau network partition:
Node1(Master)          Node2 ─────── Node3
                                (Node3 được elected master)
                       
Cả 2 group đều accept writes → data inconsistency!

Elasticsearch giải quyết bằng minimum_master_nodes (ES 6) hoặc voting configuration (ES 7+):

yaml
# ES 7+: Tự động tính dựa trên master-eligible nodes
# Nếu có 3 master-eligible nodes, minimum quorum = 2
# Network partition → nhóm 1 node không thể form cluster
# → Chỉ nhóm 2 nodes tiếp tục hoạt động

Node Failure và Recovery ​

bash
# Xem allocation status
GET /_cluster/allocation/explain

# Khi node fail, ES tự động:
# 1. Replica của shards trên failed node được promoted to primary
# 2. New replicas được tạo trên remaining nodes
# 3. Nếu primary bị mất → data có thể mất (nếu chỉ có 1 replica và cũng mất)

# Delay allocation - tránh unnecessary rebalancing
PUT /_cluster/settings
{
  "persistent": {
    "index.unassigned.node_left.delayed_timeout": "5m"
  }
}
# ES chờ 5 phút trước khi reallocate, 
# cho node fail thời gian để restart

Tóm Tắt Chương 3 ​

Kiến trúc dữ liệu:

  • Index = Table trong SQL
  • Document = Row, được lưu dưới dạng JSON
  • Field = Column, có data type
  • Mapping = Schema

Phân tán:

  • Primary Shard: Đơn vị phân tán, số cố định khi tạo
  • Replica Shard: Bản sao, số có thể thay đổi
  • Routing: hash(doc_id) % num_primary_shards
  • Không bao giờ để primary và replica cùng node

Tìm kiếm:

  • Inverted Index: Cấu trúc data lookup nhanh
  • Segment: Immutable Lucene file, merge theo thời gian
  • Refresh (1s): Buffer → Segment (near real-time)
  • Flush (5m): Lucene commit + clear translog

Cluster:

  • Master node: Quản lý cluster state
  • Data node: Lưu trữ shards
  • Coordinating: Route requests, merge results
  • Quorum: N/2 + 1 để tránh split brain

Best practices:

  • Dùng Alias thay vì index name trực tiếp trong code
  • Index Template cho time-series data
  • Data Streams cho logs/metrics
  • Single node → yellow status (replicas unassigned) là bình thường

Bước Tiếp Theo ​

→ Chương 4: CRUD Operations - Thực hành các thao tác cơ bản với documents


Chương 4: CRUD Operations - Thao Tác Cơ Bản ​

4.1 Setup Dữ Liệu Thực Hành ​

Trước tiên, tạo index và insert dữ liệu mẫu. Chúng ta sẽ dùng dataset về sản phẩm thương mại điện tử xuyên suốt khóa học.

bash
# Tạo index products với mapping cụ thể
PUT /products
{
  "settings": {
    "number_of_shards": 1,
    "number_of_replicas": 0
  },
  "mappings": {
    "properties": {
      "product_id": { "type": "keyword" },
      "name": {
        "type": "text",
        "fields": {
          "keyword": { "type": "keyword" }
        }
      },
      "brand": { "type": "keyword" },
      "category": { "type": "keyword" },
      "subcategory": { "type": "keyword" },
      "description": { "type": "text" },
      "price": { "type": "double" },
      "original_price": { "type": "double" },
      "discount_percentage": { "type": "integer" },
      "rating": { "type": "float" },
      "review_count": { "type": "integer" },
      "in_stock": { "type": "boolean" },
      "stock_quantity": { "type": "integer" },
      "tags": { "type": "keyword" },
      "images": { "type": "keyword", "index": false },
      "created_at": { "type": "date" },
      "updated_at": { "type": "date" }
    }
  }
}

4.2 Index API - Tạo Document ​

Tạo Document với ID tự định nghĩa ​

bash
# PUT - Chỉ định ID
PUT /products/_doc/1
{
  "product_id": "SP001",
  "name": "iPhone 15 Pro Max 256GB",
  "brand": "Apple",
  "category": "smartphones",
  "subcategory": "iOS",
  "description": "iPhone 15 Pro Max với chip A17 Pro, camera 48MP, màn hình 6.7 inch Super Retina XDR",
  "price": 34990000,
  "original_price": 38490000,
  "discount_percentage": 9,
  "rating": 4.8,
  "review_count": 1250,
  "in_stock": true,
  "stock_quantity": 45,
  "tags": ["flagship", "5G", "iOS", "camera"],
  "images": [
    "https://example.com/iphone15-1.jpg",
    "https://example.com/iphone15-2.jpg"
  ],
  "created_at": "2024-01-10T08:00:00Z",
  "updated_at": "2024-01-15T10:30:00Z"
}

Response:

json
{
  "_index": "products",
  "_id": "1",
  "_version": 1,
  "result": "created",
  "_shards": {
    "total": 1,
    "successful": 1,
    "failed": 0
  },
  "_seq_no": 0,
  "_primary_term": 1
}

Tạo Document với ID tự động (POST) ​

bash
# POST - ES tự sinh ID (UUID)
POST /products/_doc
{
  "product_id": "SP002",
  "name": "Samsung Galaxy S24 Ultra",
  "brand": "Samsung",
  "category": "smartphones",
  "subcategory": "Android",
  "description": "Samsung Galaxy S24 Ultra với bút S Pen, camera 200MP, màn hình 6.8 inch Dynamic AMOLED",
  "price": 31990000,
  "original_price": 33990000,
  "discount_percentage": 6,
  "rating": 4.7,
  "review_count": 890,
  "in_stock": true,
  "stock_quantity": 62,
  "tags": ["flagship", "5G", "Android", "S-Pen", "camera"],
  "created_at": "2024-01-12T09:00:00Z",
  "updated_at": "2024-01-15T11:00:00Z"
}

Response với auto-generated ID:

json
{
  "_index": "products",
  "_id": "xK7mHI4BpQz3t2RfMnLw",   // Auto-generated UUID
  "_version": 1,
  "result": "created",
  ...
}

Tạo Document chỉ khi chưa tồn tại (CREATE - not UPDATE) ​

bash
# Sử dụng _create endpoint
PUT /products/_create/1
{
  "name": "iPhone 15 Pro Max"
}
# Nếu ID=1 đã tồn tại → lỗi 409 Conflict

# Hoặc op_type=create
PUT /products/_doc/1?op_type=create
{
  "name": "iPhone 15 Pro Max"
}

Khi lỗi duplicate:

json
{
  "error": {
    "type": "version_conflict_engine_exception",
    "reason": "[1]: version conflict, document already exists (current version [1])"
  },
  "status": 409
}

4.3 Get API - Đọc Document ​

Lấy Document theo ID ​

bash
GET /products/_doc/1

# Response:
{
  "_index": "products",
  "_id": "1",
  "_version": 1,
  "_seq_no": 0,
  "_primary_term": 1,
  "found": true,
  "_source": {
    "product_id": "SP001",
    "name": "iPhone 15 Pro Max 256GB",
    "brand": "Apple",
    "price": 34990000,
    ...
  }
}

Khi Document không tồn tại ​

bash
GET /products/_doc/999

# Response:
{
  "_index": "products",
  "_id": "999",
  "found": false
}
# HTTP 404

Lấy chỉ một số fields (source filtering) ​

bash
# Chỉ lấy name và price
GET /products/_doc/1?_source_includes=name,price

# Loại trừ fields nặng
GET /products/_doc/1?_source_excludes=description,images

# Tắt _source hoàn toàn (chỉ metadata)
GET /products/_doc/1?_source=false

Multi Get API - Lấy nhiều documents cùng lúc ​

bash
GET /products/_mget
{
  "ids": ["1", "2", "3"]
}

# Hoặc từ nhiều indices
GET /_mget
{
  "docs": [
    { "_index": "products", "_id": "1" },
    { "_index": "products", "_id": "2" },
    { "_index": "orders", "_id": "ORD001" }
  ]
}

# Response:
{
  "docs": [
    {
      "_index": "products",
      "_id": "1",
      "found": true,
      "_source": { ... }
    },
    {
      "_index": "products",
      "_id": "999",
      "found": false
    }
  ]
}

Kiểm tra Document Tồn Tại (HEAD) ​

bash
# Chỉ check existence, không trả về data (tiết kiệm bandwidth)
HEAD /products/_doc/1

# HTTP 200 nếu tồn tại, 404 nếu không

4.4 Update API - Cập Nhật Document ​

Partial Update (chỉ update field cụ thể) ​

bash
# Update một số fields
POST /products/_update/1
{
  "doc": {
    "price": 32990000,
    "discount_percentage": 14,
    "updated_at": "2024-01-16T09:00:00Z"
  }
}

# Response:
{
  "_index": "products",
  "_id": "1",
  "_version": 2,
  "result": "updated",
  "_shards": { "total": 1, "successful": 1 }
}

Cơ chế hoạt động của Update:

Bước 1: Lấy document hiện tại từ Lucene
Bước 2: Merge doc mới vào document hiện tại
Bước 3: Delete document cũ (đánh dấu deleted)
Bước 4: Index document mới

Đây là lý do Update trong ES không phải in-place → chậm hơn so với SQL UPDATE.

Upsert - Update hoặc Insert nếu không tồn tại ​

bash
POST /products/_update/5
{
  "doc": {
    "stock_quantity": 100,
    "updated_at": "2024-01-16T10:00:00Z"
  },
  "upsert": {
    "product_id": "SP005",
    "name": "New Product",
    "stock_quantity": 100,
    "price": 5000000,
    "created_at": "2024-01-16T10:00:00Z",
    "updated_at": "2024-01-16T10:00:00Z"
  }
}

Scripted Update - Update với logic phức tạp ​

bash
# Tăng giá sản phẩm lên 10%
POST /products/_update/1
{
  "script": {
    "source": "ctx._source.price = ctx._source.price * 1.1",
    "lang": "painless"
  }
}

# Thêm element vào array
POST /products/_update/1
{
  "script": {
    "source": "ctx._source.tags.add(params.new_tag)",
    "lang": "painless",
    "params": {
      "new_tag": "sale"
    }
  }
}

# Xóa element khỏi array
POST /products/_update/1
{
  "script": {
    "source": "ctx._source.tags.removeAll(Collections.singletonList(params.tag_to_remove))",
    "lang": "painless",
    "params": {
      "tag_to_remove": "old-tag"
    }
  }
}

# Conditional update
POST /products/_update/1
{
  "script": {
    "source": """
      if (ctx._source.stock_quantity > 0) {
        ctx._source.stock_quantity--;
        if (ctx._source.stock_quantity == 0) {
          ctx._source.in_stock = false;
        }
      } else {
        ctx.op = 'noop';
      }
    """,
    "lang": "painless"
  }
}

Update By Query - Cập nhật nhiều documents ​

bash
# Tăng giá tất cả sản phẩm Apple lên 5%
POST /products/_update_by_query
{
  "script": {
    "source": "ctx._source.price = ctx._source.price * 1.05",
    "lang": "painless"
  },
  "query": {
    "term": {
      "brand": "Apple"
    }
  }
}

# Response:
{
  "took": 156,
  "timed_out": false,
  "total": 5,
  "updated": 5,
  "deleted": 0,
  "batches": 1,
  "conflicts": 0,
  "noops": 0,
  "throttled_millis": 0,
  "failures": []
}

Full Document Replace ​

bash
# PUT thay thế TOÀN BỘ document
PUT /products/_doc/1
{
  "name": "iPhone 15 Pro Max 512GB",
  "price": 39990000
  # Tất cả fields khác bị XÓA!
}

Cẩn thận: PUT _doc sẽ xóa tất cả fields không có trong request mới.


4.5 Delete API - Xóa Document ​

Xóa Document theo ID ​

bash
DELETE /products/_doc/1

# Response:
{
  "_index": "products",
  "_id": "1",
  "_version": 2,    # Version tăng khi delete
  "result": "deleted",
  "_shards": { "total": 1, "successful": 1, "failed": 0 }
}

Delete By Query - Xóa nhiều documents ​

bash
# Xóa tất cả products hết hàng và không được review
POST /products/_delete_by_query
{
  "query": {
    "bool": {
      "must": [
        { "term": { "in_stock": false } },
        { "range": { "review_count": { "lte": 0 } } }
      ]
    }
  }
}

# Xóa với async (cho dataset lớn)
POST /products/_delete_by_query?wait_for_completion=false
{
  "query": { "match_all": {} }
}
# Returns task ID để track tiến trình

Xóa Index ​

bash
DELETE /products

# Xóa nhiều indices
DELETE /products,orders

# Xóa theo pattern (cẩn thận!)
DELETE /logs-2023-*

4.6 Bulk API - Thao Tác Hàng Loạt ​

Đây là API quan trọng nhất cho performance khi xử lý nhiều documents.

Format của Bulk Request ​

{"action": {"metadata"}}
{"document_data"}
{"action": {"metadata"}}
{"document_data"}
...

Lưu ý quan trọng: Mỗi cặp action/data phải trên 2 dòng riêng biệt (NDJSON format).

bash
POST /products/_bulk
{"index": {"_id": "2"}}
{"product_id": "SP002", "name": "Samsung Galaxy S24 Ultra", "brand": "Samsung", "price": 31990000, "rating": 4.7, "in_stock": true, "category": "smartphones"}
{"index": {"_id": "3"}}
{"product_id": "SP003", "name": "Xiaomi 14 Pro", "brand": "Xiaomi", "price": 19990000, "rating": 4.5, "in_stock": true, "category": "smartphones"}
{"create": {"_id": "4"}}
{"product_id": "SP004", "name": "OPPO Find X7 Ultra", "brand": "OPPO", "price": 23990000, "rating": 4.6, "in_stock": false, "category": "smartphones"}
{"update": {"_id": "2"}}
{"doc": {"discount_percentage": 10}}
{"delete": {"_id": "999"}}

Note: Dòng cuối cùng phải có newline \n.

Bulk với nhiều indices ​

bash
POST /_bulk
{"index": {"_index": "products", "_id": "10"}}
{"name": "Laptop Dell XPS 15", "price": 45000000}
{"index": {"_index": "orders", "_id": "ORD001"}}
{"user_id": "U001", "total": 45000000, "status": "processing"}
{"delete": {"_index": "products", "_id": "old-product-1"}}

Bulk Response và Xử Lý Lỗi ​

json
{
  "took": 45,
  "errors": true,
  "items": [
    {
      "index": {
        "_index": "products",
        "_id": "2",
        "result": "created",
        "status": 201
      }
    },
    {
      "index": {
        "_index": "products",
        "_id": "3",
        "result": "created",
        "status": 201
      }
    },
    {
      "delete": {
        "_index": "products",
        "_id": "999",
        "result": "not_found",
        "status": 404
      }
    }
  ]
}

Bulk API không fail toàn bộ khi một item lỗi - mỗi item có status riêng.

Best Practices khi dùng Bulk API ​

bash
# 1. Optimal bulk size: 5-15MB per request, hoặc 1000-5000 documents
# Quá lớn → GC pressure, quá nhỏ → overhead

# 2. Tắt replicas trong khi bulk loading
PUT /products/_settings
{
  "number_of_replicas": 0,
  "refresh_interval": "-1"
}

# 3. Bulk load
POST /products/_bulk
... many documents ...

# 4. Bật lại replicas và refresh
PUT /products/_settings
{
  "number_of_replicas": 1,
  "refresh_interval": "1s"
}

POST /products/_refresh

Bulk với Node.js (Thực tế) ​

javascript
const { Client } = require('@elastic/elasticsearch');
const fs = require('fs');
const readline = require('readline');

const client = new Client({ node: 'http://localhost:9200' });

async function bulkIndexProducts(products) {
  const operations = products.flatMap(product => [
    { index: { _index: 'products', _id: product.id } },
    product
  ]);

  const { body: bulkResponse } = await client.bulk({
    refresh: true,
    operations
  });

  if (bulkResponse.errors) {
    const erroredDocuments = [];
    bulkResponse.items.forEach((action, i) => {
      const operation = Object.keys(action)[0];
      if (action[operation].error) {
        erroredDocuments.push({
          status: action[operation].status,
          error: action[operation].error,
          operation: operations[i * 2],
          document: operations[i * 2 + 1]
        });
      }
    });
    console.error('Failed documents:', erroredDocuments);
  }

  return bulkResponse;
}

// Đọc từ CSV và bulk index theo batches
async function indexFromFile(filePath) {
  const BATCH_SIZE = 1000;
  let batch = [];
  let totalIndexed = 0;

  const rl = readline.createInterface({
    input: fs.createReadStream(filePath),
    crlfDelay: Infinity
  });

  for await (const line of rl) {
    const product = JSON.parse(line);
    batch.push(product);

    if (batch.length >= BATCH_SIZE) {
      await bulkIndexProducts(batch);
      totalIndexed += batch.length;
      console.log(`Indexed ${totalIndexed} documents`);
      batch = [];
    }
  }

  // Index remaining
  if (batch.length > 0) {
    await bulkIndexProducts(batch);
    totalIndexed += batch.length;
  }

  console.log(`Total indexed: ${totalIndexed} documents`);
}

4.7 Search API Cơ Bản ​

URI Search (Quick & Dirty) ​

bash
# Search đơn giản qua URL parameter
GET /products/_search?q=iPhone

# Với field cụ thể
GET /products/_search?q=name:iPhone

# Với multiple terms
GET /products/_search?q=brand:Apple AND category:smartphones

# Với sort và pagination
GET /products/_search?q=Apple&sort=price:asc&from=0&size=10
bash
# Match all documents
GET /products/_search
{
  "query": {
    "match_all": {}
  }
}

# Với pagination và sorting
GET /products/_search
{
  "from": 0,
  "size": 10,
  "query": {
    "match": {
      "name": "iPhone"
    }
  },
  "sort": [
    { "price": "asc" },
    { "_score": "desc" }
  ],
  "_source": ["name", "brand", "price", "rating"]
}

Response structure:

json
{
  "took": 5,
  "timed_out": false,
  "_shards": {
    "total": 1,
    "successful": 1,
    "skipped": 0,
    "failed": 0
  },
  "hits": {
    "total": {
      "value": 3,
      "relation": "eq"
    },
    "max_score": 1.5,
    "hits": [
      {
        "_index": "products",
        "_id": "1",
        "_score": 1.5,
        "_source": {
          "name": "iPhone 15 Pro Max 256GB",
          "brand": "Apple",
          "price": 34990000,
          "rating": 4.8
        }
      }
    ]
  }
}

4.8 Concurrency Control ​

Optimistic Concurrency Control ​

ES sử dụng _seq_no và _primary_term cho optimistic locking:

bash
# Lấy document với seq_no và primary_term
GET /products/_doc/1

# Response có:
{
  "_seq_no": 5,
  "_primary_term": 1,
  ...
}

# Update chỉ khi seq_no và primary_term khớp
PUT /products/_doc/1?if_seq_no=5&if_primary_term=1
{
  "name": "Updated iPhone 15 Pro Max",
  "price": 33990000
}

# Nếu document đã được update bởi process khác:
# 409 Conflict
{
  "error": {
    "type": "version_conflict_engine_exception",
    "reason": "[1]: version conflict, required seqNo [5], primary term [1]. 
               current document has seqNo [7] and primary term [1]"
  }
}

Use case thực tế - Shopping cart:

javascript
async function decreaseStock(productId, quantity) {
  const maxRetries = 3;
  
  for (let attempt = 0; attempt < maxRetries; attempt++) {
    // Get current state
    const { body: product } = await client.get({
      index: 'products',
      id: productId
    });
    
    const currentStock = product._source.stock_quantity;
    
    if (currentStock < quantity) {
      throw new Error('Insufficient stock');
    }
    
    try {
      // Try to update with concurrency check
      await client.index({
        index: 'products',
        id: productId,
        if_seq_no: product._seq_no,
        if_primary_term: product._primary_term,
        document: {
          ...product._source,
          stock_quantity: currentStock - quantity,
          in_stock: (currentStock - quantity) > 0
        }
      });
      
      return { success: true, remaining_stock: currentStock - quantity };
      
    } catch (error) {
      if (error.statusCode === 409 && attempt < maxRetries - 1) {
        // Conflict - retry
        console.log(`Retry attempt ${attempt + 1}`);
        await new Promise(resolve => setTimeout(resolve, 100 * (attempt + 1)));
        continue;
      }
      throw error;
    }
  }
  
  throw new Error('Max retries exceeded');
}

4.9 Reindex API ​

Dùng khi cần tái cấu trúc index (thay đổi mapping, số shards):

bash
# Reindex từ index này sang index khác
POST /_reindex
{
  "source": {
    "index": "products-v1"
  },
  "dest": {
    "index": "products-v2"
  }
}

# Reindex với query filter (chỉ copy một phần)
POST /_reindex
{
  "source": {
    "index": "products",
    "query": {
      "term": { "brand": "Apple" }
    }
  },
  "dest": {
    "index": "apple-products"
  }
}

# Reindex với script transformation
POST /_reindex
{
  "source": {
    "index": "old-products"
  },
  "dest": {
    "index": "new-products"
  },
  "script": {
    "source": """
      // Đổi tên field
      ctx._source.product_name = ctx._source.name;
      ctx._source.remove('name');
      
      // Tính discount
      if (ctx._source.original_price != null && ctx._source.price != null) {
        ctx._source.discount_amount = ctx._source.original_price - ctx._source.price;
      }
      
      // Convert string to integer
      ctx._source.view_count = Integer.parseInt(ctx._source.views);
      ctx._source.remove('views');
    """,
    "lang": "painless"
  }
}

# Async reindex cho large datasets
POST /_reindex?wait_for_completion=false
{
  "source": { "index": "millions-of-products" },
  "dest": { "index": "new-millions-of-products" }
}
# Returns: {"task": "abc123:456"}

# Check task progress
GET /_tasks/abc123:456

4.10 Insert Bộ Dữ Liệu Hoàn Chỉnh để Thực Hành ​

bash
# Insert nhiều sản phẩm để practice
POST /products/_bulk
{"index": {"_id": "1"}}
{"product_id": "SP001", "name": "iPhone 15 Pro Max 256GB", "brand": "Apple", "category": "smartphones", "subcategory": "iOS", "price": 34990000, "original_price": 38490000, "discount_percentage": 9, "rating": 4.8, "review_count": 1250, "in_stock": true, "stock_quantity": 45, "tags": ["flagship", "5G", "iOS", "camera"], "created_at": "2024-01-10T08:00:00Z"}
{"index": {"_id": "2"}}
{"product_id": "SP002", "name": "Samsung Galaxy S24 Ultra 256GB", "brand": "Samsung", "category": "smartphones", "subcategory": "Android", "price": 31990000, "original_price": 33990000, "discount_percentage": 6, "rating": 4.7, "review_count": 890, "in_stock": true, "stock_quantity": 62, "tags": ["flagship", "5G", "Android", "S-Pen"], "created_at": "2024-01-12T09:00:00Z"}
{"index": {"_id": "3"}}
{"product_id": "SP003", "name": "Xiaomi 14 Pro 512GB", "brand": "Xiaomi", "category": "smartphones", "subcategory": "Android", "price": 19990000, "original_price": 21990000, "discount_percentage": 9, "rating": 4.5, "review_count": 456, "in_stock": true, "stock_quantity": 120, "tags": ["flagship", "5G", "Android", "Leica"], "created_at": "2024-01-08T10:00:00Z"}
{"index": {"_id": "4"}}
{"product_id": "SP004", "name": "MacBook Pro 16 M3 Max", "brand": "Apple", "category": "laptops", "subcategory": "macOS", "price": 89990000, "original_price": 94990000, "discount_percentage": 5, "rating": 4.9, "review_count": 678, "in_stock": true, "stock_quantity": 23, "tags": ["professional", "M3", "macOS"], "created_at": "2024-01-05T08:00:00Z"}
{"index": {"_id": "5"}}
{"product_id": "SP005", "name": "Dell XPS 15 9530", "brand": "Dell", "category": "laptops", "subcategory": "Windows", "price": 45990000, "original_price": 49990000, "discount_percentage": 8, "rating": 4.4, "review_count": 234, "in_stock": true, "stock_quantity": 18, "tags": ["professional", "OLED", "Windows"], "created_at": "2024-01-06T09:00:00Z"}
{"index": {"_id": "6"}}
{"product_id": "SP006", "name": "iPad Pro 12.9 M2 256GB WiFi", "brand": "Apple", "category": "tablets", "subcategory": "iPadOS", "price": 27990000, "original_price": 29990000, "discount_percentage": 7, "rating": 4.7, "review_count": 567, "in_stock": true, "stock_quantity": 34, "tags": ["M2", "iPadOS", "creative"], "created_at": "2024-01-07T08:00:00Z"}
{"index": {"_id": "7"}}
{"product_id": "SP007", "name": "Sony WH-1000XM5 Headphones", "brand": "Sony", "category": "audio", "subcategory": "headphones", "price": 8990000, "original_price": 9990000, "discount_percentage": 10, "rating": 4.8, "review_count": 2100, "in_stock": true, "stock_quantity": 89, "tags": ["ANC", "wireless", "premium"], "created_at": "2024-01-09T10:00:00Z"}
{"index": {"_id": "8"}}
{"product_id": "SP008", "name": "Samsung 65\" QLED 4K Smart TV", "brand": "Samsung", "category": "televisions", "subcategory": "QLED", "price": 35990000, "original_price": 42990000, "discount_percentage": 16, "rating": 4.6, "review_count": 345, "in_stock": false, "stock_quantity": 0, "tags": ["4K", "QLED", "Smart TV", "large"], "created_at": "2024-01-04T08:00:00Z"}
{"index": {"_id": "9"}}
{"product_id": "SP009", "name": "Asus ROG Strix G16 RTX 4070", "brand": "Asus", "category": "laptops", "subcategory": "gaming", "price": 52990000, "original_price": 57990000, "discount_percentage": 9, "rating": 4.5, "review_count": 189, "in_stock": true, "stock_quantity": 15, "tags": ["gaming", "RTX4070", "144Hz", "RGB"], "created_at": "2024-01-11T10:00:00Z"}
{"index": {"_id": "10"}}
{"product_id": "SP010", "name": "AirPods Pro 2nd Gen", "brand": "Apple", "category": "audio", "subcategory": "earbuds", "price": 6490000, "original_price": 6990000, "discount_percentage": 7, "rating": 4.7, "review_count": 3400, "in_stock": true, "stock_quantity": 156, "tags": ["ANC", "wireless", "iOS"], "created_at": "2024-01-13T09:00:00Z"}

4.11 Thực Hành: CRUD với Real-World Scenarios ​

Scenario 1: E-commerce - Xử lý đơn hàng ​

bash
# Kiểm tra tồn kho trước khi đặt hàng
GET /products/_doc/1

# Giảm số lượng tồn kho (sử dụng scripted update)
POST /products/_update/1
{
  "script": {
    "source": """
      if (ctx._source.stock_quantity >= params.quantity) {
        ctx._source.stock_quantity -= params.quantity;
        ctx._source.in_stock = ctx._source.stock_quantity > 0;
        ctx._source.updated_at = params.now;
      } else {
        // Không update, thông báo là noop
        ctx.op = 'noop';
      }
    """,
    "lang": "painless",
    "params": {
      "quantity": 2,
      "now": "2024-01-16T14:30:00Z"
    }
  }
}

Scenario 2: Content Management - Bulk publish articles ​

bash
# Tạo index articles
PUT /articles
{
  "mappings": {
    "properties": {
      "title": { "type": "text", "fields": { "keyword": { "type": "keyword" } } },
      "content": { "type": "text" },
      "author": { "type": "keyword" },
      "category": { "type": "keyword" },
      "tags": { "type": "keyword" },
      "status": { "type": "keyword" },
      "published_at": { "type": "date" },
      "view_count": { "type": "integer" },
      "like_count": { "type": "integer" }
    }
  }
}

# Bulk insert articles
POST /articles/_bulk
{"index": {"_id": "ART001"}}
{"title": "Hướng dẫn sử dụng Elasticsearch cho người mới", "content": "Elasticsearch là một search engine mạnh mẽ...", "author": "Nguyễn Văn A", "category": "technology", "tags": ["elasticsearch", "backend", "search"], "status": "published", "published_at": "2024-01-15T08:00:00Z", "view_count": 1520, "like_count": 89}
{"index": {"_id": "ART002"}}
{"title": "Top 10 frameworks Node.js năm 2024", "content": "Node.js ecosystem ngày càng phát triển...", "author": "Trần Thị B", "category": "technology", "tags": ["nodejs", "javascript", "backend"], "status": "published", "published_at": "2024-01-14T10:00:00Z", "view_count": 2340, "like_count": 156}
{"index": {"_id": "ART003"}}
{"title": "Docker và Kubernetes trong thực tế", "content": "Containerization đã thay đổi cách chúng ta deploy...", "author": "Lê Văn C", "category": "devops", "tags": ["docker", "kubernetes", "devops"], "status": "draft", "view_count": 0, "like_count": 0}

# Update tất cả draft articles của 1 author thành published
POST /articles/_update_by_query
{
  "query": {
    "bool": {
      "must": [
        { "term": { "status": "draft" } },
        { "term": { "author": "Lê Văn C" } }
      ]
    }
  },
  "script": {
    "source": """
      ctx._source.status = 'published';
      ctx._source.published_at = params.published_at;
    """,
    "lang": "painless",
    "params": {
      "published_at": "2024-01-16T09:00:00Z"
    }
  }
}

Scenario 3: User Activity Tracking ​

bash
# Index user events  
PUT /user-events
{
  "settings": { "number_of_shards": 1 },
  "mappings": {
    "properties": {
      "user_id": { "type": "keyword" },
      "event_type": { "type": "keyword" },
      "product_id": { "type": "keyword" },
      "timestamp": { "type": "date" },
      "session_id": { "type": "keyword" },
      "device": { "type": "keyword" },
      "metadata": { "type": "object", "dynamic": true }
    }
  }
}

# Track events realtime
POST /user-events/_doc
{
  "user_id": "U001",
  "event_type": "product_view",
  "product_id": "SP001",
  "timestamp": "2024-01-16T14:30:00Z",
  "session_id": "sess_abc123",
  "device": "mobile",
  "metadata": {
    "referrer": "search",
    "search_query": "iPhone 15",
    "position": 1
  }
}

POST /user-events/_doc
{
  "user_id": "U001",
  "event_type": "add_to_cart",
  "product_id": "SP001",
  "timestamp": "2024-01-16T14:32:00Z",
  "session_id": "sess_abc123",
  "device": "mobile",
  "metadata": {
    "quantity": 1
  }
}

Tóm Tắt Chương 4 ​

OperationAPIMethodGhi chú
Tạo với ID/index/_doc/idPUTOverwrite nếu tồn tại
Tạo auto ID/index/_docPOSTUUID tự sinh
Create only/index/_create/idPUT409 nếu đã có
Lấy document/index/_doc/idGET404 nếu không có
Lấy nhiều docs/index/_mgetGET/POSTBatch get
Check tồn tại/index/_doc/idHEAD200/404
Update partial/index/_update/idPOSTMerge fields
Update script/index/_update/idPOSTLogic phức tạp
Update nhiều/_update_by_queryPOSTVới filter
Xóa document/index/_doc/idDELETE
Xóa nhiều/_delete_by_queryPOSTVới filter
Bulk ops/index/_bulkPOSTNhiều ops cùng lúc
Reindex/_reindexPOSTCopy/transform

Performance tips:

  • Dùng Bulk API cho > 10 documents
  • Tắt refresh_interval và replicas khi bulk loading
  • Dùng _source_includes để giảm network traffic
  • Dùng optimistic concurrency (_seq_no) khi cần tránh race conditions

Bước Tiếp Theo ​

→ Chương 5: Mapping và Data Types - Hiểu sâu về cách ES lưu trữ và phân tích các loại dữ liệu khác nhau


Chương 5: Mapping và Data Types ​

5.1 Mapping là gì? ​

Mapping trong Elasticsearch tương tự như schema trong SQL - nó định nghĩa cách ES lưu trữ và index các fields trong document. Tuy nhiên, ES có hai chế độ mapping:

  1. Dynamic Mapping: ES tự đoán data type khi thấy field mới
  2. Explicit Mapping: Bạn tự định nghĩa rõ ràng từng field

Tại sao Explicit Mapping quan trọng? ​

javascript
// ES dynamic mapping đoán sai:
{
  "phone": "0901234567"  // ES đoán là text (đúng)
  "price": "29.99"       // ES đoán là text (SAI! nên là double)
  "user_id": "123456"    // ES đoán là long (có thể không mong muốn)
  "zip_code": "10000"    // ES đoán là long (SAI! nên là keyword)
}

Hậu quả của dynamic mapping sai:

  • price là text → không sort được, không range query được
  • zip_code là long → không thể query "01234" (leading zero bị mất)
  • user_id là long → tốn memory không cần thiết

5.2 Dynamic Mapping ​

Default Type Mapping Rules ​

JSON valueES data type
"hello"text với keyword sub-field
123long
12.34float
true/falseboolean
"2024-01-15"date
{ "a": 1 }object
[1, 2, 3]long array

Kiểm tra Dynamic Mapping ​

bash
# Index document không có explicit mapping
PUT /auto-mapping-test/_doc/1
{
  "name": "John Doe",
  "age": 30,
  "salary": 15000000.50,
  "is_active": true,
  "created_date": "2024-01-15",
  "address": {
    "city": "Hanoi",
    "district": "Cau Giay"
  },
  "hobbies": ["reading", "coding"]
}

# Xem mapping được tạo tự động
GET /auto-mapping-test/_mapping

Kết quả:

json
{
  "auto-mapping-test": {
    "mappings": {
      "properties": {
        "address": {
          "properties": {
            "city": {
              "type": "text",
              "fields": { "keyword": { "type": "keyword", "ignore_above": 256 } }
            },
            "district": {
              "type": "text",
              "fields": { "keyword": { "type": "keyword", "ignore_above": 256 } }
            }
          }
        },
        "age": { "type": "long" },
        "created_date": { "type": "date" },
        "hobbies": {
          "type": "text",
          "fields": { "keyword": { "type": "keyword", "ignore_above": 256 } }
        },
        "is_active": { "type": "boolean" },
        "name": {
          "type": "text",
          "fields": { "keyword": { "type": "keyword", "ignore_above": 256 } }
        },
        "salary": { "type": "float" }
      }
    }
  }
}

Cấu Hình Dynamic Mapping ​

bash
PUT /products
{
  "mappings": {
    "dynamic": "strict",    # strict | true | false | runtime
    "properties": {
      "name": { "type": "text" },
      "price": { "type": "double" }
    }
  }
}
Giá trịHành vi
true (default)Tự động tạo mapping cho fields mới
falseIgnore fields không có trong mapping (không index, nhưng lưu trong _source)
strictThrow error nếu field không khai báo
runtimeTạo runtime fields thay vì standard fields

5.3 Tất Cả Data Types ​

String Types ​

bash
PUT /articles
{
  "mappings": {
    "properties": {
      "title": {
        "type": "text",
        "analyzer": "standard",       # Analyzer khi indexing
        "search_analyzer": "standard", # Analyzer khi searching (default: same as analyzer)
        "index_options": "positions",  # docs | freqs | positions | offsets
        "term_vector": "with_positions_offsets",  # Cho highlighting
        "similarity": "BM25",          # Similarity algorithm
        "fielddata": false,            # Cần true để sort/agg trên text (tốn RAM!)
        "eager_global_ordinals": false,
        "norms": true                  # Cần cho relevance scoring
      }
    }
  }
}
keyword - Exact match, aggregations, sorting ​
bash
{
  "status": {
    "type": "keyword",
    "ignore_above": 256,     # Strings dài hơn bị ignored khi indexing
    "null_value": "NULL",    # Giá trị thay thế khi field null
    "doc_values": true,      # Cần cho sort/agg
    "normalizer": "lowercase_normalizer",  # Apply trước khi index
    "split_queries_on_whitespace": false
  }
}

Keyword normalizer:

bash
PUT /test-normalizer
{
  "settings": {
    "analysis": {
      "normalizer": {
        "lowercase_normalizer": {
          "type": "custom",
          "filter": ["lowercase", "asciifolding"]
        }
      }
    }
  },
  "mappings": {
    "properties": {
      "status": {
        "type": "keyword",
        "normalizer": "lowercase_normalizer"
      }
    }
  }
}

# "Active", "ACTIVE", "active" đều match nhau với này

Numeric Types ​

bash
{
  "mappings": {
    "properties": {
      "byte_field": { "type": "byte" },           # -128 to 127
      "short_field": { "type": "short" },         # -32768 to 32767
      "int_field": { "type": "integer" },         # ±2^31
      "long_field": { "type": "long" },           # ±2^63 (default cho integers)
      "float_field": { "type": "float" },         # 32-bit IEEE 754
      "double_field": { "type": "double" },       # 64-bit IEEE 754
      "half_float": { "type": "half_float" },     # 16-bit IEEE 754
      "scaled_float": {
        "type": "scaled_float",
        "scaling_factor": 100                     # Lưu 29.99 as 2999 (integer)
      }
    }
  }
}

Khi dùng loại nào?

TypeUse case
byte/short/integerSố nguyên nhỏ (age, quantity, score)
longSố nguyên lớn (timestamp millis, user_id lớn)
floatTỷ lệ, phần trăm, coordinates (chấp nhận floating point error)
doubleTính toán tài chính (cần precision cao)
scaled_floatTiền tệ (price * 100 lưu như integer) - tiết kiệm disk
bash
# Best practice cho tiền:
{
  "price": {
    "type": "scaled_float",
    "scaling_factor": 100
  }
}
# 29990000 VND lưu như 2999000000 integer (tránh floating point errors)

Date Types ​

bash
{
  "mappings": {
    "properties": {
      "created_at": {
        "type": "date",
        "format": "yyyy-MM-dd HH:mm:ss||yyyy-MM-dd||epoch_millis||strict_date_optional_time",
        "null_value": null,
        "ignore_malformed": false
      },
      "timestamp_ms": {
        "type": "date",
        "format": "epoch_millis"       # Unix timestamp in ms
      },
      "timestamp_sec": {
        "type": "date",
        "format": "epoch_second"       # Unix timestamp in seconds
      }
    }
  }
}

Date formats được hỗ trợ:

bash
"format": "strict_date_optional_time"  # ISO8601: 2024-01-15T10:30:00Z
"format": "yyyy-MM-dd"                 # 2024-01-15
"format": "dd/MM/yyyy"                 # 15/01/2024
"format": "epoch_millis"               # 1705312200000
"format": "epoch_second"              # 1705312200

# Multiple formats với ||
"format": "yyyy-MM-dd HH:mm:ss||yyyy-MM-dd||epoch_millis"

Date math trong queries:

bash
GET /products/_search
{
  "query": {
    "range": {
      "created_at": {
        "gte": "now-7d/d",   # Rounded to start of day, 7 days ago
        "lte": "now/d"       # Rounded to start of today
      }
    }
  }
}

# Date math operators:
# now-1h    = 1 giờ trước
# now+1d    = ngày mai
# now/d     = làm tròn xuống ngày
# now/M     = làm tròn xuống tháng
# 2024-01-15||-3d = 3 ngày trước 2024-01-15

Boolean Type ​

bash
{
  "in_stock": {
    "type": "boolean",
    "null_value": false    # Default khi field null
  }
}

# Accepted values:
# true: true, "true", "1"
# false: false, "false", "0"

Object Type ​

bash
{
  "address": {
    "type": "object",          # (default cho nested JSON)
    "dynamic": true,
    "enabled": true,           # false = lưu nhưng không index
    "properties": {
      "street": { "type": "text" },
      "city": { "type": "keyword" },
      "lat": { "type": "double" },
      "lng": { "type": "double" }
    }
  }
}

# Document:
{
  "address": {
    "street": "123 Đường Cầu Giấy",
    "city": "Hà Nội",
    "lat": 21.0278,
    "lng": 105.8342
  }
}

# Internally stored as flat:
# address.street, address.city, address.lat, address.lng

Vấn đề với Object arrays:

bash
# BÀI TOÁN:
{
  "users": [
    { "email": "[email protected]", "role": "admin" },
    { "email": "[email protected]", "role": "viewer" }
  ]
}

# ES FLATTENS to:
# users.email = ["[email protected]", "[email protected]"]
# users.role = ["admin", "viewer"]

# Query: "alice" with "viewer" role → MATCH (sai!) 
# vì mối quan hệ giữa email và role bị phá vỡ

# Giải pháp: nested type (xem bên dưới)

Nested Type ​

bash
{
  "reviews": {
    "type": "nested",
    "properties": {
      "user_id": { "type": "keyword" },
      "rating": { "type": "integer" },
      "comment": { "type": "text" },
      "created_at": { "type": "date" }
    }
  }
}

# Document:
{
  "product_id": "SP001",
  "reviews": [
    {
      "user_id": "U001",
      "rating": 5,
      "comment": "Sản phẩm tuyệt vời!",
      "created_at": "2024-01-05T10:00:00Z"
    },
    {
      "user_id": "U002",
      "rating": 3,
      "comment": "Bình thường, không xuất sắc.",
      "created_at": "2024-01-10T14:00:00Z"
    }
  ]
}

# Query với nested:
GET /products/_search
{
  "query": {
    "nested": {
      "path": "reviews",
      "query": {
        "bool": {
          "must": [
            { "term": { "reviews.user_id": "U001" } },
            { "range": { "reviews.rating": { "gte": 4 } } }
          ]
        }
      }
    }
  }
}

Lưu ý: Nested documents được lưu như separate hidden documents → tốn IO, query chậm hơn. Chỉ dùng khi cần duy trì relationship giữa các fields trong object.


Geo Types ​

geo_point - Điểm trên bản đồ ​
bash
{
  "location": {
    "type": "geo_point"
  }
}

# Nhiều cách biểu diễn:
{ "location": { "lat": 21.0278, "lon": 105.8342 } }
{ "location": [105.8342, 21.0278] }  # [lon, lat]!
{ "location": "21.0278,105.8342" }   # "lat,lon"
{ "location": "u4pruydqqvj" }        # Geohash
bash
# Distance query
GET /stores/_search
{
  "query": {
    "geo_distance": {
      "distance": "10km",
      "location": {
        "lat": 21.0278,
        "lon": 105.8342
      }
    }
  }
}
geo_shape - Shapes (polygon, line...) ​
bash
{
  "area": {
    "type": "geo_shape"
  }
}

# Ví dụ polygon
{
  "area": {
    "type": "polygon",
    "coordinates": [
      [[105.82, 21.02], [105.85, 21.02], [105.85, 21.05], [105.82, 21.05], [105.82, 21.02]]
    ]
  }
}

Specialized Types ​

ip - IP addresses ​
bash
{
  "client_ip": {
    "type": "ip"
  }
}

# Supports IPv4 and IPv6
{ "client_ip": "192.168.1.100" }
{ "client_ip": "2001:db8::1" }

# CIDR range query
GET /access-logs/_search
{
  "query": {
    "term": {
      "client_ip": "192.168.1.0/24"  # CIDR notation
    }
  }
}
range - Giá trị khoảng ​
bash
{
  "age_range": { "type": "integer_range" },
  "date_range": { "type": "date_range" },
  "price_range": { "type": "float_range" },
  "ip_range": { "type": "ip_range" }
}

# Document với range values
{
  "age_range": {
    "gte": 25,
    "lte": 35
  },
  "date_range": {
    "gte": "2024-01-01",
    "lte": "2024-01-31"
  }
}

# Tìm documents có range bao gồm giá trị
GET /promotions/_search
{
  "query": {
    "term": {
      "date_range": {
        "value": "2024-01-15"  # Ngày 15 có nằm trong range?
      }
    }
  }
}
completion - Autocomplete ​
bash
{
  "product_suggest": {
    "type": "completion",
    "analyzer": "simple",
    "search_analyzer": "simple",
    "preserve_separators": true,
    "preserve_position_increments": true,
    "max_input_length": 50
  }
}

# Document với suggest input
{
  "name": "iPhone 15 Pro Max",
  "product_suggest": {
    "input": ["iPhone", "iPhone 15", "iPhone 15 Pro", "iPhone 15 Pro Max", "Apple iPhone"],
    "weight": 10  # Độ ưu tiên trong suggestions
  }
}

# Suggest query
POST /products/_search
{
  "suggest": {
    "product_name_suggest": {
      "prefix": "iph",
      "completion": {
        "field": "product_suggest",
        "size": 5,
        "skip_duplicates": true
      }
    }
  }
}
join - Parent-Child Relationship ​
bash
{
  "mappings": {
    "properties": {
      "product_category": {
        "type": "join",
        "relations": {
          "category": "product"  # category là parent của product
        }
      }
    }
  }
}

# Index parent
PUT /catalog/_doc/cat-1
{
  "name": "Smartphones",
  "product_category": { "name": "category" }
}

# Index child (phải có routing = parent ID)
PUT /catalog/_doc/prod-1?routing=cat-1
{
  "name": "iPhone 15",
  "price": 34990000,
  "product_category": {
    "name": "product",
    "parent": "cat-1"
  }
}
dense_vector - Vector Search (AI/ML) ​
bash
{
  "description_vector": {
    "type": "dense_vector",
    "dims": 384,             # Số dimensions (phụ thuộc model)
    "index": true,           # Cần để kNN search
    "similarity": "cosine"   # cosine | dot_product | l2_norm
  }
}

# Index với vector embedding
PUT /articles/_doc/1
{
  "title": "Elasticsearch và AI",
  "description": "...",
  "description_vector": [0.1, -0.3, 0.7, ...]  # 384 float values
}

# kNN search (Semantic Search)
POST /articles/_search
{
  "knn": {
    "field": "description_vector",
    "query_vector": [0.15, -0.28, 0.65, ...],  # Embedding của query
    "k": 10,
    "num_candidates": 100
  }
}

5.4 Multi-Fields ​

Multi-fields cho phép index cùng một field theo nhiều cách - rất phổ biến trong thực tế:

bash
PUT /products
{
  "mappings": {
    "properties": {
      "name": {
        "type": "text",               # Full-text search
        "analyzer": "english",
        "fields": {
          "keyword": {
            "type": "keyword"          # Exact match, sort, agg
          },
          "suggest": {
            "type": "completion"       # Autocomplete
          },
          "ngram": {
            "type": "text",
            "analyzer": "ngram_analyzer"  # Partial match (n-grams)
          }
        }
      },
      "category": {
        "type": "keyword",
        "fields": {
          "text": {
            "type": "text"
          }
        }
      }
    }
  }
}

Cách query multi-fields:

bash
# Full-text search
{ "match": { "name": "iphone pro" } }

# Exact match
{ "term": { "name.keyword": "iPhone 15 Pro Max 256GB" } }

# Sort theo name alphabetically
{ "sort": [{ "name.keyword": "asc" }] }

# Aggregation theo name
{ "aggs": { "top_products": { "terms": { "field": "name.keyword" } } } }

# Suggest
{ "suggest": { "prefix": "iph", "completion": { "field": "name.suggest" } } }

5.5 Mapping Parameters Quan Trọng ​

index - Có lập chỉ mục không? ​

bash
{
  "image_urls": {
    "type": "keyword",
    "index": false     # Lưu trong _source nhưng không searchable
  }
}
# Dùng cho fields chỉ cần display, không cần tìm kiếm

doc_values - Cột-based storage ​

bash
{
  "price": {
    "type": "double",
    "doc_values": true   # Default: true (cần cho sort/agg)
    # false = không sort/agg được nhưng tiết kiệm disk
  }
}

Hiểu về doc_values:

Inverted Index (row storage):
"apple" → [doc1, doc5, doc9]
"samsung" → [doc2, doc3, doc7]

Doc Values (column storage):
doc1: price=34990000, brand=Apple
doc2: price=31990000, brand=Samsung
doc3: price=29990000, brand=Samsung
  • Inverted index tốt cho search (term → documents)
  • Doc values tốt cho sort/aggregation (documents → values)

store - Lưu riêng trong index ​

bash
{
  "title": {
    "type": "text",
    "store": true      # Lưu field riêng biệt ngoài _source
  }
}
# Ít dùng. Dùng khi _source rất lớn nhưng chỉ cần retrieve 1 field

enabled - Tắt indexing hoàn toàn ​

bash
{
  "raw_json": {
    "type": "object",
    "enabled": false   # Lưu nhưng không index, không search được
  }
}
# Dùng cho fields chỉ cần lưu (blob storage)

null_value - Giá trị mặc định khi null ​

bash
{
  "status": {
    "type": "keyword",
    "null_value": "unknown"
  }
}
# null sẽ được indexed như "unknown"
# Query: term { "status": "unknown" } sẽ tìm được null fields

copy_to - Sao chép giá trị sang field khác ​

bash
{
  "first_name": {
    "type": "text",
    "copy_to": "full_name"
  },
  "last_name": {
    "type": "text",
    "copy_to": "full_name"
  },
  "full_name": {
    "type": "text"
  }
}

# Document:
{ "first_name": "Nguyễn", "last_name": "Văn A" }

# full_name tự động = "Nguyễn Văn A"
# Search: match { "full_name": "Nguyễn A" } → tìm được
# full_name KHÔNG xuất hiện trong _source, chỉ được indexed

ignore_above - Bỏ qua string quá dài ​

bash
{
  "tag": {
    "type": "keyword",
    "ignore_above": 256   # Strings > 256 chars sẽ không indexed
  }
}
# Tránh index garbage data, giảm index size

ignore_malformed - Bỏ qua dữ liệu sai format ​

bash
{
  "price": {
    "type": "double",
    "ignore_malformed": true
  }
}
# "price": "not-a-number" → document vẫn được indexed, field bị ignored
# Thay vì throw error (behavior mặc định)

5.6 Index Templates và Component Templates ​

Component Templates - Tái sử dụng Mapping ​

bash
# Tạo component template cho common fields
PUT /_component_template/common-fields
{
  "template": {
    "mappings": {
      "properties": {
        "created_at": { "type": "date" },
        "updated_at": { "type": "date" },
        "created_by": { "type": "keyword" },
        "is_deleted": { "type": "boolean" }
      }
    }
  }
}

# Component template cho settings
PUT /_component_template/standard-settings
{
  "template": {
    "settings": {
      "number_of_replicas": 1,
      "refresh_interval": "1s",
      "index.codec": "best_compression"
    }
  }
}

# Index template kết hợp nhiều component templates
PUT /_index_template/products-template
{
  "index_patterns": ["products-*"],
  "composed_of": ["standard-settings", "common-fields"],
  "priority": 100,
  "template": {
    "mappings": {
      "properties": {
        "name": { "type": "text", "fields": { "keyword": { "type": "keyword" } } },
        "price": { "type": "scaled_float", "scaling_factor": 100 },
        "brand": { "type": "keyword" }
      }
    }
  }
}

# Bất kỳ index nào match "products-*" sẽ kế thừa settings này
PUT /products-2024-01
# Tự động có: common-fields + standard-settings + products-specific mappings

5.7 Runtime Fields ​

Runtime fields được tính toán tại thời điểm query, không cần reindex:

bash
PUT /products
{
  "mappings": {
    "runtime": {
      "discounted_price": {
        "type": "double",
        "script": {
          "source": """
            double price = doc['price'].value;
            double discount = doc['discount_percentage'].value;
            emit(price * (1 - discount / 100.0));
          """,
          "lang": "painless"
        }
      },
      "is_premium": {
        "type": "boolean",
        "script": {
          "source": "emit(doc['price'].value > 20000000)",
          "lang": "painless"
        }
      },
      "category_brand": {
        "type": "keyword",
        "script": {
          "source": "emit(doc['category'].value + '-' + doc['brand'].value)",
          "lang": "painless"
        }
      }
    },
    "properties": {
      "price": { "type": "double" },
      "discount_percentage": { "type": "integer" },
      "category": { "type": "keyword" },
      "brand": { "type": "keyword" }
    }
  }
}

# Query dùng runtime field
GET /products/_search
{
  "query": {
    "range": {
      "discounted_price": {
        "gte": 15000000,
        "lte": 25000000
      }
    }
  },
  "_source": ["name", "price", "discount_percentage"],
  "fields": ["discounted_price"]  # Include computed field in response
}

Runtime fields tạm thời (chỉ cho 1 query):

bash
GET /products/_search
{
  "runtime_mappings": {
    "final_price": {
      "type": "double",
      "script": {
        "source": """
          double base = doc['price'].value;
          if (doc['discount_percentage'].size() > 0) {
            base = base * (1 - doc['discount_percentage'].value / 100.0);
          }
          emit(base);
        """
      }
    }
  },
  "query": {
    "range": {
      "final_price": { "lte": 10000000 }
    }
  }
}

5.8 Mapping Best Practices ​

Mapping cho E-commerce Product Index ​

bash
PUT /ecommerce-products
{
  "settings": {
    "number_of_shards": 3,
    "number_of_replicas": 1,
    "analysis": {
      "analyzer": {
        "product_name_analyzer": {
          "type": "custom",
          "tokenizer": "standard",
          "filter": ["lowercase", "word_delimiter_graph", "stop"]
        },
        "sku_analyzer": {
          "type": "keyword",
          "tokenizer": "keyword"
        }
      }
    }
  },
  "mappings": {
    "dynamic": "strict",
    "properties": {
      
      "product_id": {
        "type": "keyword"
      },
      
      "sku": {
        "type": "keyword",
        "normalizer": "lowercase"
      },
      
      "name": {
        "type": "text",
        "analyzer": "product_name_analyzer",
        "fields": {
          "keyword": {
            "type": "keyword",
            "ignore_above": 512
          },
          "suggest": {
            "type": "completion"
          }
        }
      },
      
      "description": {
        "type": "text",
        "index_options": "offsets"
      },
      
      "short_description": {
        "type": "text",
        "copy_to": "combined_text"
      },
      
      "combined_text": {
        "type": "text",
        "store": false
      },
      
      "brand": {
        "type": "keyword"
      },
      
      "category": {
        "type": "keyword"
      },
      
      "category_path": {
        "type": "text",
        "analyzer": "path_hierarchy_analyzer"
      },
      
      "price": {
        "type": "scaled_float",
        "scaling_factor": 100
      },
      
      "original_price": {
        "type": "scaled_float",
        "scaling_factor": 100
      },
      
      "discount_pct": {
        "type": "byte"
      },
      
      "currency": {
        "type": "keyword"
      },
      
      "stock": {
        "properties": {
          "quantity": { "type": "integer" },
          "in_stock": { "type": "boolean" },
          "warehouse": { "type": "keyword" }
        }
      },
      
      "ratings": {
        "properties": {
          "average": { "type": "half_float" },
          "count": { "type": "integer" },
          "distribution": {
            "properties": {
              "1_star": { "type": "integer" },
              "2_star": { "type": "integer" },
              "3_star": { "type": "integer" },
              "4_star": { "type": "integer" },
              "5_star": { "type": "integer" }
            }
          }
        }
      },
      
      "attributes": {
        "type": "nested",
        "properties": {
          "name": { "type": "keyword" },
          "value": { "type": "keyword" }
        }
      },
      
      "tags": {
        "type": "keyword"
      },
      
      "images": {
        "type": "keyword",
        "index": false,
        "doc_values": false
      },
      
      "status": {
        "type": "keyword"
      },
      
      "visibility": {
        "type": "keyword"
      },
      
      "created_at": { "type": "date" },
      "updated_at": { "type": "date" },
      "published_at": { "type": "date" }
    }
  }
}

Mapping cho Log Index ​

bash
PUT /app-logs
{
  "settings": {
    "number_of_shards": 1,
    "number_of_replicas": 0,
    "index.lifecycle.name": "logs-policy"
  },
  "mappings": {
    "dynamic": "false",
    "properties": {
      "@timestamp": { "type": "date" },
      "level": { "type": "keyword" },
      "service": { "type": "keyword" },
      "message": { "type": "text", "norms": false },
      "trace_id": { "type": "keyword", "doc_values": false },
      "span_id": { "type": "keyword", "doc_values": false },
      "user_id": { "type": "keyword" },
      "http": {
        "properties": {
          "method": { "type": "keyword" },
          "path": { "type": "keyword" },
          "status_code": { "type": "short" },
          "duration_ms": { "type": "float" }
        }
      },
      "error": {
        "properties": {
          "type": { "type": "keyword" },
          "message": { "type": "text", "norms": false },
          "stack_trace": { "type": "text", "index": false }
        }
      },
      "host": { "type": "keyword" },
      "environment": { "type": "keyword" }
    }
  }
}

Giải thích tối ưu hóa cho logs:

  • "norms": false cho message: norms tốn RAM và không cần thiết (không cần relevance scoring cho logs)
  • "doc_values": false cho trace_id: không cần sort/agg trên field này
  • "dynamic": "false" : Ngăn ES tạo mapping cho fields không biết trước
  • "index": false cho stack_trace: stack trace rất dài, chỉ cần lưu không cần search

5.9 Cập Nhật Mapping ​

Một số thập thêm field vào mapping:

bash
# Thêm field mới vào mapping hiện có
PUT /products/_mapping
{
  "properties": {
    "weight_kg": { "type": "float" },
    "country_of_origin": { "type": "keyword" }
  }
}

Những gì KHÔNG thể thay đổi sau khi tạo:

  • Thay đổi type của field (text → keyword)
  • Thay đổi analyzer của field
  • Thay đổi index từ false sang true
  • Thay đổi số primary shards

Giải pháp: Reindex sang index mới với mapping mới

bash
# Bước 1: Tạo index mới với mapping đúng
PUT /products-v2
{ "mappings": { ... correct mappings ... } }

# Bước 2: Reindex data
POST /_reindex
{
  "source": { "index": "products" },
  "dest": { "index": "products-v2" }
}

# Bước 3: Switch alias
POST /_aliases
{
  "actions": [
    { "remove": { "index": "products", "alias": "products-current" } },
    { "add": { "index": "products-v2", "alias": "products-current" } }
  ]
}

Tóm Tắt Chương 5 ​

Data types quan trọng nhất:

  • text vs keyword: text cho search, keyword cho exact match/sort/agg
  • date: hỗ trợ nhiều formats, date math rất mạnh
  • nested: giữ relationship trong object arrays
  • scaled_float: tốt nhất cho tiền tệ
  • keyword với normalizer: case-insensitive exact match
  • dense_vector: cho AI/ML similarity search

Mapping best practices:

  1. Dùng "dynamic": "strict" cho production (tránh surprises)
  2. copy_to để tạo "search all fields" field
  3. Tắt doc_values và norms cho text fields không cần sort/relevance
  4. ignore_above cho keyword fields để tránh index garbage
  5. ignore_malformed: true cho data với uncertain quality
  6. Luôn dùng explicit mapping, không rely vào dynamic mapping cho production

Bước Tiếp Theo ​

→ Chương 6: Query DSL Cơ Bản - Master the art of writing Elasticsearch queries


Chương 6: Query DSL Cơ Bản ​

6.1 Query Context vs Filter Context ​

Đây là khái niệm cực kỳ quan trọng ảnh hưởng đến cả chức năng và performance.

Query Context - "How well does this match?" ​

Câu hỏi: Document relevant đến mức nào?

bash
GET /products/_search
{
  "query": {
    "match": {           # ← Đây là query context
      "name": "iPhone"  # Tính điểm relevance (_score)
    }
  }
}
  • ES tính _score cho mỗi document
  • Kết quả được sort theo score (mặc định)
  • Chậm hơn vì cần tính toán score
  • Dùng khi cần relevance ranking

Filter Context - "Does this match?" ​

Câu hỏi: Document có khớp không? (Yes/No)

bash
GET /products/_search
{
  "query": {
    "bool": {
      "filter": [        # ← Đây là filter context
        { "term": { "brand": "Apple" } },
        { "range": { "price": { "lte": 20000000 } } }
      ]
    }
  }
}
  • Không tính _score (luôn là 0 hoặc 1)
  • Kết quả được cache bởi ES
  • Nhanh hơn query context
  • Dùng cho: filter theo status, filter theo category, range filter...

Kết Hợp: "Best of Both Worlds" ​

bash
GET /products/_search
{
  "query": {
    "bool": {
      "must": [                         # Query context - tính _score
        {
          "multi_match": {
            "query": "điện thoại samsung",
            "fields": ["name^3", "description"]
          }
        }
      ],
      "filter": [                       # Filter context - không tính score, cached
        { "term": { "in_stock": true } },
        { "term": { "category": "smartphones" } },
        { "range": { "price": { "gte": 5000000, "lte": 30000000 } } }
      ]
    }
  }
}

Nguyên tắc: Đặt tất cả điều kiện "phải đúng" vào filter context khi không cần scoring.


match - Query Cơ Bản Nhất ​

bash
GET /products/_search
{
  "query": {
    "match": {
      "name": "iphone pro"
    }
  }
}

Bên trong xử lý:

  1. Analyze "iphone pro" → tokens: ["iphone", "pro"]
  2. Tìm documents có "iphone" OR "pro"
  3. Tính BM25 score
  4. Sort và return

Các tùy chọn của match:

bash
GET /products/_search
{
  "query": {
    "match": {
      "name": {
        "query": "iphone samsung galaxy",
        "operator": "and",          # Phải có ALL terms (mặc định: or)
        "minimum_should_match": "75%",  # Ít nhất 75% terms phải match
        "fuzziness": "AUTO",        # Typo tolerance
        "prefix_length": 2,         # Không fuzzy cho 2 ký tự đầu
        "max_expansions": 10,       # Max fuzzy expansions
        "zero_terms_query": "none",  # none|all khi query chỉ có stop words
        "lenient": false            # Có bỏ qua format errors không
      }
    }
  }
}

fuzziness giải thích:

Giá trịÝ nghĩa
0Không fuzzy (exact match)
11 ký tự sai (edit distance = 1)
22 ký tự sai
"AUTO"ES tự quyết: 0 cho 1-2 chars, 1 cho 3-5 chars, 2 cho 6+ chars
bash
# "samsumg" → tìm được "samsung" với fuzziness: 1
# "iphne" → tìm được "iphone" với fuzziness: 1
GET /products/_search
{
  "query": {
    "match": {
      "brand": {
        "query": "samsumg",
        "fuzziness": "AUTO"
      }
    }
  }
}

match_phrase - Tìm Cụm Từ Chính Xác ​

bash
# Tìm "pro max" liên tiếp theo đúng thứ tự
GET /products/_search
{
  "query": {
    "match_phrase": {
      "name": "pro max"
    }
  }
}
# Match: "iPhone 15 Pro Max" ✓
# Not match: "Max Pro iPhone 15" ✗ (sai thứ tự)
# Not match: "Pro chip Max battery" ✗ (không liên tiếp)

slop - Cho phép gap giữa các terms:

bash
GET /products/_search
{
  "query": {
    "match_phrase": {
      "name": {
        "query": "iPhone Max",
        "slop": 2    # Cho phép 2 words khác nằm giữa "iPhone" và "Max"
      }
    }
  }
}
# Match: "iPhone 15 Pro Max" ✓ (có 2 words ở giữa)
# Match: "iPhone Max 15" ✓ (slop = 1)

match_phrase_prefix - Autocomplete đơn giản ​

bash
# "iph" match "iphone", "iphone 15", "iphone case"...
GET /products/_search
{
  "query": {
    "match_phrase_prefix": {
      "name": {
        "query": "iphone pro",   # Prefix của word cuối
        "max_expansions": 20     # Giới hạn prefix expansions
      }
    }
  }
}
# "iphone pro" → match "iphone 16 pro", "iphone pro max", "iphone pro case"

multi_match - Tìm trên Nhiều Fields ​

bash
GET /products/_search
{
  "query": {
    "multi_match": {
      "query": "samsung flagship",
      "fields": [
        "name^3",            # Boost x3 nếu match trong name
        "description^1",     # Boost x1 (default)
        "brand^2",           # Boost x2
        "tags"
      ],
      "type": "best_fields"  # best_fields | most_fields | cross_fields | phrase | phrase_prefix
    }
  }
}

Các loại type của multi_match:

best_fields (default) ​
  • Lấy score của field match tốt nhất
  • Thêm tie_breaker cho các fields khác match ít hơn
bash
{
  "type": "best_fields",
  "tie_breaker": 0.3
  # score = best_field_score + 0.3 * other_matching_fields_score
}
most_fields ​
  • Cộng scores của tất cả fields match
  • Dùng khi cùng content được index nhiều cách (language analyzers)
bash
{
  "type": "most_fields"
}
cross_fields ​
  • Treat các fields như một field lớn
  • Terms có thể xuất hiện ở bất kỳ field nào
bash
{
  "type": "cross_fields",
  "operator": "and"
  # "John" phải match trong một trong các fields
  # "Smith" phải match trong một trong các fields
}

Use case: Tìm người theo tên:

bash
GET /users/_search
{
  "query": {
    "multi_match": {
      "query": "Nguyễn Văn A",
      "fields": ["first_name", "last_name"],
      "type": "cross_fields",
      "operator": "and"
    }
  }
}

6.3 Term-Level Queries - Exact Match ​

Term-level queries không analyze input - so sánh trực tiếp với indexed values.

term - Exact Match Một Giá Trị ​

bash
GET /products/_search
{
  "query": {
    "term": {
      "brand": {
        "value": "Apple",
        "boost": 1.0         # Boost factor
      }
    }
  }
}

# Short form:
{
  "query": {
    "term": { "brand": "Apple" }
  }
}

Cẩn thận với text fields:

bash
# SAI - text fields được analyze (lowercase), "Apple" không tồn tại
{
  "term": { "name": "Apple" }   # ← SAI
}

# ĐÚNG - dùng .keyword sub-field cho exact match
{
  "term": { "name.keyword": "Apple iPhone 15" }  # ← ĐÚNG
}

# HOẶC dùng match cho text fields
{
  "match": { "name": "iPhone 15" }  # ← ĐÚNG (analyzed)
}

terms - Match Một trong Nhiều Giá Trị ​

bash
GET /products/_search
{
  "query": {
    "terms": {
      "brand": ["Apple", "Samsung", "Xiaomi"]
    }
  }
}
# Tương đương SQL: WHERE brand IN ('Apple', 'Samsung', 'Xiaomi')

Terms Lookup - Lấy terms từ document khác:

bash
# Lấy danh sách followed brands từ user profile
GET /products/_search
{
  "query": {
    "terms": {
      "brand": {
        "index": "users",         # Lấy từ index này
        "id": "user_123",         # Document ID
        "path": "followed_brands" # Field chứa values
      }
    }
  }
}

range - Query Theo Khoảng Giá Trị ​

bash
# Số
GET /products/_search
{
  "query": {
    "range": {
      "price": {
        "gte": 5000000,   # greater than or equal
        "lte": 20000000,  # less than or equal
        "gt": 4000000,    # strictly greater than
        "lt": 20000001    # strictly less than
      }
    }
  }
}

# Ngày tháng
GET /orders/_search
{
  "query": {
    "range": {
      "created_at": {
        "gte": "2024-01-01",
        "lte": "2024-01-31",
        "format": "yyyy-MM-dd",
        "time_zone": "+07:00"
      }
    }
  }
}

# Relative dates
GET /orders/_search
{
  "query": {
    "range": {
      "created_at": {
        "gte": "now-30d/d",  # 30 ngày trước, làm tròn theo ngày
        "lte": "now/d"       # Hiện tại, làm tròn theo ngày
      }
    }
  }
}

exists - Document có Field không? ​

bash
# Tìm documents có field description
GET /products/_search
{
  "query": {
    "exists": { "field": "description" }
  }
}

# Tìm documents KHÔNG có field (must_not + exists)
GET /products/_search
{
  "query": {
    "bool": {
      "must_not": [
        { "exists": { "field": "description" } }
      ]
    }
  }
}

Lưu ý về "null" vs "not exists":

  • null → field KHÔNG tồn tại (không được index bởi exists)
  • "null_value" trong mapping → field có giá trị đặc biệt → CÓ tồn tại với exists

prefix - Tìm Theo Tiền Tố ​

bash
# Tìm brands bắt đầu bằng "Sam"
GET /products/_search
{
  "query": {
    "prefix": {
      "brand": {
        "value": "Sam",
        "rewrite": "constant_score"
      }
    }
  }
}
# Match: "Samsung", "Samsonite", "Samlex"...

wildcard - Tìm Theo Pattern ​

bash
GET /products/_search
{
  "query": {
    "wildcard": {
      "product_id": {
        "value": "SP0*",          # * = any characters
        "case_insensitive": true  # ES 7.10+
      }
    }
  }
}

# ? = single character
{
  "wildcard": {
    "product_id": "SP0??"  # Khớp với SP001, SP099 (6 chars total)
  }
}

Cảnh báo performance: Wildcard với * ở đầu (*pattern) rất chậm vì không dùng được index.


regexp - Regular Expression ​

bash
GET /products/_search
{
  "query": {
    "regexp": {
      "product_id": {
        "value": "SP[0-9]{3}",
        "flags": "ALL",
        "case_insensitive": true,
        "max_determinized_states": 10000  # Giới hạn complexity
      }
    }
  }
}

Cảnh báo: Regexp queries rất chậm, tránh dùng trong production searches.


fuzzy - Tìm Kiếm Mờ ​

bash
GET /products/_search
{
  "query": {
    "fuzzy": {
      "brand": {
        "value": "Samsumg",
        "fuzziness": 1,
        "prefix_length": 3,    # Prefix "Sam" phải match chính xác
        "max_expansions": 50,
        "transpositions": true  # "ab" → "ba" (count as 1 edit)
      }
    }
  }
}

ids - Tìm Theo Document IDs ​

bash
GET /products/_search
{
  "query": {
    "ids": {
      "values": ["1", "2", "3", "4"]
    }
  }
}

6.4 Compound Queries - Kết Hợp Queries ​

bool Query - Linh Hoạt Nhất ​

Bool query kết hợp nhiều queries với logic Boolean:

bash
GET /products/_search
{
  "query": {
    "bool": {
      "must": [...],         # ANĐ - phải match, ảnh hưởng score
      "should": [...],       # HOẶC - nên match, tăng score nếu match
      "must_not": [...],     # KHÔNG - không được match
      "filter": [...]        # ANĐ - phải match, KHÔNG ảnh hưởng score (cached)
    }
  }
}

Ví dụ thực tế - Tìm kiếm sản phẩm:

bash
GET /products/_search
{
  "query": {
    "bool": {
      "must": [
        {
          "multi_match": {
            "query": "điện thoại 5G",
            "fields": ["name^3", "description"]
          }
        }
      ],
      "should": [
        { "term": { "tags": "flagship" } },
        { "range": { "rating": { "gte": 4.5 } } }
      ],
      "must_not": [
        { "term": { "status": "discontinued" } }
      ],
      "filter": [
        { "term": { "in_stock": true } },
        { "term": { "category": "smartphones" } },
        {
          "range": {
            "price": {
              "gte": 5000000,
              "lte": 30000000
            }
          }
        }
      ],
      "minimum_should_match": 1
    }
  }
}

minimum_should_match:

bash
"bool": {
  "should": [
    { "term": { "tags": "flagship" } },
    { "term": { "tags": "5G" } },
    { "term": { "tags": "camera" } }
  ],
  "minimum_should_match": 2  # Ít nhất 2 trong 3 should phải match
}

Nested bool queries:

bash
GET /products/_search
{
  "query": {
    "bool": {
      "must": [
        { "match": { "name": "smartphone" } }
      ],
      "filter": [
        {
          "bool": {
            "should": [
              { "term": { "brand": "Apple" } },
              { "term": { "brand": "Samsung" } }
            ],
            "minimum_should_match": 1
          }
        },
        { "term": { "in_stock": true } }
      ]
    }
  }
}

boosting Query ​

Tìm documents match positive query, nhưng giảm score của documents match negative query:

bash
GET /products/_search
{
  "query": {
    "boosting": {
      "positive": {
        "match": { "name": "smartphone" }  # Tìm smartphones
      },
      "negative": {
        "term": { "condition": "refurbished" }  # Giảm score hàng tân trang
      },
      "negative_boost": 0.3   # Score của refurbished = original * 0.3
    }
  }
}

dis_max - Disjunction Max ​

Lấy max score từ các sub-queries (thay vì cộng như bool should):

bash
GET /products/_search
{
  "query": {
    "dis_max": {
      "queries": [
        { "match": { "name": "iPhone" } },
        { "match": { "description": "iPhone" } }
      ],
      "tie_breaker": 0.3   # 0 = strict max, 1 = average, 0.3 = weighted
    }
  }
}

Khi dùng dis_max vs multi_match best_fields?

  • Thực ra multi_match với type: best_fields là wrapper của dis_max. Hầu hết dùng multi_match.

constant_score - Filter với Fixed Score ​

bash
GET /products/_search
{
  "query": {
    "constant_score": {
      "filter": {
        "term": { "brand": "Apple" }
      },
      "boost": 1.5    # Tất cả matching docs có score = 1.5
    }
  }
}

6.5 Special Queries ​

match_all và match_none ​

bash
# Trả về tất cả documents
GET /products/_search
{
  "query": { "match_all": {} }
}

# Không trả về document nào (dùng trong dynamic queries)
GET /products/_search
{
  "query": { "match_none": {} }
}

query_string - Apache Lucene Query Syntax ​

bash
GET /products/_search
{
  "query": {
    "query_string": {
      "query": "brand:Apple AND price:[20000000 TO 50000000] AND -category:accessories",
      "default_field": "name",
      "allow_leading_wildcard": false
    }
  }
}

# Query string syntax:
# AND, OR, NOT, +, -
# field:value
# range: price:[10 TO 50], date:[2024-01-01 TO 2024-12-31]
# wildcard: name:ipho*
# phrase: name:"iphone pro"
# boost: name:iphone^3

simple_query_string - Giống query_string nhưng không throw errors (safer):

bash
GET /products/_search
{
  "query": {
    "simple_query_string": {
      "query": "iphone +apple -case",
      "fields": ["name^3", "description"],
      "default_operator": "and"
    }
  }
}

# Syntax:
# + = must match (AND)
# | = should match (OR)
# - = must not match
# " " = phrase
# * = prefix
# ( ) = grouping
# ~ = fuzzy

6.6 Highlighting - Tô Sáng Kết Quả ​

bash
GET /products/_search
{
  "query": {
    "match": { "name": "iPhone pro" }
  },
  "highlight": {
    "fields": {
      "name": {
        "pre_tags": ["<strong>"],
        "post_tags": ["</strong>"],
        "number_of_fragments": 0,      # 0 = return full field (no fragmentation)
        "fragment_size": 150           # Kích thước mỗi fragment
      },
      "description": {
        "pre_tags": ["<em style='color:red'>"],
        "post_tags": ["</em>"],
        "number_of_fragments": 3,      # Số fragments trả về
        "fragment_size": 200
      }
    },
    "require_field_match": false,      # false = highlight even if not in query
    "type": "unified"                  # unified | plain | fvh
  }
}

Response với highlighting:

json
{
  "hits": {
    "hits": [
      {
        "_source": {
          "name": "iPhone 15 Pro Max 256GB",
          "description": "iPhone 15 Pro Max với chip A17 Pro..."
        },
        "highlight": {
          "name": ["<strong>iPhone</strong> 15 <strong>Pro</strong> Max 256GB"],
          "description": [
            "...với chip A17 <strong>Pro</strong>...",
            "...<strong>iPhone</strong> 15 Pro Max..."
          ]
        }
      }
    ]
  }
}

Highlight types:

  • unified (default): Tốt nhất, sử dụng BM25 để chọn tốt fragments
  • plain: Chậm, cần re-analyze text; dùng khi unified không work tốt
  • fvh (Fast Vector Highlighter): Nhanh nhất, cần term_vector: with_positions_offsets trong mapping

6.7 Pagination ​

From/Size (Standard Pagination) ​

bash
GET /products/_search
{
  "from": 0,      # Offset (trang 1 = 0, trang 2 = 10, ...)
  "size": 10,     # Kích thước trang
  "query": { "match_all": {} },
  "sort": [{ "price": "asc" }]
}

Giới hạn: from + size <= 10000 (mặc định). Để override:

bash
PUT /products/_settings
{
  "index.max_result_window": 50000
}

Nhưng không nên override vì deep pagination rất tốn RAM:

  • From 9990, size 10 → ES phải fetch 10000 docs từ mỗi shard, sort, lấy 10 cuối

Search After (Efficient Deep Pagination) ​

bash
# Trang đầu tiên
GET /products/_search
{
  "size": 10,
  "query": { "match_all": {} },
  "sort": [
    { "price": "asc" },
    { "_id": "asc" }    # Tiebreaker (phải unique)
  ]
}

# Response last hit:
# "_source": { "price": 5990000 }
# "_id": "prod-789"

# Trang tiếp theo - dùng sort values của last document
GET /products/_search
{
  "size": 10,
  "query": { "match_all": {} },
  "sort": [
    { "price": "asc" },
    { "_id": "asc" }
  ],
  "search_after": [5990000, "prod-789"]  # Giá trị từ last document
}

Ưu điểm của search_after:

  • Không có giới hạn 10000
  • Constant memory usage bất kể page nào
  • Tuy nhiên: không thể jump đến random page, chỉ có thể next/prev

Point in Time (PIT) - Consistent Pagination ​

Khi data thay đổi liên tục, search_after có thể trả về kết quả không nhất quán. PIT tạo "snapshot" của index state:

bash
# Tạo PIT
POST /products/_pit?keep_alive=5m

# Response:
{ "id": "46ToAwMDaWR5BXV1..." }

# Search với PIT
GET /_search
{
  "size": 10,
  "query": { "match_all": {} },
  "sort": [
    { "price": "asc" },
    { "_shard_doc": "asc" }  # Implicit tiebreaker với PIT
  ],
  "pit": {
    "id": "46ToAwMDaWR5BXV1...",
    "keep_alive": "5m"
  }
}

# Next page
GET /_search
{
  "size": 10,
  "sort": [...],
  "search_after": [5990000, 12345],
  "pit": {
    "id": "46ToAwMDaWR5BXV1...",
    "keep_alive": "5m"
  }
}

# Xóa PIT khi xong
DELETE /_pit
{
  "id": "46ToAwMDaWR5BXV1..."
}

6.8 Sorting ​

Sort cơ bản ​

bash
GET /products/_search
{
  "query": { "match_all": {} },
  "sort": [
    { "price": "asc" },       # Giá từ thấp đến cao
    { "rating": "desc" },     # Rating từ cao xuống
    "_score"                  # Sau đó theo relevance
  ]
}

Sort với missing values ​

bash
GET /products/_search
{
  "sort": [
    {
      "discount_percentage": {
        "order": "desc",
        "missing": "_last"   # _first | _last | custom_value
      }
    }
  ]
}

Sort theo field trong nested object ​

bash
GET /hotels/_search
{
  "sort": [
    {
      "rooms.price": {
        "order": "asc",
        "nested": {
          "path": "rooms",
          "filter": {
            "term": { "rooms.type": "double" }
          }
        },
        "mode": "min"   # min | max | sum | avg | median
      }
    }
  ]
}

6.9 Source Filtering ​

bash
GET /products/_search
{
  "query": { "match_all": {} },
  
  # Cách 1: Chỉ lấy một số fields
  "_source": ["name", "price", "brand"],
  
  # Cách 2: Loại trừ fields
  "_source": {
    "excludes": ["description", "images"]
  },
  
  # Cách 3: Include và exclude
  "_source": {
    "includes": ["name", "specs.*"],
    "excludes": ["specs.internal"]
  }
}

fields vs _source ​

Từ ES 7.10+, có thể dùng fields để lấy thêm thông tin:

bash
GET /products/_search
{
  "_source": false,
  "fields": [
    "name",
    "price",
    { "field": "created_at", "format": "dd/MM/yyyy" }  # Custom format
  ]
}

6.10 Script Score - Tùy Chỉnh Scoring ​

bash
GET /products/_search
{
  "query": {
    "script_score": {
      "query": {
        "bool": {
          "filter": [
            { "term": { "in_stock": true } }
          ]
        }
      },
      "script": {
        "source": """
          // Custom score: kết hợp nhiều signals
          double base = _score;
          double rating = doc['rating'].value;
          double reviewBoost = Math.log(1 + doc['review_count'].value);
          double freshness = 1.0;
          
          // Boost newer products
          long created = doc['created_at'].value.toEpochMilli();
          long now = System.currentTimeMillis();
          long daysOld = (now - created) / (1000L * 60 * 60 * 24);
          if (daysOld < 30) freshness = 1.5;
          else if (daysOld < 90) freshness = 1.2;
          
          return base * rating * reviewBoost * freshness;
        """,
        "lang": "painless"
      }
    }
  }
}

6.11 Thực hành: Xây Dựng Search API ​

Use Case: E-commerce Product Search API ​

bash
# Query hoàn chỉnh cho trang tìm kiếm sản phẩm
GET /products/_search
{
  "from": 0,
  "size": 20,
  
  "query": {
    "bool": {
      "must": [
        {
          "multi_match": {
            "query": "điện thoại samsung 5G",
            "fields": ["name^5", "description^1", "brand^3", "tags^2"],
            "type": "best_fields",
            "fuzziness": "AUTO",
            "minimum_should_match": "60%"
          }
        }
      ],
      "filter": [
        { "term": { "in_stock": true } },
        { "term": { "status": "active" } },
        {
          "range": {
            "price": { "gte": 5000000, "lte": 35000000 }
          }
        }
      ],
      "should": [
        { "term": { "tags": "5G" } },
        { "term": { "tags": "flagship" } },
        {
          "range": { "rating": { "gte": 4.5 } }
        }
      ],
      "minimum_should_match": 0
    }
  },
  
  "sort": [
    { "_score": "desc" },
    { "rating": "desc" },
    { "review_count": "desc" }
  ],
  
  "highlight": {
    "fields": {
      "name": { "pre_tags": ["<mark>"], "post_tags": ["</mark>"] },
      "description": {
        "pre_tags": ["<mark>"],
        "post_tags": ["</mark>"],
        "number_of_fragments": 2,
        "fragment_size": 150
      }
    }
  },
  
  "aggs": {
    "brands": {
      "terms": { "field": "brand", "size": 20 }
    },
    "price_stats": {
      "stats": { "field": "price" }
    },
    "price_ranges": {
      "range": {
        "field": "price",
        "ranges": [
          { "key": "Dưới 5 triệu", "to": 5000000 },
          { "key": "5-10 triệu", "from": 5000000, "to": 10000000 },
          { "key": "10-20 triệu", "from": 10000000, "to": 20000000 },
          { "key": "20-30 triệu", "from": 20000000, "to": 30000000 },
          { "key": "Trên 30 triệu", "from": 30000000 }
        ]
      }
    },
    "avg_rating": {
      "avg": { "field": "rating" }
    }
  },
  
  "_source": ["product_id", "name", "brand", "price", "original_price", 
              "discount_percentage", "rating", "review_count", "in_stock",
              "tags", "images"]
}

Tóm Tắt Chương 6 ​

Chọn Query Đúng ​

Tình huốngQuery nên dùng
Full-text searchmatch, multi_match
Exact matchterm, terms
Khoảng giá trịrange
Kiểm tra field tồn tạiexists
Kết hợp nhiều conditionsbool
Typo tolerancematch với fuzziness
Phrase searchmatch_phrase
Tìm theo patternwildcard (cẩn thận performance)
Lấy tất cảmatch_all

Filter vs Query ​

FilterQuery
ScoreKhông tínhCó tính
CacheCó (nhanh hơn)Không
Dùng khiCondition phải đúngCần ranking
Ví dụstatus=active, price rangefull-text search

Bước Tiếp Theo ​

→ Chương 7: Query DSL Nâng Cao - Function score, nested queries, percolator và các kỹ thuật advanced khác


Chương 7: Query DSL Nâng Cao ​

7.1 Function Score Query - Tùy Chỉnh Relevance Scoring ​

Function Score Query cho phép bạn tùy chỉnh hoàn toàn cách tính điểm relevance. Đây là công cụ mạnh nhất để kiểm soát thứ tự hiển thị kết quả.

Cấu Trúc Cơ Bản ​

bash
GET /products/_search
{
  "query": {
    "function_score": {
      "query": {
        "match": { "name": "smartphone" }   # Query cơ bản
      },
      "functions": [
        {
          # Function 1
        },
        {
          # Function 2
        }
      ],
      "score_mode": "multiply",   # Cách kết hợp scores từ functions
      "boost_mode": "multiply",   # Cách kết hợp với query score
      "min_score": 0.5,           # Loại bỏ docs có score < 0.5
      "boost": 1.0
    }
  }
}

score_mode - Cách combine scores từ nhiều functions:

  • multiply (default): Nhân tất cả function scores
  • sum: Cộng tất cả
  • avg: Trung bình
  • first: Score của function đầu tiên match
  • max: Giá trị lớn nhất
  • min: Giá trị nhỏ nhất

boost_mode - Cách combine function score kết quả với query score:

  • multiply (default): query_score * function_score
  • replace: Thay query_score bằng function_score
  • sum: Cộng lại
  • avg: Trung bình
  • max/min: Lấy max/min

Function 1: weight - Boost Đơn Giản ​

bash
GET /products/_search
{
  "query": {
    "function_score": {
      "query": { "match": { "name": "phone" } },
      "functions": [
        {
          "filter": { "term": { "brand": "Apple" } },
          "weight": 2.0         # Apple products nhận score * 2
        },
        {
          "filter": { "term": { "tags": "sale" } },
          "weight": 1.5         # Sale products boost 1.5x
        },
        {
          "filter": {
            "range": { "rating": { "gte": 4.5 } }
          },
          "weight": 1.3         # High-rated products boost 1.3x
        }
      ],
      "score_mode": "multiply"
    }
  }
}

Function 2: field_value_factor - Boost theo Giá Trị Field ​

bash
GET /products/_search
{
  "query": {
    "function_score": {
      "query": { "match": { "description": "smartphone" } },
      "field_value_factor": {
        "field": "popularity_score",    # Field chứa giá trị
        "factor": 1.2,                  # Nhân với factor
        "modifier": "log1p",            # Áp dụng hàm toán học
        "missing": 1                    # Giá trị khi field null
      },
      "boost_mode": "multiply"
    }
  }
}

modifier options:

ModifierFormula
nonefield_value * factor
loglog(field_value * factor)
log1plog(1 + field_value * factor)
log2plog(2 + field_value * factor)
lnln(field_value * factor)
ln1pln(1 + field_value * factor)
square(field_value * factor)^2
sqrtsqrt(field_value * factor)
reciprocal1 / (field_value * factor)

Thực tế: Boost theo số lượt review và rating:

bash
GET /products/_search
{
  "query": {
    "function_score": {
      "query": {
        "match": { "name": "laptop" }
      },
      "functions": [
        {
          "field_value_factor": {
            "field": "review_count",
            "modifier": "log1p",
            "factor": 0.1,
            "missing": 0
          }
        },
        {
          "field_value_factor": {
            "field": "rating",
            "modifier": "square",
            "factor": 0.5,
            "missing": 3.0
          }
        }
      ],
      "score_mode": "sum",
      "boost_mode": "multiply"
    }
  }
}

Function 3: gauss/linear/exp - Decay Functions ​

Decay functions giúp giảm score theo khoảng cách từ một origin point. Rất hữu ích cho:

  • Boost sản phẩm theo thời gian (newer = higher score)
  • Boost địa điểm gần người dùng
  • Boost sản phẩm trong khoảng giá mong muốn
Gauss Decay - Giảm dần như hình chuông (smooth) ​
bash
GET /products/_search
{
  "query": {
    "function_score": {
      "query": { "match_all": {} },
      "gauss": {
        "created_at": {
          "origin": "now",       # Từ điểm này
          "scale": "30d",        # Sau 30 ngày thì score = 0.5
          "offset": "7d",        # 7 ngày đầu không bị giảm (score = 1)
          "decay": 0.5           # Score tại scale = 0.5
        }
      }
    }
  }
}
Linear Decay - Giảm tuyến tính ​
bash
GET /stores/_search
{
  "query": {
    "function_score": {
      "query": { "match_all": {} },
      "linear": {
        "location": {
          "origin": "21.0278,105.8342",  # User's location
          "scale": "10km",               # score = 0.5 tại 10km
          "offset": "1km",               # Trong 1km score = 1
          "decay": 0.5
        }
      }
    }
  }
}
Exp Decay - Giảm theo hàm mũ (steep drop-off) ​
bash
GET /flights/_search
{
  "query": {
    "function_score": {
      "query": {
        "match": { "destination": "Hanoi" }
      },
      "exp": {
        "price": {
          "origin": 3000000,    # Target price
          "scale": 500000,      # 500k VND offset
          "decay": 0.5
        }
      }
    }
  }
}

Function 4: random_score - Kết Quả Ngẫu Nhiên Nhất Quán ​

bash
GET /recommendations/_search
{
  "query": {
    "function_score": {
      "query": {
        "bool": {
          "filter": { "term": { "category": "electronics" } }
        }
      },
      "random_score": {
        "seed": 12345,           # Cùng seed = cùng kết quả
        "field": "_seq_no"       # Field để randomize
      }
    }
  }
}

Use case: Randomize sản phẩm, nhưng nhất quán trong cùng 1 session (dùng session_id làm seed).


Function 5: script_score ​

bash
GET /products/_search
{
  "query": {
    "function_score": {
      "query": { "match": { "name": "phone" } },
      "script_score": {
        "script": {
          "source": """
            double score = _score;
            
            // Boost trending products
            if (doc['is_trending'].value) {
              score *= 1.5;
            }
            
            // Boost high-margin products
            double margin = (doc['price'].value - doc['cost'].value) / doc['price'].value;
            score *= (1 + margin * 0.5);
            
            // Freshness boost
            long ageInDays = (System.currentTimeMillis() - doc['created_at'].value.toEpochMilli()) 
                              / (1000L * 60 * 60 * 24);
            double freshnessBoost = Math.max(0.5, 1.0 - ageInDays / 365.0);
            score *= freshnessBoost;
            
            return score;
          """
        }
      }
    }
  }
}

Ví Dụ Thực Tế: E-commerce Search Ranking ​

bash
GET /products/_search
{
  "query": {
    "function_score": {
      "query": {
        "bool": {
          "must": [
            {
              "multi_match": {
                "query": "samsung điện thoại",
                "fields": ["name^3", "description"]
              }
            }
          ],
          "filter": [
            { "term": { "in_stock": true } },
            { "term": { "status": "active" } }
          ]
        }
      },
      "functions": [
        {
          "filter": { "term": { "tags": "sponsored" } },
          "weight": 3.0
        },
        {
          "field_value_factor": {
            "field": "rating",
            "modifier": "square",
            "factor": 0.4,
            "missing": 3.0
          }
        },
        {
          "field_value_factor": {
            "field": "review_count",
            "modifier": "log1p",
            "factor": 0.15,
            "missing": 0
          }
        },
        {
          "gauss": {
            "created_at": {
              "origin": "now",
              "scale": "60d",
              "offset": "14d",
              "decay": 0.5
            }
          }
        },
        {
          "filter": {
            "range": {
              "discount_percentage": { "gte": 10 }
            }
          },
          "weight": 1.2
        }
      ],
      "score_mode": "sum",
      "boost_mode": "multiply"
    }
  }
}

7.2 Nested Queries ​

Khi nào dùng Nested? ​

Khi document có array of objects và cần query theo combination của fields trong object:

bash
# Data:
{
  "product": "Laptop Dell",
  "variants": [
    { "color": "black", "storage": "512GB", "price": 25000000 },
    { "color": "silver", "storage": "1TB", "price": 30000000 }
  ]
}

# Vấn đề với plain object:
# Tìm "black AND 1TB" sẽ incorrectly match vì ES flatten array
# "black" ở variant 1, "1TB" ở variant 2 → sai!

Nested Query Cơ Bản ​

bash
# Mapping:
PUT /products-with-variants
{
  "mappings": {
    "properties": {
      "name": { "type": "text" },
      "variants": {
        "type": "nested",
        "properties": {
          "color": { "type": "keyword" },
          "storage": { "type": "keyword" },
          "price": { "type": "double" }
        }
      }
    }
  }
}

# Query: Tìm sản phẩm có variant màu black VÀ storage 1TB
GET /products-with-variants/_search
{
  "query": {
    "nested": {
      "path": "variants",
      "query": {
        "bool": {
          "must": [
            { "term": { "variants.color": "black" } },
            { "term": { "variants.storage": "1TB" } }
          ]
        }
      },
      "score_mode": "max",     # Điểm cao nhất từ matching nested docs
      "inner_hits": {
        "name": "matching_variants",   # Trả về nested docs đã match
        "size": 3,
        "highlight": {
          "fields": { "variants.color": {} }
        }
      }
    }
  }
}

Nested Aggregations ​

bash
GET /products-with-variants/_search
{
  "aggs": {
    "variants_agg": {
      "nested": {
        "path": "variants"
      },
      "aggs": {
        "by_color": {
          "terms": { "field": "variants.color" }
        },
        "price_stats": {
          "stats": { "field": "variants.price" }
        },
        "by_storage": {
          "terms": { "field": "variants.storage" },
          "aggs": {
            "avg_price_per_storage": {
              "avg": { "field": "variants.price" }
            }
          }
        }
      }
    }
  }
}

Reverse Nested Aggregation ​

bash
GET /products-with-variants/_search
{
  "aggs": {
    "colors": {
      "nested": { "path": "variants" },
      "aggs": {
        "color_terms": {
          "terms": { "field": "variants.color" },
          "aggs": {
            "products_count": {
              "reverse_nested": {}   # Trở về parent document
            }
          }
        }
      }
    }
  }
}

7.3 Parent-Child Relationships ​

Parent-child cho phép define relationship giữa các documents trong cùng index mà không cần denormalize.

Khi nào dùng Parent-Child vs Nested? ​

NestedParent-Child
Lưu trữEmbedded trong parentSeparate documents
UpdateReindex entire parentĐộc lập
Query performanceNhanh hơnChậm hơn (join)
Khi dùngÍt thay đổi, không quá nhiềuNhiều updates, số lượng lớn

Thiết Lập Parent-Child ​

bash
PUT /company
{
  "mappings": {
    "properties": {
      "my_join_field": {
        "type": "join",
        "relations": {
          "department": "employee",  # department là parent của employee
          "employee": "task"         # employee là parent của task (multi-level)
        }
      },
      "name": { "type": "keyword" },
      "description": { "type": "text" }
    }
  }
}

# Index parent (department)
PUT /company/_doc/dept-1
{
  "name": "Engineering",
  "description": "Software engineering department",
  "my_join_field": {
    "name": "department"
  }
}

# Index child (employee) - phải có routing = parent_id
PUT /company/_doc/emp-1?routing=dept-1
{
  "name": "Nguyễn Văn A",
  "email": "[email protected]",
  "salary": 30000000,
  "my_join_field": {
    "name": "employee",
    "parent": "dept-1"
  }
}

PUT /company/_doc/emp-2?routing=dept-1
{
  "name": "Trần Thị B",
  "email": "[email protected]",
  "salary": 35000000,
  "my_join_field": {
    "name": "employee",
    "parent": "dept-1"
  }
}

# Index grandchild (task) - routing = employee (closest parent) 
# But cần specify routing của shard (= original parent dept-1)
PUT /company/_doc/task-1?routing=dept-1
{
  "title": "Build search UI",
  "status": "in_progress",
  "my_join_field": {
    "name": "task",
    "parent": "emp-1"
  }
}

Parent-Child Queries ​

has_child - Tìm parents có child matching ​
bash
# Tìm departments có employee với salary > 30M
GET /company/_search
{
  "query": {
    "has_child": {
      "type": "employee",
      "query": {
        "range": { "salary": { "gt": 30000000 } }
      },
      "min_children": 1,      # Ít nhất 1 matching child
      "max_children": 10,     # Tối đa 10 matching children
      "score_mode": "max",    # none | avg | sum | max | min
      "inner_hits": {
        "size": 5
      }
    }
  }
}
has_parent - Tìm children có parent matching ​
bash
# Tìm employees thuộc department "Engineering"
GET /company/_search
{
  "query": {
    "has_parent": {
      "parent_type": "department",
      "query": {
        "term": { "name": "Engineering" }
      },
      "score": true   # Kế thừa score từ parent
    }
  }
}
parent_id - Tìm children theo parent ID cụ thể ​
bash
GET /company/_search
{
  "query": {
    "parent_id": {
      "type": "employee",
      "id": "dept-1"     # Lấy tất cả employees của dept-1
    }
  }
}

7.4 Percolate Query ​

Percolator là một unique feature: thay vì "tìm documents match query", bạn "tìm queries match document".

Use cases:

  • Notifications: Khi có sản phẩm mới match filter của user
  • Alerting: Khi log event match một alert rule
  • Content classification: Tự động tag content

Thiết Lập Percolator ​

bash
# Tạo index chứa các queries (percolator index)
PUT /product-alerts
{
  "mappings": {
    "properties": {
      "query": {
        "type": "percolator"    # Field lưu query
      },
      # Mirror mapping của index cần match
      "name": { "type": "text" },
      "brand": { "type": "keyword" },
      "price": { "type": "double" },
      "category": { "type": "keyword" },
      "tags": { "type": "keyword" }
    }
  }
}

# Đăng ký user alerts như stored queries
PUT /product-alerts/_doc/alert-user1-iphone
{
  "query": {
    "bool": {
      "must": [
        { "match": { "name": "iPhone" } }
      ],
      "filter": [
        { "range": { "price": { "lte": 30000000 } } }
      ]
    }
  },
  "user_id": "user_001",
  "alert_name": "iPhone under 30M"
}

PUT /product-alerts/_doc/alert-user2-samsung
{
  "query": {
    "bool": {
      "must": [
        { "term": { "brand": "Samsung" } },
        { "range": { "price": { "lte": 25000000 } } }
      ]
    }
  },
  "user_id": "user_002",
  "alert_name": "Samsung cheap phone"
}

Dùng Percolate Query ​

bash
# Khi sản phẩm mới được tạo, kiểm tra alerts nào match
GET /product-alerts/_search
{
  "query": {
    "percolate": {
      "field": "query",          # Field chứa stored queries
      "document": {              # Document mới được tạo
        "name": "iPhone 15 Pro Max 256GB",
        "brand": "Apple",
        "price": 34990000,
        "category": "smartphones",
        "tags": ["flagship", "5G"]
      }
    }
  }
}

# Response: Trả về tất cả alert queries match document
{
  "hits": {
    "hits": [
      {
        "_id": "alert-user1-iphone",
        "_source": {
          "user_id": "user_001",
          "alert_name": "iPhone under 30M",
          "query": {...}
        }
        # NOTE: Price 34.99M > 30M → Không match alert này
        # → Không xuất hiện trong kết quả
      }
    ]
  }
}

Percolate nhiều documents cùng lúc ​

bash
GET /product-alerts/_search
{
  "query": {
    "percolate": {
      "field": "query",
      "documents": [
        {
          "name": "Samsung Galaxy S24",
          "brand": "Samsung",
          "price": 24990000
        },
        {
          "name": "iPhone 16",
          "brand": "Apple",
          "price": 28000000
        }
      ]
    }
  }
}

7.5 Geo Queries ​

geo_distance - Trong Bán Kính ​

bash
# Setup mapping
PUT /restaurants
{
  "mappings": {
    "properties": {
      "name": { "type": "text" },
      "location": { "type": "geo_point" },
      "rating": { "type": "float" }
    }
  }
}

# Index restaurants
POST /restaurants/_bulk
{"index": {"_id": "r1"}}
{"name": "Phở Hà Nội", "location": {"lat": 21.028, "lon": 105.834 }, "rating": 4.5}
{"index": {"_id": "r2"}}
{"name": "Bún Bò Huế", "location": {"lat": 21.032, "lon": 105.838 }, "rating": 4.3}
{"index": {"_id": "r3"}}
{"name": "Pizza Hut", "location": {"lat": 21.006, "lon": 105.852 }, "rating": 3.8}

# Tìm nhà hàng trong bán kính 5km
GET /restaurants/_search
{
  "query": {
    "geo_distance": {
      "distance": "5km",
      "distance_type": "arc",   # arc (chính xác) | plane (xấp xỉ, nhanh hơn)
      "location": {
        "lat": 21.028,
        "lon": 105.834
      }
    }
  },
  "sort": [
    {
      "_geo_distance": {
        "location": { "lat": 21.028, "lon": 105.834 },
        "order": "asc",
        "unit": "km",
        "distance_type": "arc"
      }
    }
  ],
  "script_fields": {
    "distance_km": {
      "script": {
        "source": "doc['location'].arcDistance(params.lat, params.lon) / 1000",
        "params": { "lat": 21.028, "lon": 105.834 }
      }
    }
  }
}

geo_bounding_box - Trong Hộp Chữ Nhật ​

bash
GET /restaurants/_search
{
  "query": {
    "geo_bounding_box": {
      "location": {
        "top_left": { "lat": 21.05, "lon": 105.80 },
        "bottom_right": { "lat": 21.00, "lon": 105.90 }
      }
    }
  }
}

geo_polygon - Trong Đa Giác ​

bash
GET /properties/_search
{
  "query": {
    "geo_polygon": {
      "location": {
        "points": [
          { "lat": 21.05, "lon": 105.80 },
          { "lat": 21.05, "lon": 105.90 },
          { "lat": 21.00, "lon": 105.90 },
          { "lat": 21.00, "lon": 105.80 }
        ]
      }
    }
  }
}

geo_shape - Query với GeoJSON shapes ​

bash
GET /districts/_search
{
  "query": {
    "geo_shape": {
      "area": {
        "shape": {
          "type": "circle",
          "coordinates": [105.834, 21.028],
          "radius": "10km"
        },
        "relation": "intersects"  # intersects | within | contains | disjoint
      }
    }
  }
}

7.6 More Like This (MLT) Query ​

Tìm documents tương tự với document/text cho trước:

bash
# Tìm bài viết tương tự bài viết có ID "article-123"
GET /articles/_search
{
  "query": {
    "more_like_this": {
      "fields": ["title", "content"],
      "like": [
        {
          "_index": "articles",
          "_id": "article-123"
        }
      ],
      "min_term_freq": 1,       # Term phải xuất hiện ít nhất 1 lần
      "max_query_terms": 25,    # Số terms trong query được generated
      "min_doc_freq": 1,        # Term phải xuất hiện trong ít nhất N docs
      "minimum_should_match": "20%"
    }
  }
}

# Tìm documents tương tự text cho trước
GET /articles/_search
{
  "query": {
    "more_like_this": {
      "fields": ["title", "content"],
      "like": "Elasticsearch search engine distributed",
      "min_term_freq": 1,
      "max_query_terms": 20
    }
  }
}

7.7 Span Queries ​

Span queries cho phép query về vị trí của terms trong document (useful cho legal/academic search):

bash
# Tìm "quick" và "fox" không quá 5 vị trí cách nhau
GET /articles/_search
{
  "query": {
    "span_near": {
      "clauses": [
        { "span_term": { "content": "elasticsearch" } },
        { "span_term": { "content": "distributed" } }
      ],
      "slop": 5,          # Tối đa 5 words giữa chúng
      "in_order": false   # Có cần đúng thứ tự không
    }
  }
}

ES 8.x có native support cho vector search, quan trọng cho AI/ML applications:

bash
# Mapping với dense_vector
PUT /articles-semantic
{
  "mappings": {
    "properties": {
      "title": { "type": "text" },
      "content": { "type": "text" },
      "embedding": {
        "type": "dense_vector",
        "dims": 384,
        "index": true,
        "similarity": "cosine"
      }
    }
  }
}

# Index document với vector (embedding từ sentence-transformers model)
PUT /articles-semantic/_doc/1
{
  "title": "Tìm kiếm ngữ nghĩa với Elasticsearch",
  "content": "Vector search là tương lai...",
  "embedding": [0.1, -0.3, 0.7, 0.2, ...]  # 384 dimensions
}

# kNN search - Semantic search
GET /articles-semantic/_search
{
  "knn": {
    "field": "embedding",
    "query_vector": [-0.1, 0.3, 0.65, ...],  # Embedding của search query
    "k": 10,                                   # Top K results
    "num_candidates": 100                      # Candidates để xem xét
  },
  "fields": ["title", "content"],
  "_source": false
}

# Hybrid search: kNN + BM25
GET /articles-semantic/_search
{
  "knn": {
    "field": "embedding",
    "query_vector": [...],
    "k": 5,
    "num_candidates": 50,
    "boost": 0.7
  },
  "query": {
    "match": {
      "content": "elasticsearch distributed search",
      "boost": 0.3
    }
  }
}

Real-world Vector Search Pipeline ​

python
from sentence_transformers import SentenceTransformer
from elasticsearch import Elasticsearch

model = SentenceTransformer('all-MiniLM-L6-v2')
es = Elasticsearch("http://localhost:9200")

# Encode documents khi indexing
def index_article(article):
    embedding = model.encode(article['content']).tolist()
    es.index(
        index='articles-semantic',
        document={
            **article,
            'embedding': embedding
        }
    )

# Encode query khi searching
def semantic_search(query_text, size=10):
    query_embedding = model.encode(query_text).tolist()
    
    result = es.search(
        index='articles-semantic',
        body={
            "knn": {
                "field": "embedding",
                "query_vector": query_embedding,
                "k": size,
                "num_candidates": size * 10
            },
            "fields": ["title"],
            "_source": {"excludes": ["embedding"]}
        }
    )
    
    return result['hits']['hits']

# Usage:
results = semantic_search("cách tối ưu hóa elasticsearch performance")

7.9 Advanced Highlighting ​

Fast Vector Highlighter (FVH) ​

Cần mapping đặc biệt nhưng nhanh hơn default highlighter:

bash
# Mapping
PUT /articles
{
  "mappings": {
    "properties": {
      "content": {
        "type": "text",
        "term_vector": "with_positions_offsets"  # Required for FVH
      }
    }
  }
}

# Query với FVH
GET /articles/_search
{
  "query": { "match": { "content": "elasticsearch" } },
  "highlight": {
    "type": "fvh",
    "fields": {
      "content": {
        "fragment_size": 150,
        "number_of_fragments": 3,
        "order": "score",         # score | none
        "boundary_max_scan": 20,  # Scan max chars để tìm boundary
        "boundary_chars": ".,!?\n",
        "matched_fields": ["content", "content.english"]  # Multi-field highlighting
      }
    }
  }
}

7.10 Collapse - Deduplication ​

Nhóm kết quả theo một field, chỉ trả về top N documents mỗi nhóm:

bash
# Tìm sản phẩm, mỗi brand chỉ hiển thị 1 sản phẩm tốt nhất
GET /products/_search
{
  "query": { "match": { "name": "phone" } },
  "collapse": {
    "field": "brand",            # Collapse theo brand
    "inner_hits": {
      "name": "all_brands",      # Lấy thêm 2 sản phẩm khác của brand
      "size": 2,
      "sort": [{ "price": "asc" }]
    },
    "max_concurrent_group_searches": 4
  },
  "sort": ["_score"]
}

7.11 Search Templates ​

Parameterize queries để reuse:

bash
# Lưu search template
PUT /_scripts/product-search-template
{
  "script": {
    "lang": "mustache",
    "source": {
      "query": {
        "bool": {
          "must": [
            {
              "multi_match": {
                "query": "{{query}}",
                "fields": ["name^3", "description"]
              }
            }
          ],
          "filter": [
            {{#brand}}
            { "term": { "brand": "{{brand}}" } },
            {{/brand}}
            {
              "range": {
                "price": {
                  {{#min_price}}"gte": {{min_price}},{{/min_price}}
                  {{#max_price}}"lte": {{max_price}}{{/max_price}}
                }
              }
            }
          ]
        }
      },
      "from": "{{from}}{{^from}}0{{/from}}",
      "size": "{{size}}{{^size}}10{{/size}}"
    }
  }
}

# Sử dụng template
GET /products/_search/template
{
  "id": "product-search-template",
  "params": {
    "query": "iphone pro",
    "brand": "Apple",
    "min_price": 20000000,
    "max_price": 50000000,
    "from": 0,
    "size": 20
  }
}

# Preview rendered template
GET /_render/template
{
  "id": "product-search-template",
  "params": {
    "query": "samsung",
    "size": 5
  }
}

Tóm Tắt Chương 7 ​

Advanced Query Reference ​

QueryKhi dùng
function_scoreTùy chỉnh scoring với business logic
nestedQuery arrays duy trì relationships
has_child/has_parentParent-child documents
percolateReverse matching (queries match documents)
more_like_thisContent-based recommendations
knnSemantic/vector search với AI/ML
geo_distanceTìm kiếm theo vị trí địa lý
span_nearProximity search (khoảng cách terms)

Decay Functions Cheat Sheet ​

FunctionShapeDùng khi
gaussBell curvePopularity decay, thường được dùng nhất
linearStraight lineKhoảng cách địa lý
expExponentialGiá cả rất gần target quan trọng hơn

Bước Tiếp Theo ​

→ Chương 8: Text Analysis - Hiểu sâu về cách ES phân tích và xử lý văn bản, đặc biệt tiếng Việt


Chương 8: Text Analysis - Phân Tích Văn Bản ​

8.1 Tại Sao Text Analysis Quan Trọng? ​

Text analysis là quá trình chuyển đổi text thành tokens để lưu vào inverted index. Đây là nơi quyết định search có "thông minh" không.

Vấn đề nếu không có Analysis ​

User searches: "Điện thoại iOS 5G tốt nhất"

Không có analysis:
- Token: ["Điện thoại iOS 5G tốt nhất"] — 1 token duy nhất
- Chỉ exact match → không tìm được

Standard analysis:  
- Tokens: ["điện", "thoại", "ios", "5g", "tốt", "nhất"]
- Tìm documents có chứa bất kỳ token nào

Custom analysis với synonym:
- Tokens: ["điện", "thoại", "điện_thoại", "smartphone", "ios", "apple", "5g", "tốt", "nhất"]
- Còn highlight được từ đồng nghĩa

8.2 Analysis Pipeline ​

Khi ES xử lý text, nó qua 3 bước:

Raw Text
   │
   ▼
┌─────────────┐
│  CHARACTER  │  Xử lý ký tự trước tokenization
│   FILTERS   │  HTML strip, pattern replace, etc.
└─────────────┘
   │
   ▼
┌─────────────┐
│  TOKENIZER  │  Chia text thành tokens
│             │  (1 tokenizer duy nhất mỗi analyzer)
└─────────────┘
   │
   ▼
┌─────────────┐
│   TOKEN     │  Xử lý từng token
│   FILTERS   │  Lowercase, stop words, synonyms, stemming...
└─────────────┘
   │
   ▼
Tokens (terms in inverted index)

8.3 Kiểm Tra Analysis ​

Luôn test analyzer trước khi deploy:

bash
# Test analyzer built-in
GET /_analyze
{
  "analyzer": "standard",
  "text": "iPhone 15 Pro Max là điện thoại tốt nhất 2024"
}

# Response:
{
  "tokens": [
    { "token": "iphone", "start_offset": 0, "end_offset": 6, "type": "<ALPHANUM>", "position": 0 },
    { "token": "15", "start_offset": 7, "end_offset": 9, "type": "<NUM>", "position": 1 },
    { "token": "pro", "start_offset": 10, "end_offset": 13, "type": "<ALPHANUM>", "position": 2 },
    { "token": "max", "start_offset": 14, "end_offset": 17, "type": "<ALPHANUM>", "position": 3 },
    { "token": "là", "start_offset": 18, "end_offset": 20, "type": "<ALPHANUM>", "position": 4 },
    { "token": "điện", "start_offset": 21, "end_offset": 25, "type": "<ALPHANUM>", "position": 5 },
    { "token": "thoại", "start_offset": 26, "end_offset": 31, "type": "<ALPHANUM>", "position": 6 },
    { "token": "tốt", "start_offset": 32, "end_offset": 35, "type": "<ALPHANUM>", "position": 7 },
    { "token": "nhất", "start_offset": 36, "end_offset": 40, "type": "<ALPHANUM>", "position": 8 },
    { "token": "2024", "start_offset": 41, "end_offset": 45, "type": "<NUM>", "position": 9 }
  ]
}

# Test custom tokenizer
GET /_analyze
{
  "tokenizer": "standard",
  "filter": ["lowercase", "stop"],
  "text": "The Quick Brown Fox"
}

# Test trên index cụ thể
GET /products/_analyze
{
  "field": "name",   # Dùng analyzer của field này
  "text": "iPhone 15 Pro Max"
}

8.4 Built-in Analyzers ​

standard Analyzer (Default) ​

bash
GET /_analyze
{
  "analyzer": "standard",
  "text": "The 2 QUICK Brown-Foxes jumped over lazy dog's bone."
}
# Tokens: [the, 2, quick, brown, foxes, jumped, over, lazy, dog's, bone]
  • Tokenize theo word boundaries (Unicode)
  • Lowercase everything
  • Remove qua basic stop words (lang-dependent)

simple Analyzer ​

bash
GET /_analyze
{
  "analyzer": "simple",
  "text": "iPhone 15 Pro Max"
}
# Tokens: [iphone, pro, max]  ← Bỏ numbers, chia theo non-letter chars

whitespace Analyzer ​

bash
GET /_analyze
{
  "analyzer": "whitespace",
  "text": "iPhone 15 Pro Max"
}
# Tokens: [iPhone, 15, Pro, Max]  ← Chỉ split theo whitespace, GIỮ case

keyword Analyzer ​

bash
GET /_analyze
{
  "analyzer": "keyword",
  "text": "iPhone 15 Pro Max"
}
# Tokens: [iPhone 15 Pro Max]  ← Không phân tích, giữ nguyên toàn bộ

pattern Analyzer ​

bash
GET /_analyze
{
  "analyzer": "pattern",
  "text": "product:SKU001:category:electronics"
}
# Default split by: \W+
# Tokens: [product, sku001, category, electronics]

Language Analyzers ​

bash
# English analyzer - stemming, stop words
GET /_analyze
{
  "analyzer": "english",
  "text": "The cats are running quickly through the trees"
}
# Tokens: [cat, run, quickli, tree]  ← Stemmed!

# Default tiếng Việt:
# Không có built-in Vietnamese analyzer
# Cần custom analyzer hoặc plugin

8.5 Tokenizers ​

standard Tokenizer ​

bash
GET /_analyze
{
  "tokenizer": "standard",
  "text": "Hello, World! Testing 1-2-3."
}
# Tokens: [Hello, World, Testing, 1, 2, 3]
# Dựa trên Unicode Text Segmentation

whitespace Tokenizer ​

bash
GET /_analyze
{
  "tokenizer": "whitespace",
  "text": "Hello World testing-1-2-3"
}
# Tokens: [Hello, World, testing-1-2-3]  ← Giữ dấu gạch ngang

keyword Tokenizer ​

bash
# Không phân tích, toàn bộ text = 1 token
GET /_analyze
{
  "tokenizer": "keyword",
  "text": "My Product SKU-001"
}
# Tokens: [My Product SKU-001]

pattern Tokenizer (Regex-based) ​

bash
GET /_analyze
{
  "tokenizer": {
    "type": "pattern",
    "pattern": ","     # Split theo dấu phẩy
  },
  "text": "apple,samsung,xiaomi,oppo"
}
# Tokens: [apple, samsung, xiaomi, oppo]

ngram Tokenizer ​

N-gram tạo ra tất cả subsequences có độ dài n từ text. Rất hữu ích cho partial matching:

bash
GET /_analyze
{
  "tokenizer": {
    "type": "ngram",
    "min_gram": 2,
    "max_gram": 3,
    "token_chars": ["letter", "digit"]
  },
  "text": "iphone"
}
# Tokens: [ip, iph, ph, pho, ho, hon, on, one, ne]
# Cho phép tìm "pho" match "iphone" → Partial search!

edge_ngram Tokenizer ​

Chỉ tạo ngrams từ đầu từ - rất tốt cho autocomplete:

bash
GET /_analyze
{
  "tokenizer": {
    "type": "edge_ngram",
    "min_gram": 2,
    "max_gram": 10,
    "token_chars": ["letter", "digit"]
  },
  "text": "iphone"
}
# Tokens: [ip, iph, ipho, iphon, iphone]
# Gõ "iph" → tìm được "iphone"!

path_hierarchy Tokenizer ​

Dành cho đường dẫn phân cấp:

bash
GET /_analyze
{
  "tokenizer": "path_hierarchy",
  "text": "electronics/smartphones/apple/iphone-15"
}
# Tokens: [electronics, electronics/smartphones, electronics/smartphones/apple, 
#          electronics/smartphones/apple/iphone-15]
# Query "electronics" tìm được tất cả products trong category điện tử!

8.6 Token Filters ​

Token filters xử lý từng token sau khi tokenizer:

lowercase Filter ​

bash
# "iPhone" → "iphone"
# "SAMSUNG" → "samsung"

stop Filter - Xóa Stop Words ​

bash
GET /_analyze
{
  "tokenizer": "standard",
  "filter": [
    {
      "type": "stop",
      "stopwords": ["_english_"]  # Built-in stop word lists
    }
  ],
  "text": "The quick brown fox"
}
# Tokens: [quick, brown, fox]  ← "The" bị xóa

Lưu ý: Stop words trong tiếng Việt (_vietnamese_) không có trong built-in - cần custom list:

bash
{
  "type": "stop",
  "stopwords": ["là", "và", "của", "có", "trong", "với", "được", "để", "cho", "từ", "tại", "về", "theo", "đó", "này", "đã", "sẽ", "thì", "không", "một", "những", "các", "cũng", "như", "nhưng", "hay", "hoặc", "mà"]
}

synonym Filter - Từ Đồng Nghĩa ​

bash
PUT /products-vi
{
  "settings": {
    "analysis": {
      "filter": {
        "vi_synonyms": {
          "type": "synonym",
          "synonyms": [
            "điện thoại, smartphone, mobile phone",
            "laptop, máy tính xách tay, notebook",
            "tai nghe, headphone, earphone",
            "iphone => apple iphone",     # ← implies: "iphone" → indexed as "apple iphone"
            "sam sung => samsung"          # ← Typo correction via synonym
          ],
          "lenient": true   # Bỏ qua lỗi khi load synonyms
        }
      }
    }
  }
}

Synonym file (dùng file thay vì hardcode):

bash
{
  "type": "synonym_graph",
  "synonyms_path": "analysis/synonyms.txt",  # Relative to ES config dir
  "updateable": true   # Có thể reload mà không cần restart
}

File synonyms.txt:

# E-commerce synonyms
điện thoại, smartphone, mobile phone, dt
laptop, máy tính xách tay, notebook, máy tính
tai nghe, headphone, earphone, headset

# Brand synonyms
sam sung => samsung
iphone 15 pro => apple iphone 15 pro

stemmer Filter - Stemming ​

Đưa từ về dạng gốc (root form):

bash
GET /_analyze
{
  "tokenizer": "standard",
  "filter": [
    "lowercase",
    {
      "type": "stemmer",
      "language": "english"
    }
  ],
  "text": "running runners ran"
}
# Tokens: [run, runner, ran]  ← stems (not perfect)

# Snowball stemmer (thường tốt hơn)
{
  "type": "snowball",
  "language": "English"
}

word_delimiter_graph Filter ​

Tách từ phức hợp và xử lý dấu câu phức tạp:

bash
GET /_analyze
{
  "tokenizer": "keyword",
  "filter": [
    {
      "type": "word_delimiter_graph",
      "generate_word_parts": true,
      "generate_number_parts": true,
      "catenate_words": true,    # "WiFi-6E" → "WiFi6E" cũng được index
      "catenate_numbers": true,
      "split_on_case_change": true    # "camelCase" → "camel" + "Case"
    }
  ],
  "text": "Wi-Fi 6E"
}
# Tokens: [Wi, WiFi, Fi, 6E, 6, E]

edge_ngram Filter ​

Khác với edge_ngram tokenizer, filter này áp dụng sau tokenization:

bash
GET /_analyze
{
  "tokenizer": "standard",
  "filter": [
    {
      "type": "edge_ngram",
      "min_gram": 1,
      "max_gram": 10
    }
  ],
  "text": "iPhone Samsung"
}
# Tokens: [i, ip, iph, ipho, iphon, iphone, s, sa, sam, sams, samsu, samsun, samsung]

asciifolding Filter ​

Chuyển ký tự có dấu sang ASCII tương đương:

bash
GET /_analyze
{
  "tokenizer": "standard",
  "filter": ["asciifolding"],
  "text": "café résumé"
}
# Tokens: [cafe, resume]
# "café" → "cafe", "résumé" → "resume"

phonetic Filter ​

Tìm kiếm theo phát âm (Soundex, Metaphone...):

bash
# Cần cài plugin: elasticsearch-analysis-phonetic
PUT /phonetic-test
{
  "settings": {
    "analysis": {
      "analyzer": {
        "phonetic_analyzer": {
          "tokenizer": "standard",
          "filter": ["lowercase", "my_phonetic"]
        }
      },
      "filter": {
        "my_phonetic": {
          "type": "phonetic",
          "encoder": "metaphone",
          "replace": false
        }
      }
    }
  }
}

8.7 Character Filters ​

html_strip - Loại Bỏ HTML Tags ​

bash
GET /_analyze
{
  "char_filter": ["html_strip"],
  "tokenizer": "standard",
  "text": "<h1>Sản phẩm</h1> <p>Mô tả <strong>tốt</strong></p>"
}
# Tokens: [Sản, phẩm, Mô, tả, tốt]  ← HTML tags removed

mapping - Thay Thế Ký Tự ​

bash
GET /_analyze
{
  "char_filter": [
    {
      "type": "mapping",
      "mappings": [
        "₫ => vnd",
        "0 => zero",
        "1 => one",
        "& => and",
        ":) => happy",
        "😊 => happy"
      ]
    }
  ],
  "tokenizer": "standard",
  "text": "Giá: 500₫ bạn & tôi :)"
}
# "500₫ bạn & tôi :)" → "500vnd bạn and tôi happy"

pattern_replace - Thay Thế Theo Regex ​

bash
GET /_analyze
{
  "char_filter": [
    {
      "type": "pattern_replace",
      "pattern": "(\\d+)-SKU",
      "replacement": "SKU_$1"
    }
  ],
  "tokenizer": "standard",
  "text": "Product 123-SKU in stock"
}
# "123-SKU" → "SKU_123"

8.8 Custom Analyzers ​

Ví Dụ 1: Product Name Analyzer ​

bash
PUT /ecommerce
{
  "settings": {
    "analysis": {
      "char_filter": {
        "html_and_special": {
          "type": "mapping",
          "mappings": [
            "& => and",
            "/ => or",
            "₫ => vnd",
            "% => percent"
          ]
        }
      },
      "tokenizer": {
        "product_tokenizer": {
          "type": "standard"
        }
      },
      "filter": {
        "product_stop": {
          "type": "stop",
          "stopwords": ["tại", "của", "và", "hoặc", "với", "cho"]
        },
        "product_synonyms": {
          "type": "synonym",
          "synonyms": [
            "điện thoại, smartphone, mobile",
            "laptop, notebook, máy tính xách tay",
            "gb => gb",
            "tb => terabyte"
          ]
        },
        "sku_code_preserving": {
          "type": "word_delimiter_graph",
          "generate_word_parts": true,
          "generate_number_parts": true,
          "split_on_case_change": false,
          "preserve_original": true
        }
      },
      "analyzer": {
        "product_name_analyzer": {
          "type": "custom",
          "char_filter": ["html_and_special"],
          "tokenizer": "standard",
          "filter": [
            "lowercase",
            "sku_code_preserving",
            "product_stop",
            "product_synonyms"
          ]
        },
        "product_search_analyzer": {
          "type": "custom",
          "char_filter": ["html_and_special"],
          "tokenizer": "standard",
          "filter": [
            "lowercase",
            "product_stop",
            "product_synonyms"
          ]
        }
      }
    }
  },
  "mappings": {
    "properties": {
      "name": {
        "type": "text",
        "analyzer": "product_name_analyzer",
        "search_analyzer": "product_search_analyzer"
      }
    }
  }
}

# Test:
GET /ecommerce/_analyze
{
  "analyzer": "product_name_analyzer",
  "text": "Samsung Galaxy S24 Ultra 256GB/12GB RAM"
}

Ví Dụ 2: Autocomplete Analyzer (Edge N-gram) ​

bash
PUT /autocomplete-index
{
  "settings": {
    "analysis": {
      "analyzer": {
        "autocomplete_index_analyzer": {
          "type": "custom",
          "tokenizer": "standard",
          "filter": [
            "lowercase",
            "autocomplete_filter"
          ]
        },
        "autocomplete_search_analyzer": {
          "type": "custom",
          "tokenizer": "standard",
          "filter": ["lowercase"]
          # Không dùng edge_ngram khi search!
        }
      },
      "filter": {
        "autocomplete_filter": {
          "type": "edge_ngram",
          "min_gram": 2,
          "max_gram": 20
        }
      }
    }
  },
  "mappings": {
    "properties": {
      "name": {
        "type": "text",
        "analyzer": "autocomplete_index_analyzer",
        "search_analyzer": "autocomplete_search_analyzer"
        # Khi index: "iphone" → [ip, iph, ipho, iphon, iphone]
        # Khi search: "ipho" → [ipho]
        # Match: "ipho" in [ip, iph, ipho, iphon, iphone] → FOUND!
      }
    }
  }
}

Test autocomplete:

bash
# Index
PUT /autocomplete-index/_doc/1
{ "name": "iPhone 15 Pro Max 256GB" }

PUT /autocomplete-index/_doc/2
{ "name": "iPad Pro 12.9 M2" }

# Search - autocomplete
GET /autocomplete-index/_search
{
  "query": {
    "match": {
      "name": {
        "query": "ipho",
        "analyzer": "autocomplete_search_analyzer"
      }
    }
  }
}
# Kết quả: Both documents match vì cả 2 có "iphone"/"ipad" chứa "ipho"... không!
# Chỉ iPhone match "ipho"

Ví Dụ 3: N-gram Analyzer cho Tìm Kiếm Substring ​

bash
PUT /substring-search
{
  "settings": {
    "analysis": {
      "analyzer": {
        "ngram_analyzer": {
          "type": "custom",
          "tokenizer": "ngram_tokenizer",
          "filter": ["lowercase"]
        },
        "search_analyzer": {
          "type": "custom",
          "tokenizer": "standard",
          "filter": ["lowercase"]
        }
      },
      "tokenizer": {
        "ngram_tokenizer": {
          "type": "ngram",
          "min_gram": 2,
          "max_gram": 4,
          "token_chars": ["letter", "digit"]
        }
      }
    }
  },
  "mappings": {
    "properties": {
      "product_code": {
        "type": "text",
        "analyzer": "ngram_analyzer",
        "search_analyzer": "search_analyzer"
      }
    }
  }
}

# Test: "SP001234" sẽ tạo ra: 
# [sp, spo, spoo, po, poo, p00, oo, o01, 01, 012, 12, 123, 23, 234, 34]
# User search "001" → match!

8.9 Xử Lý Tiếng Việt ​

Tiếng Việt là một ngôn ngữ phân tích đặc thù:

  • Không có space để tách từ (thực ra có space nhưng đơn vị từ là chữ tiếng Việt)
  • Tiếng Việt có tính đặc biệt: "điện thoại" là 2 âm tiết = 1 từ
  • Dấu câu ảnh hưởng nghĩa: "ma", "má", "mà", "mã", "mả", "mạ" là 6 từ khác nhau

Vấn Đề với Standard Analyzer ​

bash
GET /_analyze
{
  "analyzer": "standard",
  "text": "điện thoại di động thông minh"
}
# Tokens: [điện, thoại, di, động, thông, minh]
# "điện thoại" bị tách thành 2 token riêng biệt
# "thông minh" → 2 tokens riêng biệt

Hậu quả: "điện thoại" (phone) và "thoại kịch" (play) đều chứa "thoại" → có thể cross-match.

Plugin ICU (Unicode Support) ​

bash
# Cài plugin
bin/elasticsearch-plugin install analysis-icu

# ICU Analyzer
GET /_analyze
{
  "analyzer": "icu_analyzer",
  "text": "điện thoại di động thông minh"
}
# Better Unicode handling (diacritics, word boundaries)

Plugin Vietnamese Analysis (vn-nlp) ​

Dùng plugin tích hợp NLP cho tiếng Việt:

bash
# Option 1: elasticsearch-analysis-vietnamese plugin
# https://github.com/duydo/elasticsearch-analysis-vietnamese

# Cài đặt (cần compatible với ES version):
bin/elasticsearch-plugin install \
  file:///path/to/elasticsearch-analysis-vietnamese-8.x.x.zip

# Sau khi cài:
PUT /vi-articles
{
  "settings": {
    "analysis": {
      "analyzer": {
        "vi_analyzer": {
          "type": "vi_analyzer"  # Plugin analyzer
        }
      }
    }
  }
}

GET /vi-articles/_analyze
{
  "analyzer": "vi_analyzer",
  "text": "điện thoại di động thông minh tốt nhất 2024"
}
# Tokens: [điện_thoại, di_động, thông_minh, tốt_nhất, 2024]
# Words are properly segmented!

Custom Vietnamese Analyzer (Không cần plugin) ​

Nếu không thể cài plugin, có thể dùng stop words và synonym:

bash
PUT /vi-products
{
  "settings": {
    "analysis": {
      "char_filter": {
        "remove_diacritics_map": {
          "type": "mapping",
          "mappings": [
            "à => a", "á => a", "ả => a", "ã => a", "ạ => a",
            "â => a", "ầ => a", "ấ => a", "ẩ => a", "ẫ => a", "ậ => a",
            "ă => a", "ằ => a", "ắ => a", "ẳ => a", "ẵ => a", "ặ => a",
            "è => e", "é => e", "ẻ => e", "ẽ => e", "ẹ => e",
            "ê => e", "ề => e", "ế => e", "ể => e", "ễ => e", "ệ => e",
            "ì => i", "í => i", "ỉ => i", "ĩ => i", "ị => i",
            "ò => o", "ó => o", "ỏ => o", "õ => o", "ọ => o",
            "ô => o", "ồ => o", "ố => o", "ổ => o", "ỗ => o", "ộ => o",
            "ơ => o", "ờ => o", "ớ => o", "ở => o", "ỡ => o", "ợ => o",
            "ù => u", "ú => u", "ủ => u", "ũ => u", "ụ => u",
            "ư => u", "ừ => u", "ứ => u", "ử => u", "ữ => u", "ự => u",
            "ỳ => y", "ý => y", "ỷ => y", "ỹ => y", "ỵ => y",
            "đ => d"
          ]
        }
      },
      "filter": {
        "vi_stop_words": {
          "type": "stop",
          "stopwords": [
            "là", "và", "của", "có", "trong", "với", "được", "để", "cho",
            "từ", "tại", "về", "theo", "đó", "này", "đã", "sẽ", "thì",
            "không", "một", "những", "các", "cũng", "như", "nhưng",
            "hay", "hoặc", "mà", "rất", "thêm", "bởi", "vì", "sau",
            "trước", "khi", "nếu", "vẫn", "còn", "đã", "đang"
          ]
        },
        "vi_synonyms": {
          "type": "synonym",
          "synonyms": [
            "đt, dien thoai, smartphone => điện thoại",
            "laptop, may tinh xach tay, notebook => laptop",
            "airpods, tai nghe apple => tai nghe apple",
            "op lung, bao da, case => phụ kiện điện thoại",
            "pin du phong, power bank => sạc dự phòng"
          ]
        }
      },
      "analyzer": {
        "vi_standard": {
          "type": "custom",
          "tokenizer": "standard",
          "filter": [
            "lowercase",
            "vi_stop_words",
            "vi_synonyms"
          ]
        },
        "vi_no_diacritics": {
          "type": "custom",
          "char_filter": ["remove_diacritics_map"],
          "tokenizer": "standard",
          "filter": [
            "lowercase",
            "vi_stop_words"
          ]
        }
      }
    }
  },
  "mappings": {
    "properties": {
      "name": {
        "type": "text",
        "analyzer": "vi_standard",
        "fields": {
          "no_diacritics": {
            "type": "text",
            "analyzer": "vi_no_diacritics"  # Tìm không dấu cũng được
          }
        }
      }
    }
  }
}

Test tìm không dấu:

bash
GET /vi-products/_analyze
{
  "analyzer": "vi_no_diacritics",
  "text": "dien thoai samsung galaxy"
}
# "điện thoại samsung galaxy" và "dien thoai samsung galaxy" đều match nhau

# Query tìm không dấu
GET /vi-products/_search
{
  "query": {
    "multi_match": {
      "query": "dien thoai samsung",
      "fields": ["name", "name.no_diacritics"]
    }
  }
}

8.10 Index-time vs Search-time Analysis ​

Quan trọng: ES analyze text 2 lần:

  1. Khi index: Text → tokens → stored in inverted index
  2. Khi search: Query → tokens → lookup in index

Mặc định: Cùng analyzer cho cả 2. Nhưng có thể dùng khác nhau:

bash
PUT /products
{
  "mappings": {
    "properties": {
      "name": {
        "type": "text",
        "analyzer": "autocomplete_index_analyzer",   # Index: create edge ngrams
        "search_analyzer": "standard"                # Search: standard tokens
      }
    }
  }
}

Khi dùng khác nhau:

  • Autocomplete: Index với edge_ngram, search với standard
  • Typo tolerance: Index với standard, search với phonetic
  • Synonym expansion: Có thể khác nhau để tránh index quá nhiều

8.11 Analyze API nâng cao ​

bash
# Xem chi tiết quá trình analysis
GET /_analyze
{
  "tokenizer": "standard",
  "filter": [
    "lowercase",
    "stop"
  ],
  "char_filter": ["html_strip"],
  "text": "<h1>Hello World</h1>",
  "explain": true    # Chi tiết từng bước
}

# Response:
{
  "detail": {
    "custom_analyzer": true,
    "charfilters": [
      {
        "name": "html_strip",
        "filtered_text": ["\nHello World\n"]
      }
    ],
    "tokenizer": {
      "name": "standard",
      "tokens": [
        { "token": "Hello", "start_offset": 1, "end_offset": 6 },
        { "token": "World", "start_offset": 7, "end_offset": 12 }
      ]
    },
    "tokenfilters": [
      {
        "name": "lowercase",
        "tokens": [
          { "token": "hello", "start_offset": 1, "end_offset": 6 },
          { "token": "world", "start_offset": 7, "end_offset": 12 }
        ]
      }
    ]
  }
}

8.12 Multi-language Support ​

bash
PUT /multilang-articles
{
  "settings": {
    "analysis": {
      "analyzer": {
        "en_analyzer": {
          "type": "english"
        },
        "vi_analyzer": {
          "type": "custom",
          "tokenizer": "standard",
          "filter": ["lowercase"]
        },
        "fr_analyzer": {
          "type": "french"
        }
      }
    }
  },
  "mappings": {
    "properties": {
      "title_en": { "type": "text", "analyzer": "en_analyzer" },
      "title_vi": { "type": "text", "analyzer": "vi_analyzer" },
      "title_fr": { "type": "text", "analyzer": "fr_analyzer" },
      "language": { "type": "keyword" }
    }
  }
}

# Query based on language
GET /multilang-articles/_search
{
  "query": {
    "bool": {
      "should": [
        {
          "bool": {
            "must": [
              { "term": { "language": "en" } },
              { "match": { "title_en": "search engine" } }
            ]
          }
        },
        {
          "bool": {
            "must": [
              { "term": { "language": "vi" } },
              { "match": { "title_vi": "công cụ tìm kiếm" } }
            ]
          }
        }
      ]
    }
  }
}

Tóm Tắt Chương 8 ​

Analysis Pipeline Recap ​

Text → Char Filters → Tokenizer → Token Filters → Tokens
  • Char Filters: html_strip, mapping, pattern_replace
  • Tokenizers: standard, whitespace, ngram, edge_ngram, path_hierarchy
  • Token Filters: lowercase, stop, synonym, stemmer, edge_ngram

Quyết Định Analyzer ​

Dùng để làm gìAnalyzer phù hợp
Full-text search tiếng Anhenglish (stemming)
Full-text search tiếng ViệtCustom (stop words + synonyms)
Exact matchkeyword
AutocompleteCustom edge_ngram
Partial/substring searchCustom ngram
Product codes/IDskeyword hoặc whitespace
Log messagesstandard hoặc whitespace
Multi-languageSeparate fields per language

Nguyên Tắc Vàng ​

  1. Test trước khi deploy với _analyze API
  2. Index và Search analyzer phải compatible (tokens của search phải có trong index)
  3. Đừng dùng edge_ngram làm search analyzer (sẽ tạo quá nhiều tokens)
  4. Synonyms nên dùng synonym_graph filter và chỉ ở search-time nếu có thể
  5. Stop words cẩn thận - "not" là stop word nhưng quan trọng trong nhiều contexts

Bước Tiếp Theo ​

→ Chương 9: Aggregations - Analytics mạnh mẽ với Elasticsearch Aggregation Framework


Chương 9: Aggregations - Phân Tích Dữ Liệu ​

9.1 Aggregations Là Gì? ​

Aggregations là tính năng analytics mạnh mẽ của Elasticsearch cho phép tính toán thống kê, nhóm dữ liệu, và phân tích xu hướng trên tập dữ liệu lớn trong real-time.

Query vs Aggregation ​

bash
# Query: Tìm kiếm documents
GET /orders/_search
{
  "query": { "term": { "status": "completed" } },
  "hits": { "total": 1250 }
}

# Aggregation: Phân tích dữ liệu
GET /orders/_search
{
  "size": 0,             # Không cần documents, chỉ cần aggregation result
  "aggs": {
    "total_revenue": {   # Tên aggregation (tùy đặt)
      "sum": {
        "field": "amount"
      }
    }
  }
}
# Kết quả: Total revenue = 254,750,000 VNĐ

9.2 Các Loại Aggregation ​

Aggregations
├── Metric Aggregations    (Tính toán số liệu: sum, avg, min, max)
├── Bucket Aggregations    (Nhóm documents: terms, date_histogram, range)
├── Pipeline Aggregations  (Aggregation trên aggregation khác)
└── Matrix Aggregations    (Tính toán ma trận, ít dùng)

9.3 Metric Aggregations ​

avg, sum, min, max, value_count ​

bash
GET /orders/_search
{
  "size": 0,
  "aggs": {
    "avg_order_value": {
      "avg": { "field": "amount" }
    },
    "total_revenue": {
      "sum": { "field": "amount" }
    },
    "min_order": {
      "min": { "field": "amount" }
    },
    "max_order": {
      "max": { "field": "amount" }
    },
    "order_count": {
      "value_count": { "field": "order_id" }
    }
  }
}

# Response:
{
  "aggregations": {
    "avg_order_value": { "value": 204125.0 },
    "total_revenue": { "value": 255156250.0 },
    "min_order": { "value": 15000.0 },
    "max_order": { "value": 45000000.0 },
    "order_count": { "value": 1250 }
  }
}

stats - Tổng Hợp 5 Metrics ​

bash
GET /orders/_search
{
  "size": 0,
  "aggs": {
    "order_stats": {
      "stats": { "field": "amount" }
    }
  }
}

# Response:
{
  "aggregations": {
    "order_stats": {
      "count": 1250,
      "min": 15000.0,
      "max": 45000000.0,
      "avg": 204125.0,
      "sum": 255156250.0
    }
  }
}

extended_stats - Thêm Phân Phối Thống Kê ​

bash
GET /orders/_search
{
  "size": 0,
  "aggs": {
    "order_extended_stats": {
      "extended_stats": {
        "field": "amount",
        "sigma": 2.0    # Standard deviation bounds
      }
    }
  }
}

# Response:
{
  "aggregations": {
    "order_extended_stats": {
      "count": 1250,
      "min": 15000.0,
      "max": 45000000.0,
      "avg": 204125.0,
      "sum": 255156250.0,
      "sum_of_squares": 2.1e+15,
      "variance": 1543200000.0,
      "std_deviation": 39283.5,
      "std_deviation_bounds": {
        "upper": 282692.0,    # avg + 2 * std_dev
        "lower": 125558.0     # avg - 2 * std_dev
      }
    }
  }
}

percentiles - Phân Vị ​

bash
GET /response-times/_search
{
  "size": 0,
  "aggs": {
    "latency_percentiles": {
      "percentiles": {
        "field": "response_time_ms",
        "percents": [50, 90, 95, 99, 99.9],
        "keyed": false,   # Array thay vì object
        "tdigest": {
          "compression": 100   # Độ chính xác (default 100, cao hơn = chính xác hơn)
        }
      }
    }
  }
}

# Response:
{
  "aggregations": {
    "latency_percentiles": {
      "values": [
        { "key": 50.0, "value": 45.2 },   # p50 = median = 45ms
        { "key": 90.0, "value": 189.7 },  # p90 = 189ms
        { "key": 95.0, "value": 340.1 },  # p95 = 340ms
        { "key": 99.0, "value": 1250.0 }, # p99 = 1.25s
        { "key": 99.9, "value": 4500.0 }  # p99.9 = 4.5s
      ]
    }
  }
}
# Kết luận: 99% requests < 1.25s, nhưng có outlier tới 4.5s

percentile_ranks - Xếp Hạng Phân Vị ​

bash
GET /response-times/_search
{
  "size": 0,
  "aggs": {
    "rank_200ms": {
      "percentile_ranks": {
        "field": "response_time_ms",
        "values": [200, 500, 1000]   # Bao nhiêu % requests < 200ms, 500ms, 1000ms?
      }
    }
  }
}

# Response:
{
  "aggregations": {
    "rank_200ms": {
      "values": {
        "200.0": 90.5,    # 90.5% của requests < 200ms
        "500.0": 97.2,    # 97.2% < 500ms  
        "1000.0": 99.1    # 99.1% < 1000ms
      }
    }
  }
}

cardinality - Đếm Distinct ​

bash
GET /logs/_search
{
  "size": 0,
  "aggs": {
    "unique_users": {
      "cardinality": {
        "field": "user_id",
        "precision_threshold": 10000   # Tradeoff: memory vs accuracy
      }
    },
    "unique_ips": {
      "cardinality": {
        "field": "ip_address"
      }
    }
  }
}
# Kết quả ước tính (HyperLogLog algorithm), error rate ~0.5-3%
# precision_threshold cao hơn = chính xác hơn = tốn nhiều RAM hơn

top_hits - Lấy Document Mẫu ​

bash
GET /orders/_search
{
  "size": 0,
  "aggs": {
    "top_recent_orders": {
      "top_hits": {
        "size": 3,
        "_source": ["order_id", "customer_name", "amount"],
        "sort": [{ "created_at": "desc" }]
      }
    }
  }
}

Scripted Metric Aggregation ​

bash
GET /orders/_search
{
  "size": 0,
  "aggs": {
    "profit_metric": {
      "scripted_metric": {
        "init_script": "state.total_revenue = 0; state.total_cost = 0",
        "map_script": """
          state.total_revenue += doc['revenue'].value;
          state.total_cost += doc['cost'].value;
        """,
        "combine_script": """
          return ['revenue': state.total_revenue, 'cost': state.total_cost]
        """,
        "reduce_script": """
          double total_revenue = 0;
          double total_cost = 0;
          for (state in states) {
            total_revenue += state.revenue;
            total_cost += state.cost;
          }
          return (total_revenue - total_cost) / total_revenue * 100;
        """
      }
    }
  }
}
# Tính profit margin theo Painless script

9.4 Bucket Aggregations ​

Bucket aggregations nhóm documents vào các "bucket":

terms - Nhóm Theo Giá Trị ​

bash
GET /products/_search
{
  "size": 0,
  "aggs": {
    "by_category": {
      "terms": {
        "field": "category.keyword",
        "size": 10,              # Số buckets trả về (default 10)
        "order": { "_count": "desc" }   # Sắp xếp theo số documents
      }
    }
  }
}

# Response:
{
  "aggregations": {
    "by_category": {
      "doc_count_error_upper_bound": 0,    # Error bound với distributed computing
      "sum_other_doc_count": 150,          # Documents không nằm trong top 10
      "buckets": [
        { "key": "smartphones", "doc_count": 450 },
        { "key": "laptops", "doc_count": 280 },
        { "key": "tablets", "doc_count": 120 },
        ...
      ]
    }
  }
}

Lưu ý quan trọng về terms aggregation:

  • terms không trả về exact count trên multi-shard clusters
  • Mỗi shard trả về top N của mình, kết hợp có thể miss documents
  • Tăng shard_size để chính xác hơn (default: size * 1.5 + 10)
bash
{
  "terms": {
    "field": "category.keyword",
    "size": 10,
    "shard_size": 100   # Mỗi shard trả về 100, combine để lấy top 10
  }
}

rare_terms - Tìm Giá Trị Hiếm ​

bash
GET /logs/_search
{
  "size": 0,
  "aggs": {
    "rare_error_codes": {
      "rare_terms": {
        "field": "error_code",
        "max_doc_count": 5    # Chỉ buckets có <= 5 documents
      }
    }
  }
}

date_histogram - Nhóm Theo Thời Gian ​

bash
GET /orders/_search
{
  "size": 0,
  "aggs": {
    "orders_over_time": {
      "date_histogram": {
        "field": "created_at",
        "calendar_interval": "day",    # 1d, 1w, 1M, 1q, 1y
        "time_zone": "Asia/Ho_Chi_Minh",
        "min_doc_count": 0,            # Hiển thị cả ngày không có đơn
        "extended_bounds": {           # Force range kể cả không có data
          "min": "2024-01-01",
          "max": "2024-12-31"
        },
        "format": "yyyy-MM-dd"         # Format key
      }
    }
  }
}

# Response:
{
  "aggregations": {
    "orders_over_time": {
      "buckets": [
        { "key_as_string": "2024-01-01", "key": 1704067200000, "doc_count": 45 },
        { "key_as_string": "2024-01-02", "key": 1704153600000, "doc_count": 67 },
        { "key_as_string": "2024-01-03", "key": 1704240000000, "doc_count": 0 },
        ...
      ]
    }
  }
}

Fixed interval thay vì calendar:

bash
{
  "date_histogram": {
    "field": "timestamp",
    "fixed_interval": "1h",     # 1h, 30m, 15m, 1m, etc.
    "offset": "+8h"             # Shift bucket boundaries
  }
}

histogram - Nhóm Theo Khoảng Số ​

bash
GET /products/_search
{
  "size": 0,
  "aggs": {
    "price_distribution": {
      "histogram": {
        "field": "price",
        "interval": 500000,      # 0-500k, 500k-1M, 1M-1.5M...
        "min_doc_count": 1
      }
    }
  }
}

# Response:
{
  "aggregations": {
    "price_distribution": {
      "buckets": [
        { "key": 0.0, "doc_count": 150 },       # 0-500k
        { "key": 500000.0, "doc_count": 280 },   # 500k-1M
        { "key": 1000000.0, "doc_count": 350 },  # 1M-1.5M
        { "key": 1500000.0, "doc_count": 200 },  # 1.5M-2M
        { "key": 2000000.0, "doc_count": 120 }   # 2M-2.5M
      ]
    }
  }
}

range - Khoảng Tùy Chỉnh ​

bash
GET /products/_search
{
  "size": 0,
  "aggs": {
    "price_ranges": {
      "range": {
        "field": "price",
        "ranges": [
          { "key": "Dưới 1 triệu", "to": 1000000 },
          { "key": "1-5 triệu", "from": 1000000, "to": 5000000 },
          { "key": "5-10 triệu", "from": 5000000, "to": 10000000 },
          { "key": "10-20 triệu", "from": 10000000, "to": 20000000 },
          { "key": "Trên 20 triệu", "from": 20000000 }
        ]
      }
    }
  }
}

date_range ​

bash
GET /orders/_search
{
  "size": 0,
  "aggs": {
    "order_periods": {
      "date_range": {
        "field": "created_at",
        "time_zone": "Asia/Ho_Chi_Minh",
        "format": "yyyy-MM-dd",
        "ranges": [
          { "key": "Hôm qua", "from": "now-2d/d", "to": "now-1d/d" },
          { "key": "Hôm nay", "from": "now/d", "to": "now" },
          { "key": "Tháng này", "from": "now/M", "to": "now" },
          { "key": "Tháng trước", "from": "now-1M/M", "to": "now/M" }
        ]
      }
    }
  }
}

filters - Multiple Named Filters ​

bash
GET /logs/_search
{
  "size": 0,
  "aggs": {
    "log_levels": {
      "filters": {
        "filters": {
          "errors": { "term": { "level": "ERROR" } },
          "warnings": { "term": { "level": "WARN" } },
          "infos": { "term": { "level": "INFO" } },
          "criticals": { "match": { "message": "critical fatal" } }
        }
      }
    }
  }
}

# Response:
{
  "aggregations": {
    "log_levels": {
      "buckets": {
        "errors": { "doc_count": 45 },
        "warnings": { "doc_count": 230 },
        "infos": { "doc_count": 15420 },
        "criticals": { "doc_count": 12 }
      }
    }
  }
}

geo_distance - Nhóm Theo Khoảng Cách Địa Lý ​

bash
GET /restaurants/_search
{
  "size": 0,
  "aggs": {
    "distance_from_center": {
      "geo_distance": {
        "field": "location",
        "origin": { "lat": 10.7769, "lon": 106.7009 },  # Trung tâm TPHCM
        "unit": "km",
        "ranges": [
          { "key": "< 1km", "to": 1 },
          { "key": "1-3km", "from": 1, "to": 3 },
          { "key": "3-5km", "from": 3, "to": 5 },
          { "key": "> 5km", "from": 5 }
        ]
      }
    }
  }
}

significant_terms - Thuật Ngữ Đặc Trưng ​

bash
GET /articles/_search
{
  "query": {
    "term": { "category": "technology" }
  },
  "size": 0,
  "aggs": {
    "significant_tech_terms": {
      "significant_terms": {
        "field": "content"
      }
    }
  }
}
# Tìm các từ đặc trưng cho articles trong category technology
# so với toàn bộ corpus

nested Aggregation ​

bash
# Với dữ liệu nested (review trong product):
GET /products/_search
{
  "size": 0,
  "aggs": {
    "reviews": {
      "nested": {
        "path": "reviews"
      },
      "aggs": {
        "avg_rating": {
          "avg": { "field": "reviews.rating" }
        },
        "review_distribution": {
          "terms": { "field": "reviews.rating" }
        }
      }
    }
  }
}

9.5 Sub-Aggregations (Lồng Aggregation) ​

Đây là tính năng mạnh nhất của aggregation - nested aggregations:

bash
GET /orders/_search
{
  "size": 0,
  "aggs": {
    "orders_by_month": {
      "date_histogram": {
        "field": "created_at",
        "calendar_interval": "month",
        "format": "yyyy-MM"
      },
      "aggs": {                          # Sub-aggregation
        "revenue_per_month": {
          "sum": { "field": "amount" }
        },
        "avg_order_value": {
          "avg": { "field": "amount" }
        },
        "top_categories": {
          "terms": {
            "field": "category.keyword",
            "size": 3
          },
          "aggs": {                      # Sub-sub-aggregation!
            "category_revenue": {
              "sum": { "field": "amount" }
            }
          }
        }
      }
    }
  }
}

# Response structure:
{
  "aggregations": {
    "orders_by_month": {
      "buckets": [
        {
          "key_as_string": "2024-01",
          "doc_count": 1250,
          "revenue_per_month": { "value": 25000000 },
          "avg_order_value": { "value": 20000 },
          "top_categories": {
            "buckets": [
              {
                "key": "smartphones",
                "doc_count": 450,
                "category_revenue": { "value": 10000000 }
              },
              {
                "key": "laptops",
                "doc_count": 280,
                "category_revenue": { "value": 8500000 }
              }
            ]
          }
        }
      ]
    }
  }
}

9.6 Pipeline Aggregations ​

Pipeline aggregations tính toán trên kết quả của aggregation khác:

avg_bucket, sum_bucket, min_bucket, max_bucket ​

bash
GET /orders/_search
{
  "size": 0,
  "aggs": {
    "monthly_revenue": {
      "date_histogram": {
        "field": "created_at",
        "calendar_interval": "month"
      },
      "aggs": {
        "revenue": {
          "sum": { "field": "amount" }
        }
      }
    },
    "avg_monthly_revenue": {
      "avg_bucket": {
        "buckets_path": "monthly_revenue>revenue"  # Parent aggregation path
      }
    },
    "max_monthly_revenue": {
      "max_bucket": {
        "buckets_path": "monthly_revenue>revenue"
      }
    }
  }
}

# "monthly_revenue>revenue" = "trong monthly_revenue, lấy revenue metric"

derivative - Tốc Độ Thay Đổi ​

bash
GET /orders/_search
{
  "size": 0,
  "aggs": {
    "sales_per_week": {
      "date_histogram": {
        "field": "created_at",
        "calendar_interval": "week"
      },
      "aggs": {
        "total_sales": {
          "sum": { "field": "amount" }
        },
        "sales_growth": {   # Derivative: week-over-week growth
          "derivative": {
            "buckets_path": "total_sales"
          }
        }
      }
    }
  }
}

# Response:
# Week 1: total_sales = 5M, sales_growth = null (no previous)
# Week 2: total_sales = 6M, sales_growth = +1M
# Week 3: total_sales = 5.5M, sales_growth = -500K (decline!)

cumulative_sum - Tổng Tích Lũy ​

bash
GET /orders/_search
{
  "size": 0,
  "aggs": {
    "daily_sales": {
      "date_histogram": {
        "field": "created_at",
        "calendar_interval": "day",
        "format": "yyyy-MM-dd"
      },
      "aggs": {
        "daily_revenue": {
          "sum": { "field": "amount" }
        },
        "cumulative_revenue": {
          "cumulative_sum": {
            "buckets_path": "daily_revenue"
          }
        }
      }
    }
  }
}

# Response:
# Day 1: daily = 500K, cumulative = 500K
# Day 2: daily = 750K, cumulative = 1.25M
# Day 3: daily = 600K, cumulative = 1.85M
# ...Biểu đồ cummulative revenue!

moving_avg - Moving Average (Đã Deprecated) ​

bash
# moving_fn thay thế moving_avg:
GET /metrics/_search
{
  "size": 0,
  "aggs": {
    "hourly_requests": {
      "date_histogram": {
        "field": "timestamp",
        "fixed_interval": "1h"
      },
      "aggs": {
        "request_count": {
          "value_count": { "field": "request_id" }
        },
        "smooth_trend": {
          "moving_fn": {
            "buckets_path": "request_count",
            "window": 24,       # 24-hour window
            "script": "MovingFunctions.unweightedAvg(values)"
          }
        }
      }
    }
  }
}

bucket_sort - Sắp Xếp Buckets ​

bash
GET /products/_search
{
  "size": 0,
  "aggs": {
    "top_selling_categories": {
      "terms": {
        "field": "category.keyword"
      },
      "aggs": {
        "total_sales": {
          "sum": { "field": "sales_count" }
        },
        "sort_by_sales": {
          "bucket_sort": {
            "sort": [{ "total_sales": { "order": "desc" } }],
            "size": 5         # Top 5 categories
          }
        }
      }
    }
  }
}

bucket_selector - Lọc Buckets ​

bash
GET /products/_search
{
  "size": 0,
  "aggs": {
    "monthly_sales": {
      "date_histogram": {
        "field": "sold_date",
        "calendar_interval": "month"
      },
      "aggs": {
        "total_sales": {
          "sum": { "field": "amount" }
        },
        "only_high_months": {
          "bucket_selector": {
            "buckets_path": { "totalSales": "total_sales" },
            "script": "params.totalSales > 10000000"  # Chỉ tháng > 10M
          }
        }
      }
    }
  }
}

9.7 Kết Hợp Query + Aggregation ​

bash
GET /orders/_search
{
  "query": {                    # Scope: chỉ aggregation trên orders của user này
    "term": { "customer_id": "CUST001" }
  },
  "size": 0,
  "aggs": {
    "my_orders_by_status": {
      "terms": { "field": "status" }
    },
    "total_spent": {
      "sum": { "field": "amount" }
    }
  }
}

Global Aggregation - Ignore Query Scope ​

bash
GET /orders/_search
{
  "query": {
    "term": { "status": "completed" }
  },
  "size": 0,
  "aggs": {
    "completed_revenue": {
      "sum": { "field": "amount" }              # Trên completed orders
    },
    "all_orders_revenue": {
      "global": {},                              # Bỏ qua query filter
      "aggs": {
        "total": {
          "sum": { "field": "amount" }           # Trên TẤT CẢ orders
        }
      }
    }
  }
}
# Có thể tính: completed_revenue / all_revenue = completion rate

Filter Aggregation ​

bash
GET /products/_search
{
  "size": 0,
  "aggs": {
    "premium_products": {
      "filter": {
        "range": { "price": { "gte": 10000000 } }
      },
      "aggs": {
        "avg_premium_price": {
          "avg": { "field": "price" }
        }
      }
    },
    "budget_products": {
      "filter": {
        "range": { "price": { "lt": 1000000 } }
      },
      "aggs": {
        "avg_budget_price": {
          "avg": { "field": "price" }
        }
      }
    }
  }
}

9.8 Use Case Thực Tế ​

Use Case 1: E-Commerce Sales Dashboard ​

bash
# Dashboard tổng quan doanh thu
GET /orders/_search
{
  "size": 0,
  "query": {
    "range": {
      "created_at": {
        "gte": "now-30d",
        "lte": "now"
      }
    }
  },
  "aggs": {
    "total_revenue": {
      "sum": { "field": "amount" }
    },
    "total_orders": {
      "value_count": { "field": "order_id" }
    },
    "avg_order_value": {
      "avg": { "field": "amount" }
    },
    "unique_customers": {
      "cardinality": { "field": "customer_id" }
    },
    "revenue_by_day": {
      "date_histogram": {
        "field": "created_at",
        "calendar_interval": "day",
        "format": "yyyy-MM-dd",
        "time_zone": "Asia/Ho_Chi_Minh"
      },
      "aggs": {
        "daily_revenue": { "sum": { "field": "amount" } },
        "daily_orders": { "value_count": { "field": "order_id" } }
      }
    },
    "revenue_by_category": {
      "terms": {
        "field": "category.keyword",
        "size": 10,
        "order": { "category_revenue": "desc" }
      },
      "aggs": {
        "category_revenue": { "sum": { "field": "amount" } },
        "avg_price": { "avg": { "field": "price" } }
      }
    },
    "payment_methods": {
      "terms": { "field": "payment_method.keyword" }
    },
    "order_status": {
      "terms": { "field": "status.keyword" }
    },
    "refund_rate": {
      "filters": {
        "filters": {
          "refunded": { "term": { "status": "refunded" } },
          "completed": { "term": { "status": "completed" } }
        }
      },
      "aggs": {
        "amount": { "sum": { "field": "amount" } }
      }
    }
  }
}

Use Case 2: Faceted Search cho E-Commerce ​

bash
# Search + Aggregation cho faceted search (filters panel)
GET /products/_search
{
  "query": {
    "bool": {
      "must": [
        { "match": { "name": "điện thoại" } }
      ],
      "filter": [
        { "term": { "category.keyword": "smartphones" } },
        { "range": { "price": { "gte": 5000000, "lte": 20000000 } } },
        { "term": { "brand.keyword": "Samsung" } }
      ]
    }
  },
  "aggs": {
    "all_brands": {
      "global": {},    # Count toàn bộ, không bị filter bởi brand filter
      "aggs": {
        "filtered": {
          "filter": {  # Tái áp dụng các filter khác (không phải brand)
            "bool": {
              "must": [
                { "match": { "name": "điện thoại" } }
              ],
              "filter": [
                { "term": { "category.keyword": "smartphones" } },
                { "range": { "price": { "gte": 5000000, "lte": 20000000 } } }
              ]
            }
          },
          "aggs": {
            "brands": {
              "terms": { "field": "brand.keyword", "size": 20 }
            }
          }
        }
      }
    },
    "price_ranges": {
      "range": {
        "field": "price",
        "ranges": [
          { "key": "Dưới 5 triệu", "to": 5000000 },
          { "key": "5-10 triệu", "from": 5000000, "to": 10000000 },
          { "key": "10-20 triệu", "from": 10000000, "to": 20000000 },
          { "key": "Trên 20 triệu", "from": 20000000 }
        ]
      }
    },
    "avg_rating": {
      "avg": { "field": "rating" }
    },
    "ram_options": {
      "terms": { "field": "specs.ram.keyword", "size": 10 }
    },
    "storage_options": {
      "terms": { "field": "specs.storage.keyword", "size": 10 }
    }
  }
}

Use Case 3: Log Analytics ​

bash
# Application performance monitoring
GET /application-logs-*/_search
{
  "size": 0,
  "query": {
    "range": {
      "@timestamp": {
        "gte": "now-1h",
        "lte": "now"
      }
    }
  },
  "aggs": {
    "error_rate": {
      "date_histogram": {
        "field": "@timestamp",
        "fixed_interval": "5m",
        "format": "HH:mm"
      },
      "aggs": {
        "total_requests": { "value_count": { "field": "request_id" } },
        "error_requests": {
          "filter": {
            "range": { "status_code": { "gte": 500 } }
          }
        },
        "error_pct": {
          "bucket_script": {
            "buckets_path": {
              "errors": "error_requests._count",
              "total": "total_requests"
            },
            "script": "params.errors / params.total * 100"
          }
        },
        "p95_latency": {
          "percentiles": {
            "field": "duration_ms",
            "percents": [95]
          }
        }
      }
    },
    "slow_endpoints": {
      "terms": {
        "field": "endpoint.keyword",
        "size": 10,
        "order": { "avg_duration": "desc" }
      },
      "aggs": {
        "avg_duration": { "avg": { "field": "duration_ms" } },
        "max_duration": { "max": { "field": "duration_ms" } },
        "error_count": {
          "filter": {
            "range": { "status_code": { "gte": 400 } }
          }
        }
      }
    },
    "status_breakdown": {
      "range": {
        "field": "status_code",
        "ranges": [
          { "key": "2xx", "from": 200, "to": 300 },
          { "key": "3xx", "from": 300, "to": 400 },
          { "key": "4xx", "from": 400, "to": 500 },
          { "key": "5xx", "from": 500, "to": 600 }
        ]
      }
    }
  }
}

Use Case 4: Time-Series Analytics với Moving Average ​

bash
# Phát hiện anomalies trong số đơn hàng
GET /orders/_search
{
  "size": 0,
  "aggs": {
    "hourly_orders": {
      "date_histogram": {
        "field": "created_at",
        "fixed_interval": "1h",
        "format": "yyyy-MM-dd HH:mm"
      },
      "aggs": {
        "order_count": {
          "value_count": { "field": "order_id" }
        },
        "moving_avg": {
          "moving_fn": {
            "buckets_path": "order_count",
            "window": 24,
            "script": "MovingFunctions.unweightedAvg(values)"
          }
        },
        "moving_std": {
          "moving_fn": {
            "buckets_path": "order_count",
            "window": 24,
            "script": "MovingFunctions.stdDev(values, MovingFunctions.unweightedAvg(values))"
          }
        },
        "upper_bound": {
          "bucket_script": {
            "buckets_path": {
              "avg": "moving_avg",
              "std": "moving_std"
            },
            "script": "params.avg + 2 * params.std"
          }
        },
        "is_anomaly": {
          "bucket_selector": {
            "buckets_path": {
              "count": "order_count",
              "upper": "upper_bound"
            },
            "script": "params.count > params.upper"
          }
        }
      }
    }
  }
}
# Tìm các giờ có số đơn bất thường cao (spike detection)

9.9 Performance Tips cho Aggregations ​

1. Sử dụng filter trước aggregation ​

bash
# CHẬM: Aggregation trên toàn bộ data
GET /orders/_search
{
  "size": 0,
  "aggs": {
    "recent_revenue": {
      "filter": { "range": { "created_at": { "gte": "now-7d" } } },
      "aggs": {
        "total": { "sum": { "field": "amount" } }
      }
    }
  }
}

# NHANH: Query filter (cached) trước aggregation
GET /orders/_search
{
  "size": 0,
  "query": {
    "range": { "created_at": { "gte": "now-7d" } }
  },
  "aggs": {
    "total_revenue": {
      "sum": { "field": "amount" }
    }
  }
}

2. Giới hạn size trong terms ​

bash
# Không nên lấy quá nhiều buckets
{
  "terms": {
    "field": "product_id",
    "size": 10      # Default 10, đừng set 1000+
  }
}

3. Dùng doc_values ​

Fields dùng trong aggregation phải có doc_values: true (default cho non-analyzed fields):

bash
PUT /orders
{
  "mappings": {
    "properties": {
      "status": {
        "type": "keyword",
        "doc_values": true    # Default: true cho keyword
      },
      "amount": {
        "type": "double",
        "doc_values": true    # Default: true cho numeric fields
      }
    }
  }
}

4. eager_global_ordinals cho high-cardinality terms ​

bash
PUT /products
{
  "mappings": {
    "properties": {
      "category": {
        "type": "keyword",
        "eager_global_ordinals": true    # Pre-compute ordinals at index time
      }
    }
  }
}

Tóm Tắt Chương 9 ​

LoạiDùng khiVí dụ
MetricTính toán số liệuavg, sum, min/max, percentiles
BucketNhóm documentsterms, date_histogram, range, geo_distance
PipelineAggregation trên aggregationderivative, cumulative_sum, bucket_selector

Khi Nào Dùng Gì ​

  • Sales dashboard: date_histogram + sum per period
  • Faceted search: terms + global agg để giữ counts độc lập
  • Performance monitoring: percentiles, date_histogram với moving_fn
  • Anomaly detection: moving_fn + bucket_selector
  • Inventory analysis: terms + sub-aggs với stats

Bước Tiếp Theo ​

→ Chương 10: Performance Optimization


Chương 10: Performance Optimization - Tối Ưu Hiệu Năng ​

10.1 Tại Sao Cần Tối Ưu? ​

Elasticsearch có thể chậm khi:

  • Cluster không được cấu hình phù hợp
  • Queries không hiệu quả
  • Mapping sai kiểu dữ liệu
  • Shard không được phân bổ hợp lý
  • Heap memory không đủ

Mục tiêu: Search < 100ms, Indexing > 10,000 docs/giây


10.2 Shard Strategy ​

Quy Tắc Kích Thước Shard ​

Shard Size:     10GB - 50GB lý tưởng
                Tối đa 65GB (trên production)
Shard Count:    Bằng số CPU core (hoặc gấp đôi)
Total Shards:   < 1000 per node (rule of thumb)
Heap Usage:     ~30MB RAM per shard

Tính toán số shard:

Số shard = ceil(Total Data Size / Target Shard Size)

Ví dụ:
- Dữ liệu: 500GB
- Target shard size: 25GB
- Số primary shard = ceil(500/25) = 20 shards
- Với 3 replicas: 20 * 4 = 80 total shards
- Với 5 nodes: 80/5 = 16 shards per node (OK!)

Shard Splitting vs Reindex ​

bash
# Nếu cần tách shard (khi data grow):
POST /my-index/_split/my-index-split
{
  "settings": {
    "index.number_of_shards": 6    # Phải là bội số (gấp đôi, ba...) của số cũ
  }
}

# Nếu cần thu gọn shard:
POST /my-index/_shrink/my-index-small
{
  "settings": {
    "index.number_of_shards": 1,
    "index.number_of_replicas": 1
  }
}
# Cần move tất cả shards về cùng 1 node trước:
PUT /my-index/_settings
{
  "index.routing.allocation.require._name": "node-1"
}

Oversharding - Lỗi Phổ Biến ​

bash
# SAI: 50 shards cho 1GB data
PUT /small-index
{
  "settings": {
    "number_of_shards": 50    # WAY TOO MANY for small data!
  }
}

# ĐÚNG:
PUT /small-index
{
  "settings": {
    "number_of_shards": 1,    # 1 shard cho data < 20GB
    "number_of_replicas": 1
  }
}

Forcemerge cho Read-only Indices ​

bash
# Sau khi index xong, merge segments để tăng tốc search:
POST /my-index/_forcemerge?max_num_segments=1

# Chỉ làm trên indices KHÔNG CÒN WRITE nữa!
# (Time-series: tháng cũ, log indices cũ)

10.3 Mapping Optimization ​

Tắt _source Khi Không Cần ​

bash
# Nếu chỉ cần search, không cần fetch original document:
PUT /analytics-events
{
  "mappings": {
    "_source": { "enabled": false }   # Tiết kiệm ~50% disk space!
    # CẢNH BÁO: Không thể update, reindex, debug sau này!
  }
}

# Thay vào đó: Dùng source filtering
GET /products/_search
{
  "_source": ["name", "price"],   # Chỉ lấy fields cần thiết
  "query": { "match_all": {} }
}

Chọn Đúng Data Type ​

bash
# CHẬM: Lưu IP dạng text
{ "ip_address": "192.168.1.1" }   # keyword type

# NHANH: Dùng ip type
{
  "mappings": {
    "properties": {
      "ip_address": { "type": "ip" }    # Native IP type, CIDR queries, range queries
    }
  }
}

# CHẬM: Date dạng string
{ "created_at": "2024-01-15 10:30:00" }   # text/keyword

# NHANH: Date type
{
  "created_at": {
    "type": "date",
    "format": "yyyy-MM-dd HH:mm:ss||epoch_millis"
  }
}

Tắt index Cho Fields Chỉ Dùng cho Aggregation/Sort ​

bash
PUT /products
{
  "mappings": {
    "properties": {
      "name": { "type": "text" },       # index: true (default)
      "price": { "type": "double" },    # index: true - Có thể range query
      "internal_cost": {
        "type": "double",
        "index": false    # Không tìm kiếm được, nhưng vẫn aggregation được
      },
      "metadata": {
        "type": "object",
        "enabled": false    # Toàn bộ object không được index (lưu nhưng không search)
      }
    }
  }
}

doc_values - Khi Nào Tắt ​

bash
PUT /logs
{
  "mappings": {
    "properties": {
      "message": {
        "type": "text",
        "doc_values": false     # text fields không có doc_values mặc định
      },
      "log_level": {
        "type": "keyword",
        "doc_values": false     # Tắt nếu KHÔNG BAO GIỜ sort/agg trên field này
                                # Tiết kiệm disk
      }
    }
  }
}

norms - Tắt Cho Fields Không Cần Relevance Scoring ​

bash
PUT /products
{
  "mappings": {
    "properties": {
      "product_code": {
        "type": "text",
        "norms": false        # Product code: exact match, không cần relevance
      },
      "description": {
        "type": "text",
        "norms": true         # Description: cần relevance scoring (default)
      }
    }
  }
}

10.4 Indexing Performance ​

Tăng Tốc Bulk Indexing ​

bash
# 1. Tắt replicas trong khi indexing
PUT /my-index/_settings
{
  "index": {
    "number_of_replicas": 0,    # Tắt replicas
    "refresh_interval": "-1"    # Tắt auto refresh
  }
}

# 2. Bulk index
POST /my-index/_bulk
{ "index": {} }
{ "field1": "value1" }
{ "index": {} }
{ "field2": "value2" }
...

# 3. Sau khi done, restore settings
PUT /my-index/_settings
{
  "index": {
    "number_of_replicas": 1,
    "refresh_interval": "1s"
  }
}

# 4. Force merge
POST /my-index/_forcemerge?max_num_segments=5

Bulk Size Optimization ​

bash
# Thử với 5MB-15MB per batch:
POST /_bulk    # Request body ~10MB
{ "index": { "_index": "products" } }
{ ... }
...
# 1000-5000 documents per request thường tốt nhất
# Benchmark với cluster của bạn

Refresh Interval ​

bash
# Default: 1 giây (gần real-time)
# Tăng để giảm write load:
PUT /high-write-index/_settings
{
  "index.refresh_interval": "30s"    # Chỉ near-real-time
}

# Tắt hoàn toàn (dùng khi initial load):
PUT /initial-load-index/_settings
{
  "index.refresh_interval": "-1"
}

Translog Settings ​

bash
PUT /my-index/_settings
{
  "index": {
    "translog": {
      "sync_interval": "5s",         # Default: 5s (durability vs performance)
      "durability": "async",         # async: faster, slight data loss risk
      "flush_threshold_size": "1gb"  # Flush translog when > 1GB
    }
  }
}

10.5 Query Optimization ​

Filter vs Query Context ​

bash
# CHẬM: Range trong query context (requires scoring)
GET /orders/_search
{
  "query": {
    "bool": {
      "must": [
        { "range": { "created_at": { "gte": "now-7d" } } },  # Must-score
        { "match": { "product_name": "iPhone" } }
      ]
    }
  }
}

# NHANH: Range trong filter context (cached, no scoring)
GET /orders/_search
{
  "query": {
    "bool": {
      "must": [
        { "match": { "product_name": "iPhone" } }   # Scoring needed
      ],
      "filter": [
        { "range": { "created_at": { "gte": "now-7d" } } }   # Cached!
      ]
    }
  }
}

Avoid Wildcards và Leading Wildcards ​

bash
# CHẬM: Leading wildcard - phải scan toàn bộ inverted index
GET /products/_search
{
  "query": {
    "wildcard": { "sku": "*123" }   # Không scan được index!
  }
}

# NHANH: Trailing wildcard (có thể dùng prefix query)
GET /products/_search
{
  "query": {
    "prefix": { "sku": "SKU-123" }  # Nhanh hơn vì prefix được index
  }
}

# TỐT NHẤT: Dùng edge_ngram để hỗ trợ partial match

Pagination ​

bash
# CHẬM: Deep pagination với from/size
GET /products/_search
{
  "from": 10000,    # ES phải fetch 10,000 + size documents, sort, discard
  "size": 10,
  "query": { "match_all": {} }
}

# NHANH: Search After
GET /products/_search
{
  "size": 10,
  "sort": [
    { "created_at": "desc" },
    { "_id": "asc" }          # Tiebreaker phải là unique!
  ],
  "search_after": ["2024-01-15", "doc-id-12345"]   # Từ trang trước
}

# TỐT NHẤT cho scan toàn bộ: PIT (Point in Time)
POST /products/_pit?keep_alive=5m
# Response: { "id": "pit-id-abc123" }

GET /products/_search
{
  "size": 1000,
  "pit": { "id": "pit-id-abc123", "keep_alive": "1m" },
  "sort": [{ "_shard_doc": "asc" }]  # Efficient sort with PIT
}

_count Thay Vì Count với Agg ​

bash
# Nếu chỉ cần đếm:
GET /orders/_count
{
  "query": {
    "term": { "status": "completed" }
  }
}
# Nhanh hơn nhiều so với search với size=0 + cardinality agg!

Query Profiling ​

bash
GET /products/_search
{
  "profile": true,    # Chi tiết timing của từng query component
  "query": {
    "bool": {
      "must": [
        { "match": { "name": "iPhone" } }
      ],
      "filter": [
        { "term": { "category": "smartphones" } }
      ]
    }
  }
}

# Response bao gồm:
{
  "profile": {
    "shards": [
      {
        "id": "[node1][products][0]",
        "searches": [
          {
            "query": [
              {
                "type": "BooleanQuery",
                "description": "+name:iphone #category:smartphones",
                "time_in_nanos": 1234567,
                "breakdown": {
                  "score": 45000,
                  "build_scorer": 125000,
                  "match": 890000
                }
              }
            ],
            "rewrite_time": 12345,
            "collector": [...]
          }
        ]
      }
    ]
  }
}

10.6 Caching ​

Node Query Cache (Filter Cache) ​

bash
# Tự động cache filter context queries
# Cache key = query DSL
GET /orders/_search
{
  "query": {
    "bool": {
      "filter": [
        { "term": { "status": "active" } }   # Được cache sau lần đầu!
      ]
    }
  }
}

# Xem cache stats:
GET /_stats/query_cache

# Cấu hình:
# elasticsearch.yml
# indices.queries.cache.size: 10%   # Default 10% heap

Shard Request Cache ​

bash
# Cache kết quả aggregation (không cache hits)
GET /orders/_search
{
  "size": 0,           # QUAN TRỌNG: size=0 mới được cache!
  "request_cache": true,
  "aggs": {
    "total": { "sum": { "field": "amount" } }
  }
}

# Xem cache:
GET /_stats/request_cache

# Cache bị invalidate khi segment thay đổi (new data, merge, refresh)

Fielddata Cache ​

bash
# Dùng cho text fields trong aggregation/sort (không nên dùng!)
# Text fields không có doc_values → phải load vào heap

# Cấu hình limit:
PUT /products/_settings
{
  "index.fielddata.cache": "none"    # Tắt fielddata (bắt lỗi sớm)
}

# Hoặc global limit:
# elasticsearch.yml
# indices.fielddata.cache.size: 20%

# Xem fielddata usage:
GET /_cat/fielddata?v

# Clear cache:
POST /products/_cache/clear?fielddata=true

10.7 Index Lifecycle Management (ILM) ​

ILM tự động quản lý vòng đời index:

ILM Policy ​

bash
PUT /_ilm/policy/log-policy
{
  "policy": {
    "phases": {
      "hot": {
        "min_age": "0ms",
        "actions": {
          "rollover": {
            "max_primary_shard_size": "50GB",
            "max_age": "7d",
            "max_docs": 10000000
          },
          "set_priority": {
            "priority": 100    # Hot indices: highest priority
          }
        }
      },
      "warm": {
        "min_age": "30d",    # 30 ngày sau khi rollover
        "actions": {
          "shrink": {
            "number_of_shards": 1    # Giảm số shard
          },
          "forcemerge": {
            "max_num_segments": 1    # Merge tất cả segments
          },
          "allocate": {
            "number_of_replicas": 1  # Giảm replicas
          },
          "set_priority": {
            "priority": 50
          }
        }
      },
      "cold": {
        "min_age": "90d",
        "actions": {
          "freeze": {},            # Freeze index (đọc từ disk, không keep in memory)
          "allocate": {
            "number_of_replicas": 0,  # Không có replicas
            "require": {
              "data": "cold"     # Di chuyển sang cold tier nodes
            }
          },
          "set_priority": {
            "priority": 0
          }
        }
      },
      "delete": {
        "min_age": "365d",    # Xóa sau 1 năm
        "actions": {
          "delete": {}
        }
      }
    }
  }
}

Index Template với ILM ​

bash
PUT /_index_template/logs-template
{
  "index_patterns": ["logs-*"],
  "template": {
    "settings": {
      "number_of_shards": 1,
      "number_of_replicas": 1,
      "index.lifecycle.name": "log-policy",           # Gắn ILM policy
      "index.lifecycle.rollover_alias": "logs-active" # Alias cho current index
    }
  }
}

# Bootstrap first index:
PUT /logs-000001
{
  "aliases": {
    "logs-active": {
      "is_write_index": true    # Chỉ viết vào index này
    }
  }
}

# Write via alias:
POST /logs-active/_doc
{ "message": "log message", "@timestamp": "2024-01-15T10:30:00Z" }

Rollover Thủ Công (Khi cần) ​

bash
POST /logs-active/_rollover
{
  "conditions": {
    "max_age": "7d",
    "max_docs": 5000000
  }
}
# Tạo logs-000002 nếu đủ điều kiện

10.8 Slow Log ​

bash
# Cấu hình slow log để identify slow queries:
PUT /products/_settings
{
  "index.search.slowlog.threshold.query.warn": "2s",
  "index.search.slowlog.threshold.query.info": "1s",
  "index.search.slowlog.threshold.query.debug": "500ms",
  "index.search.slowlog.threshold.fetch.warn": "1s",
  "index.search.slowlog.level": "info"
}

PUT /products/_settings
{
  "index.indexing.slowlog.threshold.index.warn": "5s",
  "index.indexing.slowlog.threshold.index.info": "2s",
  "index.indexing.slowlog.level": "info"
}

# Log sẽ xuất hiện trong: logs/elasticsearch_index_search_slowlog.json

10.9 Hardware và JVM Tuning ​

JVM Heap Settings ​

bash
# jvm.options:
# Rules:
# 1. Tối đa 50% RAM vật lý
# 2. KHÔNG vượt quá 31GB (compressed oops threshold)
# 3. Xms = Xmx (tránh GC overhead)

-Xms16g      # Min heap
-Xmx16g      # Max heap (= Min)

# Hệ thống 32GB RAM:
# 16GB heap cho ES, 16GB còn lại cho OS file cache (Lucene dùng!)

Swap ​

bash
# Tắt swap hoàn toàn:
swapoff -a
# Và trong /etc/fstab: comment out swap lines

# Hoặc lock memory:
# elasticsearch.yml
bootstrap.memory_lock: true

# Verify:
GET /_nodes/stats/process?filter_path=**.mlockall

Disk ​

bash
# SSD: Intel NVMe hoặc tương đương
# RAID: RAID-0 (ES có replication)
# Tránh: NFS, network storage cho data nodes

# elasticsearch.yml
path.data:
  - /data1/elasticsearch    # Multiple data paths
  - /data2/elasticsearch    # Tận dụng multiple disks

# Disk-based allocation:
cluster.routing.allocation.disk.watermark.low: "85%"    # Stop allocating
cluster.routing.allocation.disk.watermark.high: "90%"   # Start moving
cluster.routing.allocation.disk.watermark.flood_stage: "95%"  # Read-only!

10.10 Concurrent Search Optimization ​

batched_reduce_size ​

bash
GET /products/_search
{
  "batched_reduce_size": 512,    # Reduce từng 512 shards một (tiết kiệm memory)
  "query": { "match_all": {} }
}

Adaptive Replica Selection ​

bash
# elasticsearch.yml
cluster.routing.use_adaptive_replica_selection: true   # Default: true
# ES tự chọn shard response nhanh nhất

Search Thread Pool ​

bash
# elasticsearch.yml
thread_pool.search.size: 14      # Thường: (CPU_count * 3) / 2 + 1
thread_pool.search.queue_size: 1000

10.11 Monitoring Performance ​

Cluster Health và Stats ​

bash
# Xem cluster health:
GET /_cluster/health?pretty

# Node stats:
GET /_nodes/stats?filter_path=nodes.*.indices.search,nodes.*.indices.indexing

# Hot threads:
GET /_nodes/hot_threads

# Task API:
GET /_tasks?actions=*search*&detailed=true

# Pending tasks:
GET /_cluster/pending_tasks

Cat API ​

bash
GET /_cat/health?v              # Cluster health
GET /_cat/nodes?v&h=name,heap.percent,cpu,load_1m,master
GET /_cat/indices?v&h=index,docs.count,store.size&s=store.size:desc
GET /_cat/shards?v&h=index,shard,prirep,state,docs,store,node
GET /_cat/segments?v            # Segment count per shard
GET /_cat/allocation?v          # Disk usage per node
GET /_cat/recovery?v&active_only=true  # Active recoveries

Tóm Tắt Performance Optimization ​

AreaQuick Win
MappingDùng đúng type, tắt index/norms/fielddata khi không cần
Sharding10-50GB per shard, không over-shard
IndexingTắt replicas + refresh khi bulk load
QueriesDùng filter context, tránh leading wildcards
PaginationDùng search_after thay vì deep from/size
CachingTận dụng filter cache, avoid near-realtime khi không cần
ILMTự động rollover, warm/cold tiers
JVM50% RAM, max 31GB, Xms=Xmx
DiskSSD, RAID-0, tắt swap

Bước Tiếp Theo ​

→ Chương 11: Advanced Features


Chương 11: Advanced Features - Tính Năng Nâng Cao ​

11.1 Completion Suggester - Autocomplete Đỉnh cao ​

Completion suggester là cách tốt nhất cho autocomplete, nhanh hơn nhiều so với prefix query:

Mapping cho Completion ​

bash
PUT /products
{
  "mappings": {
    "properties": {
      "name": { "type": "text" },
      "suggest": {
        "type": "completion",
        "analyzer": "simple",
        "search_analyzer": "simple",
        "max_input_length": 50,
        "contexts": [          # Optional: Context-aware suggestions
          {
            "name": "category",
            "type": "category"
          }
        ]
      }
    }
  }
}

Indexing với Suggest Field ​

bash
PUT /products/_doc/1
{
  "name": "iPhone 15 Pro Max 256GB",
  "suggest": {
    "input": [
      "iPhone 15 Pro Max",
      "iPhone 15",
      "iPhone Pro Max",
      "điện thoại Apple"
    ],
    "weight": 90     # Sắp xếp ưu tiên (cao hơn = ưu tiên hơn)
  }
}

PUT /products/_doc/2
{
  "name": "Samsung Galaxy S24 Ultra",
  "suggest": {
    "input": [
      "Samsung Galaxy S24 Ultra",
      "Samsung S24",
      "Galaxy Ultra"
    ],
    "weight": 85
  }
}

# Context-aware:
PUT /products/_doc/3
{
  "name": "MacBook Pro 14 M3",
  "suggest": {
    "input": ["MacBook Pro", "MacBook M3"],
    "weight": 80,
    "contexts": {
      "category": ["laptop", "apple"]
    }
  }
}

Querying Completion Suggester ​

bash
POST /products/_search
{
  "suggest": {
    "product_suggest": {
      "prefix": "iph",      # User đang gõ
      "completion": {
        "field": "suggest",
        "fuzzy": {
          "fuzziness": 1    # Cho phép 1 lỗi đánh máy
        },
        "size": 5           # Số gợi ý trả về
      }
    }
  }
}

# Context-aware suggestion:
POST /products/_search
{
  "suggest": {
    "product_suggest": {
      "prefix": "mac",
      "completion": {
        "field": "suggest",
        "size": 5,
        "contexts": {
          "category": ["laptop"]    # Chỉ gợi ý laptops
        }
      }
    }
  }
}

# Response:
{
  "suggest": {
    "product_suggest": [
      {
        "text": "iph",
        "options": [
          {
            "text": "iPhone 15 Pro Max",
            "_score": 90,
            "_id": "1",
            "_source": { "name": "iPhone 15 Pro Max 256GB" }
          }
        ]
      }
    ]
  }
}

11.2 Term Suggester - Spell Correction ​

Gợi ý khi người dùng gõ sai chính tả:

bash
POST /products/_search
{
  "suggest": {
    "spelling_correction": {
      "text": "samsng gaalaxy",   # Sai chính tả
      "term": {
        "field": "name",
        "suggest_mode": "missing",     # only, missing, always
        "min_word_length": 3,
        "min_doc_freq": 1,
        "max_edits": 2,                # Edit distance
        "sort": "score"                # score | frequency
      }
    }
  }
}

# Response:
{
  "suggest": {
    "spelling_correction": [
      {
        "text": "samsng",
        "options": [
          { "text": "samsung", "score": 0.75, "freq": 450 }
        ]
      },
      {
        "text": "gaalaxy",
        "options": [
          { "text": "galaxy", "score": 0.8, "freq": 380 }
        ]
      }
    ]
  }
}

Phrase Suggester ​

bash
POST /articles/_search
{
  "suggest": {
    "phrase_suggestion": {
      "text": "điên thoại di động",   # "điên" thay vì "điện"
      "phrase": {
        "field": "content",
        "max_errors": 2,
        "confidence": 1.0,
        "highlight": {
          "pre_tag": "<em>",
          "post_tag": "</em>"
        }
      }
    }
  }
}

Thay vì tìm documents matching query, percolator tìm queries matching document:

bash
# Use case: Notification system
# User đăng ký: "Thông báo khi có iPhone 15 dưới 25 triệu"

# 1. Index percolator (lưu queries)
PUT /price-alerts
{
  "mappings": {
    "properties": {
      "query": {
        "type": "percolator"   # Lưu query DSL
      },
      "user_id": { "type": "keyword" },
      "product_query": {
        "type": "text",
        "analyzer": "standard"
      },
      "max_price": { "type": "double" }
    }
  }
}

# 2. Index user alerts (as percolator queries)
PUT /price-alerts/_doc/alert-001
{
  "user_id": "user123",
  "query": {
    "bool": {
      "must": [
        { "match": { "name": "iPhone 15" } }
      ],
      "filter": [
        { "range": { "price": { "lte": 25000000 } } }
      ]
    }
  }
}

PUT /price-alerts/_doc/alert-002
{
  "user_id": "user456",
  "query": {
    "bool": {
      "must": [
        { "match": { "name": "MacBook Pro" } }
      ],
      "filter": [
        { "range": { "price": { "lte": 40000000 } } }
      ]
    }
  }
}

# 3. Khi có sản phẩm mới, tìm users cần thông báo:
GET /price-alerts/_search
{
  "query": {
    "percolate": {
      "field": "query",
      "document": {               # Sản phẩm mới vừa được update
        "name": "iPhone 15 Pro Max 256GB Xanh",
        "price": 23500000,
        "brand": "Apple",
        "category": "smartphones"
      }
    }
  }
}

# Response: alert-001 (user123) match! → Gửi notification cho user123

Bulk Percolation (Nhiều Documents) ​

bash
GET /price-alerts/_search
{
  "query": {
    "percolate": {
      "field": "query",
      "documents": [       # Nhiều documents
        { "name": "iPhone 15", "price": 23000000 },
        { "name": "Samsung S24", "price": 18000000 }
      ]
    }
  }
}

11.4 Async Search - Tìm Kiếm Bất Đồng Bộ ​

Cho queries chạy lâu (analytics, reports):

bash
# Submit async search
POST /orders/_async_search?wait_for_completion_timeout=1s
{
  "aggs": {
    "monthly_revenue": {
      "date_histogram": {
        "field": "created_at",
        "calendar_interval": "month"
      },
      "aggs": {
        "revenue": { "sum": { "field": "amount" } }
      }
    }
  }
}

# Response ngay lập tức:
{
  "id": "async-search-id-abc123",
  "is_partial": true,      # Chưa xong
  "is_running": true,
  "start_time_in_millis": 1704067200000,
  "expiration_time_in_millis": 1704153600000,
  "response": {
    "took": 150,
    "hits": { "total": { "value": 10000 } }
  }
}

# Sau đó poll kết quả:
GET /_async_search/async-search-id-abc123
# is_running = false khi xong

# Xóa khi không cần:
DELETE /_async_search/async-search-id-abc123

11.5 Cross-Cluster Search (CCS) ​

Tìm kiếm trên nhiều clusters:

bash
# Cấu hình remote clusters:
PUT /_cluster/settings
{
  "persistent": {
    "cluster.remote": {
      "cluster-us-west": {
        "seeds": ["node1.us-west.example.com:9300"]
      },
      "cluster-eu-central": {
        "seeds": ["node1.eu-central.example.com:9300"]
      }
    }
  }
}

# Search trên nhiều clusters:
GET /logs-*,cluster-us-west:logs-*,cluster-eu-central:logs-*/_search
{
  "query": {
    "match": { "message": "error" }
  }
}

# Aggregate trên nhiều clusters:
GET /cluster-us-west:orders,cluster-eu-central:orders/_search
{
  "size": 0,
  "aggs": {
    "global_revenue": { "sum": { "field": "amount" } }
  }
}

11.6 EQL - Event Query Language ​

EQL cho phân tích security events, phát hiện sequences:

bash
# Phát hiện brute force attack:
# Sequence: nhiều lần login fail → 1 lần login success
GET /security-events/_eql/search
{
  "query": """
    sequence with maxspan=5m
      [authentication where event.outcome == "failure"] with runs=5
      [authentication where event.outcome == "success"]
  """,
  "filter": {
    "term": { "source.ip": "192.168.1.100" }
  }
}

# Tìm event theo pattern:
GET /security-events/_eql/search
{
  "query": """
    process where process.name == "cmd.exe" 
      and process.parent.name == "excel.exe"
  """
}
# Phát hiện Excel spawning cmd.exe (macro malware!)

11.7 Data Streams ​

Data Streams tối ưu cho time-series data (logs, metrics):

bash
# Tạo Index Lifecycle Policy
PUT /_ilm/policy/data-stream-policy
{
  "policy": {
    "phases": {
      "hot": {
        "actions": {
          "rollover": {
            "max_primary_shard_size": "50GB",
            "max_age": "30d"
          }
        }
      },
      "delete": {
        "min_age": "90d",
        "actions": { "delete": {} }
      }
    }
  }
}

# Tạo index template cho data stream:
PUT /_index_template/logs-template
{
  "index_patterns": ["logs-myapp-*"],
  "data_stream": {},          # Mark as data stream template
  "template": {
    "settings": {
      "index.lifecycle.name": "data-stream-policy"
    },
    "mappings": {
      "properties": {
        "@timestamp": { "type": "date" },   # REQUIRED field!
        "message": { "type": "text" },
        "level": { "type": "keyword" },
        "service": { "type": "keyword" }
      }
    }
  }
}

# Tạo data stream:
PUT /_data_stream/logs-myapp-production

# Index vào data stream:
POST /logs-myapp-production/_doc
{
  "@timestamp": "2024-01-15T10:30:00Z",
  "message": "User login successful",
  "level": "INFO",
  "service": "auth-service"
}

# Search trên data stream (transparent, search all backing indices):
GET /logs-myapp-production/_search
{
  "query": {
    "range": {
      "@timestamp": { "gte": "now-1h" }
    }
  }
}

# Xem thông tin data stream:
GET /_data_stream/logs-myapp-production

# Manual rollover:
POST /logs-myapp-production/_rollover

11.8 Transform API ​

Transform tổng hợp và transform data thành pivot tables:

bash
# Tạo transform: pivot orders thành daily revenue per product
PUT /_transform/daily-product-revenue
{
  "source": {
    "index": "orders",
    "query": {
      "range": {
        "created_at": { "gte": "now-90d" }
      }
    }
  },
  "dest": {
    "index": "revenue-by-product-daily"
  },
  "pivot": {
    "group_by": {
      "date": {
        "date_histogram": {
          "field": "created_at",
          "calendar_interval": "day"
        }
      },
      "product_id": {
        "terms": { "field": "product_id" }
      }
    },
    "aggregations": {
      "total_revenue": {
        "sum": { "field": "amount" }
      },
      "order_count": {
        "value_count": { "field": "order_id" }
      },
      "avg_price": {
        "avg": { "field": "price" }
      }
    }
  },
  "sync": {
    "time": {
      "field": "created_at",
      "delay": "60s"    # Continuously update with 60s delay
    }
  }
}

# Start transform:
POST /_transform/daily-product-revenue/_start

# Xem stats:
GET /_transform/daily-product-revenue/_stats

# Query destination index:
GET /revenue-by-product-daily/_search
{
  "query": {
    "term": { "product_id": "PROD001" }
  },
  "sort": [{ "date": "desc" }]
}

11.9 Enrich Processor (Ingest Pipeline) ​

Làm giàu documents trong quá trình indexing:

bash
# 1. Tạo source index (lookup table):
PUT /user-profiles
{
  "mappings": {
    "properties": {
      "user_id": { "type": "keyword" },
      "name": { "type": "text" },
      "tier": { "type": "keyword" },
      "location": { "type": "keyword" }
    }
  }
}

PUT /user-profiles/_doc/1
{ "user_id": "USR001", "name": "Nguyễn Văn A", "tier": "gold", "location": "TPHCM" }

# 2. Tạo enrich policy:
PUT /_enrich/policy/user-enrich-policy
{
  "match": {
    "indices": "user-profiles",
    "match_field": "user_id",
    "enrich_fields": ["name", "tier", "location"]
  }
}

POST /_enrich/policy/user-enrich-policy/_execute

# 3. Tạo ingest pipeline sử dụng enrich:
PUT /_ingest/pipeline/order-enrich-pipeline
{
  "processors": [
    {
      "enrich": {
        "policy_name": "user-enrich-policy",
        "field": "customer_id",       # Field in document to match
        "target_field": "customer",   # Where to put enriched data
        "max_matches": 1
      }
    }
  ]
}

# 4. Index sử dụng pipeline:
POST /orders/_doc?pipeline=order-enrich-pipeline
{
  "order_id": "ORD001",
  "customer_id": "USR001",
  "amount": 1500000
}

# Document được stored:
{
  "order_id": "ORD001",
  "customer_id": "USR001",
  "amount": 1500000,
  "customer": {                  # Enriched!
    "name": "Nguyễn Văn A",
    "tier": "gold",
    "location": "TPHCM"
  }
}

11.10 Cross-Fields Query ​

Tìm kiếm words trải rộng nhiều fields:

bash
# Tìm "Nguyễn Văn" có thể ở first_name hoặc last_name:
GET /users/_search
{
  "query": {
    "multi_match": {
      "query": "Nguyễn Văn A",
      "fields": ["first_name", "last_name", "full_name"],
      "type": "cross_fields",    # Treat as one big field!
      "operator": "and"
    }
  }
}

# Hoặc dùng copy_to trong mapping để gom fields:
PUT /users
{
  "mappings": {
    "properties": {
      "first_name": {
        "type": "text",
        "copy_to": "full_name"   # Copy vào virtual field
      },
      "last_name": {
        "type": "text",
        "copy_to": "full_name"
      },
      "full_name": {
        "type": "text"           # Virtual field, không stored
      }
    }
  }
}

GET /users/_search
{
  "query": {
    "match": {
      "full_name": "Nguyễn Văn A"   # Tìm trên combined field
    }
  }
}

11.11 Runtime Fields ​

Runtime fields tính toán trong quá trình query (không stored):

bash
PUT /orders
{
  "mappings": {
    "runtime": {
      "is_high_value": {
        "type": "boolean",
        "script": {
          "source": "emit(doc['amount'].value > 10000000)"
        }
      },
      "total_with_tax": {
        "type": "double",
        "script": {
          "source": "emit(doc['amount'].value * 1.1)"  # +10% VAT
        }
      },
      "order_day_of_week": {
        "type": "keyword",
        "script": {
          "source": """
            ZonedDateTime date = doc['created_at'].value;
            emit(date.getDayOfWeek().toString());
          """
        }
      }
    }
  }
}

# Query on runtime field:
GET /orders/_search
{
  "query": {
    "term": { "is_high_value": true }
  },
  "_source": ["order_id", "amount"],
  "fields": ["total_with_tax", "order_day_of_week"]
}

# Agg on runtime field:
GET /orders/_search
{
  "size": 0,
  "aggs": {
    "orders_by_day": {
      "terms": { "field": "order_day_of_week" }
    }
  }
}

Runtime Fields Trong Query (Ad-hoc) ​

bash
GET /orders/_search
{
  "runtime_mappings": {
    "discount_amount": {
      "type": "double",
      "script": "emit(doc['original_price'].value - doc['final_price'].value)"
    }
  },
  "query": {
    "range": {
      "discount_amount": { "gte": 100000 }    # Đơn có giảm giá >= 100k
    }
  }
}

11.12 Search As You Type ​

Field type đặc biệt tối ưu cho search-as-you-type:

bash
PUT /products
{
  "mappings": {
    "properties": {
      "name": {
        "type": "search_as_you_type",      # Tạo 3 sub-fields tự động
        "max_shingle_size": 3
      }
    }
  }
}

# Tự động tạo:
# name              (standard analysis)
# name._2gram       (bigrams)
# name._3gram       (trigrams)
# name._index_prefix (edge ngrams)

# Query:
GET /products/_search
{
  "query": {
    "multi_match": {
      "query": "iphone pro",
      "type": "bool_prefix",    # Specific type for SAYT
      "fields": [
        "name",
        "name._2gram",
        "name._3gram",
        "name._index_prefix"
      ]
    }
  }
}
# "iphone pro" → match "iPhone 15 Pro Max"
# Gõ từng ký tự đều trả về kết quả!

11.13 Security - Field-Level và Document-Level ​

bash
# Field-level security: Ẩn sensitive fields
POST /_security/role/product-viewer
{
  "indices": [
    {
      "names": ["products"],
      "privileges": ["read"],
      "field_security": {
        "grant": ["*"],
        "except": ["cost_price", "supplier_id"]  # Ẩn internal fields
      }
    }
  ]
}

# Document-level security: Chỉ thấy active products
POST /_security/role/customer-role
{
  "indices": [
    {
      "names": ["products"],
      "privileges": ["read"],
      "query": {
        "term": { "status": "active" }   # Chỉ thấy active products
      }
    }
  ]
}

# User chỉ thấy orders của chính họ (using username placeholder):
POST /_security/role/customer-order-role
{
  "indices": [
    {
      "names": ["orders"],
      "privileges": ["read"],
      "query": {
        "term": { "customer_username": "{{_user.username}}" }
      }
    }
  ]
}

Tóm Tắt Chương 11 ​

FeatureDùng khi
Completion SuggesterAutocomplete nhanh nhất
Term SuggesterSpell correction
PercolatorReverse search, alert/notification
Async SearchQueries chạy > 1 giây
Cross-cluster SearchSearch trên nhiều clusters
EQLSecurity event analysis, sequence detection
Data StreamsTime-series (logs, metrics)
TransformPivot tables, continuous aggregations
Enrich ProcessorLàm giàu data khi indexing
Runtime FieldsAd-hoc computed fields
Search As You TypeReal-time search-as-you-type
Field/Doc-level SecurityFine-grained access control

Bước Tiếp Theo ​

→ Chương 12: Real-world Use Cases


Chương 12: Real-world Use Cases - Ứng Dụng Thực Tế ​

12.1 Use Case 1: E-Commerce Search Engine (Tiki/Shopee-style) ​

Kiến trúc hệ thống ​

                    ┌─────────────────────────────────────┐
                    │           API Gateway               │
                    └────────────────┬────────────────────┘
                                     │
               ┌─────────────────────┼─────────────────────┐
               │                     │                      │
        ┌──────▼──────┐      ┌───────▼───────┐     ┌───────▼──────┐
        │  Product    │      │   Search      │     │  Analytics   │
        │  Service    │      │   Service     │     │   Service    │
        └──────┬──────┘      └───────┬───────┘     └───────┬──────┘
               │                     │                      │
               │         ┌────────────▼─────────────┐      │
               │         │      Elasticsearch        │      │
               │         │   (3-node production)     │      │
               │         └──────────────────────────-┘      │
               │                                            │
        ┌──────▼──────────────────────────────────────────-▼──┐
        │                    PostgreSQL                         │
        │            (Source of truth for products)            │
        └──────────────────────────────────────────────────────┘

Bước 1: Thiết Kế Index Mapping ​

bash
PUT /ecommerce-products
{
  "settings": {
    "number_of_shards": 3,
    "number_of_replicas": 1,
    "analysis": {
      "char_filter": {
        "html_strip": { "type": "html_strip" },
        "special_chars": {
          "type": "mapping",
          "mappings": ["& => and", "₫ => vnd", "% => percent"]
        }
      },
      "tokenizer": {
        "ngram_tokenizer": {
          "type": "ngram",
          "min_gram": 2,
          "max_gram": 3,
          "token_chars": ["letter", "digit"]
        }
      },
      "filter": {
        "vi_stopwords": {
          "type": "stop",
          "stopwords": ["là","và","của","có","trong","với","được","để","cho","từ","tại"]
        },
        "product_synonyms": {
          "type": "synonym_graph",
          "synonyms": [
            "điện thoại, smartphone, dt, mobile",
            "laptop, notebook, máy tính xách tay",
            "iphone => apple iphone",
            "macbook => apple macbook",
            "usb-c, usbc, type-c"
          ],
          "lenient": true
        },
        "edge_ngram_filter": {
          "type": "edge_ngram",
          "min_gram": 1,
          "max_gram": 20
        }
      },
      "analyzer": {
        "vi_product_analyzer": {
          "type": "custom",
          "char_filter": ["html_strip", "special_chars"],
          "tokenizer": "standard",
          "filter": ["lowercase", "vi_stopwords"]
        },
        "vi_search_analyzer": {
          "type": "custom",
          "char_filter": ["special_chars"],
          "tokenizer": "standard",
          "filter": ["lowercase", "vi_stopwords", "product_synonyms"]
        },
        "autocomplete_index": {
          "type": "custom",
          "tokenizer": "standard",
          "filter": ["lowercase", "edge_ngram_filter"]
        },
        "autocomplete_search": {
          "type": "custom",
          "tokenizer": "standard",
          "filter": ["lowercase"]
        }
      }
    }
  },
  "mappings": {
    "properties": {
      "product_id": { "type": "keyword" },
      "sku": { "type": "keyword" },
      "name": {
        "type": "text",
        "analyzer": "vi_product_analyzer",
        "search_analyzer": "vi_search_analyzer",
        "fields": {
          "keyword": { "type": "keyword", "ignore_above": 256 },
          "autocomplete": {
            "type": "text",
            "analyzer": "autocomplete_index",
            "search_analyzer": "autocomplete_search"
          },
          "ngram": {
            "type": "text",
            "analyzer": "vi_product_analyzer"
          }
        }
      },
      "description": {
        "type": "text",
        "analyzer": "vi_product_analyzer",
        "search_analyzer": "vi_search_analyzer",
        "norms": false
      },
      "brand": {
        "type": "text",
        "fields": { "keyword": { "type": "keyword" } }
      },
      "category": {
        "type": "keyword"
      },
      "category_path": {
        "type": "text",
        "analyzer": "path_analyzer",
        "fields": { "keyword": { "type": "keyword" } }
      },
      "price": { "type": "double" },
      "original_price": { "type": "double" },
      "discount_percent": { "type": "integer" },
      "currency": { "type": "keyword" },
      "rating": { "type": "float" },
      "review_count": { "type": "integer" },
      "sold_count": { "type": "long" },
      "stock_quantity": { "type": "integer" },
      "status": { "type": "keyword" },
      "is_featured": { "type": "boolean" },
      "is_flash_sale": { "type": "boolean" },
      "flash_sale_end": { "type": "date" },
      "images": { "type": "keyword", "index": false },
      "tags": { "type": "keyword" },
      "attributes": {
        "type": "nested",
        "properties": {
          "name": { "type": "keyword" },
          "value": { "type": "keyword" }
        }
      },
      "shipping": {
        "properties": {
          "weight_g": { "type": "integer" },
          "free_shipping": { "type": "boolean" },
          "estimated_days": { "type": "integer" }
        }
      },
      "seller": {
        "properties": {
          "seller_id": { "type": "keyword" },
          "name": { "type": "text", "fields": { "keyword": { "type": "keyword" } } },
          "is_official": { "type": "boolean" },
          "rating": { "type": "float" }
        }
      },
      "location": { "type": "geo_point" },
      "created_at": { "type": "date" },
      "updated_at": { "type": "date" },
      "boost_score": { "type": "float" },
      "suggest": {
        "type": "completion",
        "analyzer": "simple"
      }
    }
  }
}

Bước 2: Query Tìm Kiếm Sản Phẩm ​

bash
GET /ecommerce-products/_search
{
  "query": {
    "function_score": {
      "query": {
        "bool": {
          "must": [
            {
              "multi_match": {
                "query": "điện thoại samsung 5g",
                "fields": [
                  "name^5",          # Boost name
                  "brand^3",
                  "tags^2",
                  "description"
                ],
                "type": "best_fields",
                "fuzziness": "AUTO",
                "prefix_length": 2
              }
            }
          ],
          "filter": [
            { "term": { "status": "active" } },
            { "range": { "stock_quantity": { "gt": 0 } } }
          ],
          "should": [
            { "term": { "is_featured": true } },
            { "term": { "is_flash_sale": true } },
            { "term": { "seller.is_official": true } }
          ]
        }
      },
      "functions": [
        {
          "field_value_factor": {
            "field": "sold_count",
            "factor": 0.5,
            "modifier": "log1p",
            "missing": 0
          }
        },
        {
          "field_value_factor": {
            "field": "rating",
            "factor": 2,
            "modifier": "none",
            "missing": 3
          }
        },
        {
          "field_value_factor": {
            "field": "boost_score",
            "factor": 1,
            "modifier": "none",
            "missing": 1
          }
        },
        {
          "gauss": {
            "updated_at": {
              "origin": "now",
              "scale": "30d",
              "offset": "7d",
              "decay": 0.5
            }
          },
          "weight": 1.5
        }
      ],
      "score_mode": "sum",
      "boost_mode": "multiply"
    }
  },
  "aggs": {
    "brands": {
      "terms": { "field": "brand.keyword", "size": 20 }
    },
    "price_ranges": {
      "range": {
        "field": "price",
        "ranges": [
          { "key": "Dưới 3 triệu", "to": 3000000 },
          { "key": "3-7 triệu", "from": 3000000, "to": 7000000 },
          { "key": "7-15 triệu", "from": 7000000, "to": 15000000 },
          { "key": "15-30 triệu", "from": 15000000, "to": 30000000 },
          { "key": "Trên 30 triệu", "from": 30000000 }
        ]
      }
    },
    "avg_rating": { "avg": { "field": "rating" } }
  },
  "sort": [
    { "_score": "desc" },        # Relevance first
    { "is_flash_sale": "desc" },  # Flash sale items
    { "sold_count": "desc" }      # Popular items
  ],
  "from": 0,
  "size": 24,
  "highlight": {
    "fields": {
      "name": {
        "pre_tags": ["<em>"],
        "post_tags": ["</em>"]
      }
    }
  }
}

Bước 3: Node.js Service Implementation ​

javascript
// search-service.js
const { Client } = require('@elastic/elasticsearch');
const client = new Client({ 
  node: 'https://localhost:9200',
  auth: { username: 'elastic', password: process.env.ES_PASSWORD },
  tls: { ca: fs.readFileSync('certs/http_ca.crt') }
});

class ProductSearchService {
  async search({
    query,
    category,
    brand,
    priceMin,
    priceMax,
    minRating,
    sortBy = 'relevance',
    page = 1,
    size = 24
  }) {
    const must = [];
    const filter = [
      { term: { status: 'active' } },
      { range: { stock_quantity: { gt: 0 } } }
    ];

    // Text search
    if (query) {
      must.push({
        multi_match: {
          query,
          fields: ['name^5', 'brand^3', 'tags^2', 'description'],
          type: 'best_fields',
          fuzziness: 'AUTO',
          prefix_length: 2
        }
      });
    } else {
      must.push({ match_all: {} });
    }

    // Category filter
    if (category) filter.push({ term: { category } });
    
    // Brand filter
    if (brand) filter.push({ terms: { 'brand.keyword': Array.isArray(brand) ? brand : [brand] } });
    
    // Price range filter
    if (priceMin || priceMax) {
      const priceRange = { range: { price: {} } };
      if (priceMin) priceRange.range.price.gte = priceMin;
      if (priceMax) priceRange.range.price.lte = priceMax;
      filter.push(priceRange);
    }
    
    // Rating filter
    if (minRating) filter.push({ range: { rating: { gte: minRating } } });

    // Sort options
    const sortOptions = {
      relevance: [{ _score: 'desc' }, { sold_count: 'desc' }],
      price_asc: [{ price: 'asc' }],
      price_desc: [{ price: 'desc' }],
      newest: [{ created_at: 'desc' }],
      best_selling: [{ sold_count: 'desc' }],
      rating: [{ rating: 'desc' }, { review_count: 'desc' }]
    };

    const body = {
      query: {
        function_score: {
          query: {
            bool: {
              must,
              filter,
              should: [
                { term: { is_featured: true } },
                { term: { is_flash_sale: true } },
                { term: { 'seller.is_official': true } }
              ]
            }
          },
          functions: [
            {
              field_value_factor: {
                field: 'sold_count',
                factor: 0.5,
                modifier: 'log1p',
                missing: 0
              }
            },
            {
              field_value_factor: {
                field: 'rating',
                factor: 2,
                modifier: 'none',
                missing: 3
              }
            }
          ],
          score_mode: 'sum',
          boost_mode: 'multiply'
        }
      },
      aggs: {
        brands: { terms: { field: 'brand.keyword', size: 20 } },
        price_ranges: {
          range: {
            field: 'price',
            ranges: [
              { key: 'Dưới 3 triệu', to: 3000000 },
              { key: '3-7 triệu', from: 3000000, to: 7000000 },
              { key: '7-15 triệu', from: 7000000, to: 15000000 },
              { key: 'Trên 15 triệu', from: 15000000 }
            ]
          }
        },
        rating_distribution: {
          range: {
            field: 'rating',
            ranges: [
              { key: '5 sao', from: 4.5, to: 5.1 },
              { key: '4+ sao', from: 4 },
              { key: '3+ sao', from: 3 }
            ]
          }
        }
      },
      sort: sortOptions[sortBy] || sortOptions.relevance,
      from: (page - 1) * size,
      size,
      highlight: {
        fields: {
          name: { pre_tags: ['<mark>'], post_tags: ['</mark>'], number_of_fragments: 0 }
        }
      },
      _source: ['product_id', 'name', 'brand', 'price', 'original_price', 
                'discount_percent', 'rating', 'review_count', 'sold_count',
                'images', 'is_flash_sale', 'shipping']
    };

    const result = await client.search({ index: 'ecommerce-products', body });
    
    return {
      total: result.hits.total.value,
      products: result.hits.hits.map(hit => ({
        ...hit._source,
        score: hit._score,
        highlight: hit.highlight
      })),
      aggregations: {
        brands: result.aggregations.brands.buckets,
        price_ranges: result.aggregations.price_ranges.buckets,
        rating_distribution: result.aggregations.rating_distribution.buckets
      },
      page,
      size,
      total_pages: Math.ceil(result.hits.total.value / size)
    };
  }

  async autocomplete(query) {
    const result = await client.search({
      index: 'ecommerce-products',
      body: {
        suggest: {
          product_suggest: {
            prefix: query,
            completion: {
              field: 'suggest',
              fuzzy: { fuzziness: 1 },
              size: 8
            }
          }
        }
      }
    });

    return result.suggest.product_suggest[0].options.map(opt => ({
      id: opt._id,
      name: opt._source.name,
      brand: opt._source.brand,
      price: opt._source.price
    }));
  }

  async getProductIndexHandler(product) {
    // Sync từ PostgreSQL/MongoDB → Elasticsearch
    await client.index({
      index: 'ecommerce-products',
      id: product.product_id,
      document: {
        ...product,
        suggest: {
          input: [
            product.name,
            product.brand,
            `${product.brand} ${product.name}`,
            ...product.tags || []
          ],
          weight: Math.floor(product.sold_count / 100) + (product.rating * 10)
        }
      }
    });
  }
}

12.2 Use Case 2: ELK Stack Log Analytics ​

Kiến trúc ​

Applications → Beats (Filebeat/Metricbeat) → Logstash → Elasticsearch → Kibana

Logstash Pipeline Configuration ​

ruby
# /etc/logstash/conf.d/app-logs.conf

input {
  beats {
    port => 5044
  }
}

filter {
  # Parse JSON logs từ applications
  if [type] == "app-log" {
    json {
      source => "message"
      target => "app"
    }
    
    # Enrich with GeoIP
    if [app][client_ip] {
      geoip {
        source => "[app][client_ip]"
        target => "[app][geoip]"
      }
    }
    
    # User-agent parsing
    if [app][user_agent] {
      useragent {
        source => "[app][user_agent]"
        target => "[app][ua]"
      }
    }
    
    # Phân loại log level
    mutate {
      add_field => { "[@metadata][rollover_alias]" => "app-logs" }
    }
  }
  
  # Parse nginx access logs
  if [type] == "nginx" {
    grok {
      match => {
        "message" => '%{COMBINEDAPACHELOG}'
      }
    }
    date {
      match => ["timestamp", "dd/MMM/yyyy:HH:mm:ss Z"]
    }
    mutate {
      convert => { "response" => "integer" }
      convert => { "bytes" => "integer" }
      convert => { "request_time" => "float" }
    }
  }
  
  # Remove unnecessary fields
  mutate {
    remove_field => ["@version", "agent", "ecs"]
  }
}

output {
  elasticsearch {
    hosts => ["https://es-node1:9200", "https://es-node2:9200"]
    user => "logstash_user"
    password => "${LOGSTASH_ES_PASSWORD}"
    ssl_certificate_authorities => "/etc/logstash/certs/ca.crt"
    
    index => "logs-%{[@metadata][rollover_alias]}-%{+YYYY.MM.dd}"
    
    # ILM-managed rollover:
    # ilm_rollover_alias => "app-logs"
    # ilm_policy => "log-retention-policy"
  }
}

Kibana Dashboard Queries (via ES) ​

bash
# Dashboard: Error rate over time
GET /logs-app-*/_search
{
  "size": 0,
  "query": {
    "range": { "@timestamp": { "gte": "now-24h" } }
  },
  "aggs": {
    "error_rate_over_time": {
      "date_histogram": {
        "field": "@timestamp",
        "fixed_interval": "5m",
        "format": "HH:mm"
      },
      "aggs": {
        "total": { "value_count": { "field": "_id" } },
        "errors": {
          "filter": { "range": { "app.status_code": { "gte": 500 } } }
        },
        "error_rate": {
          "bucket_script": {
            "buckets_path": { "err": "errors._count", "tot": "total" },
            "script": "params.tot > 0 ? params.err / params.tot * 100 : 0"
          }
        }
      }
    }
  }
}

# Top slow endpoints
GET /logs-app-*/_search
{
  "size": 0,
  "query": {
    "range": { "@timestamp": { "gte": "now-1h" } }
  },
  "aggs": {
    "slow_endpoints": {
      "terms": {
        "field": "app.endpoint.keyword",
        "size": 20,
        "order": { "p95_latency.values.95\\.0": "desc" }
      },
      "aggs": {
        "p95_latency": {
          "percentiles": {
            "field": "app.duration_ms",
            "percents": [50, 95, 99]
          }
        },
        "error_count": {
          "filter": { "range": { "app.status_code": { "gte": 500 } } }
        }
      }
    }
  }
}

# Geographic distribution
GET /logs-app-*/_search
{
  "size": 0,
  "query": { "range": { "@timestamp": { "gte": "now-24h" } } },
  "aggs": {
    "by_country": {
      "terms": { "field": "app.geoip.country_code2.keyword", "size": 20 }
    }
  }
}

Vấn Đề ​

Warehouse có 1 triệu SKUs. Nhân viên cần tìm hàng nhanh theo nhiều tiêu chí:

  • SKU code
  • Tên sản phẩm
  • Vị trí kho
  • Nhà cung cấp
  • Ngày hết hạn
bash
PUT /warehouse-inventory
{
  "settings": {
    "number_of_shards": 2,
    "analysis": {
      "analyzer": {
        "sku_analyzer": {
          "type": "custom",
          "tokenizer": "keyword",
          "filter": ["uppercase"]
        }
      }
    }
  },
  "mappings": {
    "properties": {
      "sku": {
        "type": "keyword",
        "fields": {
          "analyze": { "type": "text", "analyzer": "sku_analyzer" }
        }
      },
      "name": {
        "type": "text",
        "fields": { "keyword": { "type": "keyword" } }
      },
      "quantity": { "type": "integer" },
      "location": {
        "properties": {
          "warehouse": { "type": "keyword" },
          "zone": { "type": "keyword" },
          "rack": { "type": "keyword" },
          "shelf": { "type": "keyword" },
          "bin": { "type": "keyword" },
          "full_location": { "type": "keyword" }
        }
      },
      "supplier": {
        "properties": {
          "supplier_id": { "type": "keyword" },
          "name": { "type": "text", "fields": { "keyword": { "type": "keyword" } } }
        }
      },
      "expiry_date": { "type": "date", "format": "yyyy-MM-dd" },
      "last_updated": { "type": "date" },
      "status": { "type": "keyword" },
      "unit_cost": { "type": "double" }
    }
  }
}

# Query: Tìm sản phẩm sắp hết hạn trong 30 ngày
GET /warehouse-inventory/_search
{
  "query": {
    "bool": {
      "must": [
        { "query_string": { "query": "thực phẩm" } }
      ],
      "filter": [
        {
          "range": {
            "expiry_date": {
              "lte": "now+30d",
              "gte": "now"
            }
          }
        },
        { "range": { "quantity": { "gt": 0 } } }
      ]
    }
  },
  "aggs": {
    "expiry_urgency": {
      "range": {
        "field": "expiry_date",
        "ranges": [
          { "key": "Hết hạn trong 7 ngày", "to": "now+7d" },
          { "key": "7-15 ngày", "from": "now+7d", "to": "now+15d" },
          { "key": "15-30 ngày", "from": "now+15d", "to": "now+30d" }
        ]
      },
      "aggs": {
        "total_quantity": { "sum": { "field": "quantity" } },
        "total_value": {
          "sum": {
            "script": {
              "source": "doc['quantity'].value * doc['unit_cost'].value"
            }
          }
        }
      }
    },
    "by_warehouse": {
      "terms": { "field": "location.warehouse" }
    }
  },
  "sort": [{ "expiry_date": "asc" }]
}

12.4 Use Case 4: Semantic Search với Vector Embeddings ​

Kiến trúc ​

User Query → Embedding Model → Vector → kNN Search → Results
Document   → Embedding Model → Vector → ES dense_vector field

Setup ​

bash
# Mapping với dense_vector
PUT /knowledge-base
{
  "mappings": {
    "properties": {
      "title": { "type": "text" },
      "content": { "type": "text" },
      "content_vector": {
        "type": "dense_vector",
        "dims": 1536,           # OpenAI embedding dimensions
        "index": true,
        "similarity": "cosine"
      },
      "category": { "type": "keyword" },
      "created_at": { "type": "date" }
    }
  }
}

Python Implementation ​

python
import openai
from elasticsearch import Elasticsearch

es = Elasticsearch(
    "https://localhost:9200",
    http_auth=("elastic", "your_password"),
    ca_certs="certs/http_ca.crt"
)

openai.api_key = os.environ["OPENAI_API_KEY"]

def get_embedding(text: str) -> list[float]:
    """Tạo embedding vector từ OpenAI API"""
    response = openai.Embedding.create(
        input=text,
        model="text-embedding-ada-002"
    )
    return response['data'][0]['embedding']

def index_document(doc_id: str, title: str, content: str, category: str):
    """Index document với embedding"""
    embedding = get_embedding(title + " " + content[:500])
    
    es.index(
        index="knowledge-base",
        id=doc_id,
        document={
            "title": title,
            "content": content,
            "content_vector": embedding,
            "category": category,
            "created_at": datetime.now().isoformat()
        }
    )

def semantic_search(query: str, category: str = None, size: int = 10):
    """Tìm kiếm semantic với vector + keyword hybrid"""
    query_vector = get_embedding(query)
    
    # Hybrid search: semantic + keyword
    body = {
        "query": {
            "bool": {
                "should": [
                    {
                        "knn": {
                            "field": "content_vector",
                            "query_vector": query_vector,
                            "num_candidates": 100,
                            "boost": 0.7    # 70% weight cho semantic
                        }
                    },
                    {
                        "multi_match": {
                            "query": query,
                            "fields": ["title^3", "content"],
                            "boost": 0.3    # 30% weight cho keyword
                        }
                    }
                ]
            }
        },
        "size": size,
        "_source": ["title", "content", "category", "created_at"]
    }
    
    if category:
        body["query"]["bool"]["filter"] = [{"term": {"category": category}}]
    
    result = es.search(index="knowledge-base", body=body)
    
    return [{
        "title": hit["_source"]["title"],
        "content": hit["_source"]["content"][:200] + "...",
        "score": hit["_score"],
        "id": hit["_id"]
    } for hit in result["hits"]["hits"]]

# Usage:
results = semantic_search("Cách tối ưu truy vấn database PostgreSQL")
# Tìm được cả:
# - Articles về PostgreSQL query optimization
# - Articles về database performance với từ đồng nghĩa
# - Articles về index tuning even if không chứa exact keywords

Scenario ​

SaaS platform, mỗi customer có data riêng biệt:

Option 1: Index per tenant (tốt cho isolation, kém về số lượng)

bash
# Mỗi customer → 1 index
products-tenant-001
products-tenant-002
...

# Update tenant settings:
PUT /products-tenant-001/_settings
{
  "index.routing.allocation.require.tier": "hot"
}

Option 2: Field per tenant (hơn về management, có isolation risk)

bash
PUT /products-shared
{
  "mappings": {
    "properties": {
      "tenant_id": { "type": "keyword" },
      "name": { "type": "text" },
      ...
    }
  }
}

# Luôn filter by tenant_id:
GET /products-shared/_search
{
  "query": {
    "bool": {
      "filter": [
        { "term": { "tenant_id": "tenant-001" } }
      ],
      "must": [
        { "match": { "name": "query" } }
      ]
    }
  }
}

Option 3: Document-level Security (DLS)

bash
POST /_security/role/tenant-001-role
{
  "indices": [{
    "names": ["products-shared"],
    "privileges": ["read"],
    "query": {
      "term": { "tenant_id": "tenant-001" }
    }
  }]
}
# Bất kỳ user nào với role này chỉ thấy data của tenant-001

12.6 Use Case 6: Real-time User Activity Stream ​

Clickstream Analytics ​

bash
PUT /user-events
{
  "settings": {
    "number_of_shards": 5,
    "index.lifecycle.name": "events-policy"
  },
  "mappings": {
    "properties": {
      "@timestamp": { "type": "date" },
      "user_id": { "type": "keyword" },
      "session_id": { "type": "keyword" },
      "event_type": { "type": "keyword" },
      "product_id": { "type": "keyword" },
      "category": { "type": "keyword" },
      "page": { "type": "keyword" },
      "duration_ms": { "type": "integer" },
      "device": {
        "properties": {
          "type": { "type": "keyword" },
          "os": { "type": "keyword" },
          "browser": { "type": "keyword" }
        }
      },
      "location": { "type": "geo_point" },
      "referrer": { "type": "keyword" },
      "ip": { "type": "ip" }
    }
  }
}

# Real-time funnel analysis
GET /user-events/_search
{
  "size": 0,
  "query": {
    "range": { "@timestamp": { "gte": "now-24h" } }
  },
  "aggs": {
    "funnel": {
      "filters": {
        "filters": {
          "1_view_product": { "term": { "event_type": "product_view" } },
          "2_add_to_cart": { "term": { "event_type": "add_to_cart" } },
          "3_checkout": { "term": { "event_type": "checkout_start" } },
          "4_purchase": { "term": { "event_type": "purchase_complete" } }
        }
      },
      "aggs": {
        "unique_users": {
          "cardinality": { "field": "user_id" }
        }
      }
    },
    "top_exit_pages": {
      "terms": {
        "field": "page",
        "size": 10,
        "order": { "avg_time": "asc" }
      },
      "aggs": {
        "avg_time": { "avg": { "field": "duration_ms" } }
      }
    }
  }
}

# User cohort analysis
GET /user-events/_search
{
  "size": 0,
  "aggs": {
    "weekly_cohorts": {
      "date_histogram": {
        "field": "first_visit",
        "calendar_interval": "week"
      },
      "aggs": {
        "retention": {
          "date_histogram": {
            "field": "@timestamp",
            "calendar_interval": "week"
          },
          "aggs": {
            "users": { "cardinality": { "field": "user_id" } }
          }
        }
      }
    }
  }
}

Tóm Tắt Chương 12 ​

Use CaseES Features Used
E-commerce SearchMulti-match, function_score, faceted search, autocomplete
Log AnalyticsTime-series, aggregations, ELK Stack
Inventory SearchRange queries, nested aggs, scripted fields
Semantic Searchdense_vector, kNN, hybrid search
Multi-tenantIndex isolation, DLS, tenant_id filtering
Clickstream AnalyticsHigh-volume indexing, time-series, cardinality

Chương 13: Best Practices & Production - Vận Hành Thực Tế ​

13.1 Production Checklist ​

Trước Khi Deploy ​

bash
# 1. Security
✅ TLS/SSL được bật (không dùng HTTP plaintext)
✅ Authentication được bật
✅ Default passwords đã đổi
✅ API keys cho external access (không dùng elastic superuser)
✅ Network không expose port 9200/9300 ra public internet

# 2. Cluster
✅ Ít nhất 3 master-eligible nodes
✅ discovery.seed_hosts được cấu hình
✅ cluster.initial_master_nodes được set (chỉ bootstrap lần đầu)
✅ vm.max_map_count = 262144

# 3. Memory
✅ Heap size = 50% RAM, max 31GB
✅ Xms = Xmx
✅ bootstrap.memory_lock = true
✅ Swap tắt

# 4. Disk
✅ SSD cho data nodes
✅ Disk watermarks được cấu hình
✅ Backup strategy installed (snapshot repository)

# 5. Index
✅ Shard sizing phù hợp (< 50GB per shard)
✅ Replica = 1 ít nhất
✅ ILM policy cho time-series indices
✅ Mapping designed kỹ (không dynamic mapping vô kiểm soát)

13.2 Security Hardening ​

Tạo User và Role System ​

bash
# Tạo roles
POST /_security/role/app-read-role
{
  "indices": [
    {
      "names": ["products", "categories"],
      "privileges": ["read", "view_index_metadata"]
    }
  ],
  "cluster": ["monitor"]
}

POST /_security/role/app-write-role
{
  "indices": [
    {
      "names": ["products", "categories"],
      "privileges": ["read", "write", "view_index_metadata"]
    }
  ],
  "cluster": ["monitor", "manage_index_templates"]
}

POST /_security/role/analytics-role
{
  "indices": [
    {
      "names": ["orders", "events-*"],
      "privileges": ["read"],
      "field_security": {
        "grant": ["*"],
        "except": ["payment_details", "personal_info"]    # Ẩn sensitive fields
      }
    }
  ]
}

# Tạo users
POST /_security/user/app-service
{
  "password": "SecurePassword123!",
  "roles": ["app-read-role", "app-write-role"],
  "full_name": "E-commerce Application",
  "email": "[email protected]"
}

POST /_security/user/analytics-dashboard
{
  "password": "AnalyticsPass456!",
  "roles": ["analytics-role"],
  "full_name": "Analytics Dashboard"
}

API Keys (Preferred cho Services) ​

bash
# Tạo API key (không expire, scoped permissions)
POST /_security/api_key
{
  "name": "ecommerce-backend-key",
  "expiration": "90d",
  "role_descriptors": {
    "product-access": {
      "indices": [
        {
          "names": ["products"],
          "privileges": ["read", "write"]
        }
      ]
    }
  }
}

# Response:
{
  "id": "api-key-id",
  "name": "ecommerce-backend-key",
  "api_key": "abc123...",
  "encoded": "base64-encoded-key"   # Dùng cái này trong header
}

# Sử dụng API key:
curl -H "Authorization: ApiKey base64-encoded-key" \
     https://localhost:9200/products/_search

# Node.js:
const client = new Client({
  node: 'https://localhost:9200',
  auth: { apiKey: 'base64-encoded-key' }
});

# Xem và revoke API keys:
GET /_security/api_key?name=ecommerce-backend-key
DELETE /_security/api_key
{ "id": "api-key-id" }

Network Security ​

bash
# elasticsearch.yml
network.host: _site_          # Chỉ bind local network
http.port: 9200
transport.port: 9300

# Hoặc explicit:
network.bind_host: ["_local_", "10.0.0.10"]
network.publish_host: "10.0.0.10"   # Địa chỉ để nodes khác connect

# Firewall (iptables):
# Allow 9200 chỉ từ application servers:
iptables -A INPUT -p tcp --dport 9200 -s 10.0.1.0/24 -j ACCEPT
iptables -A INPUT -p tcp --dport 9200 -j DROP

# Allow 9300 chỉ giữa ES nodes:
iptables -A INPUT -p tcp --dport 9300 -s 10.0.0.0/24 -j ACCEPT
iptables -A INPUT -p tcp --dport 9300 -j DROP

13.3 Backup & Restore - Snapshot ​

Cấu Hình Snapshot Repository ​

bash
# Option 1: Shared file system (NFS/EFS)
PUT /_snapshot/my-backup
{
  "type": "fs",
  "settings": {
    "location": "/mnt/backups/elasticsearch",
    "compress": true
  }
}

# Option 2: AWS S3
# Cài plugin: elasticsearch-plugin install repository-s3
PUT /_snapshot/s3-backup
{
  "type": "s3",
  "settings": {
    "bucket": "my-es-backups",
    "region": "ap-southeast-1",
    "base_path": "production-backups",
    "compress": true,
    "chunk_size": "1gb",
    "server_side_encryption": true
  }
}
# AWS credentials qua IAM Role hoặc:
# AWS_ACCESS_KEY_ID và AWS_SECRET_ACCESS_KEY trong keystore

# Option 3: GCS (Google Cloud Storage)
PUT /_snapshot/gcs-backup
{
  "type": "gcs",
  "settings": {
    "bucket": "my-es-backups-bucket",
    "base_path": "elasticsearch",
    "compress": true
  }
}

Tạo Snapshots ​

bash
# Snapshot toàn bộ cluster:
PUT /_snapshot/s3-backup/snapshot-01
{
  "indices": "*",
  "include_global_state": true,   # Bao gồm cluster settings, ILM policies
  "metadata": {
    "taken_by": "cron-job",
    "taken_because": "nightly backup"
  }
}

# Snapshot cụ thể indices:
PUT /_snapshot/s3-backup/products-snapshot-20240115
{
  "indices": ["products", "categories"],
  "include_global_state": false
}

# Xem status snapshot:
GET /_snapshot/s3-backup/snapshot-01/_status

# Liệt kê snapshots:
GET /_snapshot/s3-backup/*

# Xóa snapshot cũ:
DELETE /_snapshot/s3-backup/snapshot-01

SLM - Snapshot Lifecycle Management ​

bash
PUT /_slm/policy/nightly-backup
{
  "schedule": "0 30 1 * * ?",           # Mỗi ngày 1:30 AM
  "name": "<daily-snap-{now/d}>",
  "repository": "s3-backup",
  "config": {
    "indices": "*",
    "include_global_state": true
  },
  "retention": {
    "expire_after": "30d",      # Xóa snapshots cũ hơn 30 ngày
    "min_count": 5,             # Giữ ít nhất 5 snapshots
    "max_count": 50             # Giữ tối đa 50 snapshots
  }
}

# Activate SLM:
POST /_slm/policy/nightly-backup/_execute

# Xem SLM stats:
GET /_slm/stats

Restore ​

bash
# Restore toàn bộ snapshot:
POST /_snapshot/s3-backup/snapshot-01/_restore
{
  "indices": "*",
  "include_global_state": true,
  "rename_pattern": "(.+)",
  "rename_replacement": "restored-$1"   # Rename để tránh conflict
}

# Restore chỉ 1 index:
POST /_snapshot/s3-backup/snapshot-01/_restore
{
  "indices": ["products"],
  "ignore_unavailable": true,
  "index_settings": {
    "index.number_of_replicas": 0    # Override settings khi restore
  }
}

# Theo dõi restore:
GET /_recovery?active_only=true

13.4 Zero-Downtime Migration ​

Kịch bản: Thay Đổi Mapping ​

Bạn cần thêm field mới với analyzer khác. Không thể modify mapping existing → Cần reindex.

bash
# Bước 1: Tạo index mới với mapping mới
PUT /products-v2
{
  "settings": { ... },
  "mappings": {
    "properties": {
      "name": {
        "type": "text",
        "analyzer": "new-analyzer"   # Analyzer mới
      },
      "new_field": { "type": "keyword" }   # Field mới
    }
  }
}

# Bước 2: Reindex từ v1 sang v2 (keep v1 serving traffic)
POST /_reindex?wait_for_completion=false
{
  "source": { "index": "products-v1" },
  "dest": { "index": "products-v2" },
  "script": {
    "source": """
      ctx._source.new_field = ctx._source.category + '_' + ctx._source.brand;
    """
  }
}

# Theo dõi reindex:
GET /_tasks?actions=*reindex&detailed=true

# Bước 3: Sau khi reindex xong, verify data
GET /products-v2/_count     # Phải bằng products-v1
GET /products-v2/_search    # Test queries

# Bước 4: Chuyển alias từ v1 sang v2 (atomic operation!)
POST /_aliases
{
  "actions": [
    { "remove": { "index": "products-v1", "alias": "products" } },
    { "add": { "index": "products-v2", "alias": "products" } }
  ]
}
# All traffic now goes to products-v2!
# Zero downtime because alias switch is atomic

# Bước 5: Verify trên v2
GET /products/_search

# Bước 6: Xóa v1 sau vài ngày
DELETE /products-v1

13.5 Monitoring ​

Elasticsearch Metrics Quan Trọng ​

bash
# Cluster health:
GET /_cluster/health?level=indices

# Key metrics:
GET /_cat/nodes?v&h=name,heap.percent,heap.max,cpu,load_1m,disk.used_percent,master

# Trữ lượng này dùng nhiều nhất:
GET /_cat/indices?v&h=index,health,docs.count,store.size,pri.store.size&s=store.size:desc

# GC statistics:
GET /_nodes/stats/jvm?filter_path=nodes.*.jvm.gc

# Thread pools (quan trọng để biết bottleneck):
GET /_nodes/stats/thread_pool?filter_path=nodes.*.thread_pool

# Response:
{
  "nodes": {
    "node-1": {
      "thread_pool": {
        "search": {
          "threads": 14,
          "queue": 0,         # Queue > 0 = CPU bound!
          "active": 2,
          "rejected": 5,      # Rejected > 0 = quá tải!
          "largest": 14,
          "completed": 120000
        }
      }
    }
  }
}

Metricbeat cho Elasticsearch Monitoring ​

yaml
# metricbeat.yml
metricbeat.modules:
- module: elasticsearch
  metricsets:
    - node
    - node_stats
    - index
    - index_recovery
    - index_summary
    - shard
    - ml_job
  period: 10s
  hosts: ["https://localhost:9200"]
  username: "monitoring-user"
  password: "${ES_MONITORING_PASSWORD}"
  ssl.certificate_authorities: ["/etc/metricbeat/certs/ca.crt"]

output.elasticsearch:
  hosts: ["https://monitoring-cluster:9200"]  # Separate monitoring cluster
  username: "monitoring-writer"
  password: "${MONITORING_WRITER_PASSWORD}"
  ssl.certificate_authorities: ["/etc/metricbeat/certs/ca.crt"]

13.6 Alerting ​

Watcher Alerts ​

bash
# Alert: Cluster health đỏ
PUT /_watcher/watch/cluster-health-alert
{
  "trigger": {
    "schedule": { "interval": "1m" }    # Check mỗi phút
  },
  "input": {
    "http": {
      "request": {
        "url": "http://localhost:9200/_cluster/health",
        "auth": { "basic": { "username": "watcher-user", "password": "xxx" } }
      }
    }
  },
  "condition": {
    "compare": {
      "ctx.payload.status": {
        "eq": "red"    # Condition: status = "red"
      }
    }
  },
  "actions": {
    "slack_alert": {
      "webhook": {
        "scheme": "https",
        "host": "hooks.slack.com",
        "port": 443,
        "method": "post",
        "path": "/services/xxx/yyy/zzz",
        "params": {},
        "headers": { "Content-Type": "application/json" },
        "body": """{"text": "🚨 ES Cluster HEALTH is RED! Immediate action required."}"""
      }
    },
    "email_alert": {
      "email": {
        "to": ["[email protected]"],
        "subject": "ALERT: Elasticsearch Cluster Health",
        "body": {
          "html": "<p>Cluster status: <strong>RED</strong></p>"
        }
      }
    }
  }
}

# Alert: Disk usage cao
PUT /_watcher/watch/disk-usage-alert
{
  "trigger": {
    "schedule": { "interval": "5m" }
  },
  "input": {
    "search": {
      "request": {
        "indices": [".monitoring-es-*"],
        "body": {
          "size": 1,
          "query": {
            "bool": {
              "filter": [
                { "range": { "@timestamp": { "gte": "now-10m" } } }
              ]
            }
          },
          "aggs": {
            "max_disk": {
              "max": { "field": "elasticsearch.node.stats.fs.total.available_in_bytes" }
            }
          }
        }
      }
    }
  },
  "condition": {
    "script": {
      "source": """
        def total = ctx.payload.hits.hits[0]._source.elasticsearch.node.stats.fs.total.total_in_bytes;
        def avail = ctx.payload.aggregations.max_disk.value;
        def used_pct = (total - avail) / total * 100;
        return used_pct > 80;
      """
    }
  },
  "actions": {
    "notify": {
      "webhook": {
        "scheme": "https",
        "host": "hooks.slack.com",
        "method": "post",
        "body": """{"text": "⚠️ Elasticsearch disk usage is above 80%!"}"""
      }
    }
  }
}

13.7 Common Pitfalls và Solutions ​

1. Split-Brain (Network Partition) ​

bash
# VẤNĐỀ: Cluster bị tách thành 2 phần, cả 2 tưởng mình là master

# GIẢI PHÁP: Cấu hình đúng discovery
# elasticsearch.yml
discovery.seed_hosts:
  - "node1:9300"
  - "node2:9300"
  - "node3:9300"

cluster.initial_master_nodes:
  - "node1"
  - "node2"
  - "node3"

# LUÔN dùng số lẻ master-eligible nodes: 3, 5, 7
# Quorum: (n/2) + 1
# 3 nodes → quorum = 2 (cần 2/3 đồng ý để elect master)

2. Out of Memory / GC Issues ​

bash
# Triệu chứng: Query chậm, GC overhead, OutOfMemoryError

# Cách tìm vấn đề:
GET /_nodes/stats/jvm

# GC time > 10% CPU time → Có issue

# Solutions:
# 1. Giảm field data cache:
PUT /_cluster/settings
{
  "persistent": {
    "indices.fielddata.cache.size": "20%"
  }
}

# 2. Tránh aggregation trên text fields (dùng keyword):
# BAD: agg trên "message" text field → fielddata loaded vào heap
# GOOD: agg trên "status.keyword"

# 3. Tăng heap (max 31GB):
# jvm.options: -Xms24g -Xmx24g

# 4. Giảm số concurrent searches:
PUT /_cluster/settings
{
  "transient": {
    "cluster.routing.allocation.node_concurrent_recoveries": 2
  }
}

3. Mapping Explosion ​

bash
# VẤNĐỀ: Dynamic mapping tạo ra quá nhiều fields
# Ví dụ: Log có JSON payload với nhiều keys khác nhau
# → Cluster state bloom lên GB+

# Giải pháp 1: Index template với strict mapping
PUT /_index_template/strict-logs
{
  "index_patterns": ["logs-*"],
  "template": {
    "mappings": {
      "dynamic": "strict",          # Reject unknown fields
      "properties": {
        "timestamp": { "type": "date" },
        "message": { "type": "text" },
        "level": { "type": "keyword" }
      }
    }
  }
}

# Giải pháp 2: Flatten arbitrary JSON
{
  "properties": {
    "metadata": {
      "type": "flattened"    # Store as single field, không tạo sub-fields
    }
  }
}

# Giải pháp 3: Limit total fields
PUT /_settings
{
  "index.mapping.total_fields.limit": 200    # Default: 1000
}

4. Hotspot Shards ​

bash
# VẤNĐỀ: Một số shards nhận quá nhiều writes/reads
# → Nodes chứa shard đó bị quá tải

# Giải pháp: Routing tốt hơn
# Option 1: Không dùng custom routing (để ES phân phối tự động)
# Option 2: Nếu cần custom routing, đảm bảo key phân bổ đều:

GET /orders/_search
{
  "query": { "match_all": {} },
  "aggs": {
    "shard_distribution": {
      "terms": {
        "field": "_routing",    # Xem routing distribution
        "size": 20
      }
    }
  }
}

# Dùng routing_partition_size để phân tán với custom routing:
PUT /orders
{
  "settings": {
    "number_of_shards": 4,
    "routing_partition_size": 2    # Route tới 1 trong 2 shards thay vì 1 cố định
  }
}

5. Score chạy sai / kết quả không như mong đợi ​

bash
# Debug scoring:
GET /products/_explain/doc-id
{
  "query": {
    "match": { "name": "iPhone" }
  }
}

# Response giải thích tại sao document này có score X:
{
  "matched": true,
  "explanation": {
    "value": 4.5,
    "description": "weight(name:iphone in 0)",
    "details": [
      {
        "value": 4.5,
        "description": "score(freq=1.0), product of:",
        "details": [
          {
            "value": 2.2,
            "description": "idf, computed as log(1 + (N - n + 0.5) / (n + 0.5))"
          }
        ]
      }
    ]
  }
}

13.8 Upgrade Best Practices ​

Rolling Upgrade ​

bash
# Quy trình upgrade từ 8.x sang 8.y (minor versions):

# 1. Backup trước
PUT /_snapshot/backup/pre-upgrade-snapshot

# 2. Tắt shard allocation:
PUT /_cluster/settings
{
  "persistent": {
    "cluster.routing.allocation.enable": "primaries"
  }
}

# 3. Sync flush:
POST /_flush/synced

# 4. Stop node
systemctl stop elasticsearch

# 5. Upgrade Elasticsearch package

# 6. Start node
systemctl start elasticsearch

# 7. Verify node là healthy:
GET /_nodes/_local/stats

# 8. Re-enable allocation (sau khi mọi shard recovered):
PUT /_cluster/settings
{
  "persistent": {
    "cluster.routing.allocation.enable": null  # Reset to default
  }
}

# 9. Repeat cho node tiếp theo

13.9 Capacity Planning ​

Tính Toán Tài Nguyên ​

Data Volume:
  - Daily ingest: 1 billion events/day
  - Event size: 500 bytes avg
  - Daily raw data: 500GB/day
  - Compression factor: ~3x
  - Stored daily: ~167GB/day

Retention: 30 days
Total hot storage: 5TB

With 1 replica: 10TB total
With 20% overhead: 12TB disk space

Shard sizing (50GB target):
  12TB / 50GB = 240 shards
  With 2 replicas = 720 total shards
  With 6 data nodes = 120 shards/node (acceptable)

Memory (heap) per node:
  @30MB/shard × 120 shards = 3.6GB
  Add processing overhead: 6-8GB heap per node
  On 64GB servers: 32GB heap, 32GB OS cache

CPU:
  Search: p95 < 100ms → need ~14 CPU cores
  Indexing: 100k events/s × 500B = 50MB/s → need 8-12 cores
  Use: 32 CPU cores per node (standard server)

Sample Production Cluster Configuration ​

yaml
# production-elasticsearch.yml template

cluster.name: production-search-cluster

# Node identification
node.name: ${HOSTNAME}
node.roles: [ master, data, ingest ]      # Hot nodes

# Paths
path.data: /data/elasticsearch
path.logs: /var/log/elasticsearch

# Network
network.host: _site_
http.port: 9200
transport.port: 9300

# Discovery
discovery.seed_hosts:
  - "es-node-01:9300"
  - "es-node-02:9300"
  - "es-node-03:9300"
  - "es-node-04:9300"
  - "es-node-05:9300"

cluster.initial_master_nodes:
  - "es-node-01"
  - "es-node-02"
  - "es-node-03"

# Security
xpack.security.enabled: true
xpack.security.transport.ssl.enabled: true
xpack.security.transport.ssl.keystore.path: certs/transport.p12
xpack.security.transport.ssl.truststore.path: certs/transport.p12
xpack.security.http.ssl.enabled: true
xpack.security.http.ssl.keystore.path: certs/http.p12

# Memory
bootstrap.memory_lock: true

# Disk watermarks
cluster.routing.allocation.disk.watermark.low: "80%"
cluster.routing.allocation.disk.watermark.high: "85%"
cluster.routing.allocation.disk.watermark.flood_stage: "90%"

# Logging
logger.level: WARN

13.10 Tổng Kết Toàn Bộ Series ​

Con Đường Học Elasticsearch ​

Level 1 - Beginner (Chapters 1-4)
  ✅ Hiểu Elasticsearch là gì và khi nào dùng
  ✅ Setup local development với Docker
  ✅ Hiểu Index, Document, Mapping, Shards
  ✅ CRUD operations cơ bản

Level 2 - Intermediate (Chapters 5-8)
  ✅ Thiết kế mapping chuyên nghiệp
  ✅ Viết queries phức tạp với Bool query
  ✅ Advanced queries (nested, parent-child, geo)
  ✅ Text analysis và custom analyzers

Level 3 - Advanced (Chapters 9-11)
  ✅ Aggregation framework đầy đủ
  ✅ Performance optimization
  ✅ Advanced features (percolator, semantic search)

Level 4 - Production (Chapters 12-13)
  ✅ Real-world use cases
  ✅ Security, backup, monitoring
  ✅ Capacity planning và troubleshooting

Quick Reference Card ​

bash
# ----- CLUSTER -----
GET /_cluster/health
GET /_cat/nodes?v
GET /_cat/indices?v&s=store.size:desc

# ----- SEARCH -----
GET /{index}/_search
GET /{index}/_count
GET /{index}/_explain/{id}

# ----- INDEX -----
PUT /{index}
DELETE /{index}
PUT /{index}/_settings
GET /{index}/_mapping
POST /{index}/_reindex   # (POST /_reindex)

# ----- DOCS -----
PUT /{index}/_doc/{id}
GET /{index}/_doc/{id}
POST /{index}/_update/{id}
DELETE /{index}/_doc/{id}
POST /{index}/_bulk

# ----- ANALYSIS -----
GET /{index}/_analyze
GET /_analyze

# ----- ALIASES -----
POST /_aliases

# ----- SNAPSHOTS -----
PUT /_snapshot/{repo}/{name}
GET /_snapshot/{repo}/{name}
POST /_snapshot/{repo}/{name}/_restore

# ----- SECURITY -----
POST /_security/user/{username}
POST /_security/role/{rolename}
POST /_security/api_key

Nguồn Tham Khảo ​

Official Documentation ​

Sách ​

  • Elasticsearch: The Definitive Guide (O'Reilly)
  • Relevant Search (Manning)
  • Elasticsearch in Action (Manning)

Cộng Đồng ​