一、概述
1.1介绍
The Elastic Stack, 包括 Elasticsearch、Kibana、Beats 和 Logstash(也称为 ELK Stack)。 能够安全可靠地获取任何来源、任何格式的数据,然后实时地对数据进行搜索、分析和可视 化。Elaticsearch,简称为 ES,ES 是一个开源的高扩展的分布式全文搜索引擎,是整个 Elastic Stack 技术栈的核心。它可以近乎实时的存储、检索数据;本身扩展性很好,可以扩展到上 百台服务器,处理 PB 级别的数据。
Elasticsearch是由elastic公司开发的一套搜索引擎技术,它是elastic技术栈中的一部分。完整的技术栈包括:
-
Elasticsearch:用于数据存储、计算和搜索
-
Logstash/Beats:用于数据收集
-
Kibana:用于数据可视化
整套技术栈被称为ELK,经常用来做日志收集、系统监控和状态分析等等:
1.2全文搜索引擎
Google,百度类的网站搜索,它们都是根据网页中的关键字生成索引,我们在搜索的时 候输入关键字,它们会将该关键字即索引匹配到的所有网页返回;还有常见的项目中应用日 志的搜索等等。对于这些非结构化的数据文本,关系型数据库搜索不是能很好的支持。全文搜索引擎
全⽂检索 是计算机程序通过扫描⽂章中的 每⼀个词,对每⼀个词建⽴⼀个索 引,指明该词在⽂章中出现的次数和位置 。当⽤户 查询时根据建⽴的索引查找 , 类似于通过字典的检索字表查字的过程
索: 建⽴索引 ⽂本---->切分 ---> 词 ⽂章出现过 出现多少次
检索: 查询 关键词---> 索引中--> 符合条件⽂章 相关度排序
全⽂检索(Full-Text Retrieval)以⽂本作为检索对象,找出含有指定词汇的⽂ 本。全⾯、准确和快速是衡量全⽂检索系统的关键指标
- 只处理⽂本、不处理语义
- 搜索时英⽂不区分⼤⼩写
- 结果列表有相关度排序
1.3搜索引擎软件
Lucene 是 Apache 软件基金会 Jakarta 项目组的一个子项目,提供了一个简单却强大的 应用程式接口,能够做全文索引和搜寻。在 Java 开发环境里 Lucene 是一个成熟的免费开源 工具。就其本身而言,Lucene 是当前以及最近几年最受欢迎的免费 Java 信息检索程序库。 但 Lucene 只是一个提供全文搜索功能类库的核心工具包,而真正使用它还需要一个完善的 服务框架搭建起来进行应用。
目前市面上流行的搜索引擎软件,主流的就两款:Elasticsearch 和 Solr,这两款都是基 于 Lucene 搭建的,可以独立部署启动的搜索引擎服务软件。由于内核相同,所以两者除了 服务器安装、部署、管理、集群以外,对于数据的操作 修改、添加、保存、查询等等都十 分类似。
在使用过程中,一般都会将 Elasticsearch 和 Solr 这两个软件对比,然后进行选型。这两 个搜索引擎都是流行的,先进的的开源搜索引擎。它们都是围绕核心底层搜索库 - Lucene 构建的 - 但它们又是不同的。像所有东西一样,每个都有其优点和缺点:
1.4Elasticsearch 应用案例
1.5安装部署
ElasticSearch
ElasticSearch 简称 ES ,是基于 Apache Lucene 构建的 开源搜索引 擎 ,是当前最流⾏的 企业级搜索引擎 。 Lucene本身就可以被认为迄今为⽌性 能最好的⼀款开源搜索引擎⼯具包 ,但是lucene的API相对复杂,需要深厚的 搜索理论。很难集成到实际的应⽤中去。 ES是采⽤java语⾔编写,提供了简 单易⽤的RestFul API,开发者可以使⽤其简单的RestFul API,开发相关的搜 索功能,从⽽避免lucene的复杂性
docker run -d \
--name es \
-e "ES_JAVA_OPTS=-Xms512m -Xmx512m" \
-e "discovery.type=single-node" \
-v es-data:/usr/share/elasticsearch/data \
-v es-plugins:/usr/share/elasticsearch/plugins \
--privileged \
--network hm-net \
-p 8200:9200 \
-p 8300:9300 \
elasticsearch:7.12.1
注意,这里我们采用的是elasticsearch的7.12.1版本,由于8以上版本的JavaAPI变化很大,在企业中应用并不广泛,企业中应用较多的还是8以下的版本。
进入容器内部
sudo docker exec -it c4af11a6c1a5 /bin/bash
Kibana
Kibana Navicat 是⼀个针对 Elasticsearch mysql 的 开源分析及可视化 平台 ,使⽤Kibana可以 查询、查看并与存储在ES索引的数据进⾏交互操作 ,使 ⽤Kibana能执⾏⾼级的 数据分析,并能以图表、表格和地图的形式查看数据
docker run -d \
--name kibana \
-e ELASTICSEARCH_HOSTS=http://es:8200 \
-p 8601:5601 \
kibana:7.12.1
安装完成后,直接访问5601端口,即可看到控制台页面:
选择Explore on my own
之后,进入主页面:
然后选中Dev tools
,进入开发工具页面:
1.6核心概念
索引
⼀个索引就是⼀个拥有⼏分相似特征的⽂档的集合 。⽐如说,你可以有⼀个 商品数据的索引,⼀个订单数据的索引,还有⼀个⽤户数据的索引。 ⼀个 索引由⼀个名字来标识 (必须全部是⼩写字⺟的) ,并且当我们要对这个索引 中的⽂档进⾏索引、搜索、更新和删除的时候,都要使⽤到这个名字
随着业务发展,需要在es中存储的文档也会越来越多,比如有商品的文档、用户的文档、订单文档等等:
所有文档都散乱存放显然非常混乱,也不方便管理。
因此,我们要将类型相同的文档集中在一起管理,称为索引(Index)。例如:
映射
映射是定义⼀个⽂档和它所包含的字段如何被存储和索引的过程 。在默认配 置下,ES可以根据插⼊的数据 ⾃动地创建mapping,也可以⼿动创建 mapping 。 mapping中主要包括字段名、字段类型等
数据库的表会有约束信息,用来定义表的结构、字段的名称、类型等信息。因此,索引库中就有映射(mapping),是索引中文档的字段约束信息,类似表的结构约束。
⽂档
⽂档是索引中存储的⼀条条数据。⼀条⽂档是⼀个可被索引的最⼩单元 。ES 中的⽂档采⽤了轻量级的JSON格式数据来表示。
elasticsearch是面向文档(Document)存储的,可以是数据库中的一条商品数据,一个订单信息。文档数据会被序列化为json
格式后存储在elasticsearch
中:
因此,原本数据库中的一行数据就是ES中的一个JSON文档;而数据库中每行数据都包含很多列,这些列就转换为JSON文档中的字段(Field)。
mysql与elasticsearch
MySQL | Elasticsearch | 说明 |
---|---|---|
Table | Index | 索引(index),就是文档的集合,类似数据库的表(table) |
Row | Document | 文档(Document),就是一条条的数据,类似数据库中的行(Row),文档都是JSON格式 |
Column | Field | 字段(Field),就是JSON文档中的字段,类似数据库中的列(Column) |
Schema | Mapping | Mapping(映射)是索引中文档的约束,例如字段类型约束。类似数据库的表结构(Schema) |
SQL | DSL | DSL是elasticsearch提供的JSON风格的请求语句,用来操作elasticsearch,实现CRUD |
那是不是说,我们学习了elasticsearch就不再需要mysql了呢?
并不是如此,两者各自有自己的擅长之处:
-
Mysql:擅长事务类型操作,可以确保数据的安全和一致性
-
Elasticsearch:擅长海量数据的搜索、分析、计算
因此在企业中,往往是两者结合使用:
-
对安全性要求较高的写操作,使用mysql实现
-
对查询性能要求较高的搜索需求,使用elasticsearch实现
-
两者再基于某种方式,实现数据的同步,保证一致性
1.7倒排索引
elasticsearch之所以有如此高性能的搜索表现,正是得益于底层的倒排索引技术。那么什么是倒排索引呢?
倒排索引的概念是基于MySQL这样的正向索引而言的。
正向索引
我们先来回顾一下正向索引。
例如有一张名为tb_goods
的表:
id | title | price |
---|---|---|
1 | 小米手机 | 3499 |
2 | 华为手机 | 4999 |
3 | 华为小米充电器 | 49 |
4 | 小米手环 | 49 |
... | ... | ... |
其中的id
字段已经创建了索引,由于索引底层采用了B+树结构,因此我们根据id搜索的速度会非常快。但是其他字段例如title
,只在叶子节点上存在。
因此要根据title
搜索的时候只能遍历树中的每一个叶子节点,判断title数据是否符合要求。
比如用户的SQL语句为:
select * from tb_goods where title like '%手机%';
综上,根据id精确匹配时,可以走索引,查询效率较高。而当搜索条件为模糊匹配时,由于索引无法生效,导致从索引查询退化为全表扫描,效率很差。
因此,正向索引适合于根据索引字段的精确搜索,不适合基于部分词条的模糊匹配。
而倒排索引恰好解决的就是根据部分词条模糊匹配的问题。
倒排索引
倒排索引中有两个非常重要的概念:
-
文档(
Document
):用来搜索的数据,其中的每一条数据就是一个文档。例如一个网页、一个商品信息 -
词条(
Term
):对文档数据或用户搜索数据,利用某种算法分词,得到的具备含义的词语就是词条。例如:我是中国人,就可以分为:我、是、中国人、中国、国人这样的几个词条
创建倒排索引是对正向索引的一种特殊处理和应用,流程如下:
-
将每一个文档的数据利用分词算法根据语义拆分,得到一个个词条
-
创建表,每行数据包括词条、词条所在文档id、位置等信息
-
因为词条唯一性,可以给词条创建正向索引
此时形成的这张以词条为索引的表,就是倒排索引表,两者对比如下:
倒排索引的搜索流程如下(以搜索"华为手机"为例),如图:
总结
正向索引是最传统的,根据id索引的方式。但根据词条查询时,必须先逐条获取每个文档,然后判断文档中是否包含所需要的词条,是根据文档找词条的过程。
而倒排索引则相反,是先找到用户要搜索的词条,根据词条得到保护词条的文档的id,然后根据id获取文档。是根据词条找文档的过程。
正向索引:
优点:
可以给多个字段创建索引
根据索引字段搜索、排序速度非常快
缺点:
根据非索引字段,或者索引字段中的部分词条查找时,只能全表扫描。
倒排索引:
优点:
根据词条搜索、模糊搜索时,速度非常快
缺点:
只能给词条创建索引,而不是字段
无法根据字段做排序
1.8分词器
Analysis 和 Analyzer
Analysis : ⽂本分析是把全⽂本转换⼀系列单词(term/token)的过程,也 叫分词(Analyzer)。Analysis是通过Analyzer来实现的。 分词就是将⽂档通过 Analyzer分成⼀个⼀个的Term(关键词查询),每⼀个Term都指向包含这个Term 的⽂档
Analyzer 组成 注意: 在ES中默认使⽤标准分词器: StandardAnalyzer 特点: 中⽂单字分 词 单词分词 我是中国⼈ this is good man----> analyzer----> 我 是 中 国 ⼈ this is good man
Analyzer 组成
注意: 在ES中默认使⽤标准分词器: StandardAnalyzer 特点: 中⽂单字分 词 单词分词
我是中国⼈ this is good man----> analyzer----> 我 是 中 国 ⼈ this is good man
分析器(analyzer)都由三种构件组成的: character filters , tokenizers , token filters 。
注意:
- 三者顺序: Character Filters--->Tokenizer--->Token Filter
- 三者个数:Character Filters(0个或多个) + Tokenizer + Token Filters(0 个或多个
内置分词器
- Standard Analyzer - 默认分词器,英⽂按单词词切分,并⼩写处理
- Simple Analyzer - 按照单词切分(符号被过滤), ⼩写处理
- Stop Analyzer - ⼩写处理,停⽤词过滤(the,a,is)
- Whitespace Analyzer - 按照空格切分,不转⼩写
- Keyword Analyzer - 不分词,直接将输⼊当作输出
内置分词器测试
标准分词器
特点: 按照单词分词 英⽂统⼀转为⼩写 过滤标点符号 中⽂单字分词
POST /_analyze
{
"analyzer": "standard",
"text": "this is a , good Man 中华⼈⺠共和国"
}
Simple 分词器
特点: 英⽂按照单词分词 英⽂统⼀转为⼩写 去掉符号 中⽂按照空格 进⾏分词
POST /_analyze
{
"analyzer": "simple",
"text": "this is a , good Man 中华⼈⺠共和国"
}
Whitespace 分词器
特点: 中⽂ 英⽂ 按照空格分词 英⽂不会转为⼩写 不去掉标点符号
POST /_analyze
{
"analyzer": "whitespace",
"text": "this is a , good Man"
}
创建索引设置分词
PUT /索引名
{
"settings": {},
"mappings": {
"properties": {
"title":{
"type": "text",
"analyzer": "standard" //显示指定分词器
}
}
}
}
中⽂分词器
在ES中⽀持中⽂分词器⾮常多 如 smartCN、IK 等,推荐的就是 IK分词 器
1.9IK分词器
docker exec -it es ./bin/elasticsearch-plugin install https://github.com/medcl/elasticsearch-analysis-ik/releases/download/v7.12.1/elasticsearch-analysis-ik-7.12.1.zip
然后重启es容器:
docker restart es
IK分词器包含两种模式:
-
ik_smart
:智能语义切分 -
ik_max_word
:最细粒度切分
POST /_analyze
{
"analyzer": "ik_smart",
"text": "程序员学习java太棒了"
}
拓展词典
随着互联网的发展,“造词运动”也越发的频繁。出现了很多新的词语,在原有的词汇列表中并不存在。比如:“泰裤辣”,“传智播客” 等。
IK分词器如何拓展词条?如何停用词条?
-
利用config目录的
IkAnalyzer.cfg.xml
文件添加拓展词典和停用词典 -
在词典中添加拓展词条或者停用词条
1. 修改vim IKAnalyzer.cfg.xml
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE properties SYSTEM
"http://java.sun.com/dtd/properties.dtd">
<properties>
<comment>IK Analyzer 扩展配置</comment>
<!--⽤户可以在这⾥配置⾃⼰的扩展字典 -->
<entry key="ext_dict">ext_dict.dic</entry>
<!--⽤户可以在这⾥配置⾃⼰的扩展停⽌词字典-->
<entry key="ext_stopwords">ext_stopword.dic</entry>
</properties>
2. 在ik分词器⽬录下config⽬录中创建ext_dict.dic⽂件 编码⼀定要为
UTF-8才能⽣效
vim ext_dict.dic 加⼊扩展词即可
3. 在ik分词器⽬录下config⽬录中创建ext_stopword.dic⽂件
vim ext_stopword.dic 加⼊停⽤词即可
4.重启es⽣效
二、快速入门
2.1索引库操作
Index就类似数据库表,Mapping映射就类似表的结构。我们要向es中存储数据,必须先创建Index和Mapping
Mapping是对索引库中文档的约束,常见的Mapping属性包括:
-
type
:字段数据类型,常见的简单类型有:-
字符串:
text
(可分词的文本)、keyword
(精确值,例如:品牌、国家、ip地址) -
数值:
long
、integer
、short
、byte
、double
、float
、 -
布尔:
boolean
-
日期:
date
-
对象:
object
-
-
index
:是否创建索引,默认为true
-
analyzer
:使用哪种分词器 -
properties
:该字段的子字段 -
store:是否将数据进行独立存储,默认为 false 原始的文本会存储在_source 里面,默认情况下其他提取出来的字段都不是独立存储 的,是从_source 里面提取出来的。当然你也可以独立的存储某个字段,只要设置 "store": true 即可,获取独立存储的字段要比从_source 中解析快得多,但是也会占用 更多的空间,所以要根据实际业务需求来设置。
说明: ES中⽀持字段类型⾮常丰富,如:text、keyword、integer、 long、ip 等。更多参⻅https://www.elastic.co/guide/en/elasticsearch/re ference/7.15/mapping-types.html
2.2索引库的CRUD
创建索引库和映射
基本语法:
-
请求方式:
PUT
-
请求路径:
/索引库名
,可以自定义 -
请求参数:
mapping
映射
格式:
PUT /索引库名称
{
"mappings": {
"properties": {
"字段名":{
"type": "text",
"analyzer": "ik_smart"
},
"字段名2":{
"type": "keyword",
"index": "false"
},
"字段名3":{
"properties": {
"子字段": {
"type": "keyword"
}
}
},
// ...略
}
}
}
示例:
PUT /yanyu
{
"mappings": {
"properties": {
"info":{
"type": "text",
"analyzer": "ik_smart"
},
"email":{
"type": "keyword",
"index": "false"
},
"name":{
"properties": {
"firstName": {
"type": "keyword"
}
}
}
}
}
}
# 1.创建索引
- PUT /索引名 ====> PUT /products
- 注意:
1.ES中索引健康转态 red(索引不可⽤) 、yellwo(索引可⽤,存在⻛险)、
green(健康)
2.默认ES在创建索引时回为索引创建1个备份索引和⼀个primary索引
# 2.创建索引 进⾏索引分⽚配置
- PUT /products
{
"settings": {
"number_of_shards": 1, #指定主分⽚的数量
"number_of_replicas": 0 #指定副本分⽚的数量
}
}
查询索引库
基本语法:
-
请求方式:GET
-
请求路径:/索引库名
-
请求参数:无
格式:
GET /索引库名
示例:
GET /yanyu
查看所有索引
GET /_cat/indices?v
修改索引库
倒排索引结构虽然不复杂,但是一旦数据结构改变(比如改变了分词器),就需要重新创建倒排索引,这简直是灾难。因此索引库一旦创建,无法修改mapping。
虽然无法修改mapping中已有的字段,但是却允许添加新的字段到mapping中,因为不会对倒排索引产生影响。因此修改索引库能做的就是向索引库中添加新字段,或者更新索引库的基础属性。
语法说明:
PUT /索引库名/_mapping
{
"properties": {
"新字段名":{
"type": "integer"
}
}
}
示例:
PUT /yanyu/_mapping
{
"properties": {
"age":{
"type": "integer"
}
}
}
删除索引库
语法:
-
请求方式:DELETE
-
请求路径:/索引库名
-
请求参数:无
格式:
DELETE /索引库名
示例:
DELETE /heima
2.3映射
查询
# 1.查看某个索引的映射
- GET /索引名/_mapping =====> GET /products/_mapping
2.4文档操作
有了索引库,接下来就可以向索引库中添加数据了。
Elasticsearch中的数据其实就是JSON风格的文档。操作文档自然保护增
、删
、改
、查
等几种常见操作,我们分别来学习。
新增文档
语法:
POST /索引库名/_doc/文档id
{
"字段1": "值1",
"字段2": "值2",
"字段3": {
"子属性1": "值3",
"子属性2": "值4"
},
}
示例:
POST /yanyu/_doc/1
{
"info": "讲师",
"email": "zy@itcast.cn",
"name": {
"firstName": "云",
"lastName": "赵"
}
}
注意
POST /products/_doc/ #⾃动⽣成⽂档id
{
"title":"iphone14",
"price":8999.99,
"created_at":"2021-09-15",
"description":"iPhone 13屏幕采⽤6.8英⼨OLED屏幕"
}
查询文档
根据rest风格,新增是post,查询应该是get,不过查询一般都需要条件,这里我们把文档id带上。
语法:
GET /{索引库名称}/_doc/{id}
示例:
GET /yanyu/_doc/1
查询所有文档
在 Postman 中,向 ES 服务器发 GET 请求 :http://127.0.0.1:9200/student/_search
{
"query": {
"match_all": {}
}
}
# "query":这里的 query 代表一个查询对象,里面可以有不同的查询属性
# "match_all":查询类型,例如:match_all(代表查询所有), match,term , range 等等
# {查询条件}:查询条件会根据类型的不同,写法也有差异
GET /yanyu/_search
删除文档
删除使用DELETE请求,同样,需要根据id进行删除:
删除一个文档不会立即从磁盘上移除,它只是被标记成已删除(逻辑删除)。
语法:
DELETE /{索引库名}/_doc/id值
示例:
DELETE /yanyu/_doc/1
条件删除文档
一般删除数据都是根据文档的唯一性标识进行删除,实际操作时,也可以根据条件对多条数 据进行删除
向 ES 服务器发 POST 请求
POST /yanyu/_delete_by_query
{
"query": {
"match": {
"name.firstName": "云222"
}
}
}
修改文档
修改有两种方式:
-
全量修改:直接覆盖原来的文档
-
局部修改:修改文档中的部分字段
全量修改
全量修改是覆盖原来的文档,其本质是两步操作:
-
根据指定的id删除文档
-
新增一个相同id的文档
注意:如果根据id删除时,id不存在,第二步的新增也会执行,也就从修改变成了新增操作了。
语法:
PUT /{索引库名}/_doc/文档id
{
"字段1": "值1",
"字段2": "值2",
// ... 略
}
示例:
PUT /heima/_doc/1
{
"info": "Java讲师",
"email": "zy@itcast.cn",
"name": {
"firstName": "云",
"lastName": "赵"
}
}
局部修改
局部修改是只修改指定id匹配的文档中的部分字段。
语法:
POST /{索引库名}/_update/文档id
{
"doc": {
"字段名": "新的值",
}
}
示例:
POST /yanyu/_update/1
{
"doc": {
"email": "ZhaoYun@itcast.cn"
}
}
批处理
批处理采用POST请求,基本语法如下:
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"} }
:要更新的文档字段
-
示例,批量新增:
POST /_bulk
{"index": {"_index":"heima", "_id": "3"}}
{"info": "黑马程序员C++讲师", "email": "ww@itcast.cn", "name":{"firstName": "五", "lastName":"王"}}
{"index": {"_index":"heima", "_id": "4"}}
{"info": "黑马程序员前端讲师", "email": "zhangsan@itcast.cn", "name":{"firstName": "三", "lastName":"张"}}
批量删除:
POST /_bulk
{"delete":{"_index":"heima", "_id": "3"}}
{"delete":{"_index":"heima", "_id": "4"}}
说明:批量时不会因为⼀个失败⽽全部失败,⽽是继续执⾏后续操作,在返 回时按照执⾏的状态返回
2.5⾼级查询DSL
ES中提供了⼀种强⼤的检索数据⽅式,这种检索⽅式称之为 Query DSL , Query DSL 是利⽤ Rest API传递JSON格式的请求体(Request Body)数据 与ES进⾏交互,这种⽅式的 丰富查询语法 让ES检索变得 更强⼤,更简洁
Elasticsearch的查询可以分为两大类:
-
叶子查询(Leaf query clauses):一般是在特定的字段里查询特定值,属于简单查询,很少单独使用。
-
复合查询(Compound query clauses):以逻辑方式组合多个叶子查询或者更改叶子查询的行为方式。
快速入门
我们依然在Kibana的DevTools中学习查询的DSL语法。首先来看查询的语法结构:
GET /{索引库名}/_search
{
"query": {
"查询类型": {
// .. 查询条件
}
}
}
说明:
-
GET /{索引库名}/_search
:其中的_search
是固定路径,不能修改
GET /items/_search
{
"query": {
"match_all": {
}
}
}
你会发现虽然是match_all,但是响应结果中并不会包含索引库中的所有文档,而是仅有10条。这是因为处于安全考虑,elasticsearch设置了默认的查询页数。
叶子查询
叶子查询的类型也可以做进一步细分,详情大家可以查看官方文档:
https://www.elastic.co/guide/en/elasticsearch/reference/7.12/query-dsl.html
如图:
这里列举一些常见的,例如:
-
全文检索查询(Full Text Queries):利用分词器对用户输入搜索条件先分词,得到词条,然后再利用倒排索引搜索词条。例如:
-
match
: -
multi_match
-
-
精确查询(Term-level queries):不对用户输入搜索条件分词,根据字段内容精确值匹配。但只能查找keyword、数值、日期、boolean类型的字段。例如:
-
ids
-
term
-
range
-
-
地理坐标查询:用于搜索地理位置,搜索方式很多,例如:
-
geo_bounding_box
:按矩形搜索 -
geo_distance
:按点和半径搜索
-
--------------------------------------
全文检索查询
match查询
GET /{索引库名}/_search
{
"query": {
"match": {
"字段名": "搜索条件"
}
}
}
GET /yanyu/_search
{
"query": {
"match": {
"name.firstName": "云2"
}
}
}
多字段查询[multi_match]
与match
类似的还有multi_match
,区别在于可以同时对多个字段搜索,而且多个字段都要满足,语法示例:
GET /{索引库名}/_search
{
"query": {
"multi_match": {
"query": "搜索条件",
"fields": ["字段1", "字段2"]
}
}
}
查询所有[match_all]
match_all关键字: 返回索引中的全部⽂档
GET /products/_search
{
"query": {
"match_all": {}
}
}
-------------------------------------
精确查询
精确查询,英文是Term-level query
,顾名思义,词条级别的查询。也就是说不会对用户输入的搜索条件再分词,而是作为一个词条,与搜索的字段内容精确值匹配。因此推荐查找keyword
、数值、日期、boolean
类型的字段。例如:
-
id
-
price
-
城市
详情可以查看官方文档:
https://www.elastic.co/guide/en/elasticsearch/reference/7.12/term-level-queries.html
term
查询
以term
查询为例,其语法如下:
GET /{索引库名}/_search
{
"query": {
"term": {
"字段名": {
"value": "搜索条件"
}
}
}
}
NOTE1: 通过使⽤term查询得知ES中默认使⽤分词器为 标准分词器 (StandardAnalyzer),标准分词器对于英⽂单词分词,对于中⽂单字分词
NOTE2: 通过使⽤term查询得知, 在ES的Mapping Type 中 keyword , date ,integer, long , double , boolean or ip 这些类型不分 词,只有text类型分词
当你输入的搜索条件不是词条,而是短语时,由于不做分词,你反而搜索不到:
范围查询[range]
GET /{索引库名}/_search
{
"query": {
"range": {
"字段名": {
"gte": {最小值},
"lte": {最大值}
}
}
}
}
range
是范围查询,对于范围筛选的关键字有:
-
gte
:大于等于 -
gt
:大于 -
lte
:小于等于 -
lt
:小于
前缀查询[prefix]
prefix 关键字: ⽤来检索含有指定前缀的关键词的相关⽂档
GET /products/_search
{
"query": {
"prefix": {
"title": {
"value": "ipho"
}
}
}
}
通配符查询[wildcard]
wildcard 关键字: 通配符查询 ? ⽤来匹配⼀个任意字符 * ⽤来匹配多个任意字符
GET /products/_search
{
"query": {
"wildcard": {
"description": {
"value": "iphon*"
}
}
}
}
多id查询[ids]
ids 关键字 : 值为数组类型,⽤来根据⼀组id获取多个对应的⽂档
GET /products/_search
{
"query": {
"ids": {
"values":
["verUq3wBOTjuBizqAegi","vurUq3wBOTjuBizqAegk"]
}
}
}
模糊查询[fuzzy]
fuzzy 关键字: ⽤来模糊查询含有指定关键字的⽂档
返回包含与搜索字词相似的字词的文档。
编辑距离是将一个术语转换为另一个术语所需的一个字符更改的次数。这些更改可以包括: 更改字符(box → fox)
删除字符(black → lack)
插入字符(sic → sick)
转置两个相邻字符(act → cat)
为了找到相似的术语,fuzzy 查询会在指定的编辑距离内创建一组搜索词的所有可能的变体 或扩展。然后查询返回每个扩展的完全匹配。
通过 fuzziness 修改编辑距离。一般使用默认值 AUTO,根据术语的长度生成编辑距离。
{
"query": {
"fuzzy": {
"title": {
"value": "zhangsan",
"fuzziness": 2
}
}
}
}
GET /products/_search
{
"query": {
"fuzzy": {
"description": "iphooone"
}
}
}
因此,该查询的含义是在"products"索引中搜索描述字段("description")包与"iphooone"相似的文档。
{ "id": 1, "name": "iPhone X", "description": "The latest iPhone model with Face ID" }
默认字段分词查询[query_string]
GET /products/_search
{
"query": {
"query_string": {
"default_field": "description",
"query": "屏幕真的⾮常不错"
}
}
}
注意: 查询字段分词就将查询条件分词查询 查询字段不分词将查询条件不分词查询
假设在"products"索引中有以下文档:
- { "description": "这款手机的屏幕真的非常出色,色彩鲜艳且清晰。", "price": 5000 }
- { "description": "这台电脑的屏幕质量非常好,分辨率高,色彩还原度高。", "price": 8000 }
- { "description": "这台电视的屏幕效果非常棒,画质细腻,色彩饱满。", "price": 12000 }
- { "description": "这款相机的屏幕显示效果不错,可以实时预览拍摄画面。", "price": 3000 }
---------------------------------------
.复合查询
复合查询大致可以分为两类:
-
第一类:基于逻辑运算组合叶子查询,实现组合条件,例如
-
bool
-
-
第二类:基于某种算法修改查询时的文档相关性算分,从而改变文档排名。例如:
-
function_score
-
dis_max
-
其它复合查询及相关语法可以参考官方文档:
https://www.elastic.co/guide/en/elasticsearch/reference/7.12/compound-queries.html
---------------------------------------
布尔查询[bool]
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}}}
]
}
}
}
过滤查询
过滤查询,其实准确来说,ES中的查询操作分为2种: 查询(query) 和 过滤 (filter) 。查询即是之前提到的 query查询 ,它 (查询)默认会计算每个返 回⽂档的得分,然后根据得分排序。⽽ 过滤(filter) 只会筛选出符合的⽂ 档,并不计算 得分,⽽且它可以缓存⽂档 。所以,单从性能考虑,过滤⽐ 查询更快。 换句话说 过滤适合在⼤范围筛选数据,⽽查询则适合精确匹配数 据。⼀般应⽤时, 应先使⽤过滤操作过滤数据, 然后使⽤查询匹配数据。
GET /ems/emp/_search
{
"query": {
"bool": {
"must": [
{"match_all": {}} //查询条件
],
"filter": {....} //过滤条件
}
}
注意:
- 在执⾏ filter 和 query 时,先执⾏ filter 在执⾏
- query Elasticsearch会⾃动缓存经常使⽤的过滤器,以加快性能。
常⻅过滤类型有: term 、 terms 、ranage、exists、ids等filter。
GET /ems/emp/_search # 使用term过滤
{
"query": {
"bool": {
"must": [
{
"term": {
"name": {
"value": "⼩⿊"
}
}
}
],
"filter": {
"term": {
"content": "框架"
}
}
}
}
}
GET /dangdang/book/_search #使用terms过滤
{
"query": {
"bool": {
"must": [
{
"term": {
"name": {
"value": "中国"
}
}
}
],
"filter": {
"terms": {
"content": [
"科技",
"声⾳"
]
}
}
}
}
}
算分函数查询
当我们利用match查询时,文档结果会根据与搜索词条的关联度打分(_score),返回结果时按照分值降序排列。
例如,我们搜索 "手机",结果如下:
从elasticsearch5.1开始,采用的相关性打分算法是BM25算法,公式如下:
基于这套公式,就可以判断出某个文档与用户搜索的关键字之间的关联度,还是比较准确的。但是,在实际业务需求中,常常会有竞价排名的功能。不是相关度越高排名越靠前,而是掏的钱多的排名靠前。
示例:给IPhone这个品牌的手机算分提高十倍,分析如下:
-
过滤条件:品牌必须为IPhone
-
算分函数:常量weight,值为10
-
算分模式:相乘multiply
对应代码如下:
GET /hotel/_search
{
"query": {
"function_score": {
"query": { .... }, // 原始查询,可以是任意条件
"functions": [ // 算分函数
{
"filter": { // 满足的条件,品牌必须是Iphone
"term": {
"brand": "Iphone"
}
},
"weight": 10 // 算分权重为2
}
],
"boost_mode": "multipy" // 加权模式,求乘积
}
}
}
2.6排序和分页
排序
elasticsearch默认是根据相关度算分(_score
)来排序,但是也支持自定义方式对搜索结果排序。不过分词字段无法排序,能参与排序字段类型有:keyword
类型、数值类型、地理坐标类型、日期类型等。
详细说明可以参考官方文档:
https://www.elastic.co/guide/en/elasticsearch/reference/7.12/sort-search-results.html
语法说明:
GET /indexName/_search
{
"query": {
"match_all": {}
},
"sort": [
{
"排序字段": {
"order": "排序方式asc和desc"
}
}
]
}
GET /items/_search
{
"query": {
"match_all": {}
},
"sort": [
{
"price": {
"order": "desc"
}
}
]
}
返回指定字段[_source]
GET /products/_search
{
"query": {
"match_all": {}
},
"_source": ["title","description"]
}
过滤字段
我们也可以通过:
includes:来指定想要显示的字段
excludes:来指定不想要显示的字段
{
"_source": {
"includes": ["name","nickname"]
},
"query": {
"terms": {
"nickname": ["zhangsan"]
}
}
}
多字段排序
{
"query": {
"match_all": {}
},
"sort": [
{
"age": {
"order": "desc"
}
},
{
"_score":{
"order": "desc"
}
}
]
}
分页
返回指定条数[size】
GET /products/_search
{
"query": {
"match_all": {}
},
"size": 5
}
深度分页
elasticsearch的数据一般会采用分片存储,也就是把一个索引中的数据分成N份,存储到不同节点上。这种存储方式比较有利于数据扩展,但给分页带来了一些麻烦。
比如一个索引库中有100000条数据,分别存储到4个分片,每个分片25000条数据。现在每页查询10条,查询第99页。那么分页查询的条件如下:
GET /items/_search
{
"from": 990, // 从第990条开始查询
"size": 10, // 每页查询10条
"sort": [
{
"price": "asc"
}
]
}
从语句来分析,要查询第990~1000名的数据。
从实现思路来分析,肯定是将所有数据排序,找出前1000名,截取其中的990~1000的部分。但问题来了,我们如何才能找到所有数据中的前1000名呢?
要知道每一片的数据都不一样,第1片上的第900~1000,在另1个节点上并不一定依然是900~1000名。所以我们只能在每一个分片上都找出排名前1000的数据,然后汇总到一起,重新排序,才能找出整个索引库中真正的前1000名,此时截取990~1000的数据即可。
如图:
试想一下,假如我们现在要查询的是第999页数据呢,是不是要找第9990~10000的数据,那岂不是需要把每个分片中的前10000名数据都查询出来,汇总在一起,在内存中排序?如果查询的分页深度更深呢,需要一次检索的数据岂不是更多?
由此可知,当查询分页深度较大时,汇总数据过多,对内存和CPU会产生非常大的压力。
因此elasticsearch会禁止from+ size
超过10000的请求。
针对深度分页,elasticsearch提供了两种解决方案:
-
search after
:分页时需要排序,原理是从上一次的排序值开始,查询下一页数据。官方推荐使用的方式。 -
scroll
:原理将排序后的文档id形成快照,保存下来,基于快照做分页。官方已经不推荐使用。
详情见文档:
https://www.elastic.co/guide/en/elasticsearch/reference/7.12/paginate-search-results.html
总结:
大多数情况下,我们采用普通分页就可以了。查看百度、京东等网站,会发现其分页都有限制。例如百度最多支持77页,每页不足20条。京东最多100页,每页最多60条。
因此,一般我们采用限制分页深度的方式即可,无需实现深度分页。
2.7.高亮
在进行关键字搜索时,搜索出的内容中的关键字会显示不同的颜色,称之为高亮。
css样式肯定是前端实现页面的时候写好的,但是前端编写页面的时候是不知道页面要展示什么数据的,不可能给数据加标签。而服务端实现搜索功能,要是有elasticsearch
做分词搜索,是知道哪些词条需要高亮的。
因此词条的高亮标签肯定是由服务端提供数据的时候已经加上的。
因此实现高亮的思路就是:
-
用户输入搜索关键字搜索数据
-
服务端根据搜索关键字到elasticsearch搜索,并给搜索结果中的关键字词条添加
html
标签 -
前端提前给约定好的
html
标签添加CSS
样式
GET /{索引库名}/_search
{
"query": {
"match": {
"搜索字段": "搜索关键字"
}
},
"highlight": {
"fields": {
"高亮字段名称": {
"pre_tags": "<em>",
"post_tags": "</em>"
}
}
}
}
注意:
搜索必须有查询条件,而且是全文检索类型的查询条件,例如
match
参与高亮的字段必须是
text
类型的字段默认情况下参与高亮的字段要与搜索字段一致,除非添加:
required_field_match=false
GET /products/_search
{
"query": {
"term": {
"description": {
"value": "iphone"
}
}
},
"highlight": {
"fields": {
"*":{}
}
}
}
表示对所有字段进行高亮显示。
GET /products/_search
{
"query": {
"term": {
"description": {
"value": "iphone"
}
}
},
"highlight": {
"post_tags": ["</span>"],
"pre_tags": ["<span style='color:red'>"],
"fields": {
"*":{}
}
}
}
"post_tags": [""]表示在高亮显示的文本后面添加一个结束标签,即""。
"pre_tags": [""]表示在高亮显示的文本前面添加一个开始标签,即""。
这两个标签用于将高亮显示的文本包裹起来,以便在显示时能够以红色字体突出显示。
多字段⾼亮 使⽤ require_field_match 开启多个字段⾼亮
GET /products/_search
{
"query": {
"term": {
"description": {
"value": "iphone"
}
}
},
"highlight": {
"require_field_match": "false",
"post_tags": ["</span>"],
"pre_tags": ["<span style='color:red'>"],
返回指定条数[size]
size 关键字: 指定查询结果中返回指定条数。 默认返回值10条
分⻚查询[form]
from 关键字: ⽤来指定起始返回位置,和size关键字连⽤可实现分⻚效
果
"fields": {
"*":{}
}
}
}
2.8聚合(aggregations
)
聚合:英⽂为Aggregation,是es除搜索功能外提供的针对es数据做统计分 析的功能。聚合有助于根据搜索查询提供聚合数据。聚合查询是数据库中 重要的功能特性,ES作为搜索引擎兼数据库,同样提供了强⼤的聚合分析 能⼒。它基于查询条件来对数据进⾏分桶、计算的⽅法。有点类似于 SQL 中的 group by 再加⼀些函数⽅法的操作
聚合(aggregations
)可以让我们极其方便的实现对数据的统计、分析、运算。例如:
-
什么品牌的手机最受欢迎?
-
这些手机的平均价格、最高价格、最低价格?
-
这些手机每月的销售情况如何?
实现这些统计功能的比数据库的sql要方便的多,而且查询速度非常快,可以实现近实时搜索效果。
官方文档:
https://www.elastic.co/guide/en/elasticsearch/reference/7.12/search-aggregations.html
注意:参加聚合的字段必须是keyword、日期、数值、布尔类型
注意事项:text类型是不⽀持聚合的
测试数据
# 创建索引 index 和映射 mapping
PUT /fruit
{
"mappings": {
"properties": {
"title":{
"type": "keyword"
},
"price":{
"type":"double"
},
"description":{
"type": "text",
"analyzer": "ik_max_word"
}
}
}
}
# 放⼊测试数据
PUT /fruit/_bulk
{"index":{}}
{"title" : "⾯包","price" : 19.9,"description" : "⼩⾯包⾮常好
吃"}
{"index":{}}
{"title" : "旺仔⽜奶","price" : 29.9,"description" : "⾮常好
喝"}
{"index":{}}
{"title" : "⽇本⾖","price" : 19.9,"description" : "⽇本⾖⾮常
好吃"}
{"index":{}}
使⽤
根据某个字段分组
{"title" : "⼩馒头","price" : 19.9,"description" : "⼩馒头⾮常
好吃"}
{"index":{}}
{"title" : "⼤辣⽚","price" : 39.9,"description" : "⼤辣⽚⾮常
好吃"}
{"index":{}}
{"title" : "透⼼凉","price" : 9.9,"description" : "透⼼凉⾮常好
喝"}
{"index":{}}
{"title" : "⼩浣熊","price" : 19.9,"description" : "童年的味
道"}
{"index":{}}
{"title" : "海苔","price" : 19.9,"description" : "海的味道"}
Bucket聚合
例如我们要统计所有商品中共有哪些商品分类,其实就是以分类(category)字段对数据分组。category值一样的放在同一组,属于Bucket
聚合中的Term
聚合。
基本语法如下:
GET /items/_search
{
"size": 0,
"aggs": {
"category_agg": {
"terms": {
"field": "category",
"size": 20
}
}
}
}
# 根据某个字段进⾏分组 统计数量
GET /fruit/_search
{
"query": {
"term": {
"description": {
"value": "好吃"
}
}
},
"aggs": {
"price_group": {
"terms": {
"field": "price"
}
求最⼤值
求最⼩值
}
}
}
带条件聚合
默认情况下,Bucket聚合是对索引库的所有文档做聚合,例如我们统计商品中所有的品牌,结果如下:
但真实场景下,用户会输入搜索条件,因此聚合必须是对搜索结果聚合。那么聚合必须添加限定条件。
例如,我想知道价格高于3000元的手机品牌有哪些,该怎么统计呢?
我们需要从需求中分析出搜索查询的条件和聚合的目标:
-
搜索查询条件:
-
价格高于3000
-
必须是手机
-
-
聚合目标:统计的是品牌,肯定是对brand字段做term聚合
语法如下:
GET /items/_search
{
"query": {
"bool": {
"filter": [
{
"term": {
"category": "手机"
}
},
{
"range": {
"price": {
"gte": 300000
}
}
}
]
}
},
"size": 0,
"aggs": {
"brand_agg": {
"terms": {
"field": "brand",
"size": 20
}
}
}
}
Metric聚合
我们统计了价格高于3000的手机品牌,形成了一个个桶。现在我们需要对桶内的商品做运算,获取每个品牌价格的最小值、最大值、平均值。
这就要用到Metric
聚合了,例如stat
聚合,就可以同时获取min
、max
、avg
等结果。
求最⼤值
# 求最⼤值
GET /fruit/_search
{
"aggs": {
"price_max": {
"max": {
"field": "price"
}
}
}
}
# 求平均值
GET /fruit/_search
{
"aggs": {
"price_agv": {
"avg": {
"field": "price"
}
}
}
}
# 求和
GET /fruit/_search
{
"aggs": {
"price_sum": {
"sum": {
"field": "price"
}
}
}
}
三、Java实战
3.1RestAPI
初始化RestClient
ES官方提供了各种不同语言的客户端,用来操作ES。这些客户端的本质就是组装DSL语句,通过http请求发送给ES。
官方文档地址:
Elasticsearch Clients | Elastic
由于ES目前最新版本是8.8,提供了全新版本的客户端,老版本的客户端已经被标记为过时。而我们采用的是7.12版本,因此只能使用老版本客户端:RestAPI
<properties>
<maven.compiler.source>11</maven.compiler.source>
<maven.compiler.target>11</maven.compiler.target>
<elasticsearch.version>7.12.1</elasticsearch.version>
</properties>
<dependency>
<groupId>org.elasticsearch.client</groupId>
<artifactId>elasticsearch-rest-high-level-client</artifactId>
</dependency>
测试
public class IndexTest {
private RestHighLevelClient client;
@BeforeEach
void setUp() {
this.client = new RestHighLevelClient(RestClient.builder(
HttpHost.create("http://192.168.150.101:9200")
));
}
@Test
void testConnect() {
System.out.println(client);
}
@AfterEach
void tearDown() throws IOException {
this.client.close();
}
}
创建索引库
由于要实现对商品搜索,所以我们需要将商品添加到Elasticsearch中,不过需要根据搜索业务的需求来设定索引库结构,而不是一股脑的把MySQL数据写入Elasticsearch.
创建索引
import org.elasticsearch.client.indices.CreateIndexRequest;
@Test
void testCreateIndex() throws IOException {
// 1.创建Request对象
CreateIndexRequest request = new CreateIndexRequest("items");
// 2.准备请求参数
request.source(MAPPING_TEMPLATE, XContentType.JSON);
// 3.发送请求
client.indices().create(request, RequestOptions.DEFAULT);
}
static final String MAPPING_TEMPLATE = "{\n" +
" \"mappings\": {\n" +
" \"properties\": {\n" +
" \"id\": {\n" +
" \"type\": \"keyword\"\n" +
" },\n" +
" \"name\":{\n" +
" \"type\": \"text\",\n" +
" \"analyzer\": \"ik_max_word\"\n" +
" },\n" +
" \"price\":{\n" +
" \"type\": \"integer\"\n" +
" },\n" +
" \"stock\":{\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" +
" \"commentCount\":{\n" +
" \"type\": \"integer\"\n" +
" },\n" +
" \"isAD\":{\n" +
" \"type\": \"boolean\"\n" +
" },\n" +
" \"updateTime\":{\n" +
" \"type\": \"date\"\n" +
" }\n" +
" }\n" +
" }\n" +
"}";
判断索引库是否存在
@Test
void testExistsIndex() throws IOException {
// 1.创建Request对象
GetIndexRequest request = new GetIndexRequest("items");
// 2.发送请求
boolean exists = client.indices().exists(request, RequestOptions.DEFAULT);
// 3.输出
System.err.println(exists ? "索引库已经存在!" : "索引库不存在!");
}
删除索引库
@Test
void testDeleteIndex() throws IOException {
// 1.创建Request对象
DeleteIndexRequest request = new DeleteIndexRequest("items");
// 2.发送请求
client.indices().delete(request, RequestOptions.DEFAULT);
}
总结
JavaRestClient操作elasticsearch的流程基本类似。核心是client.indices()
方法来获取索引库的操作对象。
索引库操作的基本步骤:
-
初始化
RestHighLevelClient
-
创建XxxIndexRequest。XXX是
Create
、Get
、Delete
-
准备请求参数(
Create
时需要,其它是无参,可以省略) -
发送请求。调用
RestHighLevelClient#indices().xxx()
方法,xxx是create
、exists
、delete
3.2RestClient操作文档
索引库准备好以后,就可以操作文档了。为了与索引库操作分离,我们再次创建一个测试类,做两件事情:
-
初始化RestHighLevelClient
-
我们的商品数据在数据库,需要利用IHotelService去查询,所以注入这个接口
.新增文档
实体类
@Data
@ApiModel(description = "索引库实体")
public class ItemDoc{
@ApiModelProperty("商品id")
private String id;
@ApiModelProperty("商品名称")
private String name;
@ApiModelProperty("价格(分)")
private Integer price;
@ApiModelProperty("商品图片")
private String image;
@ApiModelProperty("类目名称")
private String category;
@ApiModelProperty("品牌名称")
private String brand;
@ApiModelProperty("销量")
private Integer sold;
@ApiModelProperty("评论数")
private Integer commentCount;
@ApiModelProperty("是否是推广广告,true/false")
private Boolean isAD;
@ApiModelProperty("更新时间")
private LocalDateTime updateTime;
}
查询文档
@Test
void testGetDocumentById() throws IOException {
// 1.准备Request对象
GetRequest request = new GetRequest("items").id("100002644680");
// 2.发送请求
GetResponse response = client.get(request, RequestOptions.DEFAULT);
// 3.获取响应结果中的source
String json = response.getSourceAsString();
ItemDoc itemDoc = JSONUtil.toBean(json, ItemDoc.class);
System.out.println("itemDoc= " + ItemDoc);
}
删除文档
@Test
void testDeleteDocument() throws IOException {
// 1.准备Request,两个参数,第一个是索引库名,第二个是文档id
DeleteRequest request = new DeleteRequest("item", "100002644680");
// 2.发送请求
client.delete(request, RequestOptions.DEFAULT);
}
修改文档
修改我们讲过两种方式:
-
全量修改:本质是先根据id删除,再新增
-
局部修改:修改文档中的指定字段值
在RestClient的API中,全量修改与新增的API完全一致,判断依据是ID:
-
如果新增时,ID已经存在,则修改
-
如果新增时,ID不存在,则新增
这里不再赘述,我们主要关注局部修改的API即可
@Test
void testUpdateDocument() throws IOException {
// 1.准备Request
UpdateRequest request = new UpdateRequest("items", "100002644680");
// 2.准备请求参数
request.doc(
"price", 58800,
"commentCount", 1
);
// 3.发送请求
client.update(request, RequestOptions.DEFAULT);
}
总结
新增文档
查询文档
修改文档
批量导入文档
在之前的案例中,我们都是操作单个文档。而数据库中的商品数据实际会达到数十万条,某些项目中可能达到数百万条。
我们如果要将这些数据导入索引库,肯定不能逐条导入,而是采用批处理方案。常见的方案有:
-
利用Logstash批量导入
-
需要安装Logstash
-
对数据的再加工能力较弱
-
无需编码,但要学习编写Logstash导入配置
-
-
利用JavaAPI批量导入
-
需要编码,但基于JavaAPI,学习成本低
-
更加灵活,可以任意对数据做再加工处理后写入索引库
-
语法说明
BulkRequest
本身其实并没有请求参数,其本质就是将多个普通的CRUD请求组合在一起发送。例如:
-
批量新增文档,就是给每个文档创建一个
IndexRequest
请求,然后封装到BulkRequest
中,一起发出。 -
批量删除,就是创建N个
DeleteRequest
请求,然后封装到BulkRequest
,一起发出
可以看到,能添加的请求有:
-
IndexRequest
,也就是新增 -
UpdateRequest
,也就是修改 -
DeleteRequest
,也就是删除
@Test
void testBulk() throws IOException {
// 1.创建Request
BulkRequest request = new BulkRequest();
// 2.准备请求参数
request.add(new IndexRequest("items").id("1").source("json doc1", XContentType.JSON));
request.add(new IndexRequest("items").id("2").source("json doc2", XContentType.JSON));
// 3.发送请求
client.bulk(request, RequestOptions.DEFAULT);
}
3.3RestClient查询
这里关键的API有两个,一个是request.source()
,它构建的就是DSL中的完整JSON参数。其中包含了query
、sort
、from
、size
、highlight
等所有功能:
另一个是QueryBuilders
,其中包含了我们学习过的各种叶子查询、复合查询等:
快速入门
@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);
}
}
叶子查询
所有的查询条件都是由QueryBuilders来构建的,叶子查询也不例外。因此整套代码中变化的部分仅仅是query条件构造的方式,其它不动。
例如match
查询:
@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);
}
再比如multi_match
查询:
@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
查询:
@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
查询:
@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);
}
复合查询
复合查询也是由QueryBuilders
来构建,我们以bool
查询为例,DSL和JavaAPI的对比如图:
@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);
}
排序和分页
@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);
}
高亮
高亮查询与前面的查询有两点不同:
-
条件同样是在
request.source()
中指定,只不过高亮条件要基于HighlightBuilder
来构造 -
高亮响应结果与搜索的文档结果不在一起,需要单独解析
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);
}
}
3.4RestClient实现聚合
可以看到在DSL中,aggs
聚合条件与query
条件是同一级别,都属于查询JSON参数。因此依然是利用request.source()
方法来设置。
聚合必须的三要素:
-
聚合名称
-
聚合类型
-
聚合字段
聚合可配置属性有:
-
size:指定聚合结果数量
-
order:指定聚合结果排序方式
-
field:指定聚合字段
不过聚合条件的要利用AggregationBuilders
这个工具类来构造。DSL与JavaAPI的语法对比如下:
聚合结果与搜索文档同一级别,因此需要单独获取和解析。具体解析语法如下:
@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);
}
}
四、spring整合
4.1初始化
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-elasticsearch</artifactId>
</dependency>
配置客户端
@Configuration
public class RestClientConfig extends
AbstractElasticsearchConfiguration {
@Override
@Bean
public RestHighLevelClient elasticsearchClient() {
final ClientConfiguration clientConfiguration =
ClientConfiguration.builder()
.connectedTo("172.16.91.10:9200")
.build();
return RestClients.create(clientConfiguration).rest();
}
}
spring:
elasticsearch:
rest:
uris: 114.116.205.180:8200
客户端对象
- ElasticsearchOperations
- RestHighLevelClient 推荐
4.2创建实体类
@Data
@Document(indexName = "products", createIndex = true)
public class Product {
@Id
private Integer id;
@Field(type = FieldType.Keyword)
private String title;
@Field(type = FieldType.Float)
private Double price;
@Field(type = FieldType.Text)
private String description;
}
4.3ElasticsearchOperations文档操作
问题
创建索引⽂档
@SpringBootTest
public class estest {
private final ElasticsearchOperations elasticsearchOperations;
@Autowired
public estest(ElasticsearchOperations elasticsearchOperations) {
this.elasticsearchOperations = elasticsearchOperations;
}
/*
*
save索引一条文档更新一条文档
save方法当文档id不存在时添加文档,当文档id存在时候更新文档
*/
@Test
public void testCreate() {
Product product = new Product();
product.setId(3); //存在id指定id 不存在id⾃动⽣成id
product.setTitle("怡宝矿泉⽔3");
product.setPrice(129.11);
product.setDescription("我们喜欢喝矿泉⽔3....");
elasticsearchOperations.save(product);
}
}
查询⽂档
@Test
public void testGet() {
Product product = elasticsearchOperations.get("1",
Product.class);
System.out.println(product);
}
查询所有
@Test
public void testFindAll() {
SearchHits<Product> productSearchHits =
elasticsearchOperations.search(Query.findAll(),
Product.class);
productSearchHits.forEach(productSearchHit -> {
System.out.println("id: " + productSearchHit.getId());
System.out.println("score: " +
productSearchHit.getScore());
Product product = productSearchHit.getContent();
System.out.println("product: " + product);
});
}
删除⽂档
@Test
public void testDelete() {
Product product = new Product();
product.setId(1);
String delete = elasticsearchOperations.delete(product);
System.out.println(delete);
}
删除所有
@Test
public void testDeleteAll() {
elasticsearchOperations.delete(Query.findAll(),
Product.class);
4.4RestHighLevelClient
创建索引映射
@SpringBootTest
public class estest2 {
private final RestHighLevelClient restHighLevelClient;
@Autowired
public estest2(RestHighLevelClient restHighLevelClient) {
this.restHighLevelClient = restHighLevelClient;
}
@Test
public void testCreateIndex() throws IOException {
CreateIndexRequest createIndexRequest = new
CreateIndexRequest("fruit");
createIndexRequest.mapping("{\n" +
" \"properties\": {\n" +
" \"title\":{\n" +
" \"type\": \"keyword\"\n" +
" },\n" +
" \"price\":{\n" +
" \"type\": \"double\"\n" +
" },\n" +
" \"created_at\":{\n" +
" \"type\": \"date\"\n" +
" },\n" +
" \"description\":{\n" +
" \"type\": \"text\"\n" +
" }\n" +
" }\n" +
" }\n" , XContentType.JSON);
CreateIndexResponse createIndexResponse =
restHighLevelClient.indices().create(createIndexRequest,
RequestOptions.DEFAULT);
System.out.println(createIndexResponse.isAcknowledged());
restHighLevelClient.close();
}
}
其他同RestClient操作文档