Elasticsearch分布式搜索引擎入门

1. 认识Elasticsearch

Elasticsearch结合kibana,Logstash,Beats,是一整套技术栈,被叫做ELK。被广泛应用于在日志数据分析,实时监控等领域。

Elasticsearch对海量数据的搜索时非常快的,远远快于传统的关系型数据库,为什么呢?这取决于它底层采用了一个特殊的索引:倒排索引

  • 文档:每条数据就是一个文档
  • 词条:文档按照语义分成的词语

传统的关系型数据库通常会给主键值一个聚簇索引(以MySQL为例),真实数据在聚簇索引的叶子节点上,如果通过主键值来做查询,那速度会非常的快。但是模糊查询就不行了,只能全表扫描。而Elasticsearch不仅给给每一个文档的id设置索引,还会给词条来做一个索引。在一次模糊查询过程中,首先会给模糊的词语进行分词,然后根据这个分词去词条里找,因为词条时做过索引的,所以查询会非常块,这时会拿到词条所属的文档id,再拿这些id去文档中查找,文档的id也是做过索引的,所以也是非常快的,于是只有两次查询,就会得到想要的结果。

那怎么分词呢?需要下载分词的插件,我下载的是IK分词器,下载可以去github下载,且最好和你用的Elasticsearch版本一致。我是用的docker部署的es,并且指定了插件挂载的目录,这里不再赘述。

IK分词有两种模式,一种是ik_smart,另一种是ik_max_word

IK分词器允许我们配置拓展词典来增加自定义的词库:

在ik插件目录下的config目录下有个文件 IKAnalyzer.cfg.xml

在配置自己的扩展词典的时候,指定一个文件,这个文件就得在当前目录下,文件的每一个词占一行。

下面对Elasticsearch进行和MySQL的对比,以达到对基础概念有一定的理解:

|--------|---------------|------------------------------------------------|
| MySQL | Elasticsearch | 说明 |
| Table | Index | 索引(index),就是文档的集合,类似数据库 的表(table) |
| Row | Document | 文档(Document),就是一条条的数据,类似 数据库的行(row),文档都是json格式 |
| Column | Field | 字段(Field),就是JSON文档中的字段,类似数据库的列 |
| Schema | Mapping | Mapping(映射)是索引中文档的约束,例如字段型约束。类似数据库的表结构(Schema) |
| SQL | DSL | DSL是Elasticsearch提供的JSON风格的请求语句,用来定义搜索条件 |
[Elasticsearch和MySQL的对比]

2.Elasticsearch基础操作

2.1Mapping映射属性和索引库操作

Mapping是对索引库中文档的约束,常见的Mapping属性包括:

  • type:字段数据类型,常见的简单类型有:

    • 字符串:text(可分词的文本)、keyword(精确值,例如:品牌、国家、ip地址)

    • 数值:longintegershortbytedoublefloat

    • 布尔:boolean

    • 日期:date

    • 对象:object

  • index:是否创建索引,默认为true

  • analyzer:使用哪种分词器

  • properties:该字段的子字段

**2-1-1:**创建索引库和映射示例:

javascript 复制代码
PUT /索引库名称
{
  "mappings": {
    "properties": {
      "字段名":{
        "type": "text",
        "analyzer": "ik_smart"
      },
      "字段名2":{
        "type": "keyword",
        "index": "false"
      },
      "字段名3":{
        "properties": {
          "子字段": {
            "type": "keyword"
          }
        }
      },
      // ...略
    }
  }
}

**2-1-2:**Elasticsearch的增删改查都遵循restful风格,所以查询就是

GET /资源名

2-1-3:删除就是

DELETE /资源名

**2-1-4:**而修改其实只能在原有基础上新增:

javascript 复制代码
PUT /索引库名/_mapping
{
  "properties": {
    "新字段名":{
      "type": "integer"
    }
  }
}
  • 创建索引库:PUT /索引库名

  • 查询索引库:GET /索引库名

  • 删除索引库:DELETE /索引库名

  • 修改索引库,添加字段:PUT /索引库名/_mapping

在设计索引库与字段映射的时候,要考虑两个方面,一个是这个字段是否在在页面展示,一个是是否参与搜索。

2.2 文档操作

