検索アプリにファセット ナビゲーションを追加する

ファセット ナビゲーションは、検索アプリでのクエリの結果に対する自律型のドリルダウン フィルター処理に使用されます。そこでは、検索範囲をドキュメントのグループ (カテゴリやブランドなど) に指定するためのフォーム コントロールをアプリケーションが提供し、Azure AI Search は、このエクスペリエンスを支えるデータ構造とフィルターを提供します。

この記事では、Azure AI Search でファセット ナビゲーション構造を作成するための基本的な手順について説明します。

  • インデックス内のフィールド属性を設定する
  • 要求と応答を構造化する
  • プレゼンテーション層にナビゲーション コントロールとフィルターを追加する

プレゼンテーション層内のコードは、ファセット ナビゲーション エクスペリエンスで最も複雑な処理を実行します。 この記事の最後に一覧表示されているデモとサンプルでは、すべてを 1 つにまとめる方法を示す機能するコードが提供されます。

検索ページでのファセット ナビゲーション

ファセットは動的であり、クエリで返されます。 検索の応答には、結果のドキュメント内を移動するために使用されるすべてのファセット カテゴリが表示されます。 まずクエリが実行された後、現在の結果からファセットがプルされ、ファセット ナビゲーション構造にアセンブルされます。

Azure AI Search では、ファセットはレイヤーの深さが 1 つであり、階層化できません。 ファセット ナビゲーション構造になじみがない場合は、次の例の左側にそれが示されています。 カウントは、ファセットごとの一致の数を示しています。 同じドキュメントを複数のファセットで表現できます。

Screenshot of faceted search results.

ファセットを使用すると、探しているものが見つけやすくなり、検索結果がゼロ件になることはありません。 開発者は、ファセットを使用することで検索インデックスのナビゲーションに最も役立つ検索条件を公開できます。

インデックスでファセットを有効にする

ファセットは、"facetable" 属性を true に設定すると、インデックス定義内のフィールドごとに有効になります。

厳密に必要なわけではありませんが、検索アプリケーションでファセット ナビゲーション エクスペリエンスを支える必要なフィルターを構築できるように、"filterable" 属性も設定する必要があります。

"hotels" サンプル インデックスの次の例は、単一値または短いフレーズが含まれているカーディナリティの低いフィールド "Category"、"Tags"、"Rating" での "facetable" と "filterable" を示しています。

{
  "name": "hotels",  
  "fields": [
    { "name": "hotelId", "type": "Edm.String", "key": true, "searchable": false, "sortable": false, "facetable": false },
    { "name": "Description", "type": "Edm.String", "filterable": false, "sortable": false, "facetable": false },
    { "name": "HotelName", "type": "Edm.String", "facetable": false },
    { "name": "Category", "type": "Edm.String", "filterable": true, "facetable": true },
    { "name": "Tags", "type": "Collection(Edm.String)", "filterable": true, "facetable": true },
    { "name": "Rating", "type": "Edm.Int32", "filterable": true, "facetable": true },
    { "name": "Location", "type": "Edm.GeographyPoint" }
  ]
}

フィールドの選択

ファセットは、単一値フィールドのほか、コレクションに対して計算できます。 ファセット ナビゲーションで最適に機能するフィールドには、次の特性があります。

  • カーディナリティが低い (検索コーパス内の各ドキュメントにまたがって繰り返される個別の値が少ない)

  • ナビゲーション ツリーに適切に表示される短く、わかりやすい値 (1 つまたは 2 つの単語)

ファセット ナビゲーション構造内のファセットは、フィールド名自体ではなく、そのフィールド内の値によって生成されます。 ファセットが Color という名前の文字列フィールドである場合、ファセットは青色、緑色、またはそのフィールドの他の任意の値になります。

ベスト プラクティスとして、null 値、スペルミスまたは大文字と小文字の不一致、同じ単語の単数と複数のバージョンがないかどうかフィールドを確認してください。 既定では、フィルターやファセットでは字句解析やスペル チェックは実行されません。つまり、単語が 1 文字だけ異なる場合でも、"facetable" フィールドのすべての値が潜在的なファセットになります。 必要に応じて、"filterable" フィールドと "facetable" フィールドにノーマライザーを割り当てて、大文字/小文字の区別と文字のバリエーションが急増しないようにすることができます。

REST および Azure SDK での既定値

