es准实时检索原理

在这个动态索引中,有三个关键的索引结构:倒排列表、临时索引、已删除列表。倒排索引是已经建好的索引结果,倒排列表存在磁盘文件中,单词词典在内存中。临时索引是在内存中实时建立的倒排索引,结果与倒排列表一样,只是存在于内存中,当有新文档时,实时解析文档并加到这个临时索引中。已删除列表存储已被删除的文档的文档ID。另外,当一个文档被更改,搜索引擎中一个普遍的做法是删除旧文档,然后新建一个新文档,间接实现更新操作,这么做的原因主要是索引文件存储在磁盘文件,写磁盘不方便。


当用户搜索时,搜索引擎同时到倒排列表和临时索引进行查询,找到包含用户查询的文档集合,并对结果进行合并,之后利用删除文档进行过滤,形成最终结果,返回给用户。这样就实现了动态环境下的准实时搜索功能。



查询DSL


ES 对搜索请求,有简易语法和完整语法两种方式。简易语法作为以后在 Kibana 上最常用的方式,一定是需要学会的。


简易语法查询:


curl -XGET http://127.0.0.1:9200/logstash-2015.06.21/testlog/_search?q=user:"chenlin7"

返回:


{"took":240,"timed_out":false,"_shards":{"total":27,"successful":27,"failed":0},"hits":{"total":1,"max_score":0.11506981,"hits":[{"_index":"logstash-2015.06.21","_type":"testlog","_id":"AU4ew3h2nBE6n0qcyVJK","_score":0.11506981,"_source":{ "date" : "1434966686000", "user" : "chenlin7", "mesg" : "first message into Elasticsearch"}}]}}


querystring 语法


上例中, ?q= 后面写的,就是 querystring 语法。鉴于这部分内容会在 Kibana 上经常使用,这里详细解析一下语法:


  • 全文检索:直接写搜索的单词,如上例中的 first;
  • 单字段的全文检索:在搜索单词之前加上字段名和冒号,比如如果知道单词 first 肯定出现在 mesg 字段,可以写作 mesg:first;
  • 单字段的精确检索:在搜索单词前后加双引号,比如 user:"chenlin7";
  • 多个检索条件的组合:可以使用 NOT, AND 和 OR 来组合检索,注意必须是大写。比如 user:("chenlin7" OR "chenlin") AND NOT mesg:first;
  • 字段是否存在:_exists_:user 表示要求 user 字段存在,_missing_:user 表示要求 user 字段不存在;
  • 通配符:用 ? 表示单字母,* 表示任意个字母。比如 fir?t mess*;
  • 正则:需要比通配符更复杂一点的表达式,可以使用正则。比如 mesg:/mes{2}ages?/。注意 ES 中正则性能很差,而且支持的功能也不是特别强大,尽量不要使用。
  • 近似搜索:用 ~ 表示搜索单词可能有一两个字母写的不对,请 ES 按照相似度返回结果。比如 frist~;
  • 范围搜索:对数值和时间,ES 都可以使用范围搜索,比如:rtt:>300,date:["now-6h" TO "now"} 等。其中,[] 表示端点数值包含在范围内,{} 表示端点数值不包含在范围内;


完整语法


ES 支持各种类型的检索请求,除了可以用 querystring 语法表达的以外,还有很多其他类型,具体列表和示例可参见: https://www.elastic.co/guide/en/elasticsearch/reference/current/query-dsl-queries.html


search 请求参数


  • from

从索引的第几条数据开始返回,默认是 0;


  • size

返回多少条数据,默认是 10。


注意:Elasticsearch 集群实际是需要给 coordinate node 返回  shards number * (from + size)  条数据,然后在单机上进行排序,最后给客户端返回这个 size 大小的数据的。所以请谨慎使用 from 和 size 参数。


此外,动态控制配置项: index.max_result_window ,默认为 10000。即  from + size  大于 10000 的话,Elasticsearch 直接拒绝掉这次请求不进行具体搜索,以保护节点。


另外,Elasticsearch 2.x 还提供了一个小优化:当设置  "size":0  时,自动改变  search_type  为 count。跳过搜索过程的 fetch 阶段。


  • timeout

coordinate node 等待超时时间。到达该阈值后,coordinate node 直接把当前收到的数据返回给客户端,不再继续等待 data node 后续的返回了。


注意:这个参数只是为了配合客户端程序,并不能取消掉 data node 上搜索任务还在继续运行和占用资源。


  • terminate_after

