在ES中,词项搜索也叫term搜索,term就有词项的意思。词项检索的意思就是说我输入一个词汇,在检索的时候不会把你输入的这个词汇做分词,匹配条件就是完整的输入的词汇,但是文档插入的时候该分词还是分词。下面会有例子说明。
全文检索不一样,全文检索就是按照分词插入,分词匹配,分词处理输入条件。
一、基于Term的查询
1、简介
term是表达语义最小的单位,搜索和利用统计语言模型进行自然语言处理都需要处理它。
在ES中term查询包括:Term Query / Range Query / Exists Query / Prefix Query / Wildcard Query(通配符查询)
ES中的Term查询,对输入不做分词(注意只是对输入),会将输入作为一个整体,在倒排索引中查找准确的词项。
而且使用相关度算分公式会为每个包含该词项的文档进行相关度算分,也就是说只要包含这个词的索引,都会算分,可能一个文档出现多次,分就高了。这个后面算分的时候看看。
可以通过Constant Score将查询转换成一个Filtering避免算分,而且可以利用到缓存,提高性能。
2、操作演示
1、准备数据
批量插入一些数据给products,动态mapping
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/_mapping 查看一下索引结构
{
"products" : {
"mappings" : {
"properties" : {
"desc" : { desc自动识别成为text,插入会分词
"type" : "text",
"fields" : {
"keyword" : {
"type" : "keyword",
"ignore_above" : 256
}
}
},
"productID" : { productID自动识别成为text,插入会分词
"type" : "text",
"fields" : {
"keyword" : {
"type" : "keyword",
"ignore_above" : 256
}
}
}
}
}
}
}
2、执行一下term查询
POST /products/_search
{
"query": {
"term": { 指定查询类型为term
"desc": {
"value":"iphone" 这里我们用的是iphone小写的
}
}
}
}
查询结果为:
hits" : {
"total" : {
"value" : 1,
"relation" : "eq"
},
"max_score" : 0.9808292,
"hits" : [
{
"_index" : "products",
"_type" : "_doc",
"_id" : "1",
"_score" : 0.9808292,
"_source" : {
"productID" : "XHDK-A-1293-#fJ3",
"desc" : "iPhone"
}
}
]
}
此时我们能查出结果,然后把查询条件里面的iphone换为iPhone大写的p
看下查询结果
"hits" : {
"total" : {
"value" : 0,
"relation" : "eq"
},
"max_score" : null,
"hits" : [ ]
}
注意,此时就没插出来东西。
我们解释一下上述问题,因为term查询是对查询条件不做分词处理的,就是精确的词项查询,但是你插入的时候也看到了动态mapping把desc识别成了text,插入的时候做了分词,前面说过了,默认分词进去是转小写的,所以此时建立的倒排索引都是小写的,这里你精确匹配的时候存在大写,自然就查不到了。
3、检验一下分词效果
我们使用标准分词验证一下他插入的时候是不是真的转了小写,其实前面讲三大组件的时候说过。
POST /_analyze
{
"analyzer": "standard",
"text": ["iPhone"]
}
分词效果是
{
"tokens" : [
{
"token" : "iphone",
"start_offset" : 0,
"end_offset" : 6,
"type" : "<ALPHANUM>",
"position" : 0
}
]
}
3、再次演示
1、我们以航班号检索一下
POST /products/_search
{
"query": {
"term": {
"productID": {
"value": "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
}
]
}
我们看到他按照空格做了拆分、并且转了小写、所以你输入整个词汇是没有这个分词的,所以你检索不出来。
2、解决方式1
你要是用分词一个的词汇去找,就能找到了,因为他有这个词项的索引
POST /products/_search
{
"query": {
"term": {
"productID": {
"value": "xhdk"
}
}
}
}
3、解决方式2
但是有时候我们就要全部匹配,因为用户想我插入的是全部的,为啥就查不出来了。你是不是在逗我。
而且插入的是个整个词汇,如果具有唯一性,你整个查出了一个。但是你要是分词的字段查可能是多个,这有时候也不符合业务。
此时要怎么处理呢,我们想一下,我们在插入text的类型的时候,他默认给你一个keyword类型的子字段,在ES中keyword是不分词的,所以你直接用keyword这个子字段查就可以了。
POST /products/_search
{
"query": {
"term": {
"productID.keyword": { 这里是语法
"value": "XHDK-A-1293-#fJ3"
}
}
}
}
查询结果如下:
{
"took" : 1,
"timed_out" : false,
"_shards" : {
"total" : 1,
"successful" : 1,
"skipped" : 0,
"failed" : 0
},
"hits" : {
"total" : {
"value" : 1,
"relation" : "eq"
},
"max_score" : 0.9808292, 返回了算法
"hits" : [
{
"_index" : "products",
"_type" : "_doc",
"_id" : "1",
"_score" : 0.9808292,
"_source" : {
"productID" : "XHDK-A-1293-#fJ3",
"desc" : "iPhone"
}
}
]
}
}
4、符合查询-Constant Score 转为Filter
我们上面看到了term查询的时候,返回了查询结果的算分结果,有时候我们不想要这个算分,因为算分影响性能,他多了算分这个步骤,自然耗时。所以就需要忽略TF-IDF(算分),避免相关性算分的开销。
避免算分可以把Query转为Filter查询,Filter还能有效利用缓存,性能更好一些。
POST /products/_search
{
"explain": true, 开启分析计划
"query": {
"constant_score": { 先转为Constant Score
"filter": { Constant Score里面可以使用filter
"term": { 最终查询还是term。
"productID.keyword": "XHDK-A-1293-#fJ3"
}
}
}
}
}
注意,我们最终查询还是term,只是在term的基础上转为了filter,两者结合。查询结级果为:
{
"took" : 3,
"timed_out" : false,
"_shards" : {
"total" : 1,
"successful" : 1,
"skipped" : 0,
"failed" : 0
},
"hits" : {
"total" : {
"value" : 1,
"relation" : "eq"
},
"max_score" : 1.0, 分数不计算了直接就是1
"hits" : [
{
"_shard" : "[products][0]",
"_node" : "wHEn3SGkRz2IuJ2Cl2M1Xw",
"_index" : "products",
"_type" : "_doc",
"_id" : "1",
"_score" : 1.0,
"_source" : {
"productID" : "XHDK-A-1293-#fJ3",
"desc" : "iPhone"
},
"_explanation" : { 查询计划分析
"value" : 1.0,
"description" : "ConstantScore(productID.keyword:XHDK-A-1293-#fJ3)",
"details" : [ ]
}
}
]
}
}
二、基于全文的查询
ES的一大特点就是全文检索,其实是lucence的东西。
1、简介
1、基于全文本的查找
在ES中,Match Query / Match Phrase Query / Query String Query
这些都是全文检索类型的Query
2、特点
- 索引文档(就是插入文档)的时候,以及检索的时候都会进行分词,查询字符串先传递到一个合适的分词器,然后生成一个供查询的词项列表,就是把输入的查询字段先分词得到一个列表。
- 查询的时候,先会对输入的查询进行分词,然后把每个词项逐个进行底层的查询,最终将结果进行合并,并且为每一个文档生成一个算分。例如查"fuck you",会查到包括fuck 或者 you的所有结果。前面我们讲的时候还可以设置操作符and 之类的。
#设置 position_increment_gap
DELETE groups
PUT groups
{
"mappings": {
"properties": {
"names":{
"type": "text",
"position_increment_gap": 0 默认是100,这里设置为0
}
}
}
}
GET groups/_mapping
POST groups/_doc
{
"names": [ "John Water", "Water Smith"]
}
POST groups/_search
{
"query": {
"match_phrase": {
"names": {
"query": "Water Water",
"slop": 100 意思就是中间填充100个位置
}
}
}
}
POST groups/_search
{
"query": {
"match_phrase": {
"names": "Water Smith"
}
}
}
2、多字段检索
对多值字段使用短语匹配时会发生奇怪的事。 想象一下你索引这个文档:
PUT /my_index/groups/1
{
"names": [ "John Abraham", "Lincoln Smith"]
}
然后运行一个对 Abraham Lincoln 的短语查询:
GET /my_index/groups/_search
{
"query": {
"match_phrase": {
"names": "Abraham Lincoln"
}
}
}
令人惊讶的是, 即使 Abraham 和 Lincoln 在 names 数组里属于两个不同的人名, 我们的文档也匹配了查询。 这一切的原因在Elasticsearch数组的索引方式。
在分析 John Abraham 的时候, 产生了如下信息:
Position 1: john
Position 2: abraham
然后在分析 Lincoln Smith 的时候, 产生了:
Position 3: lincoln
Position 4: smith
换句话说, Elasticsearch对以上数组分析生成了与分析单个字符串 John Abraham Lincoln Smith 一样几乎完全相同的语汇单元。 我们的查询示例寻找相邻的 lincoln 和 abraham , 而且这两个词条确实存在,并且它们俩正好相邻, 所以这个查询匹配了。
幸运的是, 在这样的情况下有一种叫做 position_increment_gap 的简单的解决方案, 它在字段映射中配置。
DELETE /my_index/groups/ (1)
PUT /my_index/_mapping/groups (2)
{
"properties": {
"names": {
"type": "string",
"position_increment_gap": 100 配置100的间隔,此时数据的每个元素之间分词的时候,实际上中间是有100的间隔的
}
}
}
首先删除映射 groups 以及这个类型内的所有文档。
然后创建一个有正确值的新的映射 groups 。
position_increment_gap 设置告诉 Elasticsearch 应该为数组中每个新元素增加当前词条 position 的指定值。 所以现在当我们再索引 names 数组时,会产生如下的结果:
Position 1: john
Position 2: abraham
Position 103: lincoln
Position 104: smith
现在我们的短语查询可能无法匹配该文档因为 abraham 和 lincoln 之间的距离为 100 。 要是你还想查出来,那么为了匹配这个文档你必须添加值为 100 的 slop (就是添虫100个间隔,就能匹配到了)。