一、今日内容介绍
1. App端搜索效果图
2. 今日内容
(1)文章搜索
- Elasticsearch环境搭建
- 索引库创建
- 文章搜索多条件复合查询
- 索引数据同步
(2)搜索历史记录
- MongoDB环境搭建
- 异步保存搜索历史
- 查看搜索历史列表
- 删除搜索历史
(3)联想词查询
- 联想词的来源
- 联想词功能实现
二、app端文章搜索
1. ElasticSearch环境搭建
ElasticSearch详解:Docs
步骤①:拉取ElasticSearch镜像
docker pull elasticsearch:7.4.0
②创建ElasticSerach容器
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
③配置中文分词器ik
进入文件夹/usr/share/elasticsearch/plugins/
cd /usr/share/elasticsearch/plugins/
创建并进入文件夹
mkdir analysis-ik
cd analysis-ik
把资料中的elasticsearch-analysis-ik-7.4.0.zip上传到服务器上,解压
unzip elasticsearch-analysis-ik-7.4.0.zip
删除压缩包
rm -rf elasticsearch-analysis-ik-7.4.0.zip
重启elasticsearch
docker restart elasticsearch
④使用postman测试
2. app端文章搜索
2.1 需求分析
- 用户输入关键可搜索文章列表
- 关键词高亮显示
- 文章列表展示与home展示一样,当用户点击某一篇文章,可查看文章详情
2.2 思路分析
为了加快检索的效率,在查询的时候不会直接从数据库中查询文章,需要在elasticsearch中进行高速检索。
2.3 创建索引和映射
使用postman添加映射
put请求 : http://192.168.200.130:9200/app_info_article
{
"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"
}
}
}
}
注:老师提供的虚拟机里已经创建过了,不用再创建了
2.4 数据初始化到索引库
步骤①:将资料中的es_init导入到heima-leadnews-test工程下
在pom.xml(heima-leadnews-test)添加模块
<modules>
<module>freemarker-demo</module>
<module>minio-demo</module>
<module>tess4j-demo</module>
<module>other-demo</module>
<module>kafka-demo</module>
<module>es-init</module>
</modules>
②查询所有的文章信息,批量导入到es索引库中
package com.heima.es;
import com.alibaba.fastjson.JSON;
import com.heima.es.mapper.ApArticleMapper;
import com.heima.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<SearchArticleVo> 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);
}
}
根据个人情况修改数据库连接密码
运行单元测试
③使用postman查询所有的es中数据
http://192.168.200.130:9200/app_info_article/_search
2.5 文章搜索功能实现
(1)搭建搜索微服务
步骤①:导入heima-leadnews-search微服务
在pom.xml(heima-leadnews-service)添加模块
<modules>
<module>heima-leadnews-user</module>
<module>heima-leadnews-article</module>
<module>heima-leadnews-wemedia</module>
<module>heima-leadnews-schedule</module>
<module>heima-leadnews-search</module>
</modules>
②在pom.xml(heima-leadnews-service)中添加依赖
<!--elasticsearch-->
<dependency>
<groupId>org.elasticsearch.client</groupId>
<artifactId>elasticsearch-rest-high-level-client</artifactId>
<version>7.4.0</version>
</dependency>
<dependency>
<groupId>org.elasticsearch.client</groupId>
<artifactId>elasticsearch-rest-client</artifactId>
<version>7.4.0</version>
</dependency>
<dependency>
<groupId>org.elasticsearch</groupId>
<artifactId>elasticsearch</artifactId>
<version>7.4.0</version>
</dependency>
③在nacos配置中心添加leadnews-search.yml
spring:
autoconfigure:
exclude: org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration
elasticsearch:
host: 192.168.200.130
port: 9200
(2)搜索接口定义
步骤①:UserSearchDto
package com.heima.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);
}
}
②ArticleSearchController
package com.heima.search.controller.v1;
import com.heima.model.common.dtos.ResponseResult;
import com.heima.model.search.dtos.UserSearchDto;
import com.heima.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);
}
}
③ArticleSearchService
package com.heima.search.service;
import com.heima.model.common.dtos.ResponseResult;
import com.heima.model.search.dtos.UserSearchDto;
import java.io.IOException;
public interface ArticleSearchService {
/**
* ES文章分页搜索
* @param dto
* @return
* @throws IOException
*/
ResponseResult search(UserSearchDto dto) throws IOException;
}
④ArticleSearchServiceImpl
package com.heima.search.service.impl;
import com.alibaba.fastjson.JSON;
import com.heima.model.common.dtos.ResponseResult;
import com.heima.model.common.enums.AppHttpCodeEnum;
import com.heima.model.search.dtos.UserSearchDto;
import com.heima.search.service.ArticleSearchService;
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;
@Service
@Slf4j
public class ArticleSearchServiceImpl implements ArticleSearchService {
@Autowired
private RestHighLevelClient restHighLevelClient;
/**
* es文章分页搜索
* @param dto
* @return
* @throws IOException
*/
@Override
public ResponseResult search(UserSearchDto dto) throws IOException {
// 1. 检查参数
if(dto == null || StringUtils.isBlank(dto.getSearchWords())) {
return ResponseResult.errorResult(AppHttpCodeEnum.PARAM_INVALID);
}
// 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("<font style='color: red; font-size: inherit;'>");
highlightBuilder.postTags("</font>");
searchSourceBuilder.highlighter(highlightBuilder);
searchSourceBuilder.query(boolQueryBuilder);
searchRequest.source(searchSourceBuilder);
SearchResponse searchResponse = restHighLevelClient.search(searchRequest, RequestOptions.DEFAULT);
// 3. 结果封装返回
List<Map> list = new ArrayList<>();
SearchHit[] hits = searchResponse.getHits().getHits();
for (SearchHit hit : hits) {
String json = hit.getSourceAsString();
Map map = JSON.parseObject(json, Map.class);
// 处理高亮
if(hit.getHighlightFields() != null && hit.getHighlightFields().size() > 0) {
Text[] titles = hit.getHighlightFields().get("title").getFragments();
String title = StringUtils.join(titles);
// 高亮标题
map.put("h_title", title);
} else {
// 原始标题
map.put("h_title", map.get("title"));
}
list.add(map);
}
return ResponseResult.okResult(list);
}
}
⑤在nacos配置中心的leadnews-app-gateway.yml添加搜索微服务的路由配置
#搜索微服务
- id: leadnews-search
uri: lb://leadnews-search
predicates:
- Path=/search/**
filters:
- StripPrefix= 1
⑥启动以下微服务进行测试
2.6 文章自动审核构建索引
(1)思路分析
(2)文章微服务发送消息
①把SearchArticleVo放到model工程下
package com.heima.model.search.vos;
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;
}
②在ArticleConstants类中添加新的常量
package com.heima.common.constants;
public class ArticleConstants {
public static final Short LOADTYPE_LOAD_MORE = 1;
public static final Short LOADTYPE_LOAD_NEW = 2;
public static final String DEFAULT_TAG = "__all__";
public static final String ARTICLE_ES_SYNC_TOPIC = "article.es.sync.topic";
public static final Integer HOT_ARTICLE_LIKE_WEIGHT = 3;
public static final Integer HOT_ARTICLE_COMMENT_WEIGHT = 5;
public static final Integer HOT_ARTICLE_COLLECTION_WEIGHT = 8;
public static final String HOT_ARTICLE_FIRST_PAGE = "hot_article_first_page_";
}
③在文章微服务的ArticleFreemarkerServiceImpl中的buildArticleToMinIO方法中收集数据并发送消息
package com.heima.article.service.impl;
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONArray;
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import com.heima.article.mapper.ApArticleContentMapper;
import com.heima.article.service.ApArticleService;
import com.heima.article.service.ArticleFreemarkerService;
import com.heima.common.constants.ArticleConstants;
import com.heima.file.service.FileStorageService;
import com.heima.model.article.pojos.ApArticle;
import com.heima.model.search.vos.SearchArticleVo;
import freemarker.template.Configuration;
import freemarker.template.Template;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.BeanUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.kafka.core.KafkaTemplate;
import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.io.ByteArrayInputStream;
import java.io.InputStream;
import java.io.StringWriter;
import java.util.HashMap;
import java.util.Map;
@Service
@Slf4j
@Transactional
public class ArticleFreemarkerServiceImpl implements ArticleFreemarkerService {
@Autowired
private ApArticleContentMapper apArticleContentMapper;
@Autowired
private Configuration configuration;
@Autowired
private ApArticleService apArticleService;
@Autowired
private FileStorageService fileStorageService;
@Autowired
private KafkaTemplate<String, String> kafkaTemplate;
/**
* 生成静态文件上传到MinIO中
* @param apArticle
* @param content
*/
@Async // 异步
@Override
public void buildArticleToMinIO(ApArticle apArticle, String content) {
// 已知文章的id
// 1. 获取文章内容
if(StringUtils.isNotBlank(content)) {
// 2. 文章内容通过freemarker生成html
Template template = null;
StringWriter out = new StringWriter();
try {
// 获取模板
template = configuration.getTemplate("article.ftl");
// 数据模型
Map<String, Object> contentDataModel = new HashMap<>();
contentDataModel.put("content", JSONArray.parseArray(content));
// 合成
template.process(contentDataModel, out);
} catch (Exception e) {
e.printStackTrace();
}
// 3. 把html文件上传到MinIO中
InputStream in = new ByteArrayInputStream(out.toString().getBytes());
String path = fileStorageService.uploadHtmlFile("", apArticle.getId() + ".html", in);
// 4. 修改ap_article表,保存static_url字段
apArticleService.update(Wrappers
.<ApArticle>lambdaUpdate()
.eq(ApArticle::getId, apArticle.getId())
.set(ApArticle::getStaticUrl, path));
// 5. 发送消息,创建索引
createArticleESIndex(apArticle, content, path);
}
}
/**
* 发送消息,创建索引
* @param apArticle
* @param content
* @param path
*/
private void createArticleESIndex(ApArticle apArticle, String content, String path) {
SearchArticleVo vo = new SearchArticleVo();
BeanUtils.copyProperties(apArticle, vo);
vo.setContent(content);
vo.setStaticUrl(path);
kafkaTemplate.send(ArticleConstants.ARTICLE_ES_SYNC_TOPIC, JSON.toJSONString(vo));
}
}
④在文章微服务的nacos配置中心添加如下配置 - leadnews-article.yml
kafka:
bootstrap-servers: 192.168.200.130:9092
producer:
retries: 10
key-serializer: org.apache.kafka.common.serialization.StringSerializer
value-serializer: org.apache.kafka.common.serialization.StringSerializer
⑤定义监听接收消息,保存索引数据
package com.heima.search.listener;
import com.alibaba.fastjson.JSON;
import com.heima.common.constants.ArticleConstants;
import com.heima.model.search.vos.SearchArticleVo;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.elasticsearch.action.index.IndexRequest;
import org.elasticsearch.client.RequestOptions;
import org.elasticsearch.client.RestHighLevelClient;
import org.elasticsearch.common.xcontent.XContentType;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.kafka.annotation.KafkaListener;
import org.springframework.stereotype.Component;
import java.io.IOException;
@Component
@Slf4j
public class SyncArticleListener {
@Autowired
private RestHighLevelClient restHighLevelClient;
@KafkaListener(topics = ArticleConstants.ARTICLE_ES_SYNC_TOPIC)
public void onMessage(String message) {
if(StringUtils.isNotBlank(message)) {
log.info("SyncArticleListener, message = {}", message);
SearchArticleVo searchArticleVo = JSON.parseObject(message, SearchArticleVo.class);
IndexRequest indexRequest = new IndexRequest("app_info_article");
indexRequest.id(searchArticleVo.getId().toString());
indexRequest.source(message, XContentType.JSON);
try {
restHighLevelClient.index(indexRequest, RequestOptions.DEFAULT);
} catch (IOException e) {
e.printStackTrace();
log.error("sync es error = {}", e);
}
}
}
}
⑥启动以下7个微服务进行测试
3. app端搜索-搜索记录
3.1 需求分析
3.2 数据存储说明
用户的搜索记录,需要给每一个用户都保存一份,数据量较大,要求加载速度快,通常这样的数据存储到mongoDB更合适,不建议直接存储到关系型数据库中。
3.3 MongoDB安装及集成
(1)安装MongoDB
步骤①:拉取镜像
docker pull mongo
②创建容器
docker run -di --name mongo-service --restart=always -p 27017:27017 -v ~/data/mongodata:/data mongo
(2)导入资料中的mongo-demo项目到heima-leadnews-test中
pom.xml(heima-leadnews-test)中添加模块
<modules>
<module>freemarker-demo</module>
<module>minio-demo</module>
<module>tess4j-demo</module>
<module>other-demo</module>
<module>kafka-demo</module>
<module>es-init</module>
<module>mongo-demo</module>
</modules>
其中有三项配置比较关键:
第一:mongo依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-mongodb</artifactId>
</dependency>
第二:mongo配置
server:
port: 9998
spring:
data:
mongodb:
host: 192.168.200.130
port: 27017
database: leadnews-history
第三:映射
package com.itheima.mongo.pojo;
import lombok.Data;
import org.springframework.data.mongodb.core.mapping.Document;
import java.io.Serializable;
import java.util.Date;
/**
* <p>
* 联想词表
* </p>
*
* @author itheima
*/
@Data
@Document("ap_associate_words")
public class ApAssociateWords implements Serializable {
private static final long serialVersionUID = 1L;
private String id;
/**
* 联想词
*/
private String associateWords;
/**
* 创建时间
*/
private Date createdTime;
}
核心方法:
package com.itheima.mongo.test;
import com.itheima.mongo.MongoApplication;
import com.itheima.mongo.pojo.ApAssociateWords;
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.data.domain.Sort;
import org.springframework.data.mongodb.core.MongoTemplate;
import org.springframework.data.mongodb.core.query.Criteria;
import org.springframework.data.mongodb.core.query.Query;
import org.springframework.test.context.junit4.SpringRunner;
import java.util.Date;
import java.util.List;
@SpringBootTest(classes = MongoApplication.class)
@RunWith(SpringRunner.class)
public class MongoTest {
@Autowired
private MongoTemplate mongoTemplate;
//保存
@Test
public void saveTest(){
/*for (int i = 0; i < 10; i++) {
ApAssociateWords apAssociateWords = new ApAssociateWords();
apAssociateWords.setAssociateWords("黑马头条");
apAssociateWords.setCreatedTime(new Date());
mongoTemplate.save(apAssociateWords);
}*/
ApAssociateWords apAssociateWords = new ApAssociateWords();
apAssociateWords.setAssociateWords("黑马直播");
apAssociateWords.setCreatedTime(new Date());
mongoTemplate.save(apAssociateWords);
}
//查询一个
@Test
public void saveFindOne(){
ApAssociateWords apAssociateWords = mongoTemplate.findById("60bdbfc1f2c2745b46b7f4a1", ApAssociateWords.class);
System.out.println(apAssociateWords);
}
//条件查询
@Test
public void testQuery(){
Query query = Query.query(Criteria.where("associateWords").is("黑马头条"))
.with(Sort.by(Sort.Direction.DESC,"createdTime"));
List<ApAssociateWords> apAssociateWordsList = mongoTemplate.find(query, ApAssociateWords.class);
System.out.println(apAssociateWordsList);
}
@Test
public void testDel(){
mongoTemplate.remove(Query.query(Criteria.where("associateWords").is("黑马头条")),ApAssociateWords.class);
}
}
IDEA连接MongoDB
3.4 保存搜索记录
(1)实现思路
用户输入关键字进行搜索,异步记录关键字
(2)实现步骤
步骤①:在搜索微服务集成MongoDB - pom.xml(heima-leadnews-search)
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-mongodb</artifactId>
</dependency>
②添加nacos配置
spring:
data:
mongodb:
host: 192.168.200.130
port: 27017
database: leadnews-history
③拷贝对应的实体类到搜索微服务heima-leadnews-search下
④创建ApUserSearchService新增insert方法
package com.heima.search.service;
public interface ApUserSearchService {
/**
* 保存用户搜索历史记录
* @param keyword
* @param userId
*/
public void insert(String keyword, Integer userId);
}
⑤实现类 - ApUserSearchServiceImpl
package com.heima.search.service.impl;
import com.heima.search.pojos.ApUserSearch;
import com.heima.search.service.ApUserSearchService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.domain.Sort;
import org.springframework.data.mongodb.core.MongoTemplate;
import org.springframework.data.mongodb.core.query.Criteria;
import org.springframework.data.mongodb.core.query.Query;
import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Service;
import java.util.Date;
import java.util.List;
@Service
@Slf4j
public class ApUserSearchServiceImpl implements ApUserSearchService {
@Autowired
private MongoTemplate mongoTemplate;
/**
* 保存用户搜索历史记录
* @param keyword
* @param userId
*/
@Override
@Async // 异步调用
public void insert(String keyword, Integer userId) {
// 1. 查询当前用户的搜索关键词
Query query = Query.query(Criteria.where("userId").is(userId).and("keyword").is(keyword));
ApUserSearch apUserSearch = mongoTemplate.findOne(query, ApUserSearch.class);
// 2. 存在则更新创建时间
if(apUserSearch != null) {
apUserSearch.setCreatedTime(new Date());
mongoTemplate.save(apUserSearch);
return;
}
// 3. 不存在,判断当前历史记录总数量是否超过10
apUserSearch = new ApUserSearch();
apUserSearch.setUserId(userId);
apUserSearch.setKeyword(keyword);
apUserSearch.setCreatedTime(new Date());
Query query1 = Query.query(Criteria.where("userId").is(userId));
query1.with(Sort.by(Sort.Direction.DESC, "createTime"));
List<ApUserSearch> apUserSearchList = mongoTemplate.find(query1, ApUserSearch.class);
if(apUserSearchList == null || apUserSearchList.size() < 10) {
// 少于10条搜索历史记录
mongoTemplate.save(apUserSearch);
} else {
// 删除第一条搜索记录
ApUserSearch firstUserSearch = apUserSearchList.get(0);
mongoTemplate.remove(Query.query(Criteria.where("id").is(firstUserSearch.getId())), ApUserSearch.class);
// 添加新的搜索记录
mongoTemplate.insert(apUserSearch);
/*// 获取最后一条搜索记录
ApUserSearch lastUserSearch = apUserSearchList.get(apUserSearchList.size() - 1);
// 替换最后一条搜索记录
mongoTemplate.findAndReplace(Query.query(Criteria.where("id").is(lastUserSearch.getId())), apUserSearch);*/
}
}
}
(按照老师的逻辑是移除最近的一条记录,,不太符合常理)
⑥在搜索微服务中获取当前登录的用户
在app端网关的AuthorizeFilter添加如下
package com.heima.app.gateway.filter;
import com.heima.app.gateway.util.AppJwtUtil;
import io.jsonwebtoken.Claims;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang.StringUtils;
import org.springframework.cloud.gateway.filter.GatewayFilterChain;
import org.springframework.cloud.gateway.filter.GlobalFilter;
import org.springframework.core.Ordered;
import org.springframework.http.HttpStatus;
import org.springframework.http.server.reactive.ServerHttpRequest;
import org.springframework.http.server.reactive.ServerHttpResponse;
import org.springframework.stereotype.Component;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;
@Component
@Slf4j
public class AuthorizeFilter implements Ordered, GlobalFilter {
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
// 1. 获取request和response对象
ServerHttpRequest request = exchange.getRequest();
ServerHttpResponse response = exchange.getResponse();
// 2. 判断是否是登录
if(request.getURI().getPath().contains("/login")) {
// 放行
return chain.filter(exchange);
}
// 3. 获取token
String token = request.getHeaders().getFirst("token");
// 4. 判断token是否存在
if(StringUtils.isBlank(token)) {
response.setStatusCode(HttpStatus.UNAUTHORIZED);
return response.setComplete();
}
// 5. 判断token是否有效
try {
Claims claimsBody = AppJwtUtil.getClaimsBody(token);
// 判断token是否过期
int result = AppJwtUtil.verifyToken(claimsBody);
if (result == 1 || result == 2) {
response.setStatusCode(HttpStatus.UNAUTHORIZED);
return response.setComplete();
}
// 获取token解析后的用户信息
Object userId = claimsBody.get("id");
// 存入header中
ServerHttpRequest serverHttpRequest = request.mutate().headers(httpHeaders -> {
httpHeaders.add("userId", userId + "");
}).build();
// 重置请求
exchange.mutate().request(serverHttpRequest);
} catch (Exception e) {
e.printStackTrace();
response.setStatusCode(HttpStatus.UNAUTHORIZED);
return response.setComplete();
}
// 6. 放行
return chain.filter(exchange);
}
/**
* 优先级设置:值越小,优先级越高
* @return
*/
@Override
public int getOrder() {
return 0;
}
}
在search微服务下添加拦截器
package com.heima.search.interceptor;
import com.heima.model.user.pojos.ApUser;
import com.heima.utils.thread.AppThreadLocalUtil;
import org.springframework.web.servlet.HandlerInterceptor;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
public class AppTokenInterceptor implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
String userId = request.getHeader("userId");
if(userId != null){
//存入到当前线程中
ApUser apUser = new ApUser();
apUser.setId(Integer.valueOf(userId));
AppThreadLocalUtil.setUser(apUser);
}
return true;
}
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
AppThreadLocalUtil.clear();
}
}
在utils微服务下添加AppThreadLocalUtil类
package com.heima.utils.thread;
import com.heima.model.user.pojos.ApUser;
public class AppThreadLocalUtil {
private final static ThreadLocal<ApUser> AP_USER_THREAD_LOCAL = new ThreadLocal<>();
/**
* 添加用户
* @param apUser
*/
public static void setUser(ApUser apUser) {
AP_USER_THREAD_LOCAL.set(apUser);
}
/**
* 获取用户
* @return
*/
public static ApUser getUser() {
return AP_USER_THREAD_LOCAL.get();
}
/**
* 清理用户
*/
public static void clear() {
AP_USER_THREAD_LOCAL.remove();
}
}
在article微服务中新建interceptor文件夹,新增AppTokenInterceptor类
package com.heima.article.interceptor;
import com.heima.model.user.pojos.ApUser;
import com.heima.utils.thread.AppThreadLocalUtil;
import org.springframework.web.servlet.HandlerInterceptor;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
public class AppTokenInterceptor implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
String userId = request.getHeader("userId");
if(userId != null){
//存入到当前线程中
ApUser apUser = new ApUser();
apUser.setId(Integer.valueOf(userId));
AppThreadLocalUtil.setUser(apUser);
}
return true;
}
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
AppThreadLocalUtil.clear();
}
}
search微服务下注册拦截器
package com.heima.search.config;
import com.heima.search.interceptor.AppTokenInterceptor;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
@Configuration
public class WebMvcConfig implements WebMvcConfigurer {
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(new AppTokenInterceptor()).addPathPatterns("/**");
}
}
⑦在ArticleSearchServiceImpl的search方法中调用保存历史记录
package com.heima.search.service.impl;
import com.alibaba.fastjson.JSON;
import com.heima.model.common.dtos.ResponseResult;
import com.heima.model.common.enums.AppHttpCodeEnum;
import com.heima.model.search.dtos.UserSearchDto;
import com.heima.model.user.pojos.ApUser;
import com.heima.search.service.ApUserSearchService;
import com.heima.search.service.ArticleSearchService;
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;
@Service
@Slf4j
public class ArticleSearchServiceImpl implements ArticleSearchService {
@Autowired
private RestHighLevelClient restHighLevelClient;
@Autowired
private ApUserSearchService apUserSearchService;
/**
* es文章分页搜索
* @param dto
* @return
* @throws IOException
*/
@Override
public ResponseResult search(UserSearchDto dto) throws IOException {
// 1. 检查参数
if(dto == null || StringUtils.isBlank(dto.getSearchWords())) {
return ResponseResult.errorResult(AppHttpCodeEnum.PARAM_INVALID);
}
// 2. 保存搜索历史记录
ApUser user = AppThreadLocalUtil.getUser();
if(user != null && dto.getFromIndex() == 0) {
apUserSearchService.insert(dto.getSearchWords(), user.getId());
}
// 3. 设置查询条件
// ... ...
return ResponseResult.okResult(list);
}
}
⑧在启动类添加@EnableAsync注解,开启异步调用
3.5 加载搜索记录列表
(1)思路分析
按照当前用户,按照时间倒序查询
说明 | |
接口路径 | /api/v1/history/load |
请求方式 | POST |
参数 | 无 |
响应结果 | ResponseResult |
(2)实现步骤
步骤①:新增ApUserSearchController
package com.heima.search.controller.v1;
import com.heima.model.common.dtos.ResponseResult;
import com.heima.search.service.ApUserSearchService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
@RequestMapping("/api/v1/history")
public class ApUserSearchController {
@Autowired
private ApUserSearchService apUserSearchService;
/**
* 查询搜索历史
* @return
*/
@PostMapping("/load")
public ResponseResult findUserSearch() {
return apUserSearchService.findUserSearch();
}
}
②在资料中拷贝两个mapper文件到mapper文件夹下
②在ApUserSearchService中新增方法
/**
* 查询搜索历史
* @return
*/
public ResponseResult findUserSearch();
③在ApUserSearchServiceImpl中实现方法
/**
* 查询搜索历史
* @return
*/
@Override
public ResponseResult findUserSearch() {
// 1. 获取当前用户
ApUser user = AppThreadLocalUtil.getUser();
if(user == null) {
return ResponseResult.errorResult(AppHttpCodeEnum.NEED_LOGIN);
}
// 2. 根据用户查询数据,按照时间倒序排序
List<ApUserSearch> apUserSearchList = mongoTemplate.find(Query.query(Criteria.where("userId").is(user.getId())).with(Sort.by(Sort.Direction.DESC, "createdTime")), ApUserSearch.class);
return ResponseResult.okResult(apUserSearchList);
}
④启动SearchApplication、AppGatewayApplication、ArticleApplication、UserApplication进行测试:http://localhost:8801/#/search
3.6 删除搜索历史
(1)接口定义
(2)实现步骤
①在model模块下新增HistorySearchDto
package com.heima.model.schedule.dtos;
import lombok.Data;
@Data
public class HistorySearchDto {
/**
* 接收搜索历史记录id
*/
String id;
}
②在ApUserSearchController新增方法
/**
* 删除搜索历史记录
* @param dto
* @return
*/
@PostMapping("/del")
public ResponseResult delUserSearch(@RequestBody HistorySearchDto dto) {
return apUserSearchService.delUserSearch(dto);
}
③在ApUserService中新增方法
/**
* 删除搜索历史记录
* @param dto
* @return
*/
public ResponseResult delUserSearch(HistorySearchDto dto);
④在ApUserServiceImpl中实现方法
/**
* 根据搜索记录id删除搜索历史记录
* @param dto
* @return
*/
@Override
public ResponseResult delUserSearch(HistorySearchDto dto) {
// 1. 检查参数
if(dto.getId() == null) {
return ResponseResult.errorResult(AppHttpCodeEnum.PARAM_INVALID);
}
// 2. 判断是否登录
ApUser user = AppThreadLocalUtil.getUser();
if(user == null) {
return ResponseResult.errorResult(AppHttpCodeEnum.NEED_LOGIN);
}
// 3. 删除
mongoTemplate.remove(Query.query(Criteria.where("userId").is(user.getId())
.and("id").is(dto.getId())), ApUserSearch.class);
return ResponseResult.okResult(AppHttpCodeEnum.SUCCESS);
}
⑤启动SearchApplication、AppGatewayApplication、ArticleApplication、UserApplication进行测试
4. app端搜索-关键词联想词
4.1 需求分析
根据用户输入的关键词展示联想词
4.2 搜索词 - 数据来源
通常是网上搜索频率比较高的一些词,通常在企业中有两部分来源:
第一:自己维护搜索词。通过分析用户搜索频率较高的词,按照排名作为搜索词
第二:第三方获取。关键词规划师(百度)、5118、爱站网
步骤①:导入资料中的ap_associate_words.js脚本到MongoDB中
5.3 功能实现
(1)接口定义
说明 | |
接口路径 | /api/v1/associate |
请求方式 | POST |
参数 | UserSearchDto |
响应结果 | ResponseResult |
(2)实现步骤
①新建接口 - ApAssociateWordsController
package com.heima.search.controller.v1;
import com.heima.model.common.dtos.ResponseResult;
import com.heima.model.search.dtos.UserSearchDto;
import com.heima.search.service.ApAssociateWordsService;
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;
@RestController
@RequestMapping("/api/v1/associate")
public class ApAssociateWordsController {
@Autowired
private ApAssociateWordsService apAssociateWordsService;
/**
* 查询联想词
* @param dto
* @return
*/
@PostMapping("/search")
public ResponseResult search(@RequestBody UserSearchDto dto) {
return apAssociateWordsService.findAssociate(dto);
}
}
②新建联想词业务接口 - ApAssociateWordsService
package com.heima.search.service;
import com.heima.model.common.dtos.ResponseResult;
import com.heima.model.search.dtos.UserSearchDto;
/**
* 联想词表 服务类
*/
public interface ApAssociateWordsService {
/**
* 查询联想词
* @param dto
* @return
*/
ResponseResult findAssociate(UserSearchDto dto);
}
③实现类 - ApAssociateWordsServiceImpl
package com.heima.search.service.impl;
import com.heima.model.common.dtos.ResponseResult;
import com.heima.model.common.enums.AppHttpCodeEnum;
import com.heima.model.search.dtos.UserSearchDto;
import com.heima.search.pojos.ApAssociateWords;
import com.heima.search.service.ApAssociateWordsService;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.mongodb.core.MongoTemplate;
import org.springframework.data.mongodb.core.query.Criteria;
import org.springframework.data.mongodb.core.query.Query;
import org.springframework.stereotype.Service;
import java.util.List;
@Service
public class ApAssociateWordsServiceImpl implements ApAssociateWordsService {
@Autowired
private MongoTemplate mongoTemplate;
/**
* 查询联想词
* @param dto
* @return
*/
@Override
public ResponseResult findAssociate(UserSearchDto dto) {
// 1. 参数检查
if(dto == null || StringUtils.isBlank(dto.getSearchWords())) {
return ResponseResult.errorResult(AppHttpCodeEnum.PARAM_INVALID);
}
// 2. 分页检查
if(dto.getPageSize() > 20) {
dto.setPageSize(20);
}
// 3. 执行查询,模糊查询
Query query = Query.query(Criteria.where("associateWords").regex(".*?\\" + dto.getSearchWords() + ".*"));
query.limit(dto.getPageSize());
List<ApAssociateWords> wordsList = mongoTemplate.find(query, ApAssociateWords.class);
return ResponseResult.okResult(wordsList);
}
}
正则表达式说明:
说明 | |
. | 表示匹配任意字符 |
* | 表示配置0次以上 |
*? | 表示非贪婪匹配,碰到符合条件的立马就匹配 |
④前后端联调测试