次の方法で共有


チュートリアル: .NET SDK を使用してファセット ナビゲーションを追加する

ファセットを使用すると、結果をフィルター処理するための一連のリンクを提供することで、セルフダイレクト ナビゲーションが可能になります。 このチュートリアルでは、ファセット ナビゲーション構造をページの左側に配置し、ラベルとクリック可能なテキストを使用して結果をトリミングします。

このチュートリアルでは、以下の内容を学習します。

  • モデルのプロパティを IsFacetable として設定する
  • アプリにファセット ナビゲーションを追加する

概要

ファセットは、検索インデックス内のフィールドに基づいています。 facet=[string] を含むクエリ要求は、ファセットの対象となるフィールドを提供します。 アンパサンド (&) 文字で区切られた複数のファセット (&facet=category&facet=amenitiesなど) を含めるのが一般的です。 ファセット ナビゲーション構造を実装するには、ファセットとフィルターの両方を指定する必要があります。 フィルターは、クリック イベントで結果を絞り込むために使用されます。 たとえば、[予算] をクリックすると、その条件に基づいて結果がフィルター処理されます。

このチュートリアルでは、「検索結果にページングを追加する」チュートリアルで作成したページング プロジェクト 拡張します。

このチュートリアルのコードの完成版は、次のプロジェクトにあります。

前提条件

  • 2a-add-paging (GitHub) ソリューション。 このプロジェクトは、前のチュートリアルからビルドされた独自のバージョンか、GitHub からのコピーのいずれかになります。

モデルのプロパティを IsFacetable として設定する

モデル プロパティをファセット検索に配置するには、isFacetable タグを付ける必要があります。

  1. Hotel クラスを調査します。 たとえば、カテゴリタグIsFacetableとしてタグ付けされますが、HotelNameDescription はそのタグがされません。

    public partial class Hotel
    {
        [SimpleField(IsFilterable = true, IsKey = true)]
        public string HotelId { get; set; }
    
        [SearchableField(IsSortable = true)]
        public string HotelName { get; set; }
    
        [SearchableField(AnalyzerName = LexicalAnalyzerName.Values.EnLucene)]
        public string Description { get; set; }
    
        [SearchableField(AnalyzerName = LexicalAnalyzerName.Values.FrLucene)]
        [JsonPropertyName("Description_fr")]
        public string DescriptionFr { get; set; }
    
        [SearchableField(IsFilterable = true, IsSortable = true, IsFacetable = true)]
        public string Category { get; set; }
    
        [SearchableField(IsFilterable = true, IsFacetable = true)]
        public string[] Tags { get; set; }
    
        [SimpleField(IsFilterable = true, IsSortable = true, IsFacetable = true)]
        public bool? ParkingIncluded { get; set; }
    
        [SimpleField(IsFilterable = true, IsSortable = true, IsFacetable = true)]
        public DateTimeOffset? LastRenovationDate { get; set; }
    
        [SimpleField(IsFilterable = true, IsSortable = true, IsFacetable = true)]
        public double? Rating { get; set; }
    
        public Address Address { get; set; }
    
        [SimpleField(IsFilterable = true, IsSortable = true)]
        public GeographyPoint Location { get; set; }
    
        public Room[] Rooms { get; set; }
    }
    
  2. このチュートリアルの一部としてタグを変更しないため、変更されていないhotel.csファイルを閉じます。

    検索中に要求されたフィールドが適切にタグ付けされていないと、ファセット検索でエラーが発生します。

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

この例では、ユーザーが結果の左側に表示されるリンクの一覧から、ホテルの 1 つのカテゴリまたは 1 つのアメニティを選択できるようにします。 ユーザーはまず検索テキストを入力し、カテゴリまたはアメニティを選択して検索の結果を徐々に絞り込みます。

ビューにファセットのリストを渡すのがコントローラーの仕事です。 検索の進行に応じてユーザーの選択を維持するために、状態を保持するためのメカニズムとして一時ストレージを使用します。

