Note: see the link below for the English version of this article.

https://duongnt.com/query-boosting-elasticsearch

Query boosting trong Elasticsearch

Elasticsearch là một search engine nổi tiếng dành cho văn bản. Nó hỗ trợ nhiều loại query khác nhau, và ta có thể kết hợp chúng bằng các mệnh đề logic. Tính năng này cho phép ta viết query chi tiết để tìm đúng được văn bản mà ta mong muốn.

Nhưng không phải lúc nào mọi query con trong một query cha cũng có độ quan trọng như nhau. Đôi khi ta muốn tăng hoặc giảm độ quan trọng của một hay nhiều query con. Lúc này, ta có thể dùng query boosting để điều chỉnh độ đóng góp của từng query con vào điểm số cuối cùng của document.

Thiết lập môi trường thử nghiệm

Chạy Elasticsearch trên local

Ta nên chạy Elasticsearch bằng Docker. Dưới đây là guide cách thiết lập một cluster Elasticsearch.

https://www.elastic.co/guide/en/elasticsearch/reference/current/run-elasticsearch-locally.html

Trong đó còn có hướng dẫn cài đặt Kibana. Bài hôm nay sẽ mặc định là ta dùng Dev Tool của Kibana để gửi request tới cluster Elasticsearch.

Tạo một số dữ liệu thử nghiệm

Ta sẽ tạo một index gọi là footballer với 10 văn bản. Mỗi văn bản là một cầu thủ bóng đá với tên, tuổi, vị trí và lương tính bằng nghìn Euro.

PUT footballer/_bulk
{ "create": { } }
{ "name": "Ronaldo","position":"fw", "age": 38, "salary": 4430}
{ "create": { } }
{ "name": "Messi","position":"fw", "age": 36, "salary": 1440}
{ "create": { } }
{ "name": "Sancho","position":"lw", "age": 23, "salary": 373}
{ "create": { } }
{ "name": "Antony","position":"lw", "age": 23, "salary": 200}
{ "create": { } }
{ "name": "Salah","position":"rw", "age": 30, "salary": 350}
{ "create": { } }
{ "name": "Vinicius Junior","position":"lw", "age": 22, "salary": 354}
{ "create": { } }
{ "name": "Mahrez","position":"rw", "age": 32, "salary": 160}
{ "create": { } }
{ "name": "Rashford","position":"fw", "age": 25, "salary": 247}
{ "create": { } }
{ "name": "Bukayo Saka","position":"rw", "age": 21, "salary": 70}
{ "create": { } }
{ "name": "Gnabry","position":"rw", "age": 27, "salary": 365}

Ta có thể dùng query MatchAll để kiểm tra là dữ liệu đã được nhập đúng. Query dưới đây sẽ trả cả 10 văn bản.

GET /footballer/_search
{
  "query": {
    "match_all": {}
  }
}

Tìm một cầu thủ chạy cánh phù hợp

Query đầu tiên

Ta sẽ đóng vai một huấn luyện viên đi tìm cầu thủ chạy cánh. Ta không quan tâm họ chuyên chạy cánh trái hay phải, nhưng ta muốn tìm cầu thủ trẻ từ 23 tuổi trở xuống. Dưới đây là một query thỏa mãn các điều kiện trên.

GET /footballer/_search
{
  "query": {
    "bool": {
      "must": [
        {
          "bool": {
            "should": [
              { "term": { "position": "rw" }},
              { "term": { "position": "lw" }}
            ]
          }
        },
        {
          "range": {
            "age": { "lte": 23 }
          }
        }
      ]
    }
  }
}

Kết quả trả về 4 cầu thủ thỏa mãn tất cả các điều kiện: Sancho, Antony, Vinicius Junior, and Bukayo Saka.

"hits": [
  {
    "_score": 2.1451323,
    "_source": {
      "name": "Sancho",
      "position": "lw",
      "age": 23,
    }
  },
  {
    "_score": 2.1451323,
    "_source": {
      "name": "Antony",
      "position": "lw",
      "age": 23,
    }
  },
  {
    "_score": 2.1451323,
    "_source": {
      "name": "Vinicius Junior",
      "position": "lw",
      "age": 22,
    }
  },
  {
    "_score": 1.8938179,
    "_source": {
      "name": "Bukayo Saka",
      "position": "rw",
      "age": 21,
    }
  }
]

Để ý là điểm của Bukayo Saka thấp hơn những người còn lại. Đó là vì index của ta chứa 4 cầu thủ chạy cánh phải nhưng chỉ có 3 người chạy cánh trái. Vì từ khoá rw phổ biến hơn nên Elasticsearch gán cho nó điểm IDF (inverse document frequency) thấp hơn. Từ đó dẫn đến các văn bản chứa lw có điểm cao hơn.

