目录
1. 概述
在初步学完elasticsearch之后,一直想着一个机会去实战操作一下,但在操作之前不禁有几个疑问
- 项目用了elasticsearch之后,还要用数据库吗?
- 数据库中的一对多,多对多表的关系在索引中如何表示?
- 假设es的数据是从数据库中获取的,那么当更新数据时,es中索引的数据该怎么同步?是立刻,还是有间隔的?
本来打算在服务器玩一玩es的,奈何阿里的学生机内存太小根本玩不起来,即使把jvm.options的xms改成256m后,但是部署了我自己的jar包以后,es的容器就自动因为内存不足退出了,所以只能在windows系统下玩玩。
2.ElasticSearch的调试
2.1 启动ES
笔者安装了如下软件
首先运行es(9200),head-master(9100),kibana(5601)
比起postman,笔者觉得还是kibana的界面对用户更友好一点,当然,最终项目还是要用java来写接口,所以这倒也不是什么重点。
2.2 创建搜索的微服务
导入依赖的时候,记得导入对应的es版本,笔者windows下的是7.6.1的版本,所以对应上即可
<properties>
<java.version>1.8</java.version>
<!-- 自定义es版本依赖否则会出bug-->
<elasticsearch.version>7.6.1</elasticsearch.version>
</properties>
<dependencies>
<!-- <dependency>-->
<!-- <groupId>org.springframework.data</groupId>-->
<!-- <artifactId>spring-data-elasticsearch</artifactId>-->
<!-- <version>3.0.6.RELEASE</version>-->
<!-- </dependency>-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-elasticsearch</artifactId>
</dependency>
<!-- <dependency>-->
<!-- <groupId>org.springframework.cloud</groupId>-->
<!-- <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>-->
<!-- </dependency>-->
<!-- <dependency>-->
<!-- <groupId>org.springframework.cloud</groupId>-->
<!-- <artifactId>spring-cloud-starter-feign</artifactId>-->
<!-- <version>1.4.6.RELEASE</version>-->
<!-- </dependency>-->
<dependency>
<groupId>pers.fjl</groupId>
<artifactId>blog-common</artifactId>
<version>1.0-SNAPSHOT</version>
</dependency>
</dependencies>
2.3 使用logstash同步数据库数据到es的索引中
这就是比较重要的一个工具,logstash帮助我们同步数据库的数据到索引中,使用也非常方便,只需要下载压缩包后,解压再编写对应的配置即可
input {
jdbc {
jdbc_connection_string => "jdbc:mysql://127.0.0.1:3306/blog?useUnicode=true&characterEncoding=utf-8&serverTimezone=GMT"
jdbc_user => "root"
jdbc_password => "123456"
jdbc_driver_library => "G:\download\java\javaProject\elasticSearch\logstash-7.6.1\mysqletc\mysql-connector-java-8.0.15.jar"
jdbc_driver_class => "com.mysql.jdbc.Driver"
jdbc_paging_enabled => "true"
jdbc_page_size => "50"
#以下对应着要执行的sql的绝对路径
statement => "SELECT b.blog_id, u.nickname, u.avatar, t.type_name, b.views, b.description, b.create_time, b.title, b.first_picture, b.thumbs, b.content FROM blog b, user u, type t WHERE b.uid = u.uid AND b.type_id = t.type_id"
#定时字段 分时天月年,全部为*默认为每分钟都更新
schedule => "* * * * *"
}
}
output {
elasticsearch {
# ES的IP地址及端口
hosts => ["127.0.0.1:9200"]
# 索引名称 可自定义
index => "blog-search"
# 需要关联的数据库中有有一个id字段,对应类型中的id
document_id => "%{blog_id}"
document_type => "type1"
}
stdout {
# JSON格式输出
codec => json_lines
}
}
笔者设置的是每分钟同步一次数据,进入bin目录,使用logstash -f …/mysqletc/mysql.conf 启动,可以看到的是数据已经同步到对应的索引中
需要注意的是,当删除数据库中数据的时候,索引库中的数据时没有被删除的,所以最好是在数据库增加一个状态字段,这样就可以标记该数据是否被删除,然后用es查询时只查询状态字段未被删除的字段即可。
3.Linux系统下部署
3.1 拉取es容器
首先docker拉取自己所需的es版本,然后直接运行即可,本来打算在阿里云上部署,奈何学生机2g属实不够用,于是自己用vmware创建了一个8g内存的虚拟机。
[root@pinyoyougou-docker ~]# docker run --name fjl_es -d -e ES_JAVA_OPTS="-Xms512m -Xmx512m" -e "discovery.type=single-node" -p 9200:9200 -p 9300:9300 elasticsearch:7.6.1
输入9200端口验证
3.2 让9300端口可用
众所周知,java是用9300端口操作es的,但是在虚拟机下的9300需要经过配置才可使用
我们进入容器
docker exec ‐it tensquare_elasticsearch /bin/bash
此时,我们看到elasticsearch所在的目录为/usr/share/elasticsearch ,进入config看到了 配置文件 elasticsearch.yml 我们通过vi命令编辑此文件,尴尬的是容器并没有vi命令 ,咋办?我们需要以文件挂载的 方式创建容器才行,这样我们就可以通过修改宿主机中的某个文件来实现对容器内配置文件的修改
拷贝配置文件到宿主机 首先退出容器,然后执行命令:
[root@pinyoyougou-docker ~]# docker cp fjl_es:/usr/share/elasticsearch/config/elasticsearch.yml /usr/share/elasticsearch.yml
停止和删除原来创建的容器
[root@pinyoyougou-docker ~]# docker rm fjl_es
重新执行创建容器命令
[root@pinyoyougou-docker ~]# docker run --name fjl_es
-d -e ES_JAVA_OPTS="-Xms512m -Xmx512m" -e "discovery.type=single
-node" -p 9200:9200 -p 9300:9300 -v /usr/share/elasticsearch.ym
l:/usr/share/elasticsearch/config/elasticsearch.yml
elasticsearch:7.6.1
修改/usr/share/elasticsearch.yml 将 transport.host: 0.0.0.0 前的#去掉后保 存文件退出。其作用是允许任何ip地址访问elasticsearch .开发测试阶段可以这么做,生 产环境下指定具体的IP
重启启动 docker restart tensquare_elasticsearch
3.3 安装ik分词器
进入到ik分词器的目录下
docker cp ik fjl_es:/usr/share/elasticsearch/plugins
验证是否配置成功
[root@izwz9inovbad1itlt8v4taz es]# docker cp ik fjl_es:/usr/share/elasticsearch/plugins
[root@izwz9inovbad1itlt8v4taz es]# docker cp ik fjl_es:/usr/share/elasticsearch/plugins^C
[root@izwz9inovbad1itlt8v4taz es]# docker exec -it fjl_es /bin/bash
[root@8d3d159cf4e3 elasticsearch]# ls
LICENSE.txt NOTICE.txt README.asciidoc bin config data jdk lib logs modules plugins
[root@8d3d159cf4e3 elasticsearch]# cd /plugins
bash: cd: /plugins: No such file or directory
[root@8d3d159cf4e3 elasticsearch]# cd plugins/
[root@8d3d159cf4e3 plugins]# ls
ik
3.4 安装head-master
配置跨域,打开elasticsearch.yml
cluster.name: "docker-cluster"
network.host: 0.0.0.0
transport.host: 0.0.0.0
http.cors.enabled: true
http.cors.allow-origin: "*"
docker pull mobz/elasticsearch-head:5
docker run -di --name=fjl_header -p 9100:9100 mobz/elasticsearch-head:5
3.5 配置logstash(耗时最久)
docker pull logstash:7.6.1
先发起请求,创建索引
http://119.23.62.26:9200/blog-search/type1
当时网上找了几篇文章配置都不行,最后终于成功了。首先拉取对应的logstash镜像,然后自己放置配置文件和mysql的jar包到自己新建的目录(笔者放在/usr/share/mylogstash)
需要注意的是,我们要进入到容器里才能修改logstash配置,**这里采用挂载文件的方式配置。**这样在容器启动后,容器内会自动创建对应的目录或文件。通过这种方式,我们可以明确一点,即-v参数中,冒号":"前面的目录是宿主机目录,后面的目录是容器内目录。
input {
jdbc {
jdbc_connection_string => "jdbc:mysql://192.168.10.128:3306/blog?useUnicode=true&characterEncoding=utf-8&serverTimezone=GMT"
jdbc_user => "root"
jdbc_password => "123456"
jdbc_driver_library => "/app/mysql-connector-java-8.0.15.jar"
jdbc_driver_class => "com.mysql.cj.jdbc.Driver"
jdbc_paging_enabled => "true"
jdbc_page_size => "50"
#以下对应着要执行的sql的绝对路径
statement => "SELECT b.blog_id, u.nickname, u.avatar, t.type_name, b.views, b.description, b.create_time, b.title, b.first_picture, b.thumbs, b.content FROM blog b, user u, type t WHERE b.uid = u.uid AND b.type_id = t.type_id"
#定时字段 分时天月年,全部为*默认为每分钟都更新
schedule => "* * * * *"
}
}
output {
elasticsearch {
# ES的IP地址及端口
hosts => ["192.168.10.128:9200"]
# 索引名称 可自定义
index => "blog-search"
# 需要关联的数据库中有有一个id字段,对应类型中的id
document_id => "%{blog_id}"
document_type => "type1"
}
stdout {
# JSON格式输出
codec => json_lines
}
}
docker run -d
-v /usr/share/mylogstash/mysql.conf:/usr/share/logstash/pipeline/logstash.conf
-v /usr/share/mylogstash/mysql-connector-java-8.0.15.jar:/app/mysql-connector-java-8.0.15.jar
--name=fjl_logstash logstash:7.6.1
修改容器内的logstash.yml文件的ip地址
同步成功
四. 出现的bug
4.1 elasticsearch head
虽然已经进行了跨域配置,但是依旧出现了bug,数据浏览看不到任何数据,且查看数据请求报错,错误情况如下:
修改方法:
进入head插件安装目录
编辑/usr/src/app/_site/vendor.js(我的es_Head插件部署在docker容器中,路径作为参考)
修改共有两处:
1. 6886行 /contentType: "application/x-www-form-urlencoded
改成 contentType: "application/json;charset=UTF-8"
2. 7573行 var inspectData = s.contentType === "application/x-www-form-urlencoded" &&
改成 var inspectData = s.contentType === "application/json;charset=UTF-8" &&
五. elk整合springes做查询
忙活了这么久,才把虚拟机的配置搞定,现在开始编写代码,首先编写配置类。
package pers.fjl.search.config;
import org.apache.http.HttpHost;
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 {
// <beans id='restHighLevelClient' class=RestHighLevelClient
@Bean
public RestHighLevelClient restHighLevelClient() {
RestHighLevelClient client = new RestHighLevelClient(
RestClient.builder(
new HttpHost("192.168.10.128",9200,"http")
)
);
return client;
}
}
看了一下ES的Api,QueryBuilders.matchQuery()是会对搜索词进行分词然后匹配查询的,所以一般是用的这个。QueryBuilders.termQuery()则是精确查询,查询中文的时候不会分词(只查询得到一个字的中文,除非在字段后面加上keyword),所以很少用到。
package pers.fjl;
import com.alibaba.fastjson.JSON;
import org.elasticsearch.action.admin.indices.delete.DeleteIndexRequest;
import org.elasticsearch.action.bulk.BulkRequest;
import org.elasticsearch.action.bulk.BulkResponse;
import org.elasticsearch.action.delete.DeleteRequest;
import org.elasticsearch.action.delete.DeleteResponse;
import org.elasticsearch.action.get.GetRequest;
import org.elasticsearch.action.get.GetResponse;
import org.elasticsearch.action.index.IndexRequest;
import org.elasticsearch.action.index.IndexResponse;
import org.elasticsearch.action.search.SearchRequest;
import org.elasticsearch.action.search.SearchResponse;
import org.elasticsearch.action.support.master.AcknowledgedRequest;
import org.elasticsearch.action.support.master.AcknowledgedResponse;
import org.elasticsearch.action.update.UpdateRequest;
import org.elasticsearch.action.update.UpdateResponse;
import org.elasticsearch.client.RequestOptions;
import org.elasticsearch.client.RestHighLevelClient;
import org.elasticsearch.client.indices.CreateIndexRequest;
import org.elasticsearch.client.indices.CreateIndexResponse;
import org.elasticsearch.client.indices.GetIndexRequest;
import org.elasticsearch.common.unit.TimeValue;
import org.elasticsearch.common.xcontent.XContentType;
import org.elasticsearch.index.query.MatchAllQueryBuilder;
import org.elasticsearch.index.query.MatchQueryBuilder;
import org.elasticsearch.index.query.QueryBuilders;
import org.elasticsearch.index.query.TermQueryBuilder;
import org.elasticsearch.search.SearchHit;
import org.elasticsearch.search.builder.SearchSourceBuilder;
import org.elasticsearch.search.fetch.subphase.FetchSourceContext;
import org.junit.jupiter.api.Test;
import org.springframework.boot.test.context.SpringBootTest;
import pers.fjl.po.User;
import pers.fjl.utils.ESconst;
import javax.annotation.Resource;
import java.io.IOException;
import java.util.ArrayList;
import java.util.concurrent.TimeUnit;
@SpringBootTest
class EsApiApplicationTests {
@Resource
private RestHighLevelClient restHighLevelClient;
// 测试索引创建 Request
@Test
void testCreateIndex() throws IOException {
// 1.创建索引请求
CreateIndexRequest request = new CreateIndexRequest("fjl_index");
// 2.客户端执行请求 IndicesClient请求后获得响应
CreateIndexResponse createIndexResponse = restHighLevelClient.indices().create(request, RequestOptions.DEFAULT);
System.out.println(createIndexResponse);
}
// 测试获取索引
@Test
void testExistIndex() throws IOException {
GetIndexRequest request = new GetIndexRequest("fjl_index");
boolean exists = restHighLevelClient.indices().exists(request, RequestOptions.DEFAULT);
System.out.println(exists);
}
// 测试删除索引
@Test
void testDelIndex() throws IOException {
DeleteIndexRequest request = new DeleteIndexRequest("fjl_index");
AcknowledgedResponse delete = restHighLevelClient.indices().delete(request, RequestOptions.DEFAULT);
System.out.println(delete.isAcknowledged());
}
// 测试添加文档
@Test
void testDocument() throws IOException {
User user = new User("小小甜", 21);
// 创建请求
IndexRequest request = new IndexRequest("fjl_index");
// 规则put /fjl_index/_doc/1
request.id("1");
request.timeout("1s");
// 将数据放入请求 json
request.source(JSON.toJSONString(user), XContentType.JSON);
// 客户端发送请求,获取响应结果
IndexResponse indexResponse = restHighLevelClient.index(request, RequestOptions.DEFAULT);
System.out.println(indexResponse.toString());
System.out.println(indexResponse.status());
}
// 获取文档,判断是否存在 get /index/doc/1
@Test
void testIsExists() throws IOException {
GetRequest getRequest = new GetRequest("fjl_index", "1");
// 不获取_source的上下文了
getRequest.fetchSourceContext(new FetchSourceContext(false));
getRequest.storedFields("_none_");
boolean exists = restHighLevelClient.exists(getRequest, RequestOptions.DEFAULT);
System.out.println(exists);
}
// 获取文档信息
@Test
void testGetDocument() throws IOException {
GetRequest getRequest = new GetRequest("fjl_index", "1");
GetResponse getResponse = restHighLevelClient.get(getRequest, RequestOptions.DEFAULT);
System.out.println(getResponse.getSourceAsString());
System.out.println(getResponse); // 返回全部内容
}
// 更新文档信息
@Test
void testUpdateRequest() throws IOException {
UpdateRequest updateRequest = new UpdateRequest("fjl_index", "1");
updateRequest.timeout("1s");
User user = new User("小小甜", 22);
updateRequest.doc(JSON.toJSONString(user), XContentType.JSON);
UpdateResponse updateResponse = restHighLevelClient.update(updateRequest, RequestOptions.DEFAULT);
System.out.println(updateResponse.status());
}
// 删除文档信息
@Test
void testDeleteRequest() throws IOException{
DeleteRequest deleteRequest = new DeleteRequest("fjl_index", "1");
deleteRequest.timeout("1s");
DeleteResponse deleteResponse = restHighLevelClient.delete(deleteRequest, RequestOptions.DEFAULT);
System.out.println(deleteResponse.status());
}
// 特殊的,项目一般采用的批量插入数据
@Test
void testBulkRequest() throws IOException{
BulkRequest bulkRequest = new BulkRequest();
bulkRequest.timeout("10s");
ArrayList<User> userList = new ArrayList<>();
userList.add(new User("张三", 3));
userList.add(new User("李四", 13));
userList.add(new User("杰尼龟", 23));
userList.add(new User("Lisa", 24));
userList.add(new User("Rosie", 23));
userList.add(new User("Jisoo", 25));
// 批处理请求
for (int i = 0; i < userList.size(); i++) {
bulkRequest.add(
new IndexRequest("fjl_index")
.id(""+(i+1))
.source(JSON.toJSONString(userList.get(i)), XContentType.JSON));
}
BulkResponse bulkResponse = restHighLevelClient.bulk(bulkRequest, RequestOptions.DEFAULT);
System.out.println(bulkResponse.hasFailures());
}
// 查询
@Test
void testSearch() throws IOException{
// SearchRequest searchRequest = new SearchRequest(ESconst.ES_INDEX);
SearchRequest searchRequest = new SearchRequest("blog-search");
// 构建搜索条件
SearchSourceBuilder sourceBuilder = new SearchSourceBuilder();
// 查询条件,我们可以使用QueryBuilders工具来实现
// 精确查询,这里如果查人名要用keyword,否则人名会被分词
// TermQueryBuilder termQueryBuilder = QueryBuilders.termQuery("title.keyword", "方法");
MatchQueryBuilder title = QueryBuilders.matchQuery("title", "章");
// 匹配所有查询
// MatchAllQueryBuilder matchAllQueryBuilder = QueryBuilders.matchAllQuery();
sourceBuilder.query(title);
sourceBuilder.timeout(new TimeValue(60, TimeUnit.SECONDS));
searchRequest.source(sourceBuilder);
SearchResponse searchResponse = restHighLevelClient.search(searchRequest, RequestOptions.DEFAULT);
System.out.println(JSON.toJSONString(searchResponse.getHits()));
System.out.println("==========");
int i =0;
for (SearchHit documentFields : searchResponse.getHits().getHits()) {
System.out.println(documentFields.getSourceAsMap());
i++;
}
System.out.println("共"+i+"篇文章");
}
}
至于项目中怎么实际用到,其实就是根据自己的需求对文档进行搜索,然后用sourceBuilder进行分页处理即可,至此本文结束。如果对这篇文章还有什么想法或问题,欢迎评论区留言一起讨论!