笔记 : Elasticsearch 查询笔记

599 阅读13分钟

首先分享之前的所有文章 , 欢迎点赞收藏转发三连下次一定 >>>> 😜😜😜
文章合集 : 🎁 juejin.cn/post/694164…
Github : 👉 github.com/black-ant
CASE 备份 : 👉 gitee.com/antblack/ca…

一 . 前言

这一篇主要针对 ES 查询的相关逻辑 (😜文档收集专家) , 深度直到日常使用 ,不会过多涉及理论~~

基础概念补充 :

// 简介 :
Elasticsearch 是一个分布式、可扩展、实时的搜索与数据分析引擎
Elasticsearch 通常被用作 全文检索、结构化搜索、分析 . Elasticsearch 基于 Lucence 
Elasticsearch 将所有的功能打包成一个单独的服务,这样你可以通过程序与它提供的简单的 RESTful API 进行通信.

// 特点 : 
Elasticsearch 不仅存储文档,而且 索引 每个文档的内容,使之可以被检索。
Elasticsearch 使用 JavaScript Object Notation(或者 JSON)作为文档的序列化格式

// 数据结构 : 
Elastic 集群 
    |- 索引 : 类似于一个数据库(注意区分名词和动词) , 索引是具有某种特征文档的集合
    |- 索引 // 一个集群中可以包含多个索引(indices -> 数据库 )
        |- 类型 : 类似于一个表
        |- // 每个索引可以包含多个类型 (types -> 表)
            |- 文档 : 类似于表中的一行数据 , index 里面的单条记录被称为文档
            |- // 每个类型包含多个文档 (documents ->行 )
                |- 属性 : 类似于一行数据中的一个属性
                |- // 每个文档包含多个字段(fileds - > 列)

Relational DB -> Databases -> Tables -> Rows -> Columns
Elasticsearch -> Indices   -> Types  -> Documents -> Fields

PS : Type 的变迁 在 5.X 版本和之前版本,一个 index 下可以创建多个 type
在 6.X 版本中,一个 index 下只能存在一个 type
在 7.X 版本中,去除type 概念 , index 中不在存在type

以下版本会以 > 7.x 来写案例 , 查询方式与历史版本一致 ,只不过默认使用_doc

// 注意 ,以下2种查询方式等效 ,为了方便理解 , 演示使用_doc
GET /order/_search
GET /order/_doc/_search

二 . ES 查询基础概念

1.1 系统查询条件

系统详情可以查看以下文档 : blog.csdn.net/qq_28988969…

// 系统信息
查看路由信息 : GET _cat/aliases?v
显示分片信息 : GET _cat/allocation?v

// 健康信息
查看系统健康 : GET _cat/health 
查看健康详情 : GET _cat/health?v  
// PS : Green 一切正常 / Yellow 所有数据可获取 ,部分复制品未分配 / Red 部分是数据获取不到 ,集群不可用
// PS : node.total 节点总数 / node.data 数据节点总数 / active_shards_percent 活动分片百分比

// 节点信息
查看Master节点 : GET _cat/master?v
查看Node节点 : GET _cat/nodes?v
查看Node节点属性 : GET _cat/nodeattrs?v

// 任务信息
查看等待的任务 : GET _cat/pending_tasks?v

// 插件信息
 查看节点的插件 : GET _cat/plugins?v

// 索引详情
显示索引 : GET _cat/indices?v
显示分段 : GET _cat/segments?v

1.2 ES 查询的方式

ES 常见的操作方式为 REST API , 通过操作类型 (POST , GET , PUT , DELETE)来区别具体的操作类型 , 查询主要基于 GET 方式下 :

ES 请求 :URL 格式

curl -X<VERB> '<PROTOCOL>://<HOST>/<PATH>?<QUERY_STRING>' -d '<BODY>'
curl -XGET 'http://localhost:9200/_count?pretty' -d '

ES 请求 : Body 格式

GET /order/_search
{
    "query": {
    	"match_all": {}
    }
}

1.3 查询关键字

// 基础查询方式
- term : 完全匹配,即不进行分词器分析,文档中必须包含整个搜索的词汇
- match : 先对搜索词进行分词,分词完毕后再逐个对分词结果进行匹配
- fuzzy : 相似度匹配 , 区别与一般的模糊匹配 , 相似度不是逐字匹配