各 data node 上,扫描单个分片时,找到多少条记录后,就认为足够了。这个参数可以切实保护 data node 上搜索任务不会长期运行和占用资源。但是也就意味着搜索范围没有覆盖全部索引,是一个抽样数据。准确率是不好判断的。


  • request_cache

各 data node 上,在分片级别,对请求的响应(仅限于  hits.total  数值、aggregation 和 suggestion 的结果集)做的缓存。注意:这个缓存的键值要求很严格,请求的 JSON 必须一字不易,缓存才能命中。


另外, request_cache  参数不能写在请求 JSON 里,只能以 URL 参数的形式存在。示例如下:

curl -XPOST http://localhost:9200/_search?request_cache=true -d '{ "size" : 0, "timeout" : "120s", "terminate_after" : 1000000, "query" : { "match_all" : {} }, "aggs" : { "terms" : { "terms" : { "field" : "keyname" } } }}'

下面一个比较完整的查询:

{ 

 

    "from" : 0, 

 

    "size" : 10, 

 

    "query" : { 

 

      "bool" : { 

 

        "must" : { 

 

          "terms" : { 

 

            "tag" : [ "redis-test.m6", "3001" ] 

 

          } 

 

        }, 

 

        "filter" : { 

 

          "term" : { 

 

            "source" : { 

 

              "value" : "redis", 

 

              "boost" : 10.0 

 

            } 

 

          } 

 

        }, 

 

        "should" : [ { 

 

          "fuzzy" : { 

 

            "redisCluster" : { 

 

              "value" : "redis--cluster" 

 

            } 

 

          } 

 

        }, { 

 

          "fuzzy" : { 

 

            "redisCluster" : { 

 

              "value" : "redis--cluster" 

 

            } 

 

          } 

 

        }, { 

 

          "wildcard" : { 

 

            "host" : "redis-test.m?" 

 

          } 

 

        }, { 

 

          "query_string" : { 

 

            "query" : "hubble.business.Redisproxy.redisproxy_feedb_compact_sum.READ_OPT host=monitor001 ", 

 

            "fields" : [ "so_monitorKey" ], 

 

            "default_operator" : "and", 

 

            "analyzer" : "standard" 

 

          } 

 

        } ] 

 

      } 

 

    }, 

 

    "post_filter" : { 

 

      "bool" : { 

 

        "must" : { 

 

          "range" : { 

 

            "dateTime" : { 

 

              "from" : 1494983387397, 

 

              "to" : 1494986987397, 

 

              "include_lower" : true, 

 

              "include_upper" : true 

 

            } 

 

          } 

 

        } 

 

      } 

 

    }, 

 

    "_source" : { 

 

      "includes" : [ ], 

 

      "excludes" : [ ] 

 

    }, 

 

    "sort" : [ { 

 

      "dateTime" : { 

 

        "order" : "desc" 

 

      } 

 

    } ], 

 

    "highlight" : { 

 

      "pre_tags" : [ "<span style=\"color:#ff6666\">" ], 

 

      "post_tags" : [ "</span>" ], 

 

      "fields" : { 

 

        "monitorKey" : { }, 

 

        "metric" : { } 

 

      } 

 

    } 

 

  }



java查询API中需要用到的几个组件


查询组件Query


es中JavaAPI中全部的query都要实现QueryBuilder 接口,2.X之前全部的filter都要实现QueryFilterBuilder 接口,但是目前已经取消该接口,filter可以使用QueryBuilder的全部实现类,但是他不会进行打分。


常用的QueryBuilder有哪些:


根据docId进行查询


查询全部数据


最基本的对term进行查询


可以对一个字段进行多个term进行查询,可以设置and/or


boolean 查询,对其他query可以进行按and/or逻辑关系进行封装


模糊查询


正则查询


通配符查询


基于querystring的查询


可以对多个字段同时进行多个term的查询,多个字段公用一个analyzer、and/or逻辑,但可以有自己的boost



其他一些高级查询


基于term的编辑距离的查询


该query可以同时查询多个索引


ConstantScoreQueryBuilder 该query命中的文档的得分会被该query的boost替换


对ip字段进行基于地理位置的距离查询


基于文本的推荐功能,可以设置tf df来进行限制



查询组件Filter


有两种形式:


query的形式,作为查询的一部分,先执行过滤,后执行查询

