12/08/2018, 15:43

Giới thiệu searchkick - gem hỗ trợ tìm kiếm trong Rails

Tìm kiếm là tính năng không thể thiếu của một trang web thời nay, và ElasticSearch là cái tên quá nổi tiếng. Tuy nhiên, trong bài viết này, mình muốn đề cập đén searchkick - gem hỗ trợ tìm kiếm rất tốt, dễ sử dụng hơn ES và còn quen thuộc hơn với Ruby dev. Link gem Searchkick. Cùng tìm hiểu qua ...

Tìm kiếm là tính năng không thể thiếu của một trang web thời nay, và ElasticSearch là cái tên quá nổi tiếng. Tuy nhiên, trong bài viết này, mình muốn đề cập đén searchkick - gem hỗ trợ tìm kiếm rất tốt, dễ sử dụng hơn ES và còn quen thuộc hơn với Ruby dev.

Link gem Searchkick. Cùng tìm hiểu qua đôi chút về gem này.

Intelligent search made easy

it gets smarter and the results get better. It’s friendly for developers - and magical for your users

Theo như lời giới thiệu thì searchkick có thể giúp chúng ta xử lý một số vấn đề như:

  • Tìm kiếm từ cùng một nguồn gốc: tomato, tomatoes...
  • Ký tự đặc biệt: jalapeño.
  • Xử lý khoảng trắng: hiểu dishwasher là dish washer.
  • Sai chính tả: hiểu zuchini là zucchini.
  • Từ đồng nghĩa: nhập qtip thì có thể tìm kiếm cả cotton swab

Cài đặt.

Để sử dụng, bạn cần phải cài đặt ElasticSearch, có thể tham khảo cách đơn giản bằng câu lệnh như sau:

sudo apt-get update
sudo apt-get install elasticsearch

sudo service elasticsearch start
sudo service elasticsearch restart

Hoặc có thể cài đặt và chạy qua brew:

brew install elasticsearch
brew services start elasticsearch

Cài đặt gem searchkick thì đơn giản chỉ cần thêm khai báo vào Gemfile và tiến hành bundle

# Gemfile
gem 'searchkick'

Ở trong model nào cần dùng để tìm kiếm thì chỉ cần khai báo searchkick vào là được.

  • Chú ý là phiên bản mới nhất của searchkick làm việc được với ElasticSearch 2 và 5. Còn với ElasticSearch 1 thì phải sử dụng phiên bản 1.5.1

Hãy cùng nhau tìm hiểu qua một chút về gem này.

Query

Query trong searchkick tương tự như trong SQL. ex:

Product.search(
  'apples',
  fields: [:name, :brand],
  where: {
    in_stock: true,
    store_id: { not: [25,30, 35] } #store_id not in [25,30,35]
  },
  limit: 10,
  offset: 50,
  order: { _score: :desc }
)

Nếu muốn tìm kiếm tất cả thì dùng search với '*'.

Nếu làm việc với SQL rồi thì chắc hẳn bạn sẽ rất dễ hiểu.

Kết quả tìm kiếm.

Sau khi tiến hành tìm kiếm bằng searchkick, ta sẽ có một đối tượng Searchkick::Results, đối tượng này có những method gần giống như một array.

results = Product.search('milk')
results.size
results.any?
results.each { |result| ... }

