使用Elasticsearch 与 NEST 库 构建 .NET 企业级搜索(转)

最近几年出现的云计算为组织和用户带来了福音。组织对客户的了解达到前所未有的透彻,并能够采用个性化通信锁定客户。用户几乎可以随时随地获取其数据,使其更加易于访问和使用。为了存储所有这些数据,大型数据中心遍布全世界。但是,大数据同样也意味着大挑战。

John Naisbitt 在其所著书籍《大趋势:改变我们生活的十个新方向》(华纳书局,1982 年)中的著名引述:“我们淹没在数据中却信息匮乏”形象地描述了大数据市场的现状。公司能够存储千兆字节的数据,但要弄明白这些数据并使其可搜索却很难,尤其是因为大多数数据仓库在特定大数据存储内跨多个集合以非结构化方式 (NoSQL) 存储数据或着甚至在不同仓库之间以分布的形式存储数据。此外,数据格式也是各种各样,如 JSON 文档、Microsoft Office 文件等。搜索单个非结构化集合通常不是问题,但是若要查找用户完全不知道其可能所在位置的特小结果子集,在多个集合之间搜索所有非结构化数据会非常困难。这时,企业级搜索便可发挥作用了。

企业级搜索

下面是企业级搜索面临的基本挑战:拥有很多数据源的大型组织如何能够通过一个界面向内部和外部用户提供搜索所有公共公司数据源的功能?这个单一界面可能是一个 API、公司网站或甚至是一个在后台实现了自动完成功能的简单文本框。无论公司选择哪种界面,它必须能够让用户搜索其整个数据领域,这可能包括结构化和非结构化数据库、不同格式的 Intranet 文档、其他 API 和其他类型的数据源。

由于搜索多个数据集相当复杂,因此公认的企业级搜索解决方案只有几个 — 且标准太高。企业级搜索解决方案必须包括以下功能:

  • 内容感知:知道特定类型的数据可能位于的位置。

  • 实时索引:保持所有数据都有索引。

  • 内容处理:使不同的数据源都可以访问。

最受欢迎的企业级搜索解决方案之一就是开源 Elasticsearch (elasticsearch.org)。这个基于 Java 的服务器构建在 Apache Lucene (lucene.apache.org) 之上,其通过 JSON 支持和 REST Web 接口提供对多个数据源的可伸缩全文搜索,并具有高可用性、冲突管理和实时分析。请访问 bit.ly/1vzoUrR 查看它的完整功能集。

从较高层面来说,Elasticsearch 存储数据的方式非常简单。服务器内结构的最顶层元素称为索引,多个索引可以位于同一数据存储中。索引本身只是文档(一个或多个)容器,每个文档是一个或多个字段的集合(没有定义的结构)。每个索引都可以包含按称为类型的单位聚合的数据,用于表示特定索引内数据的逻辑组。

在将 Elasticsearch 视为类似来自关系数据库领域的表时,这可能很有用。表的行和列与索引的文档和字段之间存在着相同的关联,其中文档对应行,字段对应列。但是,通过 Elasticsearch,没有固定的数据结构或数据库架构。

如前所述,开发人员可以通过 REST Web 接口与 Elasticsearch 服务器通信。这意味着,他们只需通过从浏览器或任何其他类型的 Web 客户端发送 REST Web 请求即可查询索引、类型、数据或其他系统信息。以下是一些 GET 请求示例:

  • 查询所有索引:
    http://localhost:9200/_cat/indices/?v

  • 查询索引元数据:
    http://localhost:9200/clients/_stats

  • 查询所有索引数据:
    http://localhost:9200/clients/_search?q=*:*

  • 搜索索引内的特定字段值:
    http://localhost:9200/clients/_search?q=field:value

  • 获取索引映射类型内的所有数据:
    http://localhost:9200/clients/orders/_search?q=*:*

创建搜索

