画布 1
ElasticSearch总结
  集群 es 的集群机制是对用户而言最透明的
    集群状态：节点的在线 replica 和 primary 是否满足 quorum 的要求，如果不满足则变成 yellow （缺分片副本）和 red（缺主分片副本）。这个算法有点像 gc 的三色标记算法。如果其他node 上的 replica 能够变成 primary，red 也可以恢复成 yellow。
    依赖于 elasticsearch.yml。多个节点共用 cluster.name 则可以加入一个集群，master 的选举是自动的。master 管理元数据：增删索引和节点。es 自己有 disk-based 的分配机制，但分配是以 shard 为单位，shard 是最小的部署单元，所以对大数据而言，shard 足够小是好的。master 决定 node 和 shard 之间如何做分配。
      path.data
      path.log
      cluster.name
      node.name
      network.host 可以自己选择一个内网的 ip 地址
      discovery.seed_hosts 设计集群拓扑的时候有用
      cluster.initial_master_nodes 设计集群的时候选择初始 master 节点有用
      consistency 确定写入成功的数量， 可选值：one、quorum 和 all
      number_of_replicas 非主分片数，+1 得到总分片数，基于总分片数可以算出 quorum。2.x的文档声称3节点的集群能容忍2节点丢失。
    每个节点都知道任意文档所处的位置，每个节点都可以做其他节点的 proxy，进行并行查询。因此，分片越多，读吞吐量越高。所以请求经过的拓扑结构并不一定要经过 master。这个被经过的节点叫作 coordinating node。
    节点之间的存在【写入时共识】，向上游返回成功时就能确认副本持久化成立。
    集群健康度打分
      集群
        节点/索引的基础信息
          cpu
          heap
          disk
          index
        常见问题
          存在副本分片未分配
            通过迁移副本至数据节点进行修复
          存在主分片未初始化或未分配
            通过初始化或迁移主分片至数据节点进行修复
          master 和 data 节点不均衡
            master及data节点剥离部署
            master节点数量建议为3或5，data节点数量：2~100间
      索引
        shard
          健康分片
          异常分片
          分片数量
            太多主分片数量会增加索引写压力，是要适当减少主分片数量；
          分片大小
            分片太大会增加索引读压力，不利于集群扩缩容（分片迁移耗费较长时间），集群发生故障时，恢复时间较长等问题
          均衡度
            计算公式：主分片*(1+副本数)%节点数量=0，满足公式即为均衡分布-没有拥有多余副本的节点。
        status
        docs
        size
        segment 段文件
          段文件太大会直接影响索引读写效率，甚至增加gc概率，影响es集群稳定性，必要时需要进行段合并操作，以减少段文件数量。
      节点
        jvm version
        es version
        cpu
        load average
        heap
        disk
        io
        index qps
        query qps
      日志
    集群操作
      allocation
        cluster.routing.allocation.enable
      rebalance
        cluster.routing.rebalance.enable
          只要这个开关打开，集群最终会自均衡。
        Elasticsearch runs an automatic process called rebalancing which moves shards between the nodes in your cluster to improve its balance
当集群中分片数发生变化时，就有可能触发分片的重平衡，默认情况下，ES会竟可能确保每个节点的总分片数一致。节点长时间下线，或者索引被删除，调整副本数都可能触发分片的重新分配。
  Context
    对于 2.x 版本的 es 而言，filter 本身因为不计分+易于缓存，所以得到推荐“请尽可能使用过滤式缓存”
    query context 评分 scoring query，全文搜索几乎没有正确答案，但总是需要查找相关程度。这类查询不值得缓存。
一个评分查询计算每一个文档与此查询的相关程度，同时将这个相关程度分配给表示相关性的字段 _score，并且按照相关性对匹配到的文档进行排序。这种相关性的概念是非常适合全文搜索的情况，因为全文搜索几乎没有完全 “正确” 的答案。
    filtering context（高版本直接叫 filter context） 不评分 non-scoring query。这类查询本身值得缓存-即使对 Lucene 而言。
  查询的抽象语法树
写查询的查询条件主要写在各个子句（或者叶子节点）上就可以了
{
"query": {
"bool": {
"must": [
{
"terms": {
"orderId": [
"123"
]
}
}
],
"must_not": [],
"should": [],
"filter": []
}
},
"from": 0,
"size": 10,
"sort": [
{
"payTime": {
"order": "asc"
}
}
],
"aggs": {
"all_interests": {
"terms": {
"field": "interests"
}
}
},
"profile": true
}
    query 查询的 body
      bool、must 等节点是复合语句 compound clause 的意思，term 等节点则是叶子语句 leaf clause，这些叶子里有一个个真实的 example 子节点。
      bool 它的 properties 一般为四类，每类的 value 通常是数组，filter 的的 value 则是另一个 bool
        什么时候应该使用读写路由？
我们分片很多，但不想扫描全部分片的时候，应该使用读写路由
        bool 查询采取 more-matches-is-better 匹配越多越好的方式
        复合嵌套
        must 意味着需要一个 clause 数组。
使用 and 查询的时候可以使用 must 作为查询方式，等同于逻辑 and
文档 必须 匹配这些条件才能被包含进来。
          term 不分词
            {
"orderId": "123"
}
          terms
            {
"orderId": [
"123"
]
}
类似 sql 的 in
          query_string
          range 范围查询
          exists
            {
"exists": {
"field": "title"
}
}
              类似于 is not null
          missing
            类似于 is null
          fuzzy
            这是和 match 最相似的一个关键字，查询会计算与关键词的拼写相似程度
          match 依赖于分词
          最类似 sql 语句中的等值查询的语句
        must_not
          反向的 must，元素的搭配和 must 很像，只是总的语句在这一层做了一个取反。
文档 必须不 匹配这些条件才能被包含进来。
        should 类似逻辑 or 但不完全是 or，是 at least one 的意思。
如果满足这些语句中的任意语句，将增加 _score ，否则，无任何影响。它们主要用于修正每个文档的相关性得分。
        filter
          所有的 filter 都可以被放在 bool 里（这也是自由组合的意思之一），filter 本身可以容纳 query 查询里面的任意条件，放在 filter 里的条件不参加打分。
filter 类似查询出来以后的 having。
必须匹配，但它以不评分、过滤模式来进行。这些语句对评分没有贡献，只是根据过滤标准来排除或包含文档。
            {
"bool": {
"must": {
"match": {
"title": "how to make millions"
}
},
"must_not": {
"match": {
"tag": "spam"
}
},
"should": [
{
"match": {
"tag": "starred"
}
}
],
"filter": {
"bool": {
"must": [
{
"range": {
"date": {
"gte": "2014-01-01"
}
}
},
{
"range": {
"price": {
"lte": 29.99
}
}
}
],
"must_not": [
{
"term": {
"category": "ebooks"
}
}
]
}
}
}
}
date 已经不再参与打分，但 match 仍然参与打分
              "type" : "date",
"format" : "yyyy-MM-dd HH:mm:ss||yyyy-MM-dd||epoch_millis"
支持这样查询
"createdTime": {
"gte": "2022-07-06",
"lt": "2022-07-14"
}
            {
"constant_score": {
"filter": {
"term": { "category": "ebooks" }
}
}
}
term 查询被放置在 constant_score 中，转成不评分的 filter。这种方式可以用来取代只有 filter 语句的 bool 查询- bool 是 query 的一个 child。
这个查询相比于普通的 filter，可以避免查询结果的 score 为 0 的情况发生，constant_score 默认为 1-这意味着不会有评分为0的文档不能返回，也意味着文档的返回顺序是随机的。
constant_score 总是和 filter 搭配使用的。
            组合过滤器
              所有的过滤器会跳过评分阶段，而不是分析阶段。查询置于 filter 语句内不进行评分或相关度的计算，所以所有的结果都会返回一个默认评分 1 。
              compound filter
                GET /my_store/products/_search
{
"query" : {
"filtered" : {
"filter" : {
"bool" : {
"should" : [
{ "term" : {"price" : 20}},
{ "term" : {"productID" : "XHDK-A-1293-#fJ3"}}
],
"must_not" : {
"term" : {"price" : 30}
}
}
}
}
}
}
        {
"bool": {
"must": {
"match": {
"email": "business opportunity"
}
},
"should": [
{
"match": {
"starred": true
}
},
{
"bool": {
"must": {
"match": {
"folder": "inbox"
}
},
"must_not": {
"match": {
"spam": true
}
}
}
}
],
"minimum_should_match": 1
}
}
        读写路由
          计算公式
            默认
              shardId = hash(_routing) % num_primary_shards
            引入 routing_partition_size 参数
              shard_num = (hash(_routing) + hash(_id) % routing_partition_size) % num_primary_shards
              该参数可以使routing相同的文档分配到一批分片（集群分片的子集）而不是一个分片上，从而可以从一定程度上减轻数据倾斜的问题
              routing前使用自定义hash函数也可以
          写时指定 routing
            PUT my-index-000001/_doc/1?routing=user1&refresh=true
{
"title": "This is a document"
}
              实际上，es中可以存在一个 _id 为1 的 doc 和一个_id 为1 且 routing 字段为 user 的 doc
              这类操作比较危险的就是指定了id
          读时指定
            GET my-index-000001/_doc/1?routing=user1
GET my-index-000001/_search?routing=user1,user2
{
"query": {
"match": {
"title": "document"
}
}
}
GET my-index-000001/_search
{
"query": {
"terms": {
"_routing": [ "user1" ]
}
}
}
重要且容易被忽略：【必须写时指定得对这里才能拿出来读】。
这里的terms 是多余的 terms，和原始的查询条件搭配在一起需要使用多个 terms，因为每个 terms 不支持 multiple field
这个例子里也没有 must 的 bool 数组
          指定必须使用 routing
            PUT my-index-000002
{
"mappings": {
"_routing": {
"required": true
}
}
}
PUT my-index-000002/_doc/1
{
"text": "No routing value provided"
}
              这是一种很复杂的执行模型
              一个 CRUD 操作只对单个文档进行处理，文档的唯一性由 _index, _type（在高版本里被去掉）, 和 routing values （通常默认是该文档的 _id，但有id则意味着查询条件是不必要的，使用查询语法树查询，通常使用的 routing 就不是 id ）的组合来确定。 这表示我们确切的知道集群中哪个分片含有此文档。
        叶子语句和复合语句可以以无限组合的方式进行搭配
      match 这种查询在 client 里已经很少被用到了，但等价于 query string
        query
        operator
    from 通常是 0
    size 通常是 10 （默认值）或者 1000
    sort 排序字段
      对于整个 es而言，一个类型能够被 exact value 操作，等价于可以被 sort、aggregate
      排序查询条件的 clause [数组]
        【每一个】元素是：
{
"payTime": {
"order": "asc"
}
}
      查询结果的 hit 的元素中，会多一个field（在 mappings 里不会出现） "sort" : [ 1411516800000 ]，即用于排序的值会把日期进行数学转换。
如果字段是 date，这里会返回 epoch 毫秒数。
如果指定了 sort（无论是什么），则 max_score 和 _score 都为 null。因为 score 本来就是排序用的，sort 根据字段排序，score 就用不着了。
否则，结果会按照 score 倒序排序。sort 是用来代替 score 的。
        "sort": [
{ "date": { "order": "desc" }},
{ "_score": { "order": "desc" }}
]
假定我们想要结合使用 date 和 _score 进行查询，并且匹配的结果首先按照日期排序，然后在完全相同的日期内按照相关性排序-和 MySQL 的行为完全一致。
        作为缩写，你可以只指定要排序的字段名称：
"sort": "number_of_children"
字段值默认以顺序排列，而 _score 默认以倒序排列。
      GET /_search
{
"query": {
"filtered": {
"query": {
"match": {
"tweet": "manage text search"
}
},
"filter": {
"term": {
"user_id": 2
}
}
}
},
"sort": [
{
"date": {
"order": "desc"
}
},
{
"_score": {
"order": "desc"
}
}
]
}
        注意看，查询是可以起名的，filtered 是查询的名字。
        这两个排序字段，是在同层内进行排序的，和嵌套的内聚和有点像。
        排序是很重要的。结果集会先用第一排序字段来排序，当用用作第一字段排序的值相同的时候，然后再用第二字段对第一排序值相同的文档进行排序，以此类推。
        多级排序不需要包含 _score -- 你可以使用几个不同的字段，如位置距离或者自定义数值。
      在为一个字段的多个值进行排序的时候， 其实这些值本来是没有固定的排序的-- 一个拥有多值的字段就是一个集合，你准备以哪一个作为排序依据呢？
如果 dates 是一个数组 [23, 12, 07, 56]，这个数组对 es 来讲是无顺序的，不能通过 JsonPath 来查询。
对于数字和日期，你可以从多个值中取出一个来进行排序，你可以使用min, max, avg 或 sum这些模式。
比说你可以在 dates 字段中用最早的日期来进行排序：
"sort": {
"dates": {
"order": "asc",
"mode": "min"
}
}
      在 _source 下相同的字符串上排序两次会造成不必要的资源浪费。
"tweet": {
"type": "string",
"analyzer": "english"
}
改变后的多值字段mapping如下：
"tweet": { <1>
"type": "string",
"analyzer": "english",
"fields": {
"raw": { <2>
"type": "string",
"index": "not_analyzed"
}
}
}
现在，在给数据重建索引后，我们既可以使用 tweet 字段进行全文本搜索，也可以用tweet.raw字段进行排序：
GET /_search
{
"query": {
"match": {
"tweet": "elasticsearch"
}
},
"sort": "tweet.raw"
}
对可强制分词的 string（已过期）而言，raw可能比很多人想象中更重要。raw 的存在，让我们得到了不分词整字段排序的机会。
在 string 过期的时代，text 不能用于过滤、排序和聚合，而 keyword 可以，所以经常搭配使用，但 keyword 应该 ignore_above。
    aggs 同义关键词是 aggregations
      agg 名称 如 skuIdAgg
        {
"terms": {
"field": "interests"
}
}
        field 的意思是分组字段
          这里还可以隐藏一个 size 字段，确定返回分组数。
        terms 的意思是数量求和
          没有出现 bucket，但此时就是桶聚合，返回值里也会出现 bucket 字样。
        aggregations
        没有出现 metric字样，但此时就是指标聚合，返回值里用 value 来代表 metric。
        Metric 等同于 sql 里的 COUNT()/MAX()/MIN()。
Bucket 指的是满足条件的文档集合。
    "profile": true 看看返回结果是否携带有性能剖析数据
    这一级的参数，都被称作搜索的“顶层参数”
    这两类操作总会涉及 doc values
  返回结果 {
"took": 2,
"timed_out": false,
"_shards": {
"total": 30,
"successful": 30,
"skipped": 0,
"failed": 0
},
"hits": {
"total": 0,
"max_score": null,
"hits": [{
"_index": "insurance.insurance_policy-202105",
"_type": "insurance_policy",
"_id": "187480210023746263",
"_score": 1,
"_source": {
"forceInvoice": false,
"toInsurePersonBirthday": "19540517",
"orderId": "8187483010012864986"
}
}]
},
"profile": {
"shards": [
{
"id": "[-eFLUDhdS66QN6iG-bznww][insurance.insurance_policy-202103][3]",
"searches": [
{
"query": [
{
"type": "",
"description": "orderId:[123 TO 123]",
"time": "0.06605800000ms",
"time_in_nanos": 66058,
"breakdown": {
"score": 0,
"build_scorer_count": 18,
"match_count": 0,
"create_weight": 1576,
"next_doc": 0,
"match": 0,
"create_weight_count": 1,
"next_doc_count": 0,
"score_count": 0,
"build_scorer": 63115,
"advance": 1343,
"advance_count": 5
}
}
],
"rewrite_time": 3829,
"collector": [
{
"name": "CancellableCollector",
"reason": "search_cancelled",
"time": "0.07300300000ms",
"time_in_nanos": 73003,
"children": [
{
"name": "SimpleFieldCollector",
"reason": "search_top_hits",
"time": "0.06568800000ms",
"time_in_nanos": 65688
}
]
}
]
}
],
"aggregations": []
}
]
}
}
    took 查询耗时，单位为毫秒
    timed_out 计时是否超时
      GET /_search?timeout=10ms 可以调大超时
      需要注意的是timeout不会停止执行查询，它仅仅告诉你目前顺利返回结果的节点然后关闭连接。在后台，其他分片可能依旧执行查询，尽管结果已经被发送。
使用超时是因为对于你的业务需求（译者注：SLA，Service-Level Agreement服务等级协议，在此我翻译为业务需求）来说非常重要，而不是因为你想中断执行长时间运行的查询。
    分片结果：总数、成功数、失败数和跳过数
    hits
      total 总查询结果
        分页 search_after 需要参考
          为了理解为什么深度分页是有问题的，让我们假设在一个有5个主分片的索引中搜索。当我们请求结果的第一页（结果1到10）时，每个分片产生自己最顶端10个结果然后返回它们给请求节点(requesting node)，它再排序这所有的50个结果以选出顶端的10个结果。
现在假设我们请求第1000页——结果10001到10010。工作方式都相同，不同的是每个分片都必须产生顶端的10010个结果。然后请求节点排序这50050个结果并丢弃50040个！
你可以看到在分布式系统中，排序结果的花费随着分页的深入而成倍增长。这也是为什么网络搜索引擎中任何语句不能返回多于1000个结果的原因。
      max_score 最高的相关性
      hits 另一个嵌套数组
        []
          _index
          _type
          _id
          score 相关性分数，1 为 term 命中，高的时候有4
          _source document 本身的结构化原始值-禁掉这个可以提高性能，但大多数人不会禁。
    aggregations
      agg 名称
        {
"buckets": [
{
"key": "music",
"doc_count": 2
},
{
"key": "sports",
"doc_count": 1
}
]
}
          如果有嵌套，同级嵌套就在 doc_count 下面加另一个 aggregation_name
    profiles
      shards 有多少个分片就有多少个分片的查询结果
        []
          id 随机生成，但带有索引的名称。在这里看得出索引实际上只是一个逻辑的存储空间，每个 shard 需要有个单独的查询 id。
          searches 搜索的结果
            []
          aggregations 聚合的结果
  查询类型
    1. 全文搜索--Full Text Query
    2. 精确值搜索--Term Level Query
    3. 复合搜索--Compound Query
    4. 联合查询--Join Query
    5. 地图查询--Geo Query
    6. 跨度查询--Span Query
    7. 去重查询--Collapse Query
    8. 指标聚合--Metric Agg
    9. 桶聚合查询--Bucket Agg
    10.嵌套查询--NestedQuery
  写入流程
    这个流程即为 analysis，倒排索引就在这个流程里被处理完。
    analyze 默认的文档都可能被分析。
