65.9K
CodeProject 正在变化。 阅读更多。
Home

使用 dtSearch® ASP.NET Core WebDemo 示例应用程序极速提升您的搜索体验

emptyStarIconemptyStarIconemptyStarIconemptyStarIconemptyStarIcon

0/5 (0投票)

2014 年 5 月 8 日

CPOL

8分钟阅读

viewsIcon

37165

在本文中,我将把 Telerik UI for ASP.NET 添加到我之前的 dtSearch 分面搜索文章中,并进行一些重构,使我的搜索页面看起来更好、更易于使用。

第一部分:使用 dtSearch 进行分面搜索(使用 SQL 和 .NET)
第二部分:使用 dtSearch 和 Telerik UI for ASP.NET 极大地增强您的搜索体验

相关文章: 您的口袋里的搜索引擎 - 介绍 dtSearch 在 Android 上

引言

我之前写过一篇关于 如何开始使用分面搜索功能 的文章,使用的是 dtSearch 库。代码有点复杂,但确实达到了我的目标,即在每次搜索请求时都不查询我的 SQL 数据库。我想让它更完善一些,并使代码更简单。在本文中,我将添加 Telerik UI for ASP.NET 并进行一些重构,使我的搜索页面看起来更好、更易于使用。

图 1 - 我们搜索页面的当前状态

布局 - 让我们更容易看到

我想解决的第一个问题是,让我的搜索结果中左侧的分面列不固定在某个宽度。该宽度假定请求内容的浏览器的尺寸。为了使其更具灵活性,我将整个页面包装在 Telerik RadSplitter 和 RadPanes 中。通过两个水平窗格,这将产生一种框架效果,左侧窗格的内容可以根据拆分条的位置进行扩展或隐藏在滚动条后面。创建此布局的标记从以下开始

<telerik:RadSplitter runat="server" Orientation="Vertical" Width="100%" Height="600">
  <telerik:RadPane runat="server" Width="150" MinWidth="150" MaxWidth="300">
    <!-- FACET SEARCH BAR GOES HERE -->
  </telerik:RadPane>
  <telerik:RadSplitBar runat="server"></telerik:RadSplitBar>
  <telerik:RadPane runat="server">
    <!-- SEARCH RESULTS GO HERE -->
  </telerik:RadPane>
</telerik:RadSplitter>
列表 1 - RadSplitter 和 Panes 的布局

RadSplitter 将包含两个窗格,通过 Orientation 属性垂直拆分。窗格按照它们在标记中出现的顺序从左到右呈现。左侧窗格的最小宽度为 150 像素,最大宽度为 300 像素。接下来是 RadSplitBar 控件;这是用户可以拖动以调整两个窗格大小的条。最后一个窗格将填充剩余空间。无需额外的 C# 代码即可使此布局在浏览器中运行。

搜索结果网格 - 现在支持排序!

虽然我之前的演示文稿使用了网格,并以清晰的格式向用户展示了我的产品搜索信息,但展示产品的其他功能并允许对结果进行排序会更好。幸运的是,dtSearch 和 Telerik RadGrid 都具有排序功能。让我们将在 RadGrid 中显示搜索结果,并将其排序功能连接到我们的 dtSearch 排序处理程序。

我已经使用以下标记“全副武装”了我的 RadGrid

<telerik:RadGrid runat="server" ID="grid" PageSize="10" Height="100%" 
  OnPageIndexChanged="grid_PageIndexChanged" 
  OnPageSizeChanged="grid_PageSizeChanged" 
  OnSortCommand="grid_SortCommand"
  OnItemDataBound="grid_ItemDataBound"
  AllowPaging="true" AllowCustomPaging="true" 
  AllowSorting="true" 
  EnableHeaderContextFilterMenu="false">
  <MasterTableView AutoGenerateColumns="false" 
    PagerStyle-AlwaysVisible="true"
    AlternatingItemStyle-BackColor="LightBlue">
    <Columns>
      <telerik:GridBoundColumn DataField="ProductNum" HeaderText="Product ID" />
      <telerik:GridBoundColumn DataField="Name" HeaderText="Name" />
      <telerik:GridBoundColumn DataField="Manufacturer" HeaderText="Manufacturer" />
      <telerik:GridBoundColumn DataField="Age" HeaderText="Recommended Age" />
      <telerik:GridBoundColumn DataField="NumPlayers" HeaderText="# Players" />
      <telerik:GridBoundColumn DataField="Price" HeaderText="Price" 
        DataFormatString="{0:$0.00}" ItemStyle-HorizontalAlign="Right" />
    </Columns>
  </MasterTableView>
  <ClientSettings Resizing-AllowColumnResize="true">
             
  </ClientSettings>
