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

使用 dtSearch 进行分面搜索 - 不是普通的搜索过滤器

emptyStarIconemptyStarIconemptyStarIconemptyStarIconemptyStarIcon

0/5 (0投票)

2014年4月8日

CPOL

8分钟阅读

viewsIcon

48209

downloadIcon

485

在本文中,我将向您展示如何使用 Entity Framework 数据集设置 dtSearch,然后使用分面搜索导航为结果集添加多个过滤器。

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

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

引言

我写过不少需要搜索的 Web 应用程序和网站。我最近接触了 dtSearch,并开始研究他们用于搜索索引和检索的库。在使用了一段时间后,我对它提供的功能范围印象深刻。特别是,一个我似乎从未正确实现过的功能,过滤搜索结果,他们通过其分面搜索功能完美解决了。在本文中,我将向您展示如何使用 Entity Framework 数据集设置 dtSearch,然后使用 分面搜索 导航为结果集添加多个过滤器。

使用 Entity Framework 构建搜索索引

在我的场景中,我有一个存储在 SQL Server 数据库中的大量棋盘游戏产品,我想对其进行索引。使用 dtSearch,您有多种选择来创建索引,包括一个选项,该选项将检查数据库并索引所有表中的内容。对于这个示例,我想索引一个已经配置了 Entity Framework 6 数据上下文的产品表。为了完成这项任务,我编写了自己的 dtSearch.Engine.DataSource 对象,名为 ProductDataSource,它将从我的数据库中提取产品并将其正确地呈现给 dtSearch IndexJob。DataSource 需要覆盖两个方法:Rewind 和 GetNextDoc。Rewind 在 IndexJob 启动时被调用,初始化连接并为 DataSource 准备工作。GetNextDoc 的作用顾名思义,它会在集合中前进到下一个项目,并使其可供 IndexJob 爬取和索引。

这是我的 ProductDataSource 类中的 Rewind 方法的样子

private GameShopEntities _GameShopContext = null;
private int _RecordNumber = 0;
public override bool Rewind()
{

  // Initialize a single EF context
  if (this._GameShopContext == null)
  {
    // New() is a static method that passes config string
    this._GameShopContext = GameShopEntities.New();
  }

  _RecordNumber = 0;
  this._TotalRecords = _GameShopContext.Products.Count();
  return true;

}
列表 1 - ProductDataSource 的 Rewind 方法

这相当普通。没什么太复杂的,只是创建了 Entity Framework 上下文并设置了总产品计数。更有趣的代码发生在 GetNextDoc 方法中。在此方法中,我设置了 DataSource 基对象的一些属性,以声明有关正在检查的当前产品的信息。此外,我还调用了另外两个方法来格式化附加字段以及我想要存储以表示此产品的文档。

public override bool GetNextDoc()
{

  // Exit now if we are at the end of the record set
  if (_TotalRecords <= _RecordNumber + 1) return false;

  // Reset Properties
  DocName = "";
  DocModifiedDate = DateTime.Now;
  DocCreatedDate = DateTime.Now;

  // Get the product from the data source
  _CurrentProduct = _GameShopContext.Products.Skip(_RecordNumber).First();

  FormatDocFields(_CurrentProduct);
  DocName = _CurrentProduct.ProductNo;
  DocModifiedDate = _CurrentProduct.LastUpdated;
  DocCreatedDate = _CurrentProduct.Created;

  DocStream = GetStreamForProduct(_CurrentProduct);

  _RecordNumber++;
  return true;

}
列表 2 - GetNextDoc 方法

DocName 属性本质上是此对象在搜索索引中的主键。我使用一个子方法 FormatDocFields 来将次要字段添加到集合中。