javascript 复制代码
# 新增文档
POST /索引库名/_doc/文档id
{
    "字段1": "值1",
    "字段2": "值2",
    "字段3": {
        "子属性1": "值3",
        "子属性2": "值4"
    },
}

#查询文档
GET /{索引库名称}/_doc/{id}

#删除文档
DELETE /{索引库名}/_doc/id值

#修改文档有两种方式:
# 1.全量修改,先删除原来的文档,在太添加新的,直接覆盖原来的文档;所以这个修改也可以达到和新增文档一样的效果
PUT /{索引库名}/_doc/文档id
{
    "字段1": "值1",
    "字段2": "值2",
    // ... 略
}

# 2.局部修改
POST /{索引库名}/_update/文档id
{
    "doc": {
         "字段名": "新的值",
    }
}

批处理文档操作:

javascript 复制代码
POST _bulk
{ "index" : { "_index" : "test", "_id" : "1" } }
{ "field1" : "value1" }

{ "delete" : { "_index" : "test", "_id" : "2" } }

{ "create" : { "_index" : "test", "_id" : "3" } }
{ "field1" : "value3" }

{ "update" : {"_id" : "1", "_index" : "test"} }
{ "doc" : {"field2" : "value2"} }
  • index代表新增操作

    • _index:指定索引库名

    • _id指定要操作的文档id

    • { "field1" : "value1" }:则是要新增的文档内容

  • delete代表删除操作

    • _index:指定索引库名

    • _id指定要操作的文档id

  • update代表更新操作

    • _index:指定索引库名

    • _id指定要操作的文档id

    • { "doc" : {"field2" : "value2"} }:要更新的文档字段

3.初始化RestClient

第一步,引依赖

javascript 复制代码
<dependency>
    <groupId>org.elasticsearch.client</groupId>
    <artifactId>elasticsearch-rest-high-level-client</artifactId>
</dependency>

第二步,因为SpringBoot默认的ES版本是7.17.10,所以我们需要覆盖默认的ES版本,我使用的版本是7.12.1:

javascript 复制代码
  <properties>
      <maven.compiler.source>11</maven.compiler.source>
      <maven.compiler.target>11</maven.compiler.target>
      <elasticsearch.version>7.12.1</elasticsearch.version>
  </properties>

第三步:初始化RestHighLevelClient:

javascript 复制代码
RestHighLevelClient client = new RestHighLevelClient(RestClient.builder(
        HttpHost.create("http://192.168.150.101:9200")
));

3.1:基于JavaClient创建索引库

java 复制代码
@Test
void testCreateIndex() throws IOException {
    // 准备request对象
    CreateIndexRequest request = new CreateIndexRequest("items");
    // 准备请求参数
    request.source(MAPPING_TEMPLATE, XContentType.JSON);
    // 发送请求
    client.indices().create(request, RequestOptions.DEFAULT);
}

其中MAPPING_TEMPLATE就是json格式创建索引库的字符串,如下

java 复制代码
private static final String MAPPING_TEMPLATE = "{\n" +
        "  \"mappings\": {\n" +
        "    \"properties\": {\n" +
        "      \"id\":{\n" +
        "        \"type\": \"keyword\"\n" +
        "      },\n" +
        "      \"name\":{\n" +
        "        \"type\": \"text\",\n" +
        "        \"analyzer\": \"ik_smart\"\n" +
        "      },\n" +
        "      \"price\":{\n" +
        "        \"type\": \"integer\"\n" +
        "      },\n" +
        "      \"image\":{\n" +
        "        \"type\": \"keyword\",\n" +
        "        \"index\": false\n" +
        "      },\n" +
        "      \"category\":{\n" +
        "        \"type\": \"keyword\"\n" +
        "      },\n" +
        "      \"brand\":{\n" +
        "        \"type\": \"keyword\"\n" +
        "      },\n" +
        "      \"sold\":{\n" +
        "        \"type\": \"integer\"\n" +
        "      },\n" +
        "      \"comment_count\":{\n" +
        "        \"type\": \"integer\",\n" +
        "        \"index\": false\n" +
        "      },\n" +
        "      \"isAD\":{\n" +
        "        \"type\": \"boolean\"\n" +
        "      },\n" +
        "      \"update_time\":{\n" +
        "        \"type\": \"date\"\n" +
        "      }\n" +
        "    }\n" +
        "  }\n" +
        "}";

