2
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Elasticsearch Gem「Searchkick」

2
Posted at

Searchkickは、Elasticsearchを利用した検索機能をサポートするGemです。

Searchkickでは、以下のような検索機能を利用できます。

  • ステミング:例えば、tomatoestomatoにもマッチします。
  • 特殊文字:例えば、jalapenojalapeñoにもマッチします。
  • 余分な空白:例えば、dishwasherdish washerにもマッチします。
  • スペルミス:例えば、zuchinizucchiniにもマッチします。
  • カスタム同義語:例えば、popsodaにもマッチします。

さらに、以下の機能もサポートしています。

  • SQLのようなクエリ
  • ダウンタイムなしの再インデックス
  • ユーザーごとの検索結果のパーソナライズ
  • オートコンプリート
  • 「もしかして(Did you mean)」候補
  • 多言語対応
  • ActiveRecord、Mongoid、NoBrainerとの連携

1. はじめに

Searchkick Gemについて学習するため、最初にRailsプロジェクトを作成します。

rails new elastic_search
rails g model product name
rails db:migrate

必要なGemをインストールします。

# Gemfile
gem "ffaker"
gem "searchkick"

seedファイルを作成します。

# db/seeds.rb
100.times do
  Product.create name: FFaker::Lorem.words.join(" ")
end

ProductモデルにSearchkickを追加します。

# app/models/product.rb
class Product < ApplicationRecord
  searchkick
end

RailsコンソールでProductの再インデックスを実行し、データをElasticsearchサーバーへ送信します。

Product.reindex

再インデックスが完了したら、Searchkickを使って商品を検索できます。

Product.search("perferendis")

すべての商品を検索する場合は、キーワードに"*"を指定します。

Product.search("*")

コンソールログを見ると、SearchkickがElasticsearch DSLを使用したcurl形式のリクエストを生成していることを確認できます。Elasticsearch DSLについては、次の記事で詳しく説明します。

参考ソースコードには、上記2種類の基本的なクエリを試すためのサービスが実装されています。

Search::AllService.new.perform
Search::SimpleService.new("your key word").perform

2. クエリ

a. 特定の値を使用するwhere

Searchkickでは、SQLクエリのような形式で検索できます。

例:

Product.search "your key word", where: { in_stock: true }

このクエリでは、whereパラメータにキーと値から構成されるHashを渡します。

  • キー:属性名
  • 値:その属性に対する検索条件

Searchkickはフィルタリングを行い、whereパラメータの条件を満たす商品のみを検索します。

上記の例では、in_stock属性がtrueの商品だけが検索対象になります。

参考ソースコードには、whereクエリを試すためのサービスが実装されています。

Search::Where::SimpleService
  .new("your key word", in_stock: false)
  .perform

b. 範囲を使用するwhere

属性値がtruefalseのような特定の値ではなく、10から20までのような一定の範囲に含まれる場合は、gtelteを使用します。

例:

Product.search "your key word",
               where: { orders_count: { gte: 10, lte: 20 } }

Product.search "your key word",
               where: { orders_count: (10..20) }

参考ソースコードには、gtelteを使用したwhereクエリを試すためのサービスが実装されています。

Search::Where::RangeService
  .new("your key word", gte: "10", lte: "20")
  .perform

c. inを使用するwhere

属性値が[1, 2, 3, 4, 5]のような連続した範囲ではなく、[1, 3, 5]のような離散的な配列である場合は、inを使用します。

例:

Product.search "your key word",
               where: { store_id: { in: [1, 3, 5, 7, 9] } }

参考ソースコードには、inを使用したwhereクエリを試すためのサービスが実装されています。

Search::Where::InService
  .new("your key word", store_ids: [1, 3, 5, 7, 9])
  .perform

d. notを使用するwhere

notinとは反対の条件です。

Searchkickは、属性値が指定した配列に含まれていない商品を検索します。

例:

Product.search "your key word",
               where: { store_id: { not: [1, 3, 5, 7, 9] } }

参考ソースコードには、notを使用したwhereクエリを試すためのサービスが実装されています。

Search::Where::NotService
  .new("your key word", store_ids: [1, 3, 5, 7, 9])
  .perform

e. allを使用するwhere

inと同様に、Searchkickではallも使用できます。

allを指定すると、属性値が指定した配列と一致する商品を検索します。

例:

# product.store_idが1、3、5、7、9のいずれかである商品を返す
Product.search "your key word",
               where: { store_id: { in: [1, 3, 5, 7, 9] } }

# product.store_idが[1, 3, 5, 7, 9]である商品を返す
Product.search "your key word",
               where: { store_id: { all: [1, 3, 5, 7, 9] } }

参考ソースコードには、このクエリを試すためのサービスが実装されています。

Search::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

最初のクエリは常に空の配列を返します。これは、product.store_idが配列を返すことがないためです。

f. 配列属性に対するall

上記の理由から、allは単一の値を返す属性よりも、配列を返す属性に対して使用するのが一般的です。

以下のような関連を持つモデルを追加します。

# 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

これにより、product.order_idsを呼び出し、配列を取得できます。

Product.first.order_ids
# => [1, 2, 3]

次に、allを使用してorder_idsから商品を検索します。

例:

Product.search(
  "your key word",
  where: { order_ids: [1, 2, 3] }
)

しかし、この検索結果は常に空の配列になります。

g. allsearch_datasearch_import

検索結果が空になる理由は、再インデックス時にSearchkickがElasticsearchへ送信するデータが、デフォルトではproductsテーブルのカラムだけだからです。