ファセット ナビゲーションを使用して の検索を絞り込む

SearchData モデルにフィルター文字列を追加する

  1. SearchData.cs ファイルを開き、SearchData クラスに文字列プロパティを追加して、ファセット フィルター文字列を保持します。

    public string categoryFilter { get; set; }
    public string amenityFilter { get; set; }
    

ファセット アクション メソッドを追加する

ホーム コントローラーには、ファセット 新しいアクションが 1 つ必要です。また、既存の Index アクションと Page アクション、および RunQueryAsync メソッドに対する更新が必要です。

  1. Index(SearchData モデル) アクション メソッド に置き換えます。

    public async Task<ActionResult> Index(SearchData model)
    {
        try
        {
            // Ensure the search string is valid.
            if (model.searchText == null)
            {
                model.searchText = "";
            }
    
            // Make the search call for the first page.
            await RunQueryAsync(model, 0, 0, "", "").ConfigureAwait(false);
        }
        catch
        {
            return View("Error", new ErrorViewModel { RequestId = "1" });
        }
    
        return View(model);
    }
    
  2. PageAsync(SearchData モデル) アクションメソッドを に置き換えます。

    public async Task<ActionResult> PageAsync(SearchData model)
    {
        try
        {
            int page;
    
            // Calculate the page that should be displayed.
            switch (model.paging)
            {
                case "prev":
                    page = (int)TempData["page"] - 1;
                    break;
    
                case "next":
                    page = (int)TempData["page"] + 1;
                    break;
    
                default:
                    page = int.Parse(model.paging);
                    break;
            }
    
            // Recover the leftMostPage.
            int leftMostPage = (int)TempData["leftMostPage"];
    
            // Recover the filters.
            string catFilter = TempData["categoryFilter"].ToString();
            string ameFilter = TempData["amenityFilter"].ToString();
    
            // Recover the search text.
            model.searchText = TempData["searchfor"].ToString();
    
            // Search for the new page.
            await RunQueryAsync(model, page, leftMostPage, catFilter, ameFilter);
        }
    
        catch
        {
            return View("Error", new ErrorViewModel { RequestId = "2" });
        }
        return View("Index", model);
    }
    
  3. ユーザーがファセット リンクをクリックしたときにアクティブ化される、FacetAsync(SearchData モデル) アクション メソッドを追加します。 モデルには、カテゴリまたはアメニティ検索フィルターが含まれます。 PageAsync アクションの後に追加します。

    public async Task<ActionResult> FacetAsync(SearchData model)
    {
        try
        {
            // Filters set by the model override those stored in temporary data.
            string catFilter;
            string ameFilter;
            if (model.categoryFilter != null)
            {
                catFilter = model.categoryFilter;
            } else
            {
                catFilter = TempData["categoryFilter"].ToString();
            }
    
            if (model.amenityFilter != null)
            {
                ameFilter = model.amenityFilter;
            } else
            {
                ameFilter = TempData["amenityFilter"].ToString();
            }
    
            // Recover the search text.
            model.searchText = TempData["searchfor"].ToString();
    
            // Initiate a new search.
            await RunQueryAsync(model, 0, 0, catFilter, ameFilter).ConfigureAwait(false);
        }
        catch
        {
            return View("Error", new ErrorViewModel { RequestId = "2" });
        }
    
        return View("Index", model);
    }
    

検索フィルターを設定する

