作者都是各自领域经过审查的专家,并撰写他们有经验的主题. 我们所有的内容都经过同行评审,并由同一领域的Toptal专家验证.
伊万·塞萨尔的头像

伊凡塞萨尔

Ivan在各种规模的项目上工作了十年,主要是使用 .网络技术. 他还喜欢参加算法竞赛.

工作经验

14

分享

应该一个 .网络开发人员 在项目中使用Elasticsearch? 尽管Elasticsearch是基于Java构建的, 我相信它提供了很多理由,为什么Elasticsearch值得为任何项目的全文搜索一试.

Elasticsearch作为一项技术,在过去的几年里已经取得了长足的进步. 它不仅使全文搜索感觉很神奇, 它还提供其他复杂的功能, 例如文本自动补全, 聚合管道, 和更多的.

如果考虑将基于java的服务引入到您的整洁中 .. 网。生态系统让你不舒服, 那就别担心, 就像安装和配置Elasticsearch之后一样, 你将花大部分时间和最酷的人在一起 .. 网。包: .

在本文中,您将学习如何使用amazing 搜索引擎 解决方案,Elasticsearch,在您的 .网项目.

安装与配置

安装 Elasticsearch 它本身的开发环境归结为下载Elasticsearch和,可选的, Kibana.

解压缩后,像这样的蝙蝠。文件就会派上用场了:

D: \弹性\ elasticsearch-5 cd”.2.2 \ bin”
开始elasticsearch.蝙蝠。
 
D: \弹性\ kibana-5 cd”.0.0-windows-x86 \ bin”
开始kibana.蝙蝠。
 
退出