查询索引库是否存在

java 复制代码
@Test
void testGetIndex() throws IOException {
    //获取request对象
    GetIndexRequest request = new GetIndexRequest("items");
    //查询这个索引库是否存在
    boolean exists = client.indices().exists(request, RequestOptions.DEFAULT);
    System.out.println("exists = " + exists);
}

删除索引库

java 复制代码
@Test
void testDeleteIndex() throws IOException {
    //获取request对象
    DeleteIndexRequest request = new DeleteIndexRequest("items");
    //删除索引库
    client.indices().delete(request, RequestOptions.DEFAULT);
}

3.2 基于JavaClient创建一条文档,也叫全量修改

java 复制代码
@Test
void testIndexDoc() throws IOException {
    //准备数据
    Item item = itemService.getById(561178L);
    ItemDoc itemDoc = BeanUtil.copyProperties(item, ItemDoc.class);
    String jsonStr = JSONUtil.toJsonStr(itemDoc);
    //获取request对象
    IndexRequest request = new IndexRequest("items").id(itemDoc.getId());
    //准备请求参数
    request.source(jsonStr, XContentType.JSON);
    //发送请求
    client.index(request, RequestOptions.DEFAULT);
}

获取一条文档

java 复制代码
@Test
void testGetDoc() throws IOException {
    //获取request对象
    GetRequest request = new GetRequest("items").id("561178");
    //发出请求
    GetResponse response = client.get(request, RequestOptions.DEFAULT);
    //拿到源信息
    String json = response.getSourceAsString();
    ItemDoc itemDoc = JSONUtil.toBean(json, ItemDoc.class);
    System.out.println("itemDoc = " + itemDoc);
}

删除一条文档

java 复制代码
@Test
void testDeleteDoc() throws IOException {
    //获取request对象
    DeleteRequest request = new DeleteRequest("items").id("561178");
    //发出请求
    client.delete(request, RequestOptions.DEFAULT);
}

局部修改

java 复制代码
@Test
void testUpdateDocument() throws IOException {
    // 1.准备Request
    UpdateRequest request = new UpdateRequest("items", "561178");
    // 2.准备请求参数
    request.doc(
            "price", 58800,
            "commentCount", 1
    );
    // 3.发送请求
    client.update(request, RequestOptions.DEFAULT);
}

4.DSL语句

4-1:叶子查询

  • 全文检索查询(Full Text Queries):利用分词器对用户输入搜索条件先分词,得到词条,然后再利用倒排索引搜索词条。例如:

    • match: #全文检索查询的一种,会对用户输入内容分词,然后去倒排索引库检索

    • multi_match #与match类似,只不过允许同时查询多个字段

      GET /{索引库名}/_search
      {
      "query": {
      "match": {
      "字段名": "搜索条件"
      }
      }
      }

      GET /{索引库名}/_search
      {
      "query": {
      "multi_match": {
      "query": "搜索条件",
      "fields": ["字段1", "字段2"]
      }
      }
      }

精确查询(Term-level queries):不对用户输入搜索条件分词,根据字段内容精确值匹配。但只能查找keyword、数值、日期、boolean类型的字段。例如:

  • ids

  • term # 这个是精确查询,基于分词表里的精确查询

  • range

GET /{索引库名}/_search
{
  "query": {
    "term": {
      "字段名": {
        "value": "搜索条件"
      }
    }
  }
}



GET /{索引库名}/_search
{
  "query": {
    "range": {
      "字段名": {
        "gte": {最小值},
        "lte": {最大值}
      }
    }
  }
}

**地理坐标查询:**用于搜索地理位置,搜索方式很多,例如:

  • geo_bounding_box:按矩形搜索

  • geo_distance:按点和半径搜索

4-2:复合查询

  • 第一类:基于逻辑运算组合叶子查询,实现组合条件,例如

    • bool
  • 第二类:基于某种算法修改查询时的文档相关性算分,从而改变文档排名。例如:

    • function_score

    • dis_max

从elasticsearch5.1开始,采用的相关性打分算法是BM25算法,公式如下:

基于这套公式,就可以判断出某个文档与用户搜索的关键字之间的关联度,还是比较准确的。但是,在实际业务需求中,常常会有竞价排名的功能。不是相关度越高排名越靠前,而是掏的钱多的排名靠前。