たとえば、ユーザーが特定のファセットを選択すると、Resort and Spa カテゴリをクリックすると、このカテゴリとして指定されたホテルのみが結果に返されます。 この方法で検索を絞り込むには、フィルターを設定する必要があります。

  1. RunQueryAsync メソッドを次のコードに置き換えます。 主に、カテゴリ フィルター文字列とアメニティ フィルター文字列を受け取り、SearchOptionsFilter パラメーターを設定します。

    private async Task<ActionResult> RunQueryAsync(SearchData model, int page, int leftMostPage, string catFilter, string ameFilter)
    {
        InitSearch();
    
        string facetFilter = "";
    
        if (catFilter.Length > 0 && ameFilter.Length > 0)
        {
            // Both facets apply.
            facetFilter = $"{catFilter} and {ameFilter}"; 
        } else
        {
            // One, or zero, facets apply.
            facetFilter = $"{catFilter}{ameFilter}";
        }
    
        var options = new SearchOptions
        {
            Filter = facetFilter,
    
            SearchMode = SearchMode.All,
    
            // Skip past results that have already been returned.
            Skip = page * GlobalVariables.ResultsPerPage,
    
            // Take only the next page worth of results.
            Size = GlobalVariables.ResultsPerPage,
    
            // Include the total number of results.
            IncludeTotalCount = true,
        };
    
        // Return information on the text, and number, of facets in the data.
        options.Facets.Add("Category,count:20");
        options.Facets.Add("Tags,count:20");
    
        // Enter Hotel property names into this list, so only these values will be returned.
        options.Select.Add("HotelName");
        options.Select.Add("Description");
        options.Select.Add("Category");
        options.Select.Add("Tags");
    
        // For efficiency, the search call should be asynchronous, so use SearchAsync rather than Search.
        model.resultList = await _searchClient.SearchAsync<Hotel>(model.searchText, options).ConfigureAwait(false);
    
        // This variable communicates the total number of pages to the view.
        model.pageCount = ((int)model.resultList.TotalCount + GlobalVariables.ResultsPerPage - 1) / GlobalVariables.ResultsPerPage;
    
        // This variable communicates the page number being displayed to the view.
        model.currentPage = page;
    
        // Calculate the range of page numbers to display.
        if (page == 0)
        {
            leftMostPage = 0;
        }
        else if (page <= leftMostPage)
        {
            // Trigger a switch to a lower page range.
            leftMostPage = Math.Max(page - GlobalVariables.PageRangeDelta, 0);
        }
        else if (page >= leftMostPage + GlobalVariables.MaxPageRange - 1)
        {
            // Trigger a switch to a higher page range.
            leftMostPage = Math.Min(page - GlobalVariables.PageRangeDelta, model.pageCount - GlobalVariables.MaxPageRange);
        }
        model.leftMostPage = leftMostPage;
    
        // Calculate the number of page numbers to display.
        model.pageRange = Math.Min(model.pageCount - leftMostPage, GlobalVariables.MaxPageRange);
    
        // Ensure Temp data is stored for the next call.
        TempData["page"] = page;
        TempData["leftMostPage"] = model.leftMostPage;
        TempData["searchfor"] = model.searchText;
        TempData["categoryFilter"] = catFilter;
        TempData["amenityFilter"] = ameFilter;
    
        // Return the new view.
        return View("Index", model);
    }
    

    カテゴリの および タグの プロパティが、返すために選択する 項目の一覧に追加されていることに注意してください。 この追加はファセット ナビゲーションが機能するための要件ではありませんが、この情報を使用してフィルターが正しく動作していることを確認します。