// 基础查询标准格式
- matchAll : 
- multiMatch :多词匹配
- matchPhrase : 短语匹配
- ids : 通过 ID 检索

- prefix : 前缀匹配
- range : 范围匹配
- wildcard : 模糊匹配 ,通过通配符进行匹配
- regexp : 正则匹配


// 其他关键字 : 
keyword : 该关键字主要放在 field 后面 , 用于

1.4 基础请求和返回含义

ES 返回是以 JSON 格式进行范围 ,为了后文的案例 ,这里统一把字段含义进行整理 : 

// 以一个基础的检索为例 : 
GET /order/_doc/1
{
    "_index" : "megacorp",
    "_type" : "employee",
    "_id" : "1", 		// _id : 当前 Doc ID , 可以使用自增或者自建
    "_version" : 1, 	        // _version : 每次修改会修改版本号 ,以做多版本管理
    "found" : true,		// found : 是否查询到数据 ,此处如果是 false ,标识数据未查询到
    "_source" : { 		// _source : 实际资源对象    
        "_class" : "com.gang.study.elasticsearch.demo.entity.AOrder", // Spring 默认会提供创建类
        "username" : "gang",
        "last_name" : "test",
        "age" : 25,
        "about" : "ElasticSearch",
        "interests": [ "test", "12345" ]
    }
}	
    
============== 分页
{
    "took": 6,   	// 整个搜索请求花费的毫秒数
    "timed_out": false, // 是否超时 , 将返回在请求超时前收集到的结果
    "_shards": { ... }, // 参与查询的分片数 , 有多少是成功的(successful 字段),有多少的是失败的(failed 字段)
    "hits": {
        "total": 3,     // 查询总数
        "max_score": 1,
        "hits":[内部为单次查询的数组结果]
    }
}    
    
    

1.5 filtered 查询

filtered 是比较旧的查询 , 在5.0版本更新后 , filtered 被更新为了 bool , 以下文章部分还是使用的 Filtered , 有兴趣可以比较下2者的写法 :

具体可以参考这篇文档 :# es中filtered和filter的区别

GET _search
{
  "query": {
    "filtered": {
      "query": {"match": {"outAddress": "wuhan123"}},
      "filter": { "term": { "status": "1" } }
    }
  }
}

// 新版 Bool 写法
{
  "query": {
    "bool": {
      "must": {"match": {"outAddress": "wuhan123"}},
      "filter": {"term": {"status": "1"}
      }
    }
  }
}

二. 基础查询案例

2.1 Paramter 查询

基础查询

// 通过 ID 查询 : GET /索引/类型/ID
GET /order/_doc/1
        
// 简单搜索 > 全量
GET /order/_doc/_search
    
// 简单搜索 > 带条件 (通过 q 来设置查询条件)
GET /order/_doc/_search?q=last_name:Smith    
GET /order/_doc/_search?q=+name:john +tweet:mary
GET /_search?q=mary // 查询包含 mary 的所有文档
    
// 简单搜索 > 包含语句 (或) , 不指定name: 则表示查询所有   
GET /order/_doc/_search?q=name:(mary john)

// 简单查询 > 时间比较
GET /order/_doc/_search?q=date:>2014-09-10

// 简单查询 > 返回指定字段
GET /order/_doc/123?_source=title,text
    
// 简单查询 > 返回_source (不会返回_index ,_id 等元数据)
GET /order/_doc/123/_source
    
GET /_search                            // 空搜索
GET /gb/_search 			// 在指定索引下所有类型种搜索
GET /gb,us/_search   			// 在 gb 和 us 索引下搜索
GET /g*,u*/_search   			// 模糊索引搜索
GET /gb/user/_search 			// 指定索引和类型进行搜索
GET /gb,us/user,tweet/_search           // 在多个索引和类型下搜索
GET /_all/user,tweet/_search            // 在所有所有下查询多个类型
    

分页索引

// 简单分页查询
GET /_search?size=5
GET /_search?size=5&from=5
GET /_search?size=5&from=10
   

2.2 请求体查询

2.2.1 Match 匹配

match 匹配先对搜索词进行分词,分词完毕后再逐个对分词结果进行匹配