いずれかの Azure SDK を使用する場合、コードでフィールドの属性を明示的に設定する必要があります。 これに対して、REST API には、データ型に基づいたフィールド属性の既定値があります。 次のデータ型は、既定では "filterable" かつ "facetable" です。

  • Edm.String
  • Edm.DateTimeOffset
  • Edm.Boolean
  • 上記の任意の型のコレクション (Collection(Edm.String)Collection(Edm.Double) など)

ファセット ナビゲーションでは、Edm.GeographyPoint フィールドや Collection(Edm.GeographyPoint) フィールドは使用できません。 ファセットはカーディナリティが低いフィールドで最適に機能します。 地理座標の解像度のために、特定のデータセット内で任意の 2 組の座標が等しくなることはほとんどありません。 そのため、ファセットは地理座標ではサポートされていません。 場所でファセットするには、都市フィールドまたは地域フィールドが必要です。

ヒント

パフォーマンスとストレージの最適化のためのベスト プラクティスとして、ファセットとして使用されることのないフィールドではファセットを無効にします。 具体的には、ID や製品名などの一意の値の文字列フィールドは、ファセット ナビゲーションで誤って (無駄に) 使用されないように、"facetable": false に設定する必要があります。 これは、既定でフィルターとファセットを有効にする REST API の場合に特に当てはまります。

ファセットの要求と応答

ファセットはクエリで指定され、ファセット ナビゲーション構造は応答の先頭で返されます。 要求と応答の構造はきわめて単純です。 実際に、ファセット ナビゲーションの背後にある実際の作業は、後のセクションで説明されるプレゼンテーション層で行われます。

次の REST の例は、インデックス全体を範囲とする非修飾クエリ ("search": "*") です (組み込みのホテル サンプルを参照)。 ファセットは通常、フィールドの一覧ですが、このクエリは、下のより読みやすい応答に対して 1 つだけを示しています。

POST https://{{service_name}}.search.windows.net/indexes/hotels/docs/search?api-version={{api_version}}
{
    "search": "*",
    "queryType": "simple",
    "select": "",
    "searchFields": "",
    "filter": "",
    "facets": [ "Category"], 
    "orderby": "",
    "count": true
}

ファセット ナビゲーション構造を完全に埋めるには、開いているクエリで検索ページを初期化することが有効です。 要求でクエリ用語を渡すとすぐに、ファセット ナビゲーション構造は、インデックス全体ではなく、結果内の一致だけが範囲となります。

上記の例に対する応答には、先頭にファセット ナビゲーション構造が含まれています。 この構造は、"Category" 値と、それぞれに対するホテルの数で構成されています。 その後に検索結果の残りが続きますが、ここでは、簡潔にするために切り捨てられています。 この例は、いくつかの理由で適切に機能します。 このフィールドのファセットの数は制限 (既定値は 10) に収まるため、そのすべてが表示され、50 軒のホテルのインデックス内のすべてのホテルが、これらのカテゴリのうちの正確に 1 つで表されます。

{
    "@odata.context": "https://demo-search-svc.search.windows.net/indexes('hotels')/$metadata#docs(*)",
    "@odata.count": 50,
    "@search.facets": {
        "Category": [
            {
                "count": 13,
                "value": "Budget"
            },
            {
                "count": 12,
                "value": "Resort and Spa"
            },
            {
                "count": 9,
                "value": "Luxury"
            },
            {
                "count": 7,
                "value": "Boutique"
            },
            {
                "count": 5,
                "value": "Suite"
            },
            {
                "count": 4,
                "value": "Extended-Stay"
            }
        ]
    },
    "value": [
        {
            "@search.score": 1.0,
            "HotelId": "1",
            "HotelName": "Secret Point Motel",
            "Description": "The hotel is ideally located on the main commercial artery of the city in the heart of New York. A few minutes away is Time's Square and the historic centre of the city, as well as other places of interest that make New York one of America's most attractive and cosmopolitan cities.",
            "Category": "Boutique",
            "Tags": [
                "pool",
                "air conditioning",
                "concierge"
            ],
            "ParkingIncluded": false,
        }
    ]
}

ファセットの構文

ファセット クエリ パラメーターは "facetable" フィールドのコンマ区切りリストに設定され、データ型に応じて、さらにパラメーター化してカウント、並べ替え順序、範囲を設定できます (count:<integer>sort:<>interval:<integer>values:<list>)。 ファセット パラメーターの詳細については、REST API の「クエリ パラメーター」を参照してください。