public static readonly string[] ProductFields = new[] {
  "Name", "Description", "Weight", "LongDesc", "Age", "NumPlayers", "Price", "Manufacturer"
};
private void FormatDocFields(Product product)
{

  DocFields = "";

  var sb = new StringBuilder();
  var format = "{0}\t{1}\t";
  sb.AppendFormat(format, "Name", product.Name ?? "");
  sb.AppendFormat(format, "Description", product.Description ?? "");
  sb.AppendFormat(format, "Weight", product.Weight.HasValue ? product.Weight.Value : 0);
  sb.AppendFormat(format, "LongDesc", product.LongDesc ?? "");
  sb.AppendFormat(format, "Age", product.Age ?? "");
  sb.AppendFormat(format, "NumPlayers", product.NumPlayers ?? "");
  sb.AppendFormat(format, "Price", product.Price.HasValue ? product.Price.Value: 0.00M);
  sb.AppendFormat(format, "Manufacturer", product.Manufacturer ?? "");

  DocFields = sb.ToString();

}
列表 3 - FormatDocFields 方法

此方法将选项卡分隔的键值对添加为 DocFields 集合,该集合将返回给 IndexJob。这不算坏,而且是一个简单的遵循方法,因为它会遍历 Product 对象的属性,添加我希望在搜索索引中的那些 Product 属性。

最后要分享的有趣之处是 GetStreamForProduct 方法。它获取产品并将其转换为 HTML 片段,适合在屏幕上显示搜索命中在返回字段中的位置。

private Stream GetStreamForProduct(Product product)
{

  var sb = new StringBuilder();
  sb.Append("<html><dl>");
  var ddFormat = "<dt>{0}</dt><dd>{1}</dd>";
  sb.AppendFormat(ddFormat, "Description", product.LongDesc);
  sb.AppendFormat(ddFormat, "Manufacturer", product.Manufacturer);
  sb.Append("</dl></html>");

  var ms = new MemoryStream();
  var sw = new StreamWriter(ms);
  sw.Write(sb.ToString());
  sw.Flush();

  return ms;

}
列表 4 – GetStreamForProduct 方法列表

这种语义标记够简单了吧?它是一个 HTML 定义列表,只有 description 和 manufacturer 字段在裸 HTML 标签内返回。有了 HTML 标签,dtSearch 索引器就会将片段识别为 HTML 文档。这将允许更优化的存储和作为 HTML 片段的表示。实际上,搜索应该只命中 description 和 manufacturer 字段。其余的是简单的 System.IO 流管理,用于以 dtSearch IndexJob 所需的流格式返回 HTML 片段。

索引我的数据并分配索引中的分面的最后一步是配置和运行 IndexJob

using (var indexJob = new IndexJob())
{
  var dataSource = new ProductDataSource();
  indexJob.DataSourceToIndex = dataSource;
  indexJob.IndexPath = _SearchIndexLocation;
  indexJob.ActionCreate = true;
  indexJob.ActionAdd = true;
  indexJob.CreateRelativePaths = false;

  // Create the faceted index
  indexJob.EnumerableFields = new StringCollection() { "Description","LongDesc", "Age", "NumPlayers", "Price", "Manufacturer" };

  var sc = new StringCollection();
  sc.AddRange(ProductDataSource.ProductFields);

  indexJob.StoredFields = sc;
  indexJob.IndexingFlags = IndexingFlags.dtsIndexCacheTextWithoutFields | IndexingFlags.dtsIndexCacheOriginalFile;

  ExecuteIndexJob(indexJob);
}
列表 5 - 使用 StoreFields 集合进行索引

StoredFields 属性是我声明用作搜索分面的字段的地方。

页面布局和搜索

在我的示例 ASP.NET Web Forms 项目中,我分配了一个面板和一个网格来显示搜索结果。我的标记如下所示:

<div>
    
  <asp:Label runat="server" ID="lSearch" AssociatedControlID="txtSearch" Text="Search Term:"></asp:Label>
  <asp:TextBox runat="server" ID="txtSearch"></asp:TextBox>
  <asp:Button runat="server" ID="bDoSearch" Text="Search" OnClick="bDoSearch_Click" />