// DSL 查询 > 单 Query (DSL 是通过 ReqeustBody 传入查询信息进行复杂查询)    
GET /order/_doc/_search
{
  "query" : {
    "match" : {
      "outAddress" : "武汉12"
    }
  }
}


2.2.2 Term 匹配

完全匹配,即不进行分词器分析,文档中必须包含整个搜索的词汇

GET /order/_search
{
  "query": {
    "term": {  			
      "outAddress": "12"	       
    }
  }
}

// 通过 keyword 进行精确完整匹配
{
  "query": {
    "term": {  			
      "outAddress.keyword": "武汉12"	       
    }
  }
}

Match 匹配 和 Term 匹配的区别 :

Match 会进行分词处理 , 例如一个查询语句AntBlack刚 , 会被分割成2个词 : Antblack , 刚 . 再通过倒排序进行搜索 , 判读是否存在匹配的索引

而对于 Term , 进行的是精准查询 ,如果分词器没有对应的分词 , 可能会出现查询不到的情况.

2.2.3 profix 前缀匹配

前缀匹配适用于自动补全功能 , 这里可以看成就是 FST 功能:

GET /order/_search
{
  "query": {
    "prefix": {
      "outAddress.keyword": "武汉"
    }
  }
}

2.2.4 wildcard 模糊匹配

通过通配符表达式来进行模糊匹配

GET /order/_search
{
  "query": {
    "wildcard": {
      "outAddress.keyword": "武汉?2"
    }
  }
}

// 补充 : 
* : 匹配任何字符序列
? : 匹配当个字符

2.2.5 match_phrase

// DSL 查询 > 短语搜索 (会全匹配短语)
GET /order/_doc/_search
{
    "query" : {
        "match_phrase" : { // 只有完整匹配到以下词才会匹配成功
        	"about" : "rock climbing"
        }
    }
}

2.2.6 multi_match

多字段检索 , 同时对多个字段检索 , 判断是否匹配 Query 条件

GET /order/_search
{
  "query": {
    "multi_match": {
      "query": "武汉12",
      "fields": [
        "outAddress",
        "extension"
      ]
    }
  }
}

2.2.7 Query String 表达式

这事一种比较独特的方式 ,通过表达式的方式进行查询 , 表达式也很通俗易懂, 括号和 AND OR 即可

GET /order/_search
{
  "query": {
    "query_string": {
      "default_field": "outAddress",
      "query": "(武 AND 77) OR (武 AND 12)"
    }
  }
}

2.2.8 组合匹配

// 组合过滤 : 多重嵌套 (组合过滤可以bool 嵌套 bool 以实现复杂的查询)
// 以 SQL 对比如下表达式 : 
{
  "query": {
    "filtered": {
      "filter": {
        "bool": {
          "should": [
            {
              "term": {
                "productID": "KDKE-B-9947-#kL5"
              }
            },
            {
              "bool": { // 双重 Bool
                "must": [
                  {
                    "term": {
                      "productID": "JODL-X-1937-#pV7"
                    }
                  },
                  {
                    "term": {
                      "price": 30
                    }
                  }
                ]
              }
            }
          ]
        }
      }
    }
  }
}


// 组合过滤 : 指通过 多个 Boolean 结果判断是否符合条件
> must :所有分句都必须匹配,与 AND 相同
> must_not :所有分句都必须不匹配,与 NOT 相同
> should :至少有一个分句匹配,与 OR 相同
GET /order/_doc/_search
{
  "query": {
    "filtered": {
      "filter": {
        "bool": { //  标识需要满足范围内所有判断
          "should": [
            {
              "term": {
                "price": 20
              }
            },
            {
              "term": {
                "productID": "XHDK-A-1293-#fJ3"
              }
            }
          ],
          "must_not": {
            "term": {
              "price": 30
            }
          }
        }
      }
    }
  }
}

2.2.9 正则匹配

通过正则表达式进行匹配 , 前面还是字段 , 后面为表达式 @ www.elastic.co/guide/en/el…

GET /order/_search
{
  "query": {
    "regexp":{
      "outAddress":"[0-3][2-9]"
    }
  }
}


2.2.10 存在匹配

返回包含字段索引值的文档 @ www.elastic.co/guide/en/el…

GET /order/_search
{
  "query": {
    "exists": {
      "field": "outAddress"
    }
  }
}