ビューには、いくつかの重要な変更が必要になります。

  1. まず、(wwwroot/css フォルダー内の) hotels.css ファイルを開き、次のクラスを追加します。

    .facetlist {
        list-style: none;
    }
    
    .facetchecks {
        width: 250px;
        display: normal;
        color: #666;
        margin: 10px;
        padding: 5px;
    }
    
    .facetheader {
        font-size: 10pt;
        font-weight: bold;
        color: darkgreen;
    }
    
  2. ビューの場合は、出力をテーブルに整理して、左側のファセット リストと右側の結果を適切に配置します。 index.cshtml ファイルを開きます。 HTML <本文> タグの内容全体を次のコードに置き換えます。

    <body>
        @using (Html.BeginForm("Index", "Home", FormMethod.Post))
        {
            <table>
                <tr>
                    <td></td>
                    <td>
                        <h1 class="sampleTitle">
                            <img src="~/images/azure-logo.png" width="80" />
                            Hotels Search - Facet Navigation
                        </h1>
                    </td>
                </tr>
    
                <tr>
                    <td></td>
                    <td>
                        <!-- Display the search text box, with the search icon to the right of it.-->
                        <div class="searchBoxForm">
                            @Html.TextBoxFor(m => m.searchText, new { @class = "searchBox" }) <input value="" class="searchBoxSubmit" type="submit">
                        </div>
                    </td>
                </tr>
    
                <tr>
                    <td valign="top">
                        <div id="facetplace" class="facetchecks">
    
                            @if (Model != null && Model.resultList != null)
                            {
                                List<string> categories = Model.resultList.Facets["Category"].Select(x => x.Value.ToString()).ToList();
    
                                if (categories.Count > 0)
                                {
                                    <h5 class="facetheader">Category:</h5>
                                    <ul class="facetlist">
                                        @for (var c = 0; c < categories.Count; c++)
                                        {
                                            var facetLink = $"{categories[c]} ({Model.resultList.Facets["Category"][c].Count})";
                                            <li>
                                                @Html.ActionLink(facetLink, "FacetAsync", "Home", new { categoryFilter = $"Category eq '{categories[c]}'" }, null)
                                            </li>
                                        }
                                    </ul>
                                }
    
                                List<string> tags = Model.resultList.Facets["Tags"].Select(x => x.Value.ToString()).ToList();
    
                                if (tags.Count > 0)
                                {
                                    <h5 class="facetheader">Amenities:</h5>
                                    <ul class="facetlist">
                                        @for (var c = 0; c < tags.Count; c++)
                                        {
                                            var facetLink = $"{tags[c]} ({Model.resultList.Facets["Tags"][c].Count})";
                                            <li>
                                                @Html.ActionLink(facetLink, "FacetAsync", "Home", new { amenityFilter = $"Tags/any(t: t eq '{tags[c]}')" }, null)
                                            </li>
                                        }
                                    </ul>
                                }
                            }
                        </div>
                    </td>
                    <td valign="top">
                        <div id="resultsplace">
                            @if (Model != null && Model.resultList != null)
                            {
                                // Show the result count.
                                <p class="sampleText">
                                    @Model.resultList.TotalCount Results
                                </p>
    
                                var results = Model.resultList.GetResults().ToList();
    
                                @for (var i = 0; i < results.Count; i++)
                                {
                                    string amenities = string.Join(", ", results[i].Document.Tags);
    
                                    string fullDescription = results[i].Document.Description;
                                    fullDescription += $"\nCategory: {results[i].Document.Category}";
                                    fullDescription += $"\nAmenities: {amenities}";
    
    
                                    // Display the hotel name and description.
                                    @Html.TextAreaFor(m => results[i].Document.HotelName, new { @class = "box1" })
                                    @Html.TextArea($"desc{i}", fullDescription, new { @class = "box2" })
                                }
                            }
                        </div>
                    </td>
                </tr>
    
                <tr>
                    <td></td>
                    <td valign="top">
                        @if (Model != null && Model.pageCount > 1)
                        {
                            // If there is more than one page of results, show the paging buttons.
                            <table>
                                <tr>
                                    <td class="tdPage">
                                        @if (Model.currentPage > 0)
                                        {
                                            <p class="pageButton">
                                                @Html.ActionLink("|<", "PageAsync", "Home", new { paging = "0" }, null)
                                            </p>
                                        }
                                        else
                                        {
                                            <p class="pageButtonDisabled">|&lt;</p>
                                        }
                                    </td>
    
                                    <td class="tdPage">
                                        @if (Model.currentPage > 0)
                                        {
                                            <p class="pageButton">
                                                @Html.ActionLink("<", "PageAsync", "Home", new { paging = "prev" }, null)
                                            </p>
                                        }
                                        else
                                        {
                                            <p class="pageButtonDisabled">&lt;</p>
                                        }
                                    </td>
    
                                    @for (var pn = Model.leftMostPage; pn < Model.leftMostPage + Model.pageRange; pn++)
                                    {
                                        <td class="tdPage">
                                            @if (Model.currentPage == pn)
                                            {
                                                // Convert displayed page numbers to 1-based and not 0-based.
                                                <p class="pageSelected">@(pn + 1)</p>
                                            }
                                            else
                                            {
                                                <p class="pageButton">
                                                    @Html.ActionLink((pn + 1).ToString(), "PageAsync", "Home", new { paging = @pn }, null)
                                                </p>
                                            }
                                        </td>
                                    }
    
                                    <td class="tdPage">
                                        @if (Model.currentPage < Model.pageCount - 1)
                                        {
                                            <p class="pageButton">
                                                @Html.ActionLink(">", "PageAsync", "Home", new { paging = "next" }, null)
                                            </p>
                                        }
                                        else
                                        {
                                            <p class="pageButtonDisabled">&gt;</p>
                                        }
                                    </td>
    
                                    <td class="tdPage">
                                        @if (Model.currentPage < Model.pageCount - 1)
                                        {
                                            <p class="pageButton">
                                                @Html.ActionLink(">|", "PageAsync", "Home", new { paging = Model.pageCount - 1 }, null)
                                            </p>
                                        }
                                        else
                                        {
                                            <p class="pageButtonDisabled">&gt;|</p>
                                        }
                                    </td>
                                </tr>
                            </table>
                        }
                    </td>
                </tr>
            </table>
        }
    </body>
    

    Html.ActionLink 呼び出しの使用に注意してください。 この呼び出しは、ユーザーがファセット リンクをクリックしたときに、有効なフィルター文字列をコントローラーに伝えます。

