Facetとデータ: Dynamic Facetingを使ったファセット検索の実装

こちらはAlgoliaブログの Facets & data, part 3: implementing faceted search with dynamic faceting (code included) の翻訳です。


こちらの記事はファセットとファセット検索のテクニカルなデータの側面を解説する3部構成のブログ記事の最終回です。こちらのパート3では、クエリの実行からレスポンスまでの検索の一連のプロセスに着目し、ファセットを動的に生成していく方法をご紹介します。

こちらは、Facets & Dataシリーズの3つ目の記事です。今回はファセット検索のロジックとファセットデータモデルのアウトラインをご説明します。最初の記事である – FacetとFacet化された検索 – JSONの属性それぞれに価値をもたらす – では、ファセットとは何か、そして構造化されたデータにおいてファセットがどのように重要な役割を果たすのかといったところをご紹介しました。そして、2つ目の記事 – JSONを使ったFacetのデータモデル – では、ファセットにおける最も一般的なデータ構造をご紹介しました: シンプルファセットバリュー、ネステッドファセット、階層型カテゴリ、そしてユーザーおよびAIによるタギング、これらは異なるファセット検索の観点で使われます。インデクシングやファセッティングに関する情報はAlgoliaのドキュメントに詳細が書かれておりますので、よろしければ是非ご覧ください。

さて、こちらの記事 – シリーズ3本目の最終回 – では、引き続きデータに焦点を当てながらプロセスについても触れていきます。今回はクエリのサイクルの中でのデータとプロセスを深堀りしていきます: request, execution, response, そしてこれら全てをdynamic facetingというファセットのadvancedな使い方における文脈の中でご説明していきます。

では、技術的な詳細に入っていく前に、まずは動的ファセッティングとは何かについてみていきましょう。

動的ファセットの概要

Dynamic faceting(動的ファセット)は、ユーザーのインテント(意図)に応じて異なるファセットを表示します。これを理解していくために、2つのカテゴリーの商品を販売するような音楽ストアのeコマースサイトを考えてみましょう。ユーザーの目的が好きな音楽を探すことであるにも関わらず、”brand”を提案しても、、ですし、ユーザーの目的がオーディオ機器である場合に”musical genre”を提案することに意味はあるかというと、、ということで、今回取り上げるDynamic Facetingでは最も適切なファセットだけを表示するようにします。

動的ファセット(Dynamic Faceting)のデモに関しましては – コードベースも揃っている – 動的ファセットのサンドボックスをご覧ください。

様々な商品を取り扱うeコマースサイトでは、ユーザーが検索する商品に応じて異なるファセットを表示することが効果的です。

  • 製薬会社での医療品と化粧品で異なるファセット
  • 新聞でのエンターテインメントと政治で異なるファセット
  • Amazonのようなオンラインマーケットでの様々な商品のナビゲートの際に変化するファセットのリスト

eコマースマーケットプレイスでのDynamic Facetingの事例

Amazonでは、多くのカテゴリーにおいて動的ファセットが使われています。例えば、下の画像では2つのクエリが表示されていますが、左が”music”で右が”movies”です。見て分かるようにどちらも”price”ファセットがありますが、音楽のクエリには”customer reviews”, “artics”, そして, “musical format”が含まれていて、映画のクエリには”director”, “video format”, そして”movie genre”が含まれています。

Amazonでは動的ファセットを使って、ユーザーが検索している商品に応じて、スマートで且つキュレートされた方法でユーザーをガイドしていくことによって、より良い検索体験を実現していると言えるでしょう。

では、これがどのように実現されているか見ていきましょう。

クエリのサイクルとファセット検索のロジック

まず、いくつか用語があります

  • Facet keys: “color”, “price”, “shoe_category”, “sleeves”といったもの
  • Facet values: keyに対するvalue。例えば、”color”には”red”や”green”が含まれ、”sleeves”には”short”や”long”が含まれます

データセット

今回は2つの種類(シャツと靴)の商品のデータセットを使います。以下はそのサンプルです。両方の商品には”price”, “color”, “clothing_type”というファセットが含まれていますが、シャツには”sleeves”ファセットがあるものの、靴には”shoe_category”ファセットがあります。

