文档存储Elasticsearch系列--4 ES得实际使用

前言:ES作为一款强大的搜索引擎,我们应该怎样进行数据的存储,然后进行数据的检索和聚合;

1 索引的操作:
1.1 建立一个索引:

PUT /my_index0
{
  "settings": {
    "number_of_shards": 3,// 设置主分片的数量为3 
    "number_of_replicas": 1 // 设置每个主分片的副本数量为1 ,这样该索引就有3个分片和3个副本
  },
  // 字段类型映射设置
  "mappings": {// 映射
    "properties": {// 字段属性
      "name": {//  字段名称为 name
        "type": "text",// 设置字段类型为文本
        "fields": {// 设置name 的精确值查询
          "keyword": {// 字段名称为 keyword
            "type": "keyword",// 设置 keyword 的类型为不被分析
            "ignore_above": 256 // 设置超过指定字符后,超出的部分不能被索引或者存储
          }
        },
        "analyzer": "standard" // name 使用的分析器(不设置是字符串默认使用标准分析器分析)
      },
      "age": {
        "type": "integer"
      },
      "birstTime": {
        "type": "date",
        "format": "yyyy-MM-dd HH:mm:ss||yyyy-MM-dd||epoch_millis"
      },
      "email": {
        "type": "keyword"
      },
      "sex": {
        "type": "keyword"
      },
      "iqscore": {
        "type": "float"
      },
      "idCardNumber": {
        "type": "keyword"
      },
      "idCard": {
        "type": "nested",// 设置 idCard 为嵌套对象
        "properties": {
          "idCardNumber": {
            "type": "keyword"
          },
          "idCardName": {
            "type": "text",
            "fields": {
              "keyword": {
                "type": "keyword",
                "ignore_above": 256
              }
            },
            "analyzer": "standard"
          },
          "idCardUrl": {
            "type": "keyword"
          },
          "iStatus": {
            "type": "integer"
          }
        }
      }
    }
  }
}

2.2 设置索引别名:
_alias :关键词用于索引别名设置

// 将my_index0 索引的别名设置为 my_index
PUT /my_index0/_alias/my_index

2.3 删除索引别名:

POST /_aliases
{
    "actions": [
        { "remove": { "index": "my_index_v1", "alias": "my_index" }},// 删除my_index_v1的my_index 索引别名
        { "add":    { "index": "my_index_v2", "alias": "my_index" }}// 增加my_index_v2的my_index 索引别名
    ]
}

2.4 删除索引:

DELETE /my_index	

2 文档的操作:

2.1 存入一个文档:
存入定义好id为4的文档,

POST my_index1/_doc/4
{
  "age": 12,
  "email": "",
  "birstTime": "2020-01-01 20:21:23",
  "name": "wang wu"
}

注意:如果你的数据没有自然的 ID, Elasticsearch 可以帮我们自动生成 ID。自动生成的 ID 是 URL-safe、 基于 Base64 编码且长度为20个字符的 GUID 字符串。 这些 GUID 字符串由可修改的 FlakeID 模式生成,这种模式允许多个节点并行生成唯一 ID ,且互相之间的冲突概率几乎为零。

当我们索引一个文档,怎么确认我们正在创建一个完全新的文档,而不是覆盖现有的呢?
请记住, _index 、 _type 和 _id 的组合可以唯一标识一个文档。所以,确保创建一个新文档的最简单办法是,使用索引请求的 POST 形式让 Elasticsearch 自动生成唯一 _id :有时我们需要只有在改文档不存在的时候进行创建,而不是直接覆盖掉原有的文档:
第一种方法使用 op_type 查询-字符串参数:

POST my_index1/_doc/4?op_type=create
{ ... }

第二种方法是在 URL 末端使用 /_create :

POST my_index1/_doc/4/_create
{ ... }

如果创建新文档的请求成功执行,Elasticsearch 会返回元数据和一个 201 Created 的 HTTP 响应码。
另一方面,如果具有相同的 _index 、 _type 和 _id 的文档已经存在,Elasticsearch 将会返回 409 Conflict 响应码。

2.2 更新一个文档:
在 Elasticsearch 中文档是 不可改变 的,不能修改它们。相反,如果想要更新现有的文档,需要 重建索引 或者进行替换, 我们可以使用相同的 index API 进行实现:
在内部,Elasticsearch 已将旧文档标记为已删除,并增加一个全新的文档。 尽管你不能再对旧版本的文档进行访问,但它并不会立即消失。当继续索引更多的数据,Elasticsearch 会在后台清理这些已删除文档;
2.2.1 重新索引文档:

POST my_index1/_doc/4
{
  "age": 12,
  "email": "",
  "birstTime": "2020-01-01 20:21:23",
  "name": "wang wu"
}

2.2.2 文档的部分更新:

POST my_index1/_update/4/
{
  "doc": {
    "age": 21 // 更新文档4 年龄为21
  }
}