当Elasticsearch在你的文档中检测到一个新的字符串域，它会自动设置其为一个全文字符串域，使用标准分析器对它进行分析。
所以在 mapping 里面单独指定，可以让它不被_all 之类的 field 直接命中。
      默认的分析器可以处理英文文本，但如果要搜索中文，可能需要引入中文的分析器
      首先，将一块文本分成适合于倒排索引的独立的词条(term 或者 token) 。es 没有单独的tokenizer，使用analyzer 来实现分词。
        标准分析器
        简单分析器
        空格分析器
        语言分析器
        GET /_analyze
{
"analyzer": "standard",
"text": "Text to analyze"
}
          standard 分词器，通过单词边界分割输入的文本。
          standard 语汇单元过滤器，目的是整理分词器触发的语汇单元（但是目前什么都没做）。
          lowercase 语汇单元过滤器，转换所有的语汇单元为小写。
          stop 语汇单元过滤器，删除停用词—​对搜索相关性影响不大的常用词，如 a ， the ， and ， is 。
        PUT /spanish_docs
{
"settings": {
"analysis": {
"analyzer": {
"es_std": {
"type": "standard",
"stopwords": "_spanish_"
}
}
}
}
}
使用西班牙语停用词列表，创建一个自定义的 standard 分析器。
          es_std 分析器不是全局的—​它仅仅存在于我们定义的 spanish_docs 索引中
        创建自定义分析器
PUT /my_index
{
"settings": {
"analysis": {
"char_filter": { ... custom character filters ... },
"tokenizer": { ... custom tokenizers ... },
"filter": { ... custom token filters ... },
"analyzer": { ... custom analyzers ... }
}
}
}
PUT /my_index
{
"settings": {
"analysis": {
"char_filter": {
"&_to_and": {
"type": "mapping",
"mappings": [ "&=> and "]
}},
"filter": {
"my_stopwords": {
"type": "stop",
"stopwords": [ "the", "a" ]
}},
"analyzer": {
"my_analyzer": {
"type": "custom",
"char_filter": [ "html_strip", "&_to_and" ],
"tokenizer": "standard",
"filter": [ "lowercase", "my_stopwords" ]
}}
}}}
        测试自定义分析器
GET /my_index/_analyze?analyzer=my_analyzer
The quick & brown fox
        把自定义分析器和某个字段绑定
PUT /my_index/_mapping/my_type
{
"properties": {
"title": {
"type": "string",
"analyzer": "my_analyzer"
}
}
}
      之后，将这些词条统一化为标准格式以提高它们的“可搜索性”，或者 recall。
      一个分析器就是在一个包里面组合了三种函数的一个包装器， 三种函数按照顺序被执行。
        Character filters 字符过滤器
          字符过滤器 用来 整理 一个尚未被分词的字符串。例如，如果我们的文本是HTML格式的，它会包含像 <p> 或者 <div> 这样的HTML标签，这些标签是我们不想索引的。我们可以使用 html清除 字符过滤器 来移除掉所有的HTML标签，并且像把 &Aacute; 转换为相对应的Unicode字符 Á 这样，转换HTML实体。
          一个分析器可能有0个或者多个字符过滤器。
        Tokenizer 分词器
          一个分析器必须有一个唯一的分词器。 分词器把字符串分解成单个词条或者词汇单元。 标准 分析器里使用的 标准 分词器 把一个字符串根据单词边界分解成单个词条，并且移除掉大部分的标点符号，然而还有其他不同行为的分词器存在。
        Token filters 词单元过滤器
          经过分词，作为结果的 词单元流 会按照指定的顺序通过指定的词单元过滤器 。
      这种设计模式保证 analyzer 的插件化，但它如果还能够做适配器或者代理模式，则子组件还能进一步插件化
    生成倒排索引 inverted index，大量id重复于被分析过的文档里
      Term | Doc 1 | Doc 2 | Doc 3 | ...
------------------------------------
brown | X | | X | ...
fox | X | X | X | ...
quick | X | X | | ...
the | X | | X | ...
搜索和聚合和排序正相反，搜索要求第一列必须是 term 本身。
    生成正排索引 forward index DovValues
      Doc Terms
-----------------------------------------------------------------
Doc_1 | brown, dog, fox, jumped, lazy, over, quick, the
Doc_2 | brown, dogs, foxes, in, lazy, leap, over, quick, summer
Doc_3 | dog, dogs, fox, jumped, over, quick, the
-----------------------------------------------------------------
  性能优化
    字段类型选择
      在高版本的 es 中，string 是过期类型，最好使用 text 和 keyword 来代替。
字符串类型，如果需要分词则选择text，会对每个词建立倒排索引，存储成本较高；所以无需分词得情况请使用keyword。
我们常见的用于比对的 orderId 都应该是 keyword，而 description 则应该是 text（腾讯云日志汇 @message 字段也使用text 类型为主，这样可以使用kibana来查询了）。使用一个单词的一部分来查询text的一部分，什么都查不出来。
      数值类型，尽量选择贴合实际数值大小得类型，比如价格，使用float或者double,不需要范围查询尽量使用keyword，在5.x数字类型的Term查询性能相比2.x下降80%，CPU利用率也会飙升30%左右。
      常见的数据类型有：
        integer
        long
        boolean
        {
"type": "date",
"format": "yyyy-MM-dd HH:mm:ss||yyyy-MM-dd||epoch_millis"
}
        "latestCallTime": {
"type": "text",
"fields": {
"keyword": {
"type": "keyword",
"ignore_above": 256
}
}
}
          Strings longer than the ignore_above setting will not be indexed or stored. For arrays of strings, ignore_above will be applied for each array element separately and string elements longer than ignore_above will not be indexed or stored.
这里的省略可不是截断，就是简单地 indexed, but without indexing the 超长 field. 能被 get api with id 取得，但不能被 _search api 使用 term 取得，但可以被空查询查出来。
针对 keyword 的极致性能省略。要查超长的内容，去 _source 看。
这个值是为数不多的可以动态改的属性配置。
          This option is also useful for protecting against Lucene’s term byte-length limit of 32766.
The value for ignore_above is the character count, but Lucene counts bytes. If you use UTF-8 text with many non-ASCII characters, you may want to set the limit to 32766 / 4 = 8191 since UTF-8 characters may occupy at most 4 bytes.
          查询的时候使用 latestCall.keyword 来查询。keyword 在这里不是一个关键字，而是一个fields 的一部分。
          _core_field 类型 (strings, numbers, Booleans, dates) 接收一个 fields 参数.
        date
          日期类型可以指定 format：
"actionTime": {
"type": "date",
"format": "yyyy-MM-dd HH:mm:ss||yyyy-MM-dd||epoch_millis"
},
在现实中，很多日期在 mappings 里都是三格式的类型。
    查询语句使用
      避免使用嵌套类型，nested或者object 都有问题，使用嵌套类型性能会慢10几倍，尽量把逻辑处理放在客户端代码。
如果非要选，nested 更好。
        object 意味着 es 可以扁平化对象的成员生成倒排索引，而 nested 意味着这个 property 需要使用另一个 doc 来 reference
