一、前言
上一节我们讲了es的快速入门,以及如何操作索引库和文档,但是并没有使用到es的核心功能------DSL查询,当初我们使用es就是看重其查询功能的强大,但是在上一节中我们只尝试了(批量)查询单个文档,而如果要进行更加复杂的查询,这显然是不够的,比如我们可能需要筛选多重字段,或者对查询结果进行统计、对查询的关键词进行高亮显示等等......显然这些功能我们还没有实现,这一节我们就将通过使用DSL查询来实现这些需求。
二、DSL查询
1.叶子查询
叶子查询是最简单的查询,但是也是最基础的查询,后续的查询都是基于叶子查询建立的。
(1)全文检索查询
顾名思义,全文检索查询就是对索引库中的所有文档进行查询,这个查询也可以对某些字段添加查询条件。
例如:
如果想查询出所有文档:
bash
# 查询所有
GET /items/_search
{
"query": {
"match_all": {}
}
}
match查询:如果想查询商品名中含有"脱脂牛奶"的文档:
bash
# match查询
GET /items/_search
{
"query": {
"match": {
"name": "脱脂牛奶"
}
}
}
multi_match查询:是match的多字段版本,可以对多个字段进行查询,比如刚刚我们只查询了商品名 中含有"脱脂牛奶"的文档,而使用multi_match就可以查询商品名或类目名称中含有"脱脂牛奶"的文档了。
bash
# mlti_match查询
GET /items/_search
{
"query": {
"multi_match": {
"query": "脱脂牛奶",
"fields": ["name","category_name"]
}
}
}
(2)精确查询
对于一些字段,我们不能够按照匹配度来查询,比如价格,我只想查价格为100的商品,那就真的需要查询出价格为100的商品,101或99都不行,这就需要使用精确查询了。
下面就是通过精确查询查询品牌名为德亚的所有商品:
bash
# term精确查询
GET /items/_search
{
"query": {
"term": {
"brand": {
"value": "德亚"
}
}
}
}
同样的,范围查询也是精确查询的一种,因为其对于上下限的要求是精确的:
bash
# 范围查询
GET /items/_search
{
"query": {
"range": {
"price": {
"gte": 150000,
"lte": 180000
}
}
}
}
对多个文档id的查询也属于精确查询:
bash
# ids查询
GET /items/_search
{
"query": {
"ids": {
"values": ["4641661","4641663"]
}
}
}
2.复合查询
补充一个概念------算分,算分是对于查询结果相关度的打分,比如搜索"脱脂牛奶",查询出的结果是按照相关度的分数进行降序排列的,当然,这一功能也可以用于打广告,本质就是通过修改算分权重来将打广告文档的排序往前面提。
(1)bool查询
本质就是对叶子查询的逻辑组合。
其中,must 和filter都是表示逻辑 "与" 运算,也就是必须同时匹配他们所包含的条件。
但是must和filter略有不同,他们的主要区别在于是否算分 ,用must匹配的查询结果是参与算分的,如果需要按照相关度将查询结果排序就使用must。filter相反,但是由于算分是会消耗性能的,所以对于不需要参与算分的条件,尽量还是使用filter来筛选。
bash
GET /items/_search
{
"query": {
"bool": {
"must": [
{
"match": {
"name": "智能手机"
}
}
]
, "filter": [
{
"term": {
"brand": "华为"
}
},
{
"range": {
"price": {
"gte": 90000,
"lte": 159900
}
}
}
]
}
}
}
(2)排序查询
对查询的结果进行排序:
bash
# 排序查询
GET /items/_search
{
"query": {
"match_all": {}
}
,
"sort": [
{
"sold": "desc"
},
{
"price": "asc"
}
]
}
(3)分页查询
对查询结果进行分页。
bash
# 分页查询
GET /items/_search
{
"query": {
"match_all": {}
}
,
"sort": [
{
"sold": "desc"
},
{
"price": "asc"
}
],
"from": 0,
"size": 3
}
(4)高亮
对搜索的词添加上<em>标签,用于让前端进行高亮提示:
bash
# 高亮
GET /items/_search
{
"query": {
"match": {
"name": "脱脂牛奶"
}
},
"highlight": {
"fields": {
"name": {
"pre_tags": "<em>",
"post_tags": "</em>"
}
}
}
}

3.聚合
聚合 就是对查询结果做统计、分组、计算 的操作,相当于 SQL 里的 GROUP BY、COUNT、SUM、AVG 等功能。
(1)Bucket聚合(桶聚合)
类似sql中的分组查询,回顾一下sql中的分组查询,其实就是根据某个字段进行分组,比如用类目名称进行分组,就可以得到"牛奶"的为一组,"面包"的为一组等等......那么分好组了就可以统计了,比如"牛奶"组中有五个商品,"面包"组中有十个商品。
比如这里,我们将查询到的结果用类目名称和品牌名称进行分组(桶聚合):
bash
# 聚合 1
GET /items/_search
{
"size":0,
"aggs": {
"cate_agg": {
"terms": {
"field": "category",
"size": 10
}
},
"brand_agg": {
"terms": {
"field": "brand",
"size": 10
}
}
}
}
结果如下:


(2)带条件聚合
sql
-- 查询平均年龄小于45的员工,并根据工作地址分组,获取员工数量大于等于2的工作地址
select workaddress,count(*) from emp where age < 29 group by workaddress having count(*) >= 2;
类似于上面的where部分。
这里我们用了一个bool查询来限制查到的类目和价格区间,同时利用品牌名聚合分组。
于是就能得到:每个价格高于3000 的手机 的品牌组。
bash
# 聚合 2
GET /items/_search
{
"query": {
"bool": {
"filter": [
{"term": {
"category": "手机"
}
},
{
"range": {
"price": {
"gte": 300000
}
}
}
]
}
},
"size":0,
"aggs": {
"brand_agg": {
"terms": {
"field": "brand",
"size": 10
}
}
}
}
结果如下:

(3)Metric聚合
其实就是对每个桶中的组进行统计计算。
这里我们将刚刚的分组进行价格统计(stats属性表示将最大值、最小值、平均值等都求出来)
bash
# 聚合 3
GET /items/_search
{
"query": {
"bool": {
"filter": [
{"term": {
"category": "手机"
}
},
{
"range": {
"price": {
"gte": 300000
}
}
}
]
}
},
"size":0,
"aggs": {
"brand_agg": {
"terms": {
"field": "brand",
"size": 10
},
"aggs": {
"price_stats": {
"stats": {
"field": "price"
}
}
}
}
}
}
结果如下:

三、Java客户端
刚刚的这些都是在图形化界面中的JSON格式数据,依旧是不能用于实际开发的,所以接下来我们将使用es提供的JavaAPI来将上述DSL查询用Java客户端的方式重新模拟一次。
1.查询
由于解析结果的方式都是固定的,所以这里我们将这些步骤提出来:
java
private static void parseResponseResult(SearchResponse response) {
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);
}
}
(1)全文检索查询
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);
//4.解析结果
parseResponseResult(response);
}
(2)bool查询
java
@Test
void testSortAndPage() throws IOException {
//1.创建request对象
SearchRequest request = new SearchRequest("items");
//2.组织DSL参数
request.source().query(
QueryBuilders
.boolQuery()
.must(QueryBuilders.matchQuery("name", "脱脂牛奶"))
.filter(QueryBuilders.termQuery("brand", "德亚"))
.filter(QueryBuilders.rangeQuery("price").lt(30000))
);//构建查询条件
//3.发送请求
SearchResponse response = client.search(request, RequestOptions.DEFAULT);
//4.解析结果
parseResponseResult(response);
}
(3)排序和分页查询
java
@Test
void testSortAndPage() throws IOException {
//0.模拟前端传递的分页参数
int pageNo = 2, pageSize = 5;
//1.创建request对象
SearchRequest request = new SearchRequest("items");
//2.组织DSL参数
//2.1 query条件
request.source()
.query(QueryBuilders.matchAllQuery());//构建查询条件
//2.2 分页
request.source().from((pageNo - 1) * pageSize).size(pageSize);
request.source()
.sort("sold", SortOrder.DESC)
.sort("price", SortOrder.ASC);
//3.发送请求
SearchResponse response = client.search(request, RequestOptions.DEFAULT);
//4.解析结果
parseResponseResult(response);
}
(4)高亮
高亮查询比较特殊一点,因为我们知道,高亮查询的结果中有源文本和高亮文本,如果按照先前的解析方式,我们解析出来的都是源文本,这是没有意义的,所以需要修改解析方式,让高亮文本覆盖源文本:
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"));
//3.发送请求
SearchResponse response = client.search(request, RequestOptions.DEFAULT);
//4.解析结果
parseResponseResult(response);
}
java
private static void parseResponseResult(SearchResponse response) {
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);
}
}
2.聚合
聚合这里也需要注意一下解析方式,先获取聚合,然后获取桶,最后遍历统计。
注意:要指定聚合类型
Terms****aggregation = aggregations.get(brandAggName);
java
@Test
void testAgg() throws IOException {
//1.创建request对象
SearchRequest request = new SearchRequest("items");
//2.组织DSL参数
//2.1 分页
request.source().size(0);
//2.2 聚合条件
String brandAggName = "brandAgg";
request.source().aggregation(
AggregationBuilders.terms(brandAggName).field("brand").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());
}
}
结果如下:

四、总结
至此,黑马商城的项目大体完成,这个项目让我从单体架构转换为了微服务架构,其中又学习了大量的中间件,nacos、seata、sentinel、es、RabbitMQ,这些中间件都是帮助网页开发的一把好手,项目虽完,学习不止,共勉。