为了演示如何创建一个简单的多源解决方案,我将结合使用 Elasticsearch 1.3.4 与 JSON 文档、PDF 文档和 SQL Server 数据库。开始之前,我将简要介绍 Elasticsearch 安装,然后演示如何插入每个数据源以使数据可搜索。为简便起见,我将演示一个贴近现实的示例,其中使用的数据源来自知名的 Contoso 公司。

我将使用一个 SQL Server 2014 数据库与其中的多个表,尽管我只会使用一个 dbo.Orders。正如表名所示,其将存储有关公司客户订单的记录 — 大量的记录,然而易于管理:

CREATE TABLE [dbo].[Orders]
(
  [Id] [int] IDENTITY(1,1) NOT NULL primary key,
  [Date] [datetime] NOT NULL,
  [ProductName] [nvarchar](100) NOT NULL,
  [Amount] [int] NOT NULL,
  [UnitPrice] [money] NOT NULL
);

我还在网络共享中存放着多个以文件夹层次结构组织的公司文档。这些文档涉及公司在过去组织的不同产品营销活动并且存储格式多种多样,其中包括 PDF 和 Microsoft Office Word。文档平均大小约为 1 MB。

最后,我有一个用于以 JSON 格式公开公司客户端信息的内部 API;因为我知晓响应的结构,所以我能够轻松地将其反序列化到客户端类型的对象。我的目标是使所有数据源可使用 Elasticsearch 引擎进行搜索。此外,我想在底层创建一个基于 Web API 2 的 Web 服务,以通过对 Elasticsearch 服务器进行一次调用就可在所有索引之间执行企业级查询。(若要详细了解 Web API 2,请访问 bit.ly/1ae6uya。)该 Web 服务将向最终用户返回包含潜在提示的建议列表;此列表之后可供嵌入在 ASP.NET MVC 应用程序中的自动完成控件使用,或供任何其他类型的 Web 站点使用。

设置

我需要做的第一件事就是安装 Elasticsearch 服务器。对于 Windows,您可以通过自动方式或手动方式进行安装,结果一样 — 托管 Elasticsearch 服务器的正在运行的 Windows 服务。自动安装的过程非常快速而简单,您需要做的就是下载并运行 Elasticsearch MSI 安装程序 (bit.ly/12RkHDz)。遗憾的是,没有选择 Java 版本或者更为重要的 Elasticsearch 版本的方法。相反,手动安装过程稍微有点费劲,但您对组件会有更多的控制权,因此这种方法更适合目前的情况。

将 Elasticsearch 手动设置为 Windows 服务需要执行以下步骤:

  1. 下载并安装最新的 Java SE 运行时环境 (bit.ly/1m1oKlp)。

  2. 添加名为 JAVA_HOME 的环境变量。它的值将是您在其中安装 Java 的文件夹路径(例如,C:\Program Files\Java\jre7),如图 1 中所示。

  3. 下载 Elasticsearch 文件 (bit.ly/1upadla) 并将其解压缩。

  4. 将解压缩的源移动到 Program Files| Elasticsearch(可选)。

  5. 以管理员身份运行命令提示符并使用 install 参数执行 service.bat:
    C:\Program Files\Elasticsearch\elasticsearch-1.3.4\bin>service.bat install


图 1 设置 Java_Home 环境变量

如此,Windows 服务启动并运行,Elasticsearch 服务器可通过端口 9200 在本地主机上进行访问。现在我可以通过任何 Web 浏览器向 URL http://localhost:9200/ 发出 Web 请求并将获得如下所示的响应:

{
  "status" : 200,
  "name" : "Washout",
  "version" : {
    "number" : "1.3.4",
    "build_hash" : "a70f3ccb52200f8f2c87e9c370c6597448eb3e45",
    "build_timestamp" : "2014-09-30T09:07:17Z",
    "build_snapshot" : false,
    "lucene_version" : "4.9"
  },
  "tagline" : "You Know, for Search"
}

现在,我的本地 Elasticsearch 实例已准备就绪。但是,原始版本不允许我连接到 SQL Server 或通过数据文件运行全文搜索。若要实现上述功能,我还必须安装多个插件。

扩展 Elasticsearch

