ElasticSearch Query

1. 查询语句 DSL

1.1 查询语句

  • 叶查询子句: 在特定的字段上查找特定的值,比如match、term、range,可以单独使用
  • 复合查询子句: 复杂查询可以包含叶子或者其它的复杂查询语句,用于组合成复杂的查询语句,比如not、bool、constant_score等
    • 一条复合语句可以合并任何其它查询语句,包括复合语句,这就意味着,复合语句之间可以互相嵌套,因此可以表达非常复杂的逻辑
    • 因此可以说一条复合语句可以将多条语句(叶子语句和其它复合语句),合并成一个单一的查询语句
    • 具体实例,一个bool连接了must和should,而should里又再写了一层bool去组合下面的must和must_not
         {
             "bool": {
                 "must": {
                     "match": {
                         "email": "business opportunity"
                     }
                 },
                 "should": [
                     {"match": {"starred": true}},
                     {
                         "bool": {
                             "must": {"match": {"folder": "inbox"}},
                             "must_not": {"match": {"spam": true}}
                         }
                     }
                 ]
             }
         }
      

      1.2 查询类型

      查询包含叶子查询子句和复合查询子句这两种子句
  • 查询方式有两种,包括query查询和filter过滤
  • query查询 (又称为评分查询 scoring query)
    • 在query查询中,会关注 "这个文档匹不匹配查询条件,它的相关性高吗?"
    • 用于检查内容与条件是否匹配,并且计算_score表示匹配度,返回的结果_score大的在前面,越大表示越匹配
  • filter过滤 (又称为不评分查询 non-scoring query)
    • 在filter过滤中,只关注 "这个文档是否匹配",而不计算匹配得分
    • 因此他主要用于过滤 "结构化"的数据,而不是过滤全文搜索
    • 使用filter往往会被ElasticSearch自动缓存来提高性能
    • 需要搭配 boolconstant_scorenot这种复合查询语句使用
  • 原则上,使用查询(query)语句来进行全文搜索或者其它任何需要影响相关性得分的搜索,除此以外的情况都使用过滤(filter)

1.3 查询的返回结果

  • 首先执行一个最简单的查询,查询book索引下所有类型的全部数据
    POST 127.0.0.1:9200/book/_search
    {
         "query":{
             "match_all":{}
         }
     }
    
  • 返回结果

    • 返回结果中最重要的是hits
    • hits里面的total字段表示总共匹配到的文档总数,但一个hits数组预设只会包含所查询结果的前十个文档
    • 如果要看后面的文档数据,需要自己指定from、size,来查找后面的文档数据
    • max_score值是查询出来所有匹配文档的_score的最大值,不是当前这10笔的最大值
    • took表示执行整个搜索请求耗费了多少毫秒
    • timed_out表示查询是否超时
      • 默认情况下,搜索请求不会超时
      • 如果低响应时间比完成结果更重要,可以指定timeout为10ms(10毫秒)或是1s(1秒)
    • _shards表示此次查询中参与分片的总数,以及这些分片成功了多少个失败了多少个

      {
         "took": 5,
         "timed_out": false,
         "_shards": {
             "total": 3,
             "successful": 3,
             "failed": 0
         },
         "hits": {
             "total": 3,
             "max_score": 1,
             "hits": [
                 {
                     "_index": "people",
                     "_type": "man",
                     "_id": "3",
                     "_score": 1,
                     "_source": {
                         "name": "瓦力2",
                         "country": "China",
                         "age": 30,
                         "date": "1987-03-07"
                     }
                 },
                 ... 
             ]
         }
      }
      

2.查询时搭配的方法 from、size、sort

  • 使from、size指定返回的数据、以及从哪里返回
    • 和mysql的pageSize、pageNum使用方法一样
    • 注意ES为了避免深分页造成的性能问题,from+size超过10000就不能使用分页,要改使用游标scroll进查询
  • 使sort决定排序
    • 排序条件的顺序是很重要的,会先按照第一个条件排序,仅当结果集的第一个 sort 值完全相同时才会按照第二个条件进排序,以此类推
    • sort的字段支持两种写法,一种是在字段名后接一个对象,另一种是直接接上sort的方法
  • 当个字段的内容有多个值时,系统支持一些计算进行排序,包括min、max、sum、avg、median(中间值)

  • 对缺省字段的排序 missing

    • 在排序时,可以指定missing field要放在整个排序的最前_first还是最后_last(默认)
         POST 127.0.0.1:9200/mytest/doc/_search
             {
                 "query": {
                     "match_all": {}
                 },
                 "sort": {
                     "color": {
                         "missing": "_first" // 使用_first就是将字段缺失的文档放在最前面,_last就是放在最后面(另外missing可以单独使用,也可以跟著order一起使用)
                     }
                 }
             }
      
    • missing的底层实现,其实就是先给这缺省的文档一个很大的负数,然后让他们跟普通的文档一样正常排序,因此sort的返回值会是一个很大的负数
      • 一次使用多维sort的话,要注意missing对sort的影响
      • 假设第一维是price ,第二维是number ,只要某个文档的price有缺,因此他就会在price的排序上得到一个很大的负数,所以他跟谁比price都会输,所以此文档就会排在最后面
      • 而如果某个文档是number有缺,那么就是number的排序上会得到一个很大的负数,因此他可以像正常文档一样先去比较price,当price相同时,才比较number(因为他的number是很大的负数,因此必输),所以此文档不一定会排在最后面