要想认为控制相关性算分,就需要利用elasticsearch中的function score 查询了。

基本语法

function score 查询中包含四部分内容:

  • 原始查询 条件:query部分,基于这个条件搜索文档,并且基于BM25算法给文档打分,原始算分(query score)

  • 过滤条件:filter部分,符合该条件的文档才会重新算分

  • 算分函数 :符合filter条件的文档要根据这个函数做运算,得到的函数算分(function score),有四种函数

    • weight:函数结果是常量

    • field_value_factor:以文档中的某个字段值作为函数结果

    • random_score:以随机数作为函数结果

    • script_score:自定义算分函数算法

  • 运算模式:算分函数的结果、原始查询的相关性算分,两者之间的运算方式,包括:

    • multiply:相乘

    • replace:用function score替换query score

    • 其它,例如:sum、avg、max、min

function score的运行流程如下:

  • 1)根据原始条件 查询搜索文档,并且计算相关性算分,称为原始算分(query score)

  • 2)根据过滤条件,过滤文档

  • 3)符合过滤条件 的文档,基于算分函数 运算,得到函数算分(function score)

  • 4)将原始算分 (query score)和函数算分 (function score)基于运算模式做运算,得到最终结果,作为相关性算分。

因此,其中的关键点是:

  • 过滤条件:决定哪些文档的算分被修改

  • 算分函数:决定函数算分的算法

  • 运算模式:决定最终算分结果

示例:给IPhone这个品牌的手机算分提高十倍,分析如下:

  • 过滤条件:品牌必须为IPhone

  • 算分函数:常量weight,值为10

  • 算分模式:相乘multiply

对应代码如下:

# 算发函数查询
GET /hotel/_search
{
  "query": {
    "function_score": {
      "query": {  .... }, // 原始查询,可以是任意条件
      "functions": [ // 算分函数
        {
          "filter": { // 满足的条件,品牌必须是Iphone
            "term": {
              "brand": "Iphone"
            }
          },
          "weight": 10 // 算分权重为2
        }
      ],
      "boost_mode": "multipy" // 加权模式,求乘积
    }
  }
}

bool查询,即布尔查询。就是利用逻辑运算来组合一个或多个查询子句的组合。bool查询支持的逻辑运算有:

  • must:必须匹配每个子查询,类似"与"

  • should:选择性匹配子查询,类似"或"

  • must_not:必须不匹配,不参与算分,类似"非"

  • filter:必须匹配,不参与算分

bool查询的语法如下:

GET /items/_search
{
  "query": {
    "bool": {
      "must": [
        {"match": {"name": "手机"}}
      ],
      "should": [
        {"term": {"brand": { "value": "vivo" }}},
        {"term": {"brand": { "value": "小米" }}}
      ],
      "must_not": [
        {"range": {"price": {"gte": 2500}}}
      ],
      "filter": [
        {"range": {"price": {"lte": 1000}}}
      ]
    }
  }
}

出于性能考虑,与搜索关键字无关的查询尽量采用must_not或filter逻辑运算,避免参与相关性算分。

排序

elasticsearch默认是根据相关度算分(_score)来排序,但是也支持自定义方式对搜索结果排序。不过分词字段无法排序,能参与排序字段类型有:keyword类型、数值类型、地理坐标类型、日期类型等。

GET /indexName/_search
{
  "query": {
    "match_all": {}
  },
  "sort": [
    {
      "排序字段": {
        "order": "排序方式asc和desc"
      }
    }
  ]
}
分页

基础分页:elasticsearch 默认情况下只返回top10的数据。而如果要查询更多数据就需要修改分页参数了

GET /items/_search
{
  "query": {
    "match_all": {}
  },
  "from": 0, // 分页开始的位置,默认为0
  "size": 10,  // 每页文档数量,默认10
  "sort": [
    {
      "price": {
        "order": "desc"
      }
    }
  ]
}

深度分页

  • search after:分页时需要排序,原理是从上一次的排序值开始,查询下一页数据。官方推荐使用的方式。

不过一般没有深度分页的需求

高亮显示