{ 

 

  "query" : { 

 

  "filtered" : { 

 

  "query" : { 

 

  "match_all" : {} 

 

  }, 

 

  "filter" : { 

 

  "term" : { 

 

  "category" : "book" 

 

  } 

 

  } 

 

  } 

 

  } 

 

  }


过滤的形式,先执行查询,对查询结果在执行过滤

{ 

 

  "query" : { 

 

  "match_all" : {} 

 

  }, 

 

  "post_filter" : { 

 

  "term" : { "category" : "book" } 

 

  } 

 

  }


两种形式那种更好,这要根据情况来分析,filter能将文档限定在比较少的范围就用第一种方式,query能将文档限定在很少的范围,就用第二种,当然query是需要进行打分的,复杂的query即使能限定在比较少的文档范围,打分过程复杂的话,也不如第一种方式的。


filter与query的区别:


1.filter不进行打分,query进行打分


2.filter可以进行缓存,query不可以


filter缓存提高性能:


第一次运行该查询命令后,terms会加载到filter cache中,并与查询命令提供的key关联起来。此外,一旦terms(在本例中是书的id信息)被加载到了缓存中,以后用到该缓存项的查询都不会再次从索引中加载,这意味着ElasticSearch可以通过缓存机制提高查询的效率。


缓存很强大,但实际上ElasticSearch在默认情况下并不会缓存所有的filters。这是因为部分filters会用到域数据缓存(field data cache)。该缓存一般用于按域值排序和faceting操作的场景中。默认情况下,如下的filters不会被缓存:

numeric_range 

 

  script 

 

  geo_bbox 

 

  geo_distance 

 

  geo_distance_range 

 

  geo_polygon 

 

  geo_shape 

 

  and 

 

  or 

 

  not


最后三种filters不会用到域缓存,它们主要用于控制其它的filters,因此它不会被缓存,但是它们控制的filters在用到的时候都已经缓存好了。


也可以使用如下的命令关闭该关键词过滤器的缓存:

{ 

 

  "query" : { 

 

  "filtered" : { 

 

  "query" : { 

 

  "term" : { "name" : "joe" } 

 

  }, 

 

  "filter" : { 

 

  "term" : { 

 

  "year" : 1981, 

 

  "_cache" : false 

 

  } 

 

  } 

 

  } 

 

  } 

 

  }



注意:_source域必须存储,否则terms lookup功能无法使用。


业务场景中用到了terms lookup功能,数据量也不大,推荐用户将索引(本例中是clients索引)只设置一个分片,同时将分片的副本分发到所有含有books索引的节点上。这样做是因为ElasticSearch默认会读取本地的索引数据来避免不必要的网络传输、网络延时,从而提升系统的性能。



ElasticSearch在elasticsearch.yml文件中提供了如下的参数来配置该缓存:


  • `indices.cache.filter.terms.size:`默认值为10mb,指定了ElasticSearch用于terms lookup的缓存的内存的最大容量。在绝大多数场景下,默认值已经足够,但是如果你知道你的将加载大量的数据到缓存,那么就需要增加该值。
  • `indices.cache.filter.terms.expire_after_access:`该属性指定了缓存项最后一次访问到失效的最大时间。默认,该属性关闭,即永不失效。
  • `indices.cache.filter.terms.expire_after_write:`该属性指定了缓存项第一次写入到失效的最大时间。默认,该属性关闭,即永不失效。


如果想了解更多关于LRU缓存的知识,想了解它的工作原理,请参考网页: http://en.wikipedia.org/wiki/Page\_replacement\_algorithm#Least\_recently_used



查询组件Sort

“sort”:[ 

 

  {"_score":{"order" : "desc"}}, 

 

  { "section" : { "order" : "asc", "missing" : "_last" }} 

 

  ]


注意,使用sort之后默认就不会再进行打分了。



查询组件高亮器

HighLightField hf = new HighLightField("monitorKey", "<span style=\"color:#ff6666\">", "</span>"); 

 

  int fragmentSize = -1; 

 

  int numberOfFragments = -1; 

 

  searchRequestBuilder.addHighlightedField(hf.getFieldName(), fragmentSize, numberOfFragments) 

 

  .setHighlighterPreTags(hf.getPreTag()).setHighlighterPostTags(hf.getPostTag());

查询类型


 setSearchType(SearchType searchType):执行检索的方式,主要控制搜索的两个阶段,各个节点上的检索阶段,数据收集阶段。



