一、什么是 Elasticsearch
Elasticsearch 是一个分布式的开源搜索和分析引擎,适用于所有类型的数据,包括文本、数字、地理空间、结构化和非结构化数据。Elasticsearch 在 Apache Lucene 的基础上开发而成,由 Elasticsearch N.V.(即现在的 Elastic)于 2010 年首次发布。Elasticsearch 以其简单的 REST 风格 API、分布式特性、速度和可扩展性而闻名,是 Elastic Stack 的核心组件;Elastic Stack 是适用于数据采集、充实、存储、分析和可视化的一组开源工具。人们通常将 Elastic Stack 称为 ELK Stack(代指 Elasticsearch、Logstash 和 Kibana),目前 Elastic Stack 包括一系列丰富的轻量型数据采集代理,这些代理统称为 Beats,可用来向 Elasticsearch 发送数据。
二:ES用途主要在哪些方面
ES底层是通过Lucenne(这玩意我也不是很清楚)实现的,必须自己写代码去调用他的接口,Es有着丰富的Rest API 接口,开箱即用
REST API: 天然的跨平台
三:ES中的一些概念
- 索引(index)相当于mysql中的database
- 类型(type),在index中,可以定义一个或者多个类型,类似于mysql中的table,每一种类型的数据放在一起;
在新版本中貌似已经废弃了
- 文档(document):保存在某个index下,某种类型下的一个数据(document)文档是json格式的,document相当与MYSQL中的某一个table里面的内容
**ES底层原理是基于一种叫做倒排索引
的东西实现的快速检索,什么是倒排索引呢,这里简单解释一下:
比如现在我的es某个索引中,存放了如下几条记录(注意存放的是json格式,我这里为了方便解释,就写成如下的方式)
1-红海行动
2-探索红海行动
3-红海特别行动
4-红海记录篇
5-特工红海特别索索
那么在这个时候,es中就会维护一张倒排索引表,比如在存放第一条数据的时候,存放1-红海行动
,那么这个时候,es就会将这条记录的内容进行分词,比如将红海行动这四个字拆分成“红海”,“行动”,这两个单词,当然也有可能拆分成单独的每一个字,然后这张倒排索引表中就会记录某个拆分出来的词对应的存放在几号记录中,如下图所示:
比如红海出现在了12345号记录中都有。
当我们在检索的时候,比如在搜索框中输入:红海特工行动
,那摩对应的es就会将我们输入的内容进行分词,比如将输入的内容拆分成红海、特工、行动
这三个词,然后就会通过这三个词在倒排索引表中去进行查找,看每一个拆分出来的词出现在哪些记录中,然后将这些记录对应的数据都检索出来,再此,在这些查出来的记录中有一个相关性得分的机制,比如3号记录将内容分词成了红海、特别、行动这三个词存放在倒排索引表中,那摩我们输入红海特工行动的时候,将其分词成红海、特工、行动
,那摩在这个时候就会发现红海
包含在3号记录中,行动
也出现在了3号记录中,也就是3号记录命中了2个词,那摩就会在这次检索中给3号记录打对应的分数,同理其他命中的记录也是按照这种机制进行打分,然后就会按照分数从高到低将这些记录检索出来。
四:ES安装和使用
我本机使用的是docker安装es和kibana(es的可视化界面客户端工具)
1.拉取es和kibana的镜像
docker pull elasticsearch:7.4.2
docker pull kibana:7.4.2
二者要保持版本一致
2.创建文件和目录,用于外部挂载es和kibana
mkdir ‐p /mydata/elasticsearch/config -----用于挂载es配置
mkdir ‐p /mydata/elasticsearch/data ----用于挂载es存放的数据
echo "http.host:0.0.0.0" >>/mydata/elasticsearch/config/elasticsearch.yml --- 代表es可以被外部的任何机器访问
- 创建容器并启动
docker run --name es -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
4.这个时候,会发现es启动之后报错,是因为挂载的目录没有root权限,执行
chmod -R 777 /mydata/elasticsearch/
将其下的所有文件都变为可读可写可执行权限,重启容器,成功
5.安装kibana可视化界面
docker run --name kibana -e ELASTICSEARCH_HOSTS=http://192.168.57.136:9200 -p 5601:5601 \
-d kibana:7.4.2
在浏览器访问http://192.168.57.136:5601,跳转到如下界面,说明启动成功
注意
: 如果你是在公司里面使用的公网ip去连接,那摩就会连接不上,需要使用es内部分配的ip,具体做法如下:
docker inspect (elasticsearch 容器的名称) |grep IPAddress
显示如下:
接下来就是你删除kibana容器,重新使用上一步查询出来的es内部分配的ip进行启动创建kibana容器
docker run --name kibana -e ELASTICSEARCH_HOSTS=http://上一步查出来的ip:9200 -p 5601:5601 -d kibana:7.4.2
重新访问kibanaweb界面,发现可以成功连接数es。
五:初步检索
1. _cat
- GET /_cat/nodes: 查看所有节点
- GET /_cat/health:: 查看es健康状况
- GET /_cat/master: 查看主节点
- GET /_cat/indices: 查看所有索引 show database;
2.索引一个文档(保存)
保存一个数据,保存在哪个索引的哪个类型下,指定用哪个唯一标识
PUT customer/external/1;在customer索引下的external类型下保存1号数据为
PUT customer/external/1 注意:put请求必须要携带id,不然报错,多次发送重复数据,属于更新操作
Post customer/external/1 post请求可以不携带id,那样系统会自动给我们分配一个随机唯一id,如果携带id了,那摩第一次就是新增,第二次如果还是相同数据的话(携带id),那摩就会变成更新操作
,例如使用postman往es中存放一条数据
返回结果如下:
3.查询文档
GET customer/external/1 ---------查询哪个索引下哪个类型的几号数据
结果如下:
测试,使用乐观锁来修改es中的某一条数据
请求url后面需要携带 ?if_seq_no=0&if_primary_term=1
这样在更新操作时乐观锁才会生效
现在的场景是:某一时刻线程1和线程2都查询出来的一号数据,这时候线程1将该条数据进行了修改了,修改操作如下:
返回结果如下:
可以看到seq_no版本变成了2,这时候,线程2也进来执行更新操作,请求体如下:
响应返回状态码4.9,响应体如下:
说明第一个线程操作成功时乐观锁的版本发生了变化,因此第二个线程就无法进行修改,这就是es中乐观锁的用法
4更新文档操作
这里说明一下:post请求后面携带_update的话,那摩在请求体中就需要有doc:{},在doc里面再去进行参数的更改,这样的话他就会在更新时对比原来的数据,如果数据与原来的数据一样就什么都不做,version,seq_no都不变
不带_update的话就不需要doc:{},他就不会去对比原来的数据,会一直执行更新,version和seq_no都会发生变化
同样的,put的操作也是和post一样的,就看你带不带_update了
5.删除文档&索引
DELETE customer/external/1 -----删除哪个文档下的哪个类型的哪条数据
DELETE customer ------删除索引 ,注意es没有提供删除类型的操作
bulk 批量 API
比如批量新增:
index:代表就相当于mysql中insert的意思
进阶检索
1、 SearchAPI
ES支持两种基本方式检索:
- 一个是通过使用Rest request URL 发送搜索参数 (uri+检索参数)
- 另一个是通过使用 Rest request body 来发送它们 (uri+请求体)
1)检索信息
- 一切检索从_search开始
GET bank/_search?q=*&sort=account_number:asc
查询bank下面所有的数据并且排序规则按照account_number升序进行排列
全文检索
match
:如果检索的是字符串,会将该字符串去进行分词,然后进行全文检索,并且每条记录都有相关性得分,检索的是数字,那就不会分词,直接进行精确匹配。
match_phrase:
短语匹配
将需要匹配的值当成一个整体单词(不分词)进行检索
multi_match
: 多字段匹配
如下代表文档中state或者address字段的值中包含mill的都可以匹配出来
bool(复合查询)
aggregations(执行聚合)
聚合提供了从数据中分组和提取数据的能力,最简单的聚合方法大致等于SQL GROUP BY 和SQL 聚合函数,在Elasticsearch中,您有执行搜索返回hits(命中结果),并且同时返回聚合结果,把一个响应中的所有hits(命中结果)分隔开的能力,这是非常强大且有效的,您可以执行查询和多个聚合,并且在一次使用中得到各自的(任何一个)返回结果,使用一次简洁和简化的api来避免网络往返
例如下图:(不包括平均年龄)
结果如下:
搜索address中包含mill的所有人的年龄分布及平均年龄
查询结果如下:
备注:
Elasticsearch 查询 各返回字段含义:
- took : 该命令请求花费了多长时间,单位:毫秒。
- timed_out: 搜索是否超时。
- shards: 搜索分片信息。
- total: 搜索分片总数。
- successful: 搜索成功的分片数量。
- skipped:没有搜索的分片,跳过的分片。
- failed: 搜索失败的分片数量。
- hits: 搜索结果集。项目中,我们需要的一切数据都是从hits中获取。
- total: 返回多少条数据。
- max_score: 返回结果中,最大的匹配度分值。
- hits: 默认查询前十条数据,根据分值降序排序。
- _index: 索引库名称。
- _type: 类型名称。
- _id: 该条数据的id。
- _score: 关键字与该条数据的匹配度分值。
- _source:返回的原始数据,不指定的话,默认全部显示出来。
分词
一个tokenizer(分词器)接收一个字符流,将之分割为独立的tokens(词元,通常是独立的单词),然后输出tokens流。
例如,whitespace tokenizer遇到空白字符时分割文本,它会将文本"Quick brown fox!" 分割为[Quick,brown,fox!]。
该tokenizer(分词器)还负责记录各个term(词条)的顺序或position位置(用于phrase短语和word proximity词近邻查询),以及term(词条) 所代表的原始word(单词)的start(开始)和end(结束)的character offsets(字符偏移量) (用于高亮显示搜索的内容)。
Elasticsearch提供很多内置的分词器,可以用来构建customer analyzers(自定义分词器)
ik分词器(开源的ik分词器)
之前没安装ik
的时候,es中默认使用的是英文的分词器“standard”
在github上面搜索与自己服务器中相对应的ik分词器
的版本,下载zip文件,新建一个名字为ik文件夹,将zip文件解压到ik这个指定目录下,然后我自己这里是通过xftp上传到服务器中的自己挂载的plugin目录下
重启es和kibana,这个时候就可以使用ik分词器来测试一下分词效果
分词的结果如下:
ik_smart
是智能分词,ik_max_word是最大分词
分词结果如下:
但是随着当今社会的不断发展,涌现出来了许多的网络用语,比如铁憨憨,渣男、海王、舔狗
等等,默认的ik分词中是没有这些词汇的,他会把他拆分成一个个的汉子,显然这样不好,因此就需要我们自定义扩展词库,这里我使用的是nginx印射自定义文件到es服务器中
随便启动一个nginx实例,当然这里只是为了复制出来里面的配置
docker run -p 80:80 --name nginx -d nginx:1.10
注意:这里没有镜像会拉取镜像并且创建容器并启动
将容器内的配置文件拷贝到当前目录:
docker container cp nginx:/etc/nginx .
这里需要注意:复制出来的nginx文件会自动创建出来一个nginx目录,并且把复制出来的文件写入到nginx目录下
如下图所示
然后移除掉nginx容器
docker rm nginx
更改nginx目录的名字为conf
mv nginx conf
在mydata目录下创建一个nginx目录
mkdir nginx
将conf文件夹移动到nginx目录下
mv conf 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
最终效果如下图所示:
由于nginx默认会自动印射html目录下的东西,因此在html目录新建一个目录
mkdir es
新建一个用于扩展我们自定义的分词文件
touch fenci.txt
编辑里面的内容如下:
vim fenci.txt
保存并退出,接下来进入到外部挂载的es配置文件中
cd /mydata/elasticsearch/plugins/ik/config
编辑配置文件
vim IKAnalyzer.cfg.xml
最终改成如下:
重启es和kibana,再次进行分词测试
使用Elasticsearch-Rest-Client
ES官方推荐的RestClient,封装了ES操作,API层次分明,上手简单
由于es6.+已经越来越跟不上时代了,渐渐的被废弃了,7.+版本这里官方推荐使用的是高水平客户端连接方式来进行操作,里面集成了各种丰富的api
1.导入es依赖
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.58</version>
</dependency>
<!--导入es高阶api-->
<dependency>
<groupId>org.elasticsearch.client</groupId>
<artifactId>elasticsearch-rest-high-level-client</artifactId>
<version>7.4.2</version>
</dependency>
这里需要注意一下,由于我们使用的是springboot框架,他里面默认的帮我们管理装配的是es6.+版本,与我们导入的版本不符,因此需要我们手动指定版本
<properties>
<java.version>1.8</java.version>
<elasticsearch.version>7.4.2</elasticsearch.version>
</properties>
再次刷新maven依赖,发现依赖已经更新过来了
- 编写es配置文件
package com.cd.config;
import org.apache.http.HttpHost;
import org.elasticsearch.client.RequestOptions;
import org.elasticsearch.client.RestClient;
import org.elasticsearch.client.RestHighLevelClient;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class ElasticsearchConfig {
/**
* 用于构建es 的一些请求设置项,如果有什么需要设置的,就可以在静态代码块中进行设置
* 一般情况下是不需要这些设置的
*/
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();
}
// 高水平客户端
@Bean
public RestHighLevelClient esRestClient(){
RestHighLevelClient client = new RestHighLevelClient(
RestClient.builder(
new HttpHost("192.168.57.139",9200,"http")));
// 如果是es集群节点,那摩其他结点地址就可以这样写
// new HttpHost("192.168.57.137",9200,"http")));
return client;
}
}
2.新建实体类User和Account
class User{
private String username;
private String gender;
private Integer age;
public String getUsername() {
return username;
}
public void setUsername(String username) {
this.username = username;
}
public String getGender() {
return gender;
}
public void setGender(String gender) {
this.gender = gender;
}
public Integer getAge() {
return age;
}
public void setAge(Integer age) {
this.age = age;
}
}
Acount
package com.cd.pojo;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import lombok.ToString;
@Data
@AllArgsConstructor
@NoArgsConstructor
@ToString
public class Account {
private int account_number;
private int balance;
private String firstname;
private String lastname;
private int age;
private String gender;
private String address;
private String employer;
private String email;
private String city;
private String state;
}
在测试类中测试往es中添加数据
@RunWith(SpringRunner.class)
@SpringBootTest
class ElasticsearchStudyApplicationTests {
@Autowired
private RestHighLevelClient client;
// 测试索引的创建
@Test
void testCreateIndex() throws IOException {
//1. 创建索引请求 PUT cd_index
CreateIndexRequest request = new CreateIndexRequest("cd_index");
//2.客户端执行请求 IndicesClient, 请求后获得响应
CreateIndexResponse createIndexResponse = client.indices().create(request, RequestOptions.DEFAULT);
System.out.println(createIndexResponse);
}
//2.测试获取索引 判断其是否存在
@Test
void testExistIndex() throws IOException {
//1.获得索引请求
GetIndexRequest request = new GetIndexRequest("cd_index");
// 2.判断该索引是否存在
boolean exists = client.indices().exists(request, RequestOptions.DEFAULT);
System.out.println(exists);
}
//3.测试删除索引
@Test
void testDeleteIndex() throws IOException {
//1.获取删除索引
DeleteIndexRequest request = new DeleteIndexRequest("cd_index");
//删除
AcknowledgedResponse delete = client.indices().delete(request, RequestOptions.DEFAULT);
System.out.println(delete.isAcknowledged());
}
//获得文档信息
@Test
void testGetDocument() throws IOException {
GetRequest getRequest = new GetRequest("cd_index", "1");
GetResponse getResponse = client.get(getRequest, RequestOptions.DEFAULT);
System.out.println(getResponse.getSourceAsString()); //打印文档内容,打印成json格式字符串
System.out.println(getResponse); //返回的全部内容与命令行是一样的
}
//更新文档信息
@Test
void testUpdateDocument() throws IOException {
//创建请求
UpdateRequest updateRequest = new UpdateRequest("cd_index", "1");
updateRequest.timeout("1s");
User user = new User("学习es", 20);
updateRequest.doc(JSON.toJSONString(user),XContentType.JSON);
UpdateResponse updateResponse = client.update(updateRequest, RequestOptions.DEFAULT);
System.out.println(updateResponse.status());
}
//删除文档记录
@Test
void testDeleteRequest() throws IOException {
DeleteRequest request =new DeleteRequest("cd_index","1");
request.timeout("1s");
DeleteResponse deleteResponse = client.delete(request, RequestOptions.DEFAULT);
System.out.println(deleteResponse.status());
}
/**
* 测试存储数据到es
* 更新数据也可以
*/
@Test
void indexDate() throws IOException {
// 构建一个索引请求(设定索引的名字为users)
IndexRequest indexRequest = new IndexRequest("users");
// 构建这条数据的唯一标识
indexRequest.id("1");
// 第一种方式:这里使用的是k-v对,不需要使用冒号
indexRequest.source("username","zhangsan","age",18,"gender","男");
// 第二种方式: 直接传送json字符串
User user = new User();
user.setAge(18);
user.setUsername("zhangsan");
// 将对象转换为json字符串
String jsonString = JSON.toJSONString(user);
indexRequest.source(jsonString, XContentType.JSON); // 要保存的内容
// 执行保存操作
IndexResponse index = client.index(indexRequest, ElasticsearchConfig.COMMON_OPTIONS);
System.out.println(index);
}
//特殊的, 真的项目一般都会批量插入数据
@Test
void testBulkRequest() throws IOException {
BulkRequest bulkRequest = new BulkRequest();
bulkRequest.timeout("10s");
ArrayList<User> userList = new ArrayList<User>();
userList.add(new User("kuangshen1",3));
userList.add(new User("kuangshen2",3));
userList.add(new User("kuangshen3",3));
userList.add(new User("qinjiang1",3));
userList.add(new User("qinjinag2",3));
userList.add(new User("qinjiang3",3));
//批处理请求
for (int i = 0; i <userList.size() ; i++) {
bulkRequest.add(
new IndexRequest("cd_index")
.id("" + (i + 1))
.source(JSON.toJSONString(userList.get(i)), XContentType.JSON));
}
BulkResponse bulkItemResponses = client.bulk(bulkRequest, RequestOptions.DEFAULT);
//是否失败 ,false 代表成功
System.out.println(bulkItemResponses.hasFailures());
}
//查询
//SearchRequest 搜索请求
//SearchSourceBuilder 条件构造
// HighlightBuilder 构建高亮
// TermQueryBuild 精确查询
// MatchAllQueryBuilder 匹配所有查询
// xxx QueryBuilder 对应我们看到的所有命令
@Test
void testSearch() throws IOException {
SearchRequest searchRequest = new SearchRequest("cd_index");
//构建搜索条件
SearchSourceBuilder sourceBuilder = new SearchSourceBuilder();
//查询条件,我们可以使用QueryBuilder 工具来实现
//QueryBuilders.termQuery 精确
//QueryBuilders.matchAllQuery() 匹配所有
TermQueryBuilder termQueryBuilder = QueryBuilders.termQuery("name", "qinjiang1");
// MatchAllQueryBuilder matchAllQueryBuilder = QueryBuilders.matchAllQuery();
sourceBuilder.query(termQueryBuilder);
// //构建分页
// sourceBuilder.from();
// sourceBuilder.size();
sourceBuilder.timeout(new TimeValue(60, TimeUnit.SECONDS));
//将搜索条件放入请求里面
searchRequest.source(sourceBuilder);
//执行请求
SearchResponse searchResponse = client.search(searchRequest, RequestOptions.DEFAULT);
//所有查询的响应结果封装在 searchHit里面 因此我们需要把它拿出来 searchResponse.getHits()
System.out.println(JSON.toJSONString(searchResponse.getHits()));
System.out.println("==============");
for (SearchHit documentFields : searchResponse.getHits().getHits()) {
System.out.println(documentFields.getSourceAsMap());
}
}
/**
* 测试复杂检索功能
*/
@Test
void contextLoads() throws IOException {
// 1.创建检索请求
SearchRequest searchRequest = new SearchRequest();
// 指定从哪个索引检索
searchRequest.indices("bank");
// 指定DSL,检索条件
SearchSourceBuilder sourceBuilder = new SearchSourceBuilder();
// 构造检索条件
// sourceBuilder.query();
// sourceBuilder.from();
// sourceBuilder.size();
// sourceBuilder.aggregation();
// 匹配条件
sourceBuilder.query(QueryBuilders.matchQuery("address","mill"));
// 按照年龄的值分布进行聚合 参数说明:ageAgg--为这次聚合起一个名字,聚合的字段是age,找出10条数据
TermsAggregationBuilder ageAgg = AggregationBuilders.terms("ageAgg").field("age").size(10);
sourceBuilder.aggregation(ageAgg);
// 计算平均薪资
AvgAggregationBuilder balanceAvg = AggregationBuilders.avg("balanceAvg").field("balance");
sourceBuilder.aggregation(balanceAvg);
// 打印检索条件
// System.out.println("检索条件"+sourceBuilder.toString());
searchRequest.source(sourceBuilder);
// 2. 执行检索
SearchResponse searchResponse = client.search(searchRequest, ElasticsearchConfig.COMMON_OPTIONS);
// 3. 分析结果
// 获取所有查到的数据 (此处获取到的是外边的最大的hits)
SearchHits hits = searchResponse.getHits();
// 获取所有命中的记录
SearchHit[] hits1 = hits.getHits();
for (SearchHit hit : hits1) {
// 每一个hit就是查询时命中的每一条记录
// 将其转换为json字符串
String sourceAsString = hit.getSourceAsString();
// 将字符串转换为指定类型的对象
Account account = JSON.parseObject(sourceAsString, Account.class);
System.out.println("account: "+account.toString());
}
// 获取这次检索到的分析信息:
Aggregations aggregations = searchResponse.getAggregations();
Terms ageAgg1 = aggregations.get("ageAgg");
for (Terms.Bucket bucket : ageAgg1.getBuckets()) {
String keyAsString = bucket.getKeyAsString();
System.out.println("年龄:"+keyAsString + "=====>"+bucket.getDocCount());
}
Avg balanceAvg1 = aggregations.get("balanceAvg");
System.out.println("平均薪资: "+balanceAvg1.getValue());
}
}