{
    "name": "Bold Shirt",
    "desc": “Be bold, wear a t-shirt with only one color”,
    “Image_url”: “images/shirt-123.jpg”
   “Price”: 49.99,
    "color": “white”,
    "gender": “male”,
    “clothing_type”: “shirt”,
    "sleeves": "short"
},
{
    "name": "Blazing Speed Sneakers",
    "desc": “Sneakers to win races!”,
    “Image_url”: “images/sneakers-789.jpg”
    "brand": “nike”,
    “Price”: 189.99,
    "color": "red",
    "clothing_type": "shoe",
    "shoe_category": "sneaker"
}

クエリのサイクル

検索クエリは4つのパートで構成されます。まずは概要を説明し、その後のセクションでより詳細な説明とコードのサンプルをご紹介します。

  1. ユーザーのクエリを検索エンジンに送る
  2. 検索を実行しクエリにマッチしたレコードを取得する。このステップでレコードからファセットの情報も併せて取得。
  3. 結果とファセットを送り返す
  4. 結果とファセットを表示する

このように、ファセットはpre-defined(事前に定義済み)なリストを使うのではなく、クエリごとに新しいリストを動的に生成するようなロジックとなります。以下のようにこれを実現します:

  • バックエンドではクエリの結果ごとにファセットの情報を取得する
  • フロントエンドではpre-definedなコンテナを使うのではなく、undefinedなコンテナのプレースホルダを使う

クエリのリクエスト: フィルター有り、もしくは、無しでクエリを送信

このサイクルの出発点は、クエリおよびユーザーが検索結果をフィルタリングするために選択したファセットのvalueを送信することです。結果をフィルタリングすると、まとまった結果セットが作られ、その結果として、表示される全てのアイテムに関連するファセットのリストが生成されます。一方で、ユーザーがファセットを選択しなかった場合、アイテムはより多様になります – そのため、ファセットが全ての商品に適用されるとは限りません。

しかし、これは全く問題ありません。次のステップで述べますが、トップ5の共通のファセットを表示することで、ほとんどのアイテムにこれらのファセットが含まれるようになるためです。

では、コードを見てみましょう。こちらはAlgoliaのAPIで”Get all short-sleeved summer t-shirts”というクエリを実装する方法です。(どの検索ツールを使ってもフィルタリングできると思いますのでこちらは数ある方法の1つということになります)

results = index.search('summer t-shirt', {
  filters: 'clothing_type:shirt AND sleeve:short'
});

クエリの実行: トップ5ファセットのリストの作成

今回こちらの記事で使うデータセットには2つの商品があり、それぞれがファセット属性を持っています。クエリ実行後に、検索エンジンは全てのレコードにある全てのファセットのKeyを抽出し、最も頻繁に現れる5つのファセットを選択します。

なぜトップ5なのか?ということですが、通常は5つのファセットを保持するのが画面表示として十分だから、ということになります。10以上になるとやり過ぎで、使い勝手が悪くなってしまうことが多いです。

ファセットのリストを作るのには2つの方法があります。

  1. pre-definedリスト: ファセットkeyのリストをコード内に直接、もしくは別のデータセットやローカルストレージに保存
  2. dynamically generated(動的に生成される)リスト: クエリの結果からファセットkeyのリストを抽出して、それを別のレコードとしてクエリのレスポンスに含める

今回は2.を採用しますが、実装によっては1.を用いるものもあるかと思います。

方法1 – ファセットKeyリストをハードコーディングする

こちらの方法では、商品について既に分かっていることに基づいて固定のファセットのセットを作ります。このリストはハードコードされたものでも、ファイルやデータベースのテーブルにあるものでも、新しい商品が追加されたり変更がされるたびにマニュアルに更新を行うことができます。

しかし、どのような方法で固定リストを保存するのせよ、リストには次のような情報が含まれている必要があります。

  • すべての”clothing_type=shirt”に、”color, price, clothing_type, sleeves, gender”といったファセット値を送り返す
  • すべての“clothing_type=shoe”に、“color, price, clothing_type, shoe_category, brand”といったファセット値を送り返す

