Elasticsearch gem Searchkick
Bài đăng này đã không được cập nhật trong 5 năm
Searchkick là gem support search sử dụng Elasticsearch.
Searchkick support các tính năng sau:
- stemming ví dụ
tomatoessẽ match vớitomato - special characters ví dụ
jalapenosẽ match vớijalapeño - extra whitespace ví dụ
dishwashersẽ match vớidish washer - misspellings ví dụ
zuchinisẽ match vớizucchini - custom synonyms ví dụ
popsẽ match vớisoda
Bên cạnh đó còn có:
- query like SQL
- reindex without downtime
- personalize result for each user
- auto complete
- "Did you mean" suggestions
- supports manu languages
- work with ActiveRecord, Mongoid và NoBrainer
1. Getting started:
- Chúng ta sẽ tạo 1 project để bắt đầu tìm hiểu gem seachkick
rails new elastic_search rails g model product name rails db:migrate - Cài đặt các gem cần thiết
# Gemfile gem "ffaker" gem "searchkick" - Tạo seed file
# db/seeds.rb 100.times do Product.create name: FFaker::Lorem.words.join(" ") end - Thêm searchkick vào Product
# app/models/product.rb class Product < ApplicationRecord searchkick end - Chạy reindex cho Product trên rails console để push data lên server Elasticsearch
Product.reindex - Sau khi đã chạy xong reindex, bạn có thể dùng searchkick để search product
Product.search("perferendis") - Hoặc để search tất cả product thì ta search với key word là "*"
Product.search("*") - Bạn có thể thấy trên log console, searchkick đã tạo 1 request dưới dạng curl sử dụng Elasticsearch DSL, chúng ta sẽ tìm hiểu kỹ thêm về Elasticsearch DSL ở bài sau
![]()
- Trong source code tham khảo đã implement sẵn 2 service để bạn có thể thử 2 loại query cơ bản trên
Search::AllService.new.perform Search::SimpleService.new("your key word").perform
2. Querying:
a. Where with specific value:
- Searchkick nhận câu query như câu query của SQL để search.
- Ví dụ:
Product.search "your key word", where: {in_stock: true} - Trong câu query,
paramswhere nhận giá trị làhashdạngkey-value - Trong đó key là tên của attribute, value là mô tả giá trị của attribute đó
- Searchkick sẽ thực hiện filter và chỉ search trên các product có các attribute thỏa giá trị của params where.
- Trong ví dụ trên chỉ thực hiện trên các product có attribute in_stock là true.
- Trong source code tham khảo đã implement sẵn service để bạn có thể thử query where
Search::Where::SimpleService.new("your key word", in_stock: false).perform
b. Where with range value:
- Trong các trường hợp giá trị của attribute không phải là giá trị biết trước (như true hoặc false) mà nằm trong 1 khoảng giá trị nhất định (như từ 10 đến 20) ta sử dụng câu truy vấn với
gtevàlte. - Ví dụ:
Product.search "your key word", where: {orders_count: {gte: 10, lte: 20}} Product.search "your key word", where: {orders_count: (10..20}} - Trong source code tham khảo đã implement sẵn service để bạn có thể thử query where với
gtevàlteSearch::Where::RangeService.new("your key wor", gte: "10", lte: "20").perform
c. Where with in:
- Trong trường hợp giá trị của attribute không nằm trong 1 khoảng liên tục (ví dụ:
[1, 2, 3, 4, 5]) mà là 1 mảng rới rạc (ví dụ[1, 3, 5]) ta sử dụng câu truy vấn vớiin: - Ví dụ:
Product.search "your key word", where: {store_id: {in: [1, 3, 5, 7, 9]}} - Trong source code tham khảo đã implement sẵn service để bạn có thể thử query where với
inSearch::Where::InService.new("your key word", store_ids: [1, 3, 5, 7, 9]).perform
d. Where with not:
- Ngược lại với in ta có
not, searchkick sẽ search trên các product có giá trị của attribute không nằm trong mảng giá trị cho trước: - Ví dụ:
Product.search "your key word", where: {store_id: {not: [1, 3, 5, 7, 9]}} - Trong source code tham khảo đã implement sẵn service để bạn có thể thử query where với
notSearch::Where::NotService.new("your key word", store_ids: [1, 3, 5, 7, 9]).perform
e. Where with all:
- Tương tự với với in ta có
all, searchkick sẽ search trên các product có giá trị của attribute bằng với mảng giá trị cho trước: - Ví dụ:
# Trả về các product có product.store_id là 1 hoặc 3, 5, 7 Product.search "your key word", where: {store_id: {in: [1, 3, 5, 7, 9]}} # Trả về các product có product.store_id là [1, 3, 5, 7, 9] Product.search "your key word", where: {store_id: {all: [1, 3, 5, 7, 9]}} - Trong source code tham khảo đã implement sẵn service để bạn có thể thử query where với
notSearch::Where::AllService.new("your key word", store_ids: [1, 3, 5, 7, 9]).perform Search::Where::AllService.new("your key word", store_ids: [1]).perform - Với câu truy vấn thứ nhất kết quả trả về luôn là mảng rỗng vì
product.store_idkhông bap giờ trả về mảng
f: Where with all and array value:
- Với lý do đã nêu ở trên,
allthường được dùng với các attribute trả về mảng hơn là gia trị duy nhất. - Mình tạo thêm các model có quan hệ như sau
# app/models/product.rb has_many :order_details has_many :orders, through: :order_details # app/models/order.rb has_many :order_details has_many :products, through: :order_details - Khi đó ta có thể gọi product.order_ids và nhận được giá trị trả về là mảng
Product.first.order_ids = [1, 2, 3] - Ta thử search product theo order_ids sử dụng all như sau
- Ví dụ:
Product.search("your key word", where: {order_ids: [1, 2, 3]) - Kết quả trả về luôn là mảng rỗng.
g: Where with all and search_data and search_import:
- Nguyên nhân là vì khi thực hiện reindex, data searchkick gửi lên elasticsearch mặc định là các column của table Product.
- Tức là searchkick chỉ gửi lên elasticsearch data về
id,name,created_at,updated_at,in_stock,orders_countvàstore_id. - Do đó khi search với order_ids của product thì elasticsearch luôn trả về mảng rỗng.
- Searchkick quy định data gửi lên elasticsearch bằng method
search_data, sửa lại method này như sau và thêm scopesearch_importnhư sau# app/models/product.rb class Product < ApplicationRecord belongs_to :store has_many :order_details has_many :orders, through: :order_details searchkick scope :search_import, -> {includes(:orders)} def search_data attributes.merge order_ids: order_ids end end - Thực hiện reindex cho Product
Product.reindex - Chạy lại câu query với
allvà ta thu được kết quảProduct.search("your key word", where: {order_ids: [1, 2, 3]) - Chúng ta sẽ tìm hiểu về
search_datavàsearch_importkĩ hơn ở phần sau. - Chú ý câu query
allchỉ search trên các product có order_ids là mảng con hoặc chính là mảng tham số truyền vào. - Trong source code tham khảo đã implement sẵn service để bạn có thể thử query where với
notSearch::Where::AllWithArrayService.new("your key word", order_ids: [1, 2, 3]).perform
h: Where with exists:
- Ta cũng có thể thực hiện filter trên các product có store_id với query
exists: true.Product.search("your key word", where: {store_id: {exists: true}}) - Tuy nhiên để filter trên các product có store_id là nil thì không sử dụng query exists: true được mà phải gọi trực tiếp
store_id: nilnhư sauProduct.search("your key word", where: {store_id: nil}) - Trong source code tham khảo đã implement sẵn service để bạn có thể query với 2 trường hợp trên
Search::Where::ExistsService.new("your key word", store_exists: true).perform Search::Where::ExistsService.new("your key word", store_exists: false).perform
i: Where with and:
- Operator mặc định của where là
and, record trả về phải thỏa tất cả query của where, ví dụProduct.search("your key word", where: {store_id: 1, orders_count: {gte: 70, lte: 100}}) - Ta có thể viết lại với query
_andnhư sauProduct.search("your key word", where: { _and: [ {store_id: 1}, {orders_count: (70..100)} ] }) Search::Where::AndService.new("your key word", store_id: 1, orders_count: 70..100).perform
j: Where with or:
- Trong trường hợp filter các product thỏa 1 trong 2 mệnh đề thì ta sử dụng query
_ornhư sauProduct.search("your key word", where: { _or: [ {store_id: 1}, {orders_count: (70..100)} ] }) Search::Where::OrService.new("your key word", store_id: 1, orders_count: 70..100).perform
k: Where with not:
- Trong trường loại của query
_not, ta chỉ có thể truyền 1 attribute thay vì nhiều attribute - Ví dụ
_notvớistore_idProduct.search("your key word", where: { _not: {store_id: 1} }) Search::Where::NotStoreIdService.new("your key word", store_id: 1).perform - Ví dụ
_notvớiorders_countProduct.search("your key word", where: { _not: {orders_count: 70..100} }) Search::Where::NotOrdersCountService.new("your key word", orders_count: 70..100).perform - Trong trường hợp truyền nhiều attribute thì searchkick sẽ filter với attribute sau cùng
- Ví dụ:
Product.search("your key word", where: { _not: {store_id: 1}, _not: {orders_count: 70..100} }) - Sẽ tương đương với
Product.search("your key word", where: { _not: {orders_count: 70..100} })
3. Fields:
- Theo mặc định, elasticsearch sẽ search trên tất cả các attribute của product, ta có thể chỉ định elasticsearch chỉ search trên 1 vài attribute thông qua option
fields - Ví dụ:
Product.search("your key word", fields: ["*"]) Product.search("your key word", fields: [:name]) Product.search("your key word", fields: [:description]) - Trong source code tham khảo có implement service để bạn search với
fieldsoptionsSearch::FieldsService.new("your key word", fields: ["*"]).perform Search::FieldsService.new("your key word", fields: [:name]).perform Search::FieldsService.new("your key word", fields: [:description]).perform
4. Limit and offset:
- Searchkick cung cấp thêm 2 option là
offsetvàlimitcó tác dụng tương tựoffsetvàlimittrong SQL. - Ví dụ:
Product.search("your key word", limit: 10, offset: 20) - Searchlick sẽ bỏ qua 20 product đầu tiên trong các product tìm được và chỉ trả về 10 product.
- Trong source code tham khảo có implement service để bạn search với
limitvàoffset.Search::LimitOffsetService.new("your key word", limit: 10, offset: 20).perform
5. Results:
- Method
searchcủa searchkick trả vềSearchkick::Resultsobject. - Ta có thể coi object này như array và gọi các method của array như sau
results = Product.search("your key word") results.size results.length results.any? results.each {|result| ... } Searchkick::Resultcòn có thêm 2 method làtookvàresponse# thời gian thực hiện search (ms) results.took # response elasticsearch trả về (JSON) results.responseSearchkick::Resultcòn có thêm 1 method làtotal_counttrả về toàn bộ product search được khi chưa pagination, khác với methodcount,lengthhaysize- Ví dụ
Product.search("your key word", limit: 10).length # 10 Product.search("your key word", limit: 10).size # 10 Product.search("your key word", limit: 10).count # 10 Product.search("your key word", limit: 10).total_count # 100
6. Source code
All rights reserved
