今天要聊的就是「博客管理」中全文搜索的实现,基于 SpringBoot+Vue+ES 实现,先给大家看一下效果:
全文搜索+关键字高亮,是不是和百度的效果差不多,话不多说,直接聊如何实现。
该网站前端是仿一个开源项目,本人主要是做后端,所以本教程涉及前端的部分会直接给代码,不做深入讲解数据准备
首先我们的数据是存放在 MySQL 的,建表语句如下,再利用 MyBatis-Plus 实现普通查询,比较简单,也不是本文重点,不做赘述。
create table code_note ( id bigint not null auto_increment, title varchar(128) not null default '' comment '标题', md_content text comment 'md文本', html_content text comment 'html文本', summary varchar(256) comment '摘要', category varchar(10) comment '分类', type tinyint not null default 0 comment '类型:0-文字;1-视频', create_time datetime not null comment '创建时间', publish_time datetime not null comment '发布时间', status tinyint DEFAULT 0 COMMENT '0-草稿箱;1-已发表;2-已删除', primary key (id) ); INSERT INTO yitiao_admin.code_note (id, title, author, md_content, html_content, summary, category, type, create_time, publish_time, status) VALUES (14, '一条', 'yitiao', 'canal', '
canal
', null, '默认', null, '2023-01-30 10:28:17', '2023-01-30 10:28:17', 1); 复制代码
前端页面
前端是基于 element-ui 来实现的,从文档找了半天,决定用 table 来实现,如果有更好的实现方式可以评论区留言。
其实就是只有的一列的无边框的表格,表格内又嵌入文本和按钮,再就是一些样式的调整,关键代码如下:
border >
margin-top: 5px;font-size: medium" v-html="scope.row.esContent">
{{ scope.row.author }} {{ scope.row.createTime }}
复制代码
「查询」和「全文搜索」按钮的切换使用的 v-if="searchShow",向后端发请求的部分如下:
fullSearch() { this.searchShow = true; this.pageShow = false; if (this.search === '' || this.Search === null) { this.search = 'spring' } request.get("/es/note/getByContent/", { params: { // pageNum: this.currentPage, // pageSize: this.pageSize, content: this.search } }).then(res => { console.log(res) this.searchTableData = res.data }) } 复制代码
Docker安装ES
终于要到正题啦,因为ES非常的耗内存,我的服务器剩余内存只有不到2G,所以选择用Docker部署单机版的ES。
sudo docker pull ElasticSearch:7.12.0 ## 创建挂载目录 config、data、plugins,开启全部权限 chmod -R 777 /data/opt/es ## 创建配置文件 cd config vim elasticsearch.yml http.host: 0.0.0.0 ## 启动容器 sudo docker run --name elasticsearch -p 9200:9200 -p 9300:9300 -e "discovery.type=single-node" -e ES_JAVA_OPTS="-Xms84m -Xmx512m" -v /data/opt/es/config/elasticsearch.yml:/usr/share/elasticsearch/config/elasticsearch.yml -v /data/opt/es/data:/usr/share/elasticsearch/data -v /data/opt/es/plugins:/usr/share/elasticsearch/plugins -d elasticsearch:7.12.0 # 查看日志 docker logs elasticsearch 复制代码
测试正常启动页面:http://101.43.138.173:9200/
插件使用
集群黄色解决
我们的elasticsearch是单节点的,只有一个主服务没有从服务,也就是说所以最简单的办法就是在创建索引的时候将备份数改为0。
如果我们已经创建了索引,那么我们可以直接更改索引的备份数方法举例如下:
## 请求方式为put ## url地址解释:IP地址:端口/索引名称/_settings(_settings 是接口的固定用法) curl -X PUT -H "Content-Type: Application/json" -d '{"number_of_replicas":0}' http://101.43.138.173:9200/Code_note/_settings --user name:password ## 返回 {"acknowledged":true} 复制代码
刷新插件,集群变成绿色。
设置用户名密码
# vim elasticsearch.yml http.cors.enabled: true http.cors.allow-origin: "*" http.cors.allow-headers: Authorization xpack.security.enabled: true xpack.security.transport.ssl.enabled: true 复制代码
docker exec -it fa41ca453d06 /bin/bash ./bin/elasticsearch-setup-passwords interactive ## 输入密码 复制代码
设置成功后,用户名为elastic,密码为设置的值,同时es里多了一个索引:.security-7
安装分词器
下载,版本一定要和es的对应,安装时注意,并不是一解压就好了。
首先查看插件的名字,解压后打开plugin-descriptor.properties文件,查看插件的名字,然后在挂载的plugins文件夹下新建文件夹,以插件的名字命名。
再将解压出来文件全部移动到插件名文件夹下才可以。
重启ES,查看日志
docker restart fa41ca453d06 docker logs fa41ca453d06 复制代码
至此,ES服务端部署完成,接下来就是基于SpringBoot操作ES。
Java客户端
spring-boot-starter-data-elasticsearch是比较好用的一个elasticsearch客户端,它内部会引入spring-data-elasticsearch。
版本对应关系
如果使用spring-boot-starter-data-elasticsearch,需要调整spring-boot的版本才起作用。
有下边这几种方法操作ElasticSearch:
案例代码
配置
org.springframework.boot spring-boot-starter-data-elasticsearch 2.7.7 复制代码
spring: elasticsearch: rest: uris: 101.43.138.173:9200 # 多个用逗号隔开 # username: ---用户名 # password: ---密码 connection-timeout: 1000 # 连接超时时间 read-timeout: 1000 # 读取超时时间 复制代码
索引类
// 省略部分字段 @Data @Document(indexName = "code_note") @Setting(replicas = 0) // 副本为0,单机模式 @NoArgsConstructor @AllArgsConstructor @Builder public class EsCodeNote { @Id private Long id; /** * md文本 */ @Field(type = FieldType.Text, analyzer = "ik_max_word") private String mdContent; /** * 分类 */ @Field(type = FieldType.Keyword) private String category; /** * 创建时间 */ @Field(type = FieldType.Date, format = DateFormat.custom, pattern = "yyyy-MM-dd HH:mm:ss||yyyy-MM-dd||epoch_millis") private Date createTime; } 复制代码
mapper类
@Repository public interface CodeNoteRepository extends ElasticsearchRepository { } 复制代码
service层
@Service @Slf4j @RequiredArgsConstructor public class CodeNoteService { private final ElasticsearchRestTemplate esRestTemplate; private final CodeNoteRepository codeNoteRepository; private final CodeNoteMapper noteMapper; public Object saveNoteToEs(EsCodeNote codeNote){ return codeNoteRepository.save(codeNote); } public void saveNotesToEs(List codeNotes){ codeNoteRepository.saveAll(codeNotes); } public List getFromEsByContent(String content) { //高亮 String preTag = ""; String postTag = ""; boolQueryBuilder boolQueryBuilder = new BoolQueryBuilder().should(new MatchQueryBuilder("mdContent", content)); Query query = new NativeSearchQueryBuilder() .withQuery(boolQueryBuilder) .withHighlightFields(new HighlightBuilder.Field("mdContent").preTags(preTag).postTags(postTag)).build(); // Query query1 = new NativeSearchQueryBuilder() // .withQuery(QueryBuilders.multiMatchQuery(content,"content","content.inner")).build(); // .withQuery(QueryBuilders.queryStringQuery(content)).build(); SearchHits search = esRestTemplate.search(query, EsCodeNote.class); return search.stream().map(SearchHit::getContent).collect(Collectors.toList()); } public void init() { List codeNotes = noteMapper.selectList(Wrappers.lambdaQuery(CodeNote.class)); List esCodeNotes = BeanUtil.copyToList(codeNotes, EsCodeNote.class); this.saveNotesToEs(esCodeNotes); } } 复制代码
controller
@RestController @RequestMapping("/es") @Slf4j @RequiredArgsConstructor public class EsRestController { private final CodeNoteService noteService; @PostMapping("/init") public Result createIndex() { noteService.init(); return Result.success("init all notes success"); } @GetMapping("/note/getByContent") public Result> getByContent(@RequestParam("content")String content) { return Result.success(noteService.getFromEsByContent(content)); } } 复制代码
测试
先初始化全部数据
根据mdContent分词查询
至此后端的高亮查询已经实现,如果与前端结合,还需要对查询结果做进一步封装和处理。
前后端联调
后端构建返回VO
public class EsCodeNoteRes { private Long id; /** * 题目 */ private String esTitle; private String author; /** * md文本 */ private String esContent; /** * html文本 */ private String htmlContent; // 省略部分 /** * 发布时间 */ @JsonFormat(pattern = "yyyy-MM-dd",timezone = "GMT+8") private Date publishTime; } 复制代码
对返回的结果封装
SearchHits searchHits = esRestTemplate.search(query, EsCodeNote.class); return searchHits.stream().map(search -> { EsCodeNote esCodeNote = search.getContent(); search.getHighlightFields().forEach((k, v) -> { log.info("highlight key is [{}],content is [{}]", k, v.get(0)); // 分别处理标题和正文 if (k.equals("title")) { esCodeNote.setTitle(v.get(0)); } if (k.equals("mdContent")) { esCodeNote.setMdContent(v.get(0)); } }); // 如果正文里没有关键字,取前100字符 if (!esCodeNote.getMdContent().contains(postTag)){ esCodeNote.setMdContent(esCodeNote.getMdContent().substring(0,100)); } return EsCodeNoteRes.builder() .id(esCodeNote.getId()) .esTitle(esCodeNote.getTitle()) .author(esCodeNote.getAuthor()) .esContent(esCodeNote.getMdContent()) .htmlContent(esCodeNote.getHtmlContent()) .summary(esCodeNote.getSummary()) .category(esCodeNote.getCategory()) .createTime(esCodeNote.getCreateTime()) .publishTime(esCodeNote.getPublishTime()) .build(); }).collect(Collectors.toList()); 复制代码
结果展示
道阻且长,行则将至。2023,扬帆起航。
原文链接:https://juejin.cn/post/7194734486327099429