任意的嵌套文档的类型都是 object（哪怕它是一个数组），除非你显式地指定：
"type": "nested"
object 原始文档为
{
"followers": [
{ "age": 35, "name": "Mary White"},
{ "age": 26, "name": "Alex Jones"},
{ "age": 19, "name": "Lisa Smith"}
]
}
被扁平后产生的索引是：
{
"followers.age": [19, 26, 35],
"followers.name": [alex, jones, lisa, smith, mary, white]
}
          对 object 进行搜索时，使用 json 路径方式是：
{
"query": {
"bool": {
"should": [
{
"term": {
"authors.country.keyword": "Chile"
}
},
{
"range": {
"authors.age": {
"lte": 30
}
}
}
]
}
}
使用 filter 实现另一种 and（把 filter 写进 query.bool 里面，是es官方推荐的filter的用法，而不是把 filter 作为兄弟）：
{
"query": {
"bool": {
"filter": [
{
"term": {
"authors.country.keyword": "Chile"
}
},
{
"range": {
"authors.age": {
"lte": 30
}
}
}
]
}
}
这种搜索有个致命问题，就是它实际上是在这样的“被处理过的扁平化过的”文档里进行搜索：
{
"name": "An Awesome Book",
"tags.name": [
"best-seller",
"summer-sale"
],
"authors.name": [
"Gustavo Llermaly",
"John Doe"
],
"authors.age": [
32,
20
],
"authors.country": [
"Chile, USA"
]
}
文档的 field 之间的映射关系被割裂了。这种文档把多个 doc 的 field 放在一个数组里，同属一个文档里的 field 互相不知道，要还原出原文档靠笛卡尔积是不行的。
          对 nested 类型使用 nested query 的方法：
GET books_test_nested/_search
{
"query": {
"nested": {
"path": "authors",
"query": {
"bool": {
"should": [
{
"term": {
"authors.country.keyword": "Chile"
}
},
{
"range": {
"authors.age": {
"lte": 30
}
}
}
]
}
}
}
}
{
"query": {
"nested": {
"path": "authors",
"query": {
"bool": {
"filter": [
{
"term": {
"authors.country.keyword": "Chile"
}
},
{
"range": {
"authors.age": {
"lte": 30
}
}
}
]
}
}
}
}
}
}
          PUT my-index-000001
{
"mappings": {
"properties": {
"manager": {
"properties": {
"age": { "type": "integer" },
"name": { "type": "text" }
}
},
"employees": {
"type": "nested",
"properties": {
"age": { "type": "integer" },
"name": { "type": "text" }
}
}
}
}
}
manager 无需配置，即为 object。employees需要配置，则为 nested。
      避免使用父子关系，parent-child，使用父子类型性能会慢百倍以上。
      控制返回结果集，size不要超过1000，这是引发full gc常见得原因。要翻页最好使用 search-after。
        前查询结果
          GET twitter/tweet/_search
{
"size": 10,
"query": {
"match" : {
"title" : "elasticsearch"
}
},
"sort": [
{"date": "asc"},
{"_uid": "desc"}
]
}
        后查询语句
          GET twitter/tweet/_search
{
"size": 10,
"query": {
"match" : {
"title" : "elasticsearch"
}
},
"search_after": [1463538857, "tweet#654323"],
"sort": [
{"date": "asc"},
{"_uid": "desc"}
]
}
            排序值里最好、一定要带有一个唯一值，就好像这里面第二位的 _uid 一样。
每个文档具有一个唯一值的字段应用作排序规范的仲裁。 否则，具有相同排序值的文档的排序顺序将是未定义的。
            当我们使用 search_after 时，from 值必须设置为 0 或者 -1。
      严禁使用通配符，比如*es*
        使用 * 开头对老的 es集群性能是有毁灭性影响的。
        解决方案
          如果有办法拼接出完整的模式，则可以用 term 查询。
          如果有可能使用7.9以上的集群，使用 wildcard type。
      避免使用查询时的script计算
        但有些情况下聚合需要 script 来表达复杂逻辑
      尽量使用query-bool-filter， filter的缓存机制会使的检索更快
        GET /_search
{
"query": {
"filtered": {
"query": {
"match": {
"email": "business opportunity"
}
},
"filter": {
"term": {
"folder": "inbox"
}
}
}
}
}
      控制聚合的数量，减少内存的开销-消耗和淘汰 fielddata 使用的缓存
      控制返回字段，_source过滤,只返回业务相关的字段，减少IO开销，如下，搜索返回结果中希望包含obj1和obj2开头得字段。
对于 ORM 来讲，很少有人用这种类似覆盖索引的设计。
        {
"_source": {
"includes": [
"obj1.*",
"obj2.*"
],
"excludes": [
"*.description"
]
},
"query": {
"term": {
"user": "kimchy"
}
}
}
      避免在高峰期进行段文件合并，段文件合并会消耗大量IO，影响查询和读写。
es 的很多慢查询可能都是定期段合并的结果。
        安排在凌晨进行段合并是合适的。
  分布式搜索
    query_then_fetch
      在初始查询阶段时， 查询会广播到索引中每一个分片拷贝（主分片或者副本分片中的一个）。 每个分片在本地执行搜索并构建一个匹配文档的优先队列。优先队列的大小取决于分页参数 from 和 size 。优先队列的内容只有 id 和 score-类似 InnoDB 的小 sort buffer。然后每个节点把优先队列返回给协调者节点进行排序。
这种搜索模型的实际搜索值和 from 有很大关系，最终还是会遇到深搜翻页的问题。
      决定汇总的优先队列里哪些文档需要被丢弃，然后使用 multi-get request 来通过文档 id 获取 rich doc（完整的doc，相对于 lite doc 而言）
      query phase
        当一个搜索请求被发送到一个节点Node，这个节点就变成了协调节点。这个节点的工作是向所有相关的分片广播搜索请求并且把它们的响应整合成一个全局的有序结果集。这个结果集会被返回给客户端。
第一步是向索引里的每个节点的分片副本广播请求。就像document的GET请求一样，搜索请求可以被每个分片的原本或任意副本处理（every in-sync replica）。这就是更多的副本（当结合更多的硬件时）如何提高搜索的吞吐量的方法。对于后续请求，协调节点会轮询所有的分片副本以分摊负载。
        每一个分片在本地执行查询和建立一个长度为from+size的有序优先队列——这个长度意味着它自己的结果数量就足够满足全局的请求要求。分片返回一个轻量级的结果列表给协调节点。只包含documentID值和排序需要用到的值，例如_score。
整个过程类似于归并排序算法，先分组排序再归并到一起，对于这种分布式场景非常适用。
归并排序是对所有的外排序都是说一不二的好方法。
        客户端发送一个search（搜索）请求给Node 3（node 3 是拥有某一副本的任意节点，这请求并不访问全部节点），Node 3创建了一个长度为from+size的空优先级队列。
        Node 3 转发这个搜索请求到索引中每个分片的原本或副本。每个分片在本地执行这个查询并且结果将结果到一个大小为from+size的有序【本地】优先队列里去。
这个本地队列有点像一个本地的 sorting buffer。
        每个分片返回document的ID和它优先队列里的所有document的排序值给协调节点Node 3。Node 3 把这些值合并到自己的优先队列里产生全局排序结果。
      fetch phase
        协调节点先决定哪些document是_实际（actually）_需要取回的。例如，我们指定查询{ "from": 90, "size": 10 }，那么前90条将会被丢弃，只有之后的10条会需要取回。这些document可能来自与原始查询请求相关的某个、某些或者全部分片。
        分片加载document主体——_source field。如果需要，还会根据元数据丰富结果和高亮搜索片断。一旦协调节点收到所有结果，会将它们汇集到单一的回答响应里，这个响应将会返回给客户端。
根据document的数量，分片的数量以及所使用的硬件，对10,000到50,000条结果（1,000到5,000页）深分页是可行的。但是对于足够大的from值，排序过程将会变得非常繁重，会使用巨大量的CPU，内存和带宽。因此，强烈不建议使用深分页。
如果你确实需要从集群里获取大量documents，你可以通过设置搜索类型scan禁用排序，来高效地做这件事。
用 scan来代替深分页，是一种很好的设计思想，需要使用者们加以注意。
        协调节点辨别出哪个document需要取回，并且向相关分片发出GET请求。
        每个分片加载document并且根据需要_丰富（enrich）_它们，然后再将document返回协调节点。
        一旦所有的document都被取回，协调节点会将结果返回给客户端。
      并行搜索受制于长尾搜索的查询时间，基于每个文档可以确认查询是否超时，达到超时时间 es 的结果会直接返回（即使这时候数据没有找完，会部分返回）。
    搜索选项
      注意这是 search type，get 没有这些类型和流程
      preference
        client.prepareSearch(“index”).setPreference(“_primary”)
          _primary: 指查询只在主分片中查询
          _primary_first: 指查询会先在主分片中查询，如果主分片找不到（挂了），就会在副本中查询。
          _local: 指查询操作会优先在本地节点有的分片中查询，没有的话再在其它节点查询。
          _only_node:指在指定id的节点里面进行查询，如果该节点只有要查询索引的部分分片，就只在这部分分片中查找，所以查询结果可能不完整。如_only_node:123在节点id为123的节点中查询。
          Custom (string) value:用户自定义值，指在参数cluster.routing.allocation.awareness.attributes指定的值，如这个值设置为了zone，那么preference=zone的话就在awareness.attributes=zone*这样的节点搜索，如zone1、zone2。
      timeout
      routing
      search_type
        默认值
        query_then_fetch
        count
          搜索类型只有一个query（查询）的阶段，类似 HTTP 的 head。
当不需要搜索结果只需要知道满足查询的document的数量时，可以使用这个查询类型。
        query_and_fetch
          搜索类型将查询和取回阶段合并成一个步骤。这是一个内部优化选项，当搜索请求的目标只是一个分片时可以使用，例如指定了routing（路由选择）值时
        dfs_query_then_fetch
        scan
          scan（扫描）搜索类型是和scroll（滚屏）API连在一起使用的，可以高效地取回巨大数量的结果。它是通过禁用排序来实现的。
    扫描和滚屏
      scroll（滚屏）
        滚屏搜索会及时制作快照。这个快照不会包含任何在初始阶段搜索请求后对index做的修改。它通过将旧的数据文件保存在手边，所以可以保护index的样子看起来像搜索开始时的样子。
      scan（扫描）
        深度分页代价最高的部分是对结果的全局排序，但如果禁用排序，就能以很低的代价获得全部返回结果。
      GET /old_index/_search?search_type=scan&scroll=1m (1)
{
"query": { "match_all": {}},
"size": 1000
}
GET /_search/scroll?scroll=1m (1)
c2Nhbjs1OzExODpRNV9aY1VyUVM4U0NMd2pjWlJ3YWlBOzExOTpRNV9aY1VyUVM4U0 <2>
NMd2pjWlJ3YWlBOzExNjpRNV9aY1VyUVM4U0NMd2pjWlJ3YWlBOzExNzpRNV9aY1Vy
UVM4U0NMd2pjWlJ3YWlBOzEyMDpRNV9aY1VyUVM4U0NMd2pjWlJ3YWlBOzE7dG90YW
xfaGl0czoxOw==
        保持滚屏开启另一分钟。
        _scroll_id 可以在body或者URL里传递，也可以被当做查询参数传递。
        注意，要再次指定?scroll=1m。滚屏的终止时间会在我们每次执行滚屏请求时刷新，所以他只需要给我们足够的时间来处理当前批次的结果而不是所有的匹配查询的document。
        这个滚屏请求的应答包含了第一批次的结果。虽然指定了一个1000的size ，但是获得了更多的document。当扫描时，size被应用到每一个分片上，所以我们在每个批次里最多或获得size * number_of_primary_shards（size*主分片数）个document。
        如果没有更多的命中结果返回，就处理完了所有的命中匹配的document。
此时就可以关闭查找了。
  分片内部原理
    这个api 是lucene 的 flush
    这个 api 无限使用资源，很危险。
    这个 api 是lucene 的commit
    要解决的问题：怎样在保留不变性的前提下实现倒排索引的更新？
      用更多的（补充）索引。确切地讲，用更多的段，来包含和表达文档的新版本。
    名词解析
      Lucene 是按段搜索的，每一段本身都是一个倒排索引。一段内一个文档可以被认为是唯一的。
      提交点
        一个列出了所有已知段的文件。所有提交点里的段都是可搜索的，一个提交点包含三个段，就好像包含三个倒排索引。
      ES 索引
        ES 分片的集合
      Lucene 索引
        一个 ES 分片 shard。相关性计分通常是在本地内计算的。
          这会导致一个特别的问题：被破坏的相关度
            在 什么是相关度？中，我们描述了Elasticsearch 默认使用的相似度算法，这个算法叫做词频/逆向文档频率或 TF/IDF 。词频是计算某个词在当前被查询文档里某个字段中出现的频率，出现的频率越高，文档越相关。 逆向文档频率将某个词在索引内所有文档出现的百分数考虑在内，出现的频率越高，它的权重就越低。
            数据在分片间的分布不均衡，会导致 IDF 本地计算出现倾斜。
            在真实世界的数据量下，局部的 IDF 会被迅速均化，所以上述问题并不是相关度被破坏所导致的，而是由于数据太少。
          解法
            不要在生产环境上使用 dfs_query_then_fetch 。完全没有必要。只要有足够的数据就能保证词频是均匀分布的。没有理由给每个查询额外加上 DFS 这步。
            第一种是只在主分片上创建索引，正如 match 查询 里介绍的那样，如果只有一个分片，那么本地的 IDF 就是 全局的 IDF。
              "number_of_shards": 1
            第二个方式就是在搜索请求后添加 ?search_type=dfs_query_then_fetch ， dfs 是指 分布式频率搜索（Distributed Frequency Search） ， 它告诉 Elasticsearch ，先分别获得每个分片本地的 IDF ，然后根据结果再计算整个索引的全局 IDF 。
      field
      term
    写入和打开一个新段的轻量的过程叫做 refresh。
默认情况下每个分片会每秒自动刷新一次。每次 refresh 会产生一个新的分段。频繁地refresh会产生细碎的段。
动态更新索引，refresh api 可以动态更新索引，但 refresh 不足以让数据真的被持久化（不包括 flush 语义）。
      写文档（新文档，或者老文档的新版本）被内存索引缓存（in-memory buffer），同时就写入 translog，保证数据变更不丢，类似 InnoDB 的两段式 prepare commit 提交。
        段可以存在于内存里，如同其他 sstables 实现一样。
在数据还在缓存时就可以被文件系统开启（open），也就 searchable。
      缓存被提交到磁盘
        os cache
        追加倒排索引（段）到磁盘
          此时段仍然在 commit point 之外，也是 searchable 的。
      refresh_interval 可以在既存索引上进行动态更新。 在生产环境中，当你正在建立一个大的新索引时，可以先关闭自动刷新，待开始使用该索引时，再把它们调回来：
PUT /my_logs/_settings
{ "refresh_interval": -1 }
PUT /my_logs/_settings
{ "refresh_interval": "1s" }
    flush api
      这几个动作都是靠 commit 来原子化的。
      写入提交点（commit point）到磁盘
        后 fsync segment file 的流程
        写入提交点，让一个commit point 包含新写入段
        内存缓存被清空，等待接收新的文档
        删除旧 translog，创建新的 （类似 MySQL checkpointing 以后发生的 redolog 循环使用）。
          在5.0以前，是这样。在 5.0以后，只从 lucene 索引里读，realtime 读基于 refresh 机制。
          translog 也被用来提供实时 CRUD 。当你试着通过ID查询、更新、删除一个文档，它会在尝试从相应的段中检索之前， 首先检查 translog 任何最近的变更。这意味着它总是能够实时地获取到文档的最新版本。
          段不可直接被物理删除，只是被标记 .del 进行软删除。每个提交点会包含一个 .del 文件，文件中会列出这些被删除文档的段信息。类似 Inno
        所有文件系统中等待的写入被刷新到磁盘
          这一步其实是隐藏步骤。
            不同于 MySQL 的内存页在 change buffer 中的读写机制。在 MySQL 里，change buffer是 innodDB的一部分，而此处运用的是 os cache。
      这个执行一个提交并且截断 translog 的行为在 Elasticsearch 被称作一次 flush 。 分片每30分钟被自动刷新（flush），或者在 translog 太大的时候也会刷新。
        所以也有类似双 1 写入的配置，在 Translog settings 里有。
        PUT /my_index/_settings
{
"index.translog.durability": "async",
"index.translog.sync_interval": "5s"
}
      这就是说，在重启节点或关闭索引之前执行 flush 有益于你的索引。当 Elasticsearch 尝试恢复或重新打开一个索引， 它需要重放 translog 中所有的操作，所以如果日志越短，恢复越快。
    optimize api
      ES 的段合并类似 hbase 的 compact，段很多会导致搜索变慢。
    这个过程很像 lsm tree 的刷新过程，而且查询本身是合并多段搜索结果的。为什么叫es是准实时的？NRT，near real-time，准实时。默认是每隔1秒refresh一次的，所以es是准实时的，因为写入的数据1秒之后才能被看到。一秒后，我们看到了一个新的 searchable 的段（在 in-memory buffer/translog/segement 里）。
引入change buffer、唯一索引实时脏页校验，是InnoDB 之类的方案能够实时查询的原因，ES 没有这个设计，所以是 NRT。
  深入搜索
    多语句可以查同一个字段，但我们日常中所有的查询，都是多字段的，这才是我们常见的查询。
    短语搜索本身是为了解决次序相关性问题
    我们常说的模糊匹配，实际上是部分匹配
    结构化搜索
      精确值查找
        普通的 term 查询
          {
"term" : {
"price" : 20
}
}
          包含，而不是相等
            如果我们有一个 term（词项）过滤器
{ "term" :
{ "tags" : "search" }
}
，它会与以下两个文档 同时 匹配：
              { "tags" : ["search"] }
              { "tags" : ["search", "open_source"] }
            如果一定期望得到我们前面说的那种行为（即整个字段完全相等），最好的方式是增加并索引另一个字段， 这个字段用以存储该字段包含词项的数量，同样以上面提到的两个文档为例，现在我们包括了一个维护标签数的新字段
              { "tags" : ["search"], "tag_count" : 1 }
{ "tags" : ["search", "open_source"], "tag_count" : 2 }
              一旦增加这个用来索引项 term 数目信息的字段，我们就可以构造一个 constant_score 查询，来确保结果中的文档所包含的词项数量与要求是一致的：
                GET /my_index/my_type/_search
{
"query": {
"constant_score" : {
"filter" : {
"bool" : {
"must" : [
{ "term" : { "tags" : "search" } },
{ "term" : { "tag_count" : 1 } }
]
}
}
}
}
}
            如 term 或 fuzzy 这样的底层查询不需要分析阶段，它们对单个词项进行操作。用 term 查询词项 Foo 只要在倒排索引中查找 准确词项 ，并且用 TF/IDF 算法为每个包含该词项的文档计算相关度评分 _score 。
              记住 term 查询只对倒排索引的词项精确匹配，这点很重要，它不会对词的多样性进行处理（如， foo 或 FOO ）。这里，无须考虑词项是如何存入索引的。如果是将 ["Foo","Bar"] 索引存入一个不分析的（ not_analyzed ）包含精确值的字段，或者将 Foo Bar 索引到一个带有 whitespace 空格分析器的字段，两者的结果都会是在倒排索引中有 Foo 和 Bar 这两个词。
            由于倒排索引表自身的特性，整个字段是否相等会难以计算，如果确定某个特定文档是否 只（only） 包含我们想要查找的词呢？首先我们需要在倒排索引中找到相关的记录并获取文档 ID，然后再扫描 倒排索引中的每行记录 ，查看它们是否包含其他的 terms 。
可以想象，这样不仅低效，而且代价高昂。正因如此， term 和 terms 是 必须包含（must contain） 操作，而不是 必须精确相等（must equal exactly） 。
        引入 constant_score 后
          使用 constant_score 查询以非评分模式来执行 term 查询。这样查询会比 term 更快。
          {
"query" : {
"constant_score" : {
"filter" : {
"term" : {
"price" : 20
}
}
}
}
}
        term 比较易于处理的类型
          数字（numbers）
          布尔值（Booleans）
          日期（dates）
          文本（text）
            长文本，带有空格、-之类的词，如果不用特定的 analyze 就很难搜出来，所以 not_analyzed 很重要。
但有意思的是，ams 的mappings 里很少出现 not_analyzed。
      范围查找
        日期计算（date math）
          "range" : {
"timestamp" : {
"gt" : "now-1h"
}
}
          "range" : {
"timestamp" : {
"gt" : "2014-01-01 00:00:00",
"lt" : "2014-01-01 00:00:00||+1M"
}
}
        如果我们想查找从 a 到 b （不包含）的字符串，同样可以使用 range 查询语法
          "range" : {
"title" : {
"gte" : "a",
"lt" : "b"
}
}
      处理 Null 值
        如何将某个不存在的字段存储在这个数据结构中呢？无法做到！
          最终，这也就意味着，null, [] （空数组）和 [null] 所有这些都是等价的，它们无法存于倒排索引中。
          但仍然可以用 exists 和 missing 来执行查询
          处理嵌套
            {
"name" : {
"first" : "John",
"last" : "Smith"
}
}
            {
"exists" : { "field" : "name" }
}
等价于
{
"bool": {
"should": [
{ "exists": { "field": "name.first" }},
{ "exists": { "field": "name.last" }}
]
}
}
    全文搜索
      本质上就是为了局部指定 analyzer
      基于词项与基于全文
        这种查询被认为是核心查询，但这种核心查询本身已经被大多数中国公司和团队废弃了
        高层查询（Advanced Search？）
          所以会查出更多的数据，类似百度的查询
          像 match 或 query_string 这样的查询
            被查询的 field 会触发查询的语句被 analyzed。
查询只能查找倒排索引表中真实存在的项，所以保证文档在索引时与查询字符串在搜索时应用相同的分析过程非常重要，这样查询的项才能够匹配倒排索引中的项。
            即任何文档只要字段里包含 指定词项中的至少一个词就能匹配上至少一个被分解出来的 term 查询，被匹配的词项越多，文档就越相关。
一个长句子 match query = 分词以后的多个 bool term should 起来 + scoring（ + tf + idf + 目标 field 比对）
            如果查询 日期（date） 或 整数（integer） 字段，它们会将查询字符串分别作为日期或整数对待。
            如果查询一个（ not_analyzed ）未分析的精确值字符串字段，它们会将整个查询字符串作为单个词项对待。
            但如果要查询一个（ analyzed ）已分析的全文字段，它们会先将查询字符串传递到一个合适的分析器，然后生成一个供查询的词项列表。
            一旦组成了词项列表，这个查询会对每个词项逐一执行底层的查询（term query），再将结果合并，然后为每个文档生成一个最终的相关度评分。
              这两个查询可以说完全相等
                {
"match": { "title": "brown fox"}
}
                {
"bool": {
"should": [
{ "term": { "title": "brown" }},
{ "term": { "title": "fox" }}
]
}
}
              这两个查询可以说完全相等
                {
"match": {
"title": {
"query": "brown fox",
"operator": "and"
}
}
}
                {
"bool": {
"must": [
{ "term": { "title": "brown" }},
{ "term": { "title": "fox" }}
]
}
}
          操作符
            match 查询还可以接受 operator 操作符作为输入参数，默认情况下该操作符是 or 。我们可以将它修改成 and 让所有指定词项都必须匹配。
              但此刻仍不是 match phrase
              GET /my_index/my_type/_search
{
"query": {
"match": {
"title": {
"query": "BROWN DOG!",
"operator": "and"
}
}
}
}
            match 查询支持 minimum_should_match 最小匹配参数，这让我们可以指定必须匹配的词项数用来表示一个文档是否相关。我们可以将其设置为某个具体数字，更常用的做法是将其设置为一个百分数，因为我们无法控制用户搜索时输入的单词数量
              GET /my_index/my_type/_search
{
"query": {
"match": {
"title": {
"query": "quick brown dog",
"minimum_should_match": "75%"
}
}
}
}
      组合查询
        GET /my_index/my_type/_search
{
"query": {
"bool": {
"must": { "match": { "title": "quick" }},
"must_not": { "match": { "title": "lazy" }},
"should": [
{ "match": { "title": "brown" }},
{ "match": { "title": "dog" }}
]
}
}
}
          以上的查询结果返回 title 字段包含词项 quick 但不包含 lazy 的任意文档。目前为止，这与 bool 过滤器的工作方式非常相似。
区别就在于两个 should 语句，也就是说：一个文档不必包含 brown 或 dog 这两个词项，但如果一旦包含，我们就认为它们更相关。
          bool 查询会为每个文档计算相关度评分 _score ，再将所有匹配的 must 和 should 语句的分数 _score 求和，最后除以 must 和 should 语句的总数。
must_not 语句不会影响评分；它的作用只是将不相关的文档排除。
          所有 must 语句必须匹配，所有 must_not 语句都必须不匹配，但有多少 should 语句应该匹配呢？默认情况下，没有 should 语句是必须匹配的，只有一个例外：那就是当没有 must 语句的时候，至少有一个 should 语句必须匹配。
            就像我们能控制 match 查询的精度 一样，我们可以通过 minimum_should_match 参数控制需要匹配的 should 语句的数量，它既可以是一个绝对的数字，又可以是个百分比：
              GET /my_index/my_type/_search
{
"query": {
"bool": {
"should": [
{ "match": { "title": "brown" }},
{ "match": { "title": "fox" }},
{ "match": { "title": "dog" }}
],
"minimum_should_match": 2
}
}
}
      查询语句提升权重
        GET /_search
{
"query": {
"bool": {
"must": {
"match": {
"content": {
"query": "full text search",
"operator": "and"
}
}
},
"should": [
{ "match": { "content": "Elasticsearch" }},
{ "match": { "content": "Lucene" }}
]
}
}
}
这里的 should，是给查询完的结果，增加加分的可选项。
      使用 boost 来提高相对权重
        boost 的默认值为 1 ，大于 1 会提升一个语句的相对权重。
boost 参数被用来提升一个语句的相对权重（ boost 值大于 1 ）或降低相对权重（ boost 值处于 0 到 1 之间），但是这种提升或降低并不是线性的，换句话说，如果一个 boost 值为 2 ，并不能获得两倍的评分 _score 。
        GET /_search
{
"query": {
"bool": {
"must": {
"match": {
"content": {
"query": "full text search",
"operator": "and"
}
}
},
"should": [
{ "match": {
"content": {
"query": "Elasticsearch",
"boost": 3
}
}},
{ "match": {
"content": {
"query": "Lucene",
"boost": 2
}
}}
]
}
}
}
      控制分析
        例子
          PUT /my_index/_mapping/my_type
{
"my_type": {
"properties": {
"english_title": {
"type": "string",
"analyzer": "english"
}
}
}
}
          GET /my_index/_analyze
{
"field": "my_type.title",
"text": "Foxes"
}
            字段 title ，使用默认的 standard 标准分析器，返回词项 foxes
          GET /my_index/_analyze
{
"field": "my_type.english_title",
"text": "Foxes"
}
            字段 english_title ，使用 english 英语分析器，返回词项 fox 。
        分析器的使用顺序
          索引时的顺序
            字段映射里定义的 analyzer ，否则
            索引设置中名为 default 的分析器，默认为
            standard 标准分析器
          在搜索时，顺序有些许不同
            查询自己定义的 analyzer ，否则
            字段映射里定义的 analyzer ，否则
            索引设置中名为 default 的分析器，默认为
            standard 标准分析器
          搜索时完整顺序
            查询自己定义的 analyzer ，否则
            字段映射里定义的 search_analyzer ，否则
            字段映射里定义的 analyzer ，否则
            索引设置中名为 default_search 的分析器，默认为
            索引设置中名为 default 的分析器，默认为
            standard 标准分析器
    多字段搜索
      嵌套 should
        处于同一层的每条语句具有相同的权重。在前面这个例子中，包含 translator 语句的 bool 查询，只占总评分的三分之一。如果将 translator 语句与 title 和 author 两条语句放入同一层，那么 title 和 author 语句只贡献四分之一评分。
        GET /_search
{
"query": {
"bool": {
"should": [
{ "match": { "title": "War and Peace" }},
{ "match": { "author": "Leo Tolstoy" }},
{ "bool": {
"should": [
{ "match": { "translator": "Constance Garnett" }},
{ "match": { "translator": "Louise Maude" }}
]
}}
]
}
}
}
    单字符串查询
      当用户输入了单个字符串查询的时候，通常会遇到以下三种情形
        最佳字段
        多数字段
          如 raw 字段
          为了对相关度进行微调，常用的一个技术就是将相同的数据索引到不同的字段，它们各自具有独立的分析链。
相同的文本被索引到其他字段，以提供更精确的匹配。一个字段可以包括未经词干提取过的原词，另一个字段包括其他词源、口音，还有一个字段可以提供 词语相似性 信息的瓦片词（shingles）。
          全文搜索被称作是 召回率（Recall） 与 精确率（Precision） 的战场
            召回率
              返回所有的相关文档
                为了提高召回率的效果，我们扩大搜索范围
                  如果一个用户搜索 “quick brown box” ，一个包含词语 fast foxes 的文档被认为是非常合理的返回结果
            精确率
              不返回无关文档
          std 的 multi_match 搜索
            PUT /my_index
{
"settings": { "number_of_shards": 1 },
"mappings": {
"my_type": {
"properties": {
"title": {
"type": "string",
"analyzer": "english",
"fields": {
"std": {
"type": "string",
"analyzer": "standard"
}
}
}
}
}
}
}
            GET /my_index/_search
{
"query": {
"multi_match": {
"query": "jumping rabbits",
"type": "most_fields",
"fields": [ "title", "title.std" ]
}
}
}
            GET /my_index/_search
{
"query": {
"multi_match": {
"query": "jumping rabbits",
"type": "most_fields",
"fields": [ "title^10", "title.std" ]
}
}
}
使用 ^ 来控制 boost。
        混合字段
          cross-fields entity search，比如一个 person 是由 first name 和 last name 标识的。
            _all 就是一个例子。
          term-centric
        field-centric
    multi_match
      {
"multi_match": {
"query": "Quick brown fox",
"type": "best_fields",
"fields": [ "title", "body" ],
"tie_breaker": 0.3,
"minimum_should_match": "30%"
}
}
        等于对多个 field 生成一样的match。tie_breaker 和 dis_max 太麻烦了。
      需要在 multi_match 查询中避免使用 not_analyzed 字段作为 fields 的目标。
    近似匹配
      类似于 String.contains
      短语匹配 phrase query
        GET /my_index/my_type/_search
{
"query": {
"match_phrase": {
"title": "quick brown fox"
}
}
}
        "match": {
"title": {
"query": "quick brown fox",
"type": "phrase"
}
}
      混合起来
        GET /my_index/my_type/_search
{
"query": {
"match_phrase": {
"title": {
"query": "quick fox",
"slop": 1
}
}
}
}
slop 可以让分词后的距离有空隙。
          这种查询实质上是匹配相对顺序一致的所有指定词语。
        临近查询
          配上slop，就会有如下错误。
            PUT /my_index/groups/1
{
"names": [ "John Abraham", "Lincoln Smith"]
}
GET /my_index/groups/_search
{
"query": {
"match_phrase": {
"names": "Abraham Lincoln"
}
}
}
会不当查出这数据。
        一个简单的 term 查询比一个短语查询大约快 10 倍，比邻近查询(有 slop 的短语 查询)大约快 20 倍。当然，这个代价指的是在搜索时而不是索引时。
事实上，性能上的差距只是证明一个简单的 term 查询有多快。标准全文数据的短语查询通常在几毫秒内完成，因此实际上都是完全可用，即使是在一个繁忙的集群上。
    部分匹配
      常见用例
        匹配邮编、产品序列号或其他 not_analyzed 未分析值，这些值可以是以某个特定前缀开始，也可以是与某种模式匹配的，甚至可以是与某个正则式相匹配的。
        输入即搜索（search-as-you-type） ——在用户键入搜索词过程的同时就呈现最可能的结果。
        匹配如德语或荷兰语这样有长组合词的语言，如： Weltgesundheitsorganisation （世界卫生组织，英文 World Health Organization）。
      底层查询
        prefix 前缀查询
          GET /my_index/address/_search
{
"query": {
"prefix": {
"postcode": "W1"
}
}
}
这个查询会做千万次匹配轮询
        wildcard 通配符查询
          它使用标准的 shell 通配符查询： ? 匹配任意字符， * 匹配 0 或多个字符。
          GET /my_index/address/_search
{
"query": {
"wildcard": {
"postcode": "W?F*HW"
}
}
}
          使用场景：提高召回
            原因1：基础词库不完备；
            原因2：分词粒度不精确。
          底层原理
            Deterministic Finite Automaton
              “自动机过于复杂而无法确定状态： 由于状态和转换太多，确定一个自动机需要生成的状态超过10000个上限"
        regexp
          GET /my_index/address/_search
{
"query": {
"regexp": {
"postcode": "W[0-9].+"
}
}
}
        prefix 、 wildcard 和 regexp 查询是基于词操作的，如果用它们来查询 analyzed 字段，它们会检查字段里面的每个词，而不是将字段作为整体来处理。
比方说包含 “Quick brown fox” （快速的棕色狐狸）的 title 字段会生成词： quick 、 brown 和 fox 。
会匹配以下这个查询：
{ "regexp": { "title": "br.*" }}
        对有很多唯一词的字段执行这些查询可能会消耗非常多的资源，所以要避免使用左通配这样的模式匹配（如： *foo 或 .*foo 这样的正则式）。
在索引时，就要做好多字段准备多模式的设计。
      查询时输入即搜索
        即时搜索（instant search） 或 输入即搜索（search-as-you-type） 。不仅用户能在更短的时间内得到搜索结果，我们也能引导用户搜索索引中真实存在的结果。
        {
"match_phrase_prefix" : {
"brand" : "johnnie walker bl"
}
}
          等价于 match_phrase_prefix "johnnie walker bl*"
        支持的查询选项
          slop
          max_expansions
      Ngrams
        Ngrams 是一个处理词语用的工具链。它相当于在词语上 滑动窗口 ， n 代表这个 “窗口” 的长度。
          quick
            长度 1（unigram）： [ q, u, i, c, k ]
            长度 2（bigram）： [ qu, ui, ic, ck ]
            长度 3（trigram）： [ qui, uic, ick ]
            长度 4（four-gram）： [ quic, uick ]
            长度 5（five-gram）： [ quick ]
        边界 n-grams
          quick
            q
            qu
            qui
            quic
            quick
        使用边界 ngram
          使用 ngram 来配置过滤器
            PUT /my_index
{
"settings": {
"number_of_shards": 1,
"analysis": {
"filter": {
"autocomplete_filter": {
"type": "edge_ngram",
"min_gram": 1,
"max_gram": 20
}
},
"analyzer": {
"autocomplete": {
"type": "custom",
"tokenizer": "standard",
"filter": [
"lowercase",
"autocomplete_filter"
]
}
}
}
}
}
          然后配置字段的分析器
            PUT /my_index/_mapping/my_type
{
"my_type": {
"properties": {
"name": {
"type": "string",
"analyzer": "autocomplete"
}
}
}
}
          这个功能用在这样的用例下，特别有效：使用 Wörterbuch，查出 Aussprachewörtebuch
        使用standard 避免查询被分词过度
          GET /my_index/my_type/_search
{
"query": {
"match": {
"name": {
"query": "brown fo",
"analyzer": "standard"
}
}
}
}
          PUT /my_index/my_type/_mapping
{
"my_type": {
"properties": {
"name": {
"type": "string",
"index_analyzer": "autocomplete",
"search_analyzer": "standard"
}
}
}
}
  ES写入最佳实践
    索引模板的使用
      评估是否需要模板
        在新建一个索引之前，要判断索引未来的数据变化趋势，如果索引数据量是固定的，那可以不使用模板，如果未来数据会不断增加，那强烈建议使用模板定期创建索引。
          模板的使用可以减少每次创建索引的成本
          定期创建索引可以有效减少单个索引的大小，利于后续对历史数据清理和归档，以及做数据冷热隔离
          同时小索引比较容易迁移数据
      基于时间创建索引
        按照月份或者年份或者天来创建索引
        确保单个分片不要超过20G，一半单个索引5个分片，也就是最好索引不要超过100G，业务根据数据增量情况确定创建索引的时间周期
    索引Mapping设计
      Schema设计
        尽管Elasticsearch支持半结构化数据，但是在实际使用中还是应该尽最大可能对数据结构加以控制。
        因为Elasticsearch不支持JOIN操作，所以Schema应该尽量扁平化，减少嵌套。
        对于只需要做精确匹配的字段，应该设置为不做分词，避免使用text类型，5.5以上得版本中通过type=keyword来设定。
        如果字段类型无需排序以及分词，使用keyword性能更好
      参数设定
        index.mapping.total_fields.limit：一个索引中能定义的字段的最大数量，默认是 1000
        index.mapping.depth.limit：字段的最大深度，以内部对象的数量来计算，默认是20
        index.mapping.nested_fields.limit：索引中嵌套字段的最大数量，默认是50
        _source字段：如果_source=true，Elasticsearch会将整个JSON数据也存储下来。如果你的业务中，不需要查询原始数据，只需要根据索引来过滤然后做聚合查询，那么可以将其设置为false，同样可以节省磁盘空间、提高性能。
非海量交易型业务通常保留source，大家觉得这样会更好。
          搜索结果包括了整个可用的文档——不需要额外的从另一个的数据仓库来取文档。
          如果没有 _source 字段，部分 update 请求不会生效。
          当你的映射改变时，你需要重新索引你的数据，有了_source字段你可以直接从Elasticsearch这样做，而不必从另一个（通常是速度更慢的）数据仓库取回你的所有文档。
          当你不需要看到整个文档时，单个字段可以从 _source 字段提取和通过 get 或者 search 请求返回。
          调试查询语句更加简单，因为你可以直接看到每个文档包括什么，而不是从一列id猜测它们的内容。
        dynamic字段：strict，true，false
          索引的动态映射（dynamic mapping）。
如果我们对一个已经映射过的索引新增文档，多出来的 field 会在这里产生 mapping。
这个配置是一个双刃剑：通常没有人知道以后会有什么新字段加到文档，但是又希望这些字段被自动地索引。这就意味着一个容易被污染的mappings 就这样被构造出来了。
            PUT /my_index
{
"mappings": {
"my_type": {
"dynamic": "strict",
"properties": {
"title": { "type": "string"},
"stash": {
"type": "object",
"dynamic": true
}
}
}
}
}
如果遇到新字段，对象 my_type 就会抛出异常。
而内部对象 stash 遇到新字段就会动态创建新字段。
PUT /my_index/my_type/1
{
"title": "This doc adds a new field",
"stash": { "new_field": "Success!" }
}
PUT /my_index/my_type/1
{
"title": "This throws a StrictDynamicMappingException",
"new_field": "Fail!"
}
      es 基础类型
        字符串: string
        整数 : byte, short, integer, long
        浮点数: float, double
        布尔型: boolean
        日期: date
    别名用法
      无缝切换索引
      良好的数据扩展性
    数据写入磁盘过程
    写入索引配置建议
      bulk写入，但是要控制写入的量，一次性不能过多，尽量先压测，建议每批提交5-15M的数据。
        出错会遇到 EsRejectedExecutionException。
        如果只是 binlog 驱动的写入，通常无需这样bulk写入。如果是 ad-hoc 类的立即导入-比如数据重刷，倒是可以采用 bulk api。
      尽量使用es自动生成的document id
      增加refresh间隔，如果在写入期间对实时性要求不高，可以增加refresh的间隔
      写入期间将副本数设置为0，写完后再修改副数可以提高写入效率
      适当增加index buffer的值，如上图，写入期间，数据会先缓存在index buffer中，然后再定期flush到磁盘，所以适当增加这个buffer得值，也能提高写入效率，indices.memory.index_buffer_size:15%，该值在elasticsearch.yml中设置
      副本数量不建议太大，因为写入数据后，主分片要同步数据至副本会消耗大量IO，影响写的性能。
        Querying lots of small shards makes the processing per shard faster, but more queries means more overhead, so querying a smaller number of larger shards might be faster. In short…​it depends.
查询大量小分片可以使每个分片的处理速度更快，但更多 查询意味着更多的开销，因此查询更小的 较大的分片数量可能会更快。简而言之……​这取决于。
          Aim to keep the average shard size between a few GB and a few tens of GB. For use cases with time-based data, it is common to see shards in the 20GB to 40GB range.
目标是将平均分片大小保持在几 GB 到几十 GB 之间。为了具有基于时间的数据的用例，通常会看到 20GB 到 40GB 的分片范围。
          Avoid the gazillion shards problem. The number of shards a node can hold is proportional to the available heap space. As a general rule, the number of shards per GB of heap space should be less than 20.
避免无数碎片问题。一个节点可以容纳的分片数量为 与可用堆空间成正比。作为一般规则，数量 每 GB 堆空间的分片数应小于 20。
        很多人忽略了，ES 没有 MySQL 那样的异步共识机制。
  迁移数据
    COS 快照
      类似 redis 的类似 RDB 的方案
      数据量大的场景（GB、TB、PB 级别）
对迁移速度要求较高的场景。
        基于 COS 快照的迁移方式是使用 ES 的 snapshot api 接口进行迁移，基本原理就是从源 ES 集群创建索引快照，然后在目标 ES 集群中进行恢复。
          PUT _snapshot/my_cos_backup
{
"type": "cos",
"settings": {
"app_id": "xxxxxxx",
"access_key_id": "xxxxxx",
"access_key_secret": "xxxxxxx",
"bucket": "xxxxxx",
"region": "ap-guangzhou",
"compress": true,
"chunk_size": "500mb",
"base_path": "/"
}
}
          PUT _snapshot/my_cos_backup/snapshot_1
          PUT _snapshot/my_cos_backup/snapshot_1?wait_for_completion=true
        备份指定索引
          PUT _snapshot/my_cos_backup/snapshot_2
{
"indices": "index_1,index_2"
}
        从快照恢复
          POST _snapshot/my_cos_backup/snapshot_1/_restore
    logstash
      平滑性最好
      迁移全量或增量数据，且对实时性要求不高的场景
需要对迁移的数据通过 es query 进行简单的过滤的场景
需要对迁移的数据进行复杂的过滤或处理的场景
版本跨度较大的数据迁移场景，如 5.x 版本迁移到 6.x 版本或 7.x 版本
跨版本，过滤
        input {
elasticsearch {
hosts => "1.1.1.1:9200"
user => "elastic"
password => "your_password"
index => "*"
docinfo => true
size => 5000
scroll => "5m"
}
}
output {
elasticsearch {
hosts => ["http://2.2.2.2:9200"]
user => "elastic"
password => "your_password"
index => "%{[@metadata][_index]}"
document_type => "%{[@metadata][_type]}"
document_id => "%{[@metadata][_id]}"
}
}
    elasticsearch-dump
      数据量较小的场景
      开源的 ES 数据迁移工具，基于node
  ES集群配置最佳实践
    磁盘选择
      尽可能使用使用SSD，ES最大的瓶颈往往是磁盘读写性能，SSD要比SATA查询快5-10倍，所以查询要求高的业务建议选择SSD或者PCIE，查询要求不那么高的业务可以选择SATA
      对于数据节点建议使用单机器500G以上的磁盘
      对于协调节点，200G足矣，协调节点不存数据，只做转发和聚合。
很多集群并不专门设计协调节点，也可以让集群开箱即用。
    内存选择
      尽可能选择16G以上，由于ES的段文件(索引文件)存储在缓存中，尽量满足所有的段文件都存在缓存中，提高查询的效率
        segment 是 disk-based format，所以 os file cache 对 lucene 是相当友好的
        堆内存对于排序、聚合是至关重要的
      ES JVM配置机器一半的内存，但是不要超过32G，另外32g给lucene利用为文件系统缓存 segment
      内存和磁盘1：10
    集群角色配置
      数据节点和协调节点隔离，避免协调节点因数据问题down机之后导致整个集群崩溃
      集群尽量配置固定数量的协调节点（一般3个足矣），数据节点可以扩展
    分片和副本设置
      每个数据节点上都尽量分配某个索引的一个分片，使数据足够均匀
      每个索引分片不要超过30G
      单节点数据控制再2T以内
      单索引size小于1g的，主分片数量不得大于5
      单集群索引数量不得大于3k
      单集群（总）分片数量不得大于10w。
均衡性应该很容易达到。
    调大refresh interval调高
      ES 默认每个分片每秒钟都会进行一次refresh，刷新的过程其实就是文档从索引到被搜索的过程，刷新后会生成新的段，这会产生大量的段文件。
很多人意识不到 refresh 那么频繁。
      如果业务对数据的实时性要求不高，可以调高
    translog同步改异步
      PUT /my_index/_settings
{
"index.translog.durability": "async",
"index.translog.sync_interval": "5s"
}
    定期进行段文件合并
    使用索引模板
  ES 集群监控
    集群整体状态
      集群健康状态
      集群熔断状态
      集群是否可见
      自动快照备份状态
      集群总文档数
      分偏重最大文档数
      集群分片总数
      集群最大分片 delete 次数
        出乎意料地高，实际上更新操作会导致这个流程出现
      集群最大分片文档 delete 次数占比
      活跃查询 context 数量
        读写各400时，这个值三节点全集群为15以内
    集群性能指标
      JVM 使用率
      JVM OLD 使用率
      OLD GC 次数
      Old GC 耗时
      JVM OLD 区使用率
      OLD GC 次数
      OLD GC 耗时
      YOUNG GC 次数
      YOUNG GC 耗时
      FIELD Data 缓存
        在 32g 机器上，通常会有1-3g
      集群每秒查询次数
      集群每秒写入次数
      集群每秒 BULK 写入数
      查询拒绝率
      写入拒绝率
      P90 查询任务耗时
      P95 查询任务耗时
      P99 查询任务耗时
      写入任务耗时
      集群 1min 内慢查询个数
    服务器性能指标
      CPU 使用率
      内存使用率
      1分钟负载
      硬盘存储使用率
      硬盘读iops
      硬盘写iops
      硬盘 IO util
      硬盘写流量
    监控api
      GET _nodes/hot_threads
      GET _nodes/jvm
      GET _tasks?actions=*search&detailed
      GET /_nodes/stats
  索引类型
    倒排索引不适合排序，但字段的列存储是易于排序的，所以生成倒排索引的时候还会生成易于排序的 Doc Values。MySQL 不一样的地方是，ElasticSearch 是在索引时产生排序数据结构，MySQL 依赖于 sort buffer。
插入时，就会同时构建这两种索引。这两种索引都是二维表格。正排索引表格的行键是 docId，column是doc的潜在可排序列。倒排索引的行键是被analyzed 过的 doc 的内容（最简单的例子，一个三个单词的doc，会被分解成三行），而column 则是 doc id。
    Doc Values 默认对所有字段启用，除了 analyzed strings`。也就是说所有的数字、地理坐标、日期、IP 和不分析（ `not_analyzed ）字符类型都会默认开启。
    Fielddata 的数据结构可以理解为 text 类型字段的正排索引结构。
    正排索引是文档id和字段value的映射关系数据，可以方便我们快速进行数据聚合和排序。doc -> term
      Doc Values 是在索引时与倒排索引同时生成。也就是说 Doc Values 和 倒排索引 一样，基于 Segement 生成并且是不可变的。同时 Doc Values 和 倒排索引 一样序列化到磁盘，这样对性能和扩展性有很大帮助。
Doc Values 通过序列化把数据结构持久化到磁盘，我们可以充分利用操作系统的内存，而不是 JVM 的 Heap 。 当 working set 远小于系统的可用内存，系统会自动将 Doc Values 驻留在内存中，使得其读写十分快速；不过，当其远大于可用内存时，系统会根据需要从磁盘读取 Doc Values`，然后选择性放到分页缓存中。很显然，这样性能会比在内存中差很多，但是它的大小就不再局限于服务器的内存了。如果是使用 `JVM 的 Heap 来实现那么只能是因为 OutOfMemory 导致程序崩溃了。
因为 Doc Values 不是由 JVM 来管理，所以 Elasticsearch 实例可以配置一个很小的 JVM Heap`，这样给系统留出来更多的内存。同时更小的 `Heap 可以让 JVM 更加快速和高效的回收。
之前，我们会建议分配机器内存的 50% 来给 JVM Heap`。但是对于 `Doc Values`，这样可能不是最合适的方案了。 以 `64gb 内存的机器为例，可能给 Heap 分配 4-16gb 的内存更合适，而不是 `32gb`。
        此处似乎也没有用到很高的 direct buffer 配置，而是直接使用 system vm。
      从广义来说，Doc Values 本质上是一个序列化的列式存储 。 正如我们上一节所讨论的，列式存储 适用于聚合、排序、脚本等操作。
按列布局意味着我们有一个连续的数据块： [100,1000,1500,1200,300,1900,4200] 。因为我们已经知道他们都是数字（而不是像文档或行中看到的异构集合），所以我们可以使用统一的偏移来将他们紧紧排列。
        在搜索的时候，我们能通过搜索关键词快速得到结果集。
        当排序的时候，我们需要倒排索引里面某个字段值的集合。换句话说，我们需要 转置 倒排索引。
为什么？我们总是先搜索出文档，然后再依据临时搜索结果进行排序的。
      PUT my_index
{
"mappings": {
"my_type": {
"properties": {
"session_id": {
"type": "string",
"index": "not_analyzed",
"doc_values": false
}
}
}
}
}
通过设置 doc_values: false ，这个字段将不能被用于聚合、排序以及脚本操作。
      PUT my_index
{
"mappings": {
"my_type": {
"properties": {
"customer_token": {
"type": "string",
"index": "not_analyzed",
"doc_values": true,
"index": "no"
}
}
}
}
}
反过来也是可以进行配置的：让一个字段可以被聚合，通过禁用倒排索引，使它不能被正常搜索。
    FieldData
      特性
        这是一个索引的配置项，一个 text field 的 fielddata 如果不是 true，尝试对它聚合会遇到错误。
        内存管理和生成-常驻内存
        更占内存
          ES 采用了 Circuit Breaker（熔断）机制避免 field data 一次性超过物理内存大小而导致内存溢出，如果触发熔断，查询会被终止并返回异常。
          在早期的 ELK 实践里，对于 text 类型的sort需求促生了 field data 缓存，这类缓存是 old gc的来源。优化这类问题的方法是禁用掉这类缓存，类似我们禁用掉 swap一样。
          lucene 虽然使用大量堆外内存，但 field data 是在堆里的，它甚至是堆内存主要使用者，深刻影响给 ES 的堆内存的大小。
        懒加载
          提前加载
          一个配置开启了 fielddata：true 的 text 字段的在第一次被聚合之前，是不会生成 fielddata 的。
            Fielddata is disabled on text fields by default. Set fielddata=true on [profession] in order to load fielddata in memory by uninverting the inverted index.
          "loading": "eager"
          预构建全局序号（Eager global ordinals）
"loading" : "eager_global_ordinals"
        全加载
          这里有一个令人惊讶的地方。假设你的查询是高度选择性和只返回命中的 100 个结果。大多数人认为 fielddata 只加载 100 个文档。
实际情况是，fielddata 会加载索引中(针对该特定字段的) 所有的文档，而不管查询的特异性。逻辑是这样：如果查询会访问文档 X、Y 和 Z，那很有可能会在下一个查询中访问其他文档。
        基于segment 建立
          与 doc values，倒排索引一样基于 segment 建立而不是基于整个索引建立
        开启的话需要手动开启，使用 fielddata：true
      优化
        限制内存之间的相对大小
          "indices.fielddata.cache.size" : 20%
          "indices.breaker.fielddata.limit" : "40%"
          indices.breaker.request.limit：执行聚合的内存限制，默认40%
          indices.breaker.total.limit：综合上面两个，限制在70%以内
    倒排索引可以让我们根据一个关键字快速定位到含有许多单词（term）文档的位置，主要用于快速检索。term -> doc。
      单词词典（Term Dictionary）
      倒排列表
        列表项
          文档id用于获取原始信息。
          单词频率（TF，Term Frequency），记录该单词在该文档中出现的次数，用于后续相关性算分。
          位置（Position），记录单词在文档中的分词位置（多个），用于做词语搜索（Phrase Query）。
          偏移（Offset），记录单词在文档的开始和结束位置，用于高亮显示。
      索引基本更新思想
        先删再加，这样会有间隙，不过也是很多存储更新的真正流程。
        倒排索引就是对初始文档集合建立的索引结构，一般单词词典都存储在内存，对应的倒排列表存储在磁盘文件中。
        临时索引是在内存中实时建立的倒排索引，其结构和前述的倒排索引是一样的，区别在于词典和倒排列表都在内存中存储。
        新文档进入系统时，实时解析文件并将其加入到临时索引结构中。
        删除文档列表则用来存储已被删除的文档的相应的文档ID，形成一个文档ID列表。
        修改文档可以认为是旧文档先被删除，然后系统再增加一篇新的文档（replacement docment），通过这种间接方式实现对内容更改的支持。
          从旧文档中检索JSON
          修改它
            不带并发控制的修改
              PUT /website/blog/123
{
"title": "My first blog entry",
"text": "I am starting to get the hang of this...",
"date": "2014/01/02"
}
              {
"_index" : "website",
"_type" : "blog",
"_id" : "123",
"_version" : 2,
"created": false
}
            带版本的修改
              在请求 queryString 里指定版本是最简洁的方法
              PUT /website/blog/1?version=1 <1>
{
"title": "My first blog entry",
"text": "Starting to get the hang of this..."
}
              {
"_index": "website",
"_type": "blog",
"_id": "1",
"_version": 2
"created": false
}
创建失败
            局部更新
              只有 date=date + 1 之类的场景，才需要考量局部更新的能力。
          删除旧文档
          索引新文档
      倒排索引四种更新策略
        完全重建策略
          当新增文档到达一定数量，将新增文档和原先的老文档整合，然后利用静态索引创建方法对所有文档重建索引，新索引建立完成后老索引会被遗弃。此法代价高，但是主流商业搜索引擎一般是采用此方式来维护索引的更新（这句话是书中原话）。
        再合并策略
          当新增文档进入系统，解析文档，之后更新内存中维护的临时索引，文档中出现的每个单词，在其倒排表列表末尾追加倒排表列表项；一旦临时索引将指定内存消耗光，即进行一次索引合并，这里需要倒排文件里的倒排列表存放顺序已经按照索引单词字典顺序由低到高排序，这样直接顺序扫描合并即可。其缺点是：因为要生成新的倒排索引文件，所以对老索引中的很多单词，尽管其在倒排列表并未发生任何变化，也需要将其从老索引中取出来并写入新索引中，这样对磁盘消耗是没必要的。
        原地更新策略
          试图改进再合并策略，在原地合并倒排表，这需要提前分配一定的空间给未来插入，如果提前分配的空间不够了需要迁移。实际显示，其索引更新的效率比再合并策略要低。
        混合策略
          出发点是能够结合不同索引更新策略的长处，将不同索引更新策略混合，以形成更高效的方法。
  索引操作
    删除索引
      DELETE /my_index
      DELETE /index_one,index_two
      DELETE /index_*
      DELETE /_all
      DELETE /*
  深入文档
    Lucene 视角下的文档
      在 Lucene 中，一个文档由一组简单的键值对组成。 每个字段都可以有多个值，但至少要有一个值。 类似的，一个字符串可以通过分析过程转化为多个值。Lucene 不关心这些值是字符串、数字或日期—​所有的值都被当做 不透明字节 。
      如果有两个不同的类型，每个类型都有同名的字段，但映射不同（例如：一个是字符串一个是数字），将会出现什么情况？
        简单回答是，Elasticsearch 不会允许你定义这个映射。当你配置这个映射时，将会出现异常。
当代的版本（8.1 以上），则允许同名字段：The properties setting is allowed to have different settings for fields of the same name in the same index. New properties can be added to existing fields using the update mapping API.
        因为类型是 Elasticsearch 添加的 优于 Lucene 的额外机制（以元数据 _type 字段的形式），在 Elasticsearch 中的所有类型最终都共享相同的映射
      {
"data": {
"mappings": {
"people": {
"properties": {
"name": {
"type": "string",
},
"address": {
"type": "string"
}
}
},
"transactions": {
"properties": {
"timestamp": {
"type": "date",
"format": "strict_date_optional_time"
},
"message": {
"type": "string"
}
}
}
}
}
}
实际上
{
"data": {
"mappings": {
"_type": {
"type": "string",
"index": "not_analyzed"
},
"name": {
"type": "string"
}
"address": {
"type": "string"
}
"timestamp": {
"type": "long"
}
"message": {
"type": "string"
}
}
}
}
      映射在本质上被扁平化成一个单一的、全局的模式。这就是为什么两个类型不能定义冲突的字段：当映射被扁平化时，Lucene 不知道如何去处理。
        类似不同 nginx conf 文件里，不能出现同名的 upstream 是一样的。
  索引
    索引定义
      alias
        "insurance.insurance_policy": {}
因为 Lucene不支持 type，不同的 document 在 Lucene 底层是共用同名 field 定义的，容易造成混乱，所以 ES 7.0 以后要移除 type
        两种设计思路：
1. name 作为 key，其他 props 作为{} value
2. 所有的 props 作为 {} value。
方法 1 是 ES 默认的用法，搜索 name 的路径会短一些，其他 props 的路径会长一些。方法 2 正好反过来。
      mappings
        类型名称，如 insurance_policy
作为 properties 的父节点，这个对象被文档当作跟对象。
          _all
            一个把其它字段值当作一个大字符串来索引的特殊字段。 query_string 查询子句(搜索 ?q=john )在没有指定字段时默认使用 _all 字段。这种大字段的合并，也是一种分词设计。
The _all field allows you to search for values in documents without knowing which field contains the value
这个属性在 6.0.0 以后被废弃了。
ES 很喜欢使用下划线来分词。
相应的查询 DSL 是：
{
"query": {
"match": {
"_all": "john smith 1970"
}
}
}
match_all 之类的下划线属性都是 es 的一个隐藏可查询 field。类似把所有的 field 都做成一个冗余字段，类型为 string。这在应用刚开发的时候是有用的，后面慎用。
            PUT my_index
{
"mappings": {
"_default_": { # 1
"_all": {
"enabled": false
}
},
"user": {}, # 2
"blogpost": { # 3
"_all": {
"enabled": true
}
}
}
}
              在mapping的_default_中，将_all字段设置为禁用，该mapping中的所有类型的_all字段默认为禁用。
              user类型从_default_中继承相关设置。
              blogpost类型覆盖相关默认值，并且启用_all字段。
          dynamic_templates，它的 value 是一个数组。
只对本 index 生效。
这个功能的用意，就是让用户用类型、名称匹配，来指定某大类字段的类型。
            es 里的猜是 detection，如 date detection、numeric detection
            attr_名称_类型，这是一个 dynamic_template_name。
它的使用方法是对 field 进行 detect，名称符合 xxx 模式的 field，如果 match_mapping_type 也符合指定的类型（如 string），则把它映射为 ES 的 keyword 类型的 runtime field。
比较好的 dynamic_template 应该是由 es detect 自行产生，工程师再暗地模仿。
              match 映射的类型，如 attr_order_string 的 match 是 attr_order_*
              match_mapping_type 匹配的映射类型，如 string，这些类型实际上源于 json 类型。
这里的类型是 es detect 到的第一个类型，比如输入一个 123，es 默认它就是 integer，当我们想把所有 integer 映射为 long的时候，就先配一个 match_mapping_type。
              mapping
                {
"type": "keyword"
}
这里的 keyword 是一种 field data type。
有时候要有意识地把数据 map 为 long，利于排序。
用来作 is-a 查询的时候使用 keyword。
需要 analyze 作为非结构化数据（自然语言的全文看起来是结构化的，实际上是非结构化的）进行 full-text 搜索的时候使用 text。
日期尽量建模为 date，可以利于范围查找。
指定类型可以触发数据写入的时候的 validation，也可以根据特定的数据类型进行范围查询和比较。
嵌套的 mapping 的 properties 可以使用 "index": "not_analyzed", 来保证这个索引只能被 term 查询不能被 match 查询。
              string 已经被废弃了
            例子
              PUT my_index
{
"mappings": {
"my_type": {
"dynamic_templates": [
{
"integers": {
"match_mapping_type": "long",
"mapping": {
"type": "integer"
}
}
},
{
"strings": {
"match_mapping_type": "string",
"mapping": {
"type": "text",
"fields": {
"raw": {
"type": "keyword",
"ignore_above": 256
}
}
}
}
}
]
}
}}
PUT my_index/my_type/1{
"my_integer": 5, #1
"my_string": "Some string" #2
}
                my_integer字段被映射为integer(整数类型)。
                my_string字段被映射为text(文本类型),并且是keword(关键字)多字段。
              PUT my_index
{
"mappings": {
"my_type": {
"dynamic_templates": [
{
"longs_as_strings": {
"match_mapping_type": "string",
"match": "long_*",
"unmatch": "*_text",
"mapping": {
"type": "long"
}
}
}
]
}
}}
PUT my_index/my_type/1{
"long_num": "5", #1
"long_text": "foo" #2
}
                long_num字段被映射为long(长整型)。
                long_text字段使用默认的string(字符串类型)映射。
              此示例将name对象中的任何字段的值复制到顶级full_name字段，但middle字段除外
                PUT my_index
{
"mappings": {
"my_type": {
"dynamic_templates": [
{
"full_name": {
"path_match": "name.*",
"path_unmatch": "*.middle",
"mapping": {
"type": "text",
"copy_to": "full_name"
}
}
}
]
}
}
}
PUT my_index/my_type/1
{
"name": {
"first": "Alice",
"middle": "Mary",
"last": "White"
}
}
                  match 参数只匹配字段名称， path_match 参数匹配字段在对象上的完整路径，所以 address.*.name 将匹配这样的字段：
{
"address": {
"city": {
"name": "New York"
}
}
}
              在映射中{name}和{dynamic_type}占位符会被替换为字段名称和检测到的动态类型。以下示例将所有的字符串类型设置为使用与该字段名称相同的analyzer(分析器)，并禁用所有非字符串字段的doc_values。
                PUT my_index
{
"mappings": {
"my_type": {
"dynamic_templates": [
{
"named_analyzers": {
"match_mapping_type": "string",
"match": "*",
"mapping": {
"type": "text",
"analyzer": "{name}"
}
}
},
{
"no_doc_values": {
"match_mapping_type":"*",
"mapping": {
"type": "{dynamic_type}",
"doc_values": false
}
}
}
]
}
}
}
PUT my_index/my_type/1
{
"english": "Some English text", # 1
"count": 5 # 2
}
                  english字段通过english分析器映射为string（字符串）字段。
                  count字段通过禁用的doc_values映射为long（长整型）字段。
              默认情况下，elasticsearch将字符串字段映射为具有子keyword(关键字)字段的text(文本)字段。但是，如果你只想索引结构化内容，对全文检索不感兴趣，你可以使用elasticsearch将你的字段映射为“关键字”。请注意，这意味着为了搜索这些字段，你必须搜索与索引完全相同的值。
                PUT my_index
{
"mappings": {
"my_type": {
"dynamic_templates": [
{
"strings_as_keywords": {
"match_mapping_type": "string",
"mapping": {
"type": "keyword"
}
}
}
]
}
}
}
              与上一个例子相反，如果你唯一关心的字符串字段是全文检索，并且如果你不打算在该字符串字段上运行聚合，排序或精确搜索，你可以告诉elasticsearch将它仅映射为文本字段(这是5.0之前的默认行为)
                PUT my_index
{
"mappings": {
"my_type": {
"dynamic_templates": [
{
"strings_as_text": {
"match_mapping_type": "string",
"mapping": {
"type": "text"
}
}
}
]
}
}}
              Norms(规范)是索引时评分的因子。如果你不关心评分，不按分数排序文档，则可能会出现这种情况，你可以在索引中禁用这些评分因子的存储，这样能够节省一些空间。
                PUT my_index
{
"mappings": {
"my_type": {
"dynamic_templates": [
{
"strings_as_keywords": {
"match_mapping_type": "string",
"mapping": {
"type": "text",
"norms": false,
"fields": {
"keyword": {
"type": "keyword",
"ignore_above": 256
}
}
}
}
}
]
}
}
}
              当使用elasticsearch进行时间序列分析，通常你会经常聚合许多数字字段，但不会过滤。在这种情况下，你可以禁用这些字段上的索引来节省磁盘空间，同时可能获得一些索引速度的提升
                PUT my_index
{
"mappings": {
"my_type": {
"dynamic_templates": [
{
"unindexed_longs": {
"match_mapping_type": "long",
"mapping": {
"type": "long",
"index": false
}
}
},
{
"unindexed_doubles": {
"match_mapping_type": "double",
"mapping": {
"type": "float", # 1
"index": false
}
}
}
]
}
}
}
                  大多数人不会在意这样的优化，除非这是一个臃肿的索引。
                  像默认的动态映射规则一样，doubles(双精度)映射为floats(浮点),通常情况下是足够准确的，但只需要一半的磁盘空间。
            动态映射是为了解决："2014-01-01" 应该被映射为 keyword 而不是 date 的问题。
比较好避免这类问题的方法是直接使用手工指定的mapping。但有些人设计表的时候，会出现 attr 表，这就导致 es 的 mappings 必须使用 dynamic template 来消化这些动态字段。
              PUT /my_index
{
"mappings": {
"my_type": {
"date_detection": false
}
}
}
              es 的 dynamic_date_formats 是 "yyyy/MM/dd HH:mm:ss Z||yyyy/MM/dd Z"
              PUT my_index
{
"mappings": {
"my_type": {
"dynamic_date_formats": ["MM/dd/yyyy"]
}
}
}
          properties 主要索引自身的 fields，以及 nest object 的 sub-fields。这些 field 可以来自文档和索引的定制 API。
索引的静态映射来自这里。
            field1
              {"type": keyword}
            field2
              类型也不适宜动态变更，动态变更意味着以往的索引会搜索出错
              {"type": ip}
                null
                  当然，数组可以为空。这相当于存在零值。 事实上，在 Lucene 中是不能存储 null 值的，所以我们认为存在 null 值的域为空域。
下面三种域被认为是空的，它们将不会被索引：
                    "null_value": null,
                    "empty_array": [],
                    "array_with_null_value": [ null ]
                数组
                  es 会使用数组的第一个值来确定全数组的类型，所以全部数组元素必须是同一类型的。
                  当你从 Elasticsearch 得到一个文档，每个数组的顺序和你当初索引文档时一样。你得到的 _source 域，包含与你索引的一模一样的 JSON 文档。
但是，数组是以多值域 索引的—可以搜索，但是无序的。 在搜索的时候，你不能指定 “第一个” 或者 “最后一个”。 更确切的说，把数组想象成 装在袋子里的值 。
                    看起来 es 不支持 inner field sort
                object/nested
              index
                analyzed
                  首先分析字符串，然后索引它。换句话说，以全文索引这个域。
                not_analyzed
                  索引这个域，所以它能够被搜索，但索引的是精确值。不会对它进行分析。
                no
                  不索引这个域。这个域不会被搜索到。
              嵌套的对象的 type 为 object
                {
"name": {
"type": "object",
"properties": {
"full": {
"type": "string"
},
"first": {
"type": "string"
},
"last": {
"type": "string"
}
}
}
}
              analyzer
              type index ananlyzer 是一个 field 关键的三大属性
              我们可以更新一个映射来添加一个新域，但不能将一个存在的域从 analyzed 改为 not_analyzed 。
            分隔法
              dot .
              underscore _ 也用来表明内部属性或者 api
              json :
      settings
        replica 总数量，可以动态变更
        sharding 总数量不易于变更
        refresh_interval
        慢查询日志
          fetch 阈值
          query 阈值
        action.auto_create_index: false
防止自动创建索引，这样可以先指定mappings才允许 index 和 PUT。
    索引模板
      我们可以在使用前创建它（使用 template），也可以让文档自动创建它。
使用自定义的 mapping 的好处是，可以决定日期到底会不会是存储为 Date（可以导致 normalization），还是存储为 String。
      例子
        索引的配置主要包含5个部分
        PUT /_template/my_logs
{
"template": "logstash-*",
"order": 1,
"settings": {
"number_of_shards": 1
},
"mappings": {
"_default_": {
"_all": {
"enabled": false
}
}
},
"aliases": {
"last_3_months": {}
}
}
可以让 my_logs 这个 template 应用于所有 logstash-* 模式的索引
          _template 是插入一个模板，这个模板会根据所有模式，针对未来所有新生模板起作用。
          比如本模板针对所有 logstash 生效。
          aliases 可以让全部新生成的索引，都共用别名
        PUT _template/disable_all_field
{
"order": 0,
"template": "*", #1
"mappings": {
"_default_": { #2
"_all": { #3
"enabled": false
}
}
}}
          将模式*匹配映射应用到一个index(索引)，换句话说，就是应用到所有新的索引。
          在索引中定义_default_类型的映射。
          默认情况下禁用_all字段。
    状态
      open
      close
    索引名称
      原始名称
      别名
    总文档数 docs
    总索引大小
    重索引 reindex
      old school
        用 scroll 从旧的索引检索批量文档 ， 然后用 bulk API 把文档推送到新的索引中。
          GET /old_index/_search?scroll=1m
{
"query": {
"range": {
"date": {
"gte": "2014-01-01",
"lt": "2014-02-01"
}
}
},
"sort": ["_doc"],
"size": 1000
}
这里的设计区间是前闭后开，这样就会不漏不重。
            最后一个区间一定会产生最后一个 gte-lt 组合。
      new school
        POST _reindex
{
"source": {
"index": "my-index-000001"
},
"dest": {
"index": "my-new-index-000001"
}
}
    索引别名
      用途
        在运行的集群中可以无缝的从一个索引切换到另一个索引
          这样索引查不到也无问题，是线上迁移的利器。
        给多个索引分组 (例如， last_three_months)
        给索引的一个子集创建视图
      管理别名
        _alias 用于单个操作
          从被别名的索引出发，建立别名。
            PUT /my_index_v1
PUT /my_index_v1/_alias/my_index
          你可以检测这个别名指向哪一个索引
            GET /*/_alias/my_index
          或哪些别名指向这个索引
            GET /my_index_v1/_alias/*
          {
"my_index_v1" : {
"aliases" : {
"my_index" : { }
}
}
}
        _aliases 用于执行多个原子级操作
          一个别名可以指向多个索引，所以我们在添加别名到新索引的同时必须从旧的索引中删除它。这个操作需要原子化，这意味着我们需要使用 _aliases 操作。
这个所以并不那么显然。
            POST /_aliases
{
"actions": [
{ "remove": { "index": "my_index_v1", "alias": "my_index" }},
{ "add": { "index": "my_index_v2", "alias": "my_index" }}
]
}
这个例子里，原子式地切换，不能解决新索引数据追不上旧索引的问题。归根结底，难处理的是双写。
    不变性
      不需要锁。如果你从来不更新索引，你就不需要担心多进程同时修改数据的问题。
      一旦索引被读入内核的文件系统缓存，便会留在哪里，由于其不变性。只要文件系统缓存中还有足够的空间，那么大部分读请求会直接请求内存，而不会命中磁盘。这提供了很大的性能提升。
      其它缓存(像filter缓存)，在索引的生命周期内始终有效。它们不需要在每次数据改变时被重建，因为数据不会变化。
      写入单个大的倒排索引允许数据被压缩，减少磁盘 I/O 和 需要被缓存到内存的索引的使用量。
  API
    Java
      Index
        全量更新文档，产生新的版本。_index 和 _id 可以唯一标识一个文档。等价于 RESTful API 的 Post 和 Put。需要带有 _create 之类的参数才能实现类似 MySQL 唯一索引约束异常的校验。相当于 post 语义。
很多实践，用index 当 upsert，用来做搜索引擎的统一写接口。
      update
        相当于 put 语义（而不是 patch），部分更新文档，检索-修改-重建索引-这是 document store 的标准操作
        可以使用 "script" 脚本进行更新，支持原子操作，可以配合重试参数
      get
        get 如果处理不好，可能出现大成本的查询从而拖垮整个集群，例如全索引的模糊搜索，可能造成很大的 iowait，产生大量的 cpu.busy 告警和 ygc count 告警。
        查询条件和值之间使用 : 分割，如果有必要使用 urlencoded 编码
      mget
      bulk
        可以在 body 里面搭配使用其他 API
      delete
      这个api在可搜索以前，就能读到任意文档（我们读文档主要是为了读 source）。这种读是实时读（realtime），api 参数里甚至还有一个 realtime 参数。在5.0以前的版本，实时读可以从 translog 里读，但之后必须从 lucene 的自包含 index 里读了。
这个流程不走 query_then_fetch。
    RESTful
      _search
        /_search
        在所有索引的所有类型中搜索
          /gb/_search
        在索引gb的所有类型中搜索
          /gb,us/_search
        在索引gb和us的所有类型中搜索
          /g*,u*/_search
        在以g或u开头的索引的所有类型中搜索
          /gb/user/_search
        在索引gb的类型user中搜索
          /gb,us/user,tweet/_search
        在索引gb和us的类型为user和tweet中搜索
          /_all/user,tweet/_search
      _analyze
      PUT /{index}/{type}/{id}
{
"field": "value",
...
}
PUT /website/blog/123?op_type=create
      GET /megacorp/employee/1
GET /megacorp/employee/_search?q=last_name:Smith
      GET /_search?size=5&from=10 谨防深度分页，深度分页随着分片数量上升而上升
      GET /megacorp/employee/_search
{
"query" : {
"match" : {
"about" : "rock climbing"
}
},
"_source": [ "title", "created" ]
}
我们可以基于 _source 过滤 field，节省返回贷款。
        match 在对文本搜索时是 fulltext（除非它的 index 是 not_analyzed），对于数字、日期、布尔值则是精确搜索。
full text search 和 exact value 严重的倒排索引表格是不一样的。
        {
"multi_match": {
"query": "full text search",
"fields": [ "title", "body" ]
}
}
可以在多个 field 上执行相同条件的搜索
        {
"range": {
"age": {
"gte": 20,
"lt": 30
}
}
}
      GET /megacorp/employee/_search
{
"query": {
"match": {
"last_name": "smith"
}
},
"aggs": {
"all_interests": {
"terms": {
"field": "interests"
}
}
}
}
      PUT /blogs
{
"settings" : {
"number_of_shards" : 3,
"number_of_replicas" : 1
}
}
        number_of_shards 在索引创建后不能修改（和 hbase 略有不同），默认值是 5。
在 es 6以后，有 shrink 和 split api 可以修改每个分片的大小。
        number_of_replicas
默认值是 1，对于活动的索引库，这个配置可以随时修改。
      curl -i -XHEAD http://localhost:9200/website/blog/123
      DELETE /website/blog/123
      部分更新 POST /website/blog/1/_update
{
"doc" : {
"tags" : [ "testing" ],
"views": 0
}
}
      GET /_mget
        批量使用 _index、_type、_id 获取数据，大部分功能都可以被 _search 代替
      POST /_bulk
{ "create": { "_index": "website", "_type": "blog", "_id": "123" }}
{ "title": "Cannot create - it already exists" }
{ "index": { "_index": "website", "_type": "blog", "_id": "123" }}
{ "title": "But we can update it" }
      GET /gb/_mapping/tweet
      PUT /gb/_mapping/tweet
{
"properties" : {
"tag" : {
"type" : "string",
"index": "not_analyzed"
}
}
}
        尽管你可以增加 一个存在的映射，你不能修改 存在的域映射。如果一个域的映射已经存在，那么该域的数据可能已经被索引。如果你意图修改这个域的映射，索引的数据可能会出错，不能被正常的搜索。
      POST _reindex
{
"source": {
"index": "my-index-000001"
},
"dest": {
"index": "my-new-index-000001"
}
}
        1. es 不支持索引重命名
2. 改变分片大小需要搭配使用新的 number_of_shards 的索引、 reindex 和别名来解决
    外部动词：http method
内部动词：_search/_update
  搜索算法
    相似度算法
      检索词频率 TF
        检索词在该字段出现的频率？出现频率越高，相关性也越高。
字段中出现过5次要比只出现过1次的相关性高。
      反向文档频率 IDF
        每个检索词在索引中出现的频率？频率越高，相关性越低。
检索词出现在多数文档中会比出现在少数文档中的权重更低，
即检验一个检索词在文档中的普遍重要性。
      字段长度准则
        字段的长度是多少？长度越长，相关性越低。 检索词出现在一个短的 title 要比同样的词出现在一个长的 content 字段权重更大。
      GET /_search?explain <1>
{
"query" : { "match" : { "tweet" : "honeymoon" }}
}
可以显示最后的 score。
        _explanation
          description
            计算的类型
          value
            计算结果
          details
            计算细节
        "_explanation": {
"description": "weight(tweet:honeymoon in 0)
[PerFieldSimilarity], result of:",
"value": 0.076713204,
"details": [
{
"description": "fieldWeight in 0, product of:",
"value": 0.076713204,
"details": [
{
"description": "tf(freq=1.0), with freq of:",
"value": 1,
"details": [
{
"description": "termFreq=1.0",
"value": 1
}
]
},
{
"description": "idf(docFreq=1, maxDocs=1)",
"value": 0.30685282
},
{
"description": "fieldNorm(doc=0)",
"value": 0.25,
}
]
}
]
}
      如何确认某个文档不会被匹配？极端重要
        GET /us/tweet/12/_explain
{
"query" : {
"bool" : {
"filter" : { "term" : { "user_id" : 2 }},
"must" : { "match" : { "tweet" : "honeymoon" }}
}
}
}
        "failure to match filter: cache(user_id:[2 TO 2])"
      因为词频率和 文档频率是在每个分片中计算出来的，而不是每个索引中
    分布式搜索的查询模型
    基于 scroll 的游标搜索
      游标查询允许我们 先做查询初始化，然后再批量地拉取结果。 这有点儿像传统数据库中的 cursor 。
      游标查询会取某个时间点的快照数据。 查询初始化之后索引上的任何变化会被它忽略。 它通过保存旧的数据文件来实现这个特性，结果就像保留初始化时的索引 视图 一样。
所以 scroll 不是实时数据。
      深度分页的代价根源是结果集【动态】全局排序，如果去掉全局排序的特性的话查询结果的成本就会很低。
      scroll不适合支持那种实时的和用户交互的前端分页工作，其主要用途用于从ES集群分批拉取大量结果集的情况，一般都是offline的应用场景。 比如需要将非常大的结果集拉取出来，存放到其他系统处理，或者需要做大索引的reindex等等。 不要把 scroll 用于实时请求，它主要用于大数据量的场景。例如：将一个索引的内容索引到另一个不同配置的新索引中。
      步骤
        初始化一个 scroll，得到一个 scroll id
GET /old_index/_search?scroll=1m
{
"query": { "match_all": {}},
"sort" : ["_doc"],
"size": 1000
}
保持游标查询窗口一分钟。
关键字 _doc 是最有效的排序顺序。
        滚动取值
GET /_search/scroll
{
"scroll": "1m",
"scroll_id" : "cXVlcnlUaGVuRmV0Y2g7NTsxMDk5NDpkUmpiR2FjOFNhNnlCM1ZDMWpWYnRROzEwOTk1OmRSamJHYWM4U2E2eUIzVkMxalZidFE7MTA5OTM6ZFJqYkdhYzhTYTZ5QjNWQzFqVmJ0UTsxMTE5MDpBVUtwN2lxc1FLZV8yRGVjWlI2QUVBOzEwOTk2OmRSamJHYWM4U2E2eUIzVkMxalZidFE7MDs="
}
这个游标查询返回的下一批结果。 尽管我们指定字段 size 的值为1000，我们有可能取到超过这个值数量的文档。 当查询的时候， 字段 size 作用于单个分片，所以每个批次实际返回的文档数量最大为 size * number_of_primary_shards 。
注意游标查询每次返回一个新字段 _scroll_id。每次我们做下一次游标查询， 我们必须把前一次查询返回的字段 _scroll_id 传递进去。 当没有更多的结果返回的时候，我们就处理完所有匹配的文档了。
        在时间窗口内完成的话删除 scroll 的值
  聚合查询
    聚合类型
      MetricsAgg：指标聚合，统计某个的字段的平均（同类是某一个 field 值？）、最大值等的，类似于select avg(saleAmout)...group by。
指标的差异在于操作符，term是桶聚合天然的操作符，avg之类的就不是。
        stats
          相当于 count、min、max、avg、sum
        avg 求平均值
          每种颜色的平均价格是多少，这比下面的查询，在总的大桶里计算平均价格更常在现实中出现。
            GET /cars/transactions/_search
{
"size" : 0,
"aggs": {
"colors": {
"terms": {
"field": "color"
},
"aggs": {
"avg_price": {
"avg": {
"field": "price"
}
}
}
}
}
}
              es 的经典设计模式是，只用 terms分桶，然后再建一个嵌套的metric，如 avg，使用这个avg带来的结果。
          在第一层默认桶里使用avg metric
            GET /cars/transactions/_search
{
"size" : 0,
"aggs": {
"avg_price": {
"avg": {
"field": "price"
}
}
}
}
这样也可以
              任意一组 bucket/metric 都是 agg + agg name + field 对象嵌套
              所有的嵌套，都发生在name 内
                但每一个name 内部，同级只能有一个aggs。
                每个 aggs 可能有多个 key，也就是在一个桶内，第二层可以嵌套一个桶和一个指标。
        max 的意思是组内求最大值
          在二级桶里求多个 min/max
            GET /cars/transactions/_search
{
"size" : 0,
"aggs": {
"colors": {
"terms": {
"field": "color"
},
"aggs": {
"avg_price": { "avg": { "field": "price" }
},
"make" : {
"terms" : {
"field" : "make"
},
"aggs" : {
"min_price" : { "min": { "field": "price"} },
"max_price" : { "max": { "field": "price"} }
}
}
}
}
}
}
        sum 的意思是组内值求和
        value_count
          类似于 term
          区别于 doc_count
        string_stats
      BucketAgg：桶聚合，统计每个key的行数汇总（同类是某一个key 值？），类似于mysql的select count(1)...group by。
桶聚合不跨字段计算指标，但创建文档的桶。
每个桶与一个标准 criterion 关联（也依赖于当前的聚合类型），标准决定了，当前的文档是否会掉入桶中。换句话说，桶实际上定义了文档集。bucket基于这种掉入，返回计算结果。
与 metrics 相反，bucket支持嵌套 bucket。
        建桶策略
          建单桶
            terms 配上 fields
          建多桶
          在聚合过程中，动态建桶
        建桶关键字
          term
            根据 field 的唯一值分桶
          histogram
            根据 field + interval 分桶
            GET /cars/transactions/_search
{
"size" : 0,
"aggs":{
"price":{
"histogram":{
"field": "price",
"interval": 20000
},
"aggs":{
"revenue": {
"sum": {
"field" : "price"
}
}
}
}
}
}
            我们可能会注意到空的区间，比如：$40，000-60，000，没有出现在响应中。 histogram 桶默认会忽略它，因为它有可能会导致不希望的潜在错误输出。
          date_histogram
            GET /cars/transactions/_search
{
"size" : 0,
"aggs": {
"sales": {
"date_histogram": {
"field": "sold",
"interval": "month",
"format": "yyyy-MM-dd"
}
}
}
}
            GET /cars/transactions/_search
{
"size" : 0,
"aggs": {
"sales": {
"date_histogram": {
"field": "sold",
"interval": "month",
"format": "yyyy-MM-dd",
"min_doc_count" : 0,
"extended_bounds" : {
"min" : "2014-01-01",
"max" : "2014-12-31"
}
}
}
}
}
              "min_doc_count" : 0
补0问题是很常见的
          全局桶
            GET /cars/transactions/_search
{
"size" : 0,
"query" : {
"match" : {
"make" : "ford"
}
},
"aggs" : {
"single_avg_price": {
"avg" : { "field" : "price" }
},
"all": {
"global" : {},
"aggs" : {
"avg_price": {
"avg" : { "field" : "price" }
}
}
}
}
}
              "global" : {}, 意味着这个aggs 是不受顶层 query 的条件影响的
          过滤
            针对桶过滤
              GET /cars/transactions/_search
{
"size" : 0,
"query":{
"match": {
"make": "ford"
}
},
"aggs":{
"recent_sales": {
"filter": {
"range": {
"sold": {
"from": "now-1M"
}
}
},
"aggs": {
"average_price":{
"avg": {
"field": "price"
}
}
}
}
}
}
                在搜索的hits 里，不不包含时间过滤条件的
                但经过时间过滤的文档才能参与桶内的 metric 聚合
                这是 ES 允许同时返回 hits 和 aggregations 的结果
            针对查询结果做后过滤，未过滤的结果进入桶
              GET /cars/transactions/_search
{
"size" : 0,
"query": {
"match": {
"make": "ford"
}
},
"post_filter": {
"term" : {
"color" : "green"
}
},
"aggs" : {
"all_colors": {
"terms" : { "field" : "color" }
}
}
}
                在我们需要不同过滤时， post_filter 只与聚合一起使用。
          桶排序
            内置排序
              GET /cars/transactions/_search
{
"size" : 0,
"aggs" : {
"colors" : {
"terms" : {
"field" : "color",
"order": {
"_count" : "asc"
}
}
}
}
}
                _count
                  按文档数排序。对 terms 、 histogram 、 date_histogram 有效。
                _term
                  按词项的字符串值的字母顺序排序。只在 terms 内使用。
                _key
                  按每个桶的键值数值排序（理论上与 _term 类似）。 只在 histogram 和 date_histogram 内使用。
            按照度量排序
              GET /cars/transactions/_search
{
"size" : 0,
"aggs" : {
"colors" : {
"terms" : {
"field" : "color",
"order": {
"avg_price" : "asc"
}
},
"aggs": {
"avg_price": {
"avg": {"field": "price"}
}
}
}
}
}
            按深度度量排序
              GET /cars/transactions/_search
{
"size" : 0,
"aggs" : {
"colors" : {
"histogram" : {
"field" : "price",
"interval": 20000,
"order": {
"red_green_cars>stats.variance" : "asc"
}
},
"aggs": {
"red_green_cars": {
"filter": { "terms": {"color": ["red", "green"]}},
"aggs": {
"stats": {"extended_stats": {"field" : "price"}}
}
}
}
}
}
}
          对桶再计算
            gap_policy
              skip
                This option treats missing data as if the bucket does not exist. It will skip the bucket and continue calculating using the next available value.
              insert_zeros
                This option will replace missing values with a zero (0) and pipeline aggregation computation will proceed as normal.
              keep_values
                This option is similar to skip, except if the metric provides a non-null, non-NaN value this value is used, otherwise the empty bucket is skipped.
            bucket_selector
              {
"bucket_selector": {
"buckets_path": {
"my_var1": "the_sum",
"my_var2": "the_value_count"
},
"script": "params.my_var1 > params.my_var2"
}
}
                通常还要配 script 元素
          三个不可能
            大数据、精确性和实时性
              精确 + 实时
              大数据 + 精确
              大数据 + 实时
                近似计算
                  cardinality
                    GET /cars/transactions/_search
{
"size" : 0,
"aggs" : {
"distinct_colors" : {
"cardinality" : {
"field" : "color",
"precision_threshold" : 100
}
}
}
}
                  percentiles
                    GET /website/logs/_search
{
"size" : 0,
"aggs" : {
"load_times" : {
"percentiles" : {
"field" : "latency"
}
},
"avg_load_time" : {
"avg" : {
"field" : "latency"
}
}
}
}
                      百分位数通常用来找出异常。在（统计学）的正态分布下，第 0.13 和 第 99.87 的百分位数代表与均值距离三倍标准差的值。任何处于三倍标准差之外的数据通常被认为是不寻常的，因为它与平均值相差太大。
                  percentile_ranks
                  significant_terms
                    被忽略的异常统计方法
          其他操作符
            top_hits
              桶内用特定的size返回排序最高文档-类似先求 max 10、max20，然后把相关文档全部取出来，作为 top10、top20
          小结
            在 filter 过滤中的 non-scoring 查询，同时影响搜索结果和聚合结果。
            filter 桶影响聚合。
            post_filter 只影响搜索结果。
        Adjacency matrix aggregation 邻接矩阵聚合
        Children Aggregation
        Composite Aggregation
      PipelineAgg：管道聚合，脚本，排序实现。
    排序分页
      排序：只能在同一层聚合层下进行，桶排序也只能针对同一层聚合，无法实现跨聚合层的排序，具体参考下图具体聚合结果结构。
        举个例子，现有两层聚合，cityId 为一级聚合，skuId 为二级聚合，要统计不同 cityId 下不同skuId 的销量，skuId 的销量排序只能在同一个 cityId 下，跨 city 需要额外在 java 层实现。
      分页：和排序一样，分页也只能在同一层聚合下进行分页（也可以在业务层实现)，分页有个难点在于统计key的总量，如果使用类似having的条件限制，需要额外统计key的总量。
      size 0
        如果不使用aggs以外的数据，可以设值 size 为0，可以节省内存，提高性能
        这只是确定 hits的大小，aggs 进行聚合的被圈定大小不受 size 0 的影响。
    广度优先算法
      "collect_mode" : "breadth_first"
        广度优先仅仅适用于每个组的聚合数量远远小于当前总组数的情况下
    聚合查询关键字
      AggregationBuilder：聚合查询最外层的builder，功能非常强大，可以实现指标和桶聚合（对应 avg 和 sum），比如avg、sum、count,。max、min，多个字段聚合可以通过subAggregation来添加子聚合，不建议深度聚合，不要超过2层。
      CardinalityAggregationBuilder：去重汇总的关键字。
      CollapseBuilder：折叠查询关键字。
      BucketSelectorPipelineAggregationBuilder：聚合中使用脚本计算或者过滤关键字。
      BucketSortPipelineAggregationBuilder：桶聚合需要排序关键字。
    例子
      一般的 avg 和 sum 的agg
        select skuId, avg(salesAmout), sum(salesAmout) from sku where cityId = 88 group by skuId;
        {
"size": 0,
"query": {
"term": {
"cityId": {
"value": 88,
"boost": 1
}
}
},
"aggregations": {
"skuIdAgg": {
"terms": {
"field": "skuId",
"size": 1000,
"min_doc_count": 1,
"shard_min_doc_count": 0,
"show_term_doc_count_error": false,
"order": [
{
"_count": "desc"
},
{
"_key": "asc"
}
]
},
"aggregations": {
"calcSalesAmountAvg": {
"avg": {
"field": "calcSalesAmount"
}
},
"calcSalesAmountSum": {
"sum": {
"field": "calcSalesAmount"
}
}
}
}
}
}
          一个桶2个指标，分桶有多余的配置
            shard_min_doc_count 默认就是0
            min_doc_count 默认就是1
        @Test
public void testMetricsAgg() {
SearchRequest searchRequest = new SearchRequest(INDEX_NAME);
SearchSourceBuilder searchSourceBuilder = new SearchSourceBuilder();
String avgAmountKey = "calcSalesAmountAvg";
String sumAmountKey = "calcSalesAmountSum";
String skuIdAggkey = "skuIdAgg";
//group by Agg
AggregationBuilder skuIdAgg = AggregationBuilders.terms(skuIdAggkey).field("skuId").size(1000);
//基于SKUID做sum/avg
AggregationBuilder saleAmountAvgAgg = AggregationBuilders.avg(avgAmountKey).field("calcSalesAmount");
AggregationBuilder saleAmountSumAgg = AggregationBuilders.sum(sumAmountKey).field("calcSalesAmount");
//将指标聚合加到skuId上
skuIdAgg.subAggregation(saleAmountAvgAgg);
skuIdAgg.subAggregation(saleAmountSumAgg);
//查询条件
TermQueryBuilder termQuery = QueryBuilders.termQuery("cityId", 44);
//将条件添加至searchRequest
searchSourceBuilder.aggregation(skuIdAgg);
searchSourceBuilder.query(termQuery);
searchRequest.source(searchSourceBuilder);
//执行查询
SearchResponse response = null;
try {
response = porosRestHighLevelClient.search(searchRequest, RequestOptions.DEFAULT);
} catch (Exception e) {
log.error("call es 查询信息异常", e);
}
//解析结果
if (response != null && response.status().getStatus() == SUCCESS) {
Aggregations aggregations = response.getAggregations();
ParsedStringTerms skuIdAggTerms = aggregations.get(skuIdAggkey);
//解析每个bucket
for (Terms.Bucket bucket : skuIdAggTerms.getBuckets()) {
ParsedSum sum = bucket.getAggregations().get(sumAmountKey);
ParsedAvg avg = bucket.getAggregations().get(avgAmountKey);
String skuId = bucket.getKey().toString();
long count = bucket.getDocCount();
}
}
}
      指标聚合+having
        Select skuId, avg(salesAmout) as amtAvg, sum(salesAmout) as amtSum from sku where cityId = 88 group by skuId having amtSum>100;
        {
"size": 0,
"query": {
"term": {
"cityId": {
"value": 88,
"boost": 1
}
}
},
"aggregations": {
"skuIdAgg": {
"terms": {
"field": "skuId",
"size": 1000,
"min_doc_count": 1,
"shard_min_doc_count": 0,
"show_term_doc_count_error": false,
"order": [
{
"_count": "desc"
},
{
"_key": "asc"
}
]
},
"aggregations": {
"calcSalesAmountAvg": {
"avg": {
"field": "calcSalesAmount"
}
},
"calcSalesAmountSum": {
"sum": {
"field": "calcSalesAmount"
}
},
"having": {
"bucket_selector": {
"buckets_path": {
"calcSalesAmountSum": "calcSalesAmountSum"
},
"script": {
"source": "params.calcSalesAmountSum > 18000",
"lang": "painless"
},
"gap_policy": "skip"
}
}
}
}
}
}
        @Test
public void testMetricsAgg() {
SearchRequest searchRequest = new SearchRequest(INDEX_NAME);
SearchSourceBuilder searchSourceBuilder = new SearchSourceBuilder();
String avgAmountKey = "calcSalesAmountAvg";
String sumAmountKey = "calcSalesAmountSum";
String skuIdAggkey = "skuIdAgg";
//查询条件
TermQueryBuilder termQuery = QueryBuilders.termQuery("cityId", 44);
//group by Agg
AggregationBuilder skuIdAgg = AggregationBuilders.terms(skuIdAggkey).field("skuId").size(1000);
//基于SKUID做sum/avg
AggregationBuilder saleAmountAvgAgg = AggregationBuilders.avg(avgAmountKey).field("calcSalesAmount");
AggregationBuilder saleAmountSumAgg = AggregationBuilders.sum(sumAmountKey).field("calcSalesAmount");
//<!-----聚合之后的having条件------>
//having中要使用的聚合字段，名字可以一样
Map<String, String> bucketsPathsMap = new HashMap<>(8);
bucketsPathsMap.put("calcSalesAmountSum", "calcSalesAmountSum");
//设置having语句
StringBuilder havingParams = new StringBuilder();
havingParams.append("params.calcSalesAmountSum > 1000");
Script havingScript = new Script(havingParams.toString());
//构建having bucket选择器
BucketSelectorPipelineAggregationBuilder havingBs = PipelineAggregatorBuilders.bucketSelector("having", bucketsPathsMap, havingScript);
//having条件添加至skuId外层聚
skuIdAgg.subAggregation(havingBs);
//将指标聚合加到skuId上
skuIdAgg.subAggregation(saleAmountAvgAgg);
skuIdAgg.subAggregation(saleAmountSumAgg);
//将条件添加至searchRequest
searchSourceBuilder.aggregation(skuIdAgg);
searchSourceBuilder.query(termQuery);
searchRequest.source(searchSourceBuilder);
searchSourceBuilder.size(0);
//执行查询
SearchResponse response = null;
try {
response = porosRestHighLevelClient.search(searchRequest, RequestOptions.DEFAULT);
} catch (Exception e) {
log.error("call es 查询信息异常", e);
}
//解析结果
if (response != null && response.status().getStatus() == SUCCESS) {
Aggregations aggregations = response.getAggregations();
ParsedStringTerms skuIdAggTerms = aggregations.get(skuIdAggkey);
//解析每个bucket
for (Terms.Bucket bucket : skuIdAggTerms.getBuckets()) {
ParsedSum sum = bucket.getAggregations().get(sumAmountKey);
ParsedAvg avg = bucket.getAggregations().get(avgAmountKey);
String skuId = bucket.getKey().toString();
long count = bucket.getDocCount();
}
}
}
}
      指标聚合排序分页
        Select skuId, avg(salesAmout) as amtAvg, sum(salesAmout) as amtSum from sku where cityId = 71 group by skuId order by amtAvg limit 0,10;
        {
"size": 0,
"query": {
"term": {
"cityId": {
"value": 88,
"boost": 1
}
}
},
"aggregations": {
"skuIdAgg": {
"terms": {
"field": "skuId",
"size": 1000,
"min_doc_count": 1,
"shard_min_doc_count": 0,
"show_term_doc_count_error": false,
"order": [
{
"_count": "desc"
},
{
"_key": "asc"
}
]
},
"aggregations": {
"calcSalesAmountAvg": {
"avg": {
"field": "calcSalesAmount"
}
},
"calcSalesAmountSum": {
"sum": {
"field": "calcSalesAmount"
}
},
"having": {
"bucket_selector": {
"buckets_path": {
"calcSalesAmountSum": "calcSalesAmountSum"
},
"script": {
"source": "params.calcSalesAmountSum > 1000",
"lang": "painless"
},
"gap_policy": "skip"
}
},
"page_field": {
"bucket_sort": {
"sort": [
{
"calcSalesAmountAvg": {
"order": "asc"
}
}
],
"from": 0,
"size": 10,
"gap_policy": "SKIP"
}
}
}
}
}
}
        @Test
