はじめに
こんにちは!
冬に美味しい鰆が準絶滅危惧種に指定されたということに驚きを隠せない t3qyo です。
特選京サワラはUUUOでも購入できます。飲食店様、仲卸様、スーパー発注担当者様、UUUOをどうぞ鮮魚仕入れにご活用ください!
前置きはさておき、 最近、UUUOアプリで検索の改善を行ったので共有します。
- はじめに
- なぜ改善が必要だったのか
- なぜAlgoliaを使うことにしたのか
- 構成
- Ruby on RailsでのAlgoliaとの連携
- 実装して気になったところ/つまづいた点
- 実装時に助かったところ、良かった点
- まとめ
なぜ改善が必要だったのか
もともとの検索では以下のようにカテゴリ、魚種名での検索しかできませんでした。
ただ、「産地や漁法、食べ方などで検索し、意図した商品を見つけたい」という声や
「鯛で検索しても マダイ
が引っかからないので使いづらい」 という声をいただいており、
それには現状の検索の設計を見直す必要がありました。
それとは別に現在サポートしている、地方名(例えば「イボダイ」は地方によっては「シズ」と呼ばれる)での検索にも対応する必要があり、柔軟な対応ができる検索ツールが求められました。
なぜAlgoliaを使うことにしたのか
既存のPostgresとAlgoliaとElasticSearchで比較しました。
こちらの記事が参考になります
medium.com
Postgres(既存) | Algolia | Elastic Search (自前デプロイ) |
Elastic Search on Cloud | |
---|---|---|---|---|
費用 | 既存なので0 | データ数/リクエスト数に応じる | インフラ費用のみ | 初期コスト高い |
実装コスト | 低い | 低い | 高い(インフラ含む) | やや高い(と感じた) |
日本語対応*1 | 未対応 | 対応 | 対応 | 対応 |
検索の分析 | 可能だが実装は必要 | ダッシュボード あり |
ダッシュボード あり |
ダッシュボード あり |
今回は、日本語対応と、実装コスト(素早くリリースできるか)を重視して検討し、 結果、今回の検索対象データ数と、リクエスト数を考慮するとAlgoliaが最適という検討結果になり、 Algoliaを選定しました。
構成
以下のような構成で実装しました。
Algoliaの検索のパフォーマンスはとても良く、できれば直接検索対象Indexを呼んで返したかったのですが、
我々のサービスの場合はユーザーごとに返す検索結果が異なるという特性があり、その部分のロジックはRails側を通さざるを得ないため、
上記のような構成で実装することになりました。
要件によるとは思いますが、本来はできる限りRails側を通さず直でAlgoliaにリクエストするのがベストプラクティスと思います。
また、Index(検索対象となるデータセット)は、以下の3つを用意しました。
- Query Suggestions Index (おすすめキーワード表示用)
- 検索候補表示Index (Auto Complete用)
- 検索対象の商品Index (検索結果表示用)
ユーザーが検索をするまでの流れは以下の図のイメージとなります。
検索候補表示IndexやQuery Suggestionsはユーザーごとの考慮は不要なため、Algoliaを直接呼んでいます。
余談ですが、検索候補表示の部分を、「検索対象の商品Index」を直接呼んでそこから返ってくるものを候補表示することも考えたのですが、
多くのattributeを検索対象(Searchable Attributes)とした結果、
「マグロ」と入力したときに、本来なら「本マグロ、ヨコワマグロ、クロマグロ...」などの検索候補表示が欲しいのですが、
「マグロ、延縄、青森...」など、あまり入力とは関係ないものが出てきてしまうので、
いろいろとやってみた結果、AutoComplete用のIndexとして独立させるのが最適だろうと考え、独立させました。
ただ、Record数も増えてしまうので、費用的にはあまり良いやり方ではないかもしれません。他に良いやり方があれば教えてください🙇
Ruby on RailsでのAlgoliaとの連携
algoliasearch-railsというgem使って実装しました。
Algoliaに対してデータを登録、更新する際に algolia_reindex!
という関数があり、ActiveModelに対してこれを呼ぶだけなので、とても簡単に実装できました。
class FishType < ActiveRecord::Base include AlgoliaSearch algoliasearch do attribute :name, :comment, ... indexLanguages ['ja'] queryLanguages ['ja'] # デフォルトではAlgoliaが定義した順になるが、それより優先したいものがある場合に定義 ranking %w[ desc(created_at) typo geo words filters proximity attribute exact ] # ファセット定義 attributesForFaceting %w[ name prefecture_name ... ] # 検索対象attribute定義 searchableAttributes %w[ name fish_type_name .... ] end end
上記をモデルに対して定義し、 reindexしたいタイミングで、以下を呼び出すだけです。
FishType.algolia_reindex!
そして検索側では、以下のように検索します。
FishType.raw_search('マグロ', filters: 'fish_type:本マグロ AND fishing_method:釣り')
キーワード内に(
などの記号などが含まれているとfilterがうまく作れずエラーとなるので、
filter作成時は以下のようにエスケープしておくことが必要でした。
"#{params[:facet_name]}:\"#{params[:keyword]}\"
実装して気になったところ/つまづいた点
Algolia側で検索履歴を取得することはできない
Personalize機能があるので、検索履歴を取得できるのでは?と思いましたが、
現在は検索履歴を取得できないようです。
Query Suggestionsは全ての検索対象の商品がIndexされるわけではなく、よく検索されている一部のみIndexされる
こちらの例 にもあるように、Query Suggestionsを使って検索候補表示(Auto Complete) を実装することが一般的なようですが、
Query SuggestionsにIndexされるのは、Algolia側のアルゴリズムで選定された検索対象の商品の一部のみであり、
対象となる1万件の商品のうち、よく検索される500件ほどしかIndexされないため、
弊社のように地方名から正式名称を引きたいといった、AutoCompleteの要件では利用できませんでした。
結果的にQuery Suggestionsは、単純に「おすすめキーワード」として表示する使い方となりました。
その中で「おすすめキーワード」をコントロールしたいという要件も生まれてくるのですが、
それについては次に書きます。
Query Suggestionsのコントロールについて
「ユーザーにもっとこのワードに注目して欲しい」という、管理者側で意図的に順位を上げるのは推奨されないようです。
コンソールからQuery Suggestionsの内部の priority
を編集することで一時的に上にしたいキーワードをあげることもできたのですが、
1日1回、Query SuggestionsがAlgolia側で検索結果を元に更新されており、リセットされてしまいました。*3
意図的におすすめキーワードを操作したい要件にはあまり向かないかもしれません。
Personalizationを使えば、個々人にあったQuery Suggestionsの表示も可能となるらしく、そちらを試してみる価値があるかもしれません。
検索して欲しいワードをあげるのでなく、あまり意図しない結果をコントロールすることはでき、
Query Suggestionsで検索してほしくないキーワードを設定することによりコントロールしています。
部分一致、前方一致、正規表現などでの指定もできるので割と便利です。
例えば「〜です」などのSuggestionsがあがってしまうことがあったため、「です」を後方一致で検索不可に指定しています。
APIからは exclude
のプロパティをセットすることで設定できます。
https://www.algolia.com/doc/rest-api/query-suggestions/#method-param-exclude:title:w200
Query Suggestionsからの検索でもfacetをつけたくなる
前述した通り、searchableAttributesを多く設定しているため、
「カサゴ」と検索したときに、facetに「魚種」を設定しなければ、
本来検索したかった「カサゴ」以外に、「ユメカサゴ」「オニカサゴ」も取得されてしまいます。
後述の方法でそのQuery Suggestionsが何のfacetに紐づいているかを取得し、設定することはできるのですが、
何か一つのfacetに絞り込むことは難しく、
Query Suggestionsにfacetを紐づけたいのであれば、RailsからAlgoliaに問い合わせる際に、一度検索候補用のIndexで完全一致するものはないかを検索し、
存在した場合は、そのfacetをfilterに加えて、Algoliaを検索するような動きが必要になると思われます。
Synonymは十分に精査してから取り入れる必要がある。
先に述べた、地方名での魚種の検索を実現するため、Algoliaで提供されている Synonym を使って実装しました。
元々設定されていた地方名用のデータベースから取り込みましたが、
当たり前ですが、あまりに1魚種あたりのSynonymが多いと意図しない検索結果になってしまうことが多く、
地方名用のデータベースに持っている単語それぞれの精査が必要でした。
FlutterのAlgoliaのライブラリはまだあまり使えなかった
QuerySuggestionsにfilterをかける。など弊社のように特殊なことをするには、結局は現状直接APIを呼ぶしかなかったので、Flutterのライブラリは使わずに実装しました。
実装時に助かったところ、良かった点
Algoliaは参照ドキュメントが豊富、お試しも簡単
こちらのUXについての記事も検索体験を考える上で参考になりました。
https://www.algolia.com/doc/guides/building-search-ui/ecommerce-ui-template/overview/flutter/
Algoliaの日本人エンジニアのshindoggさんのブログ、Podcastが参考になりました。
また、基本無料でお試しでき、sandboxで簡単に検索UIを絡めたpreviewを試すことができました。
これらのわかりやすさも導入の理由の一つとなりました。
https://www.algolia.com/doc/ui-libraries/autocomplete/introduction/sandboxes/
Query Suggestionsのマルチテナント、フィルタ対応
Query Suggestionsに、そのテナントで見ることができるかどうかというfacetを意図的に持たせ、
Query Suggestionsを取得する際に、そのfacetを指定して検索することで、
自テナントのQuery Suggestionsのみ取得するように対応しました。
以下を設定することで、(APIでも設定可能)
Query Suggestions側に、設定されたfacetでどれぐらいの結果があるかをもたせることができます。
"index": {"exact_nb_hits": 923,"facets": {"exact_matches": {"name": [{"value": "マグロ","count": 901}]},...}}
ダッシュボードが見やすい
現在何が検索されているか、どのキーワードの検索結果が表示されなかったか、
どの検索のCTR(Click Through Rate)が高いかをAnalyticsのタブから確認でき、とても便利です。
Staging環境などの考慮
今回は、development, staging, productionの3環境を用意しました。 RailsのENVの環境変数で簡単に切り替えができるのでわかりやすく、おすすめです。
まとめ
いろいろと書きましたが、Algoliaを使ってみて、求めていたものがわかりやすく&素早くできたなという印象です。
これから検索データが増えていくにつれ、費用感がどうなるか。気になるところですが、
とりあえず使ってみるというのが素早くできるのが素晴らしいです!
また進展がありましたら共有します。
このように、Rubyを使って試行錯誤しながら素早くUXを作っていくことに興味のあるバックエンドエンジニア、 フロントエンドエンジニアの方、少しでも興味があれば、
Twitter でも以下リンクでもぜひお声がけください。
- <業務委託スタート可>ソフトウェアエンジニア(Backend/Ruby on Rails) / 株式会社ウーオ
- <業務委託スタート可>ソフトウェアエンジニア(Frontend/Flutter on the Web) / 株式会社ウーオ
- エンジニアインターン(広島) / 株式会社ウーオ
では。