8.1 概述
- ES是一个开源搜索引擎,可以从海量数据中快速找到需要的内容。
- ES结合Kibana、Logstash、Beats,也就是elastic stack(
ELK
),被广泛应用在日志数据分析、实时性能监控等领域。 - ES是elastic stack核心,负责存储、搜索、分析数据。
- ES底层由
Lucene
实现,Lucene
是一个Java语言的搜索引擎类库,属于Apache。 - ES支持分布式,可水平扩展;提供Restful接口,可被任何语言调用。
总结:
什么是ES:一个开源的分布式搜索引擎,可以用来实现搜索、日志统计与分析、系统监控等功能。
8.2 倒排索引
正向索引:
传统数据库如MySQL采用正向索引,通过id建立索引,然后通过索引实现快速查找。
但是如果要查找具体文本内容(比如模糊查询"%浏览器"),这时就只能一行一行的匹配搜索,将可能的结果存入结果集。
倒排索引:
倒排索引采用词条
+文档
文档(document)
:一条数据就是一个文档(采用Json存储,一个文档也就是一个json)。
词条(term)
:文档按照语义分成的词语。词条是唯一的,通过词条去创建索引。
总结:
正向索引:基于文档id创建索引。查询词条时必须先找到文档,然后判断是否包含词条。
倒排索引:对文档内容分词形成词条,基于词条创建索引,并记录词条所在文档的信息。查询词条时先根据词条查询到文档id,然后获取到文档。
8.2 ES与MySQL对比
-
ES的文档:可以是数据库中的一条商品数据,一个订单信息。
ES采用JSON格式存储,文档数据会被序列化为json然后存储。 -
ES的索引(index):相同类型文档的集合。
映射(mapping):索引中文档的字段约束信息(类似数据库表的结构约束)。
-
ES和MySQL对比
MySQL:擅长事务类型操作,可以确保数据的安全和一致性。
ElasticSearch:擅长海量数据的搜索、分析、计算。
总结:
文档:一条数据就是一个文档,也就是一个json。
索引:同类型文档的集合。
字段:json文档中的字段。
映射:索引中文档的约束,比如字段名称、类型。
8.3 Elastic Stack
8.3.1 安装es
- 创建网络,使es和kibana互联
docker network create es-net
- pull镜像
docker pull elasticsearch
- 创建ES的container(如果报没有文件夹,就手动创建该文件夹)
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 \
-v es-config:/usr/share/elasticsearch/config \
--privileged \
--network=es-net \
-p 9200:9200 \
-p 9300:9300 \
elasticsearch:7.12.1
参数说明:
-e cluster.name=es-docker-cluster
:设置集群名称-e "http.host=0.0.0.0"
:监听的地址,可以外网访问-e "ES_JAVA_OPTS=-Xms512m -Xmx512m"
:ES运行内存大小,默认1T-e "discovery.type=single-node"
:非集群模式-v es-data:/usr/share/elasticsearch/data
:挂载逻辑卷,绑定es的数据目录-v es-logs:/usr/share/elasticsearch/logs
:挂载逻辑卷,绑定es的日志目录-v es-data:/usr/share/elasticsearch/config
:挂载逻辑卷,绑定es的配置目录-v es-plugins:/usr/share/elasticsearch/plugins
:挂载逻辑卷,绑定es的插件目录--privileged
:授予逻辑卷访问权--network es-net
:加入一个名为es-net的网络中-p 9200:9200
:端口映射配置-p 9300:9300
:集群通信端口
- 输入IP:9200查看响应结果
8.3.2 安装kibana
注意!!!kibana的版本必须和ES版本保持完全一致
- pull镜像
docker pull kibana
- 创建container
docker run -d \
--name kibana \
-e ELASTICSEARCH_HOSTS=http://es:9200 \
--network=es-net \
-p 5601:5601 \
kibana:7.12.1
参数说明:
--network=es-net
:加入一个名为es-net(自定义的)的网络中,与elasticsearch在同一个网络中。-e ELASTICSEARCH_HOSTS=http://es:9200"
:设置elasticsearch的地址,因为kibana已经与elasticsearch在一个网络,因此可以用容器名(docker ps -a看刚才的ES name是什么,我这里是es)直接访问elasticsearch。-p 5601:5601
:端口映射配置。
- 通过IP:5601访问
- 在kibana的web界面中,
Dev Tools
选项中可以发送请求给ES并查看到结果(比如GET /)
8.3.3 安装IK分词器
分词器作用:创建倒排索引时对文档分词;用户搜索时,对搜索内容分词。
- 进入ES的容器内
docker exec -it es /bin/bash
- 在线下载插件并安装
注意:先去GitHub的Releases查看对应版本(以.zip结尾的)。
./bin/elasticsearch-plugin install https://github.com/medcl/elasticsearch-analysis-ik/releases/download/v7.12.1/elasticsearch-analysis-ik-7.12.1.zip
- 退出容器bash
exit
- 重启ES容器
docker restart es
- 测试(ik两种模式)
ik_smart
:最少切分
ik_max_word
:最细切分
kibana的web界面中:
GET /_analyze
{
"analyzer": "ik_max_word",
"text": "爸爸的爸爸叫爷爷"
}
8.3.4 扩展分词器词典
ik分词器无法理解最新的网络热词,所以词典的词汇需要不断更新。
-
查看本地挂载的数据卷
docker volume list
-
查看某一个数据卷的具体信息,这里查看之前es挂载出来的配置目录
docker volume inspect es-config
-
在config目录的
IKAnalyzer.cfg.xml
配置文件中添加
-
在
IKAnalyzer.cfg.xml
同目录下新建ext.dic
和stopwords.dic
(文件名与配置文件中的一致就行) -
两个文件中分别添加需要的内容
-
重启es
docker restart es
-
kibana中测试效果
8.4 Kibana操作索引库
注意:只有字符串中的text会进行分词,数值、布尔、日期、对象都不参与分词(因为这些分词无意义)
8.4.1 创建索引库
ES通过Restful请求来操作索引库、文档。请求内容用DSL语句表示,下面给一个创建索引库的示例
copy_to的使用方式示例:
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"
},
"starName": {
"type": "keyword"
},
"business": {
"type": "keyword",
"copy_to": "all"
},
"location": {
"type": "geo_point"
},
"pic": {
"type": "keyword",
"index": false
},
"all": {
"type": "text",
"analyzer": "ik_max_word"
}
}
}
}
8.4.2 删改查索引库
GET
表示查询,比如GET /helloworld
DELETE
表示删除,比如DELETE /helloworld
注意!!! 索引库和mapping一旦创建无法修改,所以没有修改索引库的说法,但是可以添加新字段,比如:
PUT /索引库名/_mapping
{
"properties":{
"新字段名":{
"type":"integer"
}
}
}
8.5 Kibana操作文档
8.5.1 增删查文档
POST
新增文档
GET
查询文档,比如GET /helloworld/_doc/1
查询所有文档:GET /helloworld/_search
DELETE
删除文档,比如DELETE /helloworld/_doc/1
8.5.2 修改文档
8.6 RestClient操作索引库
初始化RestClient:
8.6.1 创建索引库
8.6.2 删除、判断索引库
分析:
client.indices()
返回的是一个索引客户端,里面包含了创建索引、删除索引等方法;
所以按照上面创建索引库来推断,删除索引库代码如下:
@Test
void testDeleteHotelIndex() throws IOException {
DeleteIndexRequest request = new DeleteIndexRequest("hotel");
//delete()方法需要一个DeleIndexRequest类型的参数,所以就构造一个
client.indices().delete(request, RequestOptions.DEFAULT);
}
判断索引库的代码如下:
@Test
void testDecideHotelIndex() throws IOException {
GetIndexRequest request = new GetIndexRequest("hotel");
//exists()方法需要一个GetIndexRequest类型的参数,所以就构造一个
boolean exists = client.indices().exists(request, RequestOptions.DEFAULT);
}
8.7 RestClient操作文档
8.7.1 新增文档
以下实现的功能:根据ID查询数据库的某条信息,然后将数据库中的信息同步到ES文档中
注:使用前要记得初始化RestHighLevelClient
//使用前要初始化client客户端
private RestHighLevelClient client = new RestHighLevelClient(RestClient.builder(
HttpHost.create("http://192.168.32.50:9200")
));
void testAddDocument() throws IOException {
//查询数据库
Hotel hotel = hotelService.getById(36934L);
//HotelDoc作用是将原始的经纬度以","拼接起来
HotelDoc hotelDoc = new HotelDoc(hotel);
//新增文档要指定文档id(String类型)
IndexRequest request = new IndexRequest("hotel").id(hotelDoc.getId().toString());
//使用JSON工具将对象转为String
request.source(JSONObject.toJSONString(hotelDoc), XContentType.JSON);
//index()表示新增文档,indices()表示操作索引
client.index(request, RequestOptions.DEFAULT);
}
8.7.2 删除文档
@Test
void testDeleteDocument() throws IOException {
DeleteRequest request = new DeleteRequest("hotel", "36934");
//delete()方法
client.delete(request, RequestOptions.DEFAULT);
}
8.7.3 修改文档
8.7.4 查询文档
8.7.5 批量导入文档
@Test
void testBulkDocument() throws IOException {
//1. 查到数据库中所有内容
List<Hotel> hotels = hotelService.list();
//2. 创建bulk请求
BulkRequest request = new BulkRequest();
//3. 遍历list,add到bulk的request中
for (Hotel hotel : hotels) {
request.add(new IndexRequest("hotel")
.id(hotel.getId().toString())
.source(JSONObject.toJSONString(hotel), XContentType.JSON)
);
}
//4. 发起bulk请求
client.bulk(request, RequestOptions.DEFAULT);
}
注意:在request.add()方法中,可以用IndexRequest做批量添加,也可以用DeleteRequest做批量删除,以及批量更新。
8.8 DSL查询文档
8.8.1 DSL Query分类
官方文档
以上查询的基本语法:
GET /indexName/_search
{
"query": {
"查询类型(上图)": {
"查询条件": "条件值"
}
}
8.8.2 全文检索查询
match
:根据一个字段查;
建议在创建索引时定义copy_to
将多个字段copy到某个字段中,查询的时候指定这个字段就可以间接查多个字段,提升性能。multi_match
:根据多个字段查,字段越多性能越差。
8.8.3 精确查询
一般是查找keyword、数值、日期、boolean等不分词的字段。所以不会对搜索条件分词。
使用案例
:在搜索酒店时,可以使用term
限定搜索城市;可以使用range
限定价格范围(range也可以限定时间)。
8.8.4 地理坐标查询
8.8.5 自定义排名靠前查询
主要功能是在原有查询内容的基础上,人为干预排名。
首先了解一下ES对于搜索结果相关性的算分:
如何自定义搜索结果算分?(人为将某些内容分数提高)
更多用法参考官方文档
8.8.6 复合查询(Boolean Query)
布尔查询是一个或多个查询子句的组合。组合方式有以下四种:
must
:必须匹配每个子查询,类似”与“。should
:选择性匹配子查询,类似”或“。must_not
:必须不匹配(取反的意思),不参与算分,类似”非“。filter
:必须匹配,但不参与算分。
不参与算分就意味着不参与排序。
案例如下:
GET /hotel/_search
{
"query": {
"bool": {
"must": [
{
"match": {
"name": "如家"
}
}
],
"must_not": [
{
"range": {
"price": {
"gt": 400
}
}
}
],
"filter": [
{
"geo_distance": {
"distance": "10km",
"location": {
"lat": 31.21,
"lon": 121.5
}
}
}
]
}
}
}
8.9 搜索结果处理
对搜索结果再次进行处理
8.9.1 排序
默认使用ES的打分进行排序,如果自定义排序字段,则不会再使用ES的打分来排序。
常用排序字段类型:数值型、日期型、keyword类型、地理坐标类型。
eg-1
:对酒店数据按照用户评价降序排序,评价相同按照价格升序排序
GET /hotel/_search
{
"query": {
"match_all": {}
},
"sort": [
{
"score": {
"order": "desc"
},
"price": {
"order": "asc"
}
}
]
}
eg-2
:实现对酒店数据按照到你的位置坐标(22.58, 113.97)的距离升序排序
GET /hotel/_search
{
"query": {
"match_all": {}
},
"sort": [
{
"_geo_distance": {
"location": "22.58, 113.97",
"order": "asc",
"unit": "km"
}
}
]
}
8.9.2 分页
ES默认情况下只返回TOP10的数据,如果要获取更多数据,就需要修改分页参数。
ES通过from
、size
来控制分页返回结果。
深度分页导致的问题
深度分页的两种解决方案
search after
:分页时先进行排序,原理是记住上一页最后一个数据的某个特征,查询下一页数据时通排序结果就可以找到第二页的第一个数据是哪个(缺点:只能向后翻页,不能向前翻页)。官方推荐使用的方式。scroll
:原理是将排序数据形成快照,保存在内存,可以进行前后翻页(缺点:内存消耗太大,另一个因为使用了快照,不保证数据一致性)。官方不推荐使用。
总结
8.9.3 高亮
作用:在搜索结果中,将搜索关键字突出显示。(比如百度搜索某个字,搜索结果就会高亮显示这个字)
原理:
- 将搜索结果中的关键字用标签标记出来。
- 在页面中给标签添加css样式。
语法:
案例如下
整个小节的总结
8.10 RestClient查询文档
整体思路:先在hibana中把需要查询的内容写出来,然后截图去代码里对照写
8.10.1 快速入门
以match_all
为例
8.10.2 全文检索查询
快捷键:
Ctrl + Alt + M 选中一段代码,快速抽取成方法。
match
代码示例:
@Test
void testMatch() throws IOException {
SearchRequest request = new SearchRequest("hotel");
request.source()
.query(QueryBuilders.matchQuery("all", "如家"));
SearchResponse response = client.search(request, RequestOptions.DEFAULT);
query
代码示例:
@Test
void testMultiMatch() throws IOException {
SearchRequest request = new SearchRequest("hotel");
//只有.query()方法内容的差别
request.source()
.query(QueryBuilders.multiMatchQuery(
"如家",
"brand", "name"));
SearchResponse response = client.search(request, RequestOptions.DEFAULT);
}
8.10.3 精确查询
term
代码示例:
@Test
void testTerm() throws IOException {
SearchRequest request = new SearchRequest("hotel");
//只有.query()方法内容的差别
request.source()
.query(QueryBuilders.termQuery("city", "上海"));
SearchResponse response = client.search(request, RequestOptions.DEFAULT);
}
range
代码示例:
@Test
void testRange() throws IOException {
SearchRequest request = new SearchRequest("hotel");
//只有.query()方法内容的差别
request.source()
//指定区间时,采用链式编程
.query(QueryBuilders.rangeQuery("price").gte(100).lte(150));
SearchResponse response = client.search(request, RequestOptions.DEFAULT);
}
8.10.4 复合查询
bool
代码示例:
@Test
void testBooleanSearch() throws IOException {
SearchRequest request = new SearchRequest("hotel");
//只有.query()方法内容的差别
request.source()
.query(QueryBuilders.boolQuery()
.must(QueryBuilders.termQuery("city", "上海"))
.filter(QueryBuilders.rangeQuery("price").lte(250))
);
SearchResponse response = client.search(request, RequestOptions.DEFAULT);
handleResponse(response);
}
对于搜索结果response
的处理
private void handleResponse(SearchResponse response) {
//对response结果进行处理
SearchHits searchHits = response.getHits();
//1. 总共查到多少条
TotalHits totalHits = searchHits.getTotalHits();
System.out.println("总共查到:" + totalHits.value + "条结果");
//2. TOP10结果
System.out.println("====================");
SearchHit[] hits = searchHits.getHits();
//3. 遍历结果,获取每个地址值
for (SearchHit hit : hits) {
String sourceStr = hit.getSourceAsString();
System.out.println(sourceStr);
}
}
8.10.5 搜索结果处理(排序、分页、高亮)
from
、size
、sort
代码示例:
@Test
void testPagingAndSort() throws IOException {
SearchRequest request = new SearchRequest("hotel");
//查询所有 + 显示第一页,每页显示5条 + 排序
request.source()
.query(QueryBuilders.matchAllQuery())
.from(0).size(5)
.sort("price", SortOrder.DESC);
SearchResponse response = client.search(request, RequestOptions.DEFAULT);
}
sort
根据坐标位置排序
highlight
高亮代码示例:
@Test
void testHighLight() throws IOException {
SearchRequest request = new SearchRequest("hotel");
request.source()
.query(QueryBuilders.matchQuery("all", "如家"))
.highlighter(new HighlightBuilder().field("name").requireFieldMatch(false));
SearchResponse response = client.search(request, RequestOptions.DEFAULT);
handleHighLightResponse(response);
}
高亮response
处理示例:
@Test
void handleHighLightResponse(SearchResponse response) {
//对response结果进行处理
SearchHits searchHits = response.getHits();
//1. 总共查到多少条
TotalHits totalHits = searchHits.getTotalHits();
System.out.println("总共查到:" + totalHits.value + "条结果");
//2. TOP10结果
System.out.println("====================");
SearchHit[] hits = searchHits.getHits();
//3. 遍历结果,获取每个地址值
for (SearchHit hit : hits) {
//3.1 获取source
HotelDoc hotelDoc = JSONObject.parseObject(hit.getSourceAsString(), HotelDoc.class);
//3.2 获取高亮内容
Map<String, HighlightField> highlightFields = hit.getHighlightFields();
//3.3 将source里"name"的值替换为高亮里"name"的值
//.isEmpty()等价于判断 ==null || size()==0
if (!CollectionUtils.isEmpty(highlightFields)) {
HighlightField highlightField = highlightFields.get("name");
if (highlightField != null) {
String name = highlightField.getFragments()[0].toString();
//替换source里"name"值为高亮值
hotelDoc.setName(name);
}
}
System.out.println(hotelDoc);
}
}
8.11 案例综合练习
注意先将RestClient交给Spring管理。
创建一个RestClientConfig
类,使用@Bean
管理
@Component
public class RestClientConfig {
@Bean
public RestHighLevelClient client() {
return new RestHighLevelClient(RestClient.builder(HttpHost.create("http://192.168.32.50:9200")));
}
}
8.11.1 酒店搜索和分页
- 实体类参考
RequestParam
。 - controller参考
HotelController
中的list()
方法。
8.11.2 酒店结果过滤
kibana
实现:
8.11.3 周边的酒店
8.11.4 酒店竞价排名
8.12 ES进阶
8.12.1 数据聚合
对文档数据进行统计、分析、运算。
聚合总类
DSL实现聚合
Bucket:
默认情况下,Bucket
聚合是对索引库的所有文档做聚合,我们可以添加query
条件限定要聚合的文档范围。
Metric:对Bucket
的聚合结果进行统计分析(类似于先进行Group By再对分组做count()、avg()等)
RestAPI实现聚合
8.12.2 自动补全
用户输入内容时,自动猜测补全后面的内容
8.12.3 数据同步
MySQL和ES数据同步问题