Elasticsearch下篇

Elasticsearch下篇

文章目录

  • Elasticsearch下篇
    • [1 DSL查询](#1 DSL查询)
      • [1.1 快速入门](#1.1 快速入门)
      • [1.2 叶子查询](#1.2 叶子查询)
        • [1.2.1 全文检索查询](#1.2.1 全文检索查询)
        • [1.2.2 精确查询](#1.2.2 精确查询)
      • [1.3 复合查询](#1.3 复合查询)
      • [1.4 排序和分页](#1.4 排序和分页)
      • [1.5 高亮显示](#1.5 高亮显示)
    • [2 JavaRestClient](#2 JavaRestClient)
      • [2.1 快速入门](#2.1 快速入门)
      • [2.2 构建查询条件](#2.2 构建查询条件)
      • [2.3 排序和分页](#2.3 排序和分页)
      • [2.4 高亮显示](#2.4 高亮显示)
    • [3 数据聚合](#3 数据聚合)
      • [3.1 DSL聚合](#3.1 DSL聚合)
      • [3.2 RestClient聚合](#3.2 RestClient聚合)

在上次学习中,我们已经导入了大量数据到elasticsearch中,实现了商品数据的存储。不过查询商品数据时 依然采用的是根据id查询,而非模糊搜索

所以今天,我们来研究下elasticsearch的数据搜索功能。Elasticsearch提供了基于JSON的DSL(Domain Specific Language)语句来定义查询条件,其JavaAPI就是在组织DSL条件。

因此,我们先学习DSL的查询语法,然后再基于DSL来对照学习JavaAPI,就会事半功倍。

1 DSL查询

Elasticsearch提供了DSL(Domain Specific Language)查询,就是以JSON格式来定义查询条件,类似这样:

DSL查询可以分为两大类:

  • 叶子查询(Leaf query clauses): 一般就是在特定的字段里查询特定值,属于简单查询,很少单独使用。
  • 复合查询(Compound query clauses): 以逻辑方式组合多个叶子查询或者更改叶子查询的行为方式。

在查询以后,还可以对查询的结果做处理,包括:

  • 排序:按照一个或多个字段值做排序
  • 分页:根据from和size做分页,类似MySQL
  • 高亮:对搜索结果中的关键字添加特殊样式,使其更加醒目
  • 聚合:对搜索结果做数据统计以形成报表

1.1 快速入门

基于DSL的查询语法如下:

json 复制代码
# 查询所有
GET /items/_search
{
  "query": {
    "match_all": {
      
    }
  }
}

会发现虽然是match_all,但是响应结果中并不会包含索引库中的所有文档,而是仅有10条。这是因为处于安全考虑,elasticsearch设置了默认的查询页数。

1.2 叶子查询

叶子查询还可以进一步细分,常见的有:

  • 全文检索(full text)查询: 利用分词器对用户输入内容分词,然后去词条列表中匹配。例如:
    • match_query
    • multi_match_query
  • 精确查询: 不对用户输入内容分词,直接精确匹配,一般是查找Keyword、数值、日期、布尔等类型。例如:
    • ids
    • range
    • term
  • 地理(geo)查询: 用于搜索地理位置,搜索方式很多。例如:
    • geo_distance
    • geo_bounding_box
1.2.1 全文检索查询

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

multi_match: 与match查询类似,只不过允许同时查询多个字段,语法:

json 复制代码
# match查询
GET /items/_search
{
  "query": {
    "match": {
      "name": "脱脂牛奶"
    }
  }
}


# multi_match查询
GET /items/_search
{
  "query": {
    "multi_match": {
      "query": "牛奶",
      "fields": ["name","category"]
    }
  }
}
1.2.2 精确查询

精确查询 ,英文是Term-level query,顾名思义,词条级别的查询。也就是说不会对用户输入的搜索条件在分词,而是作为一个词条,与搜索的字段内容精确值匹配。因此推荐查询keyword、数值、日期、boolean类型的字段。例如id、price、城市、地名、人名等作为一个整体才有含义的字段。

注意:

json 复制代码
#term 所有
GET /items/_search
{
  "query": {
    "term": {
      "name": {
        "value": "脱脂牛奶"
      }
    }
  }
}

对name进行精确查询,很容易查询不到任何信息。原因是,name是进行分词处理的,属性就是 可分词的文本 ,那么由于脱脂牛奶可以分成两个词语,一个脱脂,一个牛奶,当进行精确查询时,由于此查询中"脱脂牛奶"不分词,因此找不到一个对应的信息,查询结果为空。

json 复制代码
#range 所有
GET /items/_search
{
  "query": {
    "range": {
      "price": {
        "gt": 10000,
        "lte": 20000
      }
    }
  }
}
json 复制代码
#ids 所有
GET /items/_search
{
  "query": {
    "ids": {
      "values": ["584387","584392"]
    }
  }
}

1.3 复合查询

复合查询大致可以分为两类:

  • 第一类:基于逻辑运算组合叶子查询,实现组合条件,例如:
    • bool
  • 第二类:基于某种运算修改查询时的文档相关性算分,从而改变文档排名。例如:
    • function_score
    • dis_max

布尔查询 是一个或多个查询子句的组合。子查询的组合方式有:

  • must:必须匹配每个子查询,类似"与"
  • should:选择性匹配子查询,类似"或"
  • must_not:必须不匹配,不参与算分,类似"非"
  • filter:必须匹配,不参与算分

需求: 搜索"智能手机",但品牌必须是华为,价格是900-1599

json 复制代码
GET /items/_search
{
  "query": {
    "bool": {
      "must": [
        {
          "match": {
            "name": "智能手机"
          }
        }
      ],
      "filter": [
        {
          "term": {
            "brand": "华为"
          }
        },
        {
          "range": {
            "price": {
              "gte": 90000,
              "lte": 159900
            }
          }
        }
      ]
    }
  }
}

1.4 排序和分页

elasticsearch支持对搜索结果排序,默认是根据相关度算法(_score)来排序,也可以指定字段排序。可以排序字段类型有:keyword类型、数值类型、地理坐标类型、日期类型等。

需求: 搜索商品,按照销量排序,销量一样则按照价格升序

json 复制代码
# 排序查询
GET /items/_search
{
  "query": {
    "match_all": {}
  },
  "sort": [
    {
      "sold" : "desc"
    },
    {
      "price": "asc"
    }
  ]
}

elasticsearch默认情况下只返回top10的数据。而如果要查询更对数据就需要修改分页参数了。elasticsearch中通过修改from、size参数来控制要返回的分页结果:

  • from:从第几文档开始
  • size:总共查询几个文档

需求: 搜索商品,查询出销量排名前十的商品,销量一样时按照价格升序

json 复制代码
 #排序查询
GET /items/_search
{
  "query": {
    "match_all": {}
  },
  "sort": [
    {
      "sold" : "desc"
    },
    {
      "price": "asc"
    }
  ],
  "from": 0,
  "size": 10
}

深度分页问题

elasticsearch的数据一般会采用分片存储,也就是把一个索引中的数据分成N份,存储到不同节点上。查询数据时需要汇总各个分片的数据。

假如要查询第100页数据,每页查10条:

实现思路:

① 对数据排序

② 找出第990-1000名

假如我们现在要查询的是第999页数据呢,是不是要找第9990~10000的数据,那岂不是需要把每个分片中的前10000名数据都查询出来,汇总在一起,在内存中排序?如果查询的分页深度更深呢,需要一次检索的数据岂不是更多?

由此可知,当查询分页深度较大时,汇总数据过多,对内存和CPU会产生非常大的压力。

因此elasticsearch会禁止from+ size 超过10000的请求。

针对深度分页,ES提供了两种解决方案:

  • search after:分页时需要排序,原理是从上一次的排序值开始,查询下一页数据。官方推荐使用的方式
  • scroll:原理将排序数据形成快照,保存在内存。官方已经不推荐使用

search after模式:

  • 优点:没有查询上线,支持深度分页
  • 缺点:只能向后逐页查询,不能随机翻页
  • 场景:数据迁移,手机滚动查询

1.5 高亮显示

高亮显示: 就是在搜索结果中把搜索关键字突出显示。

json 复制代码
# 高亮
GET /items/_search
{
  "query": {
    "match": {
      "name": "脱脂牛奶"
    }
  },
  "highlight": {
    "fields": {
      "name": {
        "pre_tags": "<em>",
        "post_tags": "</em>"
      }
    }
  }
}

2 JavaRestClient

2.1 快速入门

数据搜索的java代码分成两部分:

  • 构建并发起请求
  • 解析查询结果
java 复制代码
    @Test
    void testMatchAll() throws IOException {
        //1. 创建request对象
        SearchRequest request = new SearchRequest("items");
        //2. 配置request参数
        request.source()
                .query(QueryBuilders.matchAllQuery());
        //3. 发送请求
        SearchResponse response = client.search(request, RequestOptions.DEFAULT);
        System.out.println("response = " + response);
    }

解析查询结果的API:

java 复制代码
    @Test
    void testMatchAll() throws IOException {
        //1. 创建request对象
        SearchRequest request = new SearchRequest("items");
        //2. 配置request参数
        request.source()
                .query(QueryBuilders.matchAllQuery());
        //3. 发送请求
        SearchResponse response = client.search(request, RequestOptions.DEFAULT);

        //4. 解析结果
        SearchHits searchHits = response.getHits();
        //4.1 总条数
        long value = searchHits.getTotalHits().value;
        System.out.println("value = " + value);
        //4.2 命中的数据
        SearchHit[] hits = searchHits.getHits();
        for (SearchHit hit : hits) {
            //4.2.1 获取Source的结果
            String json = hit.getSourceAsString();
            //4.2.2 处理对象 比如转为ItemDoc
            ItemDoc doc = JSONUtil.toBean(json, ItemDoc.class);
            System.out.println("doc = " + doc);
        }
    }

2.2 构建查询条件

在javaRestAPI中,所有类型的query查询条件都是有QueryBuilders来构建的:

全文检索的查询条件构造API如下:

精确查询的查询条件构造API如下:

布尔查询的查询条件构造API如下:

案例

需求:利用javaRestClient实现搜索功能,条件如下:

  • 搜索关键字为脱脂牛奶
  • 品牌必须为德亚
  • 价格必须低于300
java 复制代码
    @Test
    void testSearch() throws IOException {
        //1. 创建request对象
        SearchRequest request = new SearchRequest("items");
        //2. 组织DSL参数
        request.source()
                .query(QueryBuilders.boolQuery()
                        .must(QueryBuilders.matchQuery("name","脱脂牛奶"))
                        .filter(QueryBuilders.termQuery("brand.keyword","德亚"))
                        .filter(QueryBuilders.rangeQuery("price")
                                .lt(130000)));
        //3. 发送请求
        SearchResponse response = client.search(request, RequestOptions.DEFAULT);

        parseResponseResult(response);
    }

2.3 排序和分页

与query类似,排序和分页参数都是基于request.source()来设置:

2.4 高亮显示

高亮显示的条件构造API如下:

高亮显示的结果解析API如下:

java 复制代码
   @Test
    void testHighlight() throws IOException {

        //1. 创建request对象
        SearchRequest request = new SearchRequest("items");
        //2. 组织DSL参数
        //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. 解析结果
        parseHighlightResponseResult(response);
    }

    private static void parseHighlightResponseResult(SearchResponse response) {
        //4. 解析结果
        SearchHits searchHits = response.getHits();
        //4.1 总条数
        long value = searchHits.getTotalHits().value;
        System.out.println("value = " + value);
        //4.2 命中的数据
        SearchHit[] hits = searchHits.getHits();
        for (SearchHit hit : hits) {
            //4.2.1 获取Source的结果
            String json = hit.getSourceAsString();
            //4.2.2 处理对象 比如转为ItemDoc
            ItemDoc doc = JSONUtil.toBean(json, ItemDoc.class);
            //4.3 处理高亮结果
            Map<String, HighlightField> hfs = hit.getHighlightFields();
            if(hfs != null && !hfs.isEmpty()){
                //4.3.1 根据高亮字段名获取高亮结果
                HighlightField hf = hfs.get("name");
                //4.3.2 获取高亮结果,覆盖非高亮结果
                String hfName = hf.getFragments()[0].string();
                doc.setName(hfName);
            }
            System.out.println("doc = " + doc);
        }
    }

3 数据聚合

聚合(aggregations)可以实现对文档数据的统计、分析、运算。运算常见的有三类:

  • 桶(Bucket)聚合:用来对文档做分组
    • TermAggregation:按照文档字段值分组
    • Date Histogram:按照日期阶梯分组,例如一周为一组,或者一月为一组
  • 度量(Metric)聚合:用以计算一些值,比如:最大值、最小值、平均值等
    • Avg:求平均值
    • Max:求最大值
    • Min:求最小值
    • Stats:同时求max、min、avg、sum等
  • 管道(pipeline)聚合:其它聚合的结果为基础做聚合

3.1 DSL聚合

我们要统计所有商品中共有哪些商品分类,其实就是以分类(category)字段对数据分组。category值一样的放在同一组,属于Bucket聚合中的Term聚合。

默认情况下,Bucket聚合是对索引库的所有文档做聚合,我们可以限定要聚合的文档范围,只要添加query条件即可。例如,我想知道价格高于3000元的手机品牌有哪些:

json 复制代码
# 聚合
GET /items/_search
{
  "query": {
    "bool": {
      "filter": [
        {
          "term": {
            "category.keyword": "牛奶"
          }
        },
        {
          "range": {
            "price": {
              "gte": 20000
            }
          }
        }
      ]
    }
  }, 
  "size": 0,
  "aggs": {
    "brand_agg":{
      "terms": {
        "field": "brand.keyword",
        "size": 10
      }
    }
  }
}

除了对数据分组(Bucket)以外,还可以对每个Bucket内的数据进一步做数据计算和统计。例如:我想知道手机有哪些品牌,每个品牌的价格最小值、最大值、平均值。

json 复制代码
# 聚合
GET /items/_search
{
  "query": {
    "bool": {
      "filter": [
        {
          "term": {
            "category.keyword": "牛奶"
          }
        },
        {
          "range": {
            "price": {
              "gte": 20000
            }
          }
        }
      ]
    }
  }, 
  "size": 0,
  "aggs": {
    "brand_agg":{
      "terms": {
        "field": "brand.keyword",
        "size": 10
      },
      "aggs": {
        "price_stats": {
          "stats": {
            "field": "price"
          }
        }
      }
    }
  }
}

3.2 RestClient聚合

java 复制代码
    @Test
    void testAgg() throws IOException {
        //1. 创建request对象
        SearchRequest request = new SearchRequest("items");
        //2. 组织DSL参数
        //分页
        request.source().size(0);
        //聚合条件
        String brandAggName = "brandAgg";
        request.source().aggregation(AggregationBuilders.terms(brandAggName).field("brand.keyword").size(10));
        //3. 发送请求
        SearchResponse response = client.search(request, RequestOptions.DEFAULT);

        //4. 解析结果
        Aggregations aggregations = response.getAggregations();
        //4.1 根据聚合名称获取对应的聚合
        Terms aggregation = aggregations.get(brandAggName);
        //4.2 获取Buckets
        List<? extends Terms.Bucket> buckets = aggregation.getBuckets();
        //4.3 遍历获取每一个bucket
        for (Terms.Bucket bucket : buckets) {
            System.out.println("brand: " + bucket.getKeyAsString());
            System.out.println("count: " + bucket.getDocCount());
        }
    }
相关推荐
xlsw_1 小时前
java全栈day20--Web后端实战(Mybatis基础2)
java·开发语言·mybatis
神仙别闹2 小时前
基于java的改良版超级玛丽小游戏
java
黄油饼卷咖喱鸡就味增汤拌孜然羊肉炒饭2 小时前
SpringBoot如何实现缓存预热?
java·spring boot·spring·缓存·程序员
Data跳动2 小时前
Spark内存都消耗在哪里了?
大数据·分布式·spark
暮湫3 小时前
泛型(2)
java
超爱吃士力架3 小时前
邀请逻辑
java·linux·后端
南宫生3 小时前
力扣-图论-17【算法学习day.67】
java·学习·算法·leetcode·图论
转码的小石3 小时前
12/21java基础
java
李小白663 小时前
Spring MVC(上)
java·spring·mvc
GoodStudyAndDayDayUp3 小时前
IDEA能够从mapper跳转到xml的插件
xml·java·intellij-idea