QUERY_THEN_FETCH


是针对所有的块执行的,但返回的是足够的信息,而不是文档内容(Document)。结果会被排序和分级,基于此,只有相关的块的文档对象会被返回。由于被取到的仅仅是这些,故而返回的hit的大小正好等于指定的size。这对于有许多块的index来说是很便利的(返回结果不会有重复的,因为块被分组了)。



QUERY_AND_FETCH


最原始(也可能是最快的)实现就是简单的在所有相关的shard上执行检索并返回结果。每个shard返回一定尺寸的结果。由于每个shard已经返回了一定尺寸的hit,这种类型实际上是返回多个shard的一定尺寸的结果给调用者。最快,但不准确



DFS_QUERY_THEN_FETCH


与QUERY_THEN_FETCH相同,预期一个初始的散射相伴用来为更准确的score计算分配了的term频率。



DFS_QUERY_AND_FETCH


与QUERY_AND_FETCH相同,预期一个初始的散射相伴用来为更准确的score计算分配了的term频率。SCAN被用来检索大量的结果(甚至所有的结果),就像在传统数据库中使用的游标.全局排序,若禁止排序,无法查询实时数据。官网3.0会弃用



COUNT


只计算结果的数量,不返回具体数据。 2.0之后只有aggregations and like 查询的时候有效



聚合请求


在检索范围确定之后,ES 还支持对结果集做聚合查询,返回更直接的聚合统计结果。在 ES 1.0 版本之前,这个接口叫 Facet,1.0 版本之后,这个接口改为 Aggregation。


Kibana 分别在 v3 中使用 Facet,v4 中使用 Aggregation。不过总的来说,Aggregation 是 Facet 接口的强化升级版本,我们直接了解 Aggregation 即可。


详情api查看:


https://www.elastic.co/guide/en/elasticsearch/reference/current/search-aggregations.html


聚合示例


在 Elasticsearch 1.x 系列中,aggregation 分为 bucket、metric 和 Pipeline 三种,分别用作词元划分、数值计算和管道聚合查询。


比如实现一个时序百分比统计,在 facet 接口就无法直接完成,而在 aggregation 接口就很简单了:

# curl -XPOST 'http://127.0.0.1:9200/hubble-metric-index/_search?size=0&pretty' -d'{ "aggs" : { "percentile_over_time" : { "date_histogram" : { //  bucket 
 中的已时间为间隔的聚合 "field" : "dateTime", "interval" : "1h" }, "aggs" : { //下面是一项metric中的数值计算
 
"avg_grade" : { 
 
"avg" : { "field" : "grade" } //对grade求平均值
 
},
 
"max_grade" : { 
 
"max" : { "field" : "grade" } //对max求平均值
 
},
 
"min_grade" : { 
 
"min" : { "field" : "grade" } //对min求平均值
 
},
 
"sum_grade" : { 
 
"sum" : { "field" : "grade" } //对sum求平均值
 
},
 
"grades_stats" : {
 
"stats" : { "field" : "grade" } //算错 max min sum avg 全部的值
 
}, "percentile_one_time" : { "percentiles" : { //求每个小时所在百分比 "field" : "requesttime" } } } } }}'





数据是关于汽车交易的:汽车型号,制造商,销售价格,销售时间以及一些其他的相关数据。

POST /cars/transactions/_bulk 
 
{ "index": {}} 
 
{ "price" : 10000, "color" : "red", "make" : "honda", "date" : "2014-10-28" } 
 
.....


我们这次建立一个聚合:一个汽车交易商也许希望知道哪种颜色的车卖的最好。这可以通过一个简单的聚合完成。使用terms桶:

GET /cars/transactions/_search?search_type=count 
 
{ 
 
"aggs" : { 
 
"colors" : { 
 
"terms" : { 
 
"field" : "color" //按color进行聚合,并算出每个颜色的个数
 
} ,
 
  "aggs": { 
 
            "avg_price": { 
 
               "avg": { 
 
                  "field": "price" 
 
               } 
 
            } 
 
         } 
 
} 
 
} 
 
}
 
  
 
GET /cars/transactions/_search?search_type=count 
 
{ 
 
"aggs" : { 
 
"range": {
 
"date_range": { //将date分为几个区间,然后进行计算
 
"field": "date",
 
"time_zone": "CET",
 
"format": "yyyy-MM-dd",
 
"ranges": [
 
{ "to": "2016/02/01" }, 
 
{ "from": "2016/02/01", "to" : "now/d" },
 
{ "from": "now/d" }
 
]
 
},
 
  "aggs": { 
 
            "avg_price": { 
 
               "avg": { 
 
                  "field": "price" 
 
               } 
 
            } 
 
         } 
 
} 
 
} 
 
}



Elasticsearch aggs Cardinality 不准的问题


ES中“去重"是基于基数计算的(HLL),会有误差的。cardinality有个参数 "precision_threshold": 100 ,100是个预设值,你的真实值小于100计算出来的值就是正确的,真实值大于100计算出来的值就是模糊的,100可以自定义。

{
 
"aggs":{
 
"author_count":{
 
"cardinality":{
 
"field":"author_hash",
 
"precision_threshold":100
 
}
 
}
 
}
 
}


参考: https://www.elastic.co/guide/en/elasticsearch/reference/current/search-aggregations-metrics-cardinality-aggregation.html


官网翻译:


precision_threshold选项允许交易记忆的准确性,并定义了一种独特的计数低于该数将接近准确。高于这个值,计数可能会变得更加模糊。最大支持值为40000,此数值以上的阈值与阈值为40000。默认值为3000。



es的自定义打分


function_score查询是处理分值计算过程的终极工具。它让你能够对所有匹配了主查询的每份文档调用一个函数来调整甚至是完全替换原来的_score。


实际上,你可以通过设置过滤器来将查询得到的结果分成若干个子集,然后对每个子集使用不同的函数。这样你就能够同时得益于:高效的分值计算以及可缓存的过滤器。


它拥有几种预先定义好了的函数:


weight,当weight为2时,结果为2 * _score。


field_value_factor,使用文档中某个字段的值来改变_score,比如将受欢迎程度或者投票数量考虑在内。


random_score,使用一致性随机分值计算来对每个用户采用不同的结果排序方式,对相同用户仍然使用相同的排序方式。


衰减函数(Decay Function) - linear,exp,gauss


将像publish_date,geo_location或者price这类浮动值考虑到_score中,偏好最近发布的文档,邻近于某个地理位置(译注:其中的某个字段)的文档或者价格(译注:其中的某个字段)靠近某一点的文档。


script_score,使用自定义的脚本来完全控制分值计算逻辑。如果你需要以上预定义函数之外的功能,可以根据需要通过脚本进行实现。




es查询与其他主流db的一些区别:


其中Elasticsearch是目前市场上比较很少有的,能够在检索加载和分布式计算三个方面都做得一流的数据库。而且是开源并且免费的。它使用了很多技术来达到飞一般的速度。这些主要的优化措施可以列举如下。


  • Lucene的inverted index可以比mysql的b-tree检索更快。
  • 在 Mysql中给两个字段独立建立的索引无法联合起来使用,必须对联合查询的场景建立复合索引。而lucene可以任何AND或者OR组合使用索引进行检索。
  • Elasticsearch支持nested document,可以把一批数据点嵌套存储为一个document block,减少需要索引的文档数。
  • Opentsdb不支持二级索引,只有一个基于hbase rowkey的主索引,可以按行的排序顺序scan。这使得Opentsdb的tag实现从检索效率上来说很慢。
  • Mysql 如果经过索引过滤之后仍然要加载很多行的话,出于效率考虑query planner经常会选择进行全表扫描。所以Mysql的存储时间序列的最佳实践是不使用二级索引,只使用clustered index扫描主表。类似于Opentsdb。
  • Lucene 从 4.0 开始支持 DocValues,极大降低了内存的占用,减少了磁盘上的尺寸并且提高了加载数据到内存计算的吞吐能力。
  • Lucene支持分segment,Elasticsearch支持分index。Elasticsearch可以把分开的数据当成一张表来查询和聚合。相比之下Mysql如果自己做分库分表的时候,联合查询不方便。
  • Elasticsearch 从1.0开始支持aggregation,基本上有了普通SQL的聚合能力。从 2.0 开始支持 pipeline aggregation,可以支持类似SQL sub query的嵌套聚合的能力。这种聚合能力相比Crate.io,Solr等同门师兄弟要强大得多。

查询效率之快的三个重要技术:


mmap来加载单独需要索引的列(memory mapped byte buffer);


各种posting list的压缩方案来压缩;


Roaring Bitmap数据结构做逻辑操作;