在 更新整个文档 , 我们已经介绍过 更新一个文档的方法是检索并修改它,然后重新索引整个文档,这的确如此。然而,使用 update API 我们还可以部分更新文档,例如在某个请求时对计数器进行累加。
我们也介绍过文档是不可变的:他们不能被修改,只能被替换。 update API 必须遵循同样的规则。 从外部来看,我们在一个文档的某个位置进行部分更新。然而在内部, update API 简单使用与之前描述相同的 检索-修改-重建索引 的处理过程。 区别在于这个过程发生在分片内部,这样就避免了多次请求的网络开销。通过减少检索和重建索引步骤之间的时间,我们也减少了其他进程的变更带来冲突的可能性。
update 请求最简单的一种形式是接收文档的一部分作为 doc 的参数, 它只是与现有的文档进行合并。对象被合并到一起,覆盖现有的字段,增加新的字段。

如果更新的文档不存在会直接报错,有时我们希望文档存在就更新文档,当文档不存在时可以直接插入新文档:

POST my_index1/_update/5/
{
  "doc": {
    "age": 21
  },
  "upsert": {}
}

更新冲突:
在并发情况下对于库存进行修改,就有可能造成库存丢失,在数据库领域中,有两种方法通常被用来确保并发更新时变更不会丢失:
悲观并发控制:
这种方法被关系型数据库广泛使用,它假定有变更冲突可能发生,因此阻塞访问资源以防止冲突。 一个典型的例子是读取一行数据之前先将其锁住,确保只有放置锁的线程能够对这行数据进行修改。
乐观并发控制:
Elasticsearch 中使用的这种方法假定冲突是不可能发生的,并且不会阻塞正在尝试的操作。 然而,如果源数据在读写当中被修改,因为版本号不同更新将会失败。应用程序接下来将决定该如何解决冲突。 例如,可以重试更新、使用新的数据、或者将相关情况报告给用户。
ES中检索 和 重建索引 步骤的间隔越小,变更冲突的机会越小。 但是它并不能完全消除冲突的可能性。 还是有可能在 update 设法重新索引之前,来自另一进程的请求修改了文档。
为了避免数据丢失, update API 在 检索 步骤时检索得到文档当前的 _version 号,并传递版本号到 重建索引 步骤的 index 请求。 如果另一个进程修改了处于检索和重新索引步骤之间的文档,那么 _version 号将不匹配,更新请求将会失败。
更新失败的重试次数:
对于部分更新的很多使用场景,文档已经被改变也没有关系。 例如,如果两个进程都对页面访问量计数器进行递增操作,它们发生的先后顺序其实不太重要; 如果冲突发生了,我们唯一需要做的就是尝试再次更新。
这可以通过设置参数 retry_on_conflict 来自动完成, 这个参数规定了失败之前 update 应该重试的次数,它的默认值为 0 。

POST my_index1/_update/5?retry_on_conflict=5 
{
  "doc": {
    "age": 21
  }
}

retry_on_conflict 对于UPDATE操作,ES会先通过文档ID去GET文档,拿到文档的version之后,再对文档做Reindex。如果在GET和Reindex期间,文档被更新,version值发生变化,则更新失败。可以使用retry_on_conflict参数来设置当发生更新上述情况更新失败时,自动重试的次数,在重新获取一次版本在次进行文档索引。retry_on_conflict的默认值为0,即不重试。

2.3 删除一个文档:
删除文档的语法和我们所知道的规则相同,只是使用 DELETE 方法:

DELETE  /my_index1/_doc/5

如果找到该文档,Elasticsearch 将要返回一个 200 ok 的 HTTP 响应码。
即使文档不存在( Found 是 false ), _version 值仍然会增加。这是 Elasticsearch 内部记录本的一部分,用来确保这些改变在跨多节点时以正确的顺序执行。
正如已经在更新整个文档中提到的,删除文档不会立即将文档从磁盘中删除,只是将文档标记为已删除状态。随着你不断的索引更多的数据,Elasticsearch 将会在后台清理标记为已删除的文档。

3 查询的操作:
3.1 单个查询
(1) match 查询
无论你在任何字段上进行的是全文搜索还是精确查询,match 查询是你可用的标准查询。
如果你在一个全文字段上使用 match 查询,在执行查询前,它将用正确的分析器去分析查询字符串;

{ "match": { "tweet": "About Search" }}

如果在一个精确值的字段上使用它,例如数字、日期、布尔或者一个 not_analyzed 字符串字段,那么它将会精确匹配给定的值:

{ "match": { "age":    26           }}
{ "match": { "date":   "2014-09-01" }}
{ "match": { "public": true         }}
{ "match": { "tag":    "full_text"  }}

multi_match 查询
multi_match 查询可以在多个字段上执行相同的 match 查询:

{
    "multi_match": {
        "query":    "full text search",
        "fields":   [ "title", "body" ]
    }
}	