实现高亮的思路就是:

  • 用户输入搜索关键字搜索数据

  • 服务端根据搜索关键字到elasticsearch搜索,并给搜索结果中的关键字词条添加html标签

  • 前端提前给约定好的html标签添加CSS样式

    GET /{索引库名}/_search
    {
    "query": {
    "match": {
    "搜索字段": "搜索关键字"
    }
    },
    "highlight": {
    "fields": {
    "高亮字段名称": {
    "pre_tags": "",
    "post_tags": "
    "
    }
    }
    }
    }

5.Java对于DSL的API

5.1:叶子查询

matchAll查询

java 复制代码
@Test
void testMatchAll() throws IOException {
    // 1.创建Request
    SearchRequest request = new SearchRequest("items");
    // 2.组织请求参数
    request.source().query(QueryBuilders.matchAllQuery());
    // 3.发送请求
    SearchResponse response = client.search(request, RequestOptions.DEFAULT);
    // 4.解析响应
    handleResponse(response);
}

private void handleResponse(SearchResponse response) {
    SearchHits searchHits = response.getHits();
    // 1.获取总条数
    long total = searchHits.getTotalHits().value;
    System.out.println("共搜索到" + total + "条数据");
    // 2.遍历结果数组
    SearchHit[] hits = searchHits.getHits();
    for (SearchHit hit : hits) {
        // 3.得到_source,也就是原始json文档
        String source = hit.getSourceAsString();
        // 4.反序列化并打印
        ItemDoc item = JSONUtil.toBean(source, ItemDoc.class);
        System.out.println(item);
    }
}

代码解读

elasticsearch返回的结果是一个JSON字符串,结构包含:

  • hits:命中的结果

    • total:总条数,其中的value是具体的总条数值

    • max_score:所有结果中得分最高的文档的相关性算分

    • hits:搜索结果的文档数组,其中的每个文档都是一个json对象

      • _source:文档中的原始数据,也是json对象

因此,我们解析响应结果,就是逐层解析JSON字符串,流程如下:

  • SearchHits:通过response.getHits()获取,就是JSON中的最外层的hits,代表命中的结果

    • SearchHits #getTotalHits().value:获取总条数信息

    • SearchHits #getHits():获取SearchHit数组,也就是文档数组

      • SearchHit #getSourceAsString():获取文档结果中的_source,也就是原始的json文档数据

match查询

java 复制代码
@Test
void testMatch() throws IOException {
    // 1.创建Request
    SearchRequest request = new SearchRequest("items");
    // 2.组织请求参数
    request.source().query(QueryBuilders.matchQuery("name", "脱脂牛奶"));
    // 3.发送请求
    SearchResponse response = client.search(request, RequestOptions.DEFAULT);
    // 4.解析响应
    handleResponse(response);
}

muti_match查询

java 复制代码
@Test
void testMultiMatch() throws IOException {
    // 1.创建Request
    SearchRequest request = new SearchRequest("items");
    // 2.组织请求参数
    request.source().query(QueryBuilders.multiMatchQuery("脱脂牛奶", "name", "category"));
    // 3.发送请求
    SearchResponse response = client.search(request, RequestOptions.DEFAULT);
    // 4.解析响应
    handleResponse(response);
}

range查询

java 复制代码
@Test
void testRange() throws IOException {
    // 1.创建Request
    SearchRequest request = new SearchRequest("items");
    // 2.组织请求参数
    request.source().query(QueryBuilders.rangeQuery("price").gte(10000).lte(30000));
    // 3.发送请求
    SearchResponse response = client.search(request, RequestOptions.DEFAULT);
    // 4.解析响应
    handleResponse(response);
}

term查询

java 复制代码
@Test
void testTerm() throws IOException {
    // 1.创建Request
    SearchRequest request = new SearchRequest("items");
    // 2.组织请求参数
    request.source().query(QueryBuilders.termQuery("brand", "华为"));
    // 3.发送请求
    SearchResponse response = client.search(request, RequestOptions.DEFAULT);
    // 4.解析响应
    handleResponse(response);
}

5.2:复合查询

java 复制代码
@Test
void testBool() throws IOException {
    // 1.创建Request
    SearchRequest request = new SearchRequest("items");
    // 2.组织请求参数
    // 2.1.准备bool查询
    BoolQueryBuilder bool = QueryBuilders.boolQuery();
    // 2.2.关键字搜索
    bool.must(QueryBuilders.matchQuery("name", "脱脂牛奶"));
    // 2.3.品牌过滤
    bool.filter(QueryBuilders.termQuery("brand", "德亚"));
    // 2.4.价格过滤
    bool.filter(QueryBuilders.rangeQuery("price").lte(30000));
    request.source().query(bool);
    // 3.发送请求
    SearchResponse response = client.search(request, RequestOptions.DEFAULT);
    // 4.解析响应
    handleResponse(response);
}