POST https://{{service_name}}.search.windows.net/indexes/hotels/docs/search?api-version={{api_version}}
{
    "search": "*",
    "facets": [ "Category", "Tags,count:5", "Rating,values:1|2|3|4|5"],
    "count": true
}

ファセット ナビゲーション ツリーごとに、上位 10 個のファセットという既定の制限があります。 この既定値は、値のリストが管理しやすいサイズに保たれるため、ナビゲーション構造にとって意味のあるものです。 "count" に値を割り当てることによって、既定値をオーバーライドできます。 たとえば、"Tags,count:5" を指定すると、Tags セクションの下のタグの数が上位の 5 つに減ります。

Numeric 値と DateTime 値の場合のみ、ファセット フィールドの値を明示的に設定して (たとえば facet=Rating,values:1|2|3|4|5)、結果を連続した範囲 (数値または期間のどちらかに基づいた範囲) に分離できます。 また、facet=Rating,interval:1 のように "interval:" を追加することもできます。

各範囲は始点として 0 を、終点としてリストの値を使用して作成され、前の範囲を除くことによって個別の間隔が作成されます。

ファセット数の不一致

特定の状況では、シャーディング アーキテクチャのために、ファセット数が十分に正確でない場合があります。 すべての検索インデックスが複数のシャードに展開され、それぞれのシャードがドキュメント数によって上位 N 個のファセットを報告すると、単一の結果に結合されます。 これは、各シャードの上位 N 個のファセットに過ぎないため、ファセット応答で一致するドキュメントが欠落または不足する可能性があります。

正確性を保証するために、数: <number> を大きい数に人為的に膨らませて、各シャードから完全なレポートを強制的に実行できます。 ファセットを無制限にするために、"count": "0" を指定できます。 または、"count" を、ファセット フィールドの一意の値の数以上の値に設定できます。 たとえば、一意の値が 5 つある "size" フィールドでファセットを実行する場合、すべての一致がファセット応答で表されるように "count:5" を設定できます。

この対処法のトレードオフは、クエリ待機時間が増加することです。そのため、この方法は必要な場合にのみ使用してください。

プレゼンテーション層

アプリケーション コードでのパターンは、ファセット クエリ パラメーターを使用してファセット結果と共にファセット ナビゲーション構造を返し、さらに $filter 式を使用するというものです。 フィルター式はクリック イベントを処理し、ファセットの選択に基づいて検索結果をさらに絞り込みます。

ファセットとフィルターの組み合わせ

NYCJobs デモの JobsSearch.cs ファイルにある次のコード スニペットでは、ユーザーが Business Title ファセットから値を選択すると、選択された Business Title をフィルターに追加します。

if (businessTitleFacet != "")
  filter = "business_title eq '" + businessTitleFacet + "'";

ホテル サンプルからの別の例を次に示します。 次のコード スニペットにより、ユーザーがカテゴリ ファセットから値を選択した場合に、categoyrFacet がフィルターに追加されます。

if (!String.IsNullOrEmpty(categoryFacet))
    filter = $"category eq '{categoryFacet}'";

ファセット ナビゲーション用の HTML

NYCJobs サンプル アプリケーションの index.cshtml ファイルから取得された次の例は、検索結果ページにファセット ナビゲーションを表示するための静的な HTML 構造を示しています。 検索語句を送信するか、ファセットをオンまたはオフにすると、ファセットのリストが作成されたり、動的に再構築されたりします。

<div class="widget sidebar-widget jobs-filter-widget">
  <h5 class="widget-title">Filter Results</h5>
    <p id="filterReset"></p>
    <div class="widget-content">

      <h6 id="businessTitleFacetTitle">Business Title</h6>
      <ul class="filter-list" id="business_title_facets">
      </ul>

      <h6>Location</h6>
      <ul class="filter-list" id="posting_type_facets">
      </ul>

      <h6>Posting Type</h6>
      <ul class="filter-list" id="posting_type_facets"></ul>

      <h6>Minimum Salary</h6>
      <ul class="filter-list" id="salary_range_facets">
      </ul>

  </div>
</div>

HTML を動的に作成する

index.cshtml からの次のコード スニペット (NYCJobs デモでも同じです) は、最初のファセットである Business Title を表示するための HTML を動的に作成します。 同様の関数は、その他のファセットの HTML を動的に作成します。 各ファセットにはラベルと数があり、そのファセット結果に対して見つかった項目の数を表示します。

