图片展示型网站模板下载,文化传播公司网站建设需求,网站备案后 如何建设,intitlt 山西大同网站建设Elasticsearch 文章目录 Elasticsearch简介ELK技术栈Elasticsearch和Lucene 倒排索引正向索引倒排索引正向和倒排 ES概念文档和字段索引和映射Mysql与Elasticsearch 安装ES、Kibana安装单点ES创建网络拉取镜像运行 部署kibana拉取镜像部署 安装Ik插件扩展词词典停用词词典 索引…Elasticsearch 文章目录 Elasticsearch简介ELK技术栈Elasticsearch和Lucene 倒排索引正向索引倒排索引正向和倒排 ES概念文档和字段索引和映射Mysql与Elasticsearch 安装ES、Kibana安装单点ES创建网络拉取镜像运行 部署kibana拉取镜像部署 安装Ik插件扩展词词典停用词词典 索引库操作ping映射属性索引库的CRUD 文档操作新增文档查询文档删除文档修改文档 RestAPImapping映射分析初始化RestClient创建索引表删除索引库判断索引库是否存在总结 RestClient操作文档查询文档删除文档修改文档批量导入文档 DSL查询文档DSL查询分类全文检索查询基本语法 精准查询term查询range查询 地理坐标查询矩形范围查询附近查询 复合查询相关性算分算分函数查询布尔查询 搜索结果处理排序分页 高亮原理实现 RestClient查询文档查询请求解析响应match查询精确查询布尔查询排序与分页 3.6.高亮3.6.1.高亮请求构建3.6.2.高亮结果解析 数据聚合聚合的种类DSL实现聚合Bucket聚合语法聚合结果排序限定聚合范围Metric聚合语法 RestAPI实现聚合API语法 自动补全拼音分词器自定义分词器自动补全查询自动补全查询的JavaAPI 数据同步同步调用异步通知监听binlog ES集群搭建集群创建索引库集群节点角色集群的脑裂问题分布式存储分布式查询集群故障转移 简介
Elasticsearch是一款非常强大的开源搜索引擎具备非常多强大功能可以帮助用户从海量数据中快速找到需要的内容。例如在GitHub搜索代码、在百度搜索问题的答案、在打车软件搜索附近的车。
ELK技术栈
elasticsearch结合kibana、Logstash、Beats也就是elastic stackELK。被广泛应用在日志数据分析、实时监控等领域 而elasticsearch是elastic stack的核心负责存储、搜索、分析数据。 Elasticsearch和Lucene
elasticsearch底层是基于lucene来实现的。
Lucene是一个Java语言的搜索引擎类库是Apache公司的顶级项目由DougCutting于1999年研发。是Apache的开源搜索引擎类库提供了搜索引擎的核心API。
官网地址https://lucene.apache.org/ elasticsearch的发展历史
2004年Shay Banon基于Lucene开发了Compass2010年Shay Banon 重写了Compass取名为Elasticsearch。
官网地址: https://www.elastic.co/cn/ 倒排索引
倒排索引的概念是基于MySQL这样的正向索引而言的。
正向索引
那么什么是正向索引呢例如给下表tb_goods中的id创建索引 如果是根据id查询那么直接走索引查询速度非常快。
但如果是基于title做模糊查询只能是逐行扫描数据流程如下
1用户搜索数据条件是title符合%手机%。
2逐行获取数据比如id为1的数据。
3判断数据中的title是否符合用户搜索条件。
4如果符合则放入结果集不符合则丢弃。回到步骤1。
逐行扫描也就是全表扫描随着数据量增加其查询效率也会越来越低。当数据量达到数百万时就是一场灾难。
倒排索引
倒排索引中有两个非常重要的概念
文档Document用来搜索的数据其中的每一条数据就是一个文档。例如一个网页、一个商品信息词条Term对文档数据或用户搜索数据利用某种算法分词得到的具备含义的词语就是词条。例如我是中国人就可以分为我、是、中国人、中国、国人这样的几个词条
创建倒排索引是对正向索引的一种特殊处理流程如下
将每一个文档的数据利用算法分词得到一个个词条创建表每行数据包括词条、词条所在文档id、位置等信息因为词条唯一性可以给词条创建索引例如hash表结构索引 倒排索引的搜索流程如下以搜索华为手机为例
1用户输入条件华为手机进行搜索。
2对用户输入内容分词得到词条华为、手机。
3拿着词条在倒排索引中查找可以得到包含词条的文档id1、2、3。
4拿着文档id到正向索引中查找具体文档。 虽然要先查询倒排索引再查询倒排索引但是无论是词条、还是文档id都建立了索引查询速度非常快无需全表扫描。
正向和倒排
那么为什么一个叫做正向索引一个叫做倒排索引呢 正向索引是最传统的根据id索引的方式。但根据词条查询时必须先逐条获取每个文档然后判断文档中是否包含所需要的词条是根据文档找词条的过程。 而倒排索引则相反是先找到用户要搜索的词条根据词条得到保护词条的文档的id然后根据id获取文档。是根据词条找文档的过程。
正向索引
优点 可以给多个字段创建索引根据索引字段搜索、排序速度非常快 缺点 根据非索引字段或者索引字段中的部分词条查找时只能全表扫描。
倒排索引
优点 根据词条搜索、模糊搜索时速度非常快 缺点 只能给词条创建索引而不是字段无法根据字段做排序
ES概念
elasticsearch中有很多独有的概念与mysql中略有差别但也有相似之处。
文档和字段
elasticsearch是面向**文档Document**存储的可以是数据库中的一条商品数据一个订单信息。文档数据会被序列化为json格式后存储在elasticsearch中 而Json文档中往往包含很多的字段Field类似于数据库中的列。
索引和映射
索引Index就是相同类型的文档的集合。
例如
所有用户文档就可以组织在一起称为用户的索引所有商品的文档可以组织在一起称为商品的索引所有订单的文档可以组织在一起称为订单的索引 因此可以把索引当做是数据库中的表。
数据库的表会有约束信息用来定义表的结构、字段的名称、类型等信息。因此索引库中就有映射mapping是索引中文档的字段约束信息类似表的结构约束。
Mysql与Elasticsearch
我们统一的把mysql与elasticsearch的概念做一下对比
MySQLElasticsearch说明TableIndex索引(index)就是文档的集合类似数据库的表(table)RowDocument文档Document就是一条条的数据类似数据库中的行Row文档都是JSON格式ColumnField字段Field就是JSON文档中的字段类似数据库中的列ColumnSchemaMappingMapping映射是索引中文档的约束例如字段类型约束。类似数据库的表结构SchemaSQLDSLDSL是elasticsearch提供的JSON风格的请求语句用来操作elasticsearch实现CRUD
MySQL与Elasticsearch两者各自有自己的长处 Mysql擅长事务类型操作可以确保数据的安全和一致性 Elasticsearch擅长海量数据的搜索、分析、计算
因此在企业中往往是两者结合使用
对安全性要求较高的写操作使用mysql实现对查询性能要求较高的搜索需求使用elasticsearch实现两者再基于某种方式实现数据的同步保证一致性 安装ES、Kibana
安装单点ES
创建网络
后续还要部署kibana容器因此需要让es和kibana容器互联。这里先创建一个网络
docker network create es-net拉取镜像
镜像文件比较大可能需要等待一会
docker pull elasticsearch:7.12.1运行
运行docker命令部署单点es
docker run -d \--name es \-e ES_JAVA_OPTS-Xms1024m -Xmx1024m \-e discovery.typesingle-node \-v es-data:/usr/share/elasticsearch/data \-v es-plugins:/usr/share/elasticsearch/plugins \--privileged \--network es-net \-p 9200:9200 \-p 9300:9300 \
elasticsearch:7.12.1命令解释
-e cluster.namees-docker-cluster设置集群名称-e http.host0.0.0.0监听的地址可以外网访问-e ES_JAVA_OPTS-Xms1024m -Xmx1024m内存大小(根据虚拟机内存大小配置最小不要小于512M否则后续会出现内存不足的情况)。-e discovery.typesingle-node非集群模式-v es-data:/usr/share/elasticsearch/data挂载逻辑卷绑定es的数据目录-v es-logs:/usr/share/elasticsearch/logs挂载逻辑卷绑定es的日志目录-v es-plugins:/usr/share/elasticsearch/plugins挂载逻辑卷绑定es的插件目录--privileged授予逻辑卷访问权--network es-net 加入一个名为es-net的网络中-p 9200:9200端口映射配置
访问http://192.168.xxx.xxx:9200如果看到以下页面则证明elasticsearch安装成功。 部署kibana
kibana可以提供一个elasticsearch的可视化界面便于学习。
拉取镜像
docker pull kibana:7.12.1部署
运行docker命令部署kibana
docker run -d \
--name kibana \
-e ELASTICSEARCH_HOSTShttp://es:9200 \
--networkes-net \
-p 5601:5601 \
kibana:7.12.1--network es-net 加入一个名为es-net的网络中与elasticsearch在同一个网络中-e ELASTICSEARCH_HOSTShttp://es:9200设置elasticsearch的地址因为kibana已经与elasticsearch在一个网络因此可以用容器名直接访问elasticsearch-p 5601:5601端口映射配置
kibana启动一般比较慢需要多等待一会可以通过命令
docker logs -f kibana查看运行日志当查看到下面的日志说明成功 此时在浏览器输入地址访问http://192.168.xxx.xxx:5601即可看到结果
kibana中提供了一个DevTools界面这个界面中可以编写DSL来操作elasticsearch。并且对DSL语句有自动补全功能。
安装Ik插件
# 进入容器内部
docker exec -it es /bin/bash# 在线下载并安装
./bin/elasticsearch-plugin install https://github.com/medcl/elasticsearch-analysis-ik/releases/download/v7.12.1/elasticsearch-analysis-ik-7.12.1.zip#退出
exit
#重启容器
docker restart esIK分词器包含两种模式 ik_smart最少切分 ik_max_word最细切分
扩展词词典
随着互联网的发展“造词运动”也越发的频繁。出现了很多新的词语在原有的词汇列表中并不存在。如“泰酷辣”、“永远的神”等。
所以我们的词汇也需要不断的更新IK分词器提供了扩展词汇的功能。
1打开IK分词器config目录(前置目录/var/lib/docker/volumes/es-plugins/_data ) 2在IKAnalyzer.cfg.xml配置文件内容添加
?xml version1.0 encodingUTF-8?
!DOCTYPE properties SYSTEM http://java.sun.com/dtd/properties.dtd
propertiescommentIK Analyzer 扩展配置/comment!--用户可以在这里配置自己的扩展字典 *** 添加扩展词典--entry keyext_dictext.dic/entry
/properties3新建一个 ext.dic可以参考config目录下复制一个配置文件进行修改 泰酷辣
永远的神4重启elasticsearch docker restart es# 查看 日志
docker logs -f es日志中已经成功加载ext.dic配置文件。
停用词词典
在互联网项目中在网络间传输的速度很快所以很多语言是不允许在网络上传递的如关于宗教、政治等敏感词语那么我们在搜索时也应该忽略当前词汇。
IK分词器也提供了强大的停用词功能让我们在索引时就直接忽略当前的停用词汇表中的内容。
1IKAnalyzer.cfg.xml配置文件内容添加
?xml version1.0 encodingUTF-8?
!DOCTYPE properties SYSTEM http://java.sun.com/dtd/properties.dtd
propertiescommentIK Analyzer 扩展配置/comment!--用户可以在这里配置自己的扩展字典--entry keyext_dictext.dic/entry!--用户可以在这里配置自己的扩展停止词字典 *** 添加停用词词典--entry keyext_stopwordsstopword.dic/entry
/properties3在 stopword.dic 添加停用词
“一些敏感词汇”(该内容过于敏感不宜显示)4重启elasticsearch
# 重启服务
docker restart es
docker restart kibana# 查看 日志
docker logs -f es日志中已经成功加载stopword.dic配置文件
注当前文件的编码必须是UTF- 8格式严禁使用Windows记事本编辑。
索引库操作
索引库就类似数据库表mapping映射就类似表的结构。要向es中存储数据必须先创建“库”和“表”。
ping映射属性
mapping是对索引库中文档的约束常见的mapping属性包括
type字段数据类型常见的简单类型有 字符串text可分词的文本、keyword精确值例如品牌、国家、ip地址数值long、integer、short、byte、double、float、布尔boolean日期date对象object index是否创建索引默认为trueanalyzer使用哪种分词器properties该字段的子字段
索引库的CRUD
统一使用Kibana编写DSL的方式来演示。 创建索引库和映射 请求方式PUT请求路径/索引库名可以自定义请求参数mapping映射
格式
PUT /索引库名称
{mappings: {properties: {字段名:{type: text,analyzer: ik_smart},字段名2:{type: keyword,index: false},字段名3:{properties: {子字段: {type: keyword}}},// ...略}}
}查询索引库 请求方式GET 请求路径/索引库名 请求参数无
格式
GET /索引库名修改索引库 倒排索引结构虽然不复杂但是一旦数据结构改变比如改变了分词器就需要重新创建倒排索引这简直是灾难。因此索引库一旦创建无法修改mapping。
虽然无法修改mapping中已有的字段但是却允许添加新的字段到mapping中因为不会对倒排索引产生影响。
格式
PUT /索引库名/_mapping
{properties: {新字段名:{type: integer}}
}删除索引库 请求方式DELETE 请求路径/索引库名 请求参数无
格式
DELETE /索引库名添加字段 格式
PUT /索引库名/_mapping文档操作
新增文档
语法格式
POST /索引库名/_doc/文档id
{字段1: 值1,字段2: 值2,字段3: {子属性1: 值3,子属性2: 值4},// ...
}查询文档
根据rest风格新增是post查询应该是get不过查询一般都需要条件这里我们把文档id带上。
语法格式
GET /{索引库名称}/_doc/{id}删除文档
语法格式
DELETE /{索引库名}/_doc/id值修改文档
修改文档有两种方式一种是全量修改另一种是增量修改。 全量修改 全量修改是覆盖原来的文档其本质是
根据指定的id删除文档新增一个相同id的文档
注意如果根据id删除时id不存在第二步的新增也会执行也就从修改变成了新增操作了。
语法格式
PUT /{索引库名}/_doc/文档id
{字段1: 值1,字段2: 值2,// ... 略
} 增量修改 增量修改是只修改指定id匹配的文档中的部分字段。
语法格式
POST /{索引库名}/_update/文档id
{doc: {字段名: 新的值,}
}RestAPI
ES官方提供了各种不同语言的客户端用来操作ES。这些客户端的本质就是组装DSL语句通过http请求发送给ES。
官方文档地址https://www.elastic.co/guide/en/elasticsearch/client/index.html
其中的Java Rest Client又包括两种
Java Low Level Rest ClientJava High Level Rest Client mapping映射分析
创建索引库最关键的是mapping映射而mapping映射要考虑的信息包括
字段名字段数据类型是否参与搜索是否需要分词如果分词分词器是什么
其中
字段名、字段数据类型可以参考数据表结构的名称和类型是否参与搜索需要分析业务来判断例如图片地址就无需参与搜索是否分词需要看内容内容如果是一个整体就无需分词反之则要分词分词器可以统一使用ik_max_word
以下面数据结构表为例
CREATE TABLE tb_hotel (id bigint(20) NOT NULL COMMENT 酒店id,name varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT 酒店名称,address varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT 酒店地址,price int(10) NOT NULL COMMENT 酒店价格,score int(2) NOT NULL COMMENT 酒店评分,brand varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT 酒店品牌,city varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT 所在城市,star_name varchar(16) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT 酒店星级1星到5星1钻到5钻,business varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT 商圈,latitude varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT 纬度,longitude varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT 经度,pic varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT 酒店图片,PRIMARY KEY (id) USING BTREE
) ENGINE InnoDB CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci ROW_FORMAT Compact;创建的对应索引库结构为
PUT /hotel
{mappings: {properties: {id: {type: keyword},name:{type: text,analyzer: ik_max_word,copy_to: all},address:{type: keyword,index: false},price:{type: integer},score:{type: integer},brand:{type: keyword,copy_to: all},city:{type: keyword,copy_to: all},starName:{type: keyword},business:{type: keyword},location:{type: geo_point},pic:{type: keyword,index: false},all:{type: text,analyzer: ik_max_word}}}
}其中上面的location字段为地理坐标包含经度、纬度。
地理坐标说明 all字段为组合字段其目的是将多字段的值利用copy_to合并提供给用户搜索。
copy_to说明 初始化RestClient
在elasticsearch提供的API中与elasticsearch一切交互都封装在一个名为RestHighLevelClient的类中必须先完成这个对象的初始化建立与elasticsearch的连接。 引入es的RestHighLevelClient依赖 dependencygroupIdorg.elasticsearch.client/groupIdartifactIdelasticsearch-rest-high-level-client/artifactId
/dependency因为springboot默认控制的ES版本为7.6.2不满足使用需求所以需要覆盖默认的ES版本
propertiesjava.version1.8/java.versionelasticsearch.version7.12.1/elasticsearch.version
/properties初始化RestHighLevelClient 初始化代码
public class HotelIndexTest {private RestHighLevelClient client;BeforeEachvoid setUp() {this.client new RestHighLevelClient(RestClient.builder(HttpHost.create(http://192.168.xxx.xxx:9200)));}AfterEachvoid tearDown() throws IOException {this.client.close();}
}创建索引表
创建索引库的API如下 代码分为三步 创建Request对象。因为是创建索引库的操作因此Request是CreateIndexRequest。 添加请求参数其实就是DSL的JSON参数部分。因为json字符串很长可以定义为静态字符串常量让代码看起来更加优雅。 发送请求client.indices()方法的返回值是IndicesClient类型封装了所有与索引库操作有关的方法。
根据上面的索引库结构实现创建索引 创建一个常量类定义mapping映射的JSON字符串常量 public class HotelConstants {public 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 \copy_to\: \all\\n },\n \address\:{\n \type\: \keyword\,\n \index\: false\n },\n \price\:{\n \type\: \integer\\n },\n \score\:{\n \type\: \integer\\n },\n \brand\:{\n \type\: \keyword\,\n \copy_to\: \all\\n },\n \city\:{\n \type\: \keyword\,\n \copy_to\: \all\\n },\n \starName\:{\n \type\: \keyword\\n },\n \business\:{\n \type\: \keyword\\n },\n \location\:{\n \type\: \geo_point\\n },\n \pic\:{\n \type\: \keyword\,\n \index\: false\n },\n \all\:{\n \type\: \text\,\n \analyzer\: \ik_max_word\\n }\n }\n }\n };
}编写单元测试实现创建索引 Test
void createHotelIndex() throws IOException {// 1.创建Request对象CreateIndexRequest request new CreateIndexRequest(hotel);// 2.准备请求的参数DSL语句request.source(MAPPING_TEMPLATE, XContentType.JSON);// 3.发送请求client.indices().create(request, RequestOptions.DEFAULT);
}删除索引库
删除索引库的DSL语句非常简单
DELETE /hotel与创建索引库相比有以下几点变化
请求方式从PUT变为DELTE请求路径不变无请求参数
所以代码的差异注意体现在Request对象上。
创建Request对象。这次是DeleteIndexRequest对象准备参数。这里是无参发送请求。改用delete方法 编写单元测试实现删除索引 Test
void testDeleteHotelIndex() throws IOException {// 1.创建Request对象DeleteIndexRequest request new DeleteIndexRequest(hotel);// 2.发送请求client.indices().delete(request, RequestOptions.DEFAULT);
}判断索引库是否存在
判断索引库是否存在本质就是查询对应的DSL是
GET /hotel因此与删除的Java代码流程是类似的。依然是三步走
创建Request对象。这次是GetIndexRequest对象准备参数。这里是无参发送请求。改用exists方法
Test
void testExistsHotelIndex() throws IOException {// 1.创建Request对象GetIndexRequest request new GetIndexRequest(hotel);// 2.发送请求boolean exists client.indices().exists(request, RequestOptions.DEFAULT);// 3.输出System.err.println(exists ? 索引库已经存在 : 索引库不存在);
}总结
JavaRestClient操作elasticsearch的流程基本类似。核心是client.indices()方法来获取索引库的操作对象。
索引库操作的基本步骤
初始化RestHighLevelClient创建XxxIndexRequest。XXX是Create、Get、Delete准备DSL Create时需要其它是无参发送请求。调用RestHighLevelClient#indices().xxx()方法xxx是create、exists、delete
RestClient操作文档
为了与索引库操作分离再次添加一个测试类
SpringBootTest
public class HotelDocumentTest {Autowiredprivate IHotelService hotelService; // 利用IHotelService去查询酒店数据// 初始化RestHighLevelClientprivate RestHighLevelClient client;BeforeEachvoid setUp() {this.client new RestHighLevelClient(RestClient.builder(HttpHost.create(http://192.168.150.101:9200)));}AfterEachvoid tearDown() throws IOException {this.client.close();}
}定义一个酒店实体类
Data
NoArgsConstructor
public class HotelDoc {private Long id;private String name;private String address;private Integer price;private Integer score;private String brand;private String city;private String starName;private String business;private String location;private String pic;public HotelDoc(Hotel hotel) {this.id hotel.getId();this.name hotel.getName();this.address hotel.getAddress();this.price hotel.getPrice();this.score hotel.getScore();this.brand hotel.getBrand();this.city hotel.getCity();this.starName hotel.getStarName();this.business hotel.getBusiness();this.location hotel.getLatitude() , hotel.getLongitude();this.pic hotel.getPic();}
}前面已经知道了新增文档的DSL的语法格式
POST /索引库名/_doc/文档id// 举例说明
POST /hotel/_doc/1
{name: Jack,age: 21
}其对应的Java代码为
Test
void testIndexDocument() throws IOException {// 1.创建request对象IndexRequest request new IndexRequest(hotel).id(1); // 2.准备JSON文档request.source({\name\: \Jack\, \age\: 21}, XContentType.JSON);// 3.发送请求client.index(request, RequestOptions.DEFAULT);
}从代码中可以看出与创建索引库类似也分为三步
创建Request对象。准备请求参数(DSL中的JSON文档)。发送请求。
主要的变化在于此处直接使用了client的API不再使用client.indices()。
这里结合了数据库查询的数据将查询出来的数据转换成JSON的形式新增为文档。
Test
void testAddDocument() throws IOException {// 1.根据id查询酒店数据Hotel hotel hotelService.getById(1);// 2.转换为文档类型HotelDoc hotelDoc new HotelDoc(hotel);// 3.将HotelDoc转jsonString json JSON.toJSONString(hotelDoc);// 1.准备Request对象IndexRequest request new IndexRequest(hotel).id(hotelDoc.getId().toString());// 2.准备Json文档request.source(json, XContentType.JSON);// 3.发送请求client.index(request, RequestOptions.DEFAULT);
}查询文档
查询的DSL语句如下
GET /hotel/_doc/{id}非常简单因此代码大概分两步
准备Request对象发送请求
不过查询的目的是得到结果解析为HotelDoc因此难点是结果的解析。完整代码如下 可以看到结果是一个JSON其中文档放在一个_source属性中因此解析就是拿到_source反序列化为Java对象即可。
在测试类中编写单元测试
Test
void testGetDocumentById() throws IOException {// 1.准备RequestGetRequest request new GetRequest(hotel, 61082);// 2.发送请求得到响应GetResponse response client.get(request, RequestOptions.DEFAULT);// 3.解析响应结果String json response.getSourceAsString();HotelDoc hotelDoc JSON.parseObject(json, HotelDoc.class);System.out.println(hotelDoc);
}删除文档
删除的DSL的
DELETE /hotel/_doc/{id}与查询相比仅仅是请求方式从DELETE变成GET。
单元测试方法
Test
void testDeleteDocument() throws IOException {// 1.准备RequestDeleteRequest request new DeleteRequest(hotel, 61083);// 2.发送请求client.delete(request, RequestOptions.DEFAULT);
}修改文档
修改有两种方式
全量修改本质是先根据id删除再新增增量修改修改文档中的指定字段值
在RestClient的API中全量修改与新增的API完全一致判断依据是ID
如果新增时ID已经存在则修改如果新增时ID不存在则新增
这里主要关注增量修改。
代码示例 准备Request对象。这次是修改所以是UpdateRequest准备参数。也就是JSON文档里面包含要修改的字段更新文档。这里调用client.update()方法
单元测试方法
Test
void testUpdateDocument() throws IOException {// 1.准备RequestUpdateRequest request new UpdateRequest(hotel, 61083);// 2.准备请求参数request.doc(price, 952,starName, 四钻);// 3.发送请求client.update(request, RequestOptions.DEFAULT);
}批量导入文档
批量处理BulkRequest其本质就是将多个普通的CRUD请求组合在一起发送。
其中提供了一个add方法用来添加其他请求 可以看到能添加的请求包括
IndexRequest也就是新增UpdateRequest也就是修改DeleteRequest也就是删除
因此Bulk中添加了多个IndexRequest就是批量新增功能了。
Test
void testBulk() throws IOException {// 1.创建Bulk请求BulkRequest request new BulkRequest(); // 2.添加要批量提交的请求这里添加了两个新增文档的请求request.add(new IndexRequest(hotel).id(101).source(json source, XContentType.JSON));request.add(new IndexRequest(hotel).id(102).source(json source2, XContentType.JSON));// 3.发起bulk请求client.bulk(request, RequestOptions.DEFAULT);
}这里与上面的不同之处在于Request对象为BulkRequest。调用的请求方法为client.bulk()。
单元测试方法
Test
void testBulkRequest() throws IOException {// 批量查询酒店数据ListHotel hotels hotelService.list();// 1.创建RequestBulkRequest request new BulkRequest();// 2.准备参数添加多个新增的Requestfor (Hotel hotel : hotels) {// 2.1.转换为文档类型HotelDocHotelDoc hotelDoc new HotelDoc(hotel);// 2.2.创建新增文档的Request对象request.add(new IndexRequest(hotel).id(hotelDoc.getId().toString()).source(JSON.toJSONString(hotelDoc), XContentType.JSON));}// 3.发送请求client.bulk(request, RequestOptions.DEFAULT);
}DSL查询文档
Elasticsearch的查询依然是基于JSON风格的DSL来实现的。
DSL查询分类
Elasticsearch提供了基于JSON的DSLDomain Specific Language来定义查询。常见的查询类型包括 查询所有查询出所有数据一般测试用。例如match_all 全文检索full text查询利用分词器对用户输入内容分词然后去倒排索引库中匹配。例如 match_querymulti_match_query 精确查询根据精确词条值查找数据一般是查找keyword、数值、日期、boolean等类型字段。例如 idsrangeterm 地理geo查询根据经纬度查询。例如 geo_distancegeo_bounding_box 复合compound查询复合查询可以将上述各种查询条件组合起来合并查询条件。例如 boolfunction_score
语法格式
GET /indexName/_search
{query: {查询类型: {查询条件: 条件值}}
}我们以查询所有为例其中
查询类型为match_all没有查询条件
// 查询所有
GET /indexName/_search
{query: {match_all: {}}
}其它查询无非就是查询类型、查询条件的变化。
全文检索查询
全文检索查询的基本流程如下
对用户搜索的内容做分词得到词条。根据词条去倒排索引库中匹配得到文档id。根据文档id找到文档返回给用户。
因为是拿着词条去匹配因此参与搜索的字段也必须是可分词的text类型的字段。
基本语法
常见的全文检索查询包括
match查询单字段查询(根据一个字段查询)multi_match查询多字段查询任意一个字段符合条件就算符合查询条件(根据多个字段查询参与查询字段越多查询性能越差)。
match查询语法格式
GET /indexName/_search
{query: {match: {FIELD: TEXT // FIELD字段名}}
}mulit_match语法格式
GET /indexName/_search
{query: {multi_match: {query: TEXT,fields: [FIELD1, FIELD12]}}
}精准查询
精确查询一般是查找keyword、数值、日期、boolean等类型字段。所以不会对搜索条件分词。常见的有
term根据词条精确查询一般搜索keyword类型、数值类型、布尔类型、日期类型字段range根据值的范围查询可以是数值、日期的范围
term查询
因为精确查询的字段搜是不分词的字段因此查询的条件也必须是不分词的词条。查询时用户输入的内容跟自动值完全匹配时才认为符合条件。如果用户输入的内容过多反而搜索不到数据。
语法格式
// term查询
GET /indexName/_search
{query: {term: {FIELD: { // FIELD字段名value: VALUE}}}
}range查询
范围查询一般应用在对数值类型做范围过滤的时候。比如做价格范围过滤。
语法格式
// range查询
GET /indexName/_search
{query: {range: {FIELD: { // FIELD字段名gte: 100, // 这里的gte代表大于等于gt则代表大于lte: 200 // lte代表小于等于lt则代表小于}}}
}地理坐标查询
所谓的地理坐标查询其实就是根据经纬度查询。
官方文档https://www.elastic.co/guide/en/elasticsearch/reference/current/geo-queries.html
常见的使用场景包括
携程搜索附近的酒店。滴滴搜索附近的出租车。微信搜索附近的人。
矩形范围查询
矩形范围查询也就是geo_bounding_box查询查询坐标落在某个矩形范围的所有文档 查询时需要指定矩形的左上、右下两个点的坐标然后画出一个矩形落在该矩形内的都是符合条件的点。
语法格式
// geo_bounding_box查询
GET /indexName/_search
{query: {geo_bounding_box: {FIELD: {top_left: { // 左上点lat: 31.1,lon: 121.5},bottom_right: { // 右下点lat: 30.9,lon: 121.7}}}}
}附近查询
附近查询也叫做距离查询geo_distance查询到指定中心点小于某个距离值的所有文档。
在地图上找一个点作为圆心以指定距离为半径画一个圆落在圆内的坐标都算符合条件 语法说明
// geo_distance 查询
GET /indexName/_search
{query: {geo_distance: {distance: 15km, // 半径FIELD: 31.21,121.5 // 圆心}}
}复合查询
复合compound查询复合查询可以将其它简单查询组合起来实现更复杂的搜索逻辑。常见的有两种
fuction score算分函数查询可以控制文档相关性算分控制文档排名。bool query布尔查询利用逻辑关系组合多个其它的查询实现复杂搜索。
相关性算分
在Elasticsearch中早期使用的打分算法是TF-IDF算法公式如下 在后来的5.1版本升级中elasticsearch将算法改进为BM25算法公式如下 TF-IDF算法有一各缺陷就是词条频率越高文档得分也会越高单个词条对文档影响较大。而BM25则会让单个词条的算分有一个上限曲线更加平滑 算分函数查询
根据相关度打分是比较合理的需求但合理的不一定是产品经理需要的。
要想认为控制相关性算分就需要利用elasticsearch中的function score 查询了。 语法说明 function score 查询中包含四部分内容
原始查询条件query部分基于这个条件搜索文档并且基于BM25算法给文档打分原始算分query score)过滤条件filter部分符合该条件的文档才会重新算分算分函数符合filter条件的文档要根据这个函数做运算得到的函数算分function score有四种函数 weight函数结果是常量field_value_factor以文档中的某个字段值作为函数结果random_score以随机数作为函数结果script_score自定义算分函数算法 运算模式算分函数的结果、原始查询的相关性算分两者之间的运算方式包括 multiply相乘replace用function score替换query score其它例如sum、avg、max、min
function score的运行流程如下
1根据原始条件查询搜索文档并且计算相关性算分称为原始算分query score2根据过滤条件过滤文档3符合过滤条件的文档基于算分函数运算得到函数算分function score4将原始算分query score和函数算分function score基于运算模式做运算得到最终结果作为相关性算分。
因此其中的关键点是
过滤条件决定哪些文档的算分被修改算分函数决定函数算分的算法运算模式决定最终算分结果
布尔查询
布尔查询是一个或多个查询子句的组合每一个子句就是一个子查询。子查询的组合方式有
must必须匹配每个子查询类似“与”should选择性匹配子查询类似“或”must_not必须不匹配不参与算分类似“非”filter必须匹配不参与算分
每一个不同的字段其查询的条件、方式都不一样必须是多个不同的查询而要组合这些查询就必须用bool查询了。
需要注意的是搜索时参与打分的字段越多查询的性能也越差。因此这种多条件查询时建议这样做
搜索框的关键字搜索是全文检索查询使用must查询参与算分其它过滤条件采用filter查询。不参与算分
搜索结果处理
搜索的结果可以按照用户指定的方式去处理或展示。
排序
Elasticsearch默认是根据相关度算分_score来排序但是也支持自定义方式对搜索结果排序。可以排序字段类型有keyword类型、数值类型、地理坐标类型、日期类型等。 普通字段排序 keyword、数值、日期类型排序的语法基本一致。
语法
GET /indexName/_search
{query: {match_all: {}},sort: [{FIELD: desc // 排序字段、排序方式ASC、DESC}]
}排序条件是一个数组也就是可以写多个排序条件。按照声明的顺序当第一个条件相等时再按照第二个条件排序以此类推。 地理坐标排序 这个查询的含义是
指定一个坐标作为目标点(中心点)计算每一个文档中指定字段必须是geo_point类型的坐标到目标点的距离是多少根据距离排序
语法说明
GET /indexName/_search
{query: {match_all: {}},sort: [{_geo_distance : {FIELD : 纬度经度, // 文档中geo_point类型的字段名、目标坐标点order : asc, // 排序方式unit : km // 排序的距离单位}}]
}获取你的位置的经纬度的方式https://lbs.amap.com/demo/jsapi-v2/example/map/click-to-get-lnglat/
分页
Elasticsearch 默认情况下只返回top10的数据。而如果要查询更多数据就需要修改分页参数了。elasticsearch中通过修改from、size参数来控制要返回的分页结果
from从第几个文档开始size总共查询几个文档 基础分页 语法格式
GET /hotel/_search
{query: {match_all: {}},from: 0, // 分页开始的位置默认为0size: 10, // 期望获取的文档总数sort: [{price: asc}]
}深度分页问题 要查询9900~10000的数据查询逻辑应该这么写
GET /hotel/_search
{query: {match_all: {}},from: 9900, // 分页开始的位置默认为0size: 10, // 期望获取的文档总数sort: [{price: asc}]
}这里是查询9900开始的数据也就是 第9900~第10000条 数据。
不过Elasticsearch内部分页时必须先查询 0~10000条然后截取其中的9900 ~ 10000的这10条 查询TOP10000如果ES是单点模式这并无太大影响。
但是Elasticsearch将来一定是集群例如我集群有10个节点我要查询TOP10000的数据并不是每个节点查询1000条就可以了。
因为节点A的TO1P000在另一个节点可能排到100000名以外了。
因此要想获取整个集群的TOP10000必须先查询出每个节点的TOP10000汇总结果后重新排名重新截取TOP10000。 当查询分页深度较大时汇总数据过多对内存和CPU会产生非常大的压力因此elasticsearch会禁止from size 超过10000的请求。
针对深度分页ES提供了两种解决方案
官方文档https://www.elastic.co/guide/en/elasticsearch/reference/current/paginate-search-results.html
search after分页时需要排序原理是从上一次的排序值开始查询下一页数据。官方推荐使用的方式。scroll原理将排序后的文档id形成快照保存在内存(官方已经不推荐使用)。 三种分页查询的实现方案及其优缺点 from size
优点支持随机翻页缺点深度分页问题默认查询上限from size是10000场景百度、京东、谷歌、淘宝这样的随机翻页搜索
after search
优点没有查询上限单次查询的size不超过10000缺点只能向后逐页查询不支持随机翻页场景没有随机翻页需求的搜索例如手机向下滚动翻页
scroll
优点没有查询上限单次查询的size不超过10000缺点会有额外内存消耗并且搜索结果是非实时的场景海量数据的获取和迁移。从ES7.1开始不推荐建议用 after search方案。
高亮
原理
日常生活中在百度搜索时关键字会变成红色比较醒目这便是高亮显示。 高亮显示的实现分为两步
给文档中的所有关键字都添加一个标签例如em标签页面给em标签编写CSS样式
实现
高亮的语法
GET /hotel/_search
{query: {match: {FIELD: TEXT // 查询条件高亮一定要使用全文检索查询}},highlight: {fields: { // 指定要高亮的字段FIELD: {pre_tags: em, // 用来标记高亮字段的前置标签post_tags: /em // 用来标记高亮字段的后置标签}}}
}注意
高亮是对关键字高亮因此搜索条件必须带有关键字而不能是范围这样的查询。默认情况下高亮的字段必须与搜索指定的字段一致否则无法高亮如果要对非搜索字段高亮则需要添加一个属性required_field_matchfalse
RestClient查询文档
文档的查询同样适用昨天学习的 RestHighLevelClient对象基本步骤包括
1准备Request对象2准备请求参数3发起请求4解析响应
查询请求
代码演示
Test
void testMatchAll() throws IOException {// 1.准备RequestSearchRequest request new SearchRequest(hotel);// 2.组织DSL参数 request.source().query(QueryBuilders.matchAllQuery());// 3.发送请求得到响应结果SearchResponse response client.search(request, RequestOptions.DEFAULT);// ...解析响应结果
}// 对比DSL的查询请求
//GET /indexName/_search
//{
// query: {
// match_all: {}
// }
//}第一步创建SearchRequest对象指定索引库名。 第二步利用request.source()构建DSLDSL中可以包含查询、分页、排序、高亮等。 query()代表查询条件利用QueryBuilders.matchAllQuery()构建一个match_all查询的DSL。 第三步利用client.search()发送请求得到响应。
这里关键的API有两个一个是request.source()其中包含了查询、排序、分页、高亮等所有功能 另一个是QueryBuilders其中包含match、term、function_score、bool等各种查询 解析响应
响应结果的解析 elasticsearch返回的结果是一个JSON字符串结构包含
hits命中的结果 total总条数其中的value是具体的总条数值max_score所有结果中得分最高的文档的相关性算分hits搜索结果的文档数组其中的每个文档都是一个json对象 _source文档中的原始数据也是json对象
因此解析响应结果就是逐层解析JSON字符串流程如下
SearchHits通过response.getHits()获取就是JSON中的最外层的hits代表命中的结果。 SearchHits.getTotalHits().value获取总条数信息。SearchHits.getHits()获取SearchHit数组也就是文档数组。 SearchHit.getSourceAsString()获取文档结果中的_source也就是原始的json文档数据。 完整的查询代码 Test
void testMatchAll() throws IOException {// 1.准备RequestSearchRequest request new SearchRequest(hotel);// 2.准备DSLrequest.source().query(QueryBuilders.matchAllQuery());// 3.发送请求SearchResponse response client.search(request, RequestOptions.DEFAULT);// 4.解析响应handleResponse(response);
}private void handleResponse(SearchResponse response) {// 4.解析响应SearchHits searchHits response.getHits();// 4.1.获取总条数long total searchHits.getTotalHits().value;System.out.println(共搜索到 total 条数据);// 4.2.文档数组SearchHit[] hits searchHits.getHits();// 4.3.遍历for (SearchHit hit : hits) {// 获取文档sourceString json hit.getSourceAsString();// 反序列化HotelDoc hotelDoc JSON.parseObject(json, HotelDoc.class);System.out.println(hotelDoc hotelDoc);}
}查询的基本步骤是 创建SearchRequest对象 准备Request.source()也就是DSL。 ① QueryBuilders来构建查询条件 ② 传入Request.source() 的 query() 方法 发送请求得到结果 解析结果参考JSON结果从外到内逐层解析
match查询
全文检索的match和multi_match查询与match_all的API基本一致。差别是查询条件也就是query的部分。 因此Java代码上的差异主要是request.source().query()中的参数了。同样是利用QueryBuilders提供的方法 而结果解析代码则完全一致可以抽取并共享。
完整代码
Test
void testMatch() throws IOException {// 1.准备RequestSearchRequest request new SearchRequest(hotel);// 2.准备DSLrequest.source().query(QueryBuilders.matchQuery(all, 如家));// 3.发送请求SearchResponse response client.search(request, RequestOptions.DEFAULT);// 4.解析响应handleResponse(response);}精确查询
精确查询主要是两者
term词条精确匹配range范围查询
与之前的查询相比差异同样在查询条件其它都一样。
查询条件构造的API如下 布尔查询
布尔查询是用must、must_not、filter等方式组合其它查询代码示例如下 可以看到API与其它查询的差别同样是在查询条件的构建QueryBuilders结果解析等其他代码完全不变。
完整代码
Test
void testBool() throws IOException {// 1.准备RequestSearchRequest request new SearchRequest(hotel);// 2.准备DSL// 2.1.准备BooleanQueryBoolQueryBuilder boolQuery QueryBuilders.boolQuery();// 2.2.添加termboolQuery.must(QueryBuilders.termQuery(city, 杭州));// 2.3.添加rangeboolQuery.filter(QueryBuilders.rangeQuery(price).lte(250));request.source().query(boolQuery);// 3.发送请求SearchResponse response client.search(request, RequestOptions.DEFAULT);// 4.解析响应handleResponse(response);}排序与分页
搜索结果的排序和分页是与query同级的参数因此同样是使用request.source()来设置。
对应的API如下 完整代码
Test
void testPageAndSort() throws IOException {// 页码每页大小int page 1, size 5;// 1.准备RequestSearchRequest request new SearchRequest(hotel);// 2.准备DSL// 2.1.queryrequest.source().query(QueryBuilders.matchAllQuery());// 2.2.排序 sortrequest.source().sort(price, SortOrder.ASC);// 2.3.分页 from、sizerequest.source().from((page - 1) * size).size(5);// 3.发送请求SearchResponse response client.search(request, RequestOptions.DEFAULT);// 4.解析响应handleResponse(response);}3.6.高亮
高亮的代码与之前代码有两点差异较大
查询的DSL其中除了查询条件还需要添加高亮条件同样是与query同级。结果解析结果除了要解析_source文档数据还要解析高亮结果
3.6.1.高亮请求构建
高亮请求的构建API如下 高亮查询必须使用全文检索查询并且要有搜索关键字将来才可以对关键字高亮。
完整代码
Test
void testHighlight() throws IOException {// 1.准备RequestSearchRequest request new SearchRequest(hotel);// 2.准备DSL// 2.1.queryrequest.source().query(QueryBuilders.matchQuery(all, 如家));// 2.2.高亮request.source().highlighter(new HighlightBuilder().field(name).requireFieldMatch(false));// 3.发送请求SearchResponse response client.search(request, RequestOptions.DEFAULT);// 4.解析响应handleResponse(response);}3.6.2.高亮结果解析
高亮的结果与查询的文档结果默认是分离的并不在一起。
因此解析高亮的代码需要额外处理
第一步从结果中获取source。hit.getSourceAsString()这部分是非高亮结果json字符串。还需要反序列为HotelDoc对象第二步获取高亮结果。hit.getHighlightFields()返回值是一个Mapkey是高亮字段名称值是HighlightField对象代表高亮值第三步从map中根据高亮字段名称获取高亮字段值对象HighlightField第四步从HighlightField中获取Fragments并且转为字符串。这部分就是真正的高亮字符串了第五步用高亮的结果替换HotelDoc中的非高亮结果 完整代码
private void handleResponse(SearchResponse response) {// 4.解析响应SearchHits searchHits response.getHits();// 4.1.获取总条数long total searchHits.getTotalHits().value;System.out.println(共搜索到 total 条数据);// 4.2.文档数组SearchHit[] hits searchHits.getHits();// 4.3.遍历for (SearchHit hit : hits) {// 获取文档sourceString json hit.getSourceAsString();// 反序列化HotelDoc hotelDoc JSON.parseObject(json, HotelDoc.class);// 获取高亮结果MapString, HighlightField highlightFields hit.getHighlightFields();if (!CollectionUtils.isEmpty(highlightFields)) {// 根据字段名获取高亮结果HighlightField highlightField highlightFields.get(name);if (highlightField ! null) {// 获取高亮值String name highlightField.getFragments()[0].string();// 覆盖非高亮结果hotelDoc.setName(name);}}System.out.println(hotelDoc hotelDoc);}
}数据聚合
聚合可以极其方便的实现对数据的统计、分析、运算。
官方文档https://www.elastic.co/guide/en/elasticsearch/reference/current/search-aggregations.html
实现这些统计功能的比数据库的sql要方便的多而且查询速度非常快可以实现近实时搜索效果。
聚合的种类
**注意**参加聚合的字段必须是keyword、日期、数值、布尔类型
聚合常见的有三类 **桶Bucket**聚合用来对文档做分组 TermAggregation按照文档字段值分组例如按照品牌值分组、按照国家分组Date Histogram按照日期阶梯分组例如一周为一组或者一月为一组 **度量Metric**聚合用以计算一些值比如最大值、最小值、平均值等 Avg求平均值Max求最大值Min求最小值Stats同时求max、min、avg、sum等 **管道pipeline**聚合其它聚合的结果为基础做聚合
DSL实现聚合
日常中要统计所有数据中的分类有几种其实就是要按照分类对数据分组也就是Bucket聚合。
Bucket聚合语法
语法格式
GET /hotel/_search
{size: 0, // 设置size为0结果中不包含文档只包含聚合结果aggs: { // 定义聚合brandAgg: { //给聚合起个名字terms: { // 聚合的类型按照品牌值聚合所以选择termfield: brand, // 参与聚合的字段size: 20 // 希望获取的聚合结果数量}}}
}聚合结果排序
默认情况下Bucket聚合会 Bucket 内的文档数量记为_count并且按照_count降序排序。
可以指定order属性自定义聚合的排序方式。
GET /hotel/_search
{size: 0, aggs: {brandAgg: {terms: {field: brand,order: {_count: asc // 按照_count升序排列},size: 20}}}
}限定聚合范围
默认情况下Bucket聚合是对索引库的所有文档做聚合但真实场景下用户会输入搜索条件因此聚合必须是对搜索结果聚合。那么聚合必须添加限定条件。
可以限定要聚合的文档范围只要添加query条件即可
GET /hotel/_search
{query: {range: {price: {lte: 200 // 只对200元以下的文档聚合}}}, size: 0, aggs: {brandAgg: {terms: {field: brand,size: 20}}}
}Metric聚合语法
可以对桶内的数据进行运算获得每个品牌的用户评分的min、max、avg等值。
这里就要用到Metric聚合例如stat聚合就可以获取min、max、avg等结果。
语法格式如下
GET /hotel/_search
{size: 0, aggs: {brandAgg: { terms: {field: brand,size: 20},aggs: { // 是brands聚合的子聚合也就是分组后对每组分别计算score_stats: { // 聚合名称stats: { // 聚合类型这里stats可以计算min、max、avg等field: score // 聚合字段这里是score}}}}}
}这次的score_stats聚合是在brandAgg的聚合内部嵌套的子聚合。因此需要在每个桶分别计算。
RestAPI实现聚合
API语法
聚合条件与query条件同级别因此需要使用request.source()来指定聚合条件。
聚合条件的语法格式 聚合的结果也与查询结果不同API也比较特殊。不过同样是JSON逐层解析 自动补全
在日常的搜索中当用户在搜索框中输入字符时就会提示出相关的搜索项。 这种根据用户输入的字母提示完整词条的功能即为自动补全。
因为可能需要根据拼音字母来推断因此要用到拼音分词功能。
拼音分词器
要实现根据字母做补全就必须对文档按照拼音分词。在GitHub上恰好有elasticsearch的拼音分词插件。
地址https://github.com/medcl/elasticsearch-analysis-pinyin 安装方式
下载解压(注意要与elasticsearch的版本保持一致7.12.1)。上传至虚拟机中的elasticsearch的plugin目录。重启elasticsearch
详细安装步骤可以查看IK分词器的安装过程。 测试用法 POST /_analyze
{text: [今天天气真好],analyzer: pinyin
}测试结果
{tokens : [{token : jin,start_offset : 0,end_offset : 0,type : word,position : 0},{token : jttqzh,start_offset : 0,end_offset : 0,type : word,position : 0},{token : tian,start_offset : 0,end_offset : 0,type : word,position : 1},{token : tian,start_offset : 0,end_offset : 0,type : word,position : 2},{token : qi,start_offset : 0,end_offset : 0,type : word,position : 3},{token : zhen,start_offset : 0,end_offset : 0,type : word,position : 4},{token : hao,start_offset : 0,end_offset : 0,type : word,position : 5}]
}自定义分词器
默认的拼音分词器会将每个汉字单独分为拼音而我们希望的是每个词条形成一组拼音需要对拼音分词器做个性化定制形成自定义分词器。
elasticsearch中分词器analyzer的组成包含三部分
character filters在tokenizer之前对文本进行处理。例如删除字符、替换字符tokenizer将文本按照一定的规则切割成词条term。例如keyword就是不分词还有ik_smarttokenizer filter将tokenizer输出的词条做进一步处理。例如大小写转换、同义词处理、拼音处理等
文档分词时会依次由这三部分来处理文档 声明自定义分词器的语法格式
PUT /test
{settings: {analysis: {analyzer: { // 自定义分词器my_analyzer: { // 分词器名称tokenizer: ik_max_word,filter: py}},filter: { // 自定义tokenizer filterpy: { // 过滤器名称type: pinyin, // 过滤器类型这里是pinyinkeep_full_pinyin: false,keep_joined_full_pinyin: true,keep_original: true,limit_first_letter_length: 16,remove_duplicated_term: true,none_chinese_pinyin_tokenize: false}}}},mappings: {properties: {name: {type: text,analyzer: my_analyzer,search_analyzer: ik_smart}}}
}自动补全查询
elasticsearch提供了Completion Suggester查询来实现自动补全功能。这个查询会匹配以用户输入内容开头的词条并返回。为了提高补全查询的效率对于文档中字段的类型有一些约束 参与补全查询的字段必须是completion类型。 字段的内容一般是用来补全的多个词条形成的数组。
官方文档https://www.elastic.co/guide/en/elasticsearch/reference/7.6/search-suggesters.html 举例 一个这样的索引库
// 创建索引库
PUT test
{mappings: {properties: {title:{type: completion}}}
}然后插入下面的数据
// 示例数据
POST test/_doc
{title: [Sony, WH-1000XM3]
}
POST test/_doc
{title: [SK-II, PITERA]
}
POST test/_doc
{title: [Nintendo, switch]
}查询的DSL语句如下
// 自动补全查询
GET /test/_search
{suggest: {title_suggest: {text: s, // 关键字completion: {field: title, // 补全查询的字段skip_duplicates: true, // 跳过重复的size: 10 // 获取前10条结果}}}
}自动补全查询的JavaAPI
对照自动补全查询的DSL对应的推出JavaAPI的使用方法 而自动补全的结果也比较特殊解析的代码格式 数据同步
Elasticsearch中的酒店数据来自于mysql数据库因此mysql数据发生改变时elasticsearch也必须跟着改变这个就是elasticsearch与mysql之间的数据同步。 常见的数据同步方案有三种
同步调用异步通知监听binlog
同步调用 基本步骤
demo对外提供接口用来修改elasticsearch中的数据。后台管理服务在完成数据库操作后直接调用demo提供的接口
异步通知 基本流程
admin对mysql数据库数据完成增、删、改后发送MQ消息。demo监听MQ接收到消息后完成elasticsearch数据修改。
监听binlog 基本流程
给mysql开启binlog功能。mysql完成增、删、改操作都会记录在binlog中。demo基于canal监听binlog变化实时更新elasticsearch中的内容。 三种方式的优缺点 方式一同步调用
优点实现简单粗暴缺点业务耦合度高
方式二异步通知
优点低耦合实现难度一般缺点依赖mq的可靠性
方式三监听binlog
优点完全解除服务间耦合缺点开启binlog增加数据库负担、实现复杂度高
ES集群
单机的elasticsearch做数据存储必然面临两个问题海量数据存储问题、单点故障问题。
海量数据存储问题将索引库从逻辑上拆分为N个分片shard存储到多个节点单点故障问题将分片数据在不同节点备份replica
ES集群相关概念: 集群cluster一组拥有共同的 cluster name 的 节点。 节点node) 集群中的一个 Elasticearch 实例 分片shard索引可以被拆分为不同的部分进行存储称为分片。在集群环境下一个索引的不同分片可以拆分到不同的节点中 解决问题数据量太大单点存储量有限的问题。 此处把数据分成3片shard0、shard1、shard2 主分片Primary shard相对于副本分片的定义。 副本分片Replica shard每个主分片可以有一个或者多个副本数据和主分片一样。
数据备份可以保证高可用但是每个分片备份一份所需要的节点数量就会翻一倍成本实在是太高
为了在高可用和成本间寻求平衡可以这样做
首先对数据分片存储到不同节点然后对每个分片进行备份放到对方节点完成互相备份
这样可以大大减少所需要的服务节点数量如图以3分片每个分片备份一份为例 现在每个分片都有1个备份存储在3个节点
node0保存了分片0和1node1保存了分片0和2node2保存了分片1和2
搭建集群
部署es集群可以直接使用docker-compose来完成不过要求Linux虚拟机至少有4G的内存空间
首先编写一个docker-compose文件内容如下
version: 2.2
services:es01:image: docker.elastic.co/elasticsearch/elasticsearch:7.12.1container_name: es01environment:- node.namees01- cluster.namees-docker-cluster- discovery.seed_hostses02,es03- cluster.initial_master_nodeses01,es02,es03- bootstrap.memory_locktrue- ES_JAVA_OPTS-Xms512m -Xmx512mulimits:memlock:soft: -1hard: -1volumes:- data01:/usr/share/elasticsearch/dataports:- 9200:9200networks:- elastices02:image: docker.elastic.co/elasticsearch/elasticsearch:7.12.1container_name: es02environment:- node.namees02- cluster.namees-docker-cluster- discovery.seed_hostses01,es03- cluster.initial_master_nodeses01,es02,es03- bootstrap.memory_locktrue- ES_JAVA_OPTS-Xms512m -Xmx512mulimits:memlock:soft: -1hard: -1volumes:- data02:/usr/share/elasticsearch/datanetworks:- elastices03:image: docker.elastic.co/elasticsearch/elasticsearch:7.12.1container_name: es03environment:- node.namees03- cluster.namees-docker-cluster- discovery.seed_hostses01,es02- cluster.initial_master_nodeses01,es02,es03- bootstrap.memory_locktrue- ES_JAVA_OPTS-Xms512m -Xmx512mulimits:memlock:soft: -1hard: -1volumes:- data03:/usr/share/elasticsearch/datanetworks:- elasticvolumes:data01:driver: localdata02:driver: localdata03:driver: localnetworks:elastic:driver: bridgees运行前需要修改一些Linux系统权限修改/etc/sysctl.conf文件
vi /etc/sysctl.conf添加以下内容
vm.max_map_count262144执行命令使配置生效
sysctl -p通过docker-compose启动集群:
docker-compose up -d创建索引库
在DevTools中输入命令
PUT /item
{settings: {number_of_shards: 3, // 分片数量number_of_replicas: 1 // 副本数量},mappings: {properties: {// mapping映射定义。。。}}
}集群节点角色
elasticsearch中集群节点有着不同的职责划分
节点类型配置参数默认值节点职责master eligiblenode.mastertrue备选主节点主节点可以管理和记录集群状态决定分片在哪个节点、处理创建和删除库索引的请求datanode.datatrue数据节点存储数据、搜索、聚合、CRUDingestnode.ingesttrue数据存储之前的预处理coordinating上面三个参数都为false则coordinating节点无协调节点路由请求到其它节点合并到其它节点处理的结果返回给用户
真实的集群一定要将集群职责分离
master节点对CPU要求高但是内存要求第data节点对CPU和内存要求都高coordinating节点对网络带宽、CPU要求高
职责分离可以根据不同节点的需求分配不同的硬件去部署。而且避免业务之间的互相干扰。
Elasticsearch中的每个节点角色都有自己不同的职责因此建议集群部署时每个节点都有独立的角色。 集群的脑裂问题
默认情况下每个节点都是master eligible节点因此一旦master节点宕机其它候选节点会选举一个成为主节点。当主节点与其他节点网络故障时可能发生脑裂问题。
例如一个集群中主节点与其它节点失联 此时node2和node3认为node1宕机就会重新选主 当node3当选后集群继续对外提供服务node2和node3自成集群node1自成集群两个集群数据不同步出现数据差异。
当网络恢复后因为集群中有两个master节点集群状态的不一致出现脑裂的情况 为了避免脑裂需要要求选票超过 ( eligible节点数量 1 / 2才能当选为主因此eligible节点数量最好是奇数。对应配置项是discovery.zen.minimum_master_nodes在es7.0以后已经成为默认配置因此一般不会发生脑裂问题。
分布式存储
当新增文档时应该保存到不同分片保证数据均衡那么coordinating node如何确定数据该存储到哪个分片呢
Elasticsearch会通过hash算法来计算文档应该存储到哪个分片 说明
•_routing默认是文档的id
•算法与分片数量有关因此索引库一旦创建分片数量就不能修改。
新增文档的流程 新增一个id1的文档对id做hash运算假如得到的是2则应该存储到shard-2shard-2的主分片在node3节点将数据路由到node3保存文档同步给shard-2的副本replica-2在node2节点返回结果给coordinating-node节点
分布式查询
elasticsearch的查询分成两个阶段 scatter phase分散阶段coordinating node会把请求分发到每一个分片 gather phase聚集阶段coordinating node汇总data node的搜索结果并处理为最终结果集返回给用户 集群故障转移
集群的master节点会监控集群中的节点状态如果发现有节点宕机会立即将宕机节点的分片数据迁移到其它节点确保数据安全这个叫做故障转移。
1假设一个集群结构如图 现在node1是主节点其它两个节点是从节点。
2由于发生特殊情况node1发生了故障 宕机后的第一件事需要重新选主例如选中了node2 node2成为主节点后会检测集群监控状态发现shard-1、shard-0没有副本节点。因此需要将node1上的数据迁移到node2、node3