public void testMetricsAgg() {
SearchRequest searchRequest = new SearchRequest(INDEX_NAME);
SearchSourceBuilder searchSourceBuilder = new SearchSourceBuilder();
String avgAmountKey = "calcSalesAmountAvg";
String sumAmountKey = "calcSalesAmountSum";
String skuIdAggkey = "skuIdAgg";
//查询条件
TermQueryBuilder termQuery = QueryBuilders.termQuery("cityId", 44);
//group by Agg
AggregationBuilder skuIdAgg = AggregationBuilders.terms(skuIdAggkey).field("skuId").size(1000);
//基于SKUID做sum/avg
AggregationBuilder saleAmountAvgAgg = AggregationBuilders.avg(avgAmountKey).field("calcSalesAmount");
AggregationBuilder saleAmountSumAgg = AggregationBuilders.sum(sumAmountKey).field("calcSalesAmount");
//<!-----聚合之后的having条件------>
//having中要使用的聚合字段，名字可以一样
Map<String, String> bucketsPathsMap = new HashMap<>(8);
bucketsPathsMap.put("calcSalesAmountSum", "calcSalesAmountSum");
//设置having语句
StringBuilder havingParams = new StringBuilder();
havingParams.append("params.calcSalesAmountSum > 1000");
Script havingScript = new Script(havingParams.toString());
//构建having bucket选择器
BucketSelectorPipelineAggregationBuilder havingBs = PipelineAggregatorBuilders.bucketSelector("having", bucketsPathsMap, havingScript);
//having条件添加至skuId外层聚
skuIdAgg.subAggregation(havingBs);
//<-------聚合指标排序分页------>
List<FieldSortBuilder> fieldSorts = new ArrayList<>();
fieldSorts.add(new FieldSortBuilder(avgAmountKey).order(SortOrder.ASC));
//桶排序builder，注意排序builder要添加至上上层聚合agg，比如本次聚合是针对skuId，所以改排序builder要添加至skuIdAgg
BucketSortPipelineAggregationBuilder pageBuilder = new BucketSortPipelineAggregationBuilder("page_field", fieldSorts).from(0).size(10);
skuIdAgg.subAggregation(pageBuilder);
//将指标聚合加到skuId上
skuIdAgg.subAggregation(saleAmountAvgAgg);
skuIdAgg.subAggregation(saleAmountSumAgg);
//将条件添加至searchRequest
searchSourceBuilder.aggregation(skuIdAgg);
searchSourceBuilder.query(termQuery);
searchRequest.source(searchSourceBuilder);
searchSourceBuilder.size(0);
//执行查询
SearchResponse response = null;
try {
response = porosRestHighLevelClient.search(searchRequest, RequestOptions.DEFAULT);
} catch (Exception e) {
log.error("call es 查询信息异常", e);
}
//解析结果
if (response != null && response.status().getStatus() == SUCCESS) {
Aggregations aggregations = response.getAggregations();
ParsedStringTerms skuIdAggTerms = aggregations.get(skuIdAggkey);
//解析每个bucket
for (Terms.Bucket bucket : skuIdAggTerms.getBuckets()) {
ParsedSum sum = bucket.getAggregations().get(sumAmountKey);
ParsedAvg avg = bucket.getAggregations().get(avgAmountKey);
String skuId = bucket.getKey().toString();
long count = bucket.getDocCount();
}
}
}
      桶聚合
        select count(*),skuId from sku where cityId = 88 group by skuId;
        {
"size": 0,
"query": {
"term": {
"cityId": {
"value": 88,
"boost": 1
}
}
},
"aggregations": {
"skuIdAgg": {
"terms": {
"field": "skuId",
"size": 1000,
"min_doc_count": 1,
"shard_min_doc_count": 0,
"show_term_doc_count_error": false,
"order": [
{
"_count": "desc"
},
{
"_key": "asc"
}
]
}
}
}
}
        @Test