5.3:分页和排序

java 复制代码
@Test
void testPageAndSort() throws IOException {
    int pageNo = 1, pageSize = 5;

    // 1.创建Request
    SearchRequest request = new SearchRequest("items");
    // 2.组织请求参数
    // 2.1.搜索条件参数
    request.source().query(QueryBuilders.matchQuery("name", "脱脂牛奶"));
    // 2.2.排序参数
    request.source().sort("price", SortOrder.ASC);
    // 2.3.分页参数
    request.source().from((pageNo - 1) * pageSize).size(pageSize);
    // 3.发送请求
    SearchResponse response = client.search(request, RequestOptions.DEFAULT);
    // 4.解析响应
    handleResponse(response);
}

5.4:高亮

java 复制代码
@Test
void testHighlight() throws IOException {
    // 1.创建Request
    SearchRequest request = new SearchRequest("items");
    // 2.组织请求参数
    // 2.1.query条件
    request.source().query(QueryBuilders.matchQuery("name", "脱脂牛奶"));
    // 2.2.高亮条件
    request.source().highlighter(
            SearchSourceBuilder.highlight()
                    .field("name")
                    .preTags("<em>")
                    .postTags("</em>")
    );
    // 3.发送请求
    SearchResponse response = client.search(request, RequestOptions.DEFAULT);
    // 4.解析响应
    handleResponse(response);
}

代码解读:

  • 3、4步:从结果中获取_sourcehit.getSourceAsString(),这部分是非高亮结果,json字符串。还需要反序列为ItemDoc对象

  • 5步:获取高亮结果。hit.getHighlightFields(),返回值是一个Map,key是高亮字段名称,值是HighlightField对象,代表高亮值

  • 5.1步:从Map中根据高亮字段名称,获取高亮字段值对象HighlightField

  • 5.2步:从HighlightField中获取Fragments,并且转为字符串。这部分就是真正的高亮字符串了

  • 最后:用高亮的结果替换ItemDoc中的非高亮结果

java 复制代码
//完整代码

private void handleResponse(SearchResponse response) {
    SearchHits searchHits = response.getHits();
    // 1.获取总条数
    long total = searchHits.getTotalHits().value;
    System.out.println("共搜索到" + total + "条数据");
    // 2.遍历结果数组
    SearchHit[] hits = searchHits.getHits();
    for (SearchHit hit : hits) {
        // 3.得到_source,也就是原始json文档
        String source = hit.getSourceAsString();
        // 4.反序列化
        ItemDoc item = JSONUtil.toBean(source, ItemDoc.class);
        // 5.获取高亮结果
        Map<String, HighlightField> hfs = hit.getHighlightFields();
        if (CollUtils.isNotEmpty(hfs)) {
            // 5.1.有高亮结果,获取name的高亮结果
            HighlightField hf = hfs.get("name");
            if (hf != null) {
                // 5.2.获取第一个高亮结果片段,就是商品名称的高亮值
                String hfName = hf.getFragments()[0].string();
                item.setName(hfName);
            }
        }
        System.out.println(item);
    }
}

6.数据聚合

聚合(aggregations)可以让我们极其方便的实现对数据的统计、分析、运算。

聚合常见的有三类:

  • 桶( Bucket **)**聚合:用来对文档做分组(类似于MySQL的group by)

    • TermAggregation:按照文档字段值分组,例如按照品牌值分组、按照国家分组

    • Date Histogram:按照日期阶梯分组,例如一周为一组,或者一月为一组

  • 度量( Metric **)**聚合:用以计算一些值,比如:最大值、最小值、平均值等

    • Avg:求平均值

    • Max:求最大值

    • Min:求最小值

    • Stats:同时求maxminavgsum

  • 管道( pipeline **)**聚合:其它聚合的结果为基础做进一步运算

注意: 参加聚合的字段必须是keyword、日期、数值、布尔类型

