Elasticsearch 的搜索与过滤
一、简介
ElasticSearch 不仅会存储文档,还会索引文档内容使之可以被搜索。 搜索使用_search指令。返回内容包括hits,took,shards,timeout。分页参数包括from以及size,要特别关注深度分页。
想要进阶,只知道如何使用 match 查询是不够的,我们需要理解数据以及如何能够搜索到它们。本章会解释如何索引和查询我们的数据让我们能利用词的相似度、部分匹配、模糊匹配以及语言感知这些优势。
理解每个查询如何贡献相关度评分 _score 有助于调试我们的查询:确保我们认为的最佳匹配文档出现在结果首页,以及削减结果中几乎不相关的 “长尾(long tail)”。
搜索不仅仅是全文搜索:我们很大一部分数据都是结构化的,如日期和数字。 我们会以说明结构化搜索与全文搜索最高效的结合方式开始本章的内容。
二、timeout 机制
指定每个 shard,就只能在 timeout 时间范围内,将搜索的部分数据(也可能全部搜索到了),直接返回给 client 程序,
而不是等到所有的数据全部搜索出来以后再返回。确保说,一次搜索请求可以再用户指定的 timeout 时长内完成。
为一些对时间敏感的搜索应用提供良好的支持。
三、搜索 API
用于搜索的 REST API 可以从 _search 端点访问
GET /索引名称/_search?q = *&pretty
q=*参数表示匹配索引中的所有文档。
pretty参数返回漂亮的JSON结果。
回应数据说明
took:执行搜索的时间(以毫秒为单位)
timed_out:搜索是否超时
_shards:搜索了多少碎片,以及搜索碎片成功/失败的次数
hits:搜索结果
hits.total:符合我们搜索条件的文件总数
hits.hits:实际的搜索结果数组(默认为前10个文档)
hits.sort:对结果进行排序的键(按分数排序时丢失)
hits._score并且max_score:文档相关性打分
四、介绍查询语言
GET /索引名称/_search
{
"query": { "match_all": {} }, #query部分告诉我们我们的查询定义是什么,match_all查询用来匹配所有文档的
"from": 10, #分页
"size": 10, #如果size未指定,则默认为10
"sort": { "balance": { "order": "desc" } }, #排序
"highlight": { "fields" : { "about" : {} } }, #高亮
"_source": ["company_name", "company_name_digest"],#返回需要的几个字段
}
五、query 介绍
5.1 match_all:查询所有文档,是没有查询条件下的默认语句。
5.2 match:标准查询,不论需要全文本查询还是精确查询基本上都要用到它。
如果使用 match 查询一个全文本字段,它会在真正查询之前用分析器先分析一下 match 的查询字符。
如果使用 match 查询确切值(譬如数字、日期、布尔值、not_analyzed 字符串),ES 将会搜索给定值。
{
"query": {
"match": {
"companyName": {
"query": "上海凭安征信服务有限公司",
"operator": "or" #用来控制match查询匹配词条的逻辑条件,默认值是or,如果设置为and,表示查询满足所有条件;
"minimum_should_match":"30%" #当operator参数设置为or时,该参数用来控制应该匹配的分词的最少数量;
}
}
}
}
注意:操作符可以改为and(改为and后每个分词后的词语都要匹配上才会有结果返回)
5.3 multi_match:多重匹配查询允许做match查询的基础上同时搜索多个字段。
{
"query": {
"multi_match": {
"query": "上海凭安征信服务有限公司", #查询字符串
"fields": [ #要查询的字段
"companyName",
"legalPerson",
"*_name", #可以使用通配符指定字段
"companyNameStr^3" #使用插入符号(^)表示法可以提升单个字段
],
"type":"best_fields" #查询类型
"minimum_should_match":"30%"
}
}
}
查询类型
1.best_fields:(默认)主要是说将某一个field匹配尽可能多的关键词的doc优先返回。
2.most_fields:主要是说尽可能返回更多的field匹配到某个关键词的doc,优先返回。
3.cross_fields:对待字段与analyzer它们是一个大字段一样。在任何 字段中查找每个单词。
4.phrase:match_phrase对每个字段 运行查询并组合_score每个字段。
5.phrase_prefix/match_phrase_prefix:对每个字段 运行查询并组合_score每个字段。
5.4 bool:bool查询与bool过滤相似,用于合并多个查询子句。
不同之处在于:bool 过滤可以直接给出是否匹配成功,而 bool 查询要计算每一个查询子句的 _score。
- must:查询指定文档一定要包含;
- must_not:查询指定文档一定不要被包含;
- should:查询指定文档,有则可以为文档相关性加分;
GET /索引名称/_search { "query": { "bool": { "must": [ #must此示例组成两个match查询,并返回company_name中包含“上海”和“公司”的所有文档 #should此示例组成两个match查询,并返回company_name中包含“上海”或“公司”的所有文档 #must_not此示例组成两个match查询,并返回company_name中即不包含“上海”也不包含“公司”的所有文档 { "match": { "company_name": "上海" } }, { "match": { "company_name": "公司" } } ] } } }
5.5 match_phrase: 短语匹配相对顺序一致的所有指定词语,但是并不是 match_phrase 可以直接对分词字段进行不分词检索
{
"query": {
"match_phrase": {
"companyName": {
"query": "上海凭安"
}
}
}
}
返回文档需要满足两个条件:
1、分词后的短语按照文档分词顺序匹配上
2、分词后的短语不许在文档分词上也有(也就是是说短语分词出来的词,不能多,只能少)
精确短语匹配也许太过于严格了,也许我们希望含有"quick brown fox"的文档也能够匹配"quick fox"查询,即使位置并不是完全相等的。
我们可以在短语匹配使用slop参数来引入一些灵活性:
{
"query": {
"match_phrase": {
"title": {
"query": "quick fox",
"slop": 1
}
}
}
}
slop参数告诉match_phrase查询词条能够相隔多远时仍然将文档视为匹配。相隔多远的意思是,你需要移动一个词条多少次来让查询和文档匹配?
5.6 match_phrase_prefix:短语匹配前缀,这种查询的行为与 match_phrase 查询一致,不同的是它将查询字符串的最后一个词作为前缀使用
在之前的 前缀查询 中,我们警告过使用前缀的风险,即 prefix 查询存在严重的资源消耗问题,短语查询的这种方式也同样如此。前
缀 a 可能会匹配成千上万的词,这不仅会消耗很多系统资源,而且结果的用处也不大。
可以通过设置 max_expansions 参数来限制前缀扩展的影响,一个合理的值是可能是 50 :
{
"match_phrase_prefix" : {
"brand" : {
"query": "johnnie walker bl",
"max_expansions": 50
}
}
}
5.7 基于dis_max 实现 best field 策略进行多字段搜索
best field 策略,就是说,搜索到的结果,应该是某个 field 匹配到了尽可能多的关键词,被排在前面,而不是尽可能多的 field 匹配到了少数关键词排在了前面。dis_max 语法直接取 query 中,分数最高的那一个 query 的分数即可。
{
"query":{
"dis_max":{
"queries":{
{"match":{"title":"test"} },
{"match":{"content":"test"} }
}
}
}
}
5.8 基于 tie_breaker 参数优化 dis_max 搜索效果
默认的 dis_max 只会取某一个 query 最大的分数,完全不考虑其他 query 分数。可以使用 tie_breaker 可以将其他 query 的分数也考虑进去。 tie_breaker 的参数意义在于将其他 query 的分数,乘以 tie_breaker,然后综合与最高分数的那个 query 的分数,综合在一起进行计算。
{
"query":{
"dis_max":{
"queries":{
{"match":{"title":"test"} },
{"match":{"content":"test"} }
},
"tie_breaker":0.3
}
}
}
六、过滤
事实上有两种结构化语句:结构化查询,结构化过滤。二者非常相似,但是它们由于使用目的不同而差异。一条过滤语句会询问每个文档的字段值是否包含特定值; 一条查询语句则询问每个文档的字段值与特定值得匹配程度如何;查询语句会计算每个文档与查询语句的相关性,给出一个相关性评分_score,并且按照相关性对匹配到的文档进行排序。 原则上来说,使用查询语句来做全文本搜索,或者需要进行相关性评分的时候;其他的情况下应该使用过滤语句。
term:主要用于精确匹配哪些值,比如数字、日期、布尔值或者not_analyzed的字符串。
terms:terms和term有点类似,但是terms允许指定多个匹配条件。譬如某个字段指定了多个值,那么文档需要一起去做匹配。
range:过滤允许我们按照指定范围查找一批数据。操作符包含gt,gte,lt,lte。
exists&missing:exists和missing过滤用于查找文档中是否包含指定字段或者是否没有某个字段。
bool:bool过滤用来合并多个过滤条件的布尔逻辑。
包含如下操作符:
* must:多个查询条件完全匹配,相当于and;
* must_not:多个查询条件的相反匹配,相当于not;
* should:至少有一个查询条件匹配,相当于or;
七、查询与过滤条件合并
查询语句和过滤语句可以放在各自的上下文中。在 ES API 中可以看到许多带有 query 或者 filter 的语句,这些语句既可以包含单条 query 语句,也可以包含一条 filter 子句;也就是说,这些语句需要首先创建一个 query 或者 filter 的上下文关系。通常情况下,一条查询语句需要过滤语句的辅助,全文本搜索除外。search API中仅能包含query语句,所以需要用 filtered 来同时包含 query 和 filter 子句。
带过滤的查询语句:
GET /_search
{
"query": {
"filtered": {
"query": { "match": { "email": "business opportunity" } },
"filter": { "term": { "folder": "inbox" } }
}
}
}
查询语句中的过滤:
GET /_search
{
"query": {
"filtered": {
"filter": {
"bool": {
"must": { "term": { "folder": "inbox" } },
"must_not": {
"query": {
"match": { "email": "urgent business proposal" }
}
}
}
}
}
}
}
八、打分问题
Index 阶段 Boost vs Query 阶段 Boost
在对文档进行索引时可以进行boost,也可以在query阶段进行搜索的时候进行boost。如果一篇文档总是比其他的更重要,你应该考虑在进行索引的时
候就对这些文档进行boost。预先进行过boost的文档搜索更快,因为在进行搜索时要做的事更少。但是即使你知道一些文档总是比其他的重要,但是你
不确定到底有多重要。如果boost太大,如果匹配上了,文档就总在搜索结果的最上面。如果boost因素太小,重要的文档就没法从其他文档中凸显出
来。
如果在索引阶段就进行boost,如果你要换boost的值就得重新建一次索引。除非你手工向索引中添加文档,一个一个决定这个文档该给啥boost值,你
得用脚本或者程序来建立索引,用一些逻辑或者规则来决定boost。逻辑或规则的改变会影响很多文档,要让逻辑或规则生效,就有得重新进行索引。如
果你的索引比较小,那还可以。我们的索引重建一次要好几个小时,所以我们会尽可能不在索引阶段就进行boost。在query阶段使用boost可以随意增
加boost,改变boost的标准,随时改变boost的长度。比起运行时的一些额外开销,灵活性是值得的。
即使你在索引阶段进行了boost,有一些boost是不得不在query阶段做的,有一些boost是在query阶段才生效,因为在索引阶段无法获得足够的信
息。比如,如果你要基于文档的新鲜度(该文档的时间戳时候离当前时间较近),当前时间(索引的当前时刻)就无法从索引阶段获知。这个例子中,如
果你经常重建索引,你可以把index时间作为当前时间,这样就可以避免query阶段的boost了。
应用 Boost
基本上每一个elsaticsearch的query类型都有boost参数让你可以针对这个query进行调优,但是我们没有使用这个query因为就只有一个主 query。主query是query_string的query,对用户的query进行分词,找到匹配项,用Lucene的默认打分算法进行打分。下面我们用一些boost来 决定是否符合特定的标准。
在早先的原型中,我通过对主query进行custom_score的包裹完成了boost。顾名思义, custom_score query允许你使用定制的逻辑来计算每个文 档的得分,可以通过script传入参数。默认的,脚本集成的是MVEL,但是也支持其他语言。你可以通过指定的_score变量来操作得分,所以开始的时候 我是这样做的:
{
"query": {
"custom_score": {
"query": { ...the main query... },
"script": "_score * (doc['class'].value == 'review' ? 1.2 : 1)"
}
}
}
确实生效了,但是量大了就不那么好使。随着我增加越来越多的boost,得用好几行才能结束这个表达式。每个文档的field都需要在索引阶段存储,这样脚本才能检索到并操作这个值,这样索引变得很大操作也变慢了。幸运的是,我们可以使用更好的工具完成这个操作,custom_filters_score query。\ 文档中提到:\ custom_filters_score query允许执行一个query,并且如果命中的结果匹配到了提供的filter(按顺序),就使用boost或是脚本来计算该值。\ 可以明显简化,并提高基于参数化打分的效率,因为这些filter可以进行缓存提供较高的性能,从而boost/脚本也更简单了。\ 转成custom_filters_score query,上面的例子就变成了这样:
{
"query": {
"custom_filters_score": {
"query": { ...the main query... },
"filters": [
{
"filter": {
"term": {
"class": "review"
}
},
"boost": 1.2
}
]
}
}
}
如果你想增加 boost,就再添一个filter指定其标准,并赋boost。可以使用任何的filter,一个包裹另一个的filter甚至andfilter。如果你有多 个filter,那么需要指定这多个匹配filter如何使用score_mode进行融合。默认的,是使用第一个匹配filter的boost,但是如果你有好多个 filter都匹配上了你可以设置score_mode,比如设置multiply 应用到所有的boost上。
如下的query boost评论提升了20%,boost文章提升20%(所以评论的文章会boost 44%)(译者注: (1 + 20%)*(1 + 20%)),然后对 wiki页面进行惩罚,小于600词长的会降权到80%。
{
"query": {
"custom_filters_score": {
"query": { ...the main query... },
"filters": [
{
"filter": {
"term": {
"class": "review"
}
},
"boost": 1.2
},
{
"filter": {
"term": {
"type": "article"
}
},
"boost": 1.2
},
{
"filter": {
"and": [
{
"term": {
"type": "page"
}
},
{
"range": {
"descriptionLength": {
"to": 600
}
}
}
]
},
"boost": 0.2
}
],
"score_mode": "multiply"
}
}
}
变量 Boost
有时你会想根据一个document里面的field的boost来调节boost。比如,如果你想boost最近的文档,今天发布的文档就会比昨天发布的文档增加 boost,而昨天发布的文档比上周发布的文档更大。即使filter会进行缓存,而且跑起来相对比较快。给今天发的文章给个boost,给昨天发布的再给个 boost,上周的文章在给个boost一点都不实用。幸运的是,custom_filters_score 的query可以接受一个script ,这些情况下就不用再使用 boost了。
在下面的例子中,我用了值m, a 还有b按照slide中第七页的设置:m = 3.16E-11, a = 0.08,b = 0.05。因为我们索引中有些未来时间的日 期,所以我加了个绝对值函数abs()来计算query时间和文档的时间戳的绝对值。我是对boost值设置为1,即设置一个新鲜度的值,而不是一个衰减值。
{
"query": {
"custom_filters_score": {
"query": { ...the main query... },
"params": {
"now": ...current time when query is run, expressed as milliseconds since the epoch...
},
"filters": [
{
"filter": {
"exists": {
"field": "date"
}
},
"script": "(0.08 / ((3.16*pow(10,-11)) * abs(now - doc['date'].date.getMillis()) + 0.05)) + 1.0"
}
]
}
}
}
有了这个值,当前的文档会被加权到 160%(boost值2.6)。这个值在10天后会掉到100%,一个月后掉到60%,半年后掉到15%,一年后掉到8%,2年后 掉到4%。
最后要说明的是,Elasticsearch 的 script 进行缓存会执行效率更高,把那些随着query改变的参数通过params值传入,不要把他作为string直接 插入到脚本中。这样,脚本就变成静态值了,无法缓存,但是用参数就不会出现这样的情况。
直接查询时候使用 boost
/POST { {host} }:{ {port} }/demo/article/_search?search_type=dfs_query_then_fetch
{
"query": {
"bool": {
"should": [
{
"match": {
"content": {
"query": "1",
"boost": 2
}
}
},
{
"match": {
"content": "2"
}
}
]
}
}
}
如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。
绑定邮箱获取回复消息
由于您还没有绑定你的真实邮箱,如果其他用户或者作者回复了您的评论,将不能在第一时间通知您!
发布评论