Vì ElasticSearch đã tiến hành đánh index lại các bản ghi (trường id được đánh index), nên các id sẽ được lấy về từ ElasticSearch còn nội phần còn lại của bản ghi thì sẽ được lấy ra từ database. Nếu bạn muốn lấy về mọi thứ từ ElasticSearch thì hãy thêm key load: false vào method `search

Ngoài ra còn có một số method khác với đối tượng results như:

results.total_count # Get total results

results.took # Get the time the search took (in milliseconds)

results.response # Get the full response from Elasticsearch

Boosting

Cũng tương tự như trong ElasticSearch

# Tìm kiếm với trường title được ưu tiên hơn description
Product.search('milk', fields: ['title^10', 'description'])

Product.search('milk', boost_by: { orders_count: { factor: 10 } })

Phân trang.

Kết quả tìm kiếm có thể dễ dàng được phân trang với key page và per_page

Tìm kiếm từng phần

Mặc định, kết quả tìm kiếm phải khớp với tất cả các từ trong đầu vào (mặc định sử dụng phép toán AND). ex:

Product.search 'fresh honey' # fresh AND honey

Để tìm kiếm được từng phần của đầu vào thì bạn hãy thêm key operator: 'or' vào phương thức search.

Với từng từ trong chuỗi đầu vào, kết quả tìm kiếm phải khớp với toàn bộ từ đó. Ví dụ đầu vào là back thì sẽ không tìm kiếm được backpack. Để cải thiện việc này thì bạn phải thêm option word_start (mặc định là word) với tên trường vào khai báo searchkich. ex:

class Product < ActiveRecord::Base
  searchkick word_start: [:name]
end

Và thêm vào cả method search (sau khi bạn đã reindex - cái này mình sẽ đề cập sau) như sau:

Product.search 'back', fields: [:name], match: :word_start

Còn một số option khác mà searchkick cũng hỗ trợ như: word, word_start, word_middle, word_end, text_start,...

Ngôn ngữ

searchkick mặc định tìm kiếm với tiếng Anh, nhưng bạn cũng có thể sử dụng với ngôn ngữ khác bằng cách khai báo key language trong model.

Tham khảo danh sách các ngôn ngữ được hỗ trợ ở đây, tiếc là chưa có tiếng Việt.

Tìm kiếm từ đồng nghĩa.

Việc này sẽ phải thực hiện hơi thủ công hơn một chút bằng cách thêm key synonyms vào khai báo searchkick. Có 2 cách:

  • Khai báo trực tiếp synonyms là một mảng chứa các mảng con các cặp từ đồng nghĩa, ex:
class Product < ActiveRecord::Base
  searchkick synonyms: [["scallion", "green onion"], ["qtip => cotton swab"]]
end

  • Khai báo synonyms là đường dẫn đến file chứa các cặp từ đồng nghĩa.

Sau khi khai báo synonyms thì bạn phải tiến hành dánh index lại bằng method .reindex

Lỗi chính tả.

searchkick xử lý việc sai chính tả bằng cách tính khoảng cách Levenshtein. Mặc định, text sẽ khớp nếu như khoảng cách đó là 1.

ex khoảng cách giữa kitten và sitting là 3, vì bạn cần ít nhất 3 thay đổi để biến đổi từ này thành từ kia:

  1. kitten → sitten (thay thế ký tựk bằng s)

  2. sitten → sittin (thay thế ký tự e bằng i)

  3. sittin → sitting (chèn thêm ký tự g vào cuối)

Bạn có thể customer lại khoảng cách tính toán mà bạn muốn bằng key misspellongs: { edit_distance: 2 }

Kết quả không mong muốn.

Bạn có thể nạp vào key exclude để loại bỏ những kết quả không mong muốn tương ứng với từng đầu vào, ex:

Product.search 'cream', exclude: ['ice cream', 'whipped cream']

Đánh index.

Để kiểm soát những dữ liệu nào sẽ được đánh index thì ta dùng hàm search_data, ex:

class Product < ActiveRecord::Base
  belongs_to :department

  def search_data
    {
      name: name,
      department_name: department.name,
      on_sale: sale_price.present?
    }
  end
end

searchkick dùng method find_in_batches để load dữ liệu, nên bạn cũng có thể tiến hành eager load các quan hệ để tăng tốc độ cũng như tránh tình trạng N+1 query, sử dụng scope search_import như sau:

class Product < ActiveRecord::Base
  scope :search_import, -> { includes(:department) }
end

Mặc định, searchkick sẽ đánh index cho tất cả bản ghi. Để tránh việc phải tiến hành đánh index cho cả những bản ghi không cần thiết thì bạn phải sử dụng method should_index? cùng với scope search_import như sau:

class Product < ActiveRecord::Base
  scope :search_import, -> { where(active: true) }

  def should_index?
    active # only index active records
  end
end

Sau những bước này thị bạn cũng cần tiến hành đánh index lại cho model.

Khi nào cần đánh index lại.

Trong một số trường hợp thì bạn cần phải tiến hành đánh lại index cho model của mình bằng method .reindex:

  • Khi bạn cài đặt hoặc nâng cấp phiên bản searchkick.

  • Khi thay đổi method search_data như đã đề cập ở trên.

  • Khi bạn thay đổi method khai báo searchkick.

Ngoài ra thì seachkick còn hỗ trợ một số tính năng khác cho tìm kiếm như autocomplete, gợi ý... Mình sẽ cố gắng làm một cái demo đơn giản sử dụng những tính năng này sau nhé.

Cảm ơn bạn đã đọc bài viết.

Tham khảo.

  • https://github.com/ankane/searchkick
  • https://en.wikipedia.org/wiki/Levenshtein_distance
  • https://www.elastic.co/guide/index.html
0