こちらはハードコーディングや、それに準ずる方法と同様に、スケーラビリティという点で限界があると言えるでしょう。そのためあまり好ましい方法ではありません。例えば、”shoe_styles” = “high-top, leisure, and cross-fit”のようにより多くの関連性を追加したい場合は、手動でリストに新しい行を追加しなければなりません。手動作業は仕事を増やしますし、エラーや作業遅延が発生しやすいものです。

こちらの記事でこれから紹介していくアプローチ(方法2)では、手動でのメンテナンスをプロセスから取り除き、プロセスをdeductive(推論)にして完全に動的にしていきます。

方法2 – 動的に生成されるファセットのリスト

こちらの方法では、商品情報そのものからファセットのリストを抽出します。マニュアル(手動)とDynamic(動的)のアプローチの唯一の違いはと言えば、生成されるトップ5のファセットリストが動的であるか否かということだけです。検索結果のリスト自体は同様にフォーマットされます。

こちらがステップです:

  1. クエリの結果を取得:
    1. クエリを実行し商品情報を取得する
    2. それらのレコードを検索結果として送り返す
  2. トップ5のファセットkeyとそのvalueを全て取得:
    1. 検索結果から、全ての属性を取得する(ファセットの属性の特定方法は後ほどご紹介します)
    2. 抽出されたファセットkeyが全て含まれるレコードを生成する
    3. どのファセットが最も多く出現するか計算し、より多くのレコードを保持するものを上にしてソートする
    4. ソートされたリストからトップ5を取得する。これが共通ファセット
    5. プロダクトのvalueをそれぞれのkeyに追加する

以上です。

生成されたリストは方法1と同様に以下のようになります。

  • すべての”clothing_type=shirt”に、”color, price, clothing_type, sleeves, gender”といったファセット値を送り返す
  • すべての“clothing_type=shoe”に、“color, price, clothing_type, shoe_category, brand”といったファセット値を送り返す

この場合、検索結果のほとんどの商品がシャツであれば、4番目と5番目のファセットは”sleeves”と”gender”を表示することになり、逆に、靴の場合は4番目と5番目のファセットは”shoe_category”と”brand”が表示されることになります。

クエリのレスポンス: レスポンスを送り返す

ここでは、検索結果と生成された5つのファセットkeyのリストとそれぞれの値をシンプルに送り返します。

レスポンスには、フロントエンドが検索結果ページを構築するために必要なものが全て含まれている必要があります。全てのファセットクエリサイクルにおいてフロントエンドが必要とするものは

  • “name”, “description”, “price”, “image_url”(画像ファイルそのものは送らないでください。検索性能そのものに支障をきたします)を含む商品のリスト
  • ファセットkeyとvalueのリスト

また、レスポンスには、表示の制御やビジネスロジックに必要な追加情報が含まれています。ここではそれらを表示しませんが、詳細なクエリのレスポンスをご覧になりたい方はコチラからどうぞ。

Results:

ここでは、シャツのセットを返す方法をご説明します。全ての属性は検索結果の情報として使われます。”objectID”については技術的にレコードを一意にするため(クリック分析, ページビューの詳細 等)のものなのでここでは扱いません。

"results": [
{
    "objectID": "123",
    "name": "Bold Shirt",
    "desc": “Be bold, wear a t-shirt with only one color”,
    “Image_url”: “images/shirt-123.jpg”
   “Price”: 49.99,
    "color": “white”,
    "sleeves": "short"
}
]

Facets:

ファセットのレスポンスはファセットkeyとvalueの組み合わせです。ここでは一部だけ抽出しており、全てのファセットを含んでいるわけではありません。

"facets": {
    "Clothing Type": {
      "Shirts": 100,
      "Sneakers": 50
    },
    "Sleeves": {
      "Short": 30,
      "Long": 10
    },
}

2つの注意点

  • “sleeves”と”gender”だけがあり、”shoe_category”や”brand”はありません。なぜなら、靴よりもシャツが検索結果に多く含まれるためです
  • ファセットのとなりにある数字の値はレコードが保持するファセットの値です。こちらについては ファセットに数字を追加 という章で説明していきます