3.叶子查询子句 match、match_phrase、match_all、multi_match、term、terms、range、exists

  • 叶子查询子句分为两种
    • 全文本查询(用来查text类型的字段)
    • 字段级别查询(针对结构化的数据,像是keyword、integer、date...)

3.1 全文本查询: 针对文本类型 text 数据做查询

  • match 模糊匹配
    • 如果在一个text字段上使用match查询,在执行查询前,它将用正确的分词器去分析查询字符,分词完之后才去倒排索引做查询
    • 像是查询author有包含John Smith的,其中Johm Smith会被分词,会被分成John、Smith,则author是 John、Smith、John Smith、Alan Smith...,都会被搜出来,但是会依照匹配程度不同而有不同的_score
  • match_phrase 短语匹配
    • match_phrase 类似于 match 查询,match_phrase首先也会去分词,但是在查找时,他只会保留那些包含 "全部" 搜索词项,且"位置"与搜索词项相同的文档
    • 因此使用match_phrase查询author为John Smith时,也会先分词成John、Smith,但是只有包含这两个词、且顺序一致(John在Smith前面)的文档才会被搜出来,所以只有author是 JohnSmith、John White Smith、John xxx yyy Smith会被搜索出来,而 John、Alan Smith、SmithJohn则不会被搜出来
  • match_all 全部匹配
    • match_all 匹配所有文档,在没有指定查询方式时,它是默认的查询,会找出所有的文档,且全部的文档的_score都是1
  • multi_match 多个字段同时做模糊匹配
    • 同时在多个字段里同时做match模糊查询
    • author、title里面有包含瓦力(模糊匹配)的都会被搜出来
         POST 127.0.0.1:9200/book/_search
             {
                 "query": {
                     "multi_match": {
                         "query": "瓦力",
                         "fields": ["author","title"]
                     }
                 }
             }
      
    • 多个字段模糊匹配的评分问题
      • 因为多个字段同时模糊匹配,怎么去计算_score就会变得比较复杂,有三种方式可以选
        • 如果希望完全匹配的文档占的评分比较高,则使用best_fields
        • 如果希望越多字段匹配的文档评分越高,就要使用most_fields
        • 如果希望这个词条的分词词汇是分散到不同字段中的,那么就使用cross_fields

3.2 字段级别查询 : 针对结构化数据 keyword、integer、date... 做查询

  • term 完全匹配
    • term是代表完全匹配,即不进行分词器分析
    • 查询word_count是1000的书籍
         POST 127.0.0.1:9200/book/_search
         {
             "query": {
                 "term": {"word_count": 1000}
             }
         }
      
  • terms 多值完全匹配
    • terms 查询和 term 查询一样,但它允许你指定多值进行匹配
    • 如果这个字段包含了指定值中的任何一个值,那么这个文档就满足条件,能够被查出来
    • 查询tag中,只要有包含北京、上海、深圳其中一个词的文档,都会被查出来range
         POST 127.0.0.1:9200/book/_search
         {
             "query": {
             "term": {
                 "tag": ["北京","上海","深圳"]
             }
             }
         }
      
  • range范围查询
    • range查询找出那些落在指定区间内的数字或者时间
      • gt: 大于
      • gte: 大于等于
      • lt: 小于
      • lte: 小于等于
  • query_string 语法查询
    • 语法查询是根据一定的语法规则进行的查询,支持通配符、范围查询、正则表达式...,语法表达比较丰富,他会自动解析查询的字段,如果是text就会分词,如果是keyword...就不会分词
      • 不过ES官方并不推荐用query_string查询,因此虽然能用,但是实际上却很少用到

