一、现存问题
1.1 现存问题
- 海量数据存储
- 海量数据中完成全文检索
- 如何实现关键字的高亮显示
- 海量数据中完成统计操作
1.2 ES的介绍(搜索引擎)
Elasticsearch天生分布式,支持海量数据的存储,什么在大数据领域也有应用。
Elasticsearch采用倒排索引的方式去全文检索数据,亿级数据中检索时间在毫秒级别。
Elasticsearch提供了highlight高亮查询方式
Elasticsearch提供了及其丰富和聚合函数
Elasticsearch基于Java实现,搜索功能是基于先进最流行的Lucene实现……
Elasticsearch本身是一套技术栈,ELK中的一个组件,ELK是收集日志的一套技术栈……
二、安装ES&Kibana&IK分词器
采用docker运行Elasticsearch容器和Kibana容器
2.1 安装ES&Kibana
docker-compose.yml
version: "3.1"
services:
elasticsearch:
image: 10.9.12.200:60001/elasticsearch:7.8.0
# restart: always # 只要docker启动,当前容器自动启动
container_name: elasticsearch
ports:
- 9200:9200
environment:
- "discovery.type=single-node"
- "ES_JAVA_OPTS=-Xms256m -Xmx256m"
volumes:
- ./data:/usr/share/elasticsearch/data
- ./config/elasticsearch.yml:/usr/share/elasticsearch/config/elasticsearch.yml
# 数据卷如果映射文件,需要提前在宿主机中创建好这个文件
kibana:
image: 10.9.12.200:60001/kibana:7.8.0
# restart: always
container_name: kibana
ports:
- 5601:5601
environment:
- elasticsearch_url=http://192.168.41.98:9200
depends_on:
- elasticsearch
volumes:
- ./config/kibana.yml:/usr/share/kibana/config/kibana.yml
数据卷映射文件的内容
elasticsearch.yml
cluster.name: "docker-cluster"
network.host: 0.0.0.0
kibana.yml
server.name: kibana
server.host: "0"
elasticsearch.hosts: [ "http://elasticsearch:9200" ]
xpack.monitoring.ui.container.elasticsearch.enabled: true
准备好上述内容后,docker-compose up -d
检测启动是否成功:
2.2 安装IK分词器
在kibana中基于RESTful的形式和ES交互
需要在kibana上发送HTTP请求并携带JSON参数与ES交互
在kibana的dev tools上测试分词器
安装IK分词器
需要将zip压缩包解压的内容放到elasticsearch容器内部的 /usr/share/elasticsearch/plugins/ik_analyzer/
修改docker-compose.yml文件,给elasticsearch添加了一个数据卷,映射到 /usr/local/docker/es_docker/plugins
将zip压缩包扔到Linux中,并且放到数据库内的ik_analyzer目录里,通过unzip解压,重启elasticsearch容器
再次通过IK分词器进行分词
三、ES的存储结构
在ES服务中创建索引,并指定索引的主分片个数,以及备份分片个数。
给索引设置存储数据的结构
上述搞定后,可以向索引中添加文件……
四、索引操作
4.1 创建索引(不指定存储结构)
# 创建索引
PUT /book
{
"settings": {
"number_of_shards": 5,
"number_of_replicas": 1
}
}
4.2 Elasticsearch数据类型
-
字符串类型:
- text:文本类型,一般用于搜索……
- keyword:关键字,当前值不允许分词……
-
boolean类型:
- boolean:没啥说的…………
-
二进制类型:
- binary:只支持encoding为Base64格式……
-
数值类型:
longA signed 64-bit integer with a minimum value of -263and a maximum value of263-1.integerA signed 32-bit integer with a minimum value of -231and a maximum value of231-1.shortA signed 16-bit integer with a minimum value of -32,768and a maximum value of32,767.byteA signed 8-bit integer with a minimum value of -128and a maximum value of127.doubleA double-precision 64-bit IEEE 754 floating point number, restricted to finite values. floatA single-precision 32-bit IEEE 754 floating point number, restricted to finite values. half_floatA half-precision 16-bit IEEE 754 floating point number, restricted to finite values. scaled_floatA floating point number that is backed by a long, scaled by a fixeddoublescaling factor.unsigned_longAn unsigned 64-bit integer with a minimum value of 0 and a maximum value of 264-1. -
时间类型:
-
date:代表时间类型,根据format设置格式化方式
"format": "yyyy-MM-dd || yyyy-MM-dd HH:mm:ss || strict_date_optional_time || epoch_millis"
-
-
ip类型:
- ip:支持ipv4和ipv6类型
-
geo类型:
- geo_point:支持经纬度存储……
4.3 创建索引(结构化存储)
# 创建索引并设置存储结构
PUT /novel
{
"settings": {
"number_of_shards": 5,
"number_of_replicas": 1
},
"mappings": {
"properties": {
"name": {
"type": "text", # 这样会使用默认的standard分词器,使用IK需要额外添加信息
"analyzer": "ik_max_word" # 添加这个才会使用IK分词器
},
"author": {
"type": "keyword"
},
"count": {
"type": "long"
},
"onsale": {
"type": "date",
"format": "yyyy-MM-dd || yyyy-MM-dd HH:mm:ss || strict_date_optional_time || epoch_millis"
}
}
}
}
创建成功,查看效果
索引构建成功后
-
主分片个数不允许修改……
-
备份分片个数可以随意修改……
-
索引中的属性类型是不允许修改的,但是可以追加属性……
PUT /索引/_mapping { "properties": { "field": { "type": "datatype" } } }
4.4 查看索引
可以通过图形化界面查看
REST查看
GET /索引
4.5 删除索引
可以通过图形化界面删除
REST删除
DELETE /索引
五、文档操作
5.1 添加文档
# 添加文档,指定id添加,自动生成id
POST /novel/_doc/ # 自动生成id
{
"name": "斗破苍穹",
"author": "天残土豆",
"count": 99999,
"onsale": "2000-01-01"
}
POST /novel/_create/1 # 手动设置id
{
"name": "斗罗大陆",
"author": "唐家三少",
"count": 999999,
"onsale": "2010-01-01"
}
5.2 修改文档
# 修改文档
POST /novel/_update/1
{
"doc": {
"name": "倒斗大陆"
}
}
5.3 删除文档
# 删除文档
DELETE /novel/_doc/sXVVyHsB4k1tdHs36v4c
5.4 根据id查询文档
# 根据_id查询文档
GET /novel/_doc/1
六、Java操作ES
Elasticsearch官方推出了两种Java操作的客户端,采用Rest-High-Level-Client。
还有另一种与Elasticsearch交互客户端,可以写类似SQL的语句,与Elasticsearch交互。
还有另一种与Elasticsearch交互客户端,SpringBoot整合的data-elasticsearch。
6.1 Java连接Elasticsearch服务
6.1.1 导入依赖
<dependencies>
<dependency>
<groupId>org.elasticsearch</groupId>
<artifactId>elasticsearch</artifactId>
<version>7.8.0</version>
</dependency>
<dependency>
<groupId>org.elasticsearch.client</groupId>
<artifactId>elasticsearch-rest-high-level-client</artifactId>
<version>7.8.0</version>
</dependency>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.12</version>
</dependency>
</dependencies>
6.1.2 编写配置类
public class ESClientUtil {
/**
* 获取与ES交互的client对象
* @return
*/
public static RestHighLevelClient getClient(){
HttpHost httpHost = new HttpHost("192.168.41.98",9200);
RestClientBuilder restClientBuilder = RestClient.builder(httpHost);
RestHighLevelClient restHighLevelClient = new RestHighLevelClient(restClientBuilder);
return restHighLevelClient;
}
}
6.2 添加文档
private final String CREATED = "created";
@Test
public void addDoc() throws IOException {
//1. 创建指定的request对象
IndexRequest indexRequest = new IndexRequest();
//2. 设置索引信息,文档id
indexRequest.index("novel");
indexRequest.id("3");
//3. 准备具体文档数据
Map source = new HashMap<>(8);
source.put("name","明朝");
source.put("author","朱元璋");
source.put("count",88888);
// 时间推荐使用年月日时分秒格式,虽然kibana显示有问题,但是查询结果没问题
source.put("onsale",new Date());
indexRequest.source(source);
//4. 将request用RestHighLevelClient发送出去,接收ES服务的响应
IndexResponse resp = ESClientUtil.getClient().index(indexRequest, RequestOptions.DEFAULT);
//5. 基于响应结果,判断添加是否成功
if (CREATED.equals(resp.getResult().getLowercase())) {
System.out.println("文档添加成功!!!");
}
}
6.3 修改文档
@Test
public void updateById() throws IOException {
//1. 创建指定的request对象
UpdateRequest request = new UpdateRequest();
//2. 设置索引信息,文档id
request.index("novel");
request.id("3");
//3. 准备具体文档数据
Map<String, Object> source = new HashMap<>(4);
source.put("name","明朝那么多事!");
request.doc(source);
//4. 将request用RestHighLevelClient发送出去,接收ES服务的响应
UpdateResponse resp = ESClientUtil.getClient().update(request, RequestOptions.DEFAULT);
//5. 基于响应结果,判断操作是否成功
if (UPDATED.equals(resp.getResult().getLowercase())) {
System.out.println("文档修改成功!");
}
}
6.5 批量操作
6.6 根据id查询文档
@Test
public void get() throws IOException {
//1. 创建指定的request对象
GetRequest request = new GetRequest();
//2. 设置索引信息,文档id
request.index("novel");
request.id("3");
//3. 将request用RestHighLevelClient发送出去,接收ES服务的响应
GetResponse resp = ESClientUtil.getClient().get(request, RequestOptions.DEFAULT);
//4. 基于响应结果,判断操作是否成功
System.out.println(resp.getSourceAsMap());
}
七、ES的基本查询
7.1 倒排/反向索引
7.2 term查询
term是ES最基本的查询,基本上所有的检索方式的底层都是term查询……
7.2.1 term查询
term查询对比MySQL的话,相当于:where column = ?
term就是将用户输入的关键字与ES中的某一个属性做等值比较(当前属性的分词库)。
term不会将用户输入的关键字进行分词,直接拿用户的完整关键字匹配分词库。
term更适合查询keyword类型的属性
# term查询
POST /sms_logs_index/_search
{
"query": {
"term": {
"smsContent": {
"value": "滴滴单车平台"
}
}
}
}
7.2.2 terms查询
term查询对比MySQL的话,相当于:where column in (?,?,?)
和terms一致,让用户输入多个关键字去匹配一个属性
[[terms查询]]
POST /sms_logs_index/_search
{
"query": {
"terms": {
"smsContent": [
"滴滴打车",
"平台"
]
}
}
}
7.3 match查询
match查询是使用频率最高的查询方式,match查询的底层还是term查询
match查询会根据查询的field的属性,决定是否将用户输入的关键字进行分词
- 如果field是keyword类型,match查询不会将用户输入的关键字进行分词
- 如果field是text类型,match查询会将用户输入的关键字进行分词
7.3.1 match查询
用户输入一个关键字去匹配一个Field
# match查询
POST /sms_logs_index/_search
{
"query": {
"match": {
"smsContent": "【招商银行】尊贵的王五先生"
}
}
}
7.3.2 match_all查询
查询全部数据
# match_all 查询
POST /sms_logs_index/_search
{
"query": {
"match_all": {}
},
"from": 10, # limit的第一个参数
"size": 10, # limit的第二个参数
"sort": [ # 指定根据哪个field做排序,不根据es的分数进行排序……
{
"fee": {
"order": "asc"
}
}
]
}
7.3.3 multi_match查询
一个值匹配多个Field
提升ES的查询命中率……
# multi_match
POST /sms_logs_index/_search
{
"query": {
"multi_match": {
"query": "银行",
"fields": ["corpName","smsContent"]
}
}
}
7.4 range查询
range查询可以实现范围检索
针对数值,时间和IP地址做范围查询
# range 查询
# 数值范围
POST /sms_logs_index/_search
{
"query": {
"range": {
"fee": {
"gte": 1,
"lt": 5
}
}
}
}
# 时间范围, 时间格式规定好,推荐都用时分秒,skr~~
POST /sms_logs_index/_search
{
"query": {
"range": {
"sendDate": {
"gte": "2021-09-09 02:11:11",
"lte": "2021-09-09 05:59:11",
"format": "yyyy-MM-dd HH:mm:ss"
}
}
}
}
# ip范围
POST /sms_logs_index/_search
{
"query": {
"range": {
"ipAddr": {
"gt": "9.126.2.8",
"lte": "11.126.2.255"
}
}
}
}
7.5 Java与ES交互实现查询操作
public class Demo2Test {
/**
* range范围查询,基于fee,查询1分到5分之间的
*/
@Test
public void rangeQuery() throws IOException {
//1. req
SearchRequest request = new SearchRequest();
//2. index
request.indices("sms_logs_index");
//3. body
SearchSourceBuilder sourceBuilder = new SearchSourceBuilder();
sourceBuilder.query(QueryBuilders.rangeQuery("fee").gte(1).lte(5));
sourceBuilder.from(0);
sourceBuilder.size(2);
sourceBuilder.sort("fee", SortOrder.ASC);
request.source(sourceBuilder);
//4. execute
SearchResponse resp = ESClientUtil.getClient().search(request, RequestOptions.DEFAULT);
//5. source
for (SearchHit hit : resp.getHits().getHits()) {
System.out.println(hit.getSourceAsMap());
}
}
}
八、ES的其他查询
8.1 ids查询
ids是根据多个_id的值,直接拉取分片上的数据……
# ids查询
POST /sms_logs_index/_search
{
"query": {
"ids": {
"values": ["1","2","3"]
}
}
}
8.2 prefix查询
将用户输入的关键字去分词库中匹配term的前缀……
# prefix查询
POST /sms_logs_index/_search
{
"query": {
"prefix": {
"corpName": {
"value": "养车"
}
}
}
}
8.3 fuzzy查询
真正的模块查询,允许用户输入的关键字有错别字(错别字尽量出现在后面……)
# fuzzy查询
POST /sms_logs_index/_search
{
"query": {
"fuzzy": {
"corpName": {
"value": "盒马生鲜"
}
}
}
}
8.4 wildcard查询
和MySQL中的like查询一样的通配、占位查询,
- MySQL%代表通配,_代表占位
- wildcard中*代表通配,?代表占位
# wildcard查询
POST /sms_logs_index/_search
{
"query": {
"wildcard": {
"corpName": {
"value": "中国????????"
}
}
}
}
8.5 regexp查询
基于正则表达式匹配分词库中的term
# regexp查询
POST /sms_logs_index/_search
{
"query": {
"regexp": {
"mobile": "1390[0-9]{7}"
}
}
}
九、ES的复合查询
基于ES的query查询数据时,暂时只能一个条件一个条件的使用,没有办法将多个条件以一定的逻辑方式组合在一起。
ES也支持查询方式,允许多个条件封装到一起,这种查询叫bool查询
只要公司项目用ES做全文检索,100%用bool查询
bool查询提供了四种组合方式:
- must:等于MySQL的and
- should:等于MySQL的or
- must_not:等于MySQL的!
- filter:在query的筛选基础上,再次做筛选,这次筛选不会计算分数
# bool查询
# smsContent中包含先生 并且 fee大于等于5分
# 省份要么是北京,要么是上海
# 公司名称不是 滴滴打车
# 将上述结果再次筛选出手机号为 13900000000的
POST /sms_logs_index/_search
{
"query": {
"bool": {
"must": [
{
"match": {
"smsContent": "先生"
}
},
{
"range": {
"fee": {
"gte": 5
}
}
}
],
"should": [
{
"term": {
"province": {
"value": "北京"
}
}
},
{
"term": {
"province": {
"value": "西安"
}
}
}
],
"must_not": [
{
"term": {
"corpName": {
"value": "滴滴打车"
}
}
}
],
"filter": [
{
"term": {
"mobile": "13900000000"
}
}
],
"minimum_should_match": 0
}
}
}
十、ES的高亮查询
将用户输入的关键字匹配项,以高亮的形式返回。
# 高亮查询
POST /sms_logs_index/_search
{
"query": {
"term": {
"corpName": "滴滴打车"
}
},
"highlight": {
"pre_tags": "<span style='color:red;'>",
"post_tags": "</span>",
"fragment_size": 20,
"fields": [
{"corpName": {}}
]
}
}
Java代码
public class Demo2Test {
@Test
public void matchQuery() throws IOException {
//1. req
SearchRequest request = new SearchRequest();
//2. index
request.indices("sms_logs_index");
//3. body
SearchSourceBuilder sourceBuilder = new SearchSourceBuilder();
sourceBuilder.query(QueryBuilders.matchQuery("smsContent","先生"));
sourceBuilder.from(0);
sourceBuilder.size(2);
sourceBuilder.sort("fee", SortOrder.ASC);
HighlightBuilder highligter = new HighlightBuilder();
highligter.fragmentSize(20);
highligter.field("smsContent");
highligter.preTags("<span style='color:red;'>");
highligter.postTags("</span>");
sourceBuilder.highlighter(highligter);
request.source(sourceBuilder);
//4. execute
SearchResponse resp = ESClientUtil.getClient().search(request, RequestOptions.DEFAULT);
//5. source
for (SearchHit hit : resp.getHits().getHits()) {
System.out.println(hit.getSourceAsMap());
Map<String, HighlightField> highlightFields = hit.getHighlightFields();
HighlightField smsContent = highlightFields.get("smsContent");
if(smsContent != null){
String highlight = smsContent.getFragments()[0].toString();
System.out.println("highlight:" + highlight);
}
}
}
}
十一、ES的聚合函数ES
提供了丰富的聚合函数在海量数据中做统计
想MySQL提供的5种聚合函数,ES都支持……
ES提供的聚合函数特别多,挑几个常用的……
11.1 Extended_stats
查询出指定属性的count,min,max,sum,avg,平方和,方差,标准偏差…………
# 聚合函数-Extended stats
POST /sms_logs_index/_search
{
"aggs": {
"heiheihei": {
"extended_stats": {
"field": "fee"
}
}
}
}
11.2 Cardinality
针对非text类型的属性做 去重计数
# 聚合函数-Cardinality
POST /sms_logs_index/_search
{
"aggs": {
"xixixi": {
"cardinality": {
"field": "createDate"
}
}
}
}
11.3 Range统计
ES中针对Range范围统计提供了Range,DateRange,IPRange
可以统计范围内出现数据的数量。
# 聚合函数-Range
POST /sms_logs_index/_search
{
"aggs": {
"hahaha": {
"range": {
"field": "fee",
"ranges": [
{"to": 5},
{"from": 5,"to": 8},
{"from": 8}
]
}
}
}
}
# 聚合函数-DateRange
POST /sms_logs_index/_search
{
"aggs": {
"hehehe": {
"date_range": {
"field": "createDate",
"ranges": [
{"to": "2021-09-09 04:00:00"},
{"from": "2021-09-09 04:00:00","to": "2021-09-09 12:00:00"},
{"from": "2021-09-09 12:00:00"}
],
"format": "yyyy-MM-dd HH:mm:ss"
}
}
}
}
11.4 histogram
根据指定的属性和间隔interval做范围统计
# 聚合函数-histogram
POST /sms_logs_index/_search
{
"aggs": {
"eee": {
"histogram": {
"field": "fee",
"interval": 1
}
}
}
}
# 聚合函数-date_histogram
POST /sms_logs_index/_search
{
"aggs": {
"yesyesyes": {
"date_histogram": {
"field": "createDate",
"interval": "minute"
}
}
}
}
11.5 terms
统计某个属性不同值出现的次数,并且可以基于order做排序,基于size做筛选条数
# 聚合函数-terms
POST /sms_logs_index/_search
{
"aggs": {
"yiku": {
"terms": {
"field": "fee",
"size": 2,
"order": {
"_count": "asc"
}
}
}
}
}
11.6 Java代码实现
向下转型问题!
/**
* 聚合函数Java实现
* @author zjw
*/
public class Demo3Test {
@Test
public void terms() throws IOException {
// request
SearchRequest request = new SearchRequest();
// index
request.indices("sms_logs_index");
// body
SearchSourceBuilder sourceBuilder = new SearchSourceBuilder();
sourceBuilder.aggregation(AggregationBuilders.terms("agg").field("fee").size(10).order(BucketOrder.count(false)));
request.source(sourceBuilder);
// send,resp
SearchResponse resp = ESClientUtil.getClient().search(request, RequestOptions.DEFAULT);
// getData
Aggregations aggregations = resp.getAggregations();
// 根据聚合函数的查询条件向下转型
Terms agg = aggregations.get("agg");
for (Terms.Bucket bucket : agg.getBuckets()) {
System.out.println(bucket.getKey() +"," + bucket.getDocCount());
}
}
}
十二、ES的GEO查询
GEO查询就是基于经纬度做筛选。
一般经纬度筛选无法计算分数,会将基于GEO的查询统统滴放到filter中。
创建一个索引,指定一个属性的存储类型是geo_point
# 玩GEO,创建索引
PUT /map
{
"settings": {
"number_of_shards": 3,
"number_of_replicas": 1
},
"mappings": {
"properties": {
"name": {
"type": "keyword"
},
"location": {
"type": "geo_point"
}
}
}
}
# 插入三条数据 北科,生命科学园地铁,巩华城地铁
POST /map/_create/1
{
"name": "北科",
"location": {
"lat": 40.125318,
"lon": 116.258312
}
}
POST /map/_create/2
{
"name": "生命科学园地铁",
"location": {
"lat": 40.101109,
"lon": 116.300721
}
}
POST /map/_create/3
{
"name": "巩华城地铁",
"location": {
"lat": 40.13731,
"lon": 116.300344
}
}
GEO在ES中提供了三种查询方式
# 一个点,一个距离(半径)查询圆内数据
POST /map/_search
{
"query": {
"bool": {
"filter": [
{
"geo_distance": {
"distance": 4700,
"location": {
"lat": 40.125318,
"lon": 116.258312
}
}
}
]
}
}
}
# 两个点确定矩形
POST /map/_search
{
"query": {
"bool": {
"filter": [
{
"geo_bounding_box": {
"location": {
"top_left": {
"lat": 40.109973,
"lon": 116.278192
},
"bottom_right": {
"lat": 40.093416,
"lon": 116.32131
}
}
}
}
]
}
}
}
# 多个点确定多边形
POST /map/_search
{
"query": {
"bool": {
"filter": [
{
"geo_polygon": {
"location": {
"points": [
{
"lat": 40.143736,
"lon": 116.29084
},
{
"lat": 40.127132,
"lon": 116.306003
},
{
"lat": 40.148479,
"lon": 116.313549
}
]
}
}
}
]
}
}
}