关系型数据库的查询,针对一个确定的查询语句,要么匹配,要么不匹配,但是全文搜索因其复杂性和不确定性,会匹配到很多模棱两可的结果。关系型数据库的搜索,可归为对属性的搜索,属性内容比较少,同时为了加快检索速度通常还使用数字id来代替属性值,检索者通常比较明确地知道自己想要什么内容。而全文搜索,则可以简单归为对描述的搜索,检索者通常想搜索某个相关的内容,具体能搜索到什么,是不确定的。
针对全文搜索的场景,ElasticSearch是比较流行的一种方案
ElasticSearch是一个基于Lucene的搜索服务器。它提供了一个分布式多用户能力的全文搜索引擎,基于RESTful web接口。Elasticsearch是用Java开发的,并作为Apache许可条款下的开放源码发布,是当前流行的企业级搜索引擎。设计用于云计算中,能够达到实时搜索,稳定,可靠,快速,安装使用方便。 – 《百度百科》
安装
安装教程不再赘述,可参考官方文档
索引的建立
待补充
查询语句
为了便于理解,我们先从关系型数据库的查询说起(结构交互参考这篇文章, 也不再赘述)。
假设我们在ES里数据内容为:
{
"id":1,
"title":"title of text"
},
{
"id":2,
"title":"title of content"
},
{
"id":3,
"title":"title of text part 2"
}
如果想要查询title
为title of text
的记录1
关系型数据库的一个简单的匹配查询为:
SELECT * FROM TABLE_NAME WHERE title="title of text"
,
在ES中,匹配我们主要使用 term
或者match
两个关键字, 根据定义,term表示精确匹配(必须包含),match是模糊匹配,只要部分包含即可。
{
"query":{
"term":{
"title":"title of text"
}
}
}
// 匹配数据1,3
{
"query":{
"match":{
"title":"title of text"
}
}
}
// 匹配数据1,2,3
这里和我们在关系型数据库的理解模式有些出入,
- term为何匹配到了记录3
- match为何匹配到了记录2
要解释清楚这个问题,需要稍微介绍一下ES的查询机制
ES查询机制
倒排索引
ES使用到一种名为倒排索引的索引进行快速的检索工作,我们通常理解和接触到的数据库索引,是某一个字段内容映射到某一行记录的数据库索引,倒排索引,简单来说是词语到文章的映射。以上面的几行简单的数据分析,首先将内容分词
[‘title’,‘of’,‘text’,‘title’,‘content’,‘of’,‘part’,‘2’]
然后,of明显是无实际含义的虚词,需要过滤掉,然后建立一个词->文章的索引映射表
这里牵涉到好几个步骤:
- 需要将原来的整篇文章进行分词
- 过滤掉"的","非常"这些无实际含义的虚词
- 可能还会有一些同义词的替换,如 英语单词的复数,时态,大小写等。
经历这些步骤提取出关键词后就可以建立出单词到内容的倒排索引了:
关键词 | 内容id |
---|---|
title | 1,2,3 |
text | 1,3 |
content | 2 |
part | 3 |
2 | 3 |
当然这只是一个倒排索引的简单实例,实际上倒排索引的内容可以存放更多类似于,在文档中的词频,以及出现的位置等信息。
查询分析器
既然倒排索引是建立词->文章的索引,那么我们的查询语句,也要分解成一个个的单词才能利用起来这个倒排索引,ES会将输入的查询语句进行分词,然后查询这些分词有哪些对应的文档,最后对这些文档进行相关性计分后排序返回(评分机制相关后面再说)。
这其中,term和match的区别在于:term查询结果的文档,必须包含查询的每一个分词才会匹配
match分词则只要有一个分词命中都会匹配(当然匹配分词数多少会对相关性得分有影响)。
结果的匹配
那么回到我们原来的问题,
term查询,将会分词为[‘title’,‘text’],而且必须同时包含这两个词;
match查询,同样将会分词为[‘title’,‘text’],但只要包含其中一个词,就会作为匹配结果返回。
词语之间是没顺序的,意味着就算调换文章的词语顺序,匹配结果不变。
match、term匹配是基于词语的, 不是基于句子的。
可能你要问了,那我就是想要和关系数据库的=号那样进行完全匹配怎么办?
我们可以给title这个字段设置为不需要分词:
PUT /index_name
{
"mappings" : {
"object" : {
"properties" : {
"title" : {
"type" : "string",
"index" : "not_analyzed"
}
}
}
}
}
// 注:object, title 为具体数据相关的字段
指定title的属性为not_analyzed
后,查询时,分析器不会再将查询内容进行分词分析处理,会直接精确匹配返回结果。(注:数字型和日期型的字段默认使用全精确匹配)
打分策略
可能你已经注意到,每个搜索结果里都会有一个属性_score
,这个值代表着ES计算出来此篇记录与搜索条件的相关性得分,和搜索引擎一样,ES会将匹配到的结果进行相关性得分计算,再将结果倒序返回(默认情况下)。
举一个极端的例子:搜索一个字“的”,可预见的是几乎所有的文章都会使用到这个字(假设这个词没有被当成停用词),也就是说所有的文章都可以做为一个相关的查询结果返回,这个时候,我们需要一个相关性的评分来判断查询结果的相关性,相关性越高的认为是最接近我们理想的结果。
打分的总规则
ES使用了 检索词频率/反向文档频率(简称TD/IDF)的算法来计算搜索结果的相关性,主要包括了三条原则:
- 搜索的词在文档中出现的次数越多,相关性越高
- 搜索的词在所有文档中出现的次数越少,相关性越高
- 搜索的词占文档总长度比例越大,相关性越高
这里简单介绍,后面会另开文章专门介绍
ES基本上就是以这三条规则来计算匹配结果的相似性的,与关系型数据库的优化搜索专注于提升检索速度有所区别,ES优化搜索更多的是让理论上更相关的记录获取到更高的评分。
布尔匹配
上面介绍了简单的匹配查询,下面来简单说说布尔匹配,在关系型数据库里就是 and
,or
的组合操作,
ES里使用bool
关键字进行布尔匹配查询,支持must
,must_not
,should
三种操作
{
"bool":{
"must":[
"term":{"title":"title"}
],
"must_not":[
"term":{"title":"content"}
],
"should":[
{"term":{"title":"part"}},
{"term":{"title":"exists"}},
]
}
}
以上查询含义为,title 字段必须包含 title,必须不包含content, 可选条件为包含’part’,‘exists’。预期返回的记录为1,3
需要说明的是,三种语句为可选的,不是必须全部出现,而且should语句没有任何一条满足的也不影响(但是满足了会有相关性得分加成),must语句有匹配即有返回。除非没有must语句,那么should语句则必须满足其一。
限制匹配门槛
有时候我们输入的查询语句会比较长,切分出来的分词可能有超过5个,我们希望查询到匹配超过3个词的记录,
因此我们不能使用term,但是只匹配一个词的情况我们又想排除掉,因为关联性不高。这个时候我们可以使用minimum_should_match
关键词,来限制匹配的门槛,这个参数值可以是数字,也可以是百分比,甚至可以是两者组合,非常灵活。
上面的例子,我们想要至少匹配3个单词的查询语句为:
{
"query":{
"match":{
"title":{
"query": "title of text and content",
"minimum_should_match": 3,
}
}
}
}
// 注:为了使用minimum_should_match,改写了title的value内容结构。
在这里例子中,minimum_should_match值等价于60%(5*60%=3)(假设of,and都属于有效分词),
甚至可以使用更复杂的表达式:2<50%, 表示如果总词语小于等于2个,则查询词语需要全匹配,否则使用百分比为匹配门槛,
(更详细的内容可以参考 Minimum Should Match)
范围查询
{
"query":{
"range":{
"age":{
"gte":20,
"lt":25
}
}
}
}
语法如上,查询可同时提供包含(inclusive)和不包含(exclusive)这两种范围表达式,可供组合的选项如下:
gt: > 大于(greater than)
lt: < 小于(less than)
gte: >= 大于或等于(greater than or equal to)
lte: <= 小于或等于(less than or equal to)
除了数值匹配外,还支持时间匹配,以及类似php语言内strtotime()方法来描述
时间格式参考文档
设置查询权重
有些时候,我们比较侧重于某个关键词汇,希望增加某个匹配的重要性,例如,想查找名为John的人,并且年龄为18岁。
显然名字会比年龄有着更高的权重。 对于这种情况,我们可以使用boost属性来设置一个查询的权重(默认值为1)。
{
"query":{
"bool":{
"should":[
{
"match":{
"name":{
"query":"John",
"boost":3
},
}
},
{
"match":{
"name":{
"age":18,
"boost":1
},
}
}
]
}
}
}
如上,名字匹配的权重是年龄的3倍,相应计算匹配得分的时候会比age匹配也高很多,所有查询的权重都一致。
短语匹配
假设我们有两篇攻略文章,“妲己如何对抗甄姬”,“甄姬如何对抗妲己”;然后我们使用match搜索妲己如何对抗甄姬
,会两篇内容都搜索到并且他们的相关度得分一致!因为两个文章都包含了搜索的所有词汇。 这显然与我们的期望不符。 这个问题我们可以通过短语匹配搜索来解决。
短语匹配可以通过两种查询来实现:
{
"query":{
"match_phrase":{
"title":"妲己如何对抗甄姬"
}
}
}
或
{
"query":{
"match":{
"title":{
"query":"妲己如何对抗甄姬",
"type":"phrase"
}
}
}
这样,我们可以搜索到预期的结果。这样似乎就可以解决这个问题了,然而事情并没有这么简单!
短语搜索的要求是分词出现的顺序,以及相对位置都必须一致才可以匹配。
上面的查询是完全匹配,假设ES切词为妲己、如何、对抗、甄姬
,则除了匹配这几个词之外,ES还要求这些词相隔的距离一致才算匹配(相隔距离指词语之间其他分词的个数)。这样一想匹配条件就很苛刻了。假设搜索语句变成妲己对抗甄姬
,就搜索不到任何结果了。
解决办法就是放宽短语匹配限制要求。
放宽短语搜索的限制
我们使用slop
参数来调整短语匹配的准确度:
{
"query":{
"match_phrase":{
"title":{
"query":"妲己如何对抗甄姬",
"slop":1
}
}
}
slop参数代表的含义是:被搜索的文档 与短句相关的所有词条必须在经过slop次以内的调整后可以与搜索词条完全一致。讲起来有点抽象,举个例子:
假设搜索的是妲己对抗甄姬
,切词结果为妲己,对抗,甄姬
,而目标文档,切词为妲己,如何,对抗,甄姬
,妲己
与对抗
之间多出了一个词,位置不匹配。
那么需要设置slop为多少呢?答案就是1,我们可以理解为移动一个词语就是消耗了一个单位的slop,
把我们搜索的分词,移动一步变成妲己,[], 对抗,甄姬
, 就可以匹配到目标答案了。
留下一个问题:妲己如何对抗甄姬
需要设置slop为多少,才能匹配到甄姬如何对抗妲己
呢?
总结
这篇文章主要精炼了ES的一些基本用法,可以满足一般场景的搜索需要,下一篇会详细一点介绍ES的相关度评分规则。