7. Elasticsearch TF-IDF算法及高级查询

1,727 阅读16分钟

boolean model

boolean model: 所谓bool 就是 true false
match operator: or and not
bool => must/must not/should
term->doc分数

query "hello world" --> 过滤 --> hello / world / hello & world
bool --> must/must not/should --> 过滤 --> 包含 / 不包含 / 可能包含
doc --> 不打分数 --> 正或反 true or false --> 为了减少后续要计算的doc的数量,提升性能query: hello world

"match": {
    "title": "hello world"
}

"bool": {
  "should": [
      {
          "match": {
              "title": "hello"
           }
       },
       {
          "match": {
              "title": "world"
           }
        }
   ]
}

boost:权重

TF/IDF算法

relevance score算法,简单来说,就是计算出,一个索引中的文本,与搜索文本,他们之间的关联匹配程度
Elasticsearch使用的是 term frequency/inverse document frequency算法,简称为TF/IDF算法

  • TF(词频term frequency): 关键词在每个doc中出现的次数 一个term在一个doc中,出现的次数越多,那么最后给的相关度评分就会越高

  • IDF(反文档词频inversed document frequency):关键词在整个索引中出现的次数 一个term在所有的doc中,出现的次数越多,那么最后给的相关度评分就会越低

  • length norm 字段长度越长,值越小 搜索的field的长度越长,给的相关度评分越低;field长度越短,给的相关度评分越高

举例说明

query match: hello world
假设三个文档:

dochelloworld
doc1hello eshello es
doc2big worldbig world
doc3hello worldhello world
score(q,d)  =  
            queryNorm(q)  
          · coord(q,d)    
          · ∑ (           
                tf(t in d)   
              · idf(t)2      
              · t.getBoost() 
              · norm(t,d)    
            ) (t in q)
  • score(q,d) query对一个doc最终的评分结果 这个公式的最终结果,就是一个query(叫做q),对一个doc(叫做d)的最终的总评分
  • queryNorm(q) 在不影响相互关系的前提下,把看似离散的数据,转换到一个相近的区间 是用来让一个doc的分数处于一个合理的区间内,不要太离谱,举个例子,一个doc分数是10000,一个doc分数是0.1
  • coord(q,d) 简单来说,就是对更加匹配的doc,进行一些分数上的成倍的奖励 对匹配的结果加分,越匹配的doc加分越多
  • ∑:doc对query中每个trem的权重的总和
  • tf(t in d) 计算每一个term对doc的分数的时候,就是TF/IDF算法
  • idf(t) is the inverse document frequency for term t. 计算每一个term对doc的分数的时候,就是TF/IDF算法
  • t.getBoost() 增加权重
  • norm(t,d) 搜索出的field长度越长,给的相关度评分越低; field长度越短,给的相关度评分越高

高级查询

shard local idf和global idf

在es中检索某个field中是否包含关键字,会使用到TF/IDF算法来计算相关度分数,计算相关度分数主要从以下三点考虑
1、在一个doc中field中关键字出现的次数(越大相关度越高)
2、在所有doc中field中关键字出现的次数(次数越大相关度越低)
3、doc中field的长度
在计算相关度分数时,第二点很关键,因为es默认在一个shard中统计的,不是索引里所有的primary shard
基于此种默认情况,有可能出现在多shard下计算相关度分数不准确
解决办法:
1、生产环境下,数据量大,尽可能实现均匀分配,es在多个shard中均匀路由数据的,路由的时候根据_id,负载均衡
2、测试环境下,将索引的primary shard设置为1个,number_of_shards=1
3、搜索附带search_type=dfs_query_then_fetch参数,会将local IDF取出来计算global IDF
获取所有shard的local IDF计算结果,在本地进行global IDF分数的计算,会将所有shard的doc作为上下文来进行计算,也能确保准确性。但是生产环境下,不推荐这个参数,因为性能很差

multi_match多字段搜索

best_fields、most_fields和cross_fields策略

  • best_fields:默认值,对于同一个query,单个field匹配更多的term,则优先排序。
  • most_fields:如果一次请求中,对于同一个doc,匹配到某个term的field越多,则越优先排序。
  • cross_fields: 所有术语都必须存在于至少一个字段中才能与文档匹配
