四、深入搜索
1. 基于词项和基于全文的搜索
1.1 基于 Term
的查询
Term
的重要性Term
是表达语义的最小单位。搜索和利用统计语言模型进行自然语言处理都需要处理Term
- 特点
Term Level Query:
Term Query / Range Query / Exists Query / Prefix Query / Wildcard Query- 在
ES
中,Term
查询,对输入不做分词。会将输入作为一个整体,在倒排索引中查找准确的词项,并且使用相关度算分公式为每个包含该词项的文档进行相关度算分 - 例如Apple Store
- 可以通过
Constant Score
将查询转换成一个Filtering
, 避免算分,并利用缓存,提高性能
1.2 Term
查询的例子
1.2.1 插入数据
# Term查询的例子,并思考
POST /products/_bulk
{"index":{"_id":1}}
{"productID":"XHDK-A-1293-#fJ3","desc":"iPhone"}
{"index":{"_id":2}}
{"productID":"KDKE-B-9947-#kL5","desc":"iPad"}
{"index":{"_id":3}}
{"productID":"JODL-X-1937-#pV7","desc":"MBP"}
GET /products
1.2.2 例1
POST /products/_search
{
"query": {
"term": {
"desc": {
"value": "iPhone"
}
}
}
}
发现什么结果都查不出来,这是什么原因呢?
是因为我们使用的term查询,es并不会对我们输入的条件做任何的处理,就是说我们搜索的条件就是一个带大写的"iPhone",而es在做数据索引的时候,会对这个text类型的数据进行默认分词的处理,并且转了小写,这就是为什么我们取不到数据的原因(因为是小写啊)
POST /products/_search
{
"query": {
"term": {
"desc": {
"value": "iphone"
}
}
}
}
这样查就能查出数据了
1.2.3 例2
POST /products/_search
{
"query": {
"term": {
"productID": {
"value": "XHDK-A-1293-#fJ3"
}
}
}
}
这也什么都没有查到,这是什么原因呢?通过之前的分析我们知道,term
查询不会对输入的结果进行分词转小写处理,我们来看下es
对"XHDK-A-1293-#fJ3"
这个是如何处理的
POST /_analyze
{
"analyzer": "standard",
"text": ["XHDK-A-1293-#fJ3"]
}
上面的结果是
{
"tokens" : [
{
"token" : "xhdk",
"start_offset" : 0,
"end_offset" : 4,
"type" : "<ALPHANUM>",
"position" : 0
},
{
"token" : "a",
"start_offset" : 5,
"end_offset" : 6,
"type" : "<ALPHANUM>",
"position" : 1
},
{
"token" : "1293",
"start_offset" : 7,
"end_offset" : 11,
"type" : "<NUM>",
"position" : 2
},
{
"token" : "fj3",
"start_offset" : 13,
"end_offset" : 16,
"type" : "<ALPHANUM>",
"position" : 3
}
]
}
我们可以这样查
POST /products/_search
{
"query": {
"term": {
"productID": {
"value": "xhdk"
}
}
}
}
小写的xhdk
这个可以匹配分词后的内容,所以可以查到结果,那我们如何精确匹配呢?
对"XHDK-A-1293-#fJ3"
这个进行了分词并且转小写的处理,我们拿着"XHDK-A-1293-#fJ3"
这个原本的值去查肯定查不到,我们得通过keyword
去查才行
POST /products/_search
{
"query": {
"term": {
"productID.keyword": {
"value": "XHDK-A-1293-#fJ3"
}
}
}
}
Term
查询不会做分词
如果想要完全匹配,可以采用es
中的一个多字段属性,它默认会把text
类型的字段增加一个keyword
的字段,通过keyword
字段就可以进行完全匹配了
1.3 复合查询-Constant Score
转为Filter
Term
查询还是会返回相应的算分结果的,那如果我们想跳过算分结果该如何做呢?
- 将
Query
转为Filter
,忽略TF-IDF
计算,避免相关性算分的开销 Filter
可以有效利用缓存
1.4 基于全文的查询
- 基于全文的查找
Match Query
/Match Phrase Query
/Query String Query
- 特点
- 索引和搜索时都会进行分词,查询字符串先传递到一个合适的分词器,然后生成一个供查询的词项列表
- 查询时候,先会对输入的查询进行分词,然后每个词项逐个进行底层的查询,最终结果进行合并。并为每个文档生成一个算分。例如查
"Matrix reloaded"
,会查到包括Matrix
或者reload
的所有结果
1.5 Match Query
查询过程
1.6 总结
- 基于词项的查找 vs 基于全文的查找
- 通过字段
Mapping
控制字段的分词Text
vsKeyword
- 通过参数控制查询的
Precision
&Recall
- 复合查询 -
Constant Score
查询- 即便是对
Keyword
进行Term
查询,同样会进行算分 - 可以将查询转为
Filtering
,取消相关性算分的环节,以提升性能
- 即便是对
2. 结构化搜索
2.1 结构化数据
2.2 ES
中的结构化搜索
- 布尔、时间、日期和数字这类结构化数据:有精确的格式,我们可以对这些格式进行逻辑操作。包括比较数字或时间的范围,或判定两个值的大小
- 结构化的文本可以做精确匹配或者部分匹配
Term
查询/Prefix
前缀查询
- 结构化结果只有"是"或"否"两个值
- 根据场景需要,可以决定结构化搜索是否需要打分
2.3 例子
2.3.1 插入数据
# 结构化搜索,精确匹配
DELETE products
POST /products/_bulk
{"index":{"_id":1}}
{"price":10,"avaliable":true,"date":"2018-01-01","productID":"XHDK-A-1293-#fJ3"}
{"index":{"_id":2}}
{"price":20,"avaliable":true,"date":"2019-01-01","productID":"KDKE-B-9947-#kL5"}
{"index":{"_id":3}}
{"price":30,"avaliable":true,"productID":"XHDK-A-1293-#fJ3"}
{"index":{"_id":4}}
{"price":10,"avaliable":false,"productID":"XHDK-A-1293-#fJ3"}
GET products/_mapping
2.3.2 对布尔值 term 查询,有算分
# 对布尔值 term 查询,有算分
POST /products/_search
{
"profile": "true",
"query": {
"term": {
"avaliable": true
}
}
}
2.3.3 对布尔值 term 查询,通过constant score 转成 filtering,没有算分
# 对布尔值 term 查询,通过constant score 转成 filtering,没有算分
POST products/_search
{
"profile": "true",
"explain": true,
"query": {
"constant_score": {
"filter": {
"term": {
"avaliable": true
}
}
}
}
}
2.3.4 数字Range查询
# 数字Range查询
GET products/_search
{
"query": {
"constant_score": {
"filter": {
"range": {
"price": {
"gte": 20,
"lte": 30
}
}
}
}
}
}
2.3.4 日期range
# 日期range查询
GET products/_search
{
"query": {
"constant_score": {
"filter": {
"range": {
"date": {
"gte": "now-1y"
}
}
}
}
}
}
now-1y:表示的意思是now减去1年(now表示现在,y表示年,1y就表示1年),上面的意思是date大于一年前
字段 | 字段描述 |
---|---|
y | 年 |
M | 月 |
w | 周 |
d | 天 |
H/h | 小时 |
m | 分钟 |
s | 秒 |
2.3.5 Exists 查询,查询文档中不包含某个字段的文档
# Exists 查询,查询文档中不包含某个字段的文档
GET /products/_search
{
"query": {
"constant_score": {
"filter": {
"exists": {
"field": "date"
}
}
}
}
}
2.3.6 多值字段查询
POST /movies/_bulk
{"index":{"_id":1}}
{"title":"Father of the Bridge Part II","year":1995,"gener":"Comedy"}
{"index":{"_id":2}}
{"title":"Dave","year":1993,"gener":["Comedy","Romance"]}
2.3.6.1 处理多值字段,term查询是包含而不是等于
# 处理多值字段,term查询是包含而不是等于
GET /movies/_search
{
"query": {
"constant_score": {
"filter": {
"term": {
"gener.keyword": "Comedy"
}
}
}
}
}
返回了包含"Comedy"的所有文档,那如果我们想在多值字段中精确匹配呢(意思就是只包含这一个值)?我们该怎么做
解决方案:增加一个genre_count
字段进行计数。会在组合bool query
给出解决方法
2.3.6.2 多值字段,term查询如果精确匹配(这里如果不懂,可以先跳过)
{ "tags" : ["search"], "tag_count" : 1 }
{ "tags" : ["search", "open_source"], "tag_count" : 2 }
GET /my_index/my_type/_search
{
"query": {
"constant_score" : {
"filter" : {
"bool" : {
"must" : [
{ "term" : { "tags" : "search" } },
{ "term" : { "tag_count" : 1 } }
]
}
}
}
}
}
2.3 总结
- 结构化数据 & 结构化搜索
- 如果不需要算分,可以通过
Constant Score
,将查询转为Filtering
- 如果不需要算分,可以通过
- 范围查询和
Date Math
- 使用
Exit
查询处理非空NULL
值 - 精确值 & 多值字段的精确查找
Term
查询是包含,不是完全相等。针对多值字段查询要尤其注意
3. 搜索的相关性打分
3.1 相关性和相关性算分
3.2 词频(TF)
3.3 逆文档频率 IDF
3.4 TF-IDF 的概念
3.5 Lucene 中的 TF-IDF 评分公式
3.6 BM25
3.7 定制相似度(Similarity)
3.8 通过Explain API 查看TF-IDF
3.9 Boosting Relevance
3.10 总结
- 什么是相关性 & 相关性算分介绍
- TF-IDF/BM25
- 在Elasticsearch中定制相关度算法的参数
- ES中可以对索引,字段分别设置Boosting参数
4. Query
&Filtering
与多字符串多字段查询
4.1 Query Context
& Filter Context
我们看到很多的系统都支持多个字段的查询,搜索引擎一般也提供基于时间价格等过滤条件,那么ES也是支持的,下面就来介绍ES的高级查询;
ES
高级搜索的功能:支持多项文本输入,针对多个字段进行搜索- 在
ES
中,有Query
和Filter
两种不同的Context
(Context
就是上下文,后面会介绍)Query Context
: 使用Query Context
的查询,搜索结果会进行相关性算分Filter Context
: 使用Filter Context
的查询,结果不会进行算分,从而可以利用缓存(Cache),获得更好的性能
4.2 条件组合
假设我们现在要完成如下查询:
- 假设搜索电影评论中包含了Guitar,用户打分高于3分,同时上映日期要在1993与2000年之间;
这个搜索包含了3段逻辑,分别都是针对不同的字段,评论字段要包含Guitar,用户评分要大于3,上映日期需要在给定的范围,同时包含这三段逻辑,且要有一个好的性能,我们该如何做呢?
这就需要用到es中的复合查询:bool Query
4.3 bool 查询
- 一个
bool
查询,是一个或者多个查询子句的组合- 总共包括4种子句。其中2种会影响算分,2种不影响算分;
- 相关性并不只是全文本检索的专利。也适用于 yes|no 的子句,匹配的子句越多,相关性评分越高。如果多条查询子句被合并为一条复合查询语句,比如
bool
查询,则每个查询子句计算得出的评分会被合并到总的相关性评分种。
子句 | 描述 |
---|---|
must | 必须匹配。贡献算分 |
should | 选择性匹配。贡献算分 |
must_not | Filter Context 查询子句,必须不能匹配 |
filter | Filter Context 必须匹配,但是不贡献算分 |
4.3.1 bool
查询语法
bool
查询种子查询可以任意顺序出现- 可以嵌套多个查询
- 如果你的
bool
查询种,没有must
条件,should
种必须至少满足一条查询
从这里我们回过头去看 2.3.6.2 小节,就很简单了
4.3.2 bool
查询嵌套
- 上面就实现了一个
should_not
的逻辑(虽然没有should_not
,但是我们可以这样实现)
4.3.3 bool
查询语句的结构,会对相关度算分产生影响
- 同一级下的竞争字段,具有相同的权重;
- 通过嵌套
bool
查询,可以改变对算分的影响;
4.3.3.1 控制字段Boosting
Boosting
是控制相关度的一种手段Boosting
可以用在索引,字段或查询子条件种
- 参数
boost
的含义- 当
boost
> 1 时,打分的相关度相对性提升; - 当 0 <
boost
< 1时,打分的权重相对性降低; - 当
boost
< 0 时,贡献负分;
- 当
- 插入数据
# 控制字段`Boosting`
POST /blogs/_bulk
{"index":{"_id":1}}
{"title":"Apple iPad","content":"Apple iPad,Apple iPad"}
{"index":{"_id":2}}
{"title":"Apple iPad,Apple iPad","content":"Apple iPad"}
- 查询1(
title
字段的boost
值比较高)
POST blogs/_search
{
"query": {
"bool": {
"should": [
{
"match": {
"title": {
"query": "apple, ipad",
"boost": 1.1
}
}
},
{
"match": {
"content": {
"query": "apple, ipad",
"boost": 1
}
}
}
]
}
}
}
因为title字段的boost的值比较高所以它的权重就比较大,所以文档2显示在最前面,因为文档2种title的值含有两个apple ipad
- 查询2(
content
字段的boost
值比较高)
POST blogs/_search
{
"query": {
"bool": {
"should": [
{
"match": {
"title": {
"query": "apple, ipad",
"boost": 1
}
}
},
{
"match": {
"content": {
"query": "apple, ipad",
"boost": 2
}
}
}
]
}
}
}
4.4 总结
Query Context
vsFilter Query
Bool Query
:跟多的组合条件(这里类似sql种where后面跟多个条件)- 查询结构与相关性算分
- 如何控制查询的精准度
Boosting
&Boosting Query
(这里的例子没有记录,视频种有)
5. 单字符串多字段查询:Dis Max Query
5.1 单字符串查询的实例
我们对上面的文档进行单字符串多字段查询,就是将Brown fox
这个单个字符串拿到多个字段种去匹配,上面的例子种是在title
和body
种匹配。
我们来分析一下文档中的内容:
- title
- 文档1中只出现了
Brown
- 文档1中只出现了
- body
- 文档1中出现了
Brown
Brown fox
在文档2中全部出现,并且保持和查询一致的顺序,目测相关性最高
- 文档1中出现了
# 单字符串查询的实例
POST /blogs/_bulk
{"index":{"_id":1}}
{"title":"Quick brown rabbits","content":"Brown rabbits are commonly seen"}
{"index":{"_id":2}}
{"title":"Keppping pets healthy","content":"My quick brown fox eats rabbits on a regular basis"}
# 查询1
POST /blogs/_search
{
"query": {
"bool": {
"should": [
{"match": {"title": "Brown fox"}},
{"match": {"content": "Brown fox"}}
]
}
}
}
奇怪,我们明明分析文档2的相关度应该更高才是,为什么文档1却在前面,且比文档2的算分要高?
5.2 bool
查询的should
查询的算分过程
- 查询
should
语句中的两个查询 - 加和两个查询的评分
- 乘以匹配语句的总数
- 除以所有语句的总数
分析:文档1中的
title
和content
都包含了我们查询的关键字所以对于should
的两个子查询都会匹配到,文档2虽然精准包含了查询的关键字,但是它只出现在content
中并没有出现在title
中,should
查询的子查询只有一个能匹配到,所以文档1的打分就比文档2要高,这就是原因。
5.3 Disjunction Max Query
查询
- 上例中,
title
和content
相互竞争- 不应该将分数简单叠加,而是应该找到单个最佳匹配的字段的评分
Disjunction Max Query
- 将任何与任一查询匹配的文档作为结果返回。采用字段上最匹配的评分最终评分返回
POST /blogs/_search
{
"query": {
"dis_max": {
"queries": [
{"match": {"title": "Quick fox"}},
{"match": {"content": "Quick fox"}}
]
}
}
}
当我们使用Disjunction Max Query
来查询时,因为Disjunction Max Query
采用字段上最匹配的评分最终评分返回,倘若两个文档都是不完全匹配的话,那么他们的算分也是一样的,这中情况该怎么处理呢?
POST /blogs/_bulk
{"index":{"_id":1}}
{"title":"Quick brown rabbits","content":"Brown rabbits are commonly seen"}
{"index":{"_id":2}}
{"title":"Keppping pets healthy","content":"My quick brown fox eats rabbits on a regular basis"}
POST /blogs/_search
{
"query": {
"dis_max": {
"queries": [
{"match": {"title": "Quick pets"}},
{"match": {"content": "Quick pets"}}
]
}
}
}
上述两个文档中都没有完全匹配关键字Quick pets
,那么他们的算分按理应该也是一样的,我们来看看
5.3.1 Tie Breaker
参数
Tie Breaker
是一个介于 0-1 之间的浮点数。0代表使用最佳匹配;1代表所有语句同等重要;Disjunction Max Query
会获得最佳匹配语句的评分_score
- 将其它匹配语句的评分与
Tie Breaker
相乘 - 对以上评分求和并规范化
POST /blogs/_search
{
"query": {
"dis_max": {
"queries": [
{"match": {"title": "Quick pets"}},
{"match": {"content": "Quick pets"}}
],
"tie_breaker": 0.1
}
}
}
6. 单字符串多字段查询:Multi Match
6.1 单字符串多字段查询的三种场景
- 最佳字段(
Best Fields
)- 当字段之间相互竞争,又相互关联。例如
title
和body
这样的字段(上一节有提到这个)。评分来自最匹配字段
- 当字段之间相互竞争,又相互关联。例如
- 多数字段(
Most Fields
)- 处理英文内容时:一种常见的手段是,在主字段(
Engish Analyzer
),抽取词干,加入同义词,以匹配更多的文档。相同的文本,加入子字段(Standard Analyzer
),以提供更加精确的匹配。其他字段作为匹配文档提高相关度的信号。匹配字段越多越好
- 处理英文内容时:一种常见的手段是,在主字段(
- 混合字段(
Cross Field
)- 对于某些实体,例如人名,地址,图书信息。需要在多个字段中确定信息,单个字段只能作为整体的一部分。希望在任何这些列出的字段中找到尽可能多的词
6.2 Multi Match Query
语法格式
Best Fields
是默认类型,可以不用指定Minimum should match
等参数可以传递到生成的query
中
6.3 Multi Match
中的most field
案例
6.3.1 定义索引,插入数据
DELETE title
PUT /titles
{
"mappings": {
"properties": {
"title":{
"type": "text",
"analyzer": "english"
}
}
}
}
POST titles/_bulk
{"index":{"_id":1}}
{"title":"My dog barks"}
{"index":{"_id":2}}
{"title":"I see a lot of barking dogs on the road"}
6.3.2 采用普通的match
查询
GET titles/_search
{
"query": {
"match": {
"title": "barking dogs"
}
}
}
我们分析了文档内容,显然发现第二个文档的相关性更高,但是采用普通的
match
查询,我们发现第一个文档排在前面,这是为什么呢?因为我们在设置mapping
时采用英文分词器,而且第一篇文档的长度短(这里可以百度下),所以第一个文档排在了前面,对于这种情况,我们得做一些优化。
6.3.3 重新定义Mapping
,并插入数据
DELETE titles
PUT /titles
{
"mappings": {
"properties": {
"title":{
"type": "text",
"analyzer": "english",
"fields": {
"std": {
"type": "text",
"analyzer": "standard"
}
}
}
}
}
}
POST titles/_bulk
{"index":{"_id":1}}
{"title":"My dog barks"}
{"index":{"_id":2}}
{"title":"I see a lot of barking dogs on the road"}
- 分析我们的
mapping
定义 - 加入了子字段
std
,并且子字段类型是text
,且采用standard
分词器 - 采用
english
分词器,会按照英文语法分词,而采用standard
分词器,不会针对英文语法分词,这样可以保证数据的精度
6.3.4 采用Multi Query
查询
GET /titles/_search
{
"query": {
"multi_match": {
"query": "barking dogs",
"type": "most_fields",
"fields": ["title","title.std"]
}
}
}
6.3.5 Multi Query
字段权重
- 用广度匹配字段
title
包括尽可能多的文档——以提升召回率——同时又使用字段title.std
作为信号,将相关度更高的文档置于结果顶部 - 每个字段对于最终评分的贡献可以通过自定义值
boost
来控制,比如,使title
字段更为重要,这样同时也降低了其他信号字段的作用
GET /titles/_search
{
"query": {
"multi_match": {
"query": "barking dogs",
"type": "most_fields",
"fields": ["title^10","title.std"]
}
}
}
6.4 Multi Match
中的cross field
(跨字段搜索)案例
- 当我们要在多个字段中查询的时候,我们可能会想到使用
most fields
来实现 - 没错,
most fields
可以一定程度上满足我们的需求,但是有些特殊情况它是不能满足的,比如:我们想要查询的数据同时出现在所有的字段中,most fields
就不能满足,我们使用"operator":"and"
也不能满足(这里我也有些傻傻分不清楚,看下后面的例子吧,这个要想深入理解的话得有具体的场景分析,可以自行百度),我们可以使用copy_to
的方式来解决(之前提到过),但是需要额外的存储空间;
这个时候我们可以使用cross_fields
来实现
6.4.1 插入数据
# cross_fields 案例
PUT address/_doc/1
{
"street": "5 Poland Street",
"city" : "London",
"country": "United Kingdom",
"postcode": "W1V 3DG"
}
6.4.2 使用most_fields
来查询
POST address/_search
{
"query": {
"multi_match": {
"query": "Poland Street W1V",
"type": "most_fields",
"fields": ["street","city","country","postcode"]
}
}
}
可以满足我们的需求
如果我们想所有的字段都出现查询的结果,我们可以使用"operator": "and"
加most_fields
POST address/_search
{
"query": {
"multi_match": {
"query": "Poland Street W1V",
"type": "most_fields",
"operator": "and",
"fields": ["street","city","country","postcode"]
}
}
}
但是,如果我们想期望查询文本中所有的词都在文档中出现,而又不介意在文档中哪些字段中出现,我们可以使用corss_fields+and
POST address/_search
{
"query": {
"multi_match": {
"query": "Poland Street W1V",
"type": "cross_fields",
"operator": "and",
"fields": ["street","city","country","postcode"]
}
}
}
6.5 区分按字段为中心的查询、词条为中心的查询
best_fields
- 适用于多字段查询且查询相同文本;
- 得分取其中一个字段的最高分。
- 可通过
tie_breaker
(取值0~1)将低得分字段的分数引入的最终得分中。 best_fields
可与dis_max
查询互换。ES内部转换为dis_max
查询operator
(此查询中慎用)minimum_should_match
作用于每个字段的子查询内部中。
例如:
"query":"complete conan doyle"
"field":["title","author","characters"]
"type":"best_fields"
"operator":"and"
等价于:
(+title:complete +title:conan +title:doyle) | (+autorh:complete +author:conan +autore:doyle) | (+characters:complete +characters:conan +characters:doyle)
corss_fields
- 适用于期望查询文本中所有的词都在文档中出现,而又不介意在文档中哪些字段中出现。
operator
作用于子查询与子查询之间的连接中- 应用场景:信息被索引时分割到不同字段中,如住址,姓、名。多数情况下
opertaotr
使用and
上述查询等价于:
+(title:complete author:complete charactors:complete) +(title:conan author:conan charators:conan) +(title:doyle author:doyle charactor:doyle)
- most_fields
- 适用于检索多处包含相同文本,但是恩本分析处理方式不同的文档。
- 多数情况下
operator
使用or
,ES
内部转化为bool
查询 - 应用场景:多语言处理
7. Search Template
和Index Alias
查询
7.1 Search Template
:解耦程序和搜索DSL
- 将查询参数化,这样大家就可以各司其职了,你写你的业务逻辑,我优化我的DSL
7.2 Index Alias
实现零停机运维
- 我们可以为索引创建别名
- 我们每天会创建一些新的索引,但是读写的时候,我们是希望他们从一个Index里面读出来,这样我们就可以用到别名了