(2)range 范围查询
range 查询找出那些落在指定区间内的数字或者时间:

{
    "range": {
        "age": {
            "gte":  20,
            "lt":   30
        }
    }
}

被允许的操作符如下:
gt:大于
gte:大于等于
lt:小于
lte:小于等于

(3)term 精确值查询
term 查询被用于精确值匹配,这些精确值可能是数字、时间、布尔或者那些 not_analyzed 的字符串:

{ "term": { "age":    26           }}
{ "term": { "date":   "2014-09-01" }}
{ "term": { "public": true         }}
{ "term": { "tag":    "full_text"  }}

term 查询对于输入的文本不 分析 ,所以它将给定的值进行精确查询。
terms 查询:
terms 查询和 term 查询一样,但它允许你指定多值进行匹配。如果这个字段包含了指定值中的任何一个值,那么这个文档满足条件:

{ "terms": { "tag": [ "search", "full_text", "nosql" ] }}

和 term 查询一样,terms 查询对于输入的文本不分析。它查询那些精确匹配的值(包括在大小写、重音、空格等方面的差异)。
包含,而不是相等
一定要了解 term 和 terms 是 包含(contains) 操作,而非 等值(equals) (判断)。 如何理解这句话呢?
如果我们有一个 term(词项)过滤器 { “term” : { “tags” : “search” } } ,它会与以下两个文档 同时 匹配:
{ “tags” : [“search”] }
{ “tags” : [“search”, “open_source”] }
尽管第二个文档包含除 search 以外的其他词,它还是被匹配并作为结果返回。

(4)exists 查询和 missing 查询
exists 查询和 missing 查询被用于查找那些指定字段中有值 (exists) 或无值 (missing) 的文档。这与SQL中的 IS_NULL (missing) 和 NOT IS_NULL (exists) 在本质上具有共性:

{
    "exists":   {
        "field":    "title"
    }
}

这些查询经常用于某个字段有值的情况和某个字段缺值的情况。
(5)模糊查询:
与 prefix 前缀查询的特性类似, wildcard 通配符查询也是一种底层基于词的查询,与前缀查询不同的是它允许指定匹配的正则式。它使用标准的 shell 通配符查询: ? 匹配任意字符, * 匹配 0 或多个字符。
wildcard 和 regexp 查询的工作方式与 prefix 查询完全一样,它们也需要扫描倒排索引中的词列表才能找到所有匹配的词,然后依次获取每个词相关的文档 ID ,与 prefix 查询的唯一不同是:它们能支持更为复杂的匹配模式。
这也意味着需要同样注意前缀查询存在性能问题,对有很多唯一词的字段执行这些查询可能会消耗非常多的资源,所以要避免使用左通配这样的模式匹配(如: *foo 或 .*foo 这样的正则式)。
数据在索引时的预处理有助于提高前缀匹配的效率,而通配符和正则表达式查询只能在查询时完成,尽管这些查询有其应用场景,但使用仍需谨慎。

3.2 组合多查询:
现实的查询需求从来都没有那么简单;它们需要在多个字段上查询多种多样的文本,并且根据一系列的标准来过滤。为了构建类似的高级查询,你需要一种能够将多查询组合成单一查询的查询方法。
你可以用 bool 查询来实现你的需求。这种查询将多查询组合在一起,成为用户自己想要的布尔查询。它接收以下参数:
must :
文档 必须 匹配这些条件才能被包含进来。
must_not :
文档 必须不 匹配这些条件才能被包含进来。
should:
如果满足这些语句中的任意语句,将增加 _score ,否则,无任何影响。它们主要用于修正每个文档的相关性得分。
filter
必须 匹配,但它以不评分、过滤模式来进行。这些语句对评分没有贡献,只是根据过滤标准来排除或包含文档。
下面的查询用于查找 title 字段匹配 how to make millions 并且不被标识为 spam 的文档。那些被标识为 starred 或在2014之后的文档,将比另外那些文档拥有更高的排名。如果 两者 都满足,那么它排名将更高:

{
    "bool": {
        "must":     { "match": { "title": "how to make millions" }},
        "must_not": { "match": { "tag":   "spam" }},
        "should": [
            { "match": { "tag": "starred" }},
            { "range": { "date": { "gte": "2014-01-01" }}}
        ]
    }
}

注意:
如果没有 must 语句,那么至少需要能够匹配其中的一条 should 语句。但,如果存在至少一条 must 语句,则对 should 语句的匹配没有要求。

3.3 嵌套对象查询
由于嵌套对象 被索引在独立隐藏的文档中,我们无法直接查询它们。 相应地,我们必须使用 nested 查询 去获取它们:

GET /my_index/blogpost/_search
{
  "query": {
    "bool": {
      "must": [
        {
          "match": {
            "title": "eggs"
          }
        },
        {
          "nested": {
            "path": "comments",
            "query": {
              "bool": {
                "must": [
                  {
                    "match": {
                      "comments.name": "john"
                    }
                  },
                  {
                    "match": {
                      "comments.age": 28
                    }
                  }
                ]
              }
            }
          }
        }
      ]
    }
  }
}