GET /product/_search
{
  "query": {
    "multi_match": {
      "query": "小米手机",
      "fields": ["desc","name"]
    }
  }
}

GET /product/_search
{
  "query": {
    "multi_match": {
      "type": "most_fields", 
      "query": "小米手机",
      "fields": ["desc","name"]
    }
  }
}
等同 bool 

GET /_search
{
  "query": {
    "multi_match" : {
      "query":      "Will Smith",
      "type":       "best_fields",
      "fields":     [ "first_name", "last_name" ],
      "operator":   "and" 
    }
  }
}
(+first_name:will +first_name:smith) | (+last_name:will  +last_name:smith)

dis_max 和 tie_breaker

dis_max只取某一个query最大的分数,完全不考虑其他query的分数,这种一刀切的做法,可能导致在有其他query的影响下,score不准确的情况,这时为了使用结果更准确,最好还是要考虑到其他query的影响
tie_breaker参数的意义,将其他query的分数乘以tie_breaker,然后综合考虑后与最高分数的那个query的分数综合在一起进行计算,这样做除了取最高分以外,还会考虑其他的query的分数。tie_breaker的值,设置在在0~1之间,是个小数就行,没有固定的值
最佳的精确值需要根据数据与查询调试得出,但是合理值应该与零接近(处于 0.1 - 0.4 之间),这样就不会颠覆 dis_max 最佳匹配性质的根本。

GET product/_search
{
    "query": {
        "dis_max": {
            "queries": [
                {"match": {"name": "超级快充"}},
                {"match": {"desc": "超级快充"}}
            ],
            "tie_breaker": 0.7
        }
    }
}

function score query

在使用 Elasticsearch 进行全文搜索时,搜索结果默认会以文档的相关度进行排序,如果想要改变默认的排序规则,也可以通过sort指定一个或多个排序字段。
但是使用sort排序过于绝对,它会直接忽略掉文档本身的相关度(根本不会去计算)。在很多时候这样做的效果并不好,这时候就需要对多个字段进行综合评估,得出一个最终的排序。
在 Elasticsearch 中function_score是用于处理文档分值的 DSL,它会在查询结束后对每一个匹配的文档进行一系列的重打分操作,最后以生成的最终分数进行排序。它提供了几种默认的计算分值的函数:

  • weight:设置权重
  • field_value_factor:将某个字段的值进行计算得出分数。
    • factor:对字段值进行预处理,乘以指定的数值(默认为 1)
    • modifier将字段值进行加工,有以下的几个选项:
      • none:不处理
      • log:计算对数
      • log1p:先将字段值 +1,再计算对数
      • log2p:先将字段值 +2,再计算对数
      • ln:计算自然对数
      • ln1p:先将字段值 +1,再计算自然对数
      • ln2p:先将字段值 +2,再计算自然对数
      • square:计算平方
      • sqrt:计算平方根
      • reciprocal:计算倒数
  • random_score:随机得到 0 到 1 分数
  • decay Function:衰减函数,同样以某个字段的值为标准,距离某个值越近得分越高
  • script_score:通过自定义脚本计算分值(前几种方式已经解决了大部分问题,但有局限性,只能针对一个字段计算分值,(field_value_factor 一般只用于数字类型,而衰减函数一般只用于数字、位置和时间类型),script_score支持我们自己编写一个脚本运行,在该脚本中我们可以拿到当前文档的所有字段信息,并且只需要将计算的分数作为返回值传回 Elasticsearch 即可)
  • boost_mode可以指定计算后的分数与原始的_score如何合并,有以下选项:
    • multiply:将结果乘以_score 默认值
    • sum:将结果加上_score
    • min:取结果与_score的较小值
    • max:取结果与_score的较大值
    • replace:使结果替换掉_score
  • max_boost:分数上限
