1

概要

结构化搜索针对日期、时间、数字等结构化数据的搜索,它们有自己的格式,我们可以对它们进行范围,比较大小等逻辑操作,这些逻辑操作得到的结果非黑即白,要么符合条件在结果集里,要么不符合条件在结果集之外,没有那种相似的概念。

前言

结构化搜索将会有大量的搜索实例,我们将"音乐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对过滤器的简单操作:

  1. 根据fitler条件查找匹配的文档,获取document list。如果有多个过滤条件且涉及多个字段,那么就会有多个document list,document list是按倒排索引来的。
  2. 根据document list构建bitset(包含0或1的数组),匹配了是1,没匹配上为0,如[1,0,0,0]。
  3. 迭代所有的bitset,从最稀疏的开始(可以排除到大量的文档),取数组相同位置所有值为1的记录。
  4. 将bitset缓存在内存中,用于提高性能。

filter比query好处是会caching,下次不用查倒排索引,filter大部分情况下在query之前执行query会计算doc对搜索条件的relevance score,还会根据这个score去排序
filter简单过滤出想要的数据,不计算relevance score,也不排序

filter缓存

缓存条件
  1. 最近的256个filter中,某个filter超过一定次数(次数不固定),就会自动缓存这个filter对应的bitset。
  2. filter针对小segment获取的结果,可以不缓存,segment<1000条或segment大小<index总大小的 3%。原因是数据量小,重新扫描很快,太小的segment在后台会自动合并到大的segment中,缓存意义不大
缓存更新

缓存的更新非常智能,增量更新的方式,如果有document新增或修改时,会将新文档加入bitset,而不是删除缓存或整个重新计算。

小结

本篇前半部分使用了大量的示例,可以快速阅读,后面介绍了filter的过滤原理及缓存处理机制,可以了解一下,谢谢。

专注Java高并发、分布式架构,更多技术干货分享与心得,请关注公众号:Java架构社区
可以扫左边二维码添加好友,邀请你加入Java架构社区微信群共同探讨技术
Java架构社区.jpg


黄鹰
46 声望25 粉丝