アプリを実行してテストする

ユーザーへのファセット ナビゲーションの利点は、1 回のクリックで検索を絞り込むことができる点です。これは、次の順序で表示できます。

  1. アプリを実行し、検索テキストとして「airport」と入力します。 ファセットの一覧が左側にきちんと表示されることを確認します。 これらのファセットは、テキスト データに "空港" があるホテルに適用されるすべてであり、発生頻度がカウントされます。

    ファセット ナビゲーションを使用して の検索を絞り込む

  2. リゾート&スパ カテゴリをクリックします。 すべての結果がこのカテゴリに含まれるかどうかを確認します。

    「リゾート&スパ」の検索を絞り込む

  3. コンチネンタルブレックファーストのアメニティ をクリックしてください。 すべての結果が、選択したアメニティの "リゾートとスパ" カテゴリに残っているかどうかを確認します。

    「コンチネンタルブレックファースト」 に検索を絞り込む

  4. 他のカテゴリを選択してから、1 つのアメニティを選択して、縮小結果を表示してみてください。 次に、もう一方の方法、1 つのアメニティ、1 つのカテゴリを試してみてください。 空の検索を送信してページをリセットします。

    ファセット リスト (カテゴリなど) で 1 つの選択を行うと、カテゴリ リスト内の以前の選択がオーバーライドされます。

お持ち帰り

このプロジェクトの次の点について考えてみましょう。

  • ファセット ナビゲーションに含めるために、各ファセット可能フィールドを IsFacetable プロパティでマークすることが不可欠です。
  • ファセットをフィルターと組み合わせて結果を減らします。
  • ファセットは累積され、各選択は前のファセットに基づいて構築され、結果がさらに絞り込まれます。

次のステップ

次のチュートリアルでは、順序付けの結果について説明します。 ここまでは、結果はデータベース内にある順序で並べ替えられます。