Tiếp tục với chuỗi bài giới thiệu về Elasticsearch, ở chương trước chúng ta đã cùng nhau tìm hiểu về cách tìm kiếm dữ liệu với công cụ tìm kiếm này. Tìm kiếm dữ liệu là một chuyện, tuy nhiên để có thể tận dụng được dữ liệu ta nhận được lại là điều không hề dễ dàng. 

Trong tuần này, chúng ta sẽ cùng xem việc sắp xếp và phân trang các dữ liệu ta tìm kiếm được có khó khăn gì không nhé.

1. Sắp xếp kết quả tìm kiếm

Elasticsearch cho phép bạn có thể sử dụng một hoặc nhiều phương thức sắp xếp trên các field cụ thể. Mỗi phương thức sắp xếp đều có thể được đảo nghịch lại trật tự, các phương thức này được định nghĩa trên mức thuộc từng trường dữ liệu. Ngoài ra, mỗi document có các trường đặc biệt là _score cho phép sắp xếp theo score hay _doc cho phép sắp xếp theo thứ tự document được index.

Cú pháp cơ bản để thực hiện sắp xếp:

GET <index>/_search
{
  "query": <DSL>,
  "sort": {
    "<field>": {
      "order": "<sort_order>"
    }
  }
}

Như ở bài trước, chúng ta đã list ra dữ liệu từ một index index-01 và sắp xếp kết quả trả về theo trường “age”.

GET index-01/_search
{
  "query": {
    "match_all": {}
  },
  "sort": {
    "age": {
      "order": "asc"
    }
  }
}

Elasticsearch hiện hỗ trợ chúng ta hai loại thứ tự sắp, đó là asc: theo thứ tự tăng dần, desc: theo thứ tự giảm dần. 

Khi sắp xếp, chúng ta hoàn toàn có thể tận dụng nhiều chế độ sắp xếp khác nhau. Mỗi chế độ sẽ cho phép chúng ta có thể tác động tới kết quả tìm kiếm của mình theo một cách riêng biệt.

2. Phân trang dữ liệu với from và size

Đương nhiên là với các dữ liệu lớn và số lượng dữ liệu là rất nhiều, chúng ta không thể nào trả về cho người dùng tất cả dữ liệu trong cùng một lần tìm kiếm. Cũng vì thế mà một kĩ thuật được gọi là phân trang đã ra đời, và đa số các search engine đều có API hỗ trợ tính năng này, tất nhiên là Elasticsearch cũng không phải làm một ngoại lệ.

Mặc định, các kết quả tìm kiếm sẽ được trả về bao gồm 10 kết quả trùng khớp với yêu cầu tìm kiếm. Để phân trang trên một tập kết quả lớn lớn, chúng ta sẽ sử dụng các biến from và size của API search. Giá trị from chỉ ra số lượng các kết quả tìm kiếm match được bỏ qua, mặc định là 0. Biến size là số lượng tối đa các bản ghi hit được trả về. Việc sử dụng cả hai biến này cho phép chúng ta định nghĩa một trang kết quả:

GET /_search
{
  "from": 5,
  "size": 20,
  "query": {
    "match": {
      "user.id": "kimchy"
    }
  }
}

Một lưu ý nhỏ cho các bạn, đó là việc lạm dụng from và size sẽ gây ảnh hưởng lớn tới hiệu suất tìm kiếm của hệ thống. Nếu paging của bạn quá “sâu” hay cần trả về rất nhiều kết quả, yêu cầu tìm kiếm này sẽ được thực hiện trên nhiều shard và mỗi shard đều phải tải một lượng kết quả tìm kiếm cũng như trang tìm kiếm trước đó và trong memory. Với các page quá xa hay số lượng trả về là cực kỳ lớn nó sẽ làm ảnh hưởng đến cả CPU usage và gây sức ép lớn lên bộ nhớ.

Mặc định Elasticsearch chỉ cho phép paging được thực hiện tới 10000 hits. Giới hạn này là một điểm an toàn được cấu hình bởi giá trị settings index.max_result_window tạo khi bạn mở một index. Nếu paging của chúng ta vượt quá tài liệu thứ 10000 của một index, chúng ta sẽ cần sử dụng giá trị search_after trong kết quả trả về để thay thế cho nó.

GET index-01/_search
{
  "query": {
    "match_all": {}
  },
  "sort": {
    "age": {
      "order": "asc"
    }
  },
  "from": 0,
  "size": 1
}

3. Search after

Ngoài việc sử dụng from và size để thực hiện phân trang từ với số lượng document cụ thể từ một điểm bắt đầu cụ thể, Elasticsearch cũng cho phép chúng ta sử dụng biến search_after để nhận về trang tiếp theo của kết quả tìm kiếm sử dụng một số kết quả nhận được từ trang trước đó. 

