基于词项和基于全文搜索
基于Term的查询
Term是表达语义的最小单位,在Elasticsearch中,Term查询,对输入不做分词。会将输入作为一个整体,在倒排索引中查找准确的词项,并且使用相关度算分公式,为每个包含该词项的文档进行相关度算分。
准确查询不需要算分,可以通过 Constant Score 将查询转换成一个Filtering,避免算分,并利用缓存,提高性能。
一些TermLevelQuery:
- Term Query(精确查询)
- Range Query(范围查询)
- Exists Query(存在查询)
- Prefix Query(前缀查询)
- Wildcard Query(通配符查询)
基于全文的查询
索引和搜索时都会进行分词,查询字符串先传递到一个合适的分词器,然后生成一个共查询的词项列表。
查询的时候,会先对输入的查询进行分词,然后每个词项逐个进行底层的查询,最终将结果进行合并,并为每个文档生成一个算分。
一些全文Query:
- Match Query
- Match Phrase Query
- Query String Query
比较
假设有一条文档:{ "productID" : "XHDK-A-1293-#fJ3","desc":"iPhone" },desc字段是text类型,desc.keyword是keyword类型。
有以下查询:
{
"query": {
"term": {
"desc": {
// 查不到
"value": "iPhone"
// 可以查到
// "value":"iphone"
}
}
}
}
{
"query": {
"term": {
"desc.keyword": {
// 可以查到
// "value": "iPhone"
// 查不到
"value":"iphone"
}
}
}
}
// 查询keyword类型字段时,就算使用match也不会分词
{
"query": {
"match": {
// 可以查到
"desc.keyword": "iPhone"
// 查不到
"desc.keyword":"iphone"
}
}
}
使用constant_score
很多场景,搜索的结果,只有"是" 和 "否"两种结果,这时候算分就没有必要,使用constant_score,可以提高查询效率。
POST /products/_search
{
"query": {
"constant_score": {
"filter": {
"term": {
"desc": "iPhone"
}
}
}
}
}
复合查询
ES中的查询分为:
- Query Context:相关性算分;
- Filter Context:不需要算分(yes or no),可以利用cache,获得更好的性能。
多条件查询可以使用bool查询。一个bool查询,是一个或者多个查询子句的组合,总共包括4种子句,其中2种会影响算分,2种不影响算分。
must - 必须匹配。贡献算分。
should - 选择性匹配。贡献算分。
must_not - Filter Context 查询子句,必须不能匹配。
filter - Filter Context 必须匹配,但是不贡献算分。
相关性并不只是全文本检索的专利。也适用于yes | no的子句,匹配的子句越多,相关性评分越高。如果多条查询子句被合并为一条复合查询语句,比如bool 查询,则每个查询子句计算得出的评分会被合并到总的相关性评分中。
bool查询中:
- 子查询可以任意顺序出现;
- 可以嵌套多个查询;
1.同一层级下单竞争字段,具有相同的权重;
# brown red quick dog有相同权重
POST /animals/_search
{
"query": {
"bool": {
"should": [
{ "term": { "text": "brown" }},
{ "term": { "text": "red" }},
{ "term": { "text": "quick" }},
{ "term": { "text": "dog" }}
]
}
}
}
2.通过嵌套bool查询,可以改变对算分的影响。
# red和brown加起来,和上面的才有相同的权重
POST /animals/_search
{
"query": {
"bool": {
"should": [
{ "term": { "text": "quick" }},
{ "term": { "text": "dog" }},
{
"bool":{
"should":[
{ "term": { "text": "red" }},
{ "term": { "text": "brown" }}
]
}
}
]
}
}
}
- 如果bool查询中,没有must条件,should中必须至少满足一条查询。
Boosting & Boosting Query
- Boosting
# 通过boost调整算分
POST blogs/_search
{
"query": {
"bool": {
"should": [
{"match": {
"title": {
"query": "apple,ipad",
"boost": 1.1
}
}},
{"match": {
"content": {
"query": "apple,ipad",
"boost":2
}
}}
]
}
}
}
- Boosting Query
# pie内容的数据往后排
POST news/_search
{
"query": {
"boosting": {
"positive": {
"match": {
"content": "apple"
}
},
"negative": {
"match": {
"content": "pie"
}
},
"negative_boost": 0.5
}
}
}
单字段多字符串查询
Disjunction Max Query
POST /blogs/_search
{
"query": {
"bool": {
"should": [
{ "match": { "title": "Brown fox" }},
{ "match": { "body": "Brown fox" }}
]
}
}
}
上面例子中,title与body相互竞争,不应该将分数简单叠加,而是应该找到单个最佳匹配字段的评分。
Disjunction Max Query是将任何与任一查询匹配的文档作为结果返回。采用字段上最匹配的评分最终评分返回。
# 使用一个字段上的最高评分作为最终评分
POST blogs/_search
{
"query": {
"dis_max": {
"queries": [
{ "match": { "title": "Brown fox" }},
{ "match": { "body": "Brown fox" }}
]
}
}
}
通过Tie Breaker参数调整:
- 获得最佳匹配语句的评分;
- 将其他匹配语句的评分与tie_breaker相乘;
- 对以上评分求和并规范化。
Tie Breaker是一个介于0到1之间的浮点数,0代表使用最佳匹配,1代表所有语句同样重要。
MultiMatch
单字符串多字段查询三种场景:
- 最佳字段(Best Fields):
当字段之间相互竞争,又相互关联。例如title 和body 这样的字段。评分来自最匹配字段。
# Best Fields是默认类型,可以不用指定;Minimum should match等参数可以传递到生成的query中。
POST blogs/_search
{
"query": {
"multi_match": {
"type": "best_fields",
"query": "Quick pets",
"fields": ["title","body"],
"tie_breaker": 0.2,
"minimum_should_match": "20%"
}
}
}
- 多数字段(Most Fields):
处理英文内容时,一种常见的手段是,在主字段(English Analyzer),抽取词干,加入同义词,以匹配更多文档。相同的文本,加入子字段(Standard Analyzer),以提供更加精确的匹配。其他字段作为匹配文档提高相关度的信号。匹配字段越多越好。
PUT /titles
{
"mappings": {
"properties": {
"title": {
"type": "text",
"analyzer": "english",
"fields": {"std": {"type": "text","analyzer": "standard"}}
}
}
}
}
GET /titles/_search
{
"query": {
"multi_match": {
"query": "barking dogs",
"type": "most_fields",
"fields": [ "title", "title.std" ]
}
}
}
- 混合字段(Cross Field):
对于某些实体,例如人名、地址、图书信息。需要在多个字段中确定信息,单个字段只能作为整体的一部分。希望在任何这些列出的字段中找到尽可能多的词。
GET /titles/_search
{
"query": {
"multi_match": {
"query": "barking dogs road",
"type": "cross_fields",
"operator": "and",
"fields": [ "title" ]
}
}
}
使用别名
# 可以添加过滤器
POST _aliases
{
"actions": [
{
"add": {
"index": "movies-2019",
"alias": "movies-lastest-highrate",
"filter": {
"range": {
"rating": {
"gte": 4
}
}
}
}
}
]
}
Function Score Query
可以在查询结束后,对每一个匹配的文档进行一系列的重新算分,根据新生成的分数进行排序。
提供了几种默认的计算分值的函数:
- Weight:为每一个文档设置一个简单而不被规范化的权重;
- Field Value Factor:使用该数值来修改_score,例如将"热度"和"点赞数"作为算分的参考要素;
- Randow Score:为每一个用户使用一个不同的,随机算分结果;
- 衰减函数:以某个字段的值为标准,距离某个值越近,得分越高;
- Script Score:自定义脚本完全控制所需逻辑。
// 按欢迎程度提升权重:搜索的评分作为排序的主要依据,同时votes多的靠前。
POST /blogs/_search
{
"query": {
"function_score": {
"query": {
"multi_match": {
"query": "popularity",
"fields": [ "title", "content" ]
}
},
// 新的算分 = 老的算分 * 投票数
// 票数为0或者票数很大的时候差异很大
"field_value_factor": {
"field": "votes"
}
}
}
}
- 使用Modifier 平滑曲线
POST /blogs/_search
{
"query": {
"function_score": {
"query": {
"multi_match": {
"query": "popularity",
"fields": [ "title", "content" ]
}
},
// 新的算分 = 老的算分 * log(1 + 投票数)
// 别的modifier:log、log2p、ln、ln1p、ln2p、square、sqrt、reciprocal
"field_value_factor": {
"field": "votes",
"modifier": "log1p"
}
}
}
}
- 引入Factor(曲线更平滑)
POST /blogs/_search
{
"query": {
"function_score": {
"query": {
"multi_match": {
"query": "popularity",
"fields": [ "title", "content" ]
}
},
// 新的算分 = 老的算分 * log(1 + factor * 投票数)
"field_value_factor": {
"field": "votes",
"modifier": "log1p" ,
"factor": 0.1
}
}
}
}
- Boost Mode 和Max Boost
Boost Mode :
1.Multiply:算分与函数值的乘积;
2.Sum:算分与函数值的和;
3.Min/ Max:算分与函数取 最小/最大值;
4.Replace:使用函数值取代算分。
Max Boost 可以将算分控制在一个最大值。
POST /blogs/_search
{
"query": {
"function_score": {
"query": {
"multi_match": {
"query": "popularity",
"fields": [ "title", "content" ]
}
},
"field_value_factor": {
"field": "votes",
"modifier": "log1p" ,
"factor": 0.1
},
"boost_mode": "sum",
"max_boost": 3
}
}
}
- 一致性随机函数
POST /blogs/_search
{
"query": {
"function_score": {
// 不同seed值,返回结果算分不同
"random_score": {
"seed": 911119
}
}
}
}
Suggester
现代的搜索引擎,一般提供Suggest as you type的功能。
帮助用户在输入搜索过程中,进行自动补全或者纠错。通过协助用户输入更加精准的关键词,提高后续搜索阶段文档匹配的程度。
搜索引擎中类似的功能,在Elasticsearch 中是通过Suggester API实现的。
原理:将输入的文本分解为Token,然后在索引的字典里查找相似的Term 并返回。
根据不同的适用场景,Elasticsearch 设计了4中类别的 Suggester:
- Term & Phrase Suggester;
- Complete & Context Suggester。
搜索建议
- Term Suggester
Suggester 就是一种特殊类型的搜索。
// 每个建议都包含了一个算分,相似性是通过 Levenshtein Edit Distance 的算法实现的。
// 核心思想就是一个词改动多少字符就可以和另外一个词一致。提供了很多可选参数来控制相似性的模糊程度。例如“max_edits”
POST /articles/_search
{
"size": 1,
"query": {
"match": {
"body": "lucen rock"
}
},
"suggest": {
"term-suggestion": {
// text 里是调用时候提供的文本,通常来自用户输入的内容
"text": "lucen rock",
"term": {
// 当无法搜索到结果时(missing),返回建议的词
"suggest_mode": "missing",
// 会到 指定的字段body 上搜索
"field": "body",
"prefix_length":0,
"sort": "frequency"
}
}
}
}
Suggestion Mode:
- Missing - 如果索引中已经存在,就不提供建议;
- Popular - 推荐出现频率更加高的词;
- Always - 无论是否存在,都提供建议。
- Phrase Suggester
Phrase Suggester 在 Term Suggester 上增加了一些额外的逻辑,例如一些参数: - Suggest Mode:missing、popular;
- Max Errors:最多可以拼错的Terms 数;
- Confidence:限制返回结果数,默认为1。
POST /articles/_search
{
"suggest": {
"my-suggestion": {
"text": "lucne and elasticsear rock hello world ",
"phrase": {
"field": "body",
"max_errors":2,
"confidence":0,
"direct_generator":[{
"field":"body",
"suggest_mode":"always"
}],
"highlight": {
"pre_tag": "",
"post_tag": ""
}
}
}
}
}
自动补全和基于上下文提示
Completion Suggester 提供了“自动完成(Auto Complete)”的功能。用户每输入一个字符,就需要即时发送一个查询请求到后端查询匹配项;
对性能要求比较苛刻。Elasticsearch 采用了不同的数据结构,并非通过倒排索引来完成。而是将 Analyze 的数据编码成 FST 和索引 一起存放。FST 会被 ES整个加载进内存,速度很快;
FST 只能用于前缀查找。
使用Completion Suggester 的一些步骤:
- 定义Mapping,使用“completion”type;
PUT articles
{
"mappings": {
"properties": {
"title_completion":{
"type": "completion"
}
}
}
}
- 索引数据
- 运行“suggest”查询,得到搜索建议。
// 会返回 elk 开头的数据
POST articles/_search
{
"size": 0,
"suggest": {
"article-suggester": {
"prefix": "elk ",
"completion": {
"field": "title_completion"
}
}
}
}
Context Suggester
Completion Suggester 的扩展,可以在搜索中加入更多的上下文信息,例如,输入“star”
- 咖啡相关:建议“Starbucks”;
- 电影相关:“star wars”
两种类型的 Context:
- Category - 任意的字符串;
- Geo - 地理位置信息。
实现Context Suggester 的具体步骤:
- 定制一个Mapping;
PUT comments/_mapping
{
"properties": {
"comment_autocomplete":{
"type": "completion",
//
"contexts":[{
"type":"category",
"name":"comment_category"
}]
}
}
}
- 索引数据,并且为每个文档加入Context 信息;
// 分类是电影
POST comments/_doc
{
"comment":"I love the star war movies",
"comment_autocomplete":{
"input":["star wars"],
"contexts":{
"comment_category":"movies"
}
}
}
// 分类是咖啡
POST comments/_doc
{
"comment":"Where can I find a Starbucks",
"comment_autocomplete":{
"input":["starbucks"],
"contexts":{
"comment_category":"coffee"
}
}
}
- 结合Context 进行 Suggestion 查询。
// 会查出咖啡类型的数据
POST comments/_search
{
"suggest": {
"MY_SUGGESTION": {
"prefix": "sta",
"completion":{
"field":"comment_autocomplete",
"contexts":{
"comment_category":"coffee"
}
}
}
}
}
比较
- 精准度:
Completion > Phrase > Term - 召回率
Term > Phrase > Completion - 性能
Completion > Phrase > Term
跨集群搜索
水平扩展的痛点
单集群 - 当水平扩展时,节点数不能无限增加,因为当集群的 meta 信息(节点,索引,集群状态)过多,会导致更新压力变大,单个Active Master 会成为性能瓶颈,导致整个集群无法正常工作。
早期版本,通过Tribe Node 可以实现多集群访问的需求,但还存在一定的问题:
- Tribe Node 会以 Client Node 的方式加入每个集群。集群中Master 节点的任务变更需要 Tribe Node 的回应才能继续;
- Tribe Node 不保存Cluster State 的信息,一旦重启,初始化很慢;
- 当多个集群存在索引重名的情况时,只能设置一种Prefer 规则。
Cross Cluster Search
早期 Tribe Node 的方案 存在一定的问题,现在已被 Deprecated;
Elasticsearch 5.3 引入了跨集群搜索的功能(Cross Cluster Search),推荐使用:
- 允许任何节点扮演 federated节点,以轻量的方式,将搜索请求进行代理;
- 不需要以 Client Node 的形式加入其它集群。
- 配置
//在每个集群上设置动态的设置
PUT _cluster/settings
{
"persistent": {
"cluster": {
"remote": {
"cluster0": {
"seeds": [
"127.0.0.1:9300"
],
"transport.ping_schedule": "30s"
},
"cluster1": {
"seeds": [
"127.0.0.1:9301"
],
"transport.compress": true,
"skip_unavailable": true
},
"cluster2": {
"seeds": [
"127.0.0.1:9302"
]
}
}
}
}
}
- 查询
// 在第一个集群搜索
GET /users,cluster1:users,cluster2:users/_search
{
"query": {
"range": {
"age": {
"gte": 20,
"lte": 40
}
}
}
}