个人简介:
> 个人主页:赵四司机
> 学习方向:JAVA后端开发
> ⏰往期文章:SpringBoot项目整合微信支付
> 博主推荐网站:牛客网 刷题|面试|找工作神器
> 种一棵树最好的时间是十年前,其次是现在!
> 喜欢的话麻烦点点关注喔,你们的支持是我的最大动力。
前言:
最近在做一个基于SpringCloud+Springboot+Docker的新闻头条微服务项目,用的是黑马的教程,现在项目开发进入了尾声,我打算通过写文章的形式进行梳理一遍,并且会将梳理过程中发现的Bug进行修复,有需要改进的地方我也会继续做出改进。这一系列的文章我将会放入微服务项目专栏中,这个项目适合刚接触微服务的人作为练手项目,假如你对这个项目感兴趣你可以订阅我的专栏进行查看,需要资料可以私信我,当然要是能给我点个小小的关注就更好了,你们的支持是我最大的动力。
如果你想要一个可以系统学习的网站,那么我推荐的是牛客网,个人感觉用着还是不错的,页面很整洁,而且内容也很全面,语法练习,算法题练习,面试知识汇总等等都有,论坛也很活跃,传送门链接:牛客刷题神器
目录
一:需求分析
二:技术选型
1.方案对比
2.ES简介
三:ES环境搭建
1.拉取镜像
2. 创建容器
3.配置中文分词器 ik
四:代码实现
1.实现思路
2.创建映射
3.数据初始化到索引库
4.搜索功能实现
5.测试
在App端,我们可以在首页的顶部搜索栏里输入关键字进行文章的搜索,而且对于搜索结果我们会对命中的标题进行高亮展示,对于标题没命中但是文章内容命中的文章我们也要将其展示出来,并且当用户点击搜索结果中某一条文章的时候能够实现页面跳转查看文章详情的功能。
跟以往不一样的是,以前我用的比较多的是用SQL语句进行模糊查询,往往也能达到不错的效果。但是这只适用于数据量比较少的情况下,而且数据库模糊查询还有一个问题,举个例子,假如我要搜索“什么是消息中间件”,这时候正好有一篇文章中包含有“消息中间件的介绍”的文字内容,但是这时候使用SQL的模糊查询是查询不到这个结果的,这显然是一个很大的弊端。
除此之外,当数据库中的文档数达到上万条时候,采用模糊查询就已经很慢了,要是数据达到企业级的话,这样的检索速度肯定是让人受不了的。因为采用模糊查询这时候数据库并不知道那些数据包含了这个关键词,只能一条数据一条数据进行查询,而且还需要进行字符串匹配,假如我搜索“Kafka”这一关键词,就算数据库中一万条数据只有一条是包含有这个词的采用模糊查询还是会检索数据库中的所有数据,这样做显然是很费时费力的。
而采用ES(ElasticSearch)之后能很好解决这个问题,即使是TB级的数据也能在毫秒内返回结果。那么为什么ES能具有如此高的效率呢?
Elasticsearch是一个开源的高扩展的分布式全文检索引擎,它可以近乎实时的存储、检索数据;本身扩展性很好,可以扩展到上百台服务器,处理PB级别的数据。 我们知道Java是面向对象的,而Elasticsearch是面向文档的,也就是说文档是所有可搜索数据的最小单元。ES的文档就像MySql中的一条记录,只是ES的文档会被序列化成json格式,保存在Elasticsearch中。
ES是基于倒排索引的,什么意思呢,举个例子,假如海量的文档中只有文档A、B、C、D包含有“Kafka”这个关键词,假如用户搜索“Kafka”,这时候ES就会立即返回A、B、C、D这四个文档,从而避免把时间浪费在检索其他文档上面。
由于ES对中文的分词不太友好,因此需要自己配置一个中文分词器ik。
docker pull elasticsearch:7.4.0
docker run -id --name elasticsearch -d --restart=always -p 9200:9200 -p 9300:9300 -v /usr/share/elasticsearch/plugins:/usr/share/elasticsearch/plugins -e "discovery.type=single-node" elasticsearch:7.4.0
因为在创建elasticsearch容器的时候,映射了目录,所以可以在宿主机上进行配置ik中文分词器
在去选择ik分词器的时候,需要与elasticsearch的版本好对应上。
把资料中的elasticsearch-analysis-ik-7.4.0.zip
上传到服务器上,放到对应目录(plugins)解压
#切换目录
cd /usr/share/elasticsearch/plugins
#新建目录
mkdir analysis-ik
cd analysis-ik
#root根目录中拷贝文件
mv elasticsearch-analysis-ik-7.4.0.zip /usr/share/elasticsearch/plugins/analysis-ik
#解压文件
cd /usr/share/elasticsearch/plugins/analysis-ik
unzip elasticsearch-analysis-ik-7.4.0.zip
为了加快检索速度,搜索关键词时候不会到数据库中直接搜索,而是到ES索引库中进行检索,因此我们首先需要创建好 索引库,然后需要对以前上传的文章创建索引,当后续有新文章要上传时候,我们就采取实时创建索引的策略。当用户输入关键词进行搜索时候,服务器就会到ES中进行检索,假如检索结果中标题含有该关键词,则将该标题中该关键词高亮进行返回,若标题不包含关键词但是内容包含关键词也将结果返回,如若不然则说明没有检索结果,流程图见下图:
使用apifox添加映射
映射内容:
{
"mappings":{
"properties":{
"id":{
"type":"long"
},
"publishTime":{
"type":"date"
},
"layout":{
"type":"integer"
},
"images":{
"type":"keyword",
"index": false
},
"staticUrl":{
"type":"keyword",
"index": false
},
"authorId": {
"type": "long"
},
"authorName": {
"type": "text"
},
"title":{
"type":"text",
"analyzer":"ik_smart"
},
"content":{
"type":"text",
"analyzer":"ik_smart"
}
}
}
}
最后两项表示对文章标题和内容创建索引,其他字段均用于展示使用。
①在tbug-headlines-test中创建新模块es-init
②相关配置
server:
port: 9999
spring:
application:
name: es-article
datasource:
driver-class-name: com.mysql.jdbc.Driver
url: jdbc:mysql://localhost:3306/headlines_article?useUnicode=true&characterEncoding=UTF-8&serverTimezone=UTC
username: root
password: 43
# 设置Mapper接口所对应的XML文件位置,如果你在Mapper接口中有自定义方法,需要进行该配置
mybatis-plus:
mapper-locations: classpath*:mapper/*.xml
# 设置别名包扫描路径,通过该属性可以给包中的类注册别名
type-aliases-package: com.my.model.article.pojos
#自定义elasticsearch连接配置
elasticsearch:
host: 49.23.192
port: 9200
③pojo及mapper类
pojo
package com.my.es.pojo;
import lombok.Data;
import java.util.Date;
@Data
public class SearchArticleVo {
// 文章id
private Long id;
// 文章标题
private String title;
// 文章发布时间
private Date publishTime;
// 文章布局
private Integer layout;
// 封面
private String images;
// 作者id
private Long authorId;
// 作者名词
private String authorName;
//静态url
private String staticUrl;
//文章内容
private String content;
}
mapper
package com.my.es.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.my.es.pojo.SearchArticleVo;
import com.my.model.article.pojos.ApArticle;
import org.apache.ibatis.annotations.Mapper;
import java.util.List;
@Mapper
public interface ApArticleMapper extends BaseMapper {
List loadArticleList();
}
XML
④批量导入
package com.my.es;
import com.alibaba.fastjson.JSON;
import com.my.es.mapper.ApArticleMapper;
import com.my.es.pojo.SearchArticleVo;
import org.elasticsearch.action.bulk.BulkRequest;
import org.elasticsearch.action.index.IndexRequest;
import org.elasticsearch.client.RequestOptions;
import org.elasticsearch.client.RestHighLevelClient;
import org.elasticsearch.common.xcontent.XContentType;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.junit4.SpringRunner;
import java.util.List;
@SpringBootTest
@RunWith(SpringRunner.class)
public class ApArticleTest {
@Autowired
private ApArticleMapper apArticleMapper;
@Autowired
private RestHighLevelClient restHighLevelClient;
/**
* 注意:数据量的导入,如果数据量过大,需要分页导入
* @throws Exception
*/
@Test
public void init() throws Exception {
//1.查询所有符合条件的文章数据
List searchArticleVos = apArticleMapper.loadArticleList();
//2.批量导入到es索引库
BulkRequest bulkRequest = new BulkRequest("app_info_article");
for (SearchArticleVo searchArticleVo : searchArticleVos) {
IndexRequest indexRequest = new IndexRequest().id(searchArticleVo.getId().toString())
.source(JSON.toJSONString(searchArticleVo), XContentType.JSON);
//批量添加数据
bulkRequest.add(indexRequest);
}
restHighLevelClient.bulk(bulkRequest, RequestOptions.DEFAULT);
}
}
注意这里创建了一个全局索引名称app_info_article
①导入tbug-headlines-search模块
②在tbug-headlines-service中添加依赖
org.elasticsearch.client
elasticsearch-rest-high-level-client
7.4.0
org.elasticsearch.client
elasticsearch-rest-client
7.4.0
org.elasticsearch
elasticsearch
7.4.0
③nacos配置
spring:
autoconfigure:
exclude: org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration
elasticsearch:
host: 49.234.52.192
port: 9200
④搜索接口定义
package com.my.search.controller.v1;
import com.my.model.common.dtos.ResponseResult;
import com.my.model.search.dtos.UserSearchDto;
import com.my.search.service.ArticleSearchService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.io.IOException;
@RestController
@RequestMapping("/api/v1/article/search")
public class ArticleSearchController {
@Autowired
private ArticleSearchService articleSearchService;
@PostMapping("/search")
public ResponseResult search(@RequestBody UserSearchDto dto) throws IOException {
return articleSearchService.search(dto);
}
}
⑤dto
package com.my.model.search.dtos;
import lombok.Data;
import java.util.Date;
@Data
public class UserSearchDto {
/**
* 搜索关键字
*/
String searchWords;
/**
* 当前页
*/
int pageNum;
/**
* 分页条数
*/
int pageSize;
/**
* 最小时间
*/
Date minBehotTime;
public int getFromIndex(){
if(this.pageNum<1)return 0;
if(this.pageSize<1) this.pageSize = 10;
return this.pageSize * (pageNum-1);
}
}
⑥业务层实现
package com.my.search.service;
import com.my.model.common.dtos.ResponseResult;
import com.my.model.search.dtos.UserSearchDto;
import java.io.IOException;
public interface ArticleSearchService {
/**
ES文章分页搜索
@return
*/
ResponseResult search(UserSearchDto userSearchDto) throws IOException;
}
package com.my.search.service.serviceImpl;
import com.alibaba.fastjson.JSON;
import com.my.model.common.dtos.ResponseResult;
import com.my.model.common.enums.AppHttpCodeEnum;
import com.my.model.search.dtos.UserSearchDto;
import com.my.model.user.pojos.ApUser;
import com.my.search.service.ApUserSearchService;
import com.my.search.service.ArticleSearchService;
import com.my.utils.thread.AppThreadLocalUtils;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.elasticsearch.action.search.SearchRequest;
import org.elasticsearch.action.search.SearchResponse;
import org.elasticsearch.client.RequestOptions;
import org.elasticsearch.client.RestHighLevelClient;
import org.elasticsearch.common.text.Text;
import org.elasticsearch.index.query.*;
import org.elasticsearch.search.SearchHit;
import org.elasticsearch.search.builder.SearchSourceBuilder;
import org.elasticsearch.search.fetch.subphase.highlight.HighlightBuilder;
import org.elasticsearch.search.sort.SortOrder;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
@Slf4j
@Service
public class ArticleSearchServiceImpl implements ArticleSearchService {
@Autowired
private RestHighLevelClient restHighLevelClient;
@Autowired
private ApUserSearchService apUserSearchService;
/**
* es文章分页检索
*
* @param dto
* @return
*/
@Override
public ResponseResult search(UserSearchDto dto) throws IOException {
// 1.检查参数
if (dto == null || StringUtils.isBlank(dto.getSearchWords())) {
return ResponseResult.errorResult(AppHttpCodeEnum.PARAM_INVALID);
}
log.info("文章查询...");
//异步调用保存历史记录
ApUser user = AppThreadLocalUtils.getUser();
//用户信息不为空并且为首页搜索才进行保存
if(user != null && dto.getFromIndex() == 0) {
apUserSearchService.insert(dto.getSearchWords(),user.getId());
}
// 2.设置查询条件
SearchRequest searchRequest = new SearchRequest("app_info_article");
SearchSourceBuilder searchSourceBuilder = new SearchSourceBuilder();
// 布尔查询
BoolQueryBuilder boolQueryBuilder = QueryBuilders.boolQuery();
// 关键字的分词之后查询
QueryStringQueryBuilder queryStringQueryBuilder = QueryBuilders.queryStringQuery
(dto.getSearchWords()).field("title").field("content").defaultOperator(Operator.OR);
boolQueryBuilder.must(queryStringQueryBuilder);
// 查询小于mindate的数据
RangeQueryBuilder rangeQueryBuilder = QueryBuilders.rangeQuery("publishTime")
.lt(dto.getMinBehotTime().getTime());
boolQueryBuilder.filter(rangeQueryBuilder);
// 分页查询
searchSourceBuilder.from(0);
searchSourceBuilder.size(dto.getPageSize());
// 按照发布时间倒序查询
searchSourceBuilder.sort("publishTime", SortOrder.DESC);
// 设置高亮 title
HighlightBuilder highlightBuilder = new HighlightBuilder();
highlightBuilder.field("title");
highlightBuilder.preTags("");
highlightBuilder.postTags("");
searchSourceBuilder.highlighter(highlightBuilder);
searchSourceBuilder.query(boolQueryBuilder);
searchRequest.source(searchSourceBuilder);
SearchResponse searchResponse = restHighLevelClient.search(searchRequest, RequestOptions.DEFAULT);
// 3.结果封装返回
List
⑦在app网关添加如下配置
#搜索微服务
- id: headlines-search
uri: lb://headlines-search
predicates:
- Path=/search/**
filters:
- StripPrefix= 1
启动项目进行测试,至少要启动文章微服务,用户微服务,搜索微服务,app网关微服务,app前端工程,由于我的云服务器已经过期了,所以我就不进行展示了,代码是没问题的,有问题的可以随时私信我。
下篇预告:文章自动构建索引&搜索记录&关键词联想
友情链接: 牛客网 刷题|面试|找工作神器