spring boot+elasticsearch+canal-adapter+canal-deployer+MySQL实现全局搜索
我们在项目开发中搜索是绝对绕不开的一个问题,当我们数据量小的时候我们可以直接构建条件SQL来得到我们想要的数据,但是当数据积累到一定数量时,普通的条件SQL就会有一个致命的问题(查询效率),没错,当数据量积累到一定的地步普通的SQL语句查询效率会变的及其不理想,这个时候就有了SQL优化,什么建立SQL索引等等一系列的优化方案就出来了,但是还是无法应对大数据查询,这个时候就有了个各种搜索引擎服务器了,常用有solr,elasticsearch,lucene等,选型自己查找数据本文不做详细分析值得一提的时solr和elasticsearch都是基于Lucene的搜索服务器Solr支持更多格式的数据,而Elasticsearch仅支持json格式。
第一步:环境准备
我使用的是spring boot2.3.0.RELEASE+elasticsearch7.7.1+canal-adapter 1.1.5+canal-deployer 1.1.5
首先安装elasticsearch服务器canal-adapter和canal-deployer(我使用的是centos7)
安装elasticsearch我用的是docker安装所以首先需要安装docker安装教程参考以下文章docker安装安装好之后安装elasticsearch首先使用docker搜索命令搜素镜像搜索命令docker search elasticsearch找到我们需要安装的版本使用docker pull docker.io/elasticsearch:7.7.1安装(7.7.1可替换成自己需要安装的版本)然后安装canal canal下载地址canal下载地址下载好解压运行启动脚本即可使用
第二步:spring boot集成实现
创建一个spring boot项目在pom.xml文件里面添加elasticsearch依赖
如下:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-elasticsearch</artifactId>
</dependency>
注意要修改版本为我们安装的版本
如下:
<properties>
<elasticsearch.version>7.7.1</elasticsearch.version>
</properties>
接下来我们需要对elasticsearch进行一些基础配置在application.yml文件中配置(或者application.properties中配置)
配置如下:
elasticsearch:
hostname: 192.168.149.128 #elasticsearch服务器地址
port: 9200 #elasticsearch服务器端口
index: index #elasticsearch文档索引
编写ElasticSearchConfig配置文件
如下:
@Configuration
public class ElasticSearchConfig {
@Value("${elasticsearch.hostname}")
private String hostname;
@Value("${elasticsearch.port}")
private int port;
@Bean
public RestHighLevelClient restHighLevelClient(){
return new RestHighLevelClient(RestClient.builder(new HttpHost(hostname, port, "http")));
}
}
准备PO对象例如下面这样,就是你要在elasticsearch中储存的字段
@Data
public class ElasticSearchParams {
private String name;
private String enName;
private String subhead;
private String icon;
private String uri;
private Long id;
private Long pid;
private Integer weightid;
private Long count;
private String addTime;
private Integer type;
private String video;
private String content;
private String cover;
private String label;
private String skip;
private String extend;
private String fileUrl;
private String fileUrlEn;
private String imgs;
private String title;
private Long downloadCount;
}
编写elasticsearch服务类接口
public interface ESService {
/**
* 添加数据
*
* @return
* @throws Exception
*/
Boolean addData() throws Exception;
/**
* 搜索
*
* @param keyword
* @param pageNo
* @param pageSize
* @return
* @throws Exception
*/
List<Map<String, Object>> searchData(String keyword, int pageNo, int pageSize) throws Exception;
/**
* 删除索引
*
* @return
* @throws Exception
*/
Boolean deleteIndex() throws Exception;
/**
* 查询索引是否存在
*
* @return
* @throws Exception
*/
Boolean searchIndex() throws Exception;
/**
* 创建索引
*
* @return
* @throws Exception
*/
String createIndex() throws Exception;
}
编写实现类
@Service
public class ESServiceImpl implements ESService {
private final static Logger logger = LoggerFactory.getLogger(ESServiceImpl.class);
@Value("${elasticsearch.index}")
private String index;
@Autowired
private ESMapper esMapper;
@Autowired
@Qualifier("restHighLevelClient")
private RestHighLevelClient restHighLevelClient;
@Override
public Boolean addData() throws Exception {
List<ElasticSearchParams> esParams = esMapper.getAllDataList();
GetIndexRequest getIndexRequest = new GetIndexRequest(index);
boolean exists = restHighLevelClient.indices().exists(getIndexRequest, RequestOptions.DEFAULT);
//判断索引是否存在 不存在就创建索引
if (!exists) {
createIndex();
}
//把查询的数据放入es中
BulkRequest bulkRequest = new BulkRequest();
bulkRequest.timeout(TimeValue.timeValueMinutes(5));
for (ElasticSearchParams esParam : esParams) {
bulkRequest.add(new IndexRequest(index)
.id("" + esParam.getId())
.source(JSON.toJSONString(esParam), XContentType.JSON));
}
BulkResponse bulkResponse = restHighLevelClient.bulk(bulkRequest, RequestOptions.DEFAULT);
return !bulkResponse.hasFailures();
}
@Override
@Retryable(value = Exception.class)
public List<Map<String, Object>> searchData(String keyword, int pageNo, int pageSize) throws Exception {
//条件搜索
SearchRequest searchRequest = new SearchRequest(index);
SearchSourceBuilder sourceBuilder = new SearchSourceBuilder();
//分页
sourceBuilder.from(pageNo - 1);
sourceBuilder.size(pageSize);
// 首先构造多关键字查询条件
MultiMatchQueryBuilder matchQueryBuilder = QueryBuilders.multiMatchQuery(keyword, "title", "content").field("title", 10);
sourceBuilder.query(matchQueryBuilder);
sourceBuilder.timeout(new TimeValue(60, TimeUnit.SECONDS));
//高亮
HighlightBuilder highlightBuilder = new HighlightBuilder();
highlightBuilder.fields().add(new HighlightBuilder.Field("title")); // 高亮字段
highlightBuilder.fields().add(new HighlightBuilder.Field("content")); // 高亮字段
highlightBuilder.requireFieldMatch(false);
highlightBuilder.preTags("<span style=\"color:red\">"); //高亮设置
highlightBuilder.postTags("</span>");
sourceBuilder.highlighter(highlightBuilder);
//执行搜索
searchRequest.source(sourceBuilder);
SearchResponse searchResponse = restHighLevelClient.search(searchRequest, RequestOptions.DEFAULT);
//解析结果
ArrayList<Map<String, Object>> arrayList = new ArrayList<>();
for (SearchHit documentFields : searchResponse.getHits().getHits()) {
Map<String, HighlightField> highlightFields = documentFields.getHighlightFields();
HighlightField title = highlightFields.get("title");
HighlightField content = highlightFields.get("content");
Map<String, Object> sourceAsMap = documentFields.getSourceAsMap();
//解析高亮字段
if (title != null) {
Text[] fragments = title.fragments();
String n_title = "";
for (Text text : fragments) {
n_title += text;
}
sourceAsMap.put("title", n_title); //高亮字段替换掉原来的内容即可
}
if (content != null) {
Text[] fragments = content.fragments();
String n_content = "";
for (Text text : fragments) {
n_content += text;
}
sourceAsMap.put("content", n_content); //高亮字段替换掉原来的内容即可
}
arrayList.add(sourceAsMap);
}
return arrayList;
}
@Override
public Boolean deleteIndex() throws Exception {
return restHighLevelClient.indices().delete(new DeleteIndexRequest(index), RequestOptions.DEFAULT).isAcknowledged();
}
@Override
public Boolean searchIndex() throws Exception {
return restHighLevelClient.indices().exists(new GetIndexRequest(index), RequestOptions.DEFAULT);
}
@Override
public String createIndex() throws Exception {
if (searchIndex()) {
return "索引已存在";
}
//1.创建索引请求
CreateIndexRequest request = new CreateIndexRequest(index);
//2.设置索引信息
request.settings(
Settings.builder()
.put("index.number_of_shards", 5)
.put("index.number_of_replicas", 1)
);
request.mapping(
XContentFactory.jsonBuilder()
.startObject()
.startObject("properties")
.startObject("id").field("type", "keyword").field("index", false).endObject()
.startObject("pid").field("type", "long").field("index", false).endObject()
.startObject("weightid").field("type", "long").field("index", false).endObject()
.startObject("count").field("type", "long").field("index", false).endObject()
.startObject("addTime").field("type", "text").field("index", false).endObject()
.startObject("type").field("type", "integer").field("index", false).endObject()
.startObject("video").field("type", "text").field("index", false).endObject()
.startObject("content").field("type", "text").field("analyzer", "ik_max_word").field("search_analyzer", "ik_smart").endObject()
.startObject("cover").field("type", "text").field("index", false).endObject()
.startObject("label").field("type", "text").field("index", false).endObject()
.startObject("skip").field("type", "text").field("index", false).endObject()
.startObject("extend").field("type", "text").field("index", false).endObject()
.startObject("fileUrl").field("type", "text").field("index", false).endObject()
.startObject("fileUrlEn").field("type", "text").field("index", false).endObject()
.startObject("imgs").field("type", "text").field("index", false).endObject()
.startObject("title").field("type", "text").field("analyzer", "ik_max_word").field("search_analyzer", "ik_smart").endObject()
.startObject("downloadCount").field("type", "long").field("index", false).endObject()
.startObject("name").field("type", "text").field("index", false).endObject()
.startObject("enName").field("type", "text").field("index", false).endObject()
.startObject("subhead").field("type", "text").field("index", false).endObject()
.startObject("icon").field("type", "text").field("index", false).endObject()
.startObject("uri").field("type", "text").field("index", false).endObject()
.endObject()
.endObject()
);
//3.执行创建请求
CreateIndexResponse createIndexResponse = restHighLevelClient.indices().create(request, RequestOptions.DEFAULT);
//4.返回索引名
return createIndexResponse.index();
}
}
编写ESMapper文件
public interface ESMapper {
//查询数据
List<ElasticSearchParams> getAllDataList();
}
编写Mapper
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.mebay.mapper.TMebayESMapper">
<resultMap id="esDataListMap" type="com.mebay.entity.ElasticSearchParams">
<id column="id" jdbcType="BIGINT" property="id" />
<result column="pid" jdbcType="BIGINT" property="pid" />
<result column="weightid" jdbcType="INTEGER" property="weightid" />
<result column="count" jdbcType="BIGINT" property="count" />
<result column="add_time" jdbcType="TIMESTAMP" property="addTime" />
<result column="type" jdbcType="INTEGER" property="type" />
<result column="video" jdbcType="VARCHAR" property="video" />
<result column="content" jdbcType="VARCHAR" property="content" />
<result column="cover" jdbcType="VARCHAR" property="cover" />
<result column="label" jdbcType="VARCHAR" property="label" />
<result column="skip" jdbcType="VARCHAR" property="skip" />
<result column="extend" jdbcType="VARCHAR" property="extend" />
<result column="file_url" jdbcType="VARCHAR" property="fileUrl" />
<result column="imgs" jdbcType="VARCHAR" property="imgs" />
<result column="title" jdbcType="VARCHAR" property="title" />
<result column="download_count" jdbcType="BIGINT" property="downloadCount" />
<result column="name" jdbcType="VARCHAR" property="name" />
<result column="enName" jdbcType="VARCHAR" property="enName" />
<result column="subhead" jdbcType="VARCHAR" property="subhead" />
<result column="icon" jdbcType="VARCHAR" property="icon" />
<result column="uri" jdbcType="VARCHAR" property="uri" />
</resultMap>
<select id="getAllDataList" resultType="com.mebay.entity.ElasticSearchParams">
SELECT
a.id id,
a.pid pid,
a.weightid weightid,
a.count count,
a.add_time add_time,
a.type type,
a.video video,
a.content content,
a.cover cover,
a.label label,
a.skip skip,
a.extend extend,
a.file_url file_url,
a.imgs imgs,
a.title title,
a.download_count download_count,
m.name name,
m.en_name en_name,
m.subhead subhead,
m.icon icon,
m.uri uri
from t_mebay_articles a LEFT JOIN t_mebay_menu m on a.pid = m.id
</select>
</mapper>
到了这一步已经集成完毕了,但是我们在实际的项目中肯定会涉及到数据的修改、添加、删除,如何才能在我们修改数据库数据时同步数据到es库里面呢这个时候就需要用到canal了
第三步:数据同步
canal介绍:canal是Alibaba的一款开源项目,纯Java开发。基于数据库增量日志解析,提供增量数据订阅&消费,目前主要支持了MySQL(也支持mariaDB)。canal地址配置canal deployer 进入
修改instance.properties文件
vim instance.properties
然后进入bin目录启动 执行下面圈中的脚本(./start.sh)
修改adapter配置
刚刚配置中的name对应下面这个文件夹,网上很多教程都是直接写的es但是在新版本中有es6和es7所以我们要根据版本来选择对应的
然后进入es7文件夹新建一个yml文件
文件内容如下:
然后保存退出进入bin目录启动adapter
至此我们就实现了elasticsearch实现全局搜索并且数据实时更新
因为我直接用的工程配置展示给大家看的所以很多地方我都马赛克了 最后按照这个步骤行不通的可以联系我:联系方式2511217211