正如我之前所述,Elasticsearch 的原始版本不允许索引外部数据源,如 SQL Server、Office 甚至 PDF。若要使所有这些数据源可搜索,我需要安装几个插件,这非常简单。

我的第一个目标支持对附件进行全文搜索。这里的附件,指的是作为 JSON 文档已上载至 Elasticsearch 数据存储的源文件的 base64 编码表示。(请参阅 bit.ly/12RGmvg 了解有关附件类型的信息。)我需要用于实现此目的的插件是适用于 Elasticsearch 版本 2.3.2 的 Mapper Attachments 类型(可在 bit.ly/1Alj8sy 获得)。这是一个 Elasticsearch 扩展,支持对文档进行全文搜索,其基于 Apache Tika 项目 (tika.apache.org),该项目从各种类型的文档检测并提取元数据和文本内容,并为 bit.ly/1qEyVmr 列出的文件格式提供支持。

与大多数适用于 Elasticsearch 的插件一样,此安装非常简单,我需要做的就是以管理员身份运行命令提示符并执行以下命令:

bin>plugin --install elasticsearch/elasticsearch-mapper-attachments/2.3.2

在下载并提取了该插件之后,我需要重启 Elasticsearch Windows 服务。

完成后,我需要配置 SQL Server 支持。当然,这里也有一个插件可用于执行此操作。它的名称为 JDBC River (bit.ly/12CK8Zu),允许从 JDBC 源(如 SQL Server)提取数据以在 Elasticsearch 中建立索引。尽管安装过程分三个阶段完成,但该插件的安装和配置并不难。

  1. 首先,安装 Microsoft JDBC Driver 4.0,这是一个适用于 SQL Server 的基于 Java 的数据提供程序(可从 bit.ly/1maiM2j 进行下载)。需要记住的重要一点是,我需要将下载的文件的内容提取到名为 Microsoft JDBC Driver 4.0 for SQL Server 的文件夹中(若不存在,则需创建),该文件夹直接位于 Program Files 文件夹下,因此生成的路径如下所示:C:\Program Files\Microsoft JDBC Driver 4.0 for SQL Server。

  2. 接下来,使用以下命令安装该插件:
    bin> plugin --install
    jdbc --url "http://xbib.org/repository/org/xbib/elasticsearch/plugin/elasticsearch-river-jdbc/1.3.4.4/elasticsearch-river-jdbc-1.3.4.4-plugin.zip"

  3. 最后,将第一步中提取的 SQLJDBC4.jar 文件 (C:\Program Files\Microsoft JDBC DRIVER 4.0 for SQL Server\sqljdbc_4.0\enu\SQLJDBC4.jar) 复制到 Elasticsearch 目录中的 lib 文件夹 (C:\Program Files\Elasticsearch\lib)。当这些完成后,我需要记得重新启动该 Windows 服务。

现在,所有必需插件均已安装。然而,若要验证安装是否正确完成,可将下命令作为 HTTP GET 请求发出:

http://localhost:9200/_nodes/_all/plugins

在响应中,我希望看到两个已安装插件都有列出,如图 2 中所示。


图 2 已安装的插件

设置 SQL Server

若要结合使用 JDBC River 与 SQL Server,SQL Server 实例需要可通过 TCP/IP 进行访问,默认情况下,此功能禁用。但是,启用也很简单,需要做的就是打开 SQL Server 配置管理器,并在 SQL Server 网络配置下,对于要连接到的 SQL Server 实例,将“状态”值更改为“针对 TCP/IP 协议启用”,如图 3 中所示。执行完此操作后,我应该能够凭借在“服务器名称”中使用本地主机 1433(端口 1433 是通过 TCP/IP 访问 SQL Server 的默认端口)通过 Management Studio 登录我的 SQL Server 实例。


图 3 为 SQL Server 实例启用 TCP/IP

使源可搜索

所有必需插件均已安装,现在开始加载数据。正如之前所述,我有三个不同的数据源(JSON 文档、文件和来自 SQL Server 数据库的订单表),并希望在 Elasticsearch 上进行索引。我可以通过许多不同的方法索引这些数据源,但我想展示如何使用我已实现的基于 .NET 的应用程序轻松实现此目的。因此,作为索引的前提条件,我需要为我的项目安装一个名为 NEST 的外部库 (bit.ly/1vZjtCf),这只是一个封装 Elasticsearch Web 接口的托管包装。因为此库通过 NuGet 提供,所以将其作为我的项目的一部分就像在程序包管理器控制台中执行一个命令那样简单:

PM> Install-Package NEST

现在,我的解决方案中有了 NEST 库,我可以创建一个名为 ElasticSearchRepository 的新类库项目。之所以采用此名称,是因为我决定将来自 ElasticClient 类(这是 NEST 库的一部分)的所有函数调用与该解决方案的其余部分分开。如此,项目会变得类似于基于实体框架的应用程序中广泛应用的存储库设计模式,因此其应易于理解。此外,在此项目中,只有三个类:BaseRepository 类,该类将初始化并公开继承类和 ElasticClient 的实例;其他两个存储库类是:

  • IndexRepository — 用于操作索引、设置映射并上载文档的读/写类。

  • DiscoveryRepository — 将在基于 API 的搜索操作期间使用的只读类。

图 4 显示了 BaseRepository 类的结构,其中包含了 ElasticClient 类型的受保护属性。此类型(作为 NEST 库的一部分提供)会集中 Elasticsearch 服务器和客户端应用程序之间的通信。若要为其创建实例,可以将 URL 传递到 Elasticsearch 服务器,作为可选类构造函数参数进行传递。如果该参数为 null,则将使用默认值 http://localhost:9200。

图 4 BaseRepository 类

namespace ElasticSearchRepository
{
  using System;
  using Nest;
  public class BaseRepository
  {
    protected ElasticClient client;
    public BaseRepository(Uri elastiSearchServerUrl = null)
    {
      this.client = elastiSearchServerUrl != null ?
        new ElasticClient(new ConnectionSettings(elastiSearchServerUrl)) :
        : new ElasticClient();
    }
  }
}

客户端准备就绪后,首先我将对客户端数据进行索引;这是最简单的情形,因为无需其他插件:

public class Client
{
  public int Id { get; set; }
  public string Name { get; set; }
  public string Surname { get; set; }
  public string Email { get; set; }
}

若要索引此类型的数据,我会调用 Elasticsearch 客户端的实例,以及索引 函数,其中 T 是我的客户端类的类型,表示从 API 返回的序列化数据。此泛型函数采用三个参数:T 对象类的实例、目标索引名以及索引内的映射名:

public bool IndexData(T data, string indexName =
  null, string mappingType = null)
  where T : class, new()
  {
    if (client == null)
    {
      throw new ArgumentNullException("data");
    }
    var result = this.client.Index(data,
      c => c.Index(indexName).Type(mappingType));
    return result.IsValid;
  }

最后两个参数可选,因为 NEST 将应用其默认逻辑,基于泛型类型创建目标索引名。

现在,我想要索引与公司产品相关的营销文档。因为这些文件存储在网络共享中,因此我可以将有关每个特定文档的信息封装到一个简单的 MarketingDocument 类中。值得注意的是,如果我要在 Elasticsearch 中索引某个文档,则需要将其作为 Base64 编码的字符串进行上载:

public class MarketingDocument
{
  public int Id { get; set; }
  public string Title { get; set; }
  public string ProductName { get; set; }
  // Base64-encoded file content.
  public string Document { get; set; }
}

该类准备就绪,因此我可以使用 ElasticClient 将 MarketingDocument 类中的特定字段标记为附件。为了实现此目的,我可以创建一个名为“products”的新索引并向其中添加新的营销映射(为简单起见,类名称将是映射名称):

private void CreateMarketingIndex()
  {
    client.CreateIndex("products", c =>
      c.AddMapping
      (m => m.Properties(ps =>ps.Attachment(a =>
            a.Name(o =>o.Document).TitleField(t =>
            t.Name(x => x.Name)
            TermVector(TermVectorOption.WithPositionsOffsets)
        )))));
  }

现在,.NET 类型、营销数据的映射以及附件的定义全部具备了,我可以采用与索引客户端数据相同的方法开始索引文件了:

var documents = GetMarketingDocumentsMock();
documents.ForEach((document) =>
{
  indexRepository.IndexData(document, "marketing");
});

最后一步是在 Elasticsearch 上设置 JDBC River。遗憾的是,NEST 尚不支持 JDBC River。从理论上讲,我可以通过使用 Raw 函数通过 JSON 发送原始请求以创建 JDBC River 映射,但我不想事情过分复杂化。因此,若要完成映射创建过程,我要指定以下参数:

  • 到 SQL Server 数据库的连接字符串

  • 用于查询数据的 SQL 查询

  • 更新计划

  • 目标索引名和类型(可选)

(您可在 bit.ly/12CK8Zu 查找可配置参数的完整列表。)

若要创建新的 JDBC River 映射,我需要将其中指定了请求主体的 PUT 请求发送到以下 URL:

http://localhost:9200/_river/{river_name}/_meta

图 5 的示例中,我放置了一个请求正文以创建新的 JDBC River 映射,这将连接到在本地 SQL Server 实例(可通过 TCP/IP 在端口 1433 上访问)上托管的 Contoso 数据库。

图 5 HTTP PUT 请求以创建新的 JDBC River 映射

PUT http://localhost:9200/_river/orders_river/_meta
{
"type":"jdbc",
"jdbc":
  {
  "driver": "com.microsoft.sqlserver.jdbc.SQLServerDriver",
  "url":"jdbc:sqlserver://127.0.0.1:1433;databaseName=Contoso",
  "user":"elastic",
  "password":"asd",
  "sql":"SELECT *  FROM dbo.Orders",
  "index" : "clients",
  "type" : "orders",
  "schedule": "0/30 0-59 0-23 ? * *"
  }
}

其使用登录名“elastic”和密码“asd”对用户进行身份验证并执行以下 SQL 命令:

SELECT * FROM dbo.Orders

此 SQL 查询返回数据的每一行都将在订单映射类型中的客户端索引下进行索引,该索引将每隔 30 秒进行一次(以 Cron 表示法表示,请参阅 bit.ly/1hCcmnN 了解详细信息)。

此过程完成后,您应该在 Elasticsearch 日志文件 (/logs/ elasticsearch.log) 中看到类似如下所示的信息:

[2014-10-2418:39:52,190][INFO][river.jdbc.RiverMetrics]
pipeline org.xbib.elasticsearch.plugin.jdbc.RiverPipeline@70f0a80d
complete: river jdbc/orders_river metrics: 34553 rows, 6.229481683638776 mean, 
  (0.0 0.0 0.0), ingest metrics: elapsed 2 seconds, 
  364432.0 bytes bytes, 1438.0 bytes avg, 0.1 MB/s

如果因 River 配置而导致某些事情不对劲,此日志中也会列出错误消息。

搜索数据

一旦所有数据都在 Elasticsearch 引擎中进行了索引,我就可以开始查询了。当然,我可以向 Elasticsearch 服务器发送简单请求以同时查询一个或多个索引和类型映射,但我想要构建更贴近现实情形的比较有用的方案。因此,我要将我的项目拆分为三个不同的组件。第一个组件,我已经介绍了,是 Elasticsearch,可通过 http://localhost:9200/ 获取。第二个组件是我要使用 Web API 2 技术构建的 API。最后一个组件是一个控制台应用程序,将用于在 Elasticsearch 上设置我的索引以及向其中填充数据。

若要创建新的 Web API 2 项目,首先需要创建一个空的 ASP.NET Web 应用程序项目,然后从程序包管理器控制台中,运行以下安装命令:

Install-Package Microsoft.AspNet.WebApi

创建项目后,接下来是添加新的控制器,以用于处理来自客户端的查询请求并将其传递给 Elasticsearch。添加名为 DiscoveryController 的新控制器只会涉及到新项 Web API ApiController 类 (v2.1) 的添加。并且,我需要实现一个搜索函数,其将通过以下 URL 公开:http://website/api/discovery/search?searchTerm=user_input:

[RoutePrefix("api/discovery")]
public class DiscoveryController : ApiController
{
  [HttpGet]
  [ActionName("search")]
  public IHttpActionResult Search(string searchTerm)
  {
    var discoveryRepository = new DiscoveryRepository();
    var result = discoveryRepository.Search(searchTerm);
    return this.Ok(result);
  }
}

如果 Web API 2 引擎由于自引用循环而无法序列化响应,您必须将以下内容添加到 WebApiConfig.cs 文件中(位于 AppStart 文件夹中):

GlobalConfiguration.Configuration
.Formatters
.JsonFormatter
.SerializerSettings
.ReferenceLoopHandling =
      ReferenceLoopHandling.Ignore;

图 6 中所示,在我创建的控制器的主体中,我实例化了 DiscoveryRepository 类型的类,这只是一个来自 NEST 库的封装 ElasticClient 类型的包装。在此非泛型的只读储库内,我实现了两种类型的搜索函数并且二者都会返回动态类型。这部分很重要,因为通过在这两个函数体中执行此操作,我的查询不会被限制为一个索引;而是可以同时查询所有索引和所有类型。这意味着我的结果将具有不同的结构(不同的类型)。这两个函数的唯一区别是查询方法。在第一个函数中,我只使用 QueryString 方法 (bit.ly/1mQEEg7),这是一个精确匹配搜索,而在第二个函数中,我使用的是 Fuzzy 方法 (bit.ly/1uCk7Ba),其将在索引之间执行模糊搜索。

图 6 两种搜索类型的实现

namespace ElasticSearchRepository
{
  using System;
  using System.Collections.Generic;
  using System.Linq;
  public class DiscoveryRepository : BaseRepository
  {
    public DiscoveryRepository(Uri elastiSearchServerUrl = null)
      : base(elastiSearchServerUrl)
    {
    }
    ///  
    public List<tuple<string, string="">> SearchAll(string queryTerm)
    {
      var queryResult=this.client.Search(d =>
        d.AllIndices()
        .AllTypes()
        .QueryString(queryTerm));
      return queryResult
        .Hits
        .Select(c => new Tuple<string, string="">(
          c.Indexc.Source.Name.Value))
        .Distinct()
        .ToList();
     }
     ///  
     public dynamic FuzzySearch(string queryTerm)
     {
       return this.client.Search(d =>
         d.AllIndices()
         .AllTypes()
         .Query(q => q.Fuzzy(f =>
           f.Value(queryTerm))));
     }
  }
}

现在,我的 API 准备就绪,我可以运行并开始测试了,只需将 GET 请求发送到 http://website:port/api/discovery/search?searchTerm=user_input,并将用户输入作为 searchTerm 查询参数的值进行传递即可。因此,图 7 显示了我的 API 针对搜索词“scrum”生成的结果。如屏幕快照中的突出显示所示,搜索函数对数据存储中的所有索引执行了查询,并同时从多个索引返回符合条件的结果。


图 7 搜索词“scrum”的 API 搜索结果

通过实现该 API 层,我创造了实现多个客户端(如网站或移动应用)的可能性,这些客户端将能够使用该层。这使得最终用户能够使用企业级搜索功能。您可以在我的博客上找到针对基于 ASP.NET MVC 4 的 Web 客户端实现自动完成控件示例 (bit.ly/1yThHiZ)。

总结

大数据为技术市场带来许多机遇和挑战。挑战之一(这可能也是一个难得的机遇)是在数千兆字节的数据中实现快速搜索,而无需知晓所需数据在数据领域中的准确位置。在本文中,我介绍了如何实现企业级搜索,并演示了如何结合 Elasticsearch 与 NEST 库在 .NET Framework 中实现此目标。

posted @ 2018-08-10 22:08  micwin  阅读(576)  评论(0编辑  收藏  举报