# weight
GET product/_search
{
  "query": {
    "function_score": {
      "query": {
        "match_all": {}
      },
      "functions": [
        {
          "filter": {
            "term": {
              "name": "手机"
            }
          },
          "weight": 23
        }
      ]
    }
  }
}
# field_value_factor
GET product/_search
{
  "query": {
    "function_score": {
      "query": {
        "match": {
          "name": "手机"
        }
      },
      "field_value_factor": {
        "field": "collected_num",
        "modifier": "none", 
        "factor": 1.2
      }
    }
  }
}
# random_score
# 它有一个非常有用的特性是可以通过seed属性设置一个随机种子,该函数保证在随机种子相同时返回值也相同,这点使得它可以轻松地实现对于用户的个性化推荐。
GET product/_search
{
  "query": {
    "function_score": {
      "query": {
        "match_all": {}
      },
      "functions": [
        {
          "random_score": {
            "seed": 10,
            "field": "_id"
          }
        }
      ]
    }
  }
}

# 原点(origin):该字段最理想的值,这个值可以得到满分(1.0)
# 偏移量(offset):与原点相差在偏移量之内的值也可以得到满分
# 衰减规模(scale):当值超出了原点到偏移量这段范围,它所得的分数就开始进行衰减了,衰减规模决定了这个分数衰减速度的快慢
# 衰减值(decay):该字段可以被接受的值(默认为 0.5),相当于一个分界点,具体的效果与衰减的模式有关
GET product/_search
{
  "query": {
    "function_score": {
      "query": {
        "match_all": {}
      },
      "gauss": {
        "createtime": {
          "origin": "2020-05-20",
          "scale": "10d",
          "offset": "5d",
          "decay": 0.5
        }
      }
    }
  }
}
# script_score
GET product/_search
{
  "query": {
    "function_score": {
      "query": {
        "match_all": {}
      },
      "script_score": {
        "script": {
          "source": "Math.log(1 + doc['price'].value)"
        }
      }
    }
  }
}

Nested Search

当需要存储关系对象时 比如 一个帖子下有多个评论

主要解决对象关系存储
需要手动指定索引各字段的类型 并指定关系对象为nested类型
注意只能在新建索引时指定好,否则后续不能修改,需要通过删除索引 重建

PUT /order/_doc/1
{
  "order_name": "小米10 Pro订单",
  "desc": "shouji zhong de zhandouji",
  "goods_count": 3,
  "total_price": 12699,
  "goods_list": [
    {
      "name": "小米10 PRO MAX 5G",
      "price": 4999
    },
    {
      "name": "钢化膜",
      "price": 19
    },
    {
      "name": "手机壳",
      "price": 199
    }
  ]
}

PUT /order/_doc/2
{
  "order_name": "扫地机器人订单",
  "desc": "shouji zhong de zhandouji",
  "goods_count": 2,
  "total_price": 12699,
  "goods_list": [
    {
      "name": "小米扫地机器热儿",
      "price": 1999
    },
    {
      "name": "洗碗机",
      "price": 4999
    }
  ]
}
GET order/_search
GET order/_mapping
DELETE order

PUT order
{
  "mappings": {
    "properties": {
      "desc": {
        "type": "text",
        "fields": {
          "keyword": {
            "type": "keyword",
            "ignore_above": 256
          }
        }
      },
      "goods_count": {
        "type": "long"
      },
      "goods_list": {
        "type": "nested",
        "properties": {
          "name": {
            "type": "text",
            "fields": {
              "keyword": {
                "type": "keyword",
                "ignore_above": 256
              }
            }
          },
          "price": {
            "type": "long"
          }
        }
      },
      "order_name": {
        "type": "text",
        "fields": {
          "keyword": {
            "type": "keyword",
            "ignore_above": 256
          }
        }
      },
      "total_price": {
        "type": "long"
      }
    }
  }
}

发现不设置mapping时,直接使用goods_list. 模式去查询时,得到的结果是不准确的,是因为内部对象被扁平化为一个简单的字段名称和值列表,如下

{
	"goods_list.name": ["小米扫地机器热儿", "洗碗机"],
	"goods_list.price": [4999, 1999]
}

重新设置mapping,重新插入数据,发现结果正常。 nested的主要作用是,嵌套对象将数组中的每个对象索引为单独的隐藏文档,这意味着可以独立于其他对象查询每个嵌套对象。