3.3 通用查询

  • exists 查询某字段是否存在
    • ES判断基淮 : ""空字符串算存在,[]一个数组里没有对象属于不存在,null也是不存在
    • 假设有一个索引,其中color是一个keyword类型,id是一个integer类型,先在索引中插入7笔文档做淮备
         { "id": "1", "color": "red" }
         { "id": "2", "color": "green" }
         { "id": "3", "color": "" }
         { "id": "4", "color": null }
         { "id": "5", "color": ["blue"] }
         { "id": "6", "color": [""] }
         { "id": "7", "color": [] }
      
    • 如果执行exists查询,则只有以下5笔文档会被查出来
         {
             "hits": [
                 {"id": "1","color": "red"},
                 {"id": "2","color": "green"},
                 {"id": "3","color": ""},
                 {"id": "5","color": ["blue"]},
                 {"id": "6","color": [""]}
             ]
         }
         // 因为 { "color": [] } 和 { "color": null } 的color字段属于不存在
         // 所以他们的文档不会被搜出来
      

4.复合查询子句bool、constant_score

4.1 bool查询

4.1.1 bool组合查询

  • 如果我们想要请求"author中有瓦力,且title中有ElasticSearch" 类似这样的需求,就需要用到bool组合查询
  • bool组合查询有分两种

    • 一种是放在query里的,称作bool查询
    • 另一种是放在filter里的,称作bool过滤
      • 只要这个bool的上层有filter(不一定是直接上层也可以),那他就是bool过滤
    • bool过滤和bool查询两个最主要的差别在于should代表的意义不一样,这是个大坑,要小心
        GET 127.0.0.1:9200/_search
        {
            "query": {
                "bool": {   //====================> 这个bool是一个bool查询
                    "must": "bool查询的must",
                    "should": "bool查询的should",
                    "filter": {
                        "bool": {   //----------------> 这个bool是一个bool过滤
                            "must": "bool过滤的must",
                            "should": "bool过滤的should",
                            "must_not": {
                                "bool": { //------------> 这个bool也是一个bool过滤
                                    ....
                                }
                            }
                        }
                    }
                }
            }
        }
      
  • bool过滤

    • bool过滤会使用到mustshouldmust_not三种关键词
      • must : 文档必须完全匹配条件
      • should : should里面会带一个以上的条件,文档必须至少满足should里面的一个条件
      • must_not : 文档必须不匹配条件
  • 具体实例

    • 只要author是瓦力,或是title有ElasticSearch就可以 (使用should)
        POST 127.0.0.1:9200/book/_search
        {
            "query": {
                "bool": {
                    "filter": {
                        "bool": {
                            "should": [
                                { "term": { "author": "瓦力"} },
                                { "match": {"title": "ElasticSearch"} }
                            ]
                        }
                    }
                }
            }
        }
      
    • 必须同时满足author是瓦力且title有ElasticSearch (使用must)
        POST 127.0.0.1:9200/book/_search
        {
            "query": {
                "bool": {
                    "filter": {
                        "bool": {
                            "must": [
                                {"term": {"author": "瓦力"}},
                                {"match": {"title": "ElasticSearch"}}
                            ]
                        }
                    }
                }
            }
        }
      
    • author不是瓦力 (使用must_not)
    • 满足author是瓦力,或是author是jack且title中有java,这两种情况的一种就可以(嵌套使用bool)
        POST 127.0.0.1:9200/book/_search
        {
            "query": {
                "bool": {
                    "filter": {
                        "should": [
                            // 情况一 author是瓦力
                            {"term": {"author": "瓦力"}},
                            // 情况二 author是jack且title有java
                            {
                                "bool": [
                                    {"must": {"term": {"author": "jack"}}},
                                    {"must": {"match": {"title": "java"}}}
                                ]
                            }
                        ]
                    }
                }
            }
        }
      

4.1.2 bool查询

  • bool查询会使用到mustshouldmust_notfilter 四种关键词,重点注意should的部份和bool过滤定义不同

    • must: 文档必须完全匹配条件
    • should: 如果满足这些语句中的任意语句,将增加_score,否则,无任何影响,它们主要用于修正每个文档的相关性得分
      • 但有一个特例要注意,如果一个bool查询里没有任何must存在,只有should,那么文档必须符合一个should条件才算匹配
      • bool过滤和bool查询的should差异点就在于,bool过滤的should是会影响返回的文档结果集的,但是bool查询的should通常不会影响返回的结果集(有must在的话),只会影响返回的_score分值
    • must_not: 文档必须不匹配条件
    • filter: 文档必须完全匹配条件,但它以不评分、过滤模式来进行,即是这些语句对评分没有贡献,只是根据过滤标淮来排除或包含文档
  • 具体实例

    • 找出author是瓦力的那些书,如果title有ElasticSearch或是Lucene的话_score加分
      • 由于bool查询的特性,不管title有没有ES或Lucene都会被搜出来(像是java、python也会被搜出来),只是有ES和Lucene的分数会高一点
          POST 127.0.0.1/bool/_search
          {
              "query": {
                  "bool": {
                      "must": {"term": {"author": "瓦力"}},
                      "should": [
                          {"match": {"title": "ElasticSearch"}},
                          {"match": {"title": "Lucene"}}
                      ]
                  }
              }
          }
        
    • 找出title有ElasticSearch或是Lucene的话_score加分
      • 注意此bool查询没有must,因此只有title包含ElasticSearch或是包含Lucene的文档才会被搜出来 (java、python不会被搜出来)
          POST 127.0.0.1/bool/_search
          {
              "query": {
                  "bool": {
                      "should": [
                          {"match": {"title": "ElasticSearch"}},
                          {"match": {"title": "Lucene"}}
                      ]
                  }
              }
          }
        