2.2.11 fuzzy 相似度匹配

这个匹配就比较玄学了 , 会根据 Levenshtein 判断词的相似度 , 进而匹配 . 类似于百度搜出来的乱七八糟的东西 @ www.elastic.co/guide/en/el…

GET /order/_search
{
  "query": {
    "fuzzy": {
      "outAddress": {
        "value": "266",
        "fuzziness": "AUTO",
        "max_expansions": 50,
        "prefix_length": 0,
        "transpositions": true,
        "rewrite": "constant_score"
      }
    }
  }
}

// PS :
(box → fox)
(black → lack)


三. 高级查询案例

3.1 分组和统计

复杂搜索中通过聚合等方式对结果进行分析统计

// 字段分析
GET /order/_doc/_search
{
  "aggs": {
    "all_interests": {
      "terms": {
        "field": "interests" // 对兴趣字段进行分析
      }
    }
  }
}


// 查询平均年龄
GET /order/_doc/_search
{
  "aggs": {
    "all_interests": { 		        // 分析名
      "terms": {
        "field": "interests"
      },
      "aggs": { 
        "avg_age": { 		        // 聚合名
          "avg": { 			// 统计平均年龄
            "field": "age"
          }
        }
      }
    }
  }
}

3.2 特性搜索

美化类

// 搜索高亮 (highlight)
GET /order/_doc/_search
{
  "query": {
    "match_phrase": {
      "about": "rock climbing"
    }
  },
  "highlight": {
    "fields": {
      "about": {}
    }
  }
}


// DSL : 美化查询 (可以通过 pretty 美化查询结果)
GET /order/blog/123?pretty 
    
// Rest : 通过请求判断是否存在
curl -i -XHEAD http://localhost:9200/website/blog/123
- 200 OK : 数据存在
- 404 : 数据不存在    

统计类

// 统计总数据量
显示指定索引数量 : GET /page-access/_count
显示文档总数 : GET _cat/count?v


3.2 比较查询

// DSL 查询 > Filter (通过 filter 方式比较范围)
GET /order/_doc/_search
{
  "query": {
    "filtered": { 			// 查询类型 : 能同时接收 filter 和 query
      "filter": { 			// filter 标识为查询方法
        "range": {			// range 标识为范围查询
          "age": {			// 查询字段
            "gt": 30,		        // 比较方式 : 详见附录1 
            "lt" : 40                   // range 中可以设置多个匹配类型  
          }
        }
      },
      "query": {
        "match": {
          "last_name": "smith"
        }
      }
    }
  }
}

// DSL > Filter 直接包含
{
  "filter": {
    "term": {  				 // 可以匹配数字 , 也可以匹配字符串,注意,term 和 terms 是包含关系
      "price": 20,  		         // 匹配单个价格  
    }
  }
}

{
  "query": {
    "filtered": {
      "filter": {
        "terms": {			// 注意 , 这里是 terms 	
          "price": [20,30] 	        // 匹配多个价格 	
        }
      }
    }
  }
}

// DSL > Filter 完全匹配 (上述方式是包含匹配 ,而期望完全匹配短语需要额外的语法)
{
  "filter": {
    "bool": {
      "must": [
         {"term": {"tags": "search"}},          // 查询tags 包含 search 
         {"term": {"tag_count": 1}}		// 且 tag_count 为 1 的 
      ]
    }
  }
}

3.4 多文档检索

多文档检索是同时检索多个文档


// 全库通过 ID 同时获取 2个 文档 
GET /_mget
{
  "docs": [
    {
      "_index": "order",
      "_type": "_doc",
      "_id": 2
    },
    {
      "_index": "website",
      "_type": "pageviews",
      "_id": 1,
      "_source": "views" // 只获取 Views 字段
    }
  ]
}

// 指定库检索多个文档
GET /website/_doc/_mget
{
  "docs": [
    {
      "_id": 2
    },
    {
      "_type": "pageviews",
      "_id": 1
    }
  ]
}


// 指定库通过 ids 检索
GET /website/_doc/_mget
{
  "ids": [
    "2",
    "1"
  ]
}

四. Spring 使用