title 子句是查询根文档的。
nested 子句作用于嵌套字段 comments 。在此查询中,既不能查询根文档字段,也不能查询其他嵌套文档。
comments.name 和 comments.age 子句操作在同一个嵌套文档中。;

3.4 增加带过滤器(filtering)的查询:
如果我们不想因为文档的时间而影响得分,可以用 filter 语句来重写前面的例子:

{
    "bool": {
        "must":     { "match": { "title": "how to make millions" }},
        "must_not": { "match": { "tag":   "spam" }},
        "should": [
            { "match": { "tag": "starred" }}
        ],
        "filter": {
          "range": { "date": { "gte": "2014-01-01" }} 
        }
    }
}

range 查询已经从 should 语句中移到 filter 语句;
通过将 range 查询移到 filter 语句中,我们将它转成不评分的查询,将不再影响文档的相关性排名。由于它现在是一个不评分的查询,可以使用各种对 filter 查询有效的优化手段来提升性能。
所有查询都可以借鉴这种方式。将查询移到 bool 查询的 filter 语句中,这样它就自动的转成一个不评分的 filter 了。
如果你需要通过多个不同的标准来过滤你的文档,bool 查询本身也可以被用做不评分的查询。简单地将它放置到 filter 语句中并在内部构建布尔逻辑:

{
    "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" }}
              ]
          }
        }
    }
}

将 bool 查询包裹在 filter 语句中,我们可以在过滤标准中增加布尔逻辑;
通过混合布尔查询,我们可以在我们的查询请求中灵活地编写 scoring 和 filtering 查询逻辑。
验证查询语法是否合法:

GET /{index}/_validate/query

查看错误原因:

GET /{index}/_validate/query?explain

分析一个文档匹配的情况:

GET my_index1/_doc/1/_explain
{
  "query": {
    "match": {
      "name": "li"
    }
  }

4 排序:
为了按照相关性来排序,需要将相关性表示为一个数值。在 Elasticsearch 中, 相关性得分 由一个浮点数进行表示,并在搜索结果中通过 _score 参数返回, 默认排序是 _score 降序,可以使用 constant_score 查询进行替代,这将让所有文档应用一个恒定分数(默认为 1 )。它将执行与前述查询相同的查询,并且所有的文档将像之前一样随机返回,这些文档只是有了一个分数而不是零分。
sort 排序:
4.1 单字段排序:

GET /_search
{
    "query" : {
        "bool" : {
            "filter" : { "term" : { "user_id" : 1 }}
        }
    },
    "sort": { "date": { "order": "desc" }}
}

4.2 多级排序
假定我们想要结合使用 date 和 _score 进行查询,并且匹配的结果首先按照日期排序,然后按照相关性排序:

GET /_search
{
    "query" : {
        "bool" : {
            "must":   { "match": { "tweet": "manage text search" }},
            "filter" : { "term" : { "user_id" : 2 }}
        }
    },
    "sort": [
        { "date":   { "order": "desc" }},
        { "_score": { "order": "desc" }}
    ]
}

排序条件的顺序是很重要的。结果首先按第一个条件排序,仅当结果集的第一个 sort 值完全相同时才会按照第二个条件进行排序,以此类推。
多级排序并不一定包含 _score 。你可以根据一些不同的字段进行排序,如地理距离或是脚本计算的特定值。
Query-string 搜索 也支持自定义排序,可以在查询字符串中使用 sort 参数:

GET /_search?sort=date:desc&sort=_score&q=search

4.3 多值字段的排序:
一种情形是字段有多个值的排序, 需要记住这些值并没有固有的顺序;一个多值的字段仅仅是多个值的包装,这时应该选择哪个进行排序呢?
对于数字或日期,你可以将多值字段减为单值,这可以通过使用 min 、 max 、 avg 或是 sum 排序模式 。 例如你可以按照每个 date 字段中的最早日期进行排序,通过以下方法:

"sort": {
    "dates": {
        "order": "asc",
        "mode":  "min"
    }
}

如果一个类型是字符串的字段,它即需要进行分析,有需要进行排序,那么需要对其设置多字段映射:

"tweet": { 
    "type":     "string",// tweet 主字段与之前的一样: 是一个 analyzed 全文字段。
    "analyzer": "english",
    "fields": {
        "raw": { 
            "type":  "string",
            "index": "not_analyzed" // 新的 tweet.raw 子字段是 not_analyzed.
        }
    }
}

tweet 主字段与之前的一样: 是一个 analyzed 全文字段。 新的 tweet.raw 子字段是 not_analyzed.
现在,至少只要我们重新索引了我们的数据,使用 tweet 字段用于搜索,tweet.raw 字段用于排序。

字段排序原理:
通过倒排索引可以快速完成文档检索,但是要按照文档中某个字段排序,看起来我们不得不将检索出来的文档,获取相应字段到一个集合中从而完成排序,换句话说,我们需要 转置 倒排索引
转置 结构在其他系统中经常被称作 列存储 。实质上,它将所有单字段的值存储在单数据列中,这使得对其进行操作是十分高效的,例如排序。
在 Elasticsearch 中,Doc Values 就是一种列式存储结构,默认情况下每个字段的 Doc Values 都是激活的,Doc Values 是在索引时创建的,当字段索引时,Elasticsearch 为了能够快速检索,会把字段的值加入倒排索引中,同时它也会存储该字段的 Doc Values。排序发生在索引建立时的平行数据结构中。
Elasticsearch 中的 Doc Values 常被应用到以下场景:

  • 对一个字段进行排序
  • 对一个字段进行聚合
  • 某些过滤,比如地理位置过滤
  • 某些与字段相关的脚本计算

5 分页查询:
5.1 普通分页查询:
和 SQL 使用 LIMIT 关键字返回单个 page 结果的方法相同,Elasticsearch 接受 from 和 size 参数:
size:显示应该返回的结果数量,默认是 10
from :显示应该跳过的初始结果数量,默认是 0
如果每页展示 5 条结果,可以用下面方式请求得到 1 到 3 页的结果:
考虑到分页过深以及一次请求太多结果的情况,结果集在返回之前先进行排序。 但请记住一个请求经常跨越多个分片,每个分片都产生自己的排序结果,这些结果需要进行集中排序以保证整体顺序是正确的。
5.2 在分布式系统中深度分页:
理解为什么深度分页是有问题的,我们可以假设在一个有 5 个主分片的索引中搜索。 当我们请求结果的第一页(结果从 1 到 10 ),每一个分片产生前 10 的结果,并且返回给 协调节点 ,协调节点对 50 个结果排序得到全部结果的前 10 个。
现在假设我们请求第 1000 页—​结果从 10001 到 10010 。所有都以相同的方式工作除了每个分片不得不产生前10010个结果以外。 然后协调节点对全部 50050 个结果排序最后丢弃掉这些结果中的 50040 个结果。
可以看到,在分布式系统中,对结果排序的成本随分页的深度成指数上升。这就是 web 搜索引擎对任何查询都不要返回超过 1000 个结果的原因。
如果需要分页获取大量的数据就需要用到游标查询:
游标查询 Scroll:
scroll 查询 可以用来对 Elasticsearch 有效地执行大批量的文档查询,而又不用付出深度分页那种代价。
游标查询允许我们 先做查询初始化,然后再批量地拉取结果。 这有点儿像传统数据库中的 cursor 。
游标查询会取某个时间点的快照数据。 查询初始化之后索引上的任何变化会被它忽略。 它通过保存旧的数据文件来实现这个特性,结果就像保留初始化时的索引 视图 一样。
深度分页的代价根源是结果集全局排序,如果去掉全局排序的特性的话查询结果的成本就会很低。 游标查询用字段 _doc 来排序。 这个指令让 Elasticsearch 仅仅从还有结果的分片返回下一批结果。
启用游标查询可以通过在查询的时候设置参数 scroll 的值为我们期望的游标查询的过期时间。 游标查询的过期时间会在每次做查询的时候刷新,所以这个时间只需要足够处理当前批的结果就可以了,而不是处理查询结果的所有文档的所需时间。 这个过期时间的参数很重要,因为保持这个游标查询窗口需要消耗资源,所以我们期望如果不再需要维护这种资源就该早点儿释放掉。 设置这个超时能够让 Elasticsearch 在稍后空闲的时候自动释放这部分资源。

GET /old_index/_search?scroll=1m  // 保持游标查询窗口一分钟。
{
    "query": { "match_all": {}},
    "sort" : ["_doc"],  // 关键字 _doc 是最有效的排序顺序。
    "size":  1000
}

这个查询的返回结果包括一个字段 _scroll_id, 它是一个base64编码的长字符串 。 现在我们能传递字段 _scroll_id 到 _search/scroll 查询接口获取下一批结果:

GET /_search/scroll
{
    "scroll": "1m", // 再次设置游标查询过期时间为一分钟。
    "scroll_id" : "cXVlcnlUaGVuRmV0Y2g7NTsxMDk5NDpkUmpiR2FjOFNhNnlCM1ZDMWpWYnRROzEwOTk1OmRSamJHYWM4U2E2eUIzVkMxalZidFE7MTA5OTM6ZFJqYkdhYzhTYTZ5QjNWQzFqVmJ0UTsxMTE5MDpBVUtwN2lxc1FLZV8yRGVjWlI2QUVBOzEwOTk2OmRSamJHYWM4U2E2eUIzVkMxalZidFE7MDs="
}

这个游标查询返回的下一批结果。 尽管我们指定字段 size 的值为1000,我们有可能取到超过这个值数量的文档。 当查询的时候, 字段 size 作用于单个分片,所以每个批次实际返回的文档数量最大为 size * number_of_primary_shards 。
注意游标查询每次返回一个新字段 _scroll_id。每次我们做下一次游标查询, 我们必须把前一次查询返回的字段 _scroll_id 传递进去。 当没有更多的结果返回的时候,我们就处理完所有匹配的文档了。
java ex

final Scroll scroll = new Scroll(TimeValue.timeValueMinutes(1L));
SearchRequest searchRequest = new SearchRequest("posts");
searchRequest.scroll(scroll);
SearchSourceBuilder searchSourceBuilder = new SearchSourceBuilder();
searchSourceBuilder.query(matchQuery("title", "Elasticsearch"));
searchRequest.source(searchSourceBuilder);
//  通过发送初始值来初始化搜索上下文SearchRequest

SearchResponse searchResponse = client.search(searchRequest, RequestOptions.DEFAULT); 
String scrollId = searchResponse.getScrollId();
SearchHit[] searchHits = searchResponse.getHits().getHits();


// 通过在循环中调用 Search Scroll api 来检索所有搜索命中,直到没有文档返回
while (searchHits != null && searchHits.length > 0) { 
    
// 处理返回的搜索结果    
// 创建一个新SearchScrollRequest的持有最后返回的滚动标识符和滚动间隔
//SearchScrollRequest scrollRequest = new SearchScrollRequest(scrollId); 
    scrollRequest.scroll(scroll);
    searchResponse = client.scroll(scrollRequest, RequestOptions.DEFAULT);
    scrollId = searchResponse.getScrollId();
    searchHits = searchResponse.getHits().getHits();
}

// 滚动完成后清除滚动上下文
ClearScrollRequest clearScrollRequest = new ClearScrollRequest(); 
clearScrollRequest.addScrollId(scrollId);
ClearScrollResponse clearScrollResponse = client.clearScroll(clearScrollRequest, RequestOptions.DEFAULT);
boolean succeeded = clearScrollResponse.isSucceeded();

6 聚合的操作:
6.1 普通的桶聚合;
agg 通过term 精确聚合:根据term 判断改文档需要归属于哪个桶:

GET /cars/transactions/_search
{
   "size" : 0,
   "aggs": {
      "colors": {
         "terms": {
            "field": "color"
         },
         "aggs": {
            "avg_price": { 
               "avg": {
                  "field": "price"
               }
            },
            "make": { 
                "terms": {
                    "field": "make" 
                }
            }
         }
      }
   }
}

java ex:

SearchRequest twoRequest = new SearchRequest();
twoRequest.indices(IndexConstant.ALGOSIMDOC);
// 构建搜索条件
SearchSourceBuilder twoSourceBuilder = new SearchSourceBuilder();
twoSourceBuilder.size(10000);//设置数量
twoSourceBuilder.timeout(new TimeValue(60, TimeUnit.SECONDS));
BoolQueryBuilder twoTermsQuery= QueryBuilders.boolQuery()
        .must(QueryBuilders.termsQuery("sOriginalID",sOriginalIDList));
twoSourceBuilder.query(twoTermsQuery);
//分组查询
AggregationBuilder oneAgg = AggregationBuilders.terms("aggSOriginalID") .field("sOriginalID").size(20000);
twoSourceBuilder.aggregation(oneAgg);
twoRequest.source(twoSourceBuilder);
SearchResponse responseTwo = client.search(twoRequest, RequestOptions.DEFAULT);
Terms terms =responseTwo.getAggregations().get("aggSOriginalID");
for (Terms.Bucket bucket : terms.getBuckets()) {
    long docCount = bucket.getDocCount();// 聚合字段对应的数量
    if(docCount>0){
        String keyAsString = bucket.getKeyAsString(); // 聚合字段列的值
        statisticalMap.put(keyAsString,String.valueOf(docCount));
    }
}

6.2 条形图的聚合;按照一定的数字间隔,组成条形图:

GET /cars/transactions/_search
{
   "size" : 0,
   "aggs":{
      "price":{
         "histogram":{ 
            "field": "price",
            "interval": 20000
         },
         "aggs":{
            "revenue": {
               "sum": { 
                 "field" : "price"
               }
             }
         }
      }
   }
}

6.3 日期聚合:按照时间进行分桶

GET /cars/transactions/_search
{
   "size" : 0,
   "aggs": {
      "sales": {
         "date_histogram": {
            "field": "sold",
            "interval": "month", 
            "format": "yyyy-MM-dd" 
         }
      }
   }
}

java ex:

NativeSearchQueryBuilder tradeSearchQueryBuilder = new NativeSearchQueryBuilder().withIndices(tradeIndex).withTypes(IndexConstant.MAIN_TYPE);
DateHistogramAggregationBuilder tradeTimeAgg = AggregationBuilders.dateHistogram("timeAgg")
        .field("tradeDate").dateHistogramInterval(timeTypeEnum.getValue()).format(timeFormatEnum.getValue())
        .subAggregation(getBuildingTradeAgg());
SearchQuery tradeSearchQuery = tradeSearchQueryBuilder.withQuery(tradeBoolQuery)
        .withPageable(PageRequest.of(0, 1)).addAggregation(tradeTimeAgg).build();

6.4 地理位置的聚合:
geo_distance 聚合 对一些搜索非常有用,例如找到所有距离我 1km 以内的披萨店。搜索结果应该也的确被限制在用户指定 1km 范围内,但是我们可以添加在 2km 范围内找到的其他结果:

GET /attractions/restaurant/_search
{
  "query": {
    "bool": {
      "must": {
        "match": { 
          "name": "pizza"
        }
      },
      "filter": {
        "geo_bounding_box": {
          "location": { 
            "top_left": {
              "lat":  40.8,
              "lon": -74.1
            },
            "bottom_right": {
              "lat":  40.4,
              "lon": -73.7
            }
          }
        }
      }
    }
  },
  "aggs": {
    "per_ring": {
      "geo_distance": { 
        "field":    "location",
        "unit":     "km",
        "origin": {
          "lat":    40.712,
          "lon":   -73.988
        },
        "ranges": [
          { "from": 0, "to": 1 },
          { "from": 1, "to": 2 }
        ]
      }
    }
  },
  "post_filter": { 
    "geo_distance": {
      "distance":   "1km",
      "location": {
        "lat":      40.712,
        "lon":     -73.988
      }
    }
  }
}

主查询查找名称中含有 pizza 的饭店;geo_bounding_box 筛选那些只在纽约区域的结果;geo_distance 聚合统计距离用户 1km 以内,1km 到 2km 的结果的数量;最后,post_filter 将结果缩小至那些在用户 1km 范围内的饭店。

6.6 嵌套对象的聚合:使用nested 对嵌套的对象完成嵌套对象的聚合:
(1)正向嵌套聚合:

GET /my_index/blogpost/_search
{
  "size" : 0,
  "aggs": {
    "comments": { 
      "nested": {
        "path": "comments"
      },
      "aggs": {
        "by_month": {
          "date_histogram": { 
            "field":    "comments.date",
            "interval": "month",
            "format":   "yyyy-MM",
 		"min_doc_count" : 0, 
            "extended_bounds" : { 
                "min" : "2014-01-01",
                "max" : "2014-12-31"
            }
          },
          "aggs": {
            "avg_stars": {
              "avg": { 
                "field": "comments.stars"
              }
            }
          }
        }
      }
    }
  }
}

min_doc_count 和extended_bounds:
min_doc_count 非常容易理解:它强制返回所有 buckets,即使 buckets 可能为空。
extended_bounds 参数需要一点解释。 min_doc_count 参数强制返回空 buckets,但是 Elasticsearch 默认只返回你的数据中最小值和最大值之间的 buckets。
因此如果你的数据只落在了 4 月和 7 月之间,那么你只能得到这些月份的 buckets(可能为空也可能不为空)。因此为了得到全年数据,我们需要告诉 Elasticsearch 我们想要全部 buckets, 即便那些 buckets 可能落在最小日期 之前 或 最大日期 之后。
(2)逆向嵌套聚合:
nested 聚合 只能对嵌套文档的字段进行操作。 根文档或者其他嵌套文档的字段对它是不可见的。 然而,通过 reverse_nested 聚合,我们可以 走出 嵌套层级,回到父级文档进行操作。
例如,我们要基于评论者的年龄找出评论者感兴趣 tags 的分布。 comment.age 是一个嵌套字段,但 tags 在根文档中:

GET /my_index/blogpost/_search
{
  "size" : 0,
  "aggs": {
    "comments": {
      "nested": { 
        "path": "comments"
      },
      "aggs": {
        "age_group": {
          "histogram": { 
            "field":    "comments.age",
            "interval": 10
          },
          "aggs": {
            "blogposts": {
              "reverse_nested": {}, 
              "aggs": {
                "tags": {
                  "terms": { 
                    "field": "tags"
                  }
                }
              }
            }
          }
        }
      }
    }
  }
}

6.7 过滤聚合:
适合场景:字段按照不定长范围的聚合 如聚合金额 :0-10000,10000-30000,30000-50000,50000-100000
过程: 聚合过程中 先进行filter 过滤得到对应范围的数据,然后在进行聚合:
es 语句(dsl):

POST /sales/_search?size=0
{
  "aggs": {
    "t_shirts": {
      "filter": { "term": { "type": "t-shirt" } },
      "aggs": {
        "avg_price": { "avg": { "field": "price" } }
      }
    }
  }
}

java eg 参考:

// 分段数据
Map<String, Map<String, List<String>>> step = new LinkedHashMap<>(3);
 
SumAggregationBuilder tradeArea = AggregationBuilders.sum("tradeArea").field("tradeArea");
SumAggregationBuilder tradeCount = AggregationBuilders.sum("tradeCount").field("tradeCount");
SumAggregationBuilder tradeMoney = AggregationBuilders.sum("tradeMoney").field("tradeMoney");

for (Map.Entry<String, List<String>> stringListEntry : queryList.entrySet()) {
	List<String> value = stringListEntry.getValue();
    if (!CollectionUtils.isEmpty(value)) {
        FilterAggregationBuilder tradeFiledStep = AggregationBuilders.filter(stringListEntry.getKey(),
                        new RangeQueryBuilder(typeTradeEnum.getValue()).from(value.get(0)).includeLower(true).to(value.get(1)).includeUpper(false))
                        // 此处可以添加 面积段,金额段等的聚合
                .subAggregation(tradeArea).subAggregation(tradeCount).subAggregation(tradeMoney);

        tradeCityFiled.subAggregation(tradeFiledStep);
    }
}

分段数据 实例参考:

public static final Stringstep_area = "0,50,70,90,110,130,150,200,999999999";
public static final String step_price = "0,20000,30000,40000,60000,80000,100000,999999999";
public static final String step_total_money = "0,2000000,3000000,5000000,7000000,9000000,10000000,999999999";
public static Map<String, Map<String, List<String>>> step = new LinkedHashMap<>(3);

    static {
        StringBuilder builder = new StringBuilder();
        Map<String, List<String>> mapArea = new LinkedHashMap<>(8);
        String[] splitArea = step_area.split(",");
        for (int i = 0; i < splitArea.length - 1; i++) {
            List<String> area = new ArrayList<>(2);
            String area1 = splitArea[i];
            String area2 = splitArea[i + 1];
            area.add(area1);
            area.add(area2);
            if (builder.length() > 0) {
                builder.delete(0, builder.capacity());
            }
            if ("0".equals(area1)) {
                builder.append(area2).append(mianji).append("以下");
                mapArea.put(builder.toString(), area);
                continue;
            }
            if (maxValue.equals(area2)) {
                builder.append(area1).append(mianji).append("以上");
                mapArea.put(builder.toString(), area);
                continue;
            }
            builder.append(area1).append("-").append(area2).append(mianji);
            mapArea.put(builder.toString(), area);

        }
        step.put(SZGXStaticsStepEnum.面积段.getTypeCode(), mapArea);

        Map<String, List<String>> mapPrice = new LinkedHashMap<>(8);
        String[] splitPrice = step_price.split(",");
        for (int i = 0; i < splitPrice.length - 1; i++) {
            List<String> price = new ArrayList<>(2);
            String price1 = splitPrice[i];
            String price2 = splitPrice[i + 1];
            price.add(price1);
            price.add(price2);
            if (builder.length() > 0) {
                builder.delete(0, builder.capacity());
            }
            if ("0".equals(price1)) {
                builder.append(price2).append(danjia).append("以下");
                mapPrice.put(builder.toString(), price);
                continue;
            }
            if (maxValue.equals(price2)) {
                builder.append(price1).append(danjia).append("以上");
                mapPrice.put(builder.toString(), price);
                continue;
            }
            builder.append(price1).append("-").append(price2).append(danjia);
            mapPrice.put(builder.toString(), price);

        }
        step.put(SZGXStaticsStepEnum.单价段.getTypeCode(), mapPrice);

        Map<String, List<String>> mapTotal = new LinkedHashMap<>(8);
        String[] splitTotal = step_total_money.split(",");

        for (int i = 0; i < splitTotal.length - 1; i++) {
            List<String> totalMoney = new ArrayList<>(2);
            String money1 = splitTotal[i];
            String money2 = splitTotal[i + 1];
            totalMoney.add(money1);
            totalMoney.add(money2);
            if (builder.length() > 0) {
                builder.delete(0, builder.capacity());
            }
            String money1Str = new BigDecimal(money1).divide(BigDecimal.valueOf(10000), 0, BigDecimal.ROUND_HALF_UP).toString();
            String money2Str = new BigDecimal(money2).divide(BigDecimal.valueOf(10000), 0, BigDecimal.ROUND_HALF_UP).toString();

            if ("0".equals(money1)) {
                builder.append(money2Str).append(zongjai).append("以下");
                mapTotal.put(builder.toString(), totalMoney);
                continue;
            }

            if (maxValue.equals(money2)) {
                builder.append(money1Str).append(zongjai).append("以上");
                mapTotal.put(builder.toString(), totalMoney);
                continue;
            }
            builder.append(money1Str).append("-").append(money2Str).append(zongjai);
            mapTotal.put(builder.toString(), totalMoney);

        }
        step.put(SZGXStaticsStepEnum.总价段.getTypeCode(), mapTotal);
    }

参考:
ES权威指南

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值