1.elasticsearch
1.什么是elasticsearch
是一款非常强大的开源搜索引擎,可以帮助我们从海量数据中快速找到需要的内容。
elasticsearch结合kibana,Logstash,Beats,也就是elastic stack(ELK)。被广泛应用在日志数据分析,实时监控。
elasticsearch是elastic stack的核心,负责存储,搜索,分析数据,底层是Lucene技术。Logstash,Beats负责数据抓取。Kibana负责数据可视化。
Lucene是一个Java语言的搜索引擎类库,是Apache公司的顶级项目。优点:1.易扩展,2.高性能。缺点:1.只限于Java语言开发,2.学习曲线陡峭,3.不支持水平扩展
相比于lucene,elasticsearch优势:1.支持分布式,可水平扩展 2.提供Restful接口,可被任何语言调用。
2.正向索引和倒排索引
传统数据库(如MySQL)采用正向索引(通过ID找内容)。
elasticsearch采用倒排索引(通过内容找ID):
文档(document):每条数据就是一个文档
词条(term):文档按照语义分成的词语,词条不重复,为词条做索引,对应的数据是文档ID。按词条进行搜索,然后通过词条得到文档ID,再通过文档ID去获取对应的文档数据。
3.文档
elasticsearch是面向文档存储的,可以是数据库中的一条商品数据,一个订单信息。文档数据会被序列化为json格式后存储在elasticsearch中(类似于mongodb)。
4.索引(Index)
相同类型的文档的集合。比如字段都相同的json数据,就像数据库中的表结构,一个索引映射一张表
MySQL | Elasticsearch | 说明 |
Table | Index | 索引,就是文档的集合,类似数据库的表 |
Row | Document | 文档,就是一条条的数据,类似数据库中的行,文档都是JSON格式 |
Column | Field | 字段,就是JSON文档中的字段,类似数据库中的列 |
Schema | Mapping | 映射是索引中文档的约束,例如字段类型约束。类似数据库的表结构(Schema)。 |
SQL | DSL | DSL是elasticsearch提供的JSON风格的请求语句,用来操作elasticsearch,实现CRUD. |
5.架构
MySQL:擅长事务类型操作,可以确保数据的安全和一致性。数据量少用这个。
Elasticsearch:擅长海量数据的搜索,分析,计算。
2.部署单点es
1.创建网络
docker network create es-net
2.安装elasticsearch
docker pull elasticsearch
docker run -d \
--name es \ # 容器名字
-e "ES_JAVA_OPTS=-Xms512m -Xmx512m" \ # 环境变量 JVM的堆内存大小
-e "discovery.type=single-node" \ # 运行模式-单独模式
-v es-data:/usr/share/elasticsearch/data \ # 数据卷挂载
-v es-plugins:/usr/share/elasticsearch/plugins \
--privileged \
--network es-net \ # 让es容器加入这个容器中
-p 9200:9200 \ # http协议端口,用户访问
-p 9300:9300 \ # es容器各个节点互联的端口
elasticsearch:7.12.1
# 其余变量
-e "cluster.name=es-docker-cluster":设置集群名称
-e "http.host=0.0.0.0":监听的地址,可以外网访问
-e "ES_JAVA_OPTS=-Xms512m -Xmx512m":内存大小
-e "discovery.type=single-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:端口映射配置
访问IP:9200
常见问题:
1.9200网址无法访问或报错。可能原因:服务器没有安装java jdk
查看是否安装jdk
java -version
如果未安装,装一个
yum search java | grep -i --color JDK # 查看JDK软件包列表
yum install java-1.8.0-openjdk java-1.8.0-openjdk-devel
# 安装JDK,如果没有java-1.8.0-openjdk-devel就没有javac命令
3.安装Kibana
docker pull kibana
docker run -d \
--name kibana \
-e ELASTICSEARCH_HOSTS=http://es:9200 \ # 和es在同一个服务器,所以可以使用容器名互联
--network=es-net \
-p 5601:5601 \
kibana # 版本要和es保持一致
访问IP:5601
GET _search // get请求方式,搜索 下面是搜索内容
{
"query": {
"match_all": {}
}
}
# 模拟请求模拟请求请求
GET /
常见问题:
1.访问失败
Unable to connect to Elasticsearch at http://elasticsearch:9200.
如下图:
解决方案:修改kibana的配置文件kibana.yml
docker exec -it 容器名 /bin/bash
cd /etc/kibana
cat kibana.yml # 把文件中的内容复制出来,然后进行修改
cat > kibana.yml # 把修改好的内容在粘贴进去 ctrl + c 退出
修改完成之后重启kibana容器
docker restart kibana
在容器中修改文件方法(推荐用方法1,简单):在容器中修改文件方法1
本问题处理其它方法:
4.分词器
es在创建倒排索引时需要对文档分词,在搜索时,需要对用户输入内容分词。但默认的分词规则对中文处理并不友好。
处理中文分词,一般会使用IK分词器。
1.在线安装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 es
2.离线安装ik插件
1.查看数据卷目录
docker volume inspect es-plugins
2.解压缩分词器安装包
在数据卷目录解压安装包
3.重启容器
3.测试
IK分词器包含两种模式:
ik_smart:最少切分 粗粒度
ik_max_word:最细切分 细粒度
# 测试分词器
POST /_analyze
{
"text": "123",
"analyzer": "ik_max_word"
}
4.分词器的扩展和停用词典
在ik分词器的config文件加下的IKAnalyzer.cfg文件中编辑
<properties>
<comment>IK Analyzer 扩展配置</comment>
<!-- 用户可以在这里配置自己的扩展字典 -->
<entry key="ext_dict"/>
<!-- 用户可以在这里配置自己的扩展停止词字典 -->
<entry key="ext_stopwords"/>
<!-- 用户可以在这里配置远程扩展字典 -->
<!-- <entry key="remote_ext_dict">words_location</entry> -->
<!-- 用户可以在这里配置远程扩展停止词字典 -->
<!-- <entry key="remote_ext_stopwords">words_location</entry> -->
</properties>
在对应的词典文件中修改内容
修改之后重启容器
3.索引库操作
1.mapping属性
mapping是对索引库中文档的约束,常见的mapping属性包括:
1.type:字段数据类型,常见的简单类型有:
字符串:text(可分词的文本),keyword(精确值,例如:品牌,国家,ip地址。 无法进行分词的专有名词)
数值:long,integer,short,byte,double,float
布尔:boolean
日期:date
对象:object
2.index:是否创建索引(是否参与搜索),默认为true
3.analyzer:使用哪种分词器
4.properties:该字段的子字段
2.创建索引库
DSL语句
PUT /index_name # 索引名称
{
"mappings": { # 映射
"properties": { # 字段
"info": { # 字段1名称
"type":"text", # 字段1类型
"analyzer":"ik_smart" # 分词模式
},
"email":{
"type":"keyword",
"index": false
},
"name": {
"type":"object",
"properties": { # 子字段数据
"firstName": { # 子字段1
"type":"keyword"
},
"lastName": {
"type":"keyword"
}
}
}
}
}
}
3.索引库操作
查看索引库
GET /索引库名称
删除索引库
DELETE /索引库
修改索引库
索引库和mapping一旦创建无法修改,但是可以添加新的字段
PUT /index_name/_mapping
{
"properties": {
"age":{
"type":"integer"
}
}
}
4.文档操作
没进行一次操作,文档版本号都会加1
1.添加文档
POST /index_name/_doc/1 # 索引名称/_doc/id id不给会自动生成
{
"info":"字段值",
"email":"hhh@qq.cn",
"name":{
"firstName":"value1",
"lastName":"value2"
}
}
2.查询文档
GET /索引库名称/_doc/id
3.删除文档
DELETE /索引库名称/_doc/id
4.修改文档
1.全量修改,会删除旧文档,添加新文档 如果存在则修改,不存在则新增
PUT /index_name/_doc/1 # 索引名称/_doc/id 先删除后新增
{
"info":"字段值",
"email":"hhh@qq.cn",
"name":{
"firstName":"value1",
"lastName":"value2"
}
}
2.增量修改,修改指定字段
POST /index_name/_update/1
{
"doc":{
"email":"new_value"
}
}
4.RestClient操作索引库
1.什么是RestClient
ES官方提供了各种不同语言的客户端,用来操作ES.这些客户端的本质就是组织DSL语句,通过http请求发送给ES。
2.基本步骤
1.引入es的RestHighLevelClient依赖
<dependency>
<groupId>org.elasticsearch.client</groupId>
<artifactId>elasticsearch-rest-high-level-client</artifactId>
<version>7.12.1</version>
</dependency>
2.springBoot默认的ES版本是7.6.2,所以需要覆盖默认的版本
<properties>
<java.version>1.8</java.version>
<elasticsearch.version>7.12.1</elasticsearch.version>
</properties>
3.初始化RestHighLevelClient
package cn.itcast.hotel;
import org.apache.http.HttpHost;
import org.elasticsearch.client.RestClient;
import org.elasticsearch.client.RestHighLevelClient;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import java.io.IOException;
public class HotelIndexTest {
private RestHighLevelClient client;
@Test
void testInit() {
System.out.println(client);
}
/* 创建连接 初始化 */
@BeforeEach
void setUp() {
this.client = new RestHighLevelClient(RestClient.builder(
HttpHost.create("http://192.168.1.52:9200")
));
}
/* 销毁连接 */
@AfterEach
void tearDown() throws IOException {
this.client.close();
}
}
4.创建索引库
@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);
}
package cn.itcast.hotel.constants;
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" +
" },\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\": \"keyword\",\n" +
" \"analyzer\": \"ik_max_word\"\n" +
" }\n" +
" }\n" +
" }\n" +
"}";
}
5.删除索引库
@Test
void deleteHotelIndex() throws IOException {
DeleteIndexRequest request = new DeleteIndexRequest("hotel");
client.indices().delete(request, RequestOptions.DEFAULT);
}
6.判断索引库是否存在
@Test
void existsHotelIndex() throws IOException {
GetIndexRequest request = new GetIndexRequest("hotel");
boolean exists = client.indices().exists(request, RequestOptions.DEFAULT);
System.out.println(exists ? "索引库已经存在" : "索引库不存在");
}
5.RestClient操作文档
1.基本操作
1.插入文档
@Test
void testAddDocument() throws IOException {
// 根据id查询数据库中数据
Hotel hotel = hotelService.getById(1);
// 转换为文档类型
HotelDoc hotelDoc = new HotelDoc(hotel);
// 1.准备Request对象
IndexRequest request = new IndexRequest("hotel").id(hotel.getId().toString());
// 2.准备Json文档
request.source(JSON.toJSONString(hotelDoc), XContentType.JSON);
// 3.发送请求
client.index(request, RequestOptions.DEFAULT);
}
2.查询文档
@Test
void testGetDocumentById() throws IOException {
// 1.创建request对象
GetRequest request = new GetRequest("hotel", "1");
// 2.发送请求,获取响应
GetResponse response = client.get(request, RequestOptions.DEFAULT);
// 3.解析结果
String json = response.getSourceAsString();
HotelDoc hotelDoc = JSON.parseObject(json, HotelDoc.class);
System.out.println(hotelDoc);
}
3.修改文档
1.全量更新
再次写入id一样的文档,就会删除旧文档,添加新文档
2.局部更新
@Test
void testUpdateDocumentById() throws IOException {
// 1.创建request对象
UpdateRequest request = new UpdateRequest("hotel", "1");
// 2.准备请求参数
request.doc(
"price", "100",
"startName", "五钻"
);
// 发送请求
client.update(request, RequestOptions.DEFAULT);
}
4.删除文档
@Test
void testDeleteDocument() throws IOException {
// 1.准备request
DeleteRequest request = new DeleteRequest("hotel", "1");
// 2.发送请求
client.delete(request, RequestOptions.DEFAULT);
}
2.批量导入数据到es
@Test
void testBulkRequest() throws IOException {
// 批量查询数据
List<Hotel> hotels = hotelService.list(); // 随便什么对象都行
// 1.创建request
BulkRequest request = new BulkRequest();
// 2.准备参数,添加多个request请求
// 转换为文档类型HotelDoc
for (Hotel hotel : hotels) {
HotelDoc hotelDoc = new HotelDoc(hotel); // 随便什么对象都行
request.add(new IndexRequest("hotel")
.id(hotelDoc.getId().toString())
.source(JSON.toJSONString(hotelDoc), XContentType.JSON));
}
// 3.发送请求
client.bulk(request, RequestOptions.DEFAULT);
}
6.DSL查询
DSL Query的分类
Elasticsearch提供了基于JSON的DSL来定义查询。常见的查询类型包括:
1.查询基本语法
GET /index_name/_search
{
"query": {
"查询类型": {
"查询字段" : "查询值"
}
}
}
查询所有:查询出所有数据,一般测试用。例如:match_all
GET /index_name/_search
{
"query": {
"match_all": { # 查询所有,不用写东西
}
}
}
2.全文检索(full text)查询
利用分词器对用户输入的内容分词,然后去倒排索引库中匹配,常用于搜索框搜索。例如
1.match查询
对用户输入内容进行分词,去倒排索引库去查,一次只能一个字段
GET /index_name/_search
{
"query": {
"match": {
"FIELD": "TEXT" # FIELD:字段名 TEXT: 搜索内容
}
}
}
2.multi_match
允许同时查询多个字段
GET /index_name/_search
{
"query": {
"multi_match": {
"query": "value",
"fields": ["key1", "key2", "age"] # 多个字段搜索同一个值
}
}
}
3.精确查询
根据精确词条值查找数据,一般是查找keyword,数值,日期,boolean等类型字段,所以不会对搜索条件分词。例如:
1. range 数值范围
GET /index_name/_search
{
"query": {
"range": {
"FIELD": { # 字段名
"gte": 10, # 大于等于
"lte": 20 # 小于等于
}
}
}
}
2.trem 根据词条精确值查询
GET /index_name/_search
{
"query": {
"term": {
"key": { # 字段名
"value": "搜索值"
}
}
}
}
3.地理(geo)查询
根据经纬度查询。例如:
1.geo_distance
查询到指定中心点小于某个距离值的所有文档
GET /index_name/_search
{
"query": {
"geo_distance": {
"distance": "15km",
"location": 32.2, 124.5
}
}
}
2.geo_bounding_box
查询geo_point值落在某个矩形范围的所有文档
GET /index_name/_search
{
"query": {
"geo_bounding_box": {
"FIELD": { # 类型为geo_point的字段名称
"top_left": {
"lat": 30.1,
"lon": 123.2
},
"bottom_right": {
"lat": 30.5,
"lon": 123.2
}
}
}
}
}
4.复合(compound)查询
复合查询可以将上述各种查询条件组合起来,合并查询条件。例如:
1.Boolean Query
布尔查询是一个或多个查询子句的组合。子查询的组合方式有:
must:必须匹配每个子查询, 与
should:选择性匹配子查询, 或
must_not:必须不匹配,不参与算分, 非
filter:必须匹配,不参与算分
GET /index_name/_search
{
"query": {
"bool": {
"must": [
{
"term": {
"FIELD": {
"value": "VALUE"
}
}
}
],
"must_not": [
{
"range": {
"FIELD": {
"gte": 10,
"lte": 20
}
}
}
],
"should": [
{
"term": {
"FIELD": {
"value": "VALUE"
}
}
}
],
"filter": [
{"range": {
"FIELD": {
"gte": 10,
"lte": 20
}
}}
]
}
}
}
2.function_score:算分函数查询
可以控制文档相关性算分(query score),控制文档排名。
算分函数4部分:
1.原始查询条件,搜索文档并根据相关性打分(query score)
2.过滤条件,符合条件的文档才会被重新算分
3.算分函数:算分函数的结果称为function score,将来会与query score运算,得到新算分,常见算分函数有:
weight:给一个常量值,作为函数结果(function score)
field_value_factor:用文档中的某个字段值作为函数结果
random_score:随机生成一个值,作为函数结果
script_score:自定义计算公式,公式结果作为函数结果
4.加权模式:定义function score与query score的运算方式,包括:
multiply:两者相乘。默认。
replace:用function score替换query score
其它: sum, avg, max, min
GET /hotel/_search
{
"query": {
"function_score": {
"query": { # 原始查询条件,搜索文档并根据相关性打分(query score)
"match": {
"FIELD": "TEXT"
}
},
"functions": [
{
"filter": { # 过滤条件,符合条件的文档才会被重新算分
"term": {
"FIELD": "VALUE"
}
},
"weight": 10 # 算法函数
}
],
"boost_mode": "multiply" # 加权模式
}
}
}
3.相关性算分
当我们利用match查询时,文档结果会根据与搜索词条的关联度打分(_score),返回结果时按照分值降序排列。
TF算法
TF(词条频率) = 词条出现次数/文档中词条总数
TF-IDF算法(传统算法,新版本5.0后已不采用)
IDF(逆文档频率) = Log(文档总数/包含词条的文档总数)
score =
BM25算法
7.搜索结果处理
1.排序
elasticsearch支持对搜索结果排序,默认是根据相关度算分(_score)来排序。可以排序字段类型有:keyword类型,数值类型,地理坐标类型,日期类型等。
GET /index_name/_search
{
"query": {
"match_all": {}
},
"sort": [
{
"FIELD": { # 指定要排序的字段
"order": "desc" # 排序方式
}
},
{
"_geo_distance": { # 地理坐标的排序
"FIELD": {
"lat": 40,
"lon": -70
},
"order": "asc",
"unit": "km"
}
]
}
2.分页
elasticsearch默认情况下只返回top10的数据。而如果要查询更多数据就需要修改分页参数了。
elasticsearch中通过修改from,size参数来控制要返回的分页结果。
GET /index_name/_search
{
"query": {
"match_all": {}
},
"from": 0, # 分页开始的位置,默认从0开始
"size": 20, # 去多少条
"sort": [
{
"FIELD": {
"order": "desc"
}
}
]
}
深度分页问题
ES是分布式的,所以会面临深度分页问题。例如获取from=990,size=10的数据:
1.首先在每个数据分片上都排序并查询前1000条文档。
2.然后将所有节点的结果聚合,在内存中重新排序选出前1000条文档。
3.最后从这1000条中,选取从990开始的10条文档。
如果搜索页数过深,或者结果集越大,对内存和CPU的消耗也越高。因此ES设定结果集查询的上限是10000。
深度分页解决方案
1.search after:分页时需要排序,原理是从上一次的排序值开始,查询下一次数据。推荐。但是只能往后翻页,不能往前,随机。
2.scroll:原理将排序数据形成快照,保存在内存。不推荐。
3.高亮
就是在搜索结果中把搜索关键字突出显示。
原理:
将搜索结果中的关键字用标签标记出来
在页面中给标签添加css样式。
GET /index_name/_search
{
"query": {
"match": {
"FIELD": "TEXT"
}
},
"highlight": {
"fields": {
"age": { # 指定要高亮的字段
"pre_tags": "<em>", # 用来标记高亮字段的前置标签
"post_tags": "</em>", # 用来标记高亮字段的后置标签
"require_field_match": "false" # 搜索字段是否必须与高亮字段一致,默认是true
}
}
}
}
8.RestClient查询文档
RestAPI中其中构建DSL是通过HighLevelRestClient中的resource()来实现的,其中包含了查询,排序,分页,高亮等所有功能。
RestAPI中其中构建查询条件的核心部分是由一个名为QueryBuilders的工具类提供的,其中包含了各种查询方法
示例:match_all查询
/* match_all查询 */
@Test
void testMatchAll() throws IOException {
// 1.准备Request
SearchRequest request = new SearchRequest("hotel");
// 2.准备DSL
request.source().query(QueryBuilders.matchAllQuery());
// 3.发送请求
SearchResponse response = client.search(request, RequestOptions.DEFAULT);
// 4.解析结果
SearchHits searchHits = response.getHits();
// 5.查询的总条数
long total = searchHits.getTotalHits().value;
System.out.println("total = " + total);
// 6.查询的结果数组
SearchHit[] hits = searchHits.getHits();
for (SearchHit hit : hits) {
// 获取文档source
String json = hit.getSourceAsString();
// 反序列化
HotelDoc hotelDoc = JSON.parseObject(json, HotelDoc.class);
System.out.println("hotelDoc = " + hotelDoc);
}
System.out.println(response);
}
1.全文检索查询
1.match查询
/* match查询 */
@Test
void testMatch() throws IOException {
// 1.准备Request
SearchRequest request = new SearchRequest("hotel");
// 2.准备DSL
request.source().query(QueryBuilders.matchQuery("key", "value"));
// 3.发送请求
SearchResponse response = client.search(request, RequestOptions.DEFAULT);
// 4.解析结果
SearchHits searchHits = response.getHits();
// 5.查询的总条数
long total = searchHits.getTotalHits().value;
System.out.println("total = " + total);
// 6.查询的结果数组
SearchHit[] hits = searchHits.getHits();
for (SearchHit hit : hits) {
// 获取文档source
String json = hit.getSourceAsString();
// 反序列化
HotelDoc hotelDoc = JSON.parseObject(json, HotelDoc.class);
System.out.println("hotelDoc = " + hotelDoc);
}
}
2.bool查询
/* bool查询 */
@Test
void testBool() throws IOException {
// 1.准备Request
SearchRequest request = new SearchRequest("hotel");
// 2.准备DSL
// 2.1准备booleanQuery
BoolQueryBuilder boolQuery = QueryBuilders.boolQuery();
// 2.2添加term
boolQuery.must(QueryBuilders.termQuery("key", "value"));
// 2.3添加range
boolQuery.filter(QueryBuilders.rangeQuery("key").lte(200));
request.source().query(boolQuery);
// 3.发送请求
SearchResponse response = client.search(request, RequestOptions.DEFAULT);
// 4.解析结果
handleResponse(response);
}
3.排序和分页
/* 分页排序 */
@Test
void testPageAndSort() throws IOException {
// 1.准备Request
SearchRequest request = new SearchRequest("hotel");
// 2.准备DSL
request.source().query(QueryBuilders.matchAllQuery());
// 2.1排序
request.source().sort("key", SortOrder.ASC);
// 2.2分页
request.source().from(0).size(10);
// 3.发送请求
SearchResponse response = client.search(request, RequestOptions.DEFAULT);
// 4.解析结果
handleResponse(response);
}
4.高亮
/* 高亮 */
@Test
void testHighlight() throws IOException {
// 1.准备Request
SearchRequest request = new SearchRequest("hotel");
// 2.准备DSL
request.source().query(QueryBuilders.matchQuery("key", "value"));
// 2.1高亮
request.source().highlighter(new HighlightBuilder().field("key").requireFieldMatch(false).preTags("<em>").postTags("</em>"));
// 3.发送请求
SearchResponse response = client.search(request, RequestOptions.DEFAULT);
// 4.解析结果
handleResponse(response);
}
private static void handleResponse(SearchResponse response) {
SearchHits searchHits = response.getHits();
// 5.查询的总条数
long total = searchHits.getTotalHits().value;
System.out.println("total = " + total);
// 6.查询的结果数组
SearchHit[] hits = searchHits.getHits();
for (SearchHit hit : hits) {
// 获取文档source
String json = hit.getSourceAsString();
// 反序列化
HotelDoc hotelDoc = JSON.parseObject(json, HotelDoc.class);
if (!CollectionUtils.isEmpty(hit.getHighlightFields())) {
// 获取高亮结果
Map<String, HighlightField> highlightFields = hit.getHighlightFields();
// 根据字段名获取高亮结果
HighlightField highlightField = highlightFields.get("name");
// 获取高亮值
String name = highlightField.getFragments()[0].string();
// 覆盖非高亮结果
hotelDoc.setName(name);
}
System.out.println("hotelDoc = " + hotelDoc);
}
}
9.数据聚合aggregations
可以实现对文档数据的统计,分析,运算。参与聚合的字段类型必须是keyword,数值,日期,布尔。
1.聚合分类
1.桶(Bucket)聚合:用来对文档做分组
TermAggregation:按照文档字段值分组
Date Histogram:按照日期阶梯分组
2.度量(Metric)聚合:用以计算一些值,比如:最大值,最小值,平均值等
Avg:平均
Max,Min:最值
Stats:同时求max,min,avg,sum等
3.管道(pipeline)聚合:其它聚合的结果为基础做聚合
2.DSL实现Bucket聚合
GET /index_name/_search
{
"size": 0, # 设置size为0,结果中不包含文档,只包含聚合结果
"aggs": { # 定义聚合
"NAME": { # 聚合名称
"terms": { # 聚合的类型
"field": "", # 参与聚合的字段
"order": {
"_count": "asc"
},
"size": 10 # 希望获取的聚合结果数量
}
}
}
}
默认情况下,Bucket聚合会统计Bucket内的文档数量,记为_count,并且按照_count降序排序,可通过order属性修改
默认情况下,Bucket聚合是对索引库的所有文档做聚合,如果要限定聚合的文档范围,只要添加query条件即可。
GET /index_name/_search
{
"query": {
"range": {
"FIELD": {
"gte": 10,
"lte": 20
}
}
},
"size": 0,
"aggs": {
"NAME": {
"terms": {
"field": "",
"order": {
"_count": "asc"
},
"size": 10
}
}
}
}
聚合三要素: 聚合名称,聚合类型,聚合字段
聚合可配置属性:
size:指定聚合结果数量
order:指定聚合结果排序方式
field:指定聚合字段
3.DSL实现Metrics聚合
GET /index_name/_search
{
"size": 0,
"aggs": {
"NAME": {
"terms": {
"field": "",
"order": {
"NAME2.avg": "asc" # 对聚合出的平均值进行排序
},
"size": 10
},
"aggs": { # NAME聚合的子聚合,就是分组后对每组分别计算
"NAME2": { # 聚合名称
"stats": { # 聚合类型,stats可以计算min,max,avg等
"field": "field_name" # 聚合字段
}
}
}
}
}
}
4.RestAPI实现聚合
@Test
void testAggregation() throws IOException {
// 1.准备Request
SearchRequest request = new SearchRequest("hotel");
// 2.准备DSL
request.source().size(0);
request.source().aggregation(AggregationBuilders.terms("聚合名称").field("字段名称").size(10));
// 3.发送请求
SearchResponse response = client.search(request, RequestOptions.DEFAULT);
// 4.解析结果
// 4.1解析聚合结果
Aggregations aggregations = response.getAggregations();
// 4.2根据聚合名称获取聚合结果
Terms terms = aggregations.get("组合结果");
// 4.3获取buckets
List<? extends Terms.Bucket> buckets = terms.getBuckets();
// 4.4遍历buckets
for (Terms.Bucket bucket : buckets) {
// 4.5获取key
String key = bucket.getKeyAsString();
System.out.println("key = " + key);
}
}
10.自动补全
1.安装配音分词器
下载配音分词器文件----上传至es日期插件目录(/var/lib/docker/volumes/es-plugins/_data)---重启es容器(时间很长)
测试:
POST /_analyze
{
"text": ["测试内容"],
"analyzer": "pinyin"
}
测试结果:
{
"tokens" : [
{
"token" : "ce",
"start_offset" : 0,
"end_offset" : 0,
"type" : "word",
"position" : 0
},
{
"token" : "csnr",
"start_offset" : 0,
"end_offset" : 0,
"type" : "word",
"position" : 0
},
{
"token" : "shi",
"start_offset" : 0,
"end_offset" : 0,
"type" : "word",
"position" : 1
},
{
"token" : "nei",
"start_offset" : 0,
"end_offset" : 0,
"type" : "word",
"position" : 2
},
{
"token" : "rong",
"start_offset" : 0,
"end_offset" : 0,
"type" : "word",
"position" : 3
}
]
}
2.自定义分词器
elasticsearch中分词器(analyzer)的组成包含三部分:
1.character filters:在tokenizer之前对文本进行处理。例如删除字符,替换字符
2.tokenizer:将文本安装一定的规则切割成词条(term)。例如keywrod,就是不分词;还有ik_smart
3.tokenizer filter:将tokenizer输出的词条做进一步处理。例如大小写转换,同义词处理,配音处理等。
自定义方法:在插件索引库时,通过settings来配置自定义的analyzer(分词器)
PUT /test
{
"settings": {
"analysis": {
"analyzer": { # 自定义分词器
"my_analyzer": { # 分词器名称
"tokenizer": "ik_max_word",
"filter": "py" # filter名称,指向下方py
}
},
"filter": {
"py": {
"type": "pinyin", # 分词器类型
"keep_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" # 使用上方的分词器
}
}
}
}
拼音分词器适合在创建倒排索引的时候使用,但不能在搜索的时候使用。有同音字问题。需要在创建索引时指定,在搜索时使用另一个分词器。
"mappings": {
"properties": {
"name": {
"type": "text",
"analyzer": "my_analyzer",
"search_analyzer": "ik_smart"
}
}
}
3.Completion Suggester查询
elasticsearch提供了Completion Suggester查询来实现自动补全功能。这个查询会匹配以用户输入内容开头的词条并返回。为了提高补全查询的效率,对于文档中的字段有一些约束:
1.参与补全查询的字段必须是completion类型。
2.字段的内容一般是用来补全的多个词条形成的数组。
# 自动补全的索引库
PUT index_name
{
"mappings": {
"properties": {
"title": {
"type": "completion"
}
}
}
}
# 查询语法
GET /index_name/_search
{
"suggest": {
"YOUR_SUGGESTION": { # 查询名称
"text": "YOUR TEXT", # 要查询的数据
"completion": {
"FIELD": "MESSAGE", # 字段名
"skip_duplicates": true, # 跳过重复的
"size": 10 # 一次查10个
},
}
}
}
4.RestAPI实现自动补全
@Test
void testSuggest() throws IOException {
// 1.准备Request
SearchRequest request = new SearchRequest("hotel");
// 2.准备DSL
request.source().suggest(new SuggestBuilder()
.addSuggestion("suggestName",
SuggestBuilders.completionSuggestion("字段名")
.prefix("关键字")
.skipDuplicates(true)
.size(10)
)
);
// 3.发起请求
SearchResponse response = client.search(request, RequestOptions.DEFAULT);
// 4.解析结果
Suggest suggest = response.getSuggest();
// 4.1根据补全查询名称,获取补全结果
CompletionSuggestion suggestion = suggest.getSuggestion("suggestName");
// 4.2获取options
List<CompletionSuggestion.Entry.Option> options = suggestion.getOptions();
for (CompletionSuggestion.Entry.Option option : options) {
System.out.println("option.getText() = " + option.getText().toString());
}
System.out.println("response = " + response);
}
11.数据同步
1.常用数据同步方案
1.同步调用
优点:实现简单,粗暴
缺点:业务耦合度高
2.异步通知
优点:低耦合,实现难度一般
缺点:依赖mq的可靠性
3.监听binlog
优点:完全解除服务间耦合
缺点:开启binlog增加数据库负担,实现复杂度高
12.ES集群
单机的elasticsearch做数据存储,必然面临两个问题:海量数据存储问题,单点故障问题
海量数据存储问题:将索引库从逻辑上拆分为N个分片(shard),存储到多个节点
单点故障问题:将分片数据在不同节点备份(replica)
1.搭建ES集群
1.编写一个docker-compose文件
通过容器来模拟节点
version: '2.2'
services:
es01:
image: elasticsearch:7.12.1 # 镜像
container_name: es01 # 容器名称
environment:
- node.name=es01 # 节点名称,不能重复
- cluster.name=es-docker-cluster # 集群名称,集群名称相同则在同一个集群
- discovery.seed_hosts=es02,es03 # 集群中其他节点的ip,容器内用名称指定
- cluster.initial_master_nodes=es01,es02,es03 # 候选主节点
- "ES_JAVA_OPTS=-Xms512m -Xmx512m"
volumes:
- data01:/usr/share/elasticsearch/data
ports:
- 9200:9200
networks:
- elastic
es02:
image: elasticsearch:7.12.1
container_name: es02
environment:
- node.name=es02
- cluster.name=es-docker-cluster
- discovery.seed_hosts=es01,es03
- cluster.initial_master_nodes=es01,es02,es03
- "ES_JAVA_OPTS=-Xms512m -Xmx512m"
volumes:
- data02:/usr/share/elasticsearch/data
ports:
- 9201:9200
networks:
- elastic
es03:
image: elasticsearch:7.12.1
container_name: es03
environment:
- node.name=es03
- cluster.name=es-docker-cluster
- discovery.seed_hosts=es01,es02
- cluster.initial_master_nodes=es01,es02,es03
- "ES_JAVA_OPTS=-Xms512m -Xmx512m"
volumes:
- data03:/usr/share/elasticsearch/data
networks:
- elastic
ports:
- 9202:9200
volumes:
data01:
driver: local
data02:
driver: local
data03:
driver: local
networks:
elastic:
driver: bridge
2.修改配置
es运行需要修改一些linux系统权限,修改`/etc/sysctl.conf`文件
vi /etc/sysctl.conf
添加下面的内容:
vm.max_map_count=262144
然后执行命令,让配置生效:
sysctl -p
3.启动
docker-compose up -d
2.ES集群监控
推荐使用cerebro来监控es集群状态,官方网址:https://github.com/lmenezes/cerebro
下载cerebro压缩包,解压,进入bin目录,双击其中的cerebro.bat文件即可启动服务。访问http://localhost:9000 即可进入管理界面。输入你的elasticsearch的任意节点的地址和端口,点击connect即可:
创建分片索引库
PUT /index_name
{
"settings": {
"number_of_shards": 3, // 分片数量
"number_of_replicas": 1 // 副本数量
},
"mappings": {
"properties": {
// mapping映射定义 ...
}
}
}
3.ES集群的节点角色
节点类型 | 配置参数 | 默认值 | 节点职责 |
master eligible | node.master | true | 备选主节点:主节点可以处理管理和记录集群状态,决定分片在哪个节点,处理创建和删除索引库的请求 |
data | node.data | true | 数据节点:存储数据,搜索,聚合,CRUD |
ingest | node.ingest | true | 数据存储之前的预处理 |
coordinating | 上面3个参数都为false则为coordinating节点 | 无 | 路由请求到其他节点,合并其他节点处理的结果,返回给用户 |
elasticsearch中的每个节点角色都有自己不同的职责,因此建议集群部署时,每个节点都有独立的角色
4.集群脑裂
默认情况下,每个节点都是master eligible节点,因此一旦master节点宕机,其他候选节点会推举一个成为主节点。当主节点与其他节点网络故障时,可能会发生脑裂问题。即两个主节点。
为了避免脑裂,需要要求选票超过(eligible节点数量 + 1) / 2才能当选为主,因此eligible节点数量最好是奇数。对应配置项是discovery.zen.minimum_master_nodes,在es7.0以后,已经成为默认配置,会自动计算合适的eligible节点数量,一般不会发生脑裂问题。
5.分布式存储
当新增文档是,应该保存到不同分片,保证数据均衡,coordinating node通过hash算法来计算
shard = hash(_routing) % number_of_shards
_routing默认是文档id, number_of_shards是分片数量
算法与分片数量有关,因此索引库一旦创建, 分片数量不能修改。
6.分布式查询
elasticsearch的查询分两个阶段:
scatter phase:分散阶段,coordinating node会把请求分发到每一个分片
gather phase:聚集阶段,coordinating node汇总data node的搜索结果,并处理为最终结果集返回给用户。
7.故障转移
集群的master节点会监控集群中的节点状态,如果发现有节点宕机,会立即将宕机节点的分片数据迁移到其他节点,确保数据安全,这个叫做故障转移。