public void testBucketAgg() {
SearchRequest searchRequest = new SearchRequest(INDEX_NAME);
SearchSourceBuilder searchSourceBuilder = new SearchSourceBuilder();
String skuIdAggkey = "skuIdAgg";
//查询条件
TermQueryBuilder termQuery = QueryBuilders.termQuery("cityId", 44);
//group by Agg
AggregationBuilder skuIdAgg = AggregationBuilders.terms(skuIdAggkey).field("skuId").size(1000);
//将条件添加至searchRequest
searchSourceBuilder.aggregation(skuIdAgg);
searchSourceBuilder.query(termQuery);
searchRequest.source(searchSourceBuilder);
searchSourceBuilder.size(0);
//执行查询
SearchResponse response = null;
try {
response = porosRestHighLevelClient.search(searchRequest, RequestOptions.DEFAULT);
} catch (Exception e) {
log.error("call es 查询信息异常", e);
}
//解析结果
if (response != null && response.status().getStatus() == SUCCESS) {
Aggregations aggregations = response.getAggregations();
ParsedStringTerms skuIdAggTerms = aggregations.get(skuIdAggkey);
//解析每个bucket
for (Terms.Bucket bucket : skuIdAggTerms.getBuckets()) {
String skuId = bucket.getKey().toString();
long count = bucket.getDocCount();
}
}
}
      桶聚合+having
        select count(*) as cnt,skuId from sku where cityId = 71 group by skuId having cnt>1000;
        {
"size": 0,
"query": {
"term": {
"cityId": {
"value": 214,
"boost": 1
}
}
},
"aggregations": {
"skuIdAgg": {
"terms": {
"field": "skuId",
"size": 1000,
"min_doc_count": 1,
"shard_min_doc_count": 0,
"show_term_doc_count_error": false,
"order": [
{
"_count": "desc"
},
{
"_key": "asc"
}
]
},
"aggregations": {
"skuIdCountAgg": {
"value_count": {
"field": "skuId"
}
},
"having": {
"bucket_selector": {
"buckets_path": {
"skuIdCountAgg": "skuIdCountAgg"
},
"script": {
"source": "params.skuIdCountAgg > 1",
"lang": "painless"
},
"gap_policy": "skip"
}
}
}
}
}
}
        @Test