Tinh chỉnh query của ta

Tiếp theo ta sẽ ưu tiên cầu thủ chạy cánh phải hơn, nhưng vẫn để cơ hội cho cầu thủ chạy cánh trái. Lúc này ta có thể dùng query boosting để tăng điểm của query con { "term": { "position": "rw" }}. Ta sẽ để hệ số boost là 2.

GET /footballer/_search
{
  "query": {
    "bool": {
      "must": [
        {
          "bool": {
            "should": [
              { "term": { "position": { "value": "rw", "boost": 2 }}},
              { "term": { "position": "lw" }}
            ]
          }
        },
        {
          "range": {
            "age": { "lte": 23 }
          }
        }
      ]
    }
  }
}

Kết quả trả về vẫn là 4 cầu thủ như trong phần trước, nhưng bây giờ Bukayo Saka nhảy lên đầu danh sách. Đó là vì Saka là cầu thủ chạy cánh phải, dẫn tới điểm của anh được tăng từ 1.8938179 lên 2.7876358.

"hits": [
  {
    "_score": 2.7876358,
    "_source": {
      "name": "Bukayo Saka",
      "position": "rw",
    }
  },
  {
    "_score": 2.1451323,
    "_source": {
      "name": "Sancho",
      "position": "lw",
    }
  },
  {
    "_score": 2.1451323,
    "_source": {
      "name": "Antony",
      "position": "lw",
     }
  },
  {
    "_score": 2.1451323,
    "_source": {
      "name": "Vinicius Junior",
      "position": "lw",
    }
  }
]

Boost nhiều query con một lúc

Ta có thể boost nhiều query cùng lúc. Trong ví dụ tiếp theo ta vẫn ưu tiên cầu thủ chạy cánh phải. Thêm vào đó ta muốn tìm cầu thủ trẻ hơn 24 tuổi hoặc cầu thủ có lương từ 200 trở xuống, ưu tiên hơn cho cầu thủ lương thấp. Dưới đây là query sau khi cập nhật.

GET /footballer/_search
{
  "query": {
    "bool": {
      "must": [
        {
          "bool": {
            "should": [
              { "term": { "position": { "value": "rw", "boost": 2 }}},
              { "term": { "position": "lw" }}
            ]
          }
        },
        {
          "bool": {
            "should": [
              { "range": { "age": { "lte": 23 }}},
              { "range": { "salary": {"lte": 200, "boost": 2 }}}
            ]
          }
        }
      ]
    }
  }
}

Ta có thể thấy một số thay đổi thú vị trong kết quả, cầu thủ đứng thứ 3 trong danh sách bây giờ là Mahrez. Mặc dù không còn trẻ nhưng anh vẫn được ưu tiên hơn Sancho hay Vinicius Junior bởi vì anh là cầu thủ chạy cánh phải với lương thấp.

"hits": [
  {
    "_score": 4.787636,
    "_source": {
      "name": "Bukayo Saka",
      "position": "rw",
      "age": 21,
      "salary": 70
    }
  },
  {
    "_score": 4.145132,
    "_source": {
      "name": "Antony",
      "position": "lw",
      "age": 23,
      "salary": 200
    }
  },
  {
    "_score": 3.7876358,
    "_source": {
      "name": "Mahrez",
      "position": "rw",
      "age": 32,
      "salary": 160
    }
  },
  {
    "_score": 2.1451323,
    "_source": {
      "name": "Sancho",
      "position": "lw",
      "age": 23,
      "salary": 373
    }
  },
  {
    "_score": 2.1451323,
    "_source": {
      "name": "Vinicius Junior",
      "position": "lw",
      "age": 22,
      "salary": 354
    }
  }
]

Một subquery có thể lấn át tất cả những query còn lại

Khi sử dụng query boosting ta cần cẩn thận không để một query lấn át tất cả những query còn lại. Trong thử nghiệm tiếp theo, ta sẽ tìm tất cả các cầu thủ thỏa mãn một trong các điều kiện dưới đây.

  • Là tiền đạo.
  • Là cầu thủ chạy cánh phải.
  • Là cầu thủ chạy cánh trái.
  • Tuổi từ 23 trở xuống.
  • Lương từ 200 trở xuống.

Và ta sẽ boost điều kiện là tiền đạo với hệ số 10.