# path:nested对象的查询深度
GET /order/_search
{
  "query": {
    "nested": {
      "path": "goods_list",
      "query": {
        "bool": {
          "must": [
            {
              "match": {
                "goods_list.name": "小米10"
              }
            },
            {
              "match": {
                "goods_list.price": 4999
              }
            }
          ]
        }
      }
    }
  }
}
# 删除demo
POST  blog_new/blog/1/_update
{
 "script": {
    "lang": "painless",
    "source": "ctx._source.comments.removeIf(it -> it.name == 'John');"
 }
}
# 更新demo
POST blog_new/blog/2/_update
{
  "script": {
    "source": "for(e in ctx._source.comments){if (e.name == 'steve') {e.age = 25; e.comment= 'very very good article...';}}" 
  }
}
# 聚合demo
GET blog_new/_search
{
  "size": 0,
  "aggs": {
    "comm_aggs": {
      "nested": {
        "path": "comments"
      },
      "aggs": {
        "min_age": {
          "min": {
            "field": "comments.age"
          }
        }
      }
    }
  }
}
  • score_mode:聚合分数计算方式
    • avg (默认):使用所有匹配的子对象的平均相关性得分。
    • max:使用所有匹配的子对象中的最高相关性得分。
    • min:使用所有匹配的子对象中最低的相关性得分。
    • none:不要使用匹配的子对象的相关性分数。该查询为父文档分配得分为0。
    • sum:将所有匹配的子对象的相关性得分相加。

父子查询(has_child has_parent parent_ID)

# branch:代表一个分公司
# employee:代表员工
# 关系: 一个公司可以包含多个员工
DELETE company
PUT /company
{
  "mappings": {
    "properties": {
      "join-field": {
        "type": "join",
        "relations": {
           "parent": "child"
        }
      }
    }
  }
}

GET /company/_mapping

POST /company/_doc/_bulk
{ "index": { "_id": "london" }}
{ "name": "London Westminster", "city": "london", "country": "UK", "join-field": "parent" }
{ "index": { "_id": "liverpool" }}
{ "name": "Liverpool Central", "city": "liverpool", "country": "UK", "join-field": "parent" }
{ "index": { "_id": "paris" }}
{ "name": "Champs Élysées", "city": "paris", "country": "France" , "join-field": "parent" }


PUT company/_doc/1?routing=london
{
  "name": "Mark Thomas",
  "dob": "1982-05-16",
  "hobby": "diving",
  "join-field": {
    "name": "child",
    "parent": "london"
  }
}  
PUT company/_doc/2?routing=london
{
  "name": "Barry Smith",
  "dob": "1979-04-01",
  "hobby": "hiking",
  "join-field": {
    "name": "child",
    "parent": "london"
  }
}  

PUT company/_doc/3?routing=paris
{
  "name": "Adrien Grand",
  "dob": "1987-05-11",
  "hobby": "horses",
  "join-field": {
    "name": "child",
    "parent": "paris"
  }
}

# 返回某父文档的子文档
# 查询london下的员工
GET company/_search
{
  "query": {
    "parent_id":{
      "type":"child",
      "id":"london"
    }
  }
}  
# 返回包含某子文档的父文档
GET /company/_search
{
  "query": {
    "has_child": {
      "type": "child",
      "query": {
        "match_all": {}
      }
    }
  }
}
# 返回包含某父文档的子文档
GET /company/_search
{
  "query": {
    "has_parent": {
      "parent_type": "parent",
      "query": {
        "match_all": {}
      }
    }
  }
}

Term Vectors

termvector会获取document中的某个field内的各个term的统计信息。

  • term_vector设置接受:
    • no 没有术语向量被存储。(默认)
    • yes 仅存储该字段中的术语。
    • with_positions 条款和职位已存储。
    • with_offsets 存储术语和字符偏移量。
    • with_positions_offsets 存储术语,位置和字符偏移量。
    • with_positions_payloads 术语,位置和有效载荷已存储。
    • with_positions_offsets_payloads 存储术语,位置,偏移量和有效载荷。
PUT my-index-000001
{
  "mappings": {
    "properties": {
      "text": {
        "type":        "text",
        "term_vector": "with_positions_offsets"
      }
    }
  }
}
PUT my-index-000001/_doc/1
{
  "text": "Quick brown fox"
}
GET /my-index-000001/_termvectors/1

