Sdílet prostřednictvím


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

Omezující vlastnosti umožňují vlastní navigaci tím, že poskytují sadu odkazů pro filtrování výsledků. V tomto kurzu se na levé straně stránky umístí fasetová navigační struktura s popisky a textem, na který se dá kliknout, aby se výsledky ořízly.

V tomto kurzu se naučíte:

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

Přehled

Omezující vlastnosti jsou založené na polích ve vyhledávacím indexu. Požadavek dotazu obsahující facet=[string] poskytuje pole, podle kterého se má omezující vlastnost provést. Je běžné, že obsahuje více omezujících znaků, například &facet=category&facet=amenities, oddělte je znakem ampersandu (&). Implementace fasetové navigační struktury vyžaduje, abyste zadali omezující vlastnosti i filtry. Filtr se používá u události click k zúžení výsledků. Když například kliknete na rozpočet, vyfiltrují se výsledky na základě daného kritéria.

Tento kurz rozšiřuje projekt stránkování vytvořený v kurzu 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

Nastavení vlastností modelu jako IsFacetable

Aby se vlastnost modelu nacházela ve vyhledávání omezující vlastnosti, musí být označena pomocí IsFacetable.

  1. Prohlédněte si hotelový předmět. 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, takže zavřete soubor hotel.cs beze změn.

    Poznámka

    Hledání omezující vlastnosti vyvolá chybu, pokud pole požadované ve vyhledávání není správně označené.

Přidání fazetové 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 hledaného textu a pak postupně zúží výsledky hledání výběrem kategorie nebo vybavení.

Úkolem kontroleru je předat do zobrazení seznamy omezujících vlastností. Abychom zachovali výběry uživatelů v průběhu hledání, používáme dočasné úložiště jako mechanismus pro zachování stavu.

Použití navigace omezujícími vlastnostmi k zúžení hledání

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

  1. Otevřete soubor SearchData.cs a přidejte vlastnosti řetězce do třídy SearchData , abyste mohli uchovávat řetězce filtru omezující vlastnosti.

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

Přidání metody akce Facet

Domovský řadič potřebuje jednu novou akci Facet a aktualizuje stávající akce Index a Page a metodu RunQueryAsync .

  1. Nahraďte metodu akce Index(model 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. Nahraďte metodu akce PageAsync(model 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. Přidejte metodu akce FacetAsync (model SearchData), která se aktivuje, když uživatel klikne na odkaz omezující vlastnosti. Model bude obsahovat filtr vyhledávání kategorií nebo vybavení. Přidejte ho 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 a lázně , pak by se měly ve výsledcích vrátit pouze hotely, které jsou zadané 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ě používá řetězec filtru kategorie a řetězec filtru vybavení a nastavuje parametr Filter pro 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 Category (Kategorie ) a Tags (Značky ) se přidají do seznamu Vybrat položky, které se mají vrátit. Toto přidání není podmínkou, aby navigace omezujícími vlastnostmi fungovala, ale tyto informace používáme k ověření správného fungování filtrů.

Zobrazení bude vyžadovat několik významných změn.

  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. Pro zobrazení uspořádejte výstup do tabulky, abyste přehledně zarovnali seznamy faset vlevo a výsledky napravo. Otevřete soubor index.cshtml. Celý obsah značek textu> HTML <nahraďte 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í sdělí kontroleru platné řetězce filtru, když uživatel klikne na fazetu.

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

Výhodou navigace omezující vlastnosti pro uživatele je, že můžou zúžit hledání jediným kliknutím, což můžeme ukázat v následujícím pořadí.

  1. Spusťte aplikaci a jako hledaný text zadejte "airport". Ověřte, že se seznam omezujících vlastností zobrazuje úhledně vlevo. Všechny tyto omezující vlastnosti platí pro hotely, které mají v textových datech "letiště", s počtem výskytů.

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

  2. Klikněte na kategorii Resort a lázně . Ověřte, že všechny výsledky jsou v této kategorii.

    Zúžení hledání na

  3. Klikněte na kontinentální snídani vybavení. 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 jinou kategorii, pak jednu kategorii a zobrazit zužující výsledky. Pak zkuste naopak, jednu vybavení, pak jednu kategorii. Odešlete prázdné hledání pro resetování stránky.

    Poznámka

    Když se jeden výběr provede v seznamu omezujících součástí (například kategorie), přepíše se tím jakýkoli předchozí výběr v seznamu kategorií.

Shrnutí

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

  • Každé facetable pole je nutné označit vlastností IsFacetable pro zahrnutí do navigace omezující vlastností.
  • Omezující vlastnosti se kombinují s filtry, aby se snížily výsledky.
  • Omezující vlastnosti jsou kumulativní, přičemž každý výběr vychází z předchozího, aby se výsledky dále zúžily.

Další kroky

V dalším kurzu se podíváme na řazení výsledků. Až do tohoto okamžiku jsou výsledky seřazené jednoduše v pořadí, v jakém jsou umístěné v databázi.