</div>

<asp:Panel runat="server" ID="pFacets" Width="200" style="float: left;">
        
</asp:Panel>

<asp:GridView runat="server" ID="resultsGrid" OnPageIndexChanging="results_PageIndexChanging" AllowPaging="true" AllowCustomPaging="true" AutoGenerateColumns="false" ItemType=" FacetedSearch.ProductSearchResult" ShowHeader="false" BorderWidth="0">
  <PagerSettings Mode="NumericFirstLast" Position="TopAndBottom" />
  <Columns>
    <asp:TemplateField>
      <ItemTemplate>
        <a href='https://codeproject.org.cn/Products/<%#: Item.ProductNum  %>' class="productName"><%#: Item.Name %></a><br />
        <%#: Item.HighlightedResults %>

      </ItemTemplate>
    </asp:TemplateField> 
  </Columns>
</asp:GridView>
列表 6 - 分面搜索页面的标记

顶部有一个搜索文本框和按钮,左侧有一个面板用于列出分面,右侧有一个网格,该网格将虚拟分页以迭代搜索索引中的大量产品。搜索和绑定到网格是一项艰巨的工作,因为 dtSearch 工具提供了大量的配置选项。让我们看看那段代码。

public void DoSearch(int pageNum)
{

  // Configure and execute search
  var sj = new SearchJob();
  sj.IndexesToSearch.Add(_SearchIndexLocation);
  sj.MaxFilesToRetrieve = (pageNum+1) * PageSize;
  sj.WantResultsAsFilter = true;
  sj.Request = txtSearch.Text.Trim();

  // Add filter condition if necessary
  if (!string.IsNullOrEmpty(Request.QueryString["f"]))
  {
    sj.BooleanConditions = string.Format("{0} contains {1}", Request.QueryString["f"], Request.QueryString["t"]);
  }

  sj.AutoStopLimit = 1000;
  sj.TimeoutSeconds = 10;
  sj.Execute();

  ExtractFacets(sj);

  // Present results
  sj.Results.Sort(SortFlags.dtsSortByRelevanceScore, "Name");
  this._SearchResults = sj.Results;

  // Manual Paging
  var firstItem = PageSize * pageNum;
  var lastItem = firstItem + PageSize;
  lastItem = (lastItem > _SearchResults.Count) ? _SearchResults.Count : lastItem;
  var outList = new List<ProductSearchResult>();
  for (int i = firstItem; i < lastItem; i++)
  {
    _SearchResults.GetNthDoc(i);
    outList.Add(new ProductSearchResult
    {
      ProductNum = _SearchResults.DocName,
      Name = _SearchResults.get_DocDetailItem("Name"),
      HighlightedResults = new HtmlString(HighlightResult(i))
    });
  }

  // Configure and bind to the grid virtually, so we don't load everything
  resultsGrid.DataSource = outList;
  resultsGrid.PageIndex = pageNum;
  resultsGrid.VirtualItemCount = sj.FileCount;
  resultsGrid.DataBind();

}
列表 7 - 搜索方法

这里有很多东西要看,首先是 SearchJob 的初始配置。此配置指定索引在磁盘上的位置以及要检索多少搜索结果。搜索框的文本作为 SearchJob 对象的 Request 属性传入。接下来,应用一个看起来像另一个搜索条件的过滤器。这是基于 UI 中选定的分面的附加过滤器。稍后将详细介绍。SearchJob 配置中的重要部分是 WantResultsAsFilter 属性设置为 true。这允许将结果用作生成此搜索分点的代码的输入。