Khi sử dụng search_after để tìm kiếm, chúng ta cần sử dụng cùng query và sort cho toàn bộ các câu tìm kiếm để đảm bảo dữ liệu của chúng ta là chuẩn xác. Cách sử dụng search_after có thể được mô tả qua cú pháp sau:

GET <index>/_search
{
  "query": <query>,
  "size": <size>,
  "sort": <sort>
}

Trong kết quả trả về của truy vấn tìm kiếm này, chúng ta sẽ lấy các field được sort trong kết quả tìm kiếm cuối cùng và cho nó vào trong array search_after để lấy về page tiếp theo. Để minh họa cho cách tìm kiếm này, chúng ta sẽ cùng tìm hiểu ví dụ dưới đây. 

GET index-01/_search
{
  "query": {
    "match_all": {}
  },
  "size": 1,
  "sort": [
    {
      "age": {
        "order": "asc"
      }
    },
    {
      "id.keyword" : {
        "order": "asc"
      }
    }
  ]
}

>>> Xem thêm bài viết:

Tìm hiểu về ràng buộc (Constraint) trong SQL

Tìm hiểu về cơ sở dữ liệu phi quan hệ MongoDB

Sử dụng map reduce trong MongoDB

Sử dụng truy vấn như vậy chúng ta thu được kết quả sau:

{
  "took": 0,
  "timed_out": false,
  "_shards": {
    "total": 1,
    "successful": 1,
    "skipped": 0,
    "failed": 0
  },
  "hits": {
    "total": {
      "value": 4,
      "relation": "eq"
    },
    "max_score": null,
    "hits": [
      {
        "_index": "index-01",
        "_id": "i5IEpYsB-XeRF7h4FTkP",
        "_score": null,
        "_source": {
          "age": 18,
          "email": "john.doe@gmail.com",
          "name": "John Doe",
          "id": "0000002"
        },
        "sort": [
          18,
          "0000002"
        ]
      }
    ]
  }
}

Trong kết quả trả về, chúng ta sẽ chú ý tới bản ghi cuối cùng. Tại đây, chúng ta lấy hai giá trị được sử dụng trong câu lệnh sắp xếp, đó là thuộc tính age và id. Các giá trị tìm kiếm này được  để trong thuộc tính sort nằm trong các document kết quả tìm kiếm, ở đây giá trị tìm kiếm được là [18,"0000002"],đây chính là thông tin chúng ta cần để làm search_after cho lần tìm kiếm tiếp theo. Chúng ta sẽ tìm kiếm với câu lệnh như sau:

GET index-01/_search
{
  "query": {
    "match_all": {}
  },
  "size": 1,
  "sort": [
    {
      "age": {
        "order": "asc"
      }
    },
    {
      "id.keyword": {
        "order": "asc"
      }
    }
  ],
  "search_after": [
    18,
    "0000002"
  ]
}

Và như thế, chúng ta đã có thể phân trang bằng cách tìm kiếm từng trang kết quả một sử dụng search_after.

Kết

Trên đây là bài viết của Stringee về các cách sắp xếp dữ liệu và phân trang trên Elasticsearch,  tìm kiếm và phân trang là một phần không thể thiếu trong việc phát triển các chương trình có tính áp dụng thực tiễn cao. Tuy nhiên, nếu chúng ta không có đủ kiến thức cũng như kinh nghiệm để lựa chọn cách sử dụng linh hoạt các tính năng trên sẽ không mang lại nhiều hiệu quả. Với các kiến thức trên, mong bạn có thể phần nào hiểu và có thể vận dụng chúng vào công việc của mình.


Stringee Communication APIs là giải pháp cung cấp các tính năng giao tiếp như gọi thoại, gọi video, tin nhắn chat, SMS hay tổng đài CSKH cho phép tích hợp trực tiếp vào ứng dụng/website của doanh nghiệp nhanh chóng. Nhờ đó giúp tiết kiệm đến 80% thời gian và chi phí cho doanh nghiệp bởi thông thường nếu tự phát triển các tính năng này có thể mất từ 1 - 3 năm.

Bộ API giao tiếp của Stringee hiện đang được tin dùng bởi các doanh nghiệp ở mọi quy mô, lĩnh vực ngành nghề như TPBank, VOVBacsi24, VNDirect, Shinhan Finance, Ahamove, Logivan, Homedy,  Adavigo, bTaskee…

Quý bạn đọc quan tâm xin mời đăng ký NHẬN TƯ VẤN TẠI ĐÂY: