+15

[Phỏng vấn Backend]: Mô tả cách bạn sử dụng elasticsearch trong thực tế?

I. Giới thiệu

Ở các bài viết trước, đã nêu lý do tại sao sử dụng elasticsearch. Bài viết này mình sẽ nói về cách mình sử dụng elasticsearch trong thực tế. Zô chủ đề chính luôn nào...

II. Thiết kế Schema (mapping)

1. Phân tích yêu cầu

  • Biết người biết ta, mới trăm trận trăm thắng được. Đầu tiên mình sẽ xác định yêu cầu của business, từ đó sẽ tìm các trường dữ liệu chính cần lưu trữ. Mục tiêu business mình làm là tìm sản phẩm phù hợp mà phí ship rẻ nhất cho người mua. Trong trường hợp của mình là một số trường của sản phẩm (id, tên, description, price) cùng với các nhà cung cấp (id, vị trí địa lý).

2. Cấu trúc document

Có nhiều cấu trúc trong Elasticsearch để lựa chọn như:

  • Flat structure: Cấu trúc phẳng, đơn giản nhất, dễ chỉnh sửa, hiệu suất cao.
  • Nested objects: Cho phép lưu trữ mảng các object phức tạp.
  • Parent-child relationships: Cho các mối quan hệ phức tạp giữa các entities.

Nói qua về dự án, mình đang xây dựng một thứ gần giống sàn e-commerce, nơi các chủ cửa hàng có thể tạo và quản lý sản phẩm mà về cơ bản nó là một thứ gì đó độc lập dù có tên giống với các cửa hàng khác. Đặc điểm này thì lại phù hợp với flat structure

{
  "product_id": "P001_S001",
  "store_id": "S001",
  "store_location": {
    "lat": 10.762622,
    "lon": 106.660172
  },
  "product_name": "Smartphone X của cửa hàng A",
  "product_description": "Smartphone X bản đặc biệt chỉ có tại cửa hàng A",
  "price": 999.99
}

3. Kiểu dữ liệu

Có nhiều loại data type khác nhau để lựa chọn:

  • Text: Cho full-text search
  • Keyword: Cho exact matching và aggregations
  • Numeric types: (long, integer, short, byte, double, float)
  • Date
  • Boolean
  • Geo-point và Geo-shape: Cho dữ liệu địa lý
  • IP: Cho địa chỉ IP

III. Indexing

Analyzers là bộ não của Elasticsearch, đóng vai trò phân tích và chuẩn hóa dữ liệu, phân tích văn bản thành các đơn vị nhỏ hơn (thường là từ, cụm từ), tạo tiền đề cho những phép tìm kiếm chính xác và hiệu quả.

1. Quá trình phân tích

Quá trình analyzers sẽ gồm có 3 giai đoạn:

  1. Character filters: Làm sạch văn bản thô.

    • Loại bỏ HTML tags
    • Chuyển đổi ký tự & thành and
    • Thay thế các ký tự đặc biệt khác
  2. Tokenizer: chia văn bản đầu vào thành các từ hoặc cụm từ riêng lẻ (gọi là token). Ví dụ: "Hiếu học code đẹp trai" có thể được chia thành ["Hiếu", "học", "code", "đẹp", "trai"].

  3. Token filters: Áp dụng các bộ lọc để thay đổi, thêm hoặc xóa các token. Có thể bao gồm:

    • Chuyển đổi chữ hoa thành chữ thường
    • Loại bỏ dấu câu
    • Loại bỏ từ dừng (stop words): từ dừng là mấy từ không có nghĩa nhiều (à, ừ, nhưng, mà...)
    • Tạo từ gốc (stemming) hoặc từ nguyên mẫu (lemmatization). Ví du worked -> work
  4. Indexing: Sau khi đã làm sạch dữ liệu thì Elasticsearch tạo inverted index để lưu trữ thông tin về các token và các tài liệu chứa chúng. (Inverted index là gì thì mình đã bàn ở phần 1 rồi)

2. Bốn kiểu Analyzers phổ biến

Elasticsearch cung cấp sẵn 4 loại analyzers để đáp ứng các nhu cầu khác nhau:

  • Standard analyzer: Nó phân tích văn bản thành các từ, loại bỏ dấu câu, và chuyển thành chữ thường. Ví dụ: "Hello, World!" → ["hello", "world"]. Dự án mình không có gì đặc biệt lắm nên dùng loại thường này.

  • Simple analyzer: Đơn giản nhưng hiệu quả. Nó chỉ quan tâm đến chữ cái và bỏ qua mọi thứ khác. "The Quick-Brown Fox" → ["the", "quick", "brown", "fox"]

  • Whitespace analyzer: Nó chỉ tách từ bằng khoảng trắng, giữ nguyên chữ hoa/thường. "The Quick Brown Fox" → ["The", "Quick", "Brown", "Fox"]

  • Language-specific analyzers: Elasticsearch hiểu được đặc thù của từng ngôn ngữ. Ví dụ, tiếng Anh sẽ biến "dogs" thành "dog", tiếng Việt có thể xử lý dấu thanh.

  • Ngoài ra còn các loại khác như Keyword, Pattern, Fingerprint, Custom.