public void testBucketAgg() {
SearchRequest searchRequest = new SearchRequest(INDEX_NAME);
SearchSourceBuilder searchSourceBuilder = new SearchSourceBuilder();
String skuIdAggkey = "skuIdAgg";
String skuIdCountAggKey = "skuIdCountAgg";
//查询条件
TermQueryBuilder termQuery = QueryBuilders.termQuery("cityId", 44);
//group by Agg
AggregationBuilder skuIdAgg = AggregationBuilders.terms(skuIdAggkey).field("skuId").size(1000);
AggregationBuilder skuIdCountAgg = AggregationBuilders.count(skuIdCountAggKey).field("skuId");
skuIdAgg.subAggregation(skuIdCountAgg);
//<!-----聚合之后的having条件------>
//having中要使用的聚合字段，名字可以一样
Map<String, String> bucketsPathsMap = new HashMap<>(8);
bucketsPathsMap.put("skuIdCountAgg", "skuIdCountAgg");
//设置having语句
StringBuilder havingParams = new StringBuilder();
havingParams.append("params.skuIdCountAgg > 1");
Script havingScript = new Script(havingParams.toString());
//构建having bucket选择器
BucketSelectorPipelineAggregationBuilder havingBs = PipelineAggregatorBuilders.bucketSelector("having", bucketsPathsMap, havingScript);
//having条件添加至skuId外层聚
skuIdAgg.subAggregation(havingBs);
//将条件添加至searchRequest
searchSourceBuilder.aggregation(skuIdAgg);
searchSourceBuilder.query(termQuery);
searchRequest.source(searchSourceBuilder);
searchSourceBuilder.size(0);
//执行查询
SearchResponse response = null;
try {
response = porosRestHighLevelClient.search(searchRequest, RequestOptions.DEFAULT);
} catch (Exception e) {
log.error("call es 查询信息异常", e);
}
//解析结果
if (response != null && response.status().getStatus() == SUCCESS) {
Aggregations aggregations = response.getAggregations();
ParsedStringTerms skuIdAggTerms = aggregations.get(skuIdAggkey);
//解析每个bucket
for (Terms.Bucket bucket : skuIdAggTerms.getBuckets()) {
String skuId = bucket.getKey().toString();
long count = bucket.getDocCount();
}
}
}
      桶聚合排序分页
        select count(*) as cnt, skuId from sku where cityId = 51 group by skuId having cnt>1000 order by cnt limit 0,5
        {
"size": 0,
"query": {
"term": {
"cityId": {
"value": 88,
"boost": 1
}
}
},
"aggregations": {
"skuIdAgg": {
"terms": {
"field": "skuId",
"size": 2147483647,
"min_doc_count": 1,
"shard_min_doc_count": 0,
"show_term_doc_count_error": false,
"order": [
{
"_count": "desc"
},
{
"_key": "asc"
}
]
},
"aggregations": {
"skuIdCountAgg": {
"value_count": {
"field": "skuId"
}
},
"having": {
"bucket_selector": {
"buckets_path": {
"skuIdCountAgg": "skuIdCountAgg"
},
"script": {
"source": "params.skuIdCountAgg > 2",
"lang": "painless"
},
"gap_policy": "skip"
}
},
"page_field": {
"bucket_sort": {
"sort": [
{
"skuIdCountAgg": {
"order": "asc"
}
}
],
"from": 0,
"size": 5,
"gap_policy": "SKIP"
}
}
}
}
}
}
        @Test
