Elasticsearch系列---结构化搜索
概要
结构化搜索针对日期、时间、数字等结构化数据的搜索,它们有自己的格式,我们可以对它们进行范围,比较大小等逻辑操作,这些逻辑操作得到的结果非黑即白,要么符合条件在结果集里,要么不符合条件在结果集之外,没有那种相似的概念。
前言
结构化搜索将会有大量的搜索实例,我们将"音乐APP"作为主要的案例背景,去开发一些跟音乐APP相关的搜索或数据分析,有助力于我们理解实战的目标,顺带巩固一下学习的知识。
我们将一首歌需要的字段暂定为:
name | code | type | remark |
---|---|---|---|
ID | id | keyword | 文档ID |
歌手 | author | text | |
歌曲名称 | name | text | |
歌词 | content | text | |
语种 | language | text | |
标签 | tags | text | |
歌曲时长 | length | long | 记录秒数 |
喜欢次数 | likes | long | 点击喜欢1次,自增1 |
是否发布 | isRelease | boolean | true已发布,false未发布 |
发布日期 | releaseDate | date |
我们手动定义的索引mapping信息如下:
PUT /music
{
"mappings": {
"children": {
"properties": {
"id": {
"type": "keyword"
},
"author_first_name": {
"type": "text",
"analyzer": "english"
},
"author_last_name": {
"type": "text",
"analyzer": "english"
},
"author": {
"type": "text",
"analyzer": "english",
"fields": {
"keyword": {
"type": "keyword",
"ignore_above": 256
}
}
},
"name": {
"type": "text",
"fields": {
"keyword": {
"type": "keyword",
"ignore_above": 256
}
}
},
"content": {
"type": "text",
"fields": {
"keyword": {
"type": "keyword",
"ignore_above": 256
}
}
},
"language": {
"type": "text",
"analyzer": "english",
"fielddata": true
},
"tags": {
"type": "text",
"analyzer": "english"
},
"length": {
"type": "long"
},
"likes": {
"type": "long"
},
"isRelease": {
"type": "boolean"
},
"releaseDate": {
"type": "date"
}
}
}
}
}
我们预先导入一批数据进去:
POST /music/children/_bulk
{ "index": { "_id": 1 }}
{ "id" : "34116101-7fa2-5630-a1a4-1735e19d2834", "author_first_name":"Peter", "author_last_name":"Gymbo", "author" : "Peter Gymbo", "name": "gymbo", "content":"I hava a friend who loves smile, gymbo is his name", "language":"english", "tags":["enlighten","gymbo","friend"], "length":53, "likes": 5, "isRelease":true, "releaseDate": "2019-12-20" }
{ "index": { "_id": 2 }}
{ "id" : "34117101-54cb-59a1-9b7a-82adb46fa58d", "author_first_name":"John", "author_last_name":"Smith", "author" : "John Smith", "name": "wake me, shark me", "content":"don't let me sleep too late, gonna get up brightly early in the morning", "language":"english", "tags":["wake","early","morning"], "length":55, "likes": 8,"isRelease":true, "releaseDate": "2019-12-21" }
{ "index": { "_id": 3 }}
{ "id" : "34117201-8d01-49d4-a495-69634ae67017", "author_first_name":"Jimmie", "author_last_name":"Davis", "author" : "Jimmie Davis", "name": "you are my sunshine", "content":"you are my sunshine, my only sunshine, you make me happy, when skies are gray", "language":"english", "tags":["sunshine","happy"], "length":65,"likes": 12, "isRelease":true, "releaseDate": "2019-12-22" }
{ "index": { "_id": 4 }}
{ "id" : "55fa74f7-35f3-4313-a678-18c19c918a78", "author_first_name":"Peter", "author_last_name":"Raffi", "author" : "Peter Raffi", "name": "brush your teeth", "content":"When you wake up in the morning it's a quarter to one, and you want to have a little fun You brush your teeth", "language":"english", "tags":"teeth", "length":45,"likes": 17, "isRelease":true, "releaseDate": "2019-12-22" }
{ "index": { "_id": 5 }}
{ "id" : "1740e61c-63da-474f-9058-c2ab3c4f0b0a", "author_first_name":"Jean", "author_last_name":"Ritchie", "author" : "Jean Ritchie", "name": "love somebody", "content":"love somebody, yes I do", "language":"english", "tags":"love", "length":38, "likes": 3,"isRelease":true, "releaseDate": "2019-12-22" }
精确值查找
我们根据文档的mapping设计,可以按ID、按日期进行查找。
根据ID搜索歌曲
GET /music/children/_search
{
"query" : {
"constant_score" : {
"filter" : {
"term" : {
"id" : "34116101-7fa2-5630-a1a4-1735e19d2834"
}
}
}
}
}
注意ID建立时,类型是指定为keyword,这样ID在索引时不会进行分词。如果类型为text,UUID值在索引时会分词,这样反而查不到结果了。
按日期搜索歌曲
GET /music/children/_search
{
"query" : {
"constant_score" : {
"filter" : {
"term" : {
"releaseDate" : "2019-12-21"
}
}
}
}
}
按歌曲时长搜索
GET /music/children/_search
{
"query" : {
"constant_score" : {
"filter" : {
"term" : {
"length" : 53
}
}
}
}
}
搜索已发布的歌曲
GET /music/children/_search
{
"query" : {
"constant_score" : {
"filter" : {
"term" : {
"isRelease" : true
}
}
}
}
}
以上3个小例子可以发现:准确值搜索对keyword、日期、数字、boolean值天然支持。
组合过滤
前面的4个小例子都是单条件过滤的,实际的需求肯定会有多个条件,不过万变不离其宗,再复杂的搜索需求,也是由一个一个的基础条件复合而成的,我们来看几个简单的组合过滤的例子。
复习一下之前学过的逻辑:
- bool 组合多个条件,可以嵌套
- must 必须匹配
- should 可以匹配(类似于or,多个条件在should里)
- must_not 必须不匹配
搜索发布日期为2019-12-20,或歌曲ID为2a8f4288-c0a9-5c9b-8f99-67339b66f4c0,但发布日期不能是2019-12-21的歌曲
GET /music/children/_search
{
"query": {
"constant_score": {
"filter": {
"bool": {
"should": [
{"term":{
"releaseDate":"2019-12-20"
}},
{"term":{
"id":"2a8f4288-c0a9-5c9b-8f99-67339b66f4c0"
}}
],
"must_not": {
"term": {
"releaseDate":"2019-12-21"
}
}
}
}
}
}
}
搜索歌曲ID为2a8f4288-c0a9-5c9b-8f99-67339b66f4c0,或者是歌曲ID为34116101-7fa2-5630-a1a4-1735e19d2834而且发布日期为2019-12-20的帖子
GET /music/children/_search
{
"query": {
"constant_score": {
"filter": {
"bool": {
"should": [
{"term":{
"id":"2a8f4288-c0a9-5c9b-8f99-67339b66f4c0"
}},
{
"bool": {
"must" : [
{
"term" : {
"id":"34116101-7fa2-5630-a1a4-1735e19d2834"
}},
{ "term" : {
"releaseDate":"2019-12-20"
}}
]
}
}
]
}
}
}
}
}
多值搜索
使用语法terms,可以同时搜索多个值,类似mysql的in语句。
GET /music/children/_search
{
"query": {
"constant_score": {
"filter": {
"terms": {
"id": [
"34116101-7fa2-5630-a1a4-1735e19d2834",
"99268c7e-8308-569a-a975-bbce7d3f9a8e"
]
}
}
}
}
}
范围查询
针对Long类型和date类型的数据,是支持范围查询的,使用gt、lt、gte、lte来完成范围的判断。与mysql的>、<、>=、<=以及between...and异曲同工。
搜索时长在45-60秒之间的歌曲
对Long类型的范围查询,直接使用范围表达式:
GET /music/children/_search
{
"query": {
"constant_score": {
"filter": {
"range": {
"length": {
"gte": 45,
"lte": 60
}
}
}
}
}
}
日期的范围搜索
针对日期的范围搜索,除了直接写日期,加上常规的范围表达式之外,还可以使用+1d、-1d表示对指定日期的加减,如"2019-12-21||-1d"表示"2019-12-20",也可以使用now-1d表示昨天,挺有趣。
给个示例:搜索2019-12-21前一天新发布的歌曲
GET /music/children/_search
{
"query": {
"constant_score": {
"filter": {
"range": {
"releaseDate" :{
"gt":"2019-12-21||-1d"
}
}
}
}
}
}
Null值处理
倒排索引在建立时,是不接受空值的,这就意味着null,[],[null]这些各种形式的null值,不无法存入倒排索引的,那这样怎么办?
Elasticsearch提供了两种查询,类似于mysql的is not null和not exists。
存在查询
exists查询,会返回那些指定字段有值的文档,与mysql的is not null类似。
案例中的tags字段,就是一个选填项,有些记录可能是null值,如果我需要查询所有的tags值的记录,请求如下:
GET /music/children/_search
{
"query": {
"constant_score": {
"filter": {
"exists": {
"field": "tags"
}
}
}
}
}
缺失查询
缺失查询原来是有关键字missing表示,效果与exists相反,语法上与mysql的is null类似,但6.x版本就已经废弃了,我们可以改用must not + exists实现相同的效果。
还是使用tags字段为例,查询tags为空的文档:
GET /music/children/_search
{
"query": {
"bool": {
"must_not": {
"exists": {
"field": "tags"
}
}
}
}
}
filter缓存
过滤器为什么效率那么高?除了本身的设计集合来达到高效过滤之外,还将查询结果适当地缓存化。
filter执行原理
我们了解一下Elasticsearch对过滤器的简单操作:
- 根据fitler条件查找匹配的文档,获取document list。如果有多个过滤条件且涉及多个字段,那么就会有多个document list,document list是按倒排索引来的。
- 根据document list构建bitset(包含0或1的数组),匹配了是1,没匹配上为0,如[1,0,0,0]。
- 迭代所有的bitset,从最稀疏的开始(可以排除到大量的文档),取数组相同位置所有值为1的记录。
- 将bitset缓存在内存中,用于提高性能。
filter比query好处是会caching,下次不用查倒排索引,filter大部分情况下在query之前执行query会计算doc对搜索条件的relevance score,还会根据这个score去排序
filter简单过滤出想要的数据,不计算relevance score,也不排序
filter缓存
缓存条件
- 最近的256个filter中,某个filter超过一定次数(次数不固定),就会自动缓存这个filter对应的bitset。
- filter针对小segment获取的结果,可以不缓存,segment<1000条或segment大小<index总大小的 3%。原因是数据量小,重新扫描很快,太小的segment在后台会自动合并到大的segment中,缓存意义不大
缓存更新
缓存的更新非常智能,增量更新的方式,如果有document新增或修改时,会将新文档加入bitset,而不是删除缓存或整个重新计算。
小结
本篇前半部分使用了大量的示例,可以快速阅读,后面介绍了filter的过滤原理及缓存处理机制,可以了解一下,谢谢。
专注Java高并发、分布式架构,更多技术干货分享与心得,请关注公众号:Java架构社区
可以扫左边二维码添加好友,邀请你加入Java架构社区微信群共同探讨技术