常规方式是使用Spring Repository实现 , 如果把ElasticSearch 作为一个数据分析库 , 使用 Spring 提供的工具既可以达到大部分的功能 , 使用方式和 JPA 类似, 这里主要讲的是另外一种 , 通过 ElasticClient 实现复杂查询

3.1 Jar 包解析

SpringData 组件中对 elasticSearch 基础组件做了基本的封装 , 日常使用中使用 spring-boot-starter-data-elasticsearch 即可 .

Jar 包的分析可以看之前的 Client 分析 : juejin.cn/post/707716…

3.2 复杂查询 : 从高亮进行分析

高亮查询主要使用 RestHighLevelClient 实现 , 我们可以从一个高亮查询看出Spring 查询的主要方式 :

@Autowired
private RestHighLevelClient restHighLevelClient;

public void hightQuery() throws Exception {
    logger.info("进入高亮处理逻辑");

    // S1 : 通过 Builder 构建查询条件
    SearchSourceBuilder searchSourceBuilder = new SearchSourceBuilder();
    searchSourceBuilder
            .query(QueryBuilders.matchQuery("outAddress", "武汉12"))    // 设置查询条件
            .from(0)    // 起始条数(当前页-1)*size的值
            .size(10)   // 每页展示条数
            .sort("age", SortOrder.DESC)    // 排序
            .highlighter(new HighlightBuilder().field("username").requireFieldMatch(false).preTags("<span style='color:red;'>").postTags("</span>"));  // 设置高亮

    // S2 : 构建查询请求体
    SearchRequest searchRequest = new SearchRequest();
    searchRequest.indices("order").source(searchSourceBuilder);

    // S3 : 发送查询获取 Response
    SearchResponse searchResponse = restHighLevelClient.search(searchRequest, RequestOptions.DEFAULT);

    // S4 : 显示查询结果
    logger.info("查询完成 :{} <-------", JSONObject.toJSONString(searchResponse));
    Arrays.asList(searchResponse.getHits().getHits()).forEach(item -> {
        logger.info("------> ITEM :{} <-------", JSONObject.toJSONString(item.getSourceAsMap()));
    });
}

关注点 :

  • 高亮查询在里面实际上就一个语句 : .highlighter(....)
  • SearchSourceBuilder 是基础的查询构建器 , 通过 QueryBuilders 来进行相关的查询方式
  • QueryBuilders 中基本上可以找到大部分的查询方式 , 可以通过简单的 API 进行查询处理

3.3 复杂查询 : 异步处理

// S1 : 通过 Builder 构建查询条件
SearchSourceBuilder searchSourceBuilder = new SearchSourceBuilder();
searchSourceBuilder
        .query(QueryBuilders.matchQuery("outAddress", "武汉12"))    // 设置查询条件
        .from(0)    // 起始条数(当前页-1)*size的值
        .size(10);   // 每页展示条数

// S2 : 构建查询请求体
SearchRequest searchRequest = new SearchRequest();
searchRequest.indices("order").source(searchSourceBuilder);

// S3 : 发送查询获取 Response
Cancellable searchResponse = restHighLevelClient.searchAsync(searchRequest, RequestOptions.DEFAULT, new ActionListener<SearchResponse>() {
    @Override
    public void onResponse(SearchResponse searchResponse) {
        // S4 : 显示查询结果
        logger.info("查询完成 :{} <-------", JSONObject.toJSONString(searchResponse));
        Arrays.asList(searchResponse.getHits().getHits()).forEach(item -> {
            logger.info("------> ITEM :{} <-------", JSONObject.toJSONString(item.getSourceAsMap()));
        });
    }

    @Override
    public void onFailure(Exception e) {
        logger.error("E----> error :{} -- content :{}", e.getClass(), e.getMessage());
    }
});

总结

生产中实际上对理论概念使用率不高 , 所以这一篇文章主要对 ES 的查询方式做了整理 , 便于生产中进行快速查询.

当然作为程序员 , 理论概念也很重要 , 后续会根据时间把 索引 , 倒排序等整理出来.

附录

附录-1 : Range 比较方式

gt : > 大于
lt : < 小于
gte : >= 大于或等于
lte : <= 小于或等于

// range 可以通过很多种方式来处理时间关联 , 支持日期数字操作
"gt" : "now-1h"  // 当前时间戳大于当前时间减1小时 
"lt" : "2014-01-01 00:00:00||+1M"  // 早于指定时间一个月
    