执行搜索后,将调用 ExtractFacets 方法从 SearchResults 中提取分面信息并将其格式化到屏幕上。最后,将 SearchResults 格式化并绑定到 GridView。有趣的是,在格式化结果时,有一个对 HighlightResult 的调用。我将在描述分面后描述该方法。

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(_SearchIndexLocation);
  wlb.SetFilter(filter);

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

    // Construct a header for the facet
    var fieldValueCount = wlb.ListFieldValues(facetsToSearch[facetCounter], "", int.MaxValue);
    var thisPanelItem = new HtmlGenericControl("div");
    var header = new HtmlGenericControl("h4");
    header.InnerText = facetsToSearch[facetCounter];
    thisPanelItem.Controls.Add(header);

    // For each matching value in the field
    for (var fieldValueCounter = 0; fieldValueCounter < fieldValueCount; fieldValueCounter++)
    {
      string thisWord = wlb.GetNthWord(fieldValueCounter);
      int thisWordCount = wlb.GetNthWordCount(fieldValueCounter);

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

      thisPanelItem.Controls.Add(new HtmlAnchor() { InnerText = string.Format("{0} ({1})", thisWord, thisWordCount) , HRef = "FacetedSearch.aspx?s=" + txtSearch.Text + "&f=" + facetsToSearch[facetCounter] + "&t=" + thisWord });
      thisPanelItem.Controls.Add(new HtmlGenericControl("br"));

    }

    pFacets.Controls.Add(thisPanelItem);

  }

}
列表 8 - ExtractFacets 方法用于格式化分面搜索条件

该方法首先配置一个 dtSearch.Engine.WordListBuilder 对象,该对象使用相同的搜索索引位置和上一次搜索的结果。接下来的几行将遍历 facetsToSearch 集合,并构建一个包含标题以及下方作为超链接找到的单词的 div。

HighlightResult 是要共享的最后一个方法。此方法使用 dtSearch.Engine.FileConverter 对象读取索引中存储的 HTML 片段,并使用 HTML SPAN 标签进行格式化,以突出显示从搜索文本框中找到的单词。

private string HighlightResult(int itemPos)
{

  using (FileConverter fc = new FileConverter())
  {

    fc.SetInputItem(_SearchResults, itemPos);
    fc.OutputFormat = OutputFormats.itAnsiitHTML;
    fc.OutputToString = true;
    fc.OutputStringMaxSize = 200000;
    fc.BeforeHit = "<span class='searchHit'>";
    fc.AfterHit = "</span>";

    fc.Flags = ConvertFlags.dtsConvertGetFromCache | ConvertFlags.dtsConvertInputIsHtml;
    fc.Execute();

    return fc.OutputString.Substring(0, fc.OutputString.IndexOf("</dl>")+5);

  }

}
列表 9 - HighlightResult 方法中将术语高亮显示应用于搜索结果

FileConverter 使用信息进行配置,以指示要呈现的索引中的哪个项目、所需的输出格式以及如何包装任何搜索命中的项目。传递 dtsConvertGetFromCache 标志,以指示 FileConverter 对象我想要我在 GetStreamForProduct 方法中早期存储的原始 HTML 片段。在我的页面上,我有一个页面上的 CSS 类 searchHit,它会更改字体颜色、添加下划线并设置黄色背景。

该方法的最后几行表明文档应从 Searchindex 缓存中获取,并且它已格式化为 HTML。我将剥离返回语句中 HTML DL 闭标签之后的任何额外内容。

结果

我的搜索页面在搜索类似 CHESS 的内容后如下所示:

图 1 – 搜索 Chess,左侧为分面,右侧为结果

此搜索过程是标准的,除了搜索命中的高亮显示外,我没有在结果中包含任何格式。凭借一点设计师的眼光,我可以让它看起来非常棒,让普通互联网用户乐于使用。

摘要

很快,我就能够启动 dtSearch 搜索实用程序并使用 Entity Framework 将其连接到我的 SQL 数据库。索引很容易构建,通过一点调整,我就能够提取字段信息并将其作为分面搜索选项呈现给用户。所有这些中最好的是,一旦索引完成,整个搜索操作都是在不触及我的数据库服务器的情况下发生的。对我的数据库管理员来说,这价值千金。

更多关于 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.