Kurz: Přidání fasetové navigace pomocí sady .NET SDK

Facety umožňují samoobslužnou navigaci tím, že poskytují sadu odkazů pro filtrování výsledků. V tomto tutoriálu je na levé straně stránky umístěna fasetová navigační struktura s popisky a klikacím textem, který upraví výběr výsledků.

V tomto návodu se naučíte, jak:

  • Nastavení vlastností modelu jako IsFacetable
  • Přidání fasetové navigace do aplikace

Přehled

Omezující vlastnosti jsou založeny na polích ve vyhledávacím indexu. Požadavek na dotaz, který obsahuje facet=[řetězec], určuje pole, podle kterého se má faceta rozčlenit. Je běžné zahrnout více aspektů, například &facet=category&facet=amenities, přičemž každý je oddělen znakem ampersand (&). Implementace fasetové navigační struktury vyžaduje, abyste zadali facety i filtry. Filtr se používá u události kliknutí k zúžení výsledků. Kliknutím na "rozpočet" například vyfiltrujte výsledky na základě daného kritéria.

Tento tutoriál rozšiřuje projekt stránkování vytvořený v rámci tutoriálu Přidání stránkování do výsledků hledání.

Dokončenou verzi kódu v tomto kurzu najdete v následujícím projektu:

Požadavky

  • 2a-add-paging (GitHub) řešení. Tento projekt může být buď vaše vlastní verze sestavená z předchozího kurzu, nebo kopie z GitHubu.

Nastavte vlastnosti modelu jako IsFacetable

Aby byla vlastnost modelu umístěna ve fasetovém vyhledávání, musí být označena isFacetable.

  1. Prohlédněte si hotelovou třídu. Kategorie a značky jsou například označené jako IsFacetable, ale HotelName a Description nejsou.

    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. V rámci tohoto kurzu nebudeme měnit žádné značky, proto zavřete soubor hotel.cs bez změny.

    Poznámka:

    Filtrované hledání vyvolá chybu, pokud pole požadované při hledání není správně označeno.

Přidání fasetové navigace do aplikace

V tomto příkladu umožníme uživateli vybrat jednu kategorii hotelu nebo jednu vybavení ze seznamů odkazů zobrazených nalevo od výsledků. Uživatel začne zadáním určitého hledaného textu a postupně zúží výsledky hledání výběrem kategorie nebo vybavení.

Je to úlohou kontroléru, aby předával seznamy aspektů do zobrazení. Abychom zachovali výběry uživatelů při průběhu hledání, používáme dočasné úložiště jako mechanismus pro zachování stavu.

Použití fasetové navigace k zúžení hledání

Přidání řetězců filtru do modelu SearchData

  1. cs-CZ: Otevřete soubor SearchData.cs a přidejte řetězcové vlastnosti do třídy SearchData, k uložení řetězců filtru okrajů.

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

Přidejte metodu akce Facet

Řadič potřebuje jednu novou akci, Facet, a také aktualizace pro stávající akce Index a Page, a metodu RunQueryAsync.

  1. Nahraďte metodu akce Index(SearchData model).

    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. Nahraďte metodu akce PageAsync(SearchData model).

    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. Přidejte akční metodu FacetAsync(SearchData model), která se aktivuje, když uživatel klikne na odkaz facety. Model bude obsahovat filtr vyhledávání kategorií nebo vybavení. Přidejte ji za akci 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);
    }
    

Nastavení vyhledávacího filtru

Když uživatel vybere určitou omezující vlastnost, například klikne na kategorii Resort and Spa , pak ve výsledcích by měly být vráceny pouze hotely, které jsou zadány jako tato kategorie. Abychom hledání tímto způsobem zúžili, musíme nastavit filtr.

  1. Nahraďte metodu RunQueryAsync následujícím kódem. Primárně to bere řetězec filtrů kategorií a vybavení a nastaví parametr Filter u SearchOptions.

    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);
    }
    

    Všimněte si, že vlastnosti Kategorie a Značky se přidají do seznamu položek Select , které se mají vrátit. Tento doplněk není požadavkem, aby fasetová navigace fungovala, ale tyto informace používáme k ověření správného fungování filtrů.

Zobrazení bude vyžadovat určité významné změny.

  1. Začněte otevřením souboru hotels.css (ve složce wwwroot/css) a přidejte následující třídy.

    .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. V zobrazení uspořádejte výstup do tabulky, abyste přehledně zarovnáli seznamy omezujících prvků vlevo a výsledky vpravo. Otevřete soubor index.cshtml. Nahraďte veškerý obsah značek <body> HTML následujícím kódem.

    <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>
    

    Všimněte si použití volání Html.ActionLink . Toto volání předává platné řetězce filtru ke kontroleru, když uživatel klikne na odkaz na aspekt.

Spuštění a otestování aplikace

Výhodou fasetové navigace pro uživatele je, že může zúžit vyhledávání jediným kliknutím, které můžeme zobrazit v následujícím pořadí.

  1. Spusťte aplikaci a jako hledaný text zadejte "airport". Ověřte, že se seznam aspektů zobrazuje přehledně vlevo. Tyto aspekty se týkají hotelů, které mají v textových datech "letiště," spolu s počtem, jak často se vyskytují.

    Použití fasetové navigace k zúžení hledání

  2. Klikněte na kategorii Resort and Spa . Ověřte, že jsou všechny výsledky v této kategorii.

    Zúžení vyhledávání na

  3. Klikněte na možnost kontinentální snídaně. Ověřte, že všechny výsledky jsou stále v kategorii Resort and Spa s vybraným vybavením.

    Zúžení hledání na

  4. Zkuste vybrat jakoukoli jinou kategorii, poté jednu vybavenost a podívejte se, jak se zužují výsledky. Poté zkuste opačný způsob: nejprve jedno vybavení, pak jednu kategorii. Pošlete prázdný dotaz, aby se stránka resetovala.

    Poznámka:

    Když je v seznamu faset proveden jeden výběr (například kategorie), přepíše jakýkoli předchozí výběr uvnitř seznamu kategorií.

Klíčové poznatky

Vezměte v úvahu následující poznatky z tohoto projektu:

  • Je nutné označit každé fasetové pole vlastností IsFacetable pro zahrnutí do faset navigace.
  • Facety se zkombinují s filtry, aby se zúžily výsledky.
  • Facety jsou kumulativní, přičemž každý výběr navazuje na předchozí, aby se výsledky dále zúžily.

Další kroky

V dalším kurzu se podíváme na seřazení výsledků. Do tohoto okamžiku jsou výsledky seřazené jednoduše v pořadí, v jakém se nacházejí v databázi.