4.2 constant_score 固定分数查询

  • 把查询出来的 _score 固定为一个constant,不使用ES默认的评分标淮
    • 使用boost来控制固定的score大小
    • 查询author为瓦力的书籍,且固定他们的_score为2
        POST 127.0.0.1:9200/book/_search
        {
            "query": {
                "constant_score": {
                    "filter": {
                        "term": {"author": "瓦力"}
                    },
                    "boost": 2
                }
            }
        }
      

5.游标scroll

5.1 scroll

  • ES为了避免深分页,不允许使用分页(from&size)查询10000条以后的数据,因此如果要查询第10000条以后的数据,要使用ES提供的 scroll(游标) 来查询
    • 假设取的页数较大时(深分页),如请求第20页,Elasticsearch不得不取出所有分片上的第1页到第20页的所有文档,并做排序,最终再取出from后的size条结果作为最终的返回值
    • 假设你有16个分片,则需要在coordinate node汇总到 shards (from+size)条记录,即需要16(20+10)记录后做一次全局排序
    • 所以,当索引非常非常大(千万或亿),是无法使用from + size 做深分页的,分页越深则越容易OOM,即便不OOM,也很消耗CPU和内存资源
    • 因此ES使用index.max_result_window:10000作为为保护措施,即默认 from + size 不能超过10000,虽然这个参数可以动态修改,也可以在配置文件配置,但是最好不要这么做,应该改用ES游标来取得数据
  • scroll游标原理

    • 可以把 scroll 理解为关系型数据库里的cursor,因此,scroll并不适合用来做实时搜索,而更适用于后台批处理任务,比如群发
    • scroll 具体分为初始化和遍历两步
      • 初始化时将所有符合搜索条件的搜索结果缓存起来,可以想象成快照
      • 在遍历时,从这个快照里取数据
      • 也就是说,在初始化后对索引插入、删除、更新数据都不会影响遍历结果
    • 游标可以增加性能的原因,是因为如果做深分页,每次搜索都必须重新排序,非常浪费,使用scroll就是一次把要用的数据都排完了,分批取出,因此比使用from+size还好
  • 具体实例

    • 初始化
      • 请求注意要在URL中的search后加上scroll=1m,不能写在request body中,其中1m表示这个游标要保持开启1分钟
      • 可以指定size大小,就是每次回传几笔数据,当回传到没有数据时,仍会返回200成功,只是hits里的hits会是空list
      • 在初始化时除了回传_scroll_id,也会回传前100笔(假设size=100)的数据
      • request body和一般搜索一样,因此可以说在初始化的过程中,除了加上scroll设置游标开启时间之外,其他的都跟一般的搜寻没有两样 (要设置查询条件,也会回传前size笔的数据)
          POST 127.0.0.1:9200/my_index/_search?scroll=1m
          {
              "query": {
                  "range": {
                      "createTime": {"gte": 1522229999999}
                  }
              },
              "size": 1000
          }
        
        • 返回结果
          {
            "_scroll_id":"XXX"
            ...
          }
          
    • 遍历数据
      • 请求
        • 使用初始化返回的_scroll_id来进行请求,每一次请求都会继续返回初始化中未读完数据,并且会返回一个_scroll_id,这个_scroll_id可能会改变,因此每一次请求应该带上上一次请求返回的_scroll_id
        • 要注意返回的是_scroll_id,但是放在请求里的是scroll_id,两者拼写上有不同
        • 且每次发送scroll请求时,都要再重新刷新这个scroll的开启时间,以防不小心超时导致数据取得不完整
          POST 127.0.0.1:9200/_search/scroll?scroll=1m
          {
            "scroll_id":"XXX(上次请求返回的_scroll_id)"
          }
          
  • 优化scroll查询

    • 在一般场景下,scroll通常用来取得需要排序过后的大笔数据,但是有时候数据之间的排序性对我们而言是没有关系的,只要所有数据都能取出来就好,这时能够对scroll进行优化
    • 初始化

      • 使用_doc去sort得出来的结果,这个执行的效率最快,但是数据就不会有排序,适合用在只想取得所有数据的场景

          POST 127.0.0.1:9200/my_index/_search?scroll=1m
          {
              "query": {
                  "match_all": {
        
                  }
              },
              "sort": [
                  "_doc"
              ]
          }
        
  • 清除scroll
    • 虽然我们在设置开启scroll时,设置了一个scroll的存活时间,但是如果能够在使用完顺手关闭,可以提早释放资源,降低ES的负担
        DELETE 127.0.0.1:9200/_search/scroll
        {
            "scroll_id":"XXX"
        }
      