IV. Chiến lược reindexing và zero-downtime updates

1. Thay đổi nhỏ của data (insert, update, delete)

Có 2 cách mà mình nghĩ đến cho việc sync data giữa database chính (mình dùng postgres) với elasticsearch:

  • Chạy cronjob: sau một khoảng thời gian thì sẽ vô database xem có update gì mới không thì bỏ lên Elastichsearch. Cách này thì đơn giản, nhưng không đảm bảo dữ liệu được đồng bộ ngay.
  • Trigger sự kiện thay đổi (CDC): (trigger bằng Write-ahead log (WAL) hoặc API trực tiếp), có thay đổi thì gửi yêu cầu update đến message queue -> backend service write elasticsearch sẽ nhận request và update lên ES. Cách này được cái sync riêu time hơn nhưng tốn công làm.

2. Tạo index mới:

Sau một thời gian, do nhu cầu business thay đổi nên mình phải sửa lại mapping của Elasticsearch. Mình cần phải tạo ra 1 index mới, và bài toán lúc này là làm sao để đồng bộ dữ liệu từ index cũ sang index mới mà không làm ảnh hưởng đến trải nghiệm của người dùng. Lúc này mình xử lý như sau:

  1. Tạo một index mới với mapping mới phù hợp với cấu trúc dữ liệu mới.

  2. Sử dụng Reindex API để copy dữ liệu từ index cũ sang index mới.

  3. Sử dụng alias để chuyển đổi traffic sang index mới. Ví dụ: products_v1 -> products_alias -> products_v2

  4. Thực hiện rolling update cho đến khi hết traffic trong index cũ.

  5. Đóng index cũ sau khi hoàn tất chuyển đổi.

Mặc dù chuyển xong nhưng không xoá ngay mà phải để một thời gian dài sau mình mới xoá để tránh tình trạng có vấn đề với index mới thì lại không có đường nào rollback. Việc quản lý vòng đời của index có thể sử dụng index lifecycle management (ILM) giúp lên kết hoạch xoá hoặc archive index cũ sau một thời gian nhất định.

V. Query Optimization

1. Sử dụng query DSL hiệu quả

Elasticsearch sử dụng một ngôn ngữ đặc biệt gọi là Query DSL (match, multi_match, bool, term) để xây dựng các truy vấn. DSL cung cấp rất nhiều loại truy vấn khác nhau, và việc chọn đúng loại truy vấn cho trường hợp cụ thể là rất quan trọng để có được kết quả nhanh và chính xác.

  • match: Được sử dụng cho full-text search (tìm kiếm toàn văn bản). Elasticsearch sẽ phân tích chuỗi đầu vào và so khớp nó với các token đã được lập chỉ mục.
  • multi_match: Tương tự như match, nhưng cho phép tìm kiếm qua nhiều trường cùng lúc. Điều này hữu ích khi bạn muốn tìm kiếm một từ khóa trong nhiều thuộc tính khác nhau của tài liệu.
  • bool query: Dùng để kết hợp các truy vấn khác nhau bằng các toán tử logic (như must, should, must_not, filter). bool cho phép bạn tạo các truy vấn phức tạp, bao gồm nhiều điều kiện.
  • Boosting là kỹ thuật để tăng hoặc giảm tầm quan trọng của một số trường hoặc kết quả trong quá trình tính điểm relevance score.

2. Áp dụng filters thay vì queries khi có thể:

Trong Elasticsearch, filtersqueries có những cách hoạt động khác nhau. Filters không tính relevance score (điểm liên quan) cho các tài liệu, trong khi queries thường tính toán độ phù hợp giữa truy vấn và tài liệu để sắp xếp kết quả. Điều này có nghĩa là các filters nhanh hơn queries trong nhiều trường hợp.

Khi sử dụng filter, Elasticsearch có thể lưu cache kết quả của filter đó. Nếu chạy lại cùng một truy vấn hoặc một filter tương tự, Elasticsearch sẽ dùng lại kết quả từ cache thay vì phải tính toán lại từ đầu, điều này làm tăng hiệu suất đáng kể.

