ELK Elasticsearch Ecosystem
Bắt đầu

Index document đầu tiên

Tạo index với mapping tối thiểu, thêm document bằng REST API và thực hành các thao tác CRUD cơ bản.

Phạm vi bài học

Bài này dùng Kibana Dev Tools Console và một Elasticsearch local đang chạy ở http://localhost:9200. Bạn sẽ tạo một index sách nhỏ, sau đó thêm, đọc, cập nhật, xóa và đếm document. Các request trong bài đều có thể dán trực tiếp vào Console.

Mục lục

Mô hình dữ liệu trong Elasticsearch

Index, document và field

Index là một không gian chứa các document có mục đích sử dụng gần nhau. Có thể hình dung index giống một bảng trong cơ sở dữ liệu quan hệ, nhưng cách lập chỉ mục và tìm kiếm bên trong khác nhau.

Document là một bản ghi dạng JSON. Mỗi document có một _id duy nhất trong một index. Field là một thuộc tính trong JSON, chẳng hạn title hoặc price.

Trong bài này, index books sẽ chứa các document sách:

books (index)
├── book-001 (document)
│   ├── title (field)
│   ├── author (field)
│   ├── published_at (field)
│   └── price (field)
└── một ID tự sinh (document)

Index không phải là một thư mục và document không cần có cùng số lượng field. Tuy nhiên, các field có cùng tên nên có kiểu dữ liệu ổn định. Sự ổn định đó giúp truy vấn, sắp xếp và aggregation (phép tổng hợp như đếm hoặc tính trung bình) hoạt động có thể đoán trước.

Luồng thực hành

Sơ đồ sau tóm tắt thứ tự các thao tác trong bài:

Mermaid trong trang này

Sơ đồ được chuyển từ code block Mermaid thành component tương tác khi trang được tải. Nếu bạn vừa bật dev server, hãy tải lại trang sau khi cấu hình đã được biên dịch.

Chuẩn bị và kiểm tra Elasticsearch

Mở Kibana → Dev Tools → Console. Console tách request thành hai vùng: vùng bên trái để nhập method, path và body; vùng bên phải để xem response. Nhấn nút chạy request hoặc dùng phím tắt của Kibana.

Kiểm tra cluster trước khi tạo dữ liệu:

GET /

Response thành công có dạng tương tự:

{
  "name": "es01",
  "cluster_name": "docker-cluster",
  "cluster_uuid": "...",
  "version": {
    "number": "9.x.y"
  },
  "tagline": "You Know, for Search"
}

Trường version.number cho biết phiên bản server đang chạy. Tên node và cluster có thể khác tùy cách bạn cài đặt. Nếu Console báo không kết nối được, hãy quay lại Cài đặt bằng Docker Compose hoặc Cài đặt native để kiểm tra service.

Tạo index và mapping tối thiểu

Mapping là gì

Mapping là schema (mô tả kiểu và cách lập chỉ mục của field) của một index. Ví dụ, title kiểu text được phân tích để tìm từng từ. author kiểu keyword được giữ nguyên để lọc hoặc nhóm theo tên tác giả.

Một field text không phù hợp cho mọi mục đích. Khi cần vừa tìm toàn văn vừa lọc chính xác, ta thường khai báo text kèm một subfield keyword. Trong mapping dưới đây, title có cả hai cách biểu diễn:

  • title: dùng cho tìm kiếm toàn văn.
  • title.keyword: dùng cho giá trị chính xác, sort hoặc aggregation.

Các kiểu còn lại phản ánh cách sử dụng trực tiếp: date cho ngày, double cho giá, và boolean cho trạng thái.

Tạo index books

Request sau tạo index mới và khai báo mapping trước khi nạp dữ liệu:

PUT books
{
  "mappings": {
    "properties": {
      "title": {
        "type": "text",
        "fields": {
          "keyword": {
            "type": "keyword"
          }
        }
      },
      "author": {
        "type": "keyword"
      },
      "published_at": {
        "type": "date"
      },
      "price": {
        "type": "double"
      },
      "in_stock": {
        "type": "boolean"
      }
    }
  }
}

Response thành công thường là:

{
  "acknowledged": true,
  "shards_acknowledged": true,
  "index": "books"
}

acknowledged cho biết cluster đã chấp nhận thao tác tạo index. shards_acknowledged cho biết các shard (phân vùng dữ liệu) đã được khởi tạo. Nếu chạy lại request, Elasticsearch sẽ trả lỗi resource_already_exists_exception vì index đã tồn tại. Khi muốn làm lại từ đầu trong môi trường học tập, xóa index trước bằng DELETE books, rồi chạy lại request tạo index.