5.2 分页查询

eg: 127.0.0.1:9200/_search?from=0&size=1000000

应对手段 1.x/2.0/2.1版本暂时无法抵挡这种API的错误用法,只能依靠副本先扛过去

// 这个函数会在query/fetch/agg之前执行,验证请求的合法性,可以看到非scroll的情况下根本就没有任何阻挡措施,只要from + size小于integer.MAX就行

public void preProcess() {
    if (!(from() == -1 && size() == -1)) {
        // from and size have been set.
        int numHits = from() + size();
        // 针对from + size没有特殊限制
        if (numHits < 0) {
            String msg = "Result window is too large, from + size must be less than or equal to: [" + Integer.MAX_VALUE + "] but was [" + (((long) from()) + ((long) size())) + "]";
            throw new QueryPhaseExecutionException(this, msg);
        }
    }

    if (query() == null) {
        parsedQuery(ParsedQuery.parsedMatchAllQuery());
    }
    if (queryBoost() != 1.0f) {
        parsedQuery(new ParsedQuery(new FunctionScoreQuery(query(), new BoostScoreFunction(queryBoost)), parsedQuery()));
    }
    Filter searchFilter = searchFilter(types());
    if (searchFilter != null) {
        if (Queries.isConstantMatchAllQuery(query())) {
            Query q = new XConstantScoreQuery(searchFilter);
            q.setBoost(query().getBoost());
            parsedQuery(new ParsedQuery(q, parsedQuery()));
        } else {
            parsedQuery(new ParsedQuery(new XFilteredQuery(query(), searchFilter), parsedQuery()));
        }
    }
}

2.2开始可以通过设置index.max_result_window参数组织size过大的不合理查询,默认值是10000

public void preProcess() {
    if (scrollContext == null) {
        long from = from() == -1 ? 0 : from();
        long size = size() == -1 ? 10 : size();
        long resultWindow = from + size;
        // We need settingsService's view of the settings because its dynamic.
        // indexService's isn't.
        int maxResultWindow = indexService.indexSettings().getAsInt(MAX_RESULT_WINDOW, Defaults.MAX_RESULT_WINDOW);

        if (resultWindow > maxResultWindow) {
            throw new QueryPhaseExecutionException(this,
                    "Result window is too large, from + size must be less than or equal to: [" + maxResultWindow + "] but was ["
                            + resultWindow + "]. See the scroll api for a more efficient way to request large data sets. "
                            + "This limit can be set by changing the [" + DefaultSearchContext.MAX_RESULT_WINDOW
                            + "] index level parameter.");
        }
    }

    // initialize the filtering alias based on the provided filters
    aliasFilter = indexService.aliasesService().aliasFilter(request.filteringAliases());

    if (query() == null) {
        parsedQuery(ParsedQuery.parsedMatchAllQuery());
    }
    if (queryBoost() != 1.0f) {
        parsedQuery(new ParsedQuery(new FunctionScoreQuery(query(), new BoostScoreFunction(queryBoost)), parsedQuery()));
    }
    Query searchFilter = searchFilter(types());
    if (searchFilter != null) {
        if (Queries.isConstantMatchAllQuery(query())) {
            Query q = new ConstantScoreQuery(searchFilter);
            q.setBoost(query().getBoost());
            parsedQuery(new ParsedQuery(q, parsedQuery()));
        } else {
            BooleanQuery filtered = new BooleanQuery.Builder()
                .add(query(), Occur.MUST)
                .add(searchFilter, Occur.FILTER)
                .build();
            parsedQuery(new ParsedQuery(filtered, parsedQuery()));
        }
    }
    try {
        this.query = searcher().rewrite(this.query);
    } catch (IOException e) {
        throw new QueryPhaseExecutionException(this, "Failed to rewrite main query", e);
    }
}

定位原因 这个查询在Elasticsearch内部会转换成Top(N)的search,相当于Top(10000+2000)然后再slice 2000个doc出来