つまり、SearchkickからElasticsearchへ送信されるのは、以下のようなデータです。

  • id
  • name
  • created_at
  • updated_at
  • in_stock
  • orders_count
  • store_id

そのため、商品のorder_idsを条件に検索しても、Elasticsearchは常に空の配列を返します。

Elasticsearchへ送信するデータは、search_dataメソッドで定義できます。また、以下のようにsearch_importスコープも追加します。

# 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

Productの再インデックスを実行します。

Product.reindex

再度allを使用したクエリを実行すると、検索結果を取得できます。

Product.search(
  "your key word",
  where: { order_ids: [1, 2, 3] }
)

search_datasearch_importについては、後のセクションで詳しく説明します。

なお、allクエリは、商品のorder_idsが指定した配列と同じ、またはその配列を含む場合にのみ、その商品を検索します。

参考ソースコードには、このクエリを試すためのサービスが実装されています。

Search::Where::AllWithArrayService
  .new("your key word", order_ids: [1, 2, 3])
  .perform

h. existsを使用するwhere

exists: trueを使用すると、store_idが存在する商品だけをフィルタリングできます。

Product.search(
  "your key word",
  where: { store_id: { exists: true } }
)

一方、store_idnilの商品をフィルタリングする場合は、exists: trueを使用するのではなく、store_id: nilを直接指定します。

Product.search(
  "your key word",
  where: { store_id: nil }
)

参考ソースコードには、上記2つのケースを試すためのサービスが実装されています。

Search::Where::ExistsService
  .new("your key word", store_exists: true)
  .perform

Search::Where::ExistsService
  .new("your key word", store_exists: false)
  .perform

i. andを使用するwhere

whereのデフォルト演算子はandです。

返されるレコードは、whereに指定したすべての条件を満たす必要があります。

例:

Product.search(
  "your key word",
  where: {
    store_id: 1,
    orders_count: { gte: 70, lte: 100 }
  }
)

このクエリは、_andを使用して次のように書き換えられます。

Product.search(
  "your key word",
  where: {
    _and: [
      { store_id: 1 },
      { orders_count: (70..100) }
    ]
  }
)

参考ソースコードには、_andクエリを試すためのサービスが実装されています。

Search::Where::AndService
  .new(
    "your key word",
    store_id: 1,
    orders_count: 70..100
  )
  .perform

j. orを使用するwhere

2つの条件のうち、いずれか一方を満たす商品をフィルタリングする場合は、_orクエリを使用します。

Product.search(
  "your key word",
  where: {
    _or: [
      { store_id: 1 },
      { orders_count: (70..100) }
    ]
  }
)

参考ソースコードには、_orクエリを試すためのサービスが実装されています。

Search::Where::OrService
  .new(
    "your key word",
    store_id: 1,
    orders_count: 70..100
  )
  .perform

k. _notを使用するwhere

_notクエリには、複数の属性ではなく、1つの属性のみを渡すことができます。

store_idに対して_notを使用する例:

Product.search(
  "your key word",
  where: {
    _not: { store_id: 1 }
  }
)
Search::Where::NotStoreIdService
  .new("your key word", store_id: 1)
  .perform

orders_countに対して_notを使用する例:

Product.search(
  "your key word",
  where: {
    _not: { orders_count: 70..100 }
  }
)
Search::Where::NotOrdersCountService
  .new("your key word", orders_count: 70..100)
  .perform

複数の属性を渡した場合、Searchkickは最後に指定された属性だけを使用してフィルタリングします。

例:

Product.search(
  "your key word",
  where: {
    _not: { store_id: 1 },
    _not: { orders_count: 70..100 }
  }
)

これは、以下のクエリと同じです。

Product.search(
  "your key word",
  where: {
    _not: { orders_count: 70..100 }
  }
)

3. 検索対象フィールド

デフォルトでは、Elasticsearchは商品のすべての属性を検索対象とします。

fieldsオプションを使用すると、Elasticsearchが検索する属性を限定できます。

例:

Product.search("your key word", fields: ["*"])
Product.search("your key word", fields: [:name])
Product.search("your key word", fields: [:description])

参考ソースコードには、fieldsオプションを使用した検索を試すためのサービスが実装されています。

Search::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. limitoffset

Searchkickには、SQLのLIMITOFFSETに相当するlimitoffsetオプションがあります。

例:

Product.search(
  "your key word",
  limit: 10,
  offset: 20
)

この場合、Searchkickは検索された商品の先頭20件をスキップし、その後の10件だけを返します。

参考ソースコードには、limitoffsetを使用した検索を試すためのサービスが実装されています。

Search::LimitOffsetService
  .new("your key word", limit: 10, offset: 20)
  .perform

5. 検索結果

Searchkickのsearchメソッドは、Searchkick::Resultsオブジェクトを返します。

このオブジェクトは配列のように扱うことができ、以下のような配列メソッドを呼び出せます。

results = Product.search("your key word")

results.size
results.length
results.any?
results.each { |result| ... }

Searchkick::Resultsには、tookresponseというメソッドもあります。

検索にかかった時間をミリ秒単位で取得します。

results.took

Elasticsearchから返されたレスポンスをJSON形式で取得します。

results.response

Searchkick::Resultsには、total_countメソッドもあります。

total_countは、ページネーションを適用する前に検索された商品の総件数を返します。これは、countlengthsizeとは異なります。

例:

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
2
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?