高亮语法

  • plain highlight:使用standard Lucene highlighter,对简单的查询支持度非常好。
  • unified highlight:默认的高亮语法,使用Lucene Unified Highlighter,将文本切分成句子,并对句子使用BM25计算词条的score,支持精准查询和模糊查询。
  • fast vector highlighter:使用Lucene Fast Vector highlighter,功能很强大,如果在mapping中对field开启了term_vector,并设置了with_positions_offsets,就会使用该highlighter,对内容特别长的文本(大于1MB)有性能上的优势。
GET my-index-000001/_search
{
  "query": {
    "match": {
      "text": "brown fox"
    }
  },
  "highlight": {
    "pre_tags": [
      "<em class=\"c_color\">"
    ],
    "post_tags": [
      "</em>"
    ],
    "fields": {
      "text": { "type": "plain"}
    }
  }
}

一般情况下,用plain highlight也就足够了,不需要做其他额外的设置。
如果对高亮的性能要求很高,可以尝试启用unified highlight。
如果field的值特别大,超过了1M,那么可以用fast vector highlight。

suggest

自动补全或者纠错。 通过协助用户输入更精准的关键词,提高后续全文搜索阶段文档匹配的程度

PUT suggest_carinfo
{
  "mappings": {
    "properties": {
        "title": {
          "type": "text",
          "analyzer": "ik_max_word",
          "fields": {
            "suggest": {
              "type": "completion",
              "analyzer": "ik_max_word"
            }
          }
        },
        "content": {
          "type": "text",
          "analyzer": "ik_max_word"
        }
      }
  }
}

POST _bulk
{"index":{"_index":"suggest_carinfo","_id":1}}
{"title":"宝马X5 两万公里准新车","content":"这里是宝马X5图文描述"}
{"index":{"_index":"suggest_carinfo","_id":2}}
{"title":"宝马5系","content":"这里是奥迪A6图文描述"}
{"index":{"_index":"suggest_carinfo","_id":3}}
{"title":"宝马3系","content":"这里是奔驰图文描述"}
{"index":{"_index":"suggest_carinfo","_id":4}}
{"title":"奥迪Q5 两万公里准新车","content":"这里是宝马X5图文描述"}
{"index":{"_index":"suggest_carinfo","_id":5}}
{"title":"奥迪A6 无敌车况","content":"这里是奥迪A6图文描述"}
{"index":{"_index":"suggest_carinfo","_id":6}}
{"title":"奥迪双钻","content":"这里是奔驰图文描述"}
{"index":{"_index":"suggest_carinfo","_id":7}}
{"title":"奔驰AMG 两万公里准新车","content":"这里是宝马X5图文描述"}
{"index":{"_index":"suggest_carinfo","_id":8}}
{"title":"奔驰大G 无敌车况","content":"这里是奥迪A6图文描述"}
{"index":{"_index":"suggest_carinfo","_id":9}}
{"title":"奔驰C260","content":"这里是奔驰图文描述"}