// range 可以计算字符范围 , 按照字典顺序和字母顺序进行排序
{"gte" : "a","lt" : "b"}  // 字符大于等于 a ,但是小于 b

附录2 : 全文搜索排序

// 全文搜索时会返回匹配度("max_score")用于描述当前结果的关联程度 , 同时默认通过结果相关性评分进行排序

附录3 : Null 值的索引

本质上 , null , [] (空数组)和 [null] 是相等的。它们都不存在于倒排索引中!


// exists 过滤器 : 将返回任何包含这个字段的文档
"filter" : {"exists" : { "field" : "tags" }}

// missing 过滤器 : 返回不包含这个字段的文档
"filter": {"missing" : { "field" : "tags" }}

附录4 : 为什么明明有这个数据但是我查不到 ?

ES 中通过倒排序进行查询 , 针对一句查询语句 , 会根据分词结果进行搜索 , 看下面这个例子 :

GET /order/_analyze
{
  "analyzer": "standard",
  "text": "测试12abd么"
}

// 分词结果
{
  "tokens" : [
    {
      "token" : "测",
      "start_offset" : 0,
      "end_offset" : 1,
      "type" : "<IDEOGRAPHIC>",
      "position" : 0
    },
    {
      "token" : "试",
      "start_offset" : 1,
      "end_offset" : 2,
      "type" : "<IDEOGRAPHIC>",
      "position" : 1
    },
    {
      "token" : "12abd",
      "start_offset" : 2,
      "end_offset" : 7,
      "type" : "<ALPHANUM>",
      "position" : 2
    },
    {
      "token" : "么",
      "start_offset" : 7,
      "end_offset" : 8,
      "type" : "<IDEOGRAPHIC>",
      "position" : 3
    }
  ]
}

当然不同的分词器的结果是不同的 , 但是针对原理而言 , 汉字被完全拆开了 , 数字和字母进行了组合 . 这些在倒排序的过程中 ,将会直接影响到结果.

这个时候 , 如果该查询方式没有击中分词 (例如 term 查询 , 不会做分词操作) , 那么就会出现查询不到的情况.

附录5 : 什么是 KeyWord 和 Mapping

ES 会为我们的 doc field 动态生成 Mapping (映射) , 该映射中包含了数据的类型 (type) , ES 中通过 /_mapping ,我们可以查看 Elasticsearch 在一个或多个索引中的一个或多个类型的映射。

Mapping 可以自定义创建, 用来规约一个属性如何创建对应的索引 ,以及其格式 , Mapping 中常见的关键字如下 :

  • analyzer : 定义文本字段的分词器
  • boost : 设置字段的权重
  • coerce : 是否开启自动数据类型转换功能, 默认是true(开启)
  • copy_to : 是否将多个字段的值,复制到同一个字段中 (创建一个虚拟字段)
  • doc_values : 为了加快排序、聚合操作,在建立倒排索引的时候,额外增加一个列式存储映射,是一个空间换时间的做法
  • dynamic : 是否允许根据文档动态添加mapping类型,默认true(允许)
  • eager_global_ordinals : 是否开启预加载全局序号,加快查询
  • format : 设置日期格式的,多个格式用||隔开
  • ignore_above : 在keywor类型下设置一个长度,当字符的长度超过ignore_above的值,那么它不会被索引
  • ignore_malformed : 是否忽略不规则的数据,该参数默认为 false
  • index : 是否检索
  • fields : 具体的索引方式
  • normalizer : 解析前(索引或者查询)的标准化配置
  • norms : 该字段不分词
GET /order/_mapping
{
  "order" : {
    "mappings" : {
      "properties" : {
        "_class" : {
          "type" : "text",
          "fields" : {
            "keyword" : {
              "type" : "keyword",
              "ignore_above" : 256
            }
          }
        },
        "updateTime" : {
          "type" : "long"
        },
        "username" : {
          "type" : "text",
          "fields" : {
            "keyword" : {
              "type" : "keyword",
              "ignore_above" : 256
            }
          }
        }
      }
    }
  }
}


这里只是概念普及 , 如果想深入 , 可以看看这篇文档 blog.csdn.net/winterking3…

参考文档