function UpdateBusinessTitleFacets(data) {
  var facetResultsHTML = '';
  for (var i = 0; i < data.length; i++) {
    facetResultsHTML += '<li><a href="javascript:void(0)" onclick="ChooseBusinessTitleFacet(\'' + data[i].Value + '\');">' + data[i].Value + ' (' + data[i].Count + ')</span></a></li>';
  }

  $("#business_title_facets").html(facetResultsHTML);
}

ファセットの操作に関するヒント

このセクションは、役立つ可能性があるヒントと回避策のコレクションです。

フィルター処理された結果のファセット ナビゲーション構造を非同期に保持する

Azure AI Search におけるファセット ナビゲーションの課題の 1 は、ファセットが現在の結果に対してのみ存在するという点です。 実際には、手順を遡って検索コンテンツで代替パスを探索することで、ユーザーが逆方向にナビゲートできるようにファセットの静的なセットを保持するのが一般的です。

これは一般的なユース ケースですが、ファセット ナビゲーション構造が現時点で標準で提供している機能には含まれていません。 静的なファセットを必要とする開発者は、通常、結果に適用されるクエリと、ナビゲーションのためにファセットの静的リストの作成に使用されるクエリの 2 つのフィルター処理されたクエリを発行してこの制限に対処しています。

ファセットをクリアする

検索結果ページを設計するときは、ファセットをクリアするためのメカニズムを追加することを忘れないでください。 チェック ボックスを追加する場合は、フィルターをクリアする方法を簡単に確認できます。 その他のレイアウトでは、階層リンク パターンや別の創造的な方法が必要になる場合があります。 ホテルの C# サンプルでは、空の検索を送信してページをリセットできます。 これに対して、NYCJobs サンプル アプリケーションでは、選択されたファセットの後に、そのファセットをクリアするためのクリック可能な [X] が表示されます。これは、ユーザーにとってより強力なビジュアル キューです。

追加のフィルターを使用したファセット結果のトリミング

ファセットの結果は、ファセット語句に一致する検索結果で見つかったドキュメントです。 cloud computing の検索結果を示す次の例では、254 個の項目は Content type も Internal specification で一致しています。 項目は必ずしも相互に排他的ではありません。 1 つの項目が両方のフィルターの条件を満たしている場合、その項目はそれぞれにカウントされます。 この重複は、ドキュメントのタグ付けを実装するために使用されることの多い Collection(Edm.String) フィールドのファセット処理で発生する可能性があります。

Search term: "cloud computing"
Content type
   Internal specification (254)
   Video (10)

一般に、ファセットの結果がなかなか小さくならない場合は、フィルターを増やして、検索をさらに絞り込むためのオプションをユーザーに提供することをお勧めします。

ファセットのみの検索エクスペリエンス

アプリケーションでファセット ナビゲーションのみを使用する (つまり、検索ボックスがない) 場合は、そのフィールドを searchable=falsefilterable=truefacetable=true としてマークして、よりコンパクトなインデックスを生成できます。 そのインデックスに逆インデックスは含まれず、テキスト分析やトークン化も存在しなくなります。 フィルターは、文字レベルでの完全一致で作成されます。

クエリ時に入力を検証する

信頼されていないユーザー入力に基づいて動的にファセットのリストを作成する場合は、ファセット フィールドの名前が有効であることを検証します。 または、.NET の Uri.EscapeDataString() または他のプラットフォームの同等機能を使用して URL を作成する場合は、名前をエスケープします。

デモとサンプル

一部のサンプルにはファセット ナビゲーションが含まれています。 このセクションにはサンプルへのリンクが含まれ、それぞれでどのクライアント ライブラリや言語が使用されているかも記載されています。

Web アプリに検索を追加する (React)

チュートリアルと C#PythonJavaScript でのサンプルには、ファセット ナビゲーションのほか、フィルター、候補、オートコンプリートが含まれています。 これらのサンプルでは、プレゼンテーション層に React を使用します。

NYCJobs サンプル コードとデモ (Ajax)

NYCJobs サンプルは、プレゼンテーション層で Ajax を使用する ASP.NET MVC アプリケーションです。 これは、ライブ デモ アプリとして、また GitHub 上の Azure-Samples リポジトリのソース コードとして入手できます。