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());
        }
    }
相关推荐
凸头19 分钟前
RedisSearch 和 Elasticsearch 的 HNSW向量索引对比
大数据·elasticsearch·搜索引擎
Jial-(^V^)21 分钟前
微调大模型实现新闻分类
大数据·人工智能·分类
zzb15808 小时前
RAG from Scratch-优化-query
java·数据库·人工智能·后端·spring·mybatis
V搜xhliang02468 小时前
机器人建模(URDF)与仿真配置
大数据·人工智能·深度学习·机器学习·自然语言处理·机器人
房产中介行业研习社8 小时前
2026年3月哪些房源管理系统功能全
大数据·运维·人工智能
wuqingshun3141598 小时前
如何停止一个正在退出的线程
java·开发语言·jvm
Barkamin9 小时前
队列的实现(Java)
java·开发语言
玄微云9 小时前
2026年通用软件难适配,垂直店务系统反而更省心
大数据·云计算·软件需求
骇客野人9 小时前
自己手搓磁盘清理工具(JAVA版)
java·开发语言
J2虾虾9 小时前
在SpringBoot中使用Druid
java·spring boot·后端·druid