public void testBucketAgg() {
SearchRequest searchRequest = new SearchRequest(INDEX_NAME);
SearchSourceBuilder searchSourceBuilder = new SearchSourceBuilder();
String skuIdAggkey = "skuIdAgg";
String skuIdCountAggKey = "skuIdCountAgg";
String skuIdDistinctKey = "skuIdDistinctAgg";
//查询条件
TermQueryBuilder termQuery = QueryBuilders.termQuery("cityId", 44);
//group by Agg
AggregationBuilder skuIdAgg = AggregationBuilders.terms(skuIdAggkey).field("skuId").size(Integer.MAX_VALUE);
AggregationBuilder skuIdCountAgg = AggregationBuilders.count(skuIdCountAggKey).field("skuId");
skuIdAgg.subAggregation(skuIdCountAgg);
//<!-----聚合之后的having条件------>
//having中要使用的聚合字段，名字可以一样
Map<String, String> bucketsPathsMap = new HashMap<>(8);
bucketsPathsMap.put("skuIdCountAgg", "skuIdCountAgg");
//设置having语句
StringBuilder havingParams = new StringBuilder();
havingParams.append("params.skuIdCountAgg > 2");
Script havingScript = new Script(havingParams.toString());
//构建having bucket选择器
BucketSelectorPipelineAggregationBuilder havingBs = PipelineAggregatorBuilders.bucketSelector("having", bucketsPathsMap, havingScript);
//having条件添加至skuId外层聚
skuIdAgg.subAggregation(havingBs);
AggregationBuilder skuIdDistinctAgg = AggregationBuilders.cardinality(skuIdDistinctKey).field("skuId");
//将条件添加至searchRequest
searchSourceBuilder.aggregation(skuIdAgg);
searchSourceBuilder.aggregation(skuIdDistinctAgg);
searchSourceBuilder.query(termQuery);
searchRequest.source(searchSourceBuilder);
searchSourceBuilder.size(0);
//执行查询
SearchResponse response = null;
try {
response = porosRestHighLevelClient.search(searchRequest, RequestOptions.DEFAULT);
} catch (Exception e) {
log.error("call es 查询信息异常", e);
}
//解析结果
if (response != null && response.status().getStatus() == SUCCESS) {
Aggregations aggregations = response.getAggregations();
ParsedStringTerms skuIdAggTerms = aggregations.get(skuIdAggkey);
//bucket size数
int count = skuIdAggTerms.getBuckets().size();
}
}
      多层桶聚合
        {
"query": {
"bool": {
"must": [
{
"range": {
"ownerLastChangedTime": {
"gte": "2023-09-12 20:00:00",
"lt": "2023-09-12 20:05:00"
}
}
}
],
"must_not": [
{
"terms": {
"actionSubType": [
"9"
]
}
},
{
"terms": {
"leadsSubType": [
"12001"
]
}
},
{
"terms": {
"leadsSource": [
"16",
"18",
"11",
"12",
"15"
]
}
},
{
"terms": {
"tags.tagId": [
"5",
"6",
"7",
"8"
]
}
}
]
}
},
"from": 0,
"size": 0,
"sort": [
{
"ownerLastChangedTime": "DESC"
},
{
"leadsId": "DESC"
}
],
"aggs": {
"group": {
"terms": {
"field": "ownerId",
"size": 5000
},
"aggs": {
"group": {
"terms": {
"field": "ownerAccountId",
"size": 5000
}
}
}
}
}
}
        {
"took": 26,
"timed_out": false,
"_shards": {
"total": 3,
"successful": 3,
"skipped": 0,
"failed": 0
},
"hits": {
"total": 1,
"max_score": 0,
"hits": []
},
"aggregations": {
"group": {
"doc_count_error_upper_bound": 0,
"sum_other_doc_count": 0,
"buckets": [
{
"key": 24857909,
"doc_count": 4,
"group": {
"doc_count_error_upper_bound": 0,
"sum_other_doc_count": 0,
"buckets": [
{
"key": 33867617,
"doc_count": 4
}
]
}
}
]
}
}
}
      去重汇总
        select count(distinct skuId) from sku where cityId = 88
        {
"size": 0,
"query": {
"term": {
"cityId": {
"value": 88,
"boost": 1
}
}
},
"aggregations": {
"skuIdAgg": {
"cardinality": {
"field": "skuId"
}
}
}
}
        @Test
