个性化阅读
专注于IT技术分析

.NET开发人员的Elasticsearch教程

本文概述

.NET开发人员应该在他们的项目中使用Elasticsearch吗?尽管Elasticsearch是基于Java构建的, 但我认为它提供了许多理由使Elasticsearch值得对任何项目进行全文搜索。

在过去的几年中, Elasticsearch作为一项技术已经取得了长足的进步。它不仅使全文本搜索听起来像魔术, 而且还提供了其他复杂的功能, 例如文本自动完成, 聚合管道等。

如果想在整洁的.NET生态系统中引入基于Java的服务使你感到不舒服, 那么不必担心, 因为一旦安装并配置了Elasticsearch, 你将花费大部分时间来购买最酷的.NET软件包之一。那里:NEST。

在本文中, 你将学习如何在.NET项目中使用出色的搜索引擎解决方案Elasticsearch。

安装和配置

将Elasticsearch本身安装到你的开发环境需要下载Elasticsearch和(可选)Kibana。

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

cd "D:\elastic\elasticsearch-5.2.2\bin"
start elasticsearch.bat
 
cd "D:\elastic\kibana-5.0.0-windows-x86\bin"
start kibana.bat
 
exit

启动这两种服务后, 你始终可以检查本地Kibana服务器(通常可从http:// localhost:5601获得), 使用索引和类型, 并使用纯JSON搜索, 如此处广泛介绍的那样。

第一步

作为一名全面而优秀的开发人员, 在管理层的全面支持和理解下, 你首先需要添加一个单元测试项目并编写一个至少覆盖90%代码覆盖范围的SearchService。

第一步显然是配置app.config文件以为Elasticsearch服务器提供一个连接排序字符串。

Elasticsearch的优点是它完全免费。但是, 我仍然建议你使用Elastic.co提供的Elastic Cloud服务。托管服务使所有维护和配置都非常容易。更重要的是, 你还有两个星期的免费试用期, 足够试用这里的所有示例!

由于这里我们在本地运行, 因此应执行以下配置键:

<add key="Search-Uri" value="http://localhost:9200" />

Elasticsearch安装默认在端口9200上运行, 但是你可以根据需要更改它。

ElasticClient和NEST软件包

ElasticClient是一个不错的小家伙, 它将为我们完成大部分工作, 并且附带了NEST软件包。

让我们先安装该软件包。

要配置客户端, 可以使用以下方式:

var node = new Uri(ConfigurationManager.AppSettings["Search-Uri"]);
var settings = new ConnectionSettings(node);
settings.ThrowExceptions(alwaysThrow: true); // I like exceptions
settings.PrettyJson(); // Good for DEBUG
var client = new ElasticClient(settings);

索引和映射

为了能够进行搜索, 我们必须将一些数据存储到ES中。使用的术语是”索引编制”。

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

通常, 在使用Elasticsearch时, 你可能正在寻找站点范围内的搜索引擎解决方案。你将使用某种类型的供稿或摘要, 或类似Google的搜索来返回各种实体(例如用户, 博客条目, 产品, 类别, 事件等)的所有结果。

这些可能不仅是数据库中的一个表或实体, 而且, 你将希望聚合各种数据, 并可能提取或派生一些通用属性, 例如标题, 描述, 日期, 作者/所有者, 照片等。另一件事是, 你可能不会在一个查询中进行查询, 但是, 如果你使用的是ORM, 则必须为每个博客条目, 用户, 产品, 类别, 事件或其他内容编写一个单独的查询。

我通过为每种”大”类型(例如博客文章或产品)创建索引来组织项目。然后可以为某些更具体的类型添加一些Elasticsearch类型, 这些类型将属于同一索引。例如, 如果某篇文章可以是故事, 视频文章或播客, 那么它仍将位于”文章”索引中, 但我们将在该索引中包含这四种类型。但是, 它仍然可能是数据库中的同一查询。

请记住, 每个索引确实至少需要一种类型-可能是与索引名称相同的类型。

要映射你的实体, 你将需要创建一些其他类。我通常使用DocumentSearchItemBase类, 每个专用类将从该类继承BlogPostSearchItem, ProductSearchItem等。

我喜欢在那些类中包含映射器表达式。如果需要, 我随时可以修改表达式。

在我与Elasticsearch进行的最早项目中, 我编写了一个相当大的SearchService类, 该类通过很好的冗长的switch-case语句完成了映射和索引编制工作:对于要放入Elasticsearch的每种实体类型, 都有一个开关和带有映射关系的查询做过某事。

但是, 在整个过程中, 我了解到这不是最好的方法, 至少对我而言不是。

一个更优雅的解决方案是为每个索引提供某种智能的IndexDefinition类和特定的索引定义类。这样, 我的基本IndexDefinition类可以存储所有可用索引的列表以及一些帮助程序方法(例如所需的分析器和状态报告), 而派生的特定于索引的类则处理查询数据库并具体映射每个索引的数据。特别是在以后需要向ES添加其他实体时, 这很有用。归结为添加另一个继承自IndexDefinition的SomeIndexDefinition类, 你仅需要实现一些方法即可查询将要在索引中获取的数据。

Elasticsearch说话

Elasticsearch可以做的一切事情的核心是它的查询语言。理想情况下, 你能够与Elasticsearch进行通信的所有事情就是知道如何构造查询对象。

在幕后, Elasticsearch公开了其功能, 即基于HTTP的基于JSON的API。

尽管API本身和查询对象的结构相当直观, 但是处理许多现实情况仍然很麻烦。

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

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

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

  • 具体的类型选择(在进行聚合时, 例如我们将在此处进行的操作)

  • 查询本身

  • 突出显示定义(如果需要, Elasticsearch可以自动突出显示匹配)

例如, 你可能希望实现一种搜索功能, 其中只有部分用户可以看到你网站上的高级内容, 或者你​​可能希望某些内容仅对其作者的”朋友”可见, 依此类推。

能够构造查询对象是这些问题解决方案的核心, 当尝试涵盖许多场景时, 这确实是一个问题。

从以上所有内容来看, 最重要和最困难的设置当然就是查询段了, 在这里, 我们将主要集中于此。

查询是BoolQuery和其他查询(例如MatchPhraseQuery, TermsQuery, DateRangeQuery和ExistsQuery)的组合的递归构造。这些足以满足任何基本要求, 并且应该是一个良好的开端。

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

MatchPhraseQuery可以按常规SQL数据库中的外键或诸如枚举之类的静态值来过滤结果, 例如, 当按特定作者(AuthorId)匹配结果或匹配所有公共文章(ContentPrivacy = Public)时。

TermsQuery将被翻译成”常规” SQL语言。例如, 它可以退回用户的一个朋友写的所有文章, 或者专门从固定的一组商人那里购买产品。与SQL一样, 不应过度使用它, 并在此数组中放置10, 000个成员, 因为它会影响性能, 但通常可以很好地处理合理数量。

DateRangeQuery是自记录的。

ExistsQuery是一个有趣的查询:它使你可以忽略或返回没有特定字段的文档。

这些与BoolQuery结合使用时, 可以定义复杂的过滤逻辑。

例如, 考虑一个博客站点, 在该站点中, 博客帖子可以具有一个AvailableFrom字段, 该字段指示何时应显示它们。

如果我们应用类似AvailableFrom <= Now的过滤器, 那么我们将不会获得根本没有该特定字段的文档(我们聚合数据, 并且某些文档可能未定义该字段)。要解决该问题, 你可以将ExistsQuery与DateRangeQuery结合起来, 并在满足BoolQuery中至少一个元素的条件下将其包装在BoolQuery中。像这样:

BoolQuery
    Should (at least one of the following conditions should be fulfilled)
        DateRangeQuery with AvailableFrom condition
        Negated ExistsQuery for field AvailableFrom

否定查询并不是一件简单的现成的工作。但是在BoolQuery的帮助下, 仍然有可能:

BoolQuery
    MustNot
        ExistsQuery

自动化与测试

为了使事情变得容易, 推荐的方法肯定是随手编写测试。

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

实际例子

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

在我的上一个项目中, 我使用Elasticsearch提供了一个用户供稿:所有内容按照创建日期和全文搜索(带有某些选项)排序到一个地方。提要本身非常简单;只需确保数据中某处有日期字段, 然后按该字段排序即可。

另一方面, 开箱即用的搜索效果不佳。那是因为, 自然地, Elasticsearch无法知道数据中的重要内容。假设我们有一些数据(在其他字段中)包含标题, 标签(数组)和正文字段。正文字段可以是HTML内容(使内容更真实)。

拼写错误

要求:即使出现拼写错误或单词结尾不同, 我们的搜索也应返回结果。例如, 如果有一篇标题为”用木勺可以做的宏伟的事情”的文章, 当我搜索”事物”或”木头”时, 我仍然想找到一个匹配项。

为了解决这个问题, 我们必须熟悉分析器, 令牌化器, char过滤器和令牌过滤器。这些是在编制索引时应用的​​转换。

  • 需要定义分析器。可以按索引定义。

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

  • 分析器是过滤器, 字符过滤器和标记器的组合。

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

  • 英文停用词过滤器:该过滤器会删除英语中的所有常见单词, 例如” and”或” the”。

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

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

  • Edge-n-gram标记器:此标记器使我们能够进行部分匹配。例如, 如果我们有一个句子”我的奶奶有一把木椅”, 而在寻找”木头”一词时, 我们仍然希望在该句子上获得成功。 edge-n-gram的作用是存储” woo”, ” wood”, ” woode”和” wooden”, 以便找到与至少三个字母匹配的任何部分单词。参数MinGram和MaxGram定义要存储的最小和最大字符数。在我们的情况下, 我们最少要有3个字母, 最多15个字母。

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

analysis.Analyzers(a => a
	.Custom("autocomplete", cc => cc
		.Filters("eng_stopwords", "trim", "lowercase")
		.Tokenizer("autocomplete")
	)
	.Tokenizers(tdesc => tdesc
		.EdgeNGram("autocomplete", e => e
			.MinGram(3)
			.MaxGram(15)
			.TokenChars(TokenChar.Letter, TokenChar.Digit)
		)
	)
	.TokenFilters(f => f
		.Stop("eng_stopwords", lang => lang
			.StopWords("_english_")
		)
	);

而且, 当我们要使用此分析器时, 应仅对想要的字段进行注释, 如下所示:

public class SearchItemDocumentBase
{
	...

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

现在, 让我们看一些示例, 这些示例演示了几乎所有具有大量内容的应用程序中非常普遍的要求。

清理HTML

要求:我们的某些字段中可能包含HTML文本。

自然, 你不希望搜索” section”以返回诸如” <section>…</ section>”或” body”之类的返回HTML元素” <body>”的内容。为避免这种情况, 在建立索引期间, 我们将剥离HTML并将内容仅留在其中。

幸运的是, 你不是第一个遇到此问题的人。 Elasticsearch附带了一个有用的char过滤器:

analysis.Analyzers(a => a
	.Custom("html_stripper", cc => cc
		.Filters("eng_stopwords", "trim", "lowercase")
		.CharFilters("html_strip")
		.Tokenizer("autocomplete")
	)

并应用它:

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

重要领域

要求:标题中的匹配比内容中的匹配更重要。

幸运的是, 如果匹配发生在一个字段或另一个字段中, Elasticsearch提供了提高结果的策略。这是通过使用boost选项在搜索查询构造中完成的:

const int titleBoost = 15;

.Query(qx => qx.MultiMatch(m => m
	.Query(searchRequest.Query.ToLower())
	.Fields(ff => ff
		.Field(f => f.Title, boost: titleBoost)
		.Field(f => f.Summary)
		...
	)
	.Type(TextQueryType.BestFields)
) && filteringQuery)

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

立即设置提升值并不总是那么容易。你需要进行一些操作才能获得理想的结果。

优先文章

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

Elasticsearch允许我们实现计分功能, 并通过定义字段”重要性”的方式对其进行简化, 该字段是双值-在我们的例子中大于1。你可以定义自己的重要性函数/因子并应用它同样。你可以定义多个提升和评分模式-最适合你。这个为我们很好地工作:

.Query(q => q
	.FunctionScore(fsc => fsc
		.BoostMode(FunctionBoostMode.Multiply)
		.ScoreMode(FunctionScoreMode.Sum)
		.Functions(f => f
			.FieldValueFactor(b => b
				.Field(nameof(SearchItemDocumentBase.Rating))
				.Missing(0.7)
				.Modifier(FieldValueFactorModifier.None)
			)
		)
		.Query(qx => qx.MultiMatch(m => m
			.Query(searchRequest.Query.ToLower())
			.Fields(ff => ff
				...
			)
			.Type(TextQueryType.BestFields)
		) && filteringQuery)
	)
)

每部电影都有一个评分, 我们通过演员的评分来推算演员的评分(不是很科学的方法)。我们将该等级缩放为间隔[0, 1]中的两倍。

全字匹配

要求:全字匹配的排名应该更高。

到目前为止, 我们的搜索结果相当不错, 但是你可能会注意到某些包含部分匹配项的结果可能会比完全匹配项排名更高。为了解决这个问题, 我们在文档中添加了一个名为”关键字”的附加字段, 该字段不使用自动完成分析器, 而是使用关键字标记器, 并提供提升因子以将完全匹配结果推高。

仅当精确的单词匹配时, 此字段才匹配。它不会像自动完成分析仪一样将” wooden”与” wooden”匹配。

本文总结

本文应该概述了如何在.NET项目中设置Elasticsearch, 并且不费吹灰之力就提供了一种不错的随处搜索功能。

学习曲线可能有点陡峭, 但这是值得的, 尤其是当你正确调整它并开始获得不错的搜索结果时。

始终记得添加带有预期结果的全面测试用例, 以确保在引入更改和玩耍时不会过多地弄乱参数。

这篇文章的完整代码可在GitHub上找到, 并使用从TMDB数据库中提取的数据来显示搜索结果在每个步骤中的改进情况。

赞(0) 打赏
未经允许不得转载:srcmini » .NET开发人员的Elasticsearch教程
分享到: 更多 (0)

评论 抢沙发

评论前必须登录!

 

觉得文章有用就打赏一下文章作者

微信扫一扫打赏