官网学习文档:https://www.elastic.co/guide/en/elasticsearch/reference/7.x/index.html
Elasticsearch 是一个分布式可扩展的实时搜索和分析引擎,一个建立在全文搜索引擎 Apache Lucene™ 基础上的搜索引擎.当然 Elasticsearch 并不仅仅是 Lucene 那么简单:
分布式实时文件存储,并将每一个字段都编入索引,使其可以被搜索。
实时分析的分布式搜索引擎。
可以扩展到上百台服务器,处理PB级别的结构化或非结构化数据。
动词,相当于mysql中的insert
名字,相当于mysql中的database
在index中,可以定义一个或多个类型
类似于mysql中的table,每一种类型的数据放在一起
保存在某个索引(index)下,某种类型(Type)的一个数据,文档是JSON格式的,Document就像是mysql中的某个table里面的内容
简单总结下:Index—>数据库;Type—>表;Document—>数据
分词器
# 使用docker安装
[root@pihao ~] docker pull elasticsearch:7.4.2
# 可视化检索数据
[root@pihao ~] docker pull kibana:7.4.2
# 宿主机用于存放配置文件以及数据文件
[root@pihao ~] mkdir -p /mydata/elasticsearch/config
[root@pihao ~] mkdir -p /mydata/elasticsearch/data
[root@pihao ~] chmod -R 777 /mydata/elasticsearch/ # 给data文件授权,不然后面挂载会失败
# 设置任何ip都能访问
[root@pihao ~] echo "http.host: 0.0.0.0" >> /mydata/elasticsearch/config/elasticsearch.yml
# 启动容器 9200:http请求通信端口;9300:分布式集群之间用于通信的端口
[root@pihao ~] docker run --name elasticsearch -p 9200:9200 -p 9300:9300 \
-e "discovery.type=single-node" \
-e ES_JAVA_OPTS="-Xms64m -Xmx128m" \
-v /mydata/elasticsearch/config/elasticsearch.yml:/usr/share/elasticsearch/config/elasticsearch.yml \
-v /mydata/elasticsearch/data:/usr/share/elasticsearch/data \
-v //mydata/elasticsearch/plugins:/usr/share/elasticsearch/plugins \
-d elasticsearch:7.4.2
# 查看容器的启动日志 (测试启动成功!)
[root@pihao ~] docker logs xxxx
# 或者使用地址访问 http:主机ip:9200,返回结果如下,说明启动成功
{
"name" : "a1b49510002c",
"cluster_name" : "elasticsearch",
"cluster_uuid" : "i-8c_jowQjWmurRdQ_E94Q",
"version" : {
"number" : "7.4.2",
"build_flavor" : "default",
"build_type" : "docker",
"build_hash" : "2f90bbf7b93631e52bafb59b3b049cb44ec25e96",
"build_date" : "2019-10-28T20:40:44.881551Z",
"build_snapshot" : false,
"lucene_version" : "8.2.0",
"minimum_wire_compatibility_version" : "6.8.0",
"minimum_index_compatibility_version" : "6.0.0-beta1"
},
"tagline" : "You Know, for Search" # You Know, for Search
}
[root@pihao ~] docker run --name kibana -e ELASTICSEARCH_HOSTS=http://112.74.167.52:9200 \
-p 5601:5601 -d kibana:7.4.2
启动成功后通过 http://你主机ip:5601 直接访问kibana的web页面,如出现下图则表示启动成功!
用于查询es的一些信息
保存一个数据,保存在哪个索引的哪个类型下,指定用哪个唯一标识
# 表示在customer这个索引下的external类型下的1号数据
{
"name":"pihao"
}
# 这是返回响应的内容:带"_"的表示元数据
{
"_index": "customer", # 哪个index
"_type": "external", # 哪个类型
"_id": "1", # 唯一编号
"_version": 1, # 版本信息,会叠加类似git
"result": "created", # created 新建;update 更新
"_shards": { # 分片
"total": 2,
"successful": 1,
"failed": 0
},
"_seq_no": 0,
"_primary_term": 1
}
post也可用于新增,如果不指定id,就会自动生成id,指定id就会修改这个数据,并新增版本号
post可以新增可以修改,put也可新增修改,但是put必须指定id,如果不指定就会报错
{
"_index": "customer",
"_type": "external",
"_id": "1", # 记录id
"_version": 2,
"_seq_no": 5, # 并发控制字段,每次更新就会+1,用来做乐观锁
"_primary_term": 1, # 同上,主分片重新分配,如重启就会变化
"found": true, # 表示找到了
"_source": { # 数据的内容
"name": "pihao"
}
}
# 乐观锁修改携带,表示只有在seq_no=0并且primary_term=1的情况下才会更改
PUT /customer/external/1?if_seq_no=0&if_primary_term=1
### Body 略
POST /customer/external/1/_update
与之前的差不多,但是这种方式的更新会与原来的值进行对比,_version不会叠加
# /_update这种方式必须要带上 "doc"
{
"doc":{
"name":"pihao"
}
}
# 如果该次更改的值不发生变化,那么"result"的值就是noop,表示no opearation, _version以及_seq_no也不会改变
{
"_index": "customer",
"_type": "external",
"_id": "1",
"_version": 2,
"result": "noop", # 没有修改
"_shards": {
"total": 0,
"successful": 0,
"failed": 0
},
"_seq_no": 5,
"_primary_term": 1
}
注意:es中暂时没有提供删除类型的操作!
往es中批量导入数据
# 请求体的格式,这个并不是json的格式,不能在postman中测试
{"操作类型":{"元数据"}}\n # 第一行
{"请求体"}\n # 第二行
#例如
{"index":{"_id":"1"}}
{"name":"pihao"}
{"index":{"_id":"2"}}
{"name":"haoge"}
每个执行的动作都是独立的,如果其中某个单一的动作失败了,不会影响到其他的动作
# _bulk与上面的请求不一致,没有指定index以及type
# 例 复杂示例
{"delete":{"_index":"website","_type":"blog","_id":"123"}} # 删除操作
{"create":{"_index":"website","_type":"blog","_id":"123"}} # 创建
{"title":"my first blog website"}
{"index":{"_index":"website","_type":"blog"}} # 创建
{"title":"my second blog website"}
{"update":{"_index":"website","_type":"blog","_id":"123"}} # 更新
{"doc":{"title":"my update bolg website"}}
在之前安装好的kibana中测试
数据导入
测试数据下载地址:https://download.elastic.co/demos/kibana/gettingstarted/accounts.zip
拿到数据后在kibana测试导入(ctrl+home回到页面最顶处)
{"index":{"_id":"1"}}
{"account_number":1,"balance":39225,"firstname":"Amber","lastname":"Duke","age":32,"gender":"M","address":"880 Holmes Lane","employer":"Pyrami","email":"[email protected]","city":"Brogan","state":"IL"}
## 后续数据省略...
es支持两种基本方式检索:
一个是通过使用 REST request URL 发送搜索参数(url+检索参数)
另一个是通过使用 REST reqeust body 来发送检索参数
GET /bank/_search 检索bank下的所有信息,包括docs和type
GET /bank/_search?q=*&sort=account_number:asc 请求参数方式检索
检索条件在url中
查询到1000条,默认只返回10条,类似分页查询
检索条件在请求体中
该种检索方式用的多
GET /bank/_search
{
"query":{
"match_all":{}
},
"sort":[
{
"account_number":{
"order":"desc"
}
}
]
}
Elasticsearch提供了要给可以执行查询的Json风格的DSL(domain-specific-language 领域特定语言)。这个被称为Query DSL。该查询语言非常全面
# query_name有:query,form,sort,post_filter等等
query_name:{
argument:value,
argument:value...
}
GET /bank/_search
{
query_name:{
field_name:{
argument:value,
argument:value...
}
}
}
# 分页查询 查询5条数据
GET /bank/_search
{
"query": {
"match_all": {}
},
"sort": [
{
"account_number": "asc"
}
],
"from": 0,
"size": 5
}
类似mysql中的select 字段名 而不是select *
GET /bank/_search
{
"query": {
"match_all": {}
},
"sort": [
{
"account_number": "asc"
}
],
"from": 0,
"size": 5,
"_source":["balance","firstname"] # 指定需要返回的字段
}
GET /bank/_search
{
"query":{
"match":{
"account_number":"20" # 这里也可以是数字20
}
}
}
# match 返回account_number=20的
GET /bank/_search
{
"query":{
"match":{
"address":"mill"
}
}
}
# 返回address字段中包含"mill"的所有记录,全文检索+模糊匹配
# 全文检索按照评分经行排序,会对检索条件进行分词匹配
将需要匹配的值当成一个整体单词(不分词进行检索)
GET /bank/_search
{
"query":{
"match_phrase":{
"address":"mill road"
}
}
}
# 返回address字段中包含"mill road"的所有记录,并给出相关性得分
GET /bank/_search
{
"query":{
"multi_match":{
"query":"mill",
"fields":["state","address"]
}
}
}
## 返回"state"或者"address"字段中包含有"mill"的所有记录
在复合查询中,用的比较多的有: must,must_not,should,filter
GET /bank/_search
{
"query":{
"bool":{ # 复合查询
"must":[
{
"match":{
"age":"40"
}
}
],
"must_not":[
{
"match":{
"gender":"F"
}
}
],
"should": [ # 表示应该,有最好,没有也没关系,加分项
{
"match": {
"firstname": "Cameron"
}
}
]
}
}
}
# 返回age=20并且gender!=F的所有数据
filter的作用和must的作用差不多,但是使用filter不会维护score得分
GET /bank/_search
{
"query": {
"bool": {
"filter": {
"range": {
"age": {
"gte": 18,
"lte": 30
}
}
}
}
}
}
# 返回age在18-30范围的数据
GET /bank/_search
{
"query": {
"bool": {
"must": [ # 使用must与上面的效果一样,不同的是会维护score
{
"range": {
"age": {
"gte": 18,
"lte": 30
}
}
}
]
}
}
}
和match一样,匹配某个属性的值。全文检索字段用match,其他非text字段匹配用tern
GET /bank/_search
{
"query":{
"term":{
"age":28
}
}
}
# 记住:查询非文本字段使用term语句,文本的就是用match
来看一个现象 match_phrase和 .keyword的区别
聚合提供了从数据中分组和提取数据的能力,最简单的聚合方法大致等于 sql group by 和sql的聚合函数,在ElasticSearch中,你可以执行查询和多个聚合,并且在一次使用中得到各自的返回结果,使用一次简洁和简化的API来避免网络往返
## 搜索address中包含有mill的所有人的年龄分布以及平均年龄
GET /bank/_search
{
"query":{
"match":{
"address":"mill"
}
},
"aggs":{ # 表示聚合
"ageAgg":{ # 第一个聚合,取个名字
"terms":{ # 聚合类型
"field":"age",
"order": { "_count": "asc" } # 按数量排序
"size":10 # 只看10个
}
},
"ageAvg":{ # 第二个聚合,取个名字
"avg":{ # 聚合类型
"field":"age"
}
}
},
"size":0 # 如果不想看具体的击中数据可以加size限定
}
按照年龄聚合,并且请求这些年龄段的这些人的平均薪资
# 第一步,先查出年龄分布
GET /bank/_search
{
"query":{
"match_all":{}
},
"aggs":{
"ageAgg":{
"terms":{
"field":"age",
"size":100
}
}
}
}
# 然后还要查询出里面每个年龄的平均工资,那么就需要进行子聚合
# 第二部 继续进行子聚合
GET /bank/_search
{
"query":{
"match_all":{}
},
"aggs":{
"ageAgg":{
"terms":{
"field":"age",
"size":100
},
"aggs":{ # 在ageAgg聚合中继续聚合
"ageAvg":{
"avg":{
"field":"balance" # 求薪资的平均值
}
}
}
}
}
}
查出所有年龄分布,并且这些年龄段中M的平均新增和F的平均薪资以及这个年龄段的总体平均薪资
GET /bank/_search
{
"query":{
"match_all":{}
},
"aggs":{
"ageAgg":{
"terms":{ # 先找出所有年龄的分布:数量就是每个年龄的数量 ageCount
"field":"age",
"size":100
},
"aggs":{
"ageBalanceAvg":{ # 所有年龄分布的总平均薪资
"avg":{
"field":"balance"
}
},
"genderAgg":{
"terms":{ # 对每个年龄按性别再次分布:数量就是:ageCount x 2
"field":"gender.keyword"
},
"aggs":{
"balanceAvg":{
"avg":{
"field":"balance"
}
}
}
}
}
}
}
}
Maping是用来定义一个文档document,以及它所包含的属性field是如何存储和索引的,比如
查看mapping信息
GET /bank/_mapping
PUT /my_index # 发送put请求
{
"mappings":{
"properties":{
"age":{"type":"integer"},
"email":{"type":"keyword"},
"name":{"type":"text"}
}
}
}
### 返回结果
{
"acknowledged" : true,
"shards_acknowledged" : true,
"index" : "my_index"
}
PUT /my_index/_mapping
{
"properties":{
"employee-id":{ # 新增加一个employee-id的字段,类型为keyword精确匹配
"type":"keyword",
"index":false # 不需要被索引,默认的是所有的index都为true
}
}
}
## 返回结果
{
"acknowledged" : true
}
对于已经存在的映射字段,我们不能更新。这就好比你mysql数据库中已经有数据了,你突然给我说有个字段定义的类型错了。尼玛的揍不死你!所以这样的操作不允许的,必须要创建新的索引进行数据迁移
先创建出 newbank 的正确映射。然后使用如下方式经行数据迁移
# 创建新索引并重新指定映射类型
PUT /newbank
{
"mappings":{
"properties":{
"account_number":{"type":"long"},
"address":{"type":"text"},
"age":{"type":"integer"},
"balance":{"type":"long"},
"city":{"type": "keyword"},
"email":{"type": "keyword"},
"employer":{"type": "keyword"},
"firstname":{"type": "text"},
"gender":{"type": "keyword"},
"lastname":{"type": "keyword"},
"state":{"type": "keyword"}
}
}
}
### 结果返回
{
"acknowledged" : true,
"shards_acknowledged" : true,
"index" : "newbank"
}
## 导入数据
POST /_reindex
{
"source":{
"index":"bank",
"type":"account" # 老版本导入数据时指定这个type,版本7之后舍弃了type这个概念(测试:可以不用加type)
},
"dest":{
"index":"newbank"
}
}
## ok
一个tokenizer(分词器)接收一个字符流,将之分割为独立的tokens(词元,通常是独立的单词),然后输出tokens流
参考官方文档Analyzers章节 https://www.elastic.co/guide/en/elasticsearch/reference/7.5/analysis-standard-analyzer.html
# 测试
POST _analyze
{
"analyzer": "standard",
"text": "张坤你要挺住"
}
## 发现返回一个一个的汉字,这就是默认分词器的效果,所以我们需要下载我们自己的分词器,这样就能识别中文的短语
{
"tokens" : [
{
"token" : "张",
"start_offset" : 0,
"end_offset" : 1,
"type" : "" ,
"position" : 0
},
{
"token" : "坤",
"start_offset" : 1,
"end_offset" : 2,
"type" : "" ,
"position" : 1
}]
}
注意:不能使用默认的 elasticsearch-plugin install xxx.zip 进行安装
下载地址: https://github.com/medcl/elasticsearch-analysis-ik/releases 下载对应版本
# 将下载好的ik分词器安装包放置在docker的外部挂载文件中
[root@pihao plugins]# pwd
/mydata/elasticsearch/plugins
[root@pihao plugins]# ls
elasticsearch-analysis-ik-7.4.2.zip # 使用unzip解压出来
[root@pihao plugins]# unzip elasticsearch-analysis-ik-7.4.2.zip -d 指定的目录
# 紧接着查看docker容器中的elasticsearch,看看ik分词器有没有安装好
[root@pihao plugins]# docker ps
CONTAINER ID IMAGE COMMAND CREATED STATUS
feb7cdd7b777 kibana:7.4.2 "/usr/local/bin/dumb…" 45 hours ago Up 45 hours
a1b49510002c elasticsearch:7.4.2 "/usr/local/bin/dock…" 46 hours ago Up 46 hours
[root@pihao plugins]# docker exec -it a1b49510002c /bin/bash
[root@a1b49510002c elasticsearch]# cd plugins/
[root@a1b49510002c plugins]# ls
ik7.4.2 # 解压安装的ik分词器
[root@a1b49510002c plugins]# cd ..
[root@a1b49510002c elasticsearch]# cd bin/
[root@a1b49510002c bin]# elasticsearch-plugin list 查看已安装的插件
ik7.4.2 # 显示已安装了ik
[root@a1b49510002c bin]# 安装成功
测试之前记得重启docker elasticsearch 容器
# analyzer 指定为 ik_smart \ ik_max_word
由于安装解压的词库还不能满足我们日常网络用于所需,所以我们需要导入新的词库
为了演示,我们需要启动一个能访问的应用tomcat或者nginx都行,在这里我就快速启动一个nginx吧,然后添加自己的词库
## 安装nginx
[root@pihao mydata]# docker run -p 80:80 --name nginx -d nginx:1.10
# 复制nginx容器中的配置文件
[root@pihao mydata]# ls
elasticsearch mysql nginx redis
[root@pihao mydata]# docker container cp nginx:/etc/nginx . 将容器中nginx的/etc/nginx下的文件复制
[root@pihao mydata]# ls
elasticsearch mysql nginx redis
[root@pihao mydata]# cd nginx/
[root@pihao nginx]# ls
conf.d fastcgi_params koi-utf koi-win mime.types modules nginx.conf scgi_params uwsgi_params
[root@pihao nginx]# 然后停掉nginx,把上述的nginx文件名字改为conf,然后再创建一个nginx的文件夹,把conf移入nginx上面启动一个nginx的容器主要是为了获得里面的配置文件。不然启动的时候没法挂载配置文件
# 正式启动nginx
[root@pihao nginx]# docker run -p 80:80 --name nginx \
-v /mydata/nginx/html:/usr/share/nginx/html \
-v /mydata/nginx/logs:/var/log/nginx \
-v /mydata/nginx/conf:/etc/nginx \
-d nginx:1.10
[root@pihao nginx]# 启动成功,在nginx/html里面写一个index.html文件 hello nginx
启动测试成功!
添加自定义的分词
## 在nginx的html配置下新建es文件夹,创建fenci.txt文件再添加如下测试内容
[root@pihao es]# pwd
/mydata/nginx/html/es
[root@pihao es]# ls
fenci.txt
[root@pihao es]# cat fenci.txt
张坤
菜嵩松
刘彦春
朱少醒
谢志宇
葛兰
[root@pihao es]# 测试访问 http://112.74.167.52/es/fenci.txt 测试无误 可以从elasticsearch中引入了!
修改/usr/share/elasticsearch/plugins/ik/config中的IKAnalyzer.cfg.xml
[root@pihao config]# pwd
/mydata/elasticsearch/plugins/ik7.4.2/config
[root@pihao config]# ls
extra_main.dic extra_single_word_full.dic extra_stopword.dic main.dic quantifier.dic suffix.dic
extra_single_word.dic extra_single_word_low_freq.dic IKAnalyzer.cfg.xml preposition.dic stopword.dic surname.dic
[root@pihao config]# vim IKAnalyzer.cfg.xml
保存退出后重启docker elasticsearch
测试成功
Elasticsearch-Rest-Client 官方提供的RestClient,封装了ES的操作,API层次分明,上手简单
官方地址:https://www.elastic.co/guide/en/elasticsearch/client/java-rest/current/java-rest-high.html
引入Maven依赖
<dependency>
<groupId>org.elasticsearch.client</groupId>
<artifactId>elasticsearch-rest-high-level-client</artifactId>
<version>7.11.1</version>
</dependency>
注入RestHighLevelClient
@Configuration
public class ElasticSearchConfig {
@Bean
public RestHighLevelClient esRestClient(){
RestHighLevelClient esRestClient = new RestHighLevelClient(
RestClient.builder(
new HttpHost("主机ip", 9200, "http")));
return esRestClient;
}
public static final RequestOptions COMMON_OPTIONS;
static {
RequestOptions.Builder builder = RequestOptions.DEFAULT.toBuilder();
// builder.addHeader("Authorization", "Bearer " + TOKEN); 需要的话就配置
// builder.setHttpAsyncResponseConsumerFactory(
// new HttpAsyncResponseConsumerFactory
// .HeapBufferedResponseConsumerFactory(30 * 1024 * 1024 * 1024));
COMMON_OPTIONS = builder.build();
}
}
测试保存数据
/**
* 测试存储数据
*/
@Test
public void indexData() throws IOException {
IndexRequest indexRequest = new IndexRequest("users");
indexRequest.id("1");
User user = new User();
user.setUserName("pihao");
user.setAge(20);
user.setGender("male");
String jsonString = JSON.toJSONString(user);
indexRequest.source(jsonString, XContentType.JSON); //直接传入json字符串,指定 application/json
//同步执行操作
IndexResponse index = client.index(indexRequest,ElasticSearchConfig.COMMON_OPTIONS);
//提取有用的响应数据
System.out.println(index.toString());
}
测试检索
@Test
public void searchData() throws IOException {
//创建检索请求
SearchRequest searchRequest = new SearchRequest();
//指定索引
searchRequest.indices("bank");
//指定DSL,检索条件
SearchSourceBuilder sourceBuilder = new SearchSourceBuilder();
//构造检索条件
sourceBuilder.query(QueryBuilders.matchQuery("address","mill"));
//按照年龄的值分布来聚合
TermsAggregationBuilder ageAgg = AggregationBuilders.terms("ageAgg").field("age").size(10);
sourceBuilder.aggregation(ageAgg);
//按照薪资聚合
AvgAggregationBuilder balanceAgg = AggregationBuilders.avg("balanceAgg").field("balance");
sourceBuilder.aggregation(balanceAgg);
// sourceBuilder.from();
// sourceBuilder.size();
System.out.println("检索条件:"+sourceBuilder.toString());
searchRequest.source(sourceBuilder);
//执行检索
SearchResponse searchResponse = client.search(searchRequest,ElasticSearchConfig.COMMON_OPTIONS);
//分析结果
SearchHits hits = searchResponse.getHits(); //外层最大的hits
SearchHit[] searchHits = hits.getHits(); //真正的记录
for (SearchHit hit :searchHits) {
/**
* "_index" : "bank","_type" : "acccount","_id" : "1","_score" : 1.0,"_source" 都有相应的get方法
*/
String jsonStr = hit.getSourceAsString(); //将返回的source转为json,最后肯定需要映射成java实体类
Account account = JSON.parseObject(jsonStr, Account.class);
System.out.println(account);
}
// 获取检索到的聚合信息
Aggregations aggregations = searchResponse.getAggregations();
Terms ageAgg1 = aggregations.get("ageAgg"); //这里的ageAgg是term类型的,所以可以转成Terms
List<? extends Terms.Bucket> buckets = ageAgg1.getBuckets();
for (Terms.Bucket bucket:buckets) {
String keyAsString = bucket.getKeyAsString(); //打印年龄种类
long docCount = bucket.getDocCount(); //打印每种年龄出现的频率
System.out.println("年龄: "+keyAsString+"=====> 频率: "+docCount);
}
Avg balanceAvg = aggregations.get("balanceAgg"); //balanceAgg是avg的类型
double value = balanceAvg.getValue();
System.out.println("平均薪资是: "+value);
// for (Aggregation aggregation:aggregations.asList()) { //可以直接如上面获取到具体的聚合
// System.out.println("当前聚合的名字:"+aggregation.getName());
// }
}
加油!!!