public void testDistinctCount() {
SearchRequest searchRequest = new SearchRequest(INDEX_NAME);
SearchSourceBuilder searchSourceBuilder = new SearchSourceBuilder();
String skuIdDistinctAggkey = "skuIdAgg";
//查询条件
TermQueryBuilder termQuery = QueryBuilders.termQuery("cityId", 44);
//group by Agg
AggregationBuilder distinctSkuIdAgg = AggregationBuilders.cardinality(skuIdDistinctAggkey).field("skuId");
//将条件添加至searchRequest
searchSourceBuilder.aggregation(distinctSkuIdAgg);
searchSourceBuilder.query(termQuery);
searchRequest.source(searchSourceBuilder);
searchSourceBuilder.size(0);
//执行查询
SearchResponse response = null;
try {
response = porosRestHighLevelClient.search(searchRequest, RequestOptions.DEFAULT);
} catch (Exception e) {
log.error("call es 查询信息异常", e);
}
//解析结果
if (response != null && response.status().getStatus() == SUCCESS) {
Aggregations aggregations = response.getAggregations();
ParsedCardinality parsedCardinality = aggregations.get(skuIdDistinctAggkey);
//去重后的skuId count
long distinctSkuIdCount = parsedCardinality.getValue();
}
}
      折叠查询Topk
        {
"size": 10,
"query": {
"term": {
"cityId": {
"value": 88,
"boost": 1
}
}
},
"collapse": {
"field": "skuId",
"inner_hits": {
"name": "top2",
"size": 2,
"sort": [
{
"zqDt": {
"order": "asc"
}
}
]
}
}
}
          类似先 term 再top_hit，但collapse + inner_hits 可以解决这类问题
          collapse field 的类型其实就是 group by top k 的那个 group
        @Test
public void testCollapseTopK() {
SearchRequest searchRequest = new SearchRequest(INDEX_NAME);
SearchSourceBuilder searchSourceBuilder = new SearchSourceBuilder();
String skuIdCollapseAggkey = "skuId";
//查询条件
TermQueryBuilder termQuery = QueryBuilders.termQuery("cityId", 44);
//<--------设置折叠查询---------->
CollapseBuilder collapseBuilder = new CollapseBuilder(skuIdCollapseAggkey);
//设置排序字段以及top 2
InnerHitBuilder innerHitBuilder = new InnerHitBuilder();
//注意一定要setName，否则有问题
innerHitBuilder.setName("top2");
innerHitBuilder.setSize(2);
SortBuilder sortBuilder = SortBuilders.fieldSort("zqDt").order(SortOrder.ASC);
innerHitBuilder.addSort(sortBuilder);
collapseBuilder.setInnerHits(innerHitBuilder);
searchSourceBuilder.collapse(collapseBuilder);
//将条件添加至searchRequest
searchSourceBuilder.query(termQuery);
searchRequest.source(searchSourceBuilder);
searchSourceBuilder.size(10);
searchSourceBuilder.sort(skuIdCollapseAggkey, SortOrder.ASC);
//执行查询
SearchResponse response = null;
try {
response = porosRestHighLevelClient.search(searchRequest, RequestOptions.DEFAULT);
} catch (Exception e) {
log.error("call es 查询信息异常", e);
}
//解析结果
if (response != null && response.status().getStatus() == SUCCESS) {
SearchHits searchHits = response.getHits();
for (SearchHit searchHit : searchHits.getHits()) {
//TODO解析每行记录
}
}
}
      top_hit
        POST /sales/_search?size=0
{
"aggs": {
"top_tags": {
"terms": {
"field": "type",
"size": 3
},
"aggs": {
"top_sales_hits": {
"top_hits": {
"sort": [
{
"date": {
"order": "desc"
}
}
],
"_source": {
"includes": [ "date", "price" ]
},
"size": 1
}
}
}
}
}
}
        {
"aggregations": {
"top_tags": {
"doc_count_error_upper_bound": 0,
"sum_other_doc_count": 0,
"buckets": [
{
"key": "hat",
"doc_count": 3,
"top_sales_hits": {
"hits": {
"total": {
"value": 3,
"relation": "eq"
},
"max_score": null,
"hits": [
{
"_index": "sales",
"_id": "AVnNBmauCQpcRyxw6ChK",
"_source": {
"date": "2015/03/01 00:00:00",
"price": 200
},
"sort": [
1425168000000
],
"_score": null
}
]
}
}
},
{
"key": "t-shirt",
"doc_count": 3,
"top_sales_hits": {
"hits": {
"total": {
"value": 3,
"relation": "eq"
},
"max_score": null,
"hits": [
{
"_index": "sales",
"_id": "AVnNBmauCQpcRyxw6ChL",
"_source": {
"date": "2015/03/01 00:00:00",
"price": 175
},
"sort": [
1425168000000
],
"_score": null
}
]
}
}
},
{
"key": "bag",
"doc_count": 1,
"top_sales_hits": {
"hits": {
"total": {
"value": 1,
"relation": "eq"
},
"max_score": null,
"hits": [
{
"_index": "sales",
"_id": "AVnNBmatCQpcRyxw6ChH",
"_source": {
"date": "2015/01/01 00:00:00",
"price": 150
},
"sort": [
1420070400000
],
"_score": null
}
]
}
}
}
]
}
}
}
          sort 有单独的数组元素
  坑
    除了用 index api，还有 put + doc body 操作
    ES 7.x以上默认topHits最多返回10000条，需要添加如下："track_total_hits":20000000
    去重汇总关键字 cardinality，是 agg 特有的一种操作
      {
"query": {
"bool": {
"must": [
{
"term": {
"companyId": "24"
}
},
{
"term": {
"zqDt": "20210204"
}
}
]
}
},
"aggs": {
"distinct_skuId": {
"cardinality": {
"field": "skuId"
}
}
},
"track_total_hits": true,
"from": 0,
"size": 0
}
    去重展示关键字collapse
      {
"collapse": {
"field": "cityName",
"inner_hits": {
"name": "topCity",
"size": 2,
"sort": [
{
"zqDt": "asc"
}
]
}
}
}
    Search After和Scroll的区别
      search_after 是无状态的
      1. Scroll查询虽然高效，但是非常耗资源，不适合实时查询。
      2. Search after解决了这个问题，使用一个活跃的游标，利用前一次的排序查询结果进行下一次查询。
      3.Search_after不能自由跳到一个随机页面，只能按照 sort values 跳转到下一页。但对于全量查询分解有基本的效果。
    ES update过程
      update过程，本质是先查-->再新增-->然后再标记老文档为已经删除，ES update支持针对指定字段的更新。
    ES字段类型选择keyword还是number类型？
      如果只有精确匹配查询使用 keyword 性能更优，如果有聚合或者范围查询使用number类型更优，number类型底层查询使用BKD数据结构，更适合范围查询，但是精确匹配性能不好。