はじめに
株式会社スペースリーのWebエンジニアの小澤です。弊社のプロダクトの中で、私は主に物件管理サービスを開発しており、物件一覧の検索基盤をAlgoliaからElasticsearchにリプレイスしました。このサービスはRuby on Railsで開発しており、本稿ではElasticsearchに関わるGemとしてSearchkickを選択した理由を説明します。
Searchkickとは
Searchkickは、RailsとElasticsearchまたはOpensearchを用いた検索機能の実装をサポートするGemです。Searchkickを使うと、複雑な検索クエリの組み立てを簡易化し、可読性の向上や実装コストの削減に役立ちます。また、ElasticsearchおよびOpensearchの最新バージョンにも対応しています。
ElasticsearchのGemを調査
はじめはElastic社が提供するElasticsearch Railsを第一候補として考えていました。しかし、導入を検討していた昨年末の時点で更新が2年近く止まっており、Elasticsearch 8に対応していない状態でした(現在は8に対応済みです)。
さらに、Elasticsearch Railsで使用されるElasticsearch Transportのバージョンが Faraday1 に依存していたため、Faraday2 にアップグレードできませんでした(現在はElasticsearch Transport 7.17.10以上であれば Faraday2 にアップグレード可能です)。
過去にElasticsearch Railsを使っていましたが、Gemの導入で追加されるメソッド、ドキュメントが少ないという問題点がありました。これは実装コストの削減が困難であり、学習コストの高さから属人性を高めてしまう可能性があります。このような経緯で代替のGemを調査することにしました。
各Gemの特徴の簡潔な比較(2024年7月22日時点の情報で更新)
初期の段階でSearchkickとChewyに候補を絞りました。理由は、他社の導入事例、提供される機能、そして更新頻度を基にして、弊社のサービスに導入できる水準にあると判断したためです。
Searchkick
- 最新リリース: 2023/11
- Elasticsearch8: 対応
- メリット
- Elasticsearch、Opensearchの検索エンジンに対応
- 柔軟性が高い検索オプションを提供し、直感的に検索クエリを作成できる。SQLライクな記述も可能で、学習コストが比較的低い
- 機械学習による検索結果の並び替え、オートコンプリート、スペルチェックなどの高度な機能を提供
- デメリット
- インデックスの構造や再作成の細かい制御のサポートが薄く、特定のカスタマイズが必要な場合がある
Chewy
- 最新リリース: 2024/5
- Elasticsearch8: 未対応
- メリット
- ActiveRecordライクなクエリDSLを提供し、簡潔で分かりやすいクエリを記述できる
- Named Scopeのチェーンが使用できるため、Railsユーザーに馴染みやすい
- モデルとインデックスを分離する柔軟性があり、複雑な検索インデックスの作成が容易
- 大量のデータを一括インポートできる機能や、データの変更履歴を管理するジャーナリング機能を提供
- デメリット
- Elasticsearch 8に未対応のため、最新のElasticsearchの機能を利用できない
- Elasticsearchの内部構造の知識が必要で、学習コストが高くなる可能性がある
Elasticsearch Rails
- 最新リリース: 2024/5 (2年以上経過してからのリリース)
- Elasticsearch8: 対応
- メリット
- 初期設定がシンプルで、直感的に使いやすいAPIを提供しており、開発者がすぐに使い始めることができる
- Elasticsearchの公式ライブラリであり、コミュニティや公式サポートの支援を受けられる
- デメリット
- 2年以上経過してからリリースがあったものの、更新頻度が少ないため、メンテナンス性に懸念がある
- 機能が充実しておらず、複雑な検索機能を実装するための工数が多くかかる場合がある
- ドキュメント量が少なく、Elasticsearchの公式ドキュメントを頻繁に参照する必要がある
Searchkick と Chewyの詳細な比較
Searchkick
機能
- Elasticsearch、Opensearchの検索エンジンに対応
- SQLライクな記述で直感的に検索クエリを作成でき、比較的低い学習コストで実装できる
- 基本的な検索機能に加え、オートコンプリートやスペルチェック等の機能を簡単に組み込める
- 分析機能を持つSearchjoy、サジェスト機能を持つAutosuggestと連携可能
- インデックスを更新せずにシノニムを追加可能(Dynamic Synonyms)
- クエリのデバッグがしやすい仕組みが用意されている(Debugging Queries)
- インデックスとDBの同期方法が豊富で、更新間隔の調整も可能
使用例
検索データの作成
検索機能を追加したいモデルにsearchkickを追加します。デフォルトではデータベースのレコードが更新されると、それに応じて検索データも自動的に更新されます。
class Product < ApplicationRecord searchkick end
また、reindexを実行すると全ての検索データが作成・再作成されます。
Product.reindex
基本的な検索
SQLライクな記述でクエリを作成できます。
Product.search("apples", where: {in_stock: true}, limit: 10, offset: 50, order: [{publish_at: :desc}])
ネストされたデータの検索
ネストされたデータ構造を持つドキュメントの検索も簡単です。
Product.search("san", fields: ["store.city"], where: {"store.zip_code" => 12345})
複雑な検索
Advanced Searchで複雑な検索を実行できます。
Product.search(body: {query: {match: {name: "milk"}}})
検索結果の選択
検索結果をDBから返すか、Elasticsearchから直接返すか選ぶこともできます。
Product.search("apples") => #<Searchkick::Relation [#<Product id: 1, available: 1, …>]>
引数にload: false
を追加すると、Elasticsearchの検索結果が直接返ってきます。
Product.search("apples", load: false) => #<Searchkick::Relation [#<Searchkick::HashWrapper _id="1" _index=“product_development_xxx” _score=1.0 available=true … >]>
Chewy
機能
- ActiveRecordのようなチェーンを用いて検索クエリの作成が可能で、Named Scopeを組み合わせることもできる
- 1つのモデルに対して複数のインデックスを定義でき、目的に応じたインデックスを柔軟に作成できる
- 一括更新、SidekiqやActiveJobでの更新、遅延更新に対応しており、パフォーマンスを損なわずにインデックスを更新する方法が用意されている
- 全てのactionを記録し、ダウンタイムなしでインデックスを切り替えるのに役立つジャーナリング機能
- インデックスの作成、削除、更新等のRakeタスクが充実している
使用例
検索データの作成
検索機能を追加したいモデルに対してインデックス用のクラスを追加し、
class ProductsIndex < Chewy::Index settings analysis: { analyzer: { name: { tokenizer: 'keyword', filter: ['lowercase'] } } } index_scope Product field :name end
モデル内にupdate_indexを記述して、データが更新されるようにします。
class Product < ApplicationRecord update_index('products') { self } end
また、importを実行すると全ての検索データが作成・再作成されます。
Product.import
importには豊富なオプションが用意されており、インデックスクラスに定義できる他、
class ProductsIndex < Chewy::Index index_scope Post.includes(:tags) default_import_options batch_size: 100, bulk_size: 10.megabytes, refresh: false field :name field :tags, value: -> { tags.map(&:name) } end
直接コマンドを実行する時も使用できます。
ProductsIndex.import(batch_size: 100, bulk_size: 10.megabytes, refresh: false)
基本的な検索
Active RecordライクなクエリDSLを用いて検索します。
ProductsIndex.query(match: {name: 'apple'})
複雑な検索
filter等をチェーンさせて検索クエリを組み立てることができます。
CitiesIndex.filter(term: {name: 'Bangkok'}) .query(match: {name: 'London'}) .query.not(range: {population: {gt: 1_000_000}})
結論
SearchkickとChewyを比較した結果、最終的にSearchkickを選択しました。Chewyも多くの機能を持ち魅力的でしたが、Elasticsearch 8への未対応が決定的な課題でした。今回はElasticsearchの最新バージョンに対応することを最優先事項としていたため、Searchkickの方が適していると判断しました。
Searchkickは、Elasticsearchの複雑な部分を上手く抽象化して学習コストを下げてくれる一方で、意図しないクエリが生成されることがあり、一部の実装が難しくなることもあります。しかし、総合的には拡張性の高さと便利さに非常に満足できました。
最後に
スペースリーでは一緒に働いてくださる方を募集中です!