public void execute(SearchContext searchContext) throws QueryPhaseExecutionException {
    ......
    try {
        searchContext.queryResult().from(searchContext.from());
        searchContext.queryResult().size(searchContext.size());

        Query query = searchContext.query();

        TopDocs topDocs;
        int numDocs = searchContext.from() + searchContext.size();  // 取Top(from+size)这么多的doc

        ......
                } else {
                    rescore = !searchContext.rescore().isEmpty();
                    for (RescoreSearchContext rescoreContext : searchContext.rescore()) {
                        numDocs = Math.max(rescoreContext.window(), numDocs);
                    }
                    topDocs = searchContext.searcher().search(query, numDocs);  // Lucene查询大量的doc
                }
            }
        }
        searchContext.queryResult().topDocs(topDocs);
    } catch (Throwable e) {
        throw new QueryPhaseExecutionException(searchContext, "Failed to execute main query", e);
    } finally {
        searchContext.searcher().finishStage(ContextIndexSearcher.Stage.MAIN_QUERY);
    }
    ......
}

短时间内会load大量的doc到内存里,而且GC不掉,对于例子中的查询,相当于做了一个Top(10000000)的操作,内存不够用了

6. 地理座标点 geo_point

6.1 地理位置定义

  • Elasticsearch 提供了两种表示地理位置的方式

    • 用纬度、经度表示的座标点使用geo_point字段类型(较常用)"
      • geo_points允许你找到距离另一个座标点一定范围内的座标点、计算出两点之间的距离来排序或进行相关性打分、或者聚合到显示在地图上的一个网格
    • 另一种是使用GeoJSON 格式定义的复杂地理形状,使用geo_shape字段类型
  • geo_point 经纬度座标格式

    • 在mapping定义时将字段类型定义为geo_point
        PUT 127.0.0.1:9200/attractions
        {
            "mappings": {
                "restaurant": {
                    "properties": {
                        "name": {
                            "type": "text"
                        },
                        "my_location": {
                            "type": "geo_point"
                        }
                    }
                }
            }
        }
      
    • 插入一个经纬座标点,有三种经纬度座标格式可以插入 (官方说要注意此处有坑,数组和字符串的lat/lon是相反的,所以尽量还是用对象插入吧,意义最明确)
      • 对象
          PUT 127.0.0.1:9200/attractions/restaurant/2
          {
              "name": "Pala Pizza",
              "my_location": {
                  "lat": 40.722,
                  "lon": -73.989
              }
          }
        
      • 数组 location : [ lon, lat ]
      • 字符串 location : "lat, lon"