GET /footballer/_search
{
  "query": {
    "bool": {
      "must": [
        {
          "bool": {
            "should": [
              { "term": { "position": { "value": "fw", "boost": 10 }}},
              { "term": { "position": { "value": "rw", "boost": 2 }}},
              { "term": { "position": "lw" }},
              { "range": { "age": { "lte": 23 }}},
              { "range": { "salary": {"lte": 200, "boost": 2 }}}
            ]
          }
        }
      ]
    }
  }
}

Query ở trên sẽ trả về tất cả các văn bản trong index của ta, trong đó 3 kết quả đầu tiên là như sau.

"hits": [
  {
    "_score": 11.451323,
    "_source": {
      "name": "Ronaldo",
      "position": "fw",
      "age": 38,
      "salary": 4430
    }
  },
  {
    "_score": 11.451323,
    "_source": {
      "name": "Messi",
      "position": "fw",
      "age": 36,
      "salary": 1440
    }
  },
  {
    "_score": 11.451323,
    "_source": {
      "name": "Rashford",
      "position": "fw",
      "age": 25,
      "salary": 247
    }
  },
  //... omitted
]

Đúng như ta dự đoán, 3 kết quả đầu tiên là 3 tiền đạo: Ronaldo, Messi, Rashford. Mặc dù họ chỉ thỏa mãn 1 điều kiện (vị trí), họ vẫn được xếp trên những người thỏa mãn 3 điều kiện (vị trí, tuổi, lương) như Bukayo Saka hay Antony.

Một số điểm đáng chú ý khác

Hệ số boost có thể nhỏ hơn 1

Giới hạn duy nhất của hệ số boost là không được nhỏ hơn 0. Nếu ta boost một query với hệ số nhỏ hơn 1 thì những văn bản thỏa mãn query đó sẽ vẫn được chọn, nhưng chúng sẽ có xếp hạng thấp. Ví dụ nếu ta đổi query ở phần trước thành { "term": { "position": { "value": "fw", "boost": 0.5 }}} thì Ronaldo, Messi và Rashford sẽ bị xếp cuối cùng.

Hệ số boost cũng có thể bằng 0. Lúc này, những văn bản thỏa mãn query đó vẫn được chọn nhưng bản thân query sẽ không đóng góp gì vào điểm số cuối cùng của văn bản. Ví dụ nếu ta đổi query ở trên thành { "term": { "position": { "value": "fw", "boost": 0 }}} thì Ronaldo, Messi và Rashford sẽ được chọn với điểm số là 0.

Tại sao ta không dùng kiểu boosting query?

Có lẽ các bạn đã nhận ra là ta áp dụng trực tiếp boost lên từng query con mà không dùng kiểu boosting query của Elasticsearch. Cá nhân tôi thích sử dụng boost trực tiếp hơn vì những lý do sau đây:

  • Boosting query yêu cầu ta cung cấp một positive query và một negative query. Tôi thấy rằng yêu cầu như vậy là hơi phức tạp, nhất là trong các trường hợp đơn giản, khi ta chỉ muốn tăng điểm tương ứng của một query con duy nhất.
  • Boosting query chỉ cho phép giảm điểm của negative query mà không cho phép tăng điểm của positive query.
  • Việc sử dụng trực tiếp boost là giống với cách xây dựng query thường thấy trong Elasticsearch. Điều này khiến việc tích hợp với code có sẵn trở nên đơn giản hơn.

Elasticsearch thực hiện việc boost như thế nào?

Nhiều người hiểu lầm rằng điểm cuối cùng của tất cả các document thỏa mãn query được boost sẽ được nhân với hệ số boost. Nhưng từ các ví dụ ở trên ta có thể thấy điều này là không đúng. Trong một ví dụ ở phần trước, điểm của Bukayo Saka chỉ được tằng từ 1.8938179 lên 2.7876358 mặc dù ta sử dụng hệ số boost là 2. Quá trình tính điểm thực tế là như sau:

  • Từng query con trong query cha thực hiện tính điểm từng văn bản một cách độc lập.
  • Nếu một query con được boost thì chỉ điểm của riêng nó mới được nhân với hệ số boost.
  • Cuối cùng tất cả các điểm ở trên sẽ được kết hợp lại để tính điểm cho văn bản. Quá trình này sử dụng một hệ số gọi là coordination factor.

Kết thúc

Query boosting là một tính năng thú vị trong Elasticsearch. Nó cho phép ta điều khiển độ ảnh hưởng của từng query lên điểm số cuối cùng của các văn bản, qua đó cho phép điều chỉnh độ phù hợp của giá trị trả về. Tuy nhiên, khi sử dụng query boosting ta cần hiểu được nó ảnh hưởng tới thuật toán tính điểm như thế nào.

A software developer from Vietnam and is currently living in Japan.

Leave a Reply