</telerik:RadGrid>
列表 2 - RadGrid 标记,全副武装

让我们回顾一下这个标记;有很多内容

  • PageIndexChangedPageSizeChangedSortCommandItemDataBound 定义了事件处理程序。
  • 已激活排序、分页和自定义分页。自定义分页对于驱动 dtSearch SearchJob 对象所需的自定义数据分页操作尤其必要。通常,当一个 IQueryable 集合被传递给 Telerik RadGrid 时,它会自己处理排序和分页操作。接下来我们将看看如何实现这些处理程序。
  • 列已定义了适当的格式,并且 AutoGenerateColumns 已禁用。这可以防止我们的 ProductSearchResult 的一些额外属性被转换为列。我本可以在这些列上定义一个 SortExpression 值来定义服务器端如何对每列进行排序。没有此属性,排序字段默认为绑定到该列的字段名称。
  • ClientSettings - 调整大小 - AllowColumnResize 设置为 true,以允许最终用户根据需要调整列的大小。

我已经重构了我上一篇文章中的 DoSearch 方法,使其具有以下签名,它将返回一个 ProductSearchResult 对象集合

public IEnumerable<ProductSearchResult> DoSearch(
  string searchTerm, 
  int pageNum = 0, 
  int pageSize = 10, 
  string sortFieldName = "")
{
列表 3 - 新的 DoSearch 方法签名

这使我能够将我的 grid_SortCommand 方法连接起来,将网格提交给服务器的排序信息传递给 DoSearch 方法。分页操作的语法非常相似,将分页操作委托回 DoSearch 方法

protected void grid_SortCommand(object sender, GridSortCommandEventArgs e)
{
  var sortExpression = string.Concat(e.SortExpression, " ", e.NewSortOrder == GridSortOrder.Ascending ? "ASC" : "DESC");

  grid.DataSource = DoSearch(searchBox.Text.Trim(), 0, 
    grid.PageSize, sortExpression);
  grid.CurrentPageIndex = 0;
  grid.DataBind();
}
  protected void grid_PageIndexChanged(object sender, GridPageChangedEventArgs e)
  {
    grid.DataSource = DoSearch(searchBox.Text.Trim(), e.NewPageIndex, 
      grid.PageSize, grid.MasterTableView.SortExpressions.GetSortString());
    grid.CurrentPageIndex = e.NewPageIndex;
    grid.DataBind();
  }
列表 4 - 排序和分页处理程序

dtSearch SearchJob 中的排序操作再简单不过了。SearchJob.Results 有一个 Sort 方法,它接受存储用于排序的字段的名称。我添加了一段额外的代码来处理传递给方法的 NewSortOrder 属性,以便它可以像普通的排序表达式一样传递给 DoSearch 方法。添加到我的 DoSearch 方法中的代码片段如下

// Handle sort requests from the grid
if (sortFieldName == string.Empty)
{
  searchJob.Results.Sort(SortFlags.dtsSortByRelevanceScore, "Name");
}
else
{
  var sortDirection = sortFieldName.ToUpperInvariant().Contains("DESC") ? SortFlags.dtsSortDescending : SortFlags.dtsSortAscending;
  sortFieldName = sortFieldName.Split(' ')[0];
  sj.Results.Sort(SortFlags.dtsSortByField | sortDirection, sortFieldName);
}
列表 5 - 带 SearchJob 的排序机制

初始排序机制是根据搜索结果的相关性进行排序。对于要排序的字段,将字段名传递给 Sort 方法,并相应地设置 SortFlags 以进行升序或降序排序。dtSearch 在此排序操作中的唯一诀窍是,它首先获取搜索结果,然后进行排序。因此,我需要将 SearchJob.MaxFilesToRetrieve 的限制提高到一个天文数字,以确保所有结果都在排序操作应用之前的一次性检索。

为了完成产品数据网格,我修改了我 DoSearch 方法中的分页操作,现在从搜索结果中获取产品中的每个字段。DoSearch 的完整语法如下

public IEnumerable<ProductSearchResult> DoSearch(
  string searchTerm,
  int pageNum = 0,
  int pageSize = 10,
  string sortFieldName = "")
{

  var sj = new SearchJob();
  sj.IndexesToSearch.Add(SearchIndexer._SearchIndex);
  sj.MaxFilesToRetrieve = 2000;
  sj.WantResultsAsFilter = true;
  sj.Request = searchTerm;

  // Add filter condition if necessary
  if (!string.IsNullOrEmpty(FacetFilter) && FacetFilter.Contains('='))
  {
    var filter = FacetFilter.Split('=');
    sj.BooleanConditions = string.Format("{0} contains {1}", filter[0], filter[1]);
  }

  // Prevent the code from running endlessly
  sj.AutoStopLimit = 1000;
  sj.TimeoutSeconds = 10;
  sj.Execute();

  this.TotalHitCount = sj.FileCount;
  ExtractFacets(sj);

  // Handle sort requests from the grid
  if (sortFieldName == string.Empty)
  {
    sj.Results.Sort(SortFlags.dtsSortByRelevanceScore, "Name");
  }
  else
  {
    var sortDirection = sortFieldName.ToUpperInvariant().Contains("DESC") ? SortFlags.dtsSortDescending : SortFlags.dtsSortAscending;
    sortFieldName = sortFieldName.Split(' ')[0];
    sj.Results.Sort(SortFlags.dtsSortByField | sortDirection, sortFieldName);
  }

  this.SearchResults = sj.Results;

  // Manual Paging
  var firstItem = pageSize * pageNum;
  var lastItem = firstItem + pageSize;
  lastItem = (lastItem > this.SearchResults.Count) ? this.SearchResults.Count : lastItem;
  var outList = new List<ProductSearchResult>();
  for (int i = firstItem; i < lastItem; i++)
  {
    this.SearchResults.GetNthDoc(i);
    outList.Add(new ProductSearchResult
    {
      DocPosition = i,
      ProductNum = this.SearchResults.DocName,
      Name = this.SearchResults.get_DocDetailItem("Name"),
      Manufacturer = this.SearchResults.get_DocDetailItem("Manufacturer"),
      Age = this.SearchResults.get_DocDetailItem("Age"),
      NumPlayers = this.SearchResults.get_DocDetailItem("NumPlayers"),
      Price = decimal.Parse(this.SearchResults.get_DocDetailItem("Price"))
    });
  }

  return outList;

}
列表 6 - DoSearch 方法的完整列表

首先,您会看到我正在使用一个名为 FacetFilter 的属性进行过滤。这是一个字符串值,它从左侧的分面列表返回到我的页面。我已经更新了该列以使用 Telerik RadPanelBar。它的标记很简单,定义了我想要过滤的三个分面

<telerik:RadPanelBar runat="server" ID="facets" 
  Width="100%" OnItemClick="facets_ItemClick">
  <Items>
    <telerik:RadPanelItem runat="server" Text="Manufacturer"
       Value="Manufacturer"></telerik:RadPanelItem>
    <telerik:RadPanelItem runat="server" Text="Age" 
       Value="Age"></telerik:RadPanelItem>
    <telerik:RadPanelItem runat="server" Text="# Players" 
       Value="NumPlayers"></telerik:RadPanelItem>
  </Items>
</telerik:RadPanelBar>
列表 7 - 支持 Facet PanelBar 的标记

此控件将为页面上显示的分面提供手风琴式外观和感觉。每个呈现的分面的数据在 ExtractFacets 方法中更新

  private void ExtractFacets(SearchJob sj)
  {

    var filter = sj.ResultsAsFilter;

    var facetsToSearch = new[] { "Manufacturer", "Age", "NumPlayers" };

    // Configure the WordListBuilder to identify our facets
    var wlb = new WordListBuilder();
    wlb.OpenIndex(Server.MapPath("~/SearchIndex"));
    wlb.SetFilter(filter);

    // For each facet or field
    for (var facetCounter = 0; facetCounter < facetsToSearch.Length; facetCounter++)
    {

      // Identify the header for the facet
      var fieldValueCount = wlb.ListFieldValues(facetsToSearch[facetCounter], "", int.MaxValue);
      var thisPanelItem = facets.Items.FindItemByValue(facetsToSearch[facetCounter]);
      thisPanelItem.Items.Clear();

      // For each matching value in the field
      for (var fieldValueCounter = 0; fieldValueCounter < fieldValueCount; fieldValueCounter++)
      {

        string thisWord = wlb.GetNthWord(fieldValueCounter);

        if (string.IsNullOrEmpty(thisWord) || thisWord == "-") continue;

        var label = string.Format("{0}: ({1})", thisWord, wlb.GetNthWordCount(fieldValueCounter));

        var filterValue = string.Format("{0}={1}", facetsToSearch[facetCounter], thisWord);
        thisPanelItem.Items.Add(new RadPanelItem(label) { Value = filterValue });

      }

    }
列表 8 - 更新的 ExtractFacets 方法

您会看到这次,而不是在面板中编写标题和行项目,这段代码会找到分面标题并在该标题的内容区域内添加面板项。面板项的值定义为名称-值对,以便我可以在搜索方法中构建适当的过滤器条件。这些项目的 onclick 处理程序只是触发另一个搜索操作,并将 FacetFilter 设置为提交的面板项的值。

protected void facets_ItemClick(object sender, RadPanelBarEventArgs e)
{
  SearchResults = null;
  FacetFilter = e.Item.Value;
  grid.DataSource = DoSearch(searchBox.Text.Trim());
  grid.CurrentPageIndex = 0;
  grid.MasterTableView.VirtualItemCount = TotalHitCount;
  grid.DataBind();
}
列表 9 - 分面项点击操作

高亮显示结果,有格调!

您会注意到这次我没有在网格中返回带高亮显示的产品数据。我已经更新了我的页面,使用 Telerik TooltipManager 控件 以方便的悬停提示框的形式呈现该代码。该控件允许我定义一个服务器端方法,该方法将获取并返回在提示框中显示的适当 HTML。

提示框的标记由 ToolTipManager 的此语法表示

<telerik:RadToolTipManager runat="server" ID="tipMgr" OnAjaxUpdate="tipMgr_AjaxUpdate" RelativeTo="Element"
  width="400px" Height="150px" RenderInPageRoot="true">
</telerik:RadToolTipManager>
列表 10 - ToolTipManager 的标记

RenderInPageRoot 值表示用于容纳提示框的面板将渲染在页面根目录上,而不是在另一个元素内部。这对于此示例很有用,因为我们将显示相对于网格行的提示框。

我通过网格的 ItemDataBound 事件处理程序定义了 ToolTipManager 和网格之间的关系。我将网格行添加到提示管理器控件集合中,代码如下

protected void grid_ItemDataBound(object sender, GridItemEventArgs e)
{
  var thisRow = e.Item;
  if (thisRow.ItemType == GridItemType.Item || thisRow.ItemType == GridItemType.AlternatingItem)
  {
    var dataItem = thisRow.DataItem as ProductSearchResult;
    tipMgr.TargetControls.Add(thisRow.ClientID, dataItem.DocPosition.ToString(), true);
  }
}
列表 11 - 将 GridRows 添加到 ToolTipManager 管理的控件集合中

最后,为了让我们的高亮搜索结果出现在提示框中,我实现了 tipMgr_AjaxUpdate 事件处理程序,将高亮结果添加到提示框托管的请求 HTML 面板中。由于我在 上一篇文章 中将 HighlightResult 方法写成了静态的,所以我可以重用该方法来获取 HTML 并将其添加到 div 中

protected void tipMgr_AjaxUpdate(object sender, ToolTipUpdateEventArgs e)
{

  var newDiv = new HtmlGenericControl("div");
  newDiv.InnerHtml = FacetedSearch.HighlightResult(this.SearchResults, int.Parse(e.Value));
  e.UpdatePanel.ContentTemplateContainer.Controls.Add(newDiv);
}
列表 12 - 将高亮结果添加到提示框的语法

结果

提示框和添加的排序选项使得结果网格更加易于使用和友好。作为消费者,我希望能够搜索产品,然后按价格或其他对我重要的字段进行排序。Telerik 控件使得屏幕更易于阅读,并且结果更加突出,无需额外的编码或设计工作。

图 2 - 使用 Telerik UI for ASP.NET 的搜索结果

摘要

dtSearch 为企业级搜索库提供了丰富的 capacidades。结合 Telerik UI for ASP.NET 这样的顶级用户界面工具,只需少量集成工作即可实现令人惊叹的结果。将工作分配给搜索,并将您的企业数据呈现给 dtSearch。在 www.dtsearch.com 下载开发者试用版,并在 Telerik UI for ASP.NET 获取 Telerik UI for ASP.NET 试用版

更多关于 dtSearch
dtSearch.com
口袋里的搜索引擎 – 介绍 Android 上的 dtSearch
云端疾速源代码搜索
使用 Azure 文件、RemoteApp 和 dtSearch,从任何计算机或设备跨越 PB 级数据的各种数据类型进行安全即时搜索
使用 dtSearch 引擎进行 Windows Azure SQL 数据库开发
使用 dtSearch 进行分面搜索 - 不是普通的搜索过滤器
使用 dtSearch® ASP.NET Core WebDemo 示例应用程序极速提升您的搜索体验
在您的 Windows 10 通用 (UWP) 应用程序中嵌入搜索引擎
使用 dtSearch Engine DataSource API 索引 SharePoint 网站集
使用 dtSearch® ASP.NET Core WebDemo 示例应用程序
在 AWS 上使用 dtSearch(EC2 & EBS)
使用 dtSearch 和 AWS Aurora 进行全文搜索

© . All rights reserved.