6.2 地理位置查询

  • 有四种地理座标点相关的过滤器可以用来选中或者排除文档

    • geo_bounding_box (矩形过滤): 找出落在指定矩形框中的点,效率最高
    • geo_distance (圆形过滤): 找出与指定位置在给定距离内的点
    • geo_distance_range (环形过滤): 找出与指定点距离在给定最小距离和最大距离之间的点
    • geo_polygon: 找出落在多边形中的点 (这个过滤器使用代价很大)
  • 具体实例

    • geo_bounding_box 矩形过滤
      • 指定一个矩形的顶部、底部、左边界、右边界,然后过滤器只需判断座标的经度是否在左右边界之间,纬度是否在上下边界之间就可以判断这个座标点是否在矩形范围内
      • 可以选择使用top_left + bottom_right或是top_right + bottom_left或是top +bottom + left + right
      • 找出那些位在 lat 40.7~40.8,且lon -73 ~ -74的座标点
          GET 127.0.0.1:9200/attractions/restaurant/_search
          {
              "query": {
                  "bool": {
                      "filter": {
                          "geo_bounding_box": {
                              "my_location": {
                                  "top_left": {
                                      "lat": 40.8,
                                      "lon": -74.0
                                  },
                                  "bottom_right": {
                                      "lat": 40.7,
                                      "lon": -73.0
                                  }
                              }
                          }
                      }
                  }
              }
          }
        
    • geo_distance 圆形过滤
      • 从给定的位置为圆心画一个圆,找出那些地理座标落在其中的文档
        • distance支持的单位 : km、m、cm、mm...
      • 圆形过滤计算代价比矩形过滤贵,为了优化性能,Elasticsearch会先画一个矩形框来围住整个圆形,这样就可以用矩形过滤先排除掉一些完全不可能的文档,然后再对落在矩形内的座标点用地理距离计算方式处理在
        • 使用圆形过滤时,需要思考是否真的要这么精确的距离过滤?通常使用矩形模型bounding box是更更高效的方式,并且往往也能满足应用需求
        • 假设user想找1km内的餐厅,是否不行找了一个1.2km的餐厅给他?这200m真的差距有这么大吗?需要根据实际使用情况做选择
      • 为了提高圆形过滤的性能,在计算两点间的距离时,有多种牺牲性能换取精度的算法
        • arc: 最慢但最精确的计算方式 (默认的计算方式),这种方式把世界当作球体来处理,不过这种方式的精度还是有极限,因为这个世界并不是完全的球体
        • plane: plane的计算方式把地球当成是平坦的,这种方式快一些但是精度略逊,在赤道附近的位置精度最好,而靠近两极则变差
      • 给定一个位置 (40.715, -73.988),找出距离他1km以内的所有点
          GET 127.0.0.1:9200/attractions/restaurant/_search
          {
              "query": {
                  "bool": {
                      "filter": {
                          "geo_distance": {
                              "distance": "1km",
                              "distance_type": "arc",
                              "my_location": {
                                  "lat": 40.715,
                                  "lon": -73.988
                              }
                          }
                      }
                  }
              }
          }
        
    • geo_distance_range 环形过滤
      • geo_distance_rage是一个环状的过滤,它会排除掉落在内圈中的那部分文档,只拿取落在环形范围内的文档
      • 指定到中心点的距离要指定一个最小距离(gt、gte)和一个最大距离(lt、lte)就像使用 range 过滤器一样
      • 给定一个位置 (40.715, -73.988),找出距离他介于1km~2km的那些点
          GET 127.0.0.1: 9200/attractions/restaurant/_search
          {
              "query": {
                  "bool": {
                      "filter": {
                          "geo_distance_range": {
                              "gte": "1km",
                              "lt": "2km",
                              "location": {
                                  "lat": 40.715,
                                  "lon": -73.988
                              }
                          }
                      }
                  }
              }
          }
        
  • 按照距离大小排序

    • 请求
      • 需要决定距离的单位的原因是因为,这些用于排序的距离的值,会设置在每个返回的结果的sort元素中,方便我们取得他们实际上距离那个点多少距离,而此时就必须知道距离的单位
          GET 127.0.0.1:9200/attractions/restaurant/_search
          {
              "query": { ... },
              "sort": [
                  {
                      // 计算每个文档中 my_location 字段与指定的 lat/lon 点间的距离//my_location要先在索引中建为geo_point类型的字段
                      "_geo_distance": {
                          "my_location": {
                              "lat": 40.715,
                              "lon": -73.998
                          },
                          // 将距离以 km 为单位写入到每个返回结果的sort中
                          "unit": "km",
                          "distance_type": "arc",
                          "order": "asc"
                      }
                  }
              ]
          }
        
    • 返回结果
      • 在sort存放的数字,告诉我们此餐厅到指定的位置的距离是0.0842km
          "hits": [
              {
                  "_index": "attractions",
                  "_type": "restaurant",
                  "_id": "2",
                  "_score": null,
                  "_source": {
                      "name": "New Malaysia",
                      "location": {
                          "lat": 40.715,
                          "lon": -73.997
                      }
                  },
                  "sort": [
                      0.08425653647614346
                  ]
              }
          ]
        
  • 按照距离大小打分

    • 有可能距离是决定返回结果排序的唯一重要因素,不过更常见的情况是距离会和其它因素,比如全文检索匹配度、流行程度或者价格一起决定排序结果
    • 因此需要使用function_score中指定方式让我们把这些因子处理后得到一个综合分
    • 另外按距离排序还有个缺点就是性能问题,因为需要对每一个匹配到的文档都进行距离计算,而function_score查询的rescore语句可以限制只对前 n 个结果进行计算,对性能进行优化

7.嵌套对象 nested

7.1 嵌套对象定义

  • 由于在ES中,所有单个文档的增删改都是原子性的操作,因此将相关的实体数据都储存在同一个文档是很好的,且由于所有信息都在一个文档中,因此当我们查询时就没有必要像mysql一样去关联很多张表,只要搜一遍文档就可以查出所有需要的数据,查询效率非常高
  • 因此除了基本数据类型之外,ES也支持使用复杂的数据类型,像是数组、内部对象,而要使用内部对象的话,需要使用nested来定义索引,使文档内可以包含一个内部对象

    • 为什么不用object而要使用nested来定义索引的原因是,obejct类型会使得内部对象的关联性丢失
    • 这是因为Lucene底层其实没有内部对象的概念,所以ES会利用简单的列表储存字段名和值,将object类型的对象层次摊平,再传给Lucene
    • 假设user类型是object,当插入一笔新的数据时,ES会将他转换为下面的内部文档,其中可以看见alice和white的关联性丢失了

        PUT 127.0.0.1/mytest/doc/1
        {
            "group": "fans",
            "user": [
                {"first": "John", "last": "Smith"},
                {"first": "Alice", "last": "White"}
            ]
        }
      
        转换后的内部文档
        {
            "group": "fans",
            "user.first": ["alice","john"],
            "user.last": ["smith","white"]
        }
      
    • 理论上从插入的数据来看,应该搜索 "first为Alice且last为White" 时,这个文档才算符合条件被搜出来,其他的条件都不算符合,但是因为ES把object类型的对象摊平了,所以实际上如果搜索 "first为Alice且last为Smith",这个文档也会当作符合的文档被搜出来,但这样就违反我们的意愿了,我们希望内部对象自己的关联性还是存在的
    • 因此在使用内部对象时,要改使用nested类型来取代object类型 (因为nested类型不会被摊平,下面说明)
  • nested类型就是为了解决object类型在对象数组上丢失关联性的问题的,如果将字段设置为nested类型,那个每一个嵌套对象都会被索引为一个 "隐藏的独立文档"
    • 其本质上就是将数组中的每个对象作为分离出来的隐藏文档进行索引,因此这也意味着每个嵌套对象可以独立于其他对象被查询
    • 假设将上面的例子的user改为nested类型,经过ES转换后的文档如下
        // 嵌套文档1
        {
            "user.first": [ "alice" ],
            "user.last": [ "white" ]
        }
        // 嵌套文档2
        {
            "user.first": [ "john" ],
            "user.last": [ "smith" ]
        }
        // 根文档,或者也可以称为父文档
        {"group": "fans"}
      
    • 在独立索引每一个嵌套对象后,对象中每个字段的相关性得以保留,因此我们查询时,也仅返回那些真正符合条件的文档
    • 不仅如此,由于嵌套文档直接储存在文档内部,因此查询时嵌套文档和根文档的联合成本很低,速度和单独储存几乎一样
    • 但是要注意,查询的时候返回的是整个文档,而不是嵌套文档本身,并且如果要增删改一个嵌套对象,必须把整个文档重新索引才可以