java 复制代码
GET /items/_search
{
  "size": 0, 
  "aggs": {
    "category_agg": {
      "terms": {
        "field": "category",
        "size": 20
      }
    }
  }
}

语法说明:

  • size:设置size为0,就是每页查0条,则结果中就不包含文档,只包含聚合

  • aggs:定义聚合

    • category_agg:聚合名称,自定义,但不能重复

      • terms:聚合的类型,按分类聚合,所以用term

        • field:参与聚合的字段名称

        • size:希望返回的聚合结果的最大数量

带条件的聚合

java 复制代码
GET /items/_search
{
  "query": {
    "bool": {
      "filter": [
        {
          "term": {
            "category": "手机"
          }
        },
        {
          "range": {
            "price": {
              "gte": 300000
            }
          }
        }
      ]
    }
  }, 
  "size": 0, 
  "aggs": {
    "brand_agg": {
      "terms": {
        "field": "brand",
        "size": 20
      }
    }
  }
}

Metric聚合

java 复制代码
GET /items/_search
{
  "query": {
    "bool": {
      "filter": [
        {
          "term": {
            "category": "手机"
          }
        },
        {
          "range": {
            "price": {
              "gte": 300000
            }
          }
        }
      ]
    }
  }, 
  "size": 0, 
  "aggs": {
    "brand_agg": {
      "terms": {
        "field": "brand",
        "size": 20
      },
      "aggs": {
        "stats_meric": {
          "stats": {
            "field": "price"
          }
        }
      }
    }
  }
}

可以看到我们在brand_agg聚合的内部,我们新加了一个aggs参数。这个聚合就是brand_agg的子聚合,会对brand_agg形成的每个桶中的文档分别统计。

  • stats_meric:聚合名称

    • stats:聚合类型,stats是metric聚合的一种

      • field:聚合字段,这里选择price,统计价格

Java客户端代码写数据聚合

聚合结果与搜索文档同一级别,因此需要单独获取和解析。具体解析语法如下:

完整代码如下

java 复制代码
@Test
void testAgg() throws IOException {
    // 1.创建Request
    SearchRequest request = new SearchRequest("items");
    // 2.准备请求参数
    BoolQueryBuilder bool = QueryBuilders.boolQuery()
            .filter(QueryBuilders.termQuery("category", "手机"))
            .filter(QueryBuilders.rangeQuery("price").gte(300000));
    request.source().query(bool).size(0);
    // 3.聚合参数
    request.source().aggregation(
            AggregationBuilders.terms("brand_agg").field("brand").size(5)
    );
    // 4.发送请求
    SearchResponse response = client.search(request, RequestOptions.DEFAULT);
    // 5.解析聚合结果
    Aggregations aggregations = response.getAggregations();
    // 5.1.获取品牌聚合
    Terms brandTerms = aggregations.get("brand_agg");
    // 5.2.获取聚合中的桶
    List<? extends Terms.Bucket> buckets = brandTerms.getBuckets();
    // 5.3.遍历桶内数据
    for (Terms.Bucket bucket : buckets) {
        // 5.4.获取桶内key
        String brand = bucket.getKeyAsString();
        System.out.print("brand = " + brand);
        long count = bucket.getDocCount();
        System.out.println("; count = " + count);
    }
}
相关推荐
晨欣12 分钟前
Elasticsearch和Lucene之间是什么关系?(ChatGPT回答)
elasticsearch·chatgpt·lucene
ZHOU西口43 分钟前
微服务实战系列之玩转Docker(十八)
分布式·docker·云原生·架构·数据安全·etcd·rbac
zmd-zk1 小时前
kafka+zookeeper的搭建
大数据·分布式·zookeeper·中间件·kafka
筱源源6 小时前
Elasticsearch-linux环境部署
linux·elasticsearch
xmst6 小时前
短视频如何引流?抖音小红书视频号的引流策略
搜索引擎
yx9o6 小时前
Kafka 源码 KRaft 模式本地运行
分布式·kafka
Gemini19957 小时前
分布式和微服务的区别
分布式·微服务·架构
G丶AEOM7 小时前
分布式——BASE理论
java·分布式·八股
P.H. Infinity13 小时前
【RabbitMQ】03-交换机
分布式·rabbitmq
龙哥·三年风水15 小时前
群控系统服务端开发模式-应用开发-个人资料
分布式·php·群控系统