こんにちは!SUZURI事業部デジタルコンテンツチームのyukunです。
最近、オリジナルグッズ作成販売サイトのSUZURIではグッズだけではなく、デジタルコンテンツの取り扱いを始めました。 このデジタルコンテンツの検索をElasticsearchとSearchkickというgemで実装しており、さらに類似商品の提案も手軽に実装できたので紹介します。
以下スクリーンショットのように商品ページで「こちらもおすすめ」セクションに類似アイテムを提案しています。 3Dアバターの類似としてアバターや3Dモデルのファッションアイテムなどが関連のものとして表示されているのがわかるかと思います。
商品ページ: https://suzuri.jp/moonshot-inc/digital_products/18284
Searchkick について
SearchkickとはRailsでElasticsearch(またはOpenSearch)を用いた検索を実装するためのgemです。 検索のためのインデックス作成から、検索クエリをElasticsearchにリクエストしてActiveRecordから情報を取得するという一連の実装をサポートしています。 特にElasticsearchのQuery DSLを直接書かずにSQLのような記述で、複数の情報やスコアの重みを付けるような複雑な検索クエリを実装できるのが魅力的です。
例えば、Product
モデルから "apples" という単語を含んでおり、さらに in_stock
fieldが true
のものを検索するには次のように書きます。さらに取得の範囲を指定する limit
や offset
も指定してあります。
Product.search("apples", where: {in_stock: true}, limit: 10, offset: 50)
引用: https://github.com/ankane/searchkick#querying
その他の使い方はGitHubリポジトリのReadmeを参照してください。
https://github.com/ankane/searchkick
similar method について
Searchkickを使うにあたり、一通りドキュメントを読んでいたところSimilar Itemsという項目がありました。 ただ、READMEやドキュメントには、似ていいるアイテムが見つかるとだけ書かれており詳細な情報が分かりません。
Find similar items.
product = Product.first product.similar(fields: [:name], where: {size: "12 oz"})
引用: https://github.com/ankane/searchkick#similar-items
similar methodの実態
そこで、searhkickのdebug機能を用いてsimilarの挙動を調べてみることにしました。
以下のコードのようにsearchkickのメソッド引数に debug: true
を指定すると、Elasticsearchで実行しているクエリを表示することができます。
なお、Elasticsearchには既に特定のフィールドがインデックスされていることを前提にしています。
Product.find(1).similar(fields: [:name], debug: true)
参考: https://github.com/ankane/searchkick#debugging-queries
rails consoleでDigitalProductモデルのtitleフィールドに対してsimilarを実行した結果が次のログです。
irb(main):001> DigitalProduct.find(1).similar(fields: [:title], debug: true)
DigitalProduct Load (3.4ms) SELECT "digital_products".* FROM "digital_products" WHERE "digital_products"."id" = 1 LIMIT 1
DigitalProduct Search (73.2ms) digital_products_development/_search {"query":{"more_like_this":{"like":[{"_index":"digital_products_development","_id":1}],"min_doc_freq":1,"min_term_freq":1,"analyzer":"searchkick_search2","fields":["title.analyzed"]}},"timeout":"11000ms","_source":false,"size":10}
~~~
"query"
という要素があるJSONがElasticsearchへ送信されているQuery DSLになります。
この中に "more_like_this"
という項目が見つかり、similar methodの実態はElasticsearchのMore like this Queryを使っていることが分かりました。
これはidで指定したドキュメントまたはテキストと、似ているドキュメントを探して返すクエリです。オプションでフィールドやパラメータを指定することができますが、searchkickでは全てのオプションが完全にサポートされていません。
More like this query | Elasticsearch Guide [8.11] | Elastic
また、類似度を算出しているアルゴリズムはtf-idfが使われています。簡単に説明すると、テキストに含まれる単語の出現回数からドキュメント全体でのレア度をを計算し、これを特徴ベクトルとしてコサイン類似度を計算しています。
以下の記事の解説がわかりやすく参考になるので、より詳しく知りたい方は参考にしてください。
tf-idf(term frequency - inverse document frequency)とは?:AI・機械学習の用語辞典 - @IT
実際に、DigitalProductのtitle fieldに対してsimilarを実行した結果が以下の通りです。
「猫の3Dモデル」というタイトルに対して、「猫」と「3Dモデル」の両方が含まれるものが一番類似しており、次に「3Dモデル」という単語を含むものがあり、最後に「モデル」という単語が含まれるものが出てきました。 この結果から直感的に同じ様な単語が含まれているものが類似ドキュメントとして扱われていることが分かると思います。
irb(main):006> digital_product = DigitalProduct.find(351)
DigitalProduct Load (3.9ms) SELECT "digital_products".* FROM "digital_products" WHERE "digital_products"."id" = 351 LIMIT 1
=>
...
irb(main):007> digital_product.title
=> "猫の3Dモデル"
irb(main):008> digital_product.similar(fields: [:title]).each { |p| puts p.title }
DigitalProduct Search (11.7ms) digital_products_development/_search {"query":{"more_like_this":{"like":[{"_index":"digital_products_development","_id":351}],"min_doc_freq":1,"min_term_freq":1,"analyzer":"searchkick_search2","fields":["title.analyzed"]}},"timeout":"11000ms","_source":false,"size":10}
DigitalProduct Load (1.4ms) SELECT "digital_products".* FROM "digital_products" WHERE "digital_products"."id" IN (339, 1, 33, 40, 56, 59, 77, 80, 84, 350)
みけ猫の3Dモデル
おもち 3Dモデル
おもち 3Dモデル
おもち 3Dモデル
おもち 3Dモデル
おもち 3Dモデル
おもち 3Dモデル
おもち 3Dモデル
おもち 3Dモデル
かっこいいモデル
=>
チューニング
この機能のリリース前に、内部テスターだけで商品ページに類似商品を出して定性的に期待した結果が得られるかを確かめました。
3Dモデルやスマホ壁紙はタイトルやテキストに特徴があり期待する類似商品が出てきましたが、テキストが短かったり特徴が出にくいカテゴリではあまりうまくいきませんでした。 そこで、similarのオプションでタイトルや商品説明に重みをつけたり、同じカテゴリのものを優先的に出すようにパラメータをつけて、これを変更しながら調整していきました。
More like this queryもその名の通りQueryの一種なので、Elasticsearchのクエリオプションを使うことができます。 実際に使っているコードとは一部違いますが、以下のようなパラメータを付けることで、多くの商品で期待したものが類似として出てくるようになりました。
digital_product.similar(
fields: ["title^100", "description^50"],
limit: limit,
boost_by: {"score.recent_sales_score": {factor: 20}},
boost_where: {"digital_category.name": {value: digital_category.name, factor: 100}}
)
終わりに
自分は初めて類似商品を提案する機能を実装しましたが、複雑なアルゴリズムやシステムを使わずにElasticsearchを用いて簡単に簡易的なものを作れることを知って驚きました。
そして今回の実装は、実際のデータで試したライブデータプロトタイプをディレクターに見せて提案して5日程度でリリースすることができました。
テキストが多く特徴が出やすいコンテンツであれば、そこそこ精度の良い類似検索ができたので、類似提案の始めの一手として良い選択肢になるかと思います。
類似したコンテンツ提案を手軽に実装したいときには、searchkickを使ってぜひお試しください!