3. Tối ưu hóa full-text search:

  • Cấu hình analyzers phù hợp cho từng trường. Ví dụ: đối với trường tên sản phẩm, cần một analyzer để loại bỏ ký tự đặc biệt và chuyển văn bản thành chữ thường.
  • synonyms (từ đồng nghĩa): ví dụ như "xe máy" và "honda"
  • stopwords (từ vựng): ví dụ những từ phổ biến như "và", "hoặc", "nhưng" thường không mang nhiều ý nghĩa khi tìm kiếm thì nên vứt ra.
  • fuzzy search: giúp tìm kiếm các từ gần đúng với từ khóa người dùng nhập vào. Ví dụ người dùng nhập sai từ lợn thành loz thì vẫn ra được.
  • ngram tokenizers là một kỹ thuật chia nhỏ từ thành các mẩu nhỏ hơn. Ví dụ: Từ "search" có thể được chia thành các ngram như: se, ear, arc, rch
  • Aggregations cho phép thực hiện các phép tính và phân tích dữ liệu trực tiếp trong truy vấn tìm kiếm. Điều này rất hữu ích để tạo các insights phức tạp từ dữ liệu.
    • Bucket aggregations giúp phân nhóm dữ liệu thành các nhóm (buckets) dựa trên các điều kiện nhất định. Ví dụ phân loại theo "price range"
    • Metric aggregations thực hiện các phép tính thống kê như tính tổng, trung bình, cực đại, cực tiểu, hoặc đếm (count) trên một tập hợp dữ liệu. Ví dụ: tính tổng doanh số hoặc số lượng sản phẩm trong từng nhóm đã được phân chia bởi bucket aggregation.
    • Hoặc kết hợp nhiều loại aggregations để tạo ra insights phức tạp hơn.

4. Caching và Performance Tuning

  • Cấu hình query cache:
    • Bật query cache cho các truy vấn thường xuyên được sử dụng
    • Điều chỉnh kích thước cache phù hợp với bộ nhớ có sẵn
    • Sử dụng request cache cho các truy vấn search và aggregation
  • Sử dụng field data cache:
    • Cấu hình field data cache cho các trường thường xuyên được sử dụng trong sorting và aggregations
    • Giới hạn kích thước field data cache để tránh OOM errors
  • Tối ưu hóa JVM settings:
    • Điều chỉnh heap size phù hợp (không quá 50% RAM vật lý)
    • Cấu hình garbage collection để giảm thiểu độ trễ (latency)
    • Sử dụng G1GC cho các cluster lớn

5. Scale up

Có một vài điểm lúc mới tìm hiểu về ES mình confuse nhẹ, tại trước đó mình học mongodb nên thấy khái niệm của ES vừa giống mà lại vừa khác đó là:

  • Trong MongoDB, một shard thường là một replica set độc lập, có thể chứa nhiều nodes.
  • Trong Elasticsearch, một node có thể chứa nhiều shards, và mỗi shard là một phần của một index.

Về phần scale up thì có một số điểm cần lưu ý như sau:

6. Shards và replicas

  • Tính toán số lượng shards dựa trên kích thước dữ liệu và hardware: Quy tắc chung là mỗi shard nên có kích thước hợp lý để tránh quá tải cho một shard cụ thể. Kích thước shard lý tưởng phụ thuộc vào yêu cầu hiệu suất của ứng dụng, nhưng thường thì từ 10GB đến 50GB là phù hợp cho một shard.
  • Cân bằng giữa hiệu suất và tính sẵn sàng khi cấu hình replicas: Số lượng replicas ảnh hưởng đến hiệu suất và tính sẵn sàng. Càng nhiều replicas, hệ thống sẽ có khả năng phục hồi nhanh khi có sự cố và tăng khả năng xử lý các yêu cầu đọc. Thường thì mình làm 3 con 1 write, 2 read.
  • Sử dụng forced awareness để đảm bảo high availability: tính năng giúp Elasticsearch biết về zone hoặc availability domain trong một môi trường nhiều máy chủ (cluster). đảm bảo rằng các shard chính (primary shard) và bản sao (replica) không nằm trên cùng một zone, giúp đảm bảo hệ thống hoạt động dù 1 zone nào đó bị lỗi.

Chiến lược sharding phù hợp:

Mặc định, Elasticsearch phân phối dữ liệu đến các shard một cách ngẫu nhiên, nhưng bạn có thể sử dụng custom routing để kiểm soát chi tiết hơn việc phân phối dữ liệu.

Ví dụ: Nếu bạn có các bộ dữ liệu lớn liên quan đến một khách hàng cụ thể và muốn giữ chúng trong cùng một shard để tối ưu hóa tìm kiếm, bạn có thể sử dụng custom routing bằng cách đặt routing key là customer_id.

Tham khảo

Group discord 2k+ mems: chém gió về lập trình và làm pet project cùng nhau


All rights reserved

Viblo
Hãy đăng ký một tài khoản Viblo để nhận được nhiều bài viết thú vị hơn.
Đăng kí