7.2 嵌套对象查询

  • 索引
     PUT 127.0.0.1/mytest
     {
         "mappings": {
             "doc": {
                 "properties": {
                     "group": {"type": "keyword"},
                     "user": {
                         "type": "nested",
                         "properties": {
                             "first": {"type": "keyword"},
                             "last": {"type": "keyword"},
                             "age": {"type": "integer"}
                         }
                     }
                 }
             }
         }
     }
    
  • 准备测试数据
  • 嵌套对象查询 nested

    • 由于嵌套对象被索引在独立的隐藏文档中,因此我们无法直接使用一般的query去查询他,我们必须改使用 "nested查询" 去查询他们
    • nested查询是一个叶子子句,因此外层需要使用query或是bool来包含他,且因为nested查询是一个叶子子句,所以他也可以像一般的叶子子句一样被bool层层嵌套
    • nested查询的内部必须要包含一个path参数,负责指定要用的是哪个nested类型的字段,且要包含一个query,负责进行此嵌套对象内的查询
        GET 127.0.0.1/mytest/doc/_search
        {
            "query": {
                "nested": {
                    "path": "user",
                    "query": {
                        "bool": {
                            "must": [
                                {"term": {"user.first": "amy"}},
                                {"term": {"user.last": "white"}}
                            ]
                        }
                    }
                }
            }
        }
      
  • 嵌套对象的评分 score_mode

    • 假设nested类型的user,储存的是一个数组,那么在进行嵌套查询时,可能会匹配到多个嵌套的文档,而每一个匹配的嵌套文档都有自己的相关度得分
      • 假设有一个文档如下,一个根文档内,包含了3个嵌套文档
      • 当查询 "user.first = July或user.last = Month" 时,第一个嵌套文档的分数最高,第二个嵌套文档次之,第三个嵌套文档的分数最低
        { "first": "July", "last": "Month", "age": 18 },                
        { "first": "Aug", "last": "Month", "age": 22 },                
        { "first": "Monday", "last": "Day", "age": 25 }
        
    • 为了汇集这众多的嵌套文档分数到根文档,就需要设置score_mode来指定怎样去计算这些嵌套文档的总分
      • 默认情况下,根文档的分数是这些嵌套文档分数的平均值,就是默认score_mode = avg
      • 可以透过设置score_mode为avgmaxsumnone (直接返回1.0常数值分数),来控制根文档的得分策略
      • 不过要注意,如果 nested 查询放在一个 filter 子句中,就算定义了 score_mode 也不会生效,因为filter不打分,所以score_mode 就没有任何意义
          GET 127.0.0.1/mytest/doc/_search
          {
              "query": {
                  "nested": {
                      "path": "user",
                      "score_mode": "max",    // 返回最佳匹配嵌套文档的_score给根文档使用
                      "query": {
                          "bool": {
                              "should": [
                                  {"term": {"user.first": "July"}},
                                  {"term": {"user.last": "Month"}}
                              ]
                          }
                      }
                  }
              }
          }
        
  • 使用嵌套对象的字段来排序

     {
         ...
         "sort": {
             "user.age": {
                 "nested": {
                     "path": "user"
                 },
                 "order": "asc"
             }
         }
     }
    

    7.3 Object vs. nested

  • object破坏了数据关系
  • nested更新很麻烦
  • 2.x不推荐nested

results matching ""

    No results matching ""