Thói quen an toàn cho schema

Trong môi trường thử nghiệm, dynamic mapping giúp khám phá dữ liệu nhanh. Trong ứng dụng thật, hãy thiết kế mapping trước, giới hạn field được phép và kiểm tra dữ liệu đầu vào. Elasticsearch không cho đổi trực tiếp kiểu của một field đã tồn tại; đổi kiểu thường cần tạo index mới và reindex dữ liệu.

Dynamic mapping trong một ví dụ riêng

Dynamic mapping là cơ chế Elasticsearch tự suy ra mapping khi nhận field mới. Cơ chế này tiện cho prototype, nhưng một giá trị không rõ kiểu có thể tạo schema ngoài dự kiến.

Tạo một index riêng để quan sát cơ chế đó:

POST dynamic-books/_doc
{
  "title": "Elasticsearch cho người mới bắt đầu",
  "pages": 320,
  "available": true
}

Response có result: "created". Kiểm tra mapping mà Elasticsearch đã suy ra:

GET dynamic-books/_mapping

Bạn sẽ thấy pages được suy ra là kiểu số, availableboolean, còn title thường là text kèm keyword. Kết quả chính xác còn phụ thuộc cấu hình dynamic và phiên bản server.

Ví dụ này cho thấy vì sao schema nên được kiểm soát. Nếu document đầu tiên gửi price là chuỗi không có quy ước rõ ràng, các document sau có thể không ghi được giá trị số vào cùng field. Với dữ liệu quan trọng, hãy dùng mapping tường minh như index books ở trên.

Thêm document

Thêm document với ID tự chọn

Dùng PUT /<index>/_doc/<id> khi ứng dụng đã có một mã định danh ổn định. ID book-001 giúp ta đọc, cập nhật hoặc xóa sách mà không phải tìm ID trước.

PUT books/_doc/book-001
{
  "title": "Elasticsearch cho người mới bắt đầu",
  "author": "Nguyễn An",
  "published_at": "2025-01-15",
  "price": 24.5,
  "in_stock": true
}

Response thành công có dạng:

{
  "_index": "books",
  "_id": "book-001",
  "_version": 1,
  "result": "created",
  "_shards": {
    "total": 2,
    "successful": 1,
    "failed": 0
  },
  "_seq_no": 0,
  "_primary_term": 1
}

resultcreated vì đây là document mới. _version là phiên bản nội bộ của document. _seq_no_primary_term được dùng cho cơ chế đảm bảo thứ tự và kiểm soát xung đột; người mới có thể đọc chúng nhưng không cần tự tính toán.

Để Elasticsearch tự sinh ID

Dùng POST /<index>/_doc khi client chưa có ID. Elasticsearch sẽ tạo một ID chuỗi ngẫu nhiên và trả ID đó trong response.

POST books/_doc
{
  "title": "Tìm kiếm dữ liệu với Query DSL",
  "author": "Trần Bình",
  "published_at": "2024-09-20",
  "price": 31.0,
  "in_stock": true
}

Hãy ghi lại giá trị _id trong response. Ví dụ:

{
  "_index": "books",
  "_id": "q7L4...",
  "_version": 1,
  "result": "created",
  "_shards": {
    "total": 2,
    "successful": 1,
    "failed": 0
  }
}

ID tự sinh phù hợp khi ID chỉ có ý nghĩa trong Elasticsearch. Nếu cần ghi đè đúng một thực thể từ hệ thống khác, nên dùng ID tự chọn.

Đọc lại document

Dùng ID đã biết để lấy document đầy đủ:

GET books/_doc/book-001

Response chứa các trường quan trọng sau:

{
  "_index": "books",
  "_id": "book-001",
  "_version": 1,
  "_seq_no": 0,
  "_primary_term": 1,
  "found": true,
  "_source": {
    "title": "Elasticsearch cho người mới bắt đầu",
    "author": "Nguyễn An",
    "published_at": "2025-01-15",
    "price": 24.5,
    "in_stock": true
  }
}

found: true xác nhận document tồn tại. _source là JSON gốc bạn đã gửi. Nếu ID không tồn tại, response có found: false và status HTTP 404.

Thêm nhiều document bằng _bulk

_bulk nhận nhiều thao tác trong một request. Mỗi thao tác gồm một dòng metadatamột dòng document. Đây là NDJSON (JSON được ngăn cách bằng newline), nên không bọc toàn bộ body trong mảng và không thêm dấu phẩy cuối dòng.