GET suggest_carinfo/_search?pretty
{
    "suggest": {
        "car_suggest" : {
            "prefix" : "奔", 
            "completion" : { 
                "field" : "title.suggest" 
            }
        }
    }
}
POST suggest_carinfo/_search
{
  "suggest": {
    "car_suggest": {
      "prefix": "宝马5系",
      "completion": {
        "field": "title.suggest",
        "skip_duplicates":true,
        "fuzzy": {
          "fuzziness": 2
        }
      }
    }
  }
}

  • term suggester:根据词项的词频来推荐.重要参数:
    • text:用户搜索的文本
    • field:要从哪个字段选取推荐数据
    • analyzer:使用哪种分词器
    • size:每个建议返回的最大结果数
    • sort:如何按照提示词项排序,参数值只可以是以下两个枚举:
      • score:分数>词频>词项本身
      • frequency:词频>分数>词项本身
    • suggest_mode:搜索推荐的推荐模式,参数值亦是枚举:
    • missing:默认值,仅匹配不在索引中的词项
    • popular:仅推荐比原始推荐词项文档词频(doc count)更高的相似词项
    • always:根据 建议文本中的词项 推荐 任何匹配的建议词
    • max_edits:可以具有最大偏移距离候选建议以便被认为是建议。只能是1到2之间的值。任何其他值都将导致引发错误的请求错误。默认为2
    • prefix_length:前缀匹配的时候,必须满足的最少字符
    • min_word_length:最少包含的单词数量
    • min_doc_freq:最少的文档频率
  • phrase suggester:phrase suggester和term suggester相比,对建议的文本会参考上下文,也就是一个句子的其他token,不只是单纯的token距离匹配,它可以基于共生和频率选出更好的建议。
    • direct_generator:phrase suggester使用候选生成器生成给定文本中每个项可能的项的列表。单个候选生成器类似于为文本中的每个单独的调用term suggester。生成器的输出随后与建议候选项中的候选项结合打分。目前只支持一种候选生成器,即direct_generator。建议API接受密钥直接生成器下的生成器列表;列表中的每个生成器都按原始文本中的每个项调用。
    • highlight:高亮标签
      • pre_tag:起始标签,如
      • post_tag:闭合标签,如
  • completion suggester:自动补全,自动完成,支持三种查询【前缀查询(prefix)/模糊查询(fuzzy)/正则表达式查询(regex)】
    • Completion:es的一种特有类型,专门为suggest提供,基于内存,性能很高。
    • prefix query:基于前缀查询的搜索提示,是最常用的一种搜索推荐查询。
      • prefix:客户端搜索词
      • field:建议词字段
      • size:需要返回的建议词数量
      • skip_duplicates:是否过滤掉重复建议,默认false
    • fuzzy query
      • fuzziness:允许的偏移量,默认auto
      • transpositions:如果设置为true,则换位计为一次更改而不是两次更改,默认为true。
      • min_length:返回模糊建议之前的最小输入长度,默认 3
      • prefix_length:输入的最小长度(不检查模糊替代项)默认为 1
      • unicode_aware:如果为true,则所有度量(如模糊编辑距离,换位和长度)均以Unicode代码点而不是以字节为单位。这比原始字节略慢,因此默认情况下将其设置为false。
    • regex query:可以用正则表示前缀,不建议使用
  • context suggester:完成建议者会考虑索引中的所有文档,但是通常希望提供由某些条件过滤和/或增强的建议。

前缀搜索、通配符搜索、正则搜索 fuzzy

前缀搜索
以xx开头的搜索,不计算相关度评分,和filter比,没有bitcache。前缀搜索,尽量把前缀长度设置的更长,性能差。

GET index/_search
{
  "query": {
    "prefix": {
      "title": {
        "value": "text"
      }
    }
  }
}

通配符搜索
通配符运算符是匹配一个或多个字符的占位符。例如,*通配符运算符匹配零个或多个字符。您可以将通配符运算符与其他字符结合使用以创建通配符模式

GET /noble_affix/_search
{
  "query": {
    "wildcard": {
      "createName": {
        "value": "*珍"
      }
    }
  }
}

正则 regexp查询的性能可以根据提供的正则表达式而有所不同。为了提高性能,应避免使用通配符模式,如.或 .?+未经前缀或后缀

Fuzzy模糊查询
混淆字符 (box → fox) 缺少字符 (black → lack)
多出字符 (sic → sick) 颠倒次序 (act → cat)

参数:
①value:(必需,字符串)
②fuzziness:(可选,字符串)最大误差  并非越大越好, 召回率高 但是结果不准确
③max_expansions:可选,整数)匹配的最大词项数量。默认为50。
④prefix_length:创建扩展时保留不变的开始字符数。默认为0
⑤transpositions:(可选,布尔值)指示编辑是否包括两个相邻字符的变位(ab→ba)。默认为true。
⑥rewrite:(可选,字符串)用于重写查询的方法
GET /_search
{
    "query": {
        "fuzzy": {
            "user": {
                "value": "keyword"
            }
        }
    }
}

ElasticSearch完整目录

1. Elasticsearch是什么
2.Elasticsearch基础使用
3.Elasticsearch Mapping
4.Elasticsearch 集群原理
5.Elasticsearch Scripts和读写原理
6.Elasticsearch 分词器
7.Elasticsearch TF-IDF算法及高级查询
8.Elasticsearch 地理位置及搜索
9.Elasticsearch ELK