フロントエンドの表示: 動的にファセットのリストを表示する

ここでやることは、上記のresultsとfacetsを画面に表示することです。UXデザインにおいては、中央に結果、ファセットを左に配置するというのが業界標準です。

まず、HTMLを作成していきます。resultとfacetのためのプレースホルダーコンテナを追加します。

<div id="wrapper">
    <div id="app-container">
      <div id="left-side">
        <div class="sidebar">
          <div id="facet-lists"></div>
        </div>
      </div>
      <div id="center-side">
        <div id="results-container"></div>
      </div>
    </div>
</div>

ここには1つだけ”facet-lists“というコンテナがトップ5のファセットを表示するために配置されています。レンダリングをするコードはunorderedなリストを生成してこのコンテナの中に表示します。

resultsに関しては”results-container”で。

次にデータをレンダリングします。これには様々ななやり方があり、こちらの記事は厳密なチュートリアルではありませんので、フロントエンドの詳細な実装に関してはDynamic Faceting Using Instant SearchのGitHubリポジトリをご覧ください。

ソリューションをより強力に

ファセットに数字を追加

取得したファセットvalueを持つレコード数をユーザーに伝えたいですよね。ファセットの数を得ることは、検索結果としてそれをユーザーに知らせることができて便利です。例えば、長袖シャツよりも半袖シャツの方が多いことを知ることができるのは便利かと思います。こういったファセットの数字は通常ではバックエンドでクエリ実行時に計算が行われます。

ファセットにメタデータを追加

全てのレコードには、どの属性がファセットであるかをプロセスに伝えるための情報が含まれている必要があります。そのために、レコードのファセットを定義する追加の属性を使って、書くレコードにファセットメタデータを追加する必要があります:

“facets”: [“sleeves”, “price”, ..]

もう一歩進めて、属性のtypeも含めるようにするとより便利です

“facets”: [ 
  [“sleeves”, “string”], 
  [“price”, “numeric”] 
]

この情報に元にして、フロントエンドのコードは、例えばpriceであればrange bar、sleevesであればdropdownといった作り込みが可能になります。

ファセットを使ったグルーピング

例えば、色については他のファセットとは違う扱いをしたいことがあるかと思います。これは同じシャツで異なる色のものがあるからです。この場合にロジックとしては、全ての在庫がある色の属性を含むレコードが、シャツ1枚につき1つ必要なのか、それとも、色ごとに1つのレコードが必要で、シャツごとに複数のレコードが必要になってしまうのか?一般的なデータベースの考え方ではもちろん前者の1レコードのみということになりますが、ファセット検索に置いてはことなります。これは最初の記事で議論をしてきたことです。

検索可能なアイテムを全て別のレコードとすれば、色のファセットをクリックしなくとも、検索バーを使って”red shirts”を探すことができます。これを実現するために、全てのアイテムを1つの色として設定しました。しかし、レスポンスの中おいては3つのシャツのレコードを返す必要はありません。ここでは新しいメタ属性を作成して、3つのレコードを1つのレコードにまとめることができます。それは”available_colors”です。

“available_colors”: “red, green, blue”

まとめ

今回の私たちのゴールは、ファセット&データのシリーズでデータにプロセスを行うということでした。リクエスト、実行、レスポンス、表示といったクエリのサイクルに沿って、検索を実行して、ファセットを表示する方法をご紹介しました。

ファセットについての理解をストレッチさせるために、私たちはファセット検索を動的にするという高度なユースケースを扱いました。動的ファセットは特に多様な商品やサービスを提供している企業に、より直感的で便利なファセット検索体験を実現させます。

動的ファセットのサンドボックスや、GitHub上にある動的ファセットの実装例であるdynamic faceting on the the front end and dynamic faceting using Query Rulesを是非ご覧ください。もちろんインデクシングやファセッティングに関してはAlgoliaのファセットに関するドキュメントもどうぞ。

コメント

タイトルとURLをコピーしました