POST _bulk
{ "index": { "_index": "books", "_id": "book-002" } }
{ "title": "Phân tích log với Elasticsearch", "author": "Lê Minh", "published_at": "2025-03-10", "price": 28.0, "in_stock": true }
{ "index": { "_index": "books", "_id": "book-003" } }
{ "title": "Kibana thực hành", "author": "Phạm Hà", "published_at": "2024-11-05", "price": 19.5, "in_stock": false }

Response được rút gọn như sau:

{
  "took": 12,
  "errors": false,
  "items": [
    {
      "index": {
        "_index": "books",
        "_id": "book-002",
        "status": 201,
        "result": "created"
      }
    },
    {
      "index": {
        "_index": "books",
        "_id": "book-003",
        "status": 201,
        "result": "created"
      }
    }
  ]
}

errors: false nghĩa là không có thao tác nào trong batch thất bại. Luôn kiểm tra trường này và từng phần tử trong items; HTTP status của cả request không thay thế cho việc kiểm tra từng item. Khi một item lỗi mapping, các item khác vẫn có thể thành công.

Mẹo khi soạn NDJSON

Trong Dev Tools, đặt con trỏ trong request và kiểm tra từng dòng metadata đi cùng đúng một dòng document. Khi gọi từ script hoặc cURL bên ngoài Console, body _bulk còn cần newline ở cuối.

Cập nhật document

Dùng _update/<id> với body doc để thay đổi một số field mà không phải gửi lại toàn bộ document:

POST books/_update/book-001
{
  "doc": {
    "price": 22.0,
    "in_stock": false
  }
}

Response thành công có result: "updated":

{
  "_index": "books",
  "_id": "book-001",
  "_version": 2,
  "result": "updated",
  "_shards": {
    "total": 2,
    "successful": 1,
    "failed": 0
  }
}

Đọc lại GET books/_doc/book-001 để xác nhận _source.price_source.in_stock đã đổi. Nếu muốn thay thế toàn bộ document, dùng lại PUT books/_doc/book-001 với JSON đầy đủ. Cách thay thế đó có thể làm mất field cũ nếu body mới không chứa chúng.

Xóa document và làm mới kết quả tìm kiếm

Xóa một document theo ID:

DELETE books/_doc/book-002

Response thành công có dạng:

{
  "_index": "books",
  "_id": "book-002",
  "_version": 2,
  "result": "deleted",
  "_shards": {
    "total": 2,
    "successful": 1,
    "failed": 0
  }
}

Nếu xóa cùng ID lần nữa, result sẽ là not_found. Đây không phải lỗi kết nối; chỉ là document đã không còn tồn tại.

Elasticsearch ghi thao tác vào translog (nhật ký thay đổi) trước khi document xuất hiện trong các segment tìm kiếm. Refresh là thao tác mở một view tìm kiếm mới để request search nhìn thấy thay đổi. Trong điều kiện bình thường, Elasticsearch tự refresh theo chu kỳ. Khi cần kiểm tra ngay trong bài thực hành, chạy:

POST books/_refresh

Response gồm _shards, trong đó successful cho biết các shard đã xử lý refresh. Với ứng dụng thật, không nên gọi refresh sau từng document vì chi phí I/O cao. Khi cần chờ document có thể tìm thấy trong một request cụ thể, cân nhắc ?refresh=wait_for thay vì ép refresh toàn index.

Kiểm tra _count và xử lý lỗi thường gặp

Đếm document

Sau các bước trên, ta đã tạo book-001, một document có ID tự sinh, book-002book-003, rồi xóa book-002. Vì vậy count dự kiến là 3:

GET books/_count

Response:

{
  "count": 3,
  "_shards": {
    "total": 2,
    "successful": 1,
    "skipped": 0,
    "failed": 0
  }
}

count là số document khớp điều kiện. Không truyền query nghĩa là đếm toàn bộ document trong index. Nếu bạn chạy lại một bước hoặc giữ dữ liệu cũ, con số có thể khác. Để kiểm tra đúng dữ liệu hiện có, chạy thêm:

GET books/_search
{
  "size": 10,
  "sort": [
    { "title.keyword": "asc" }
  ]
}

hits.total.value trong response search là số lượng khớp theo cách đếm mà Elasticsearch áp dụng. hits.hits chứa các document được trả về; size chỉ giới hạn số document hiển thị, không làm giảm tổng count.

Troubleshooting

Bước tiếp theo

Bạn đã có index, mapping và một tập document để thực hành. Hãy chuyển sang Truy vấn đầu tiên để dùng match, term, boolrange trên index books. Nếu muốn quan sát dữ liệu qua giao diện, xem Sample data để nạp dataset vào Kibana và mở Discover.

On this page