启动两个服务之后, 您可以随时查看本地Kibana服务器(通常在http://localhost:5601上可用)。, 尝试索引和类型, 使用纯JSON进行搜索, 如前所述 在这里.

第一步

做一个彻底的 优秀的开发者, 在管理层的全面支持和理解下, 首先添加一个单元测试项目,并编写一个至少有90%代码覆盖率的搜索Service.

第一步是明确地配置 应用程序.配置 文件为Elasticsearch服务器提供一个连接类型字符串.

Elasticsearch最酷的一点是它是完全免费的. 但是,我仍然建议使用Elastic提供的弹性云服务.co. 托管服务使所有的维护和配置相当容易. 更, 你有两周的免费试用, 这应该足以测试这里所有的例子!

由于这里我们是在本地运行,所以像这样的配置键应该做:


默认情况下,Elasticsearch安装运行在端口9200上,但您可以根据需要更改它.

ElasticClient和巢包

ElasticClient是一个很好的小东西,它将为我们做大部分的工作, 它和巢包一起提供.

让我们先安装这个包.

要配置客户端,可以使用如下命令:

var节点=新的Uri(配置管理器.AppSettings["搜索uri "]);
var设置=新连接设置(节点);
设置.ThrowExceptions(alwaysThrow: true); // I like exceptions
设置.PrettyJson(); // Good for DEBUG
var client = new ElasticClient(设置);

索引和映射

为了能够搜索一些东西,我们必须将一些数据存储到ES中. 使用的术语是“索引”.”

术语“映射”用于将数据库中的数据映射到将被序列化并存储在Elasticsearch中的对象. 我们将在本教程中使用实体框架(EF).

一般, 使用Elasticsearch时, 您可能正在寻找一个站点范围的搜索引擎解决方案. 您将使用某种形式的饲料或消化, 或者像谷歌一样的搜索,从不同的实体返回所有的结果, 比如用户, 博客条目, 产品, 类别, 事件, 等.

这些可能不仅仅是数据库中的一个表或实体, 而是, 您可能希望聚合不同的数据,并可能提取或派生一些常见属性,如title, 描述, 日期, 作者/所有者, 照片等等. 另一件事是, 您可能不会在一个查询中完成它, 但是如果你使用的是ORM, 您必须为每个博客条目编写一个单独的查询, 用户, 产品, 类别, 事件, 或者别的什么.

我通过为每个“大”类型创建索引来组织我的项目.g.,博客文章或产品. 然后可以添加一些Elasticsearch类型,用于更特定的类型,这些类型将属于同一索引. 例如, 如果一篇文章可以成为一个故事, 视频的文章, 或播客, 它仍然会在“文章”索引中, 但是我们会在索引中有这四种类型. 但是,它仍然可能是数据库中的相同查询.

请记住,每个索引至少需要一个类型——可能是与索引同名的类型.

要映射实体,需要创建一些额外的类. 我通常使用 Document搜索ItemBase 类,每个专门化类将从中继承 博客Post搜索Item, 产品搜索Item等等.

我喜欢在这些类中使用映射器表达式. 如果需要的话,我可以随时修改表达式.

在我最早的一个 项目与Elasticsearch, 我编写了一个相当大的搜索Service类,其中包含映射和索引,并使用漂亮而冗长的切换语句:对于我想要放入Elasticsearch的每个实体类型, 有一个带有映射的开关和查询可以做到这一点.

然而,在整个过程中,我了解到这不是最好的方式,至少对我来说不是.

一个更优雅的解决方案是拥有某种智能 IndexDefinition 类和每个索引的特定索引定义类. 这边是我的底边 IndexDefinition 类可以存储所有可用索引的列表和一些辅助方法(如required) 分析程序 和状态报告,而派生特定于索引 处理查询数据库和映射每个索引的数据. 这非常有用,特别是当您稍后必须向ES添加额外的实体时. 归根结底就是再加一个 SomeIndexDefinition 继承的类 IndexDefinition 需要实现一些方法来查询索引中需要的数据.

Elasticsearch演讲

使用Elasticsearch可以做的所有事情的核心是它的查询语言. 在理想的情况下, 你只需要知道如何构造一个查询对象就可以与Elasticsearch进行通信.

在幕后,Elasticsearch通过HTTP将其功能作为基于json的API公开.

尽管API本身和查询对象的结构相当直观, 处理许多现实生活中的场景仍然是一件麻烦事.

通常,对Elasticsearch的搜索请求需要以下信息:

  • 搜索哪个索引和哪些类型

  • 分页信息(要跳过多少项,要返回多少项)

  • 具体的类型选择(在执行聚合时,就像我们在这里要做的那样)

  • 查询本身

  • 高亮定义(如果我们想,Elasticsearch可以自动高亮点击)

例如, 您可能希望实现一个搜索功能,只有部分用户可以看到您网站上的优质内容, 或者您可能希望某些内容仅对其作者的“朋友”可见等等.

能够构造查询对象是解决这些问题的核心, 当试图涵盖很多场景时,这真的是一个问题.

从以上所有, 最重要也是最难设置的是, 自然, 查询段——这里, 我们将主要关注这一点.

查询是组合的递归构造 Bool查询。 以及其他查询,例如 MatchPhrase查询。, Terms查询。, DateRange查询。, Exists查询。. 这些足以满足任何基本要求,应该是一个好的开始.

A MultiMatch 查询非常重要,因为它使我们能够指定我们想要进行搜索的字段,并对结果进行更多的调整——我们将在后面返回.

A MatchPhrase查询。 可以通过传统SQL数据库中的外键或静态值(例如enums)来过滤结果吗, 当按特定作者(AuthorId),或与所有公共物品(ContentPrivacy =公共).

Terms查询。 将被翻译为“in”到传统的SQL语言. 例如, 它可以退回一个用户的朋友写的所有文章,或者从一组固定的商家独家获得产品. 和SQL一样, 一个人不应该过度使用这个,放10个,因为这会对性能产生影响, 但它通常能很好地处理合理的数量.

DateRange查询。 是自我记录.

Exists查询。 是一个有趣的特性:它使您能够忽略或返回没有特定字段的文档.

这些,当与 Bool查询。,允许您定义复杂的过滤逻辑.

例如,考虑一个博客站点,其中博客文章可以有一个 AvailableFrom 字段,该字段表示它们何时应该变得可见.

如果我们使用一个过滤器 AvailableFrom <= 现在, 然后,我们将不会获得根本没有该特定字段的文档(我们聚合数据), 有些文档可能没有定义这个字段). 为了解决这个问题,你们可以结合起来 Exists查询。DateRange查询。 把它包起来 Bool查询。 条件是至少有一个元素 Bool查询。 是实现. 像这样:

Bool查询。
    应该(至少满足下列条件之一)
        带AvailableFrom条件的日期rangquery
        对字段AvailableFrom的查询是否定的

否定查询并不是这么简单的开箱即用的工作. 但是在…的帮助下 Bool查询。,但仍有可能:

Bool查询。
    MustNot
        Exists查询。

自动化和测试

为了使事情更简单,推荐的方法绝对是编写 测试 随你的便.

这种方式, 您将能够更有效地进行实验,甚至更重要的是,您将确保引入的任何新更改(如更复杂的过滤器)都不会破坏现有功能. 我明确地不想说“单元测试”,“因为我不喜欢模仿Elasticsearch引擎之类的东西——模仿几乎永远不会是ES真正行为的现实逼近——因此, 这可能是集成测试, 如果你是术语迷.

实际的例子

所有的基础工作都通过索引完成了, 映射, 和过滤, 现在我们准备好进行最有趣的部分:调整搜索参数以产生更好的结果.

在我上一个项目中, 我使用Elasticsearch来提供一个用户提要:所有的内容聚合到一个地方,按创建日期排序,并使用一些选项进行全文搜索. The feed itself is quite straightforward; just ensure that t在这里 is a 日期 field somew在这里 in your data and order by that field.

另一方面,搜索并不能很好地打开盒子. 这是因为,Elasticsearch自然无法知道数据中重要的东西是什么. 假设我们有一些数据(在其他字段中)具有 Title, 标签 (数组) Body 字段. body字段可以是HTML内容(让事情更现实一点).

拼写错误

的要求: 即使出现拼写错误或单词结尾不同,我们的搜索也应该返回结果. 例如, 如果有一篇名为“用木勺可以做的伟大事情”的文章,,当我搜索“东西”或“木头”时,“我还是想找到一个匹配的.

为了解决这个问题,我们必须熟悉 分析程序, 分词器, 字符过滤器, 令牌过滤器. 这些是在索引时应用的转换.

  • 需要定义分析程序. 这可以为每个索引定义.

  • 分析器可以应用于文档中的某些字段. 这可以使用属性或流畅的API来完成. 在我们的示例中,我们使用属性.

  • 分析器是过滤器、字符过滤器和标记器的组合.

满足要求(部分单词匹配), 我们将创建“自动完成”分析器, 它包括:

  • 一个英语停止词过滤器:过滤器删除所有常用词在英语, 例如“and”或“the”.”

  • 修剪过滤器:去除每个标记周围的空白

  • 小写过滤器:将所有字符转换为小写. 这并不意味着当我们获取数据时, 它将被转换为小写, 而是支持大小写不变搜索.

  • Edge-n-gram标记器:这个标记器允许我们进行部分匹配. 例如, 如果我们有一个句子“我奶奶有一把木椅,寻找“木材”时,我们仍然希望能找到这句话的答案. edge-n-gram做了什么, 是商店吗?,”“木,”“woode,和“wooden”,以便找到任何至少有三个字母匹配的部分单词. 参数MinGram和MaxGram定义要存储的最小和最大字符数. 在我们的情况下,我们将有最少三个,最多15个字母.

在下面的部分中,所有这些都被绑定在一起:

分析.Analyzers(a => a
	.Custom("autocomplete", cc => cc
		.过滤器("eng_stopwords", "trim", "lowercase")
		.记号赋予器(“自动完成”)
	)
	.Tokenizers(tdesc => tdesc
		.EdgeNGram("autocomplete", e => e
			.MinGram (3)
			.MaxGram (15)
			.TokenChars (TokenChar.信,TokenChar.数字)
		)
	)
	.TokenFilters(f => f
		.Stop("eng_stopwords", lang => lang
			.StopWords(“_english_”)
		)
	);

并且,当我们想要使用这个分析器时,我们应该像这样注释我们想要的字段:

公共类搜索ItemDocumentBase
{
	...

	[Text(Analyzer = "autocomplete", Name = Name of(Title))]
	public string Title { get; set; }
	
	...
}

现在, 让我们看几个示例,这些示例展示了几乎所有具有大量内容的应用程序中的常见需求.

清理HTML

的要求: 我们的一些字段可能包含HTML文本.

自然, you wouldn’t want searching for “section” to return something like “

” or “body” returning the HTML element “.“为了避免这种情况,在索引过程中,我们将剥离HTML,只留下内容.

幸运的是,你不是第一个有这个问题的人. Elasticsearch提供了一个有用的字符过滤器:

分析.Analyzers(a => a
	.Custom("html_stripper", cc => cc
		.过滤器("eng_stopwords", "trim", "lowercase")
		.CharFilters(“html_strip”)
		.记号赋予器(“自动完成”)
	)

并应用它:

[Text(Analyzer = "html_stripper", Name = nameof(HtmlText))]
public string HtmlText { get; set; }

重要的领域

的要求: 标题中的匹配应该比内容中的匹配更重要.

幸运的是, 如果匹配发生在一个或另一个字段中,Elasticsearch提供了提升结果的策略. 方法在搜索查询构造中完成此操作 提高 选择:

int titleBoost = 15;

.查询。(qx => qx.MultiMatch(m => m
	.查询(searchRequest.查询。.低())
	.Fields(ff => ff
		.Field(f => f.Title, 提高: titleBoost)
		.Field(f => f.总结)
		...
	)
	.类型(Text查询。Type.BestFields)
) && filtering查询。)

如你所见, MultiMatch 查询在这种情况下非常有用,而且这种情况并不少见! 经常, 有些领域更重要,有些不重要,这个机制使我们能够考虑到这一点.

立即设置升压值并不总是那么容易. 你需要稍微摆弄一下,才能得到想要的结果.

优先级的文章

的要求: 有些文章比其他文章更重要. 要么作者更重要,要么文章本身有更多的赞/分享/赞/等. 更重要的文章应该排名更高.

Elasticsearch允许我们实现评分函数, 我们将其简化为定义一个字段“重要性”,,在我们的例子中是双值, 大于1. 您可以定义自己的重要性函数/因子,并类似地应用它. 您可以定义多种提升和评分模式,选择最适合您的模式. 这条很适合我们:

.查询。(q => q
	.FunctionScore(fsc => fsc
		.BoostMode (FunctionBoostMode.用)
		.ScoreMode (FunctionScoreMode.总和)
		.Functions(f => f
			.FieldValueFactor(b => b
				.字段(nameof (搜索ItemDocumentBase.评级)
				.失踪(0.7)
				.修饰符(FieldValueFactorModifier.没有一个)
			)
		)
		.查询。(qx => qx.MultiMatch(m => m
			.查询(searchRequest.查询。.低())
			.Fields(ff => ff
				...
			)
			.类型(Text查询。Type.BestFields)
		) && filtering查询。)
	)
)

每部电影都有评分, 我们通过演员出演的电影的平均评分来推断他们的评分(这不是一个很科学的方法). 我们在区间[0,1]内将该评级缩放为双值。.

全字匹配

的要求: 全词匹配的排名应该更高.

到现在为止, 我们的搜索结果相当不错, 但是您可能会注意到一些包含部分匹配的结果可能比完全匹配的结果排名更高. 为了解决这个问题, 我们在文档中添加了一个名为“Keywords”的额外字段,它不使用自动完成分析器, 而是使用关键字标记器并提供提升因子来提高精确匹配结果.

这个字段只有在匹配准确的单词时才会匹配. 它不会像自动补全分析器那样将“wood”匹配为“wooden”.

总结

本文应该向您概述了如何在您的应用程序中设置Elasticsearch .网。项目,并通过一些努力,提供了一个很好的到处搜索功能.

学习曲线可能有点陡峭, 但这是值得的, 尤其是当你把它调整得恰到好处,并开始得到很好的搜索结果时.

始终记得添加带有预期结果的完整测试用例,以确保在引入更改和尝试时不会过多地混淆参数.

本文的完整代码是 可以在GitHub上找到,并使用从 TMDB 数据库,以显示搜索结果如何随着每一步而改进.

聘请Toptal这方面的专家.
现在雇佣
伊万·塞萨尔的头像
伊凡塞萨尔

位于 克罗地亚的萨格勒布

成员自 2016年7月11日

作者简介

Ivan在各种规模的项目上工作了十年,主要是使用 .网络技术. 他还喜欢参加算法竞赛.

Toptal作者都是各自领域经过审查的专家,并撰写他们有经验的主题. 我们所有的内容都经过同行评审,并由同一领域的Toptal专家验证.

工作经验

14

世界级的文章,每周发一次.

订阅意味着同意我们的 隐私政策

世界级的文章,每周发一次.

订阅意味着同意我们的 隐私政策

Toptal开发者

加入总冠军® 社区.