【搜索文章】:搜索(es)+ 搜索记录(mongodb)+ 搜索联想词

embedded/2025/2/9 3:52:23/

需求

用户输入关键字时,可以检索出结果,
在这里插入图片描述
并且可以查看历史搜索情况,在这里插入图片描述
还可以进行联想词展示。在这里插入图片描述

ElasticSearch(搜索)

准备工作

  1. 使用docker安装es,配置ik分词器
  2. 重新建一个search模块,用来写搜索微服务的业务代码
  3. 导入es的依赖
  4. 配置RestHighLevelClient
@Getter
@Setter
@Configuration
@ConfigurationProperties(prefix = "elasticsearch")
public class ElasticSearchConfig {private String host;private int port;@Beanpublic RestHighLevelClient client(){System.out.println(host);System.out.println(port);return new RestHighLevelClient(RestClient.builder(new HttpHost(host,port,"http")));}
}
spring:autoconfigure:exclude: org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration
elasticsearch:host: 192.168.140.102port: 9200
  1. 初始化索引库数据(项目上线之前需要批量导入):
@Autowired
private ApArticleMapper apArticleMapper;@Autowiredprivate RestHighLevelClient restHighLevelClient;/*** 注意:数据量的导入,如果数据量过大,需要分页导入* @throws Exception*/@Testpublic 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);}

文章搜索

  1. 单一条件查询:直接放入SearchSourceBuilder
    如果查询逻辑简单,只有一个独立条件,可以直接将条件放入SearchSourceBuilder的query方法中
SearchSourceBuilder sourceBuilder = new SearchSourceBuilder();
sourceBuilder.query(QueryBuilders.termQuery("status", "active"));
  1. 组合多个条件:必须使用BoolQueryBuilder,当需要组合多个条件(如 AND/OR/NOT 逻辑)时,必须显式使用 BoolQueryBuilder。
类型作用是否影响评分是否可缓存
must子条件,必须满足,类似逻辑 AND✅ 是❌ 否
filter子条件 必须满足,但不参与相关性评分❌ 否✅ 是(可缓存)
BoolQueryBuilder boolQuery = QueryBuilders.boolQuery().must(QueryBuilders.termQuery("status", "active")) // AND 条件.must(QueryBuilders.rangeQuery("age").gte(18)) // 另一个 AND 条件.should(QueryBuilders.termQuery("tag", "urgent")) // OR 条件.mustNot(QueryBuilders.termQuery("deleted", true)); // NOT 条件SearchSourceBuilder sourceBuilder = new SearchSourceBuilder();
sourceBuilder.query(boolQuery);

虽然技术上可以将所有查询都包装成 BoolQuery,但直接使用单一条件更简洁

private final RestHighLevelClient restHighLevelClient;
@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 searchSourceBuilder = new SearchSourceBuilder();// boolQuery主要是构建复杂的查询逻辑BoolQueryBuilder boolQuery = QueryBuilders.boolQuery(); // 布尔查询// 2-1. 关键词分词后查询QueryStringQueryBuilder queryStringQueryBuilder = QueryBuilders.queryStringQuery(dto.getSearchWords()) // 分词之后再查询.field("title") // 对标题分词.field("content") // 对内容分词.defaultOperator(Operator.OR);// 分词之后的条件(或的关系)boolQuery.must(queryStringQueryBuilder); // 2-1. 放入布尔查询中(must:参与算分)// 2-2. 查询小于minBehotTime的数据RangeQueryBuilder rangeQueryBuilder = QueryBuilders.rangeQuery("publishTime") // 发布时间.lt(dto.getMinBehotTime().getTime());// 小于minBehotTimeboolQuery.filter(rangeQueryBuilder); // 2-2. 放入布尔查询中(filter:不参与算分)// 2-3. 分页查询searchSourceBuilder.from(0);searchSourceBuilder.size(dto.getPageSize());// 2-4. 按照发布时间倒叙查询searchSourceBuilder.sort("publishTime", SortOrder.DESC);// 2-5. 设置高亮HighlightBuilder highlightBuilder = new HighlightBuilder();highlightBuilder.field("title");// 哪个字段高亮highlightBuilder.preTags("<font style='color: red; font-size: inherit;'>"); // 高亮字段前缀highlightBuilder.postTags("</font>"); // 高亮字段的后缀searchSourceBuilder.highlighter(highlightBuilder);searchSourceBuilder.query(boolQuery);searchRequest.source(searchSourceBuilder);SearchResponse searchResponse = restHighLevelClient.search(searchRequest, RequestOptions.DEFAULT);// 3. 结果封装返回SearchHit[] hits = searchResponse.getHits().getHits();List<Map> list = new ArrayList<>();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); // 高亮之后的titlemap.put("h_title", title); // 设置高亮标题}else {map.put("h_title", map.get("title")); // 没有设置高亮,就把原本的标题放入h_title中}list.add(map);}return ResponseResult.okResult(list);
}

新增文章创建索引

在这里插入图片描述
思路:文章审核成功后使用kafka发送消息,文章微服务是消息的生产者;搜索微服务接收到消息后,添加数据到索引库,搜索微服务是消息的消费者。

  1. 文章微服务(生产者)

到yml中配置生产者:

spring:kafka:bootstrap-servers: 192.168.140.102:9092producer:# 重试次数retries: 10# key、value的序列化器key-serializer: org.apache.kafka.common.serialization.StringSerializervalue-serializer: org.apache.kafka.common.serialization.StringSerializer

往消息队列中发送消息:

// 发送消息,创建索引
SearchArticleVo searchArticleVo = new SearchArticleVo();
BeanUtils.copyProperties(article, searchArticleVo);
searchArticleVo.setContent(dto.getContent());
searchArticleVo.setStaticUrl(path);
kafkaTemplate.send(ArticleConstants.ARTICLE_ES_SYNC_TOPIC, JSON.toJSONString(searchArticleVo));
  1. 搜索微服务(消费者)

到yml中配置消费者:

spring:kafka:bootstrap-servers: 192.168.140.102:9092consumer:# 消费组group-id: ${spring.application.name}# key、value的反序列化器key-deserializer: org.apache.kafka.common.serialization.StringDeserializervalue-deserializer: org.apache.kafka.common.serialization.StringDeserializer

mongodb_191">mongodb(搜索记录)

需要给每个用户保存一份搜索记录,数据量大,要求加载速度快,通常这样的数据存储到mongodb更合适,不建议存到mysql中。

  1. mongodb
    • 支持分片,适合存储用户搜索日志这种持续写入的场景
    • 基于磁盘存储,成本低
  2. mysql:
    • 对高频写入(如每秒数千次插入)的支持较弱
    • 搜索记录通常是半结构化或非结构化数据,需频繁变更表结构来适应新字段
  3. redis:
    • redis基于内存的,内存成本高,适合存储热数据(如缓存)
    • Redis 的 RDB 快照和 AOF 日志是异步持久化机制,在宕机时可能丢失部分数据
    • 数据量过大时,从磁盘加载备份到内存的恢复过程耗时较长
  • MongoDB:适合作为主存储,满足海量数据、灵活查询、低成本持久化的核心需求。
  • Redis:适合作为缓存层,加速近期数据的访问,但无法替代 MongoDB 的长期存储角色。
  • MySQL:不适合高频写入和非结构化日志场景。

准备工作

1. 配置环境

使用docker安装mongodb

docker run -di \
--name mongo-service \
--restart=always \
-p 27017:27017 \
-v ~/data/mongodata:/data \
mongo

mongodb_220">2. springboot集成mongodb

  1. 添加mongodb依赖:
<dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-data-mongodb</artifactId>
</dependency>
  1. 配置mongodb
spring:data:mongodb:host: 192.168.140.102port: 27017database: leadnews-history
  1. 映射
@Data
@Document("ap_associate_words") // 映射哪个集合【mongodb表名】
public class ApAssociateWords implements Serializable {private static final long serialVersionUID = 1L;private String id;/*** 联想词*/private String associateWords;/*** 创建时间*/private Date createdTime;}
  1. 核心方法
  • 保存或修改:
@Autowired
private MongoTemplate mongoTemplate;//保存
@Test
public void saveTest(){ApAssociateWords apAssociateWords = new ApAssociateWords();apAssociateWords.setAssociateWords("黑马头条");apAssociateWords.setCreatedTime(new Date());mongoTemplate.save(apAssociateWords);
}
  • 查询一个对象
@Test
public void saveFindOne(){ApAssociateWords apAssociateWords = mongoTemplate.findById("67a330c35faec30826dcbe8e", 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);
}

保存搜索记录

在这里插入图片描述

用户搜索后,为了让用户能更快的得到搜索的结果,异步发送请求记录关键字。
在这里插入图片描述

private final MongoTemplate mongoTemplate;
// 保存搜索记录
@Override
@Async
public void save(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); // 有id-修改、没有id-新增return;}// 3. 不存在 - 判断该用户的当前历史总数量是否 > 10apUserSearch = new ApUserSearch();apUserSearch.setUserId(userId);apUserSearch.setKeyword(keyword);apUserSearch.setCreatedTime(new Date());// 4. 当前用户的当前历史总数量 < 10 - 直接保存Query query1 = Query.query(Criteria.where("userId").is(userId));query1.with(Sort.by(Sort.Direction.DESC, "createdTime")); // 按照时间倒序排列List<ApUserSearch> apUserSearches = mongoTemplate.find(query1, ApUserSearch.class);if(apUserSearches == null || apUserSearches.size() < 10) {mongoTemplate.save(apUserSearch); // 直接保存}else {// 5. 当前用户的当前历史总数量 >= 10 - 替换最后一条记录ApUserSearch lastUserSearch = apUserSearches.get(apUserSearches.size() - 1);mongoTemplate.findAndReplace(Query.query(Criteria.where("id").is(lastUserSearch.getId())), apUserSearch); // 修改最后一条记录}
}

在之前写的文章搜索的业务代码中,异步调用“保存搜索记录”的方法。
其中:userId通过app网关的过滤器拦截到前端发过来的userId,并把userId放到请求头中传给搜索微服务,搜索微服务的拦截器获取app网关发来的userId,存到ThreadLocal中。
注意:由于是异步调用save方法,是又开了一个线程,此时这个线程是没办法从ThreadLocal中获取到userId,只能通过主线程传过来。

查询搜索历史

public ResponseResult findUserSearch() {// 获取当前用户ApUser user = AppThreadLocalUtil.getUser();// 根据用户查询当前数据(按照时间倒叙)if(user == null) {return ResponseResult.errorResult(AppHttpCodeEnum.NEED_LOGIN);}List<ApUserSearch> list = mongoTemplate.find(Query.query(Criteria.where("userId").is(user.getId())).with(Sort.by(Sort.Direction.DESC, "createdTime")), ApUserSearch.class);return ResponseResult.okResult(list);}

根据用户id和当前某个用户的id查找记录,并按照创建时间降序排列。

删除某一个历史记录

public ResponseResult delUserSearch(HistorySearchDto dto) {// 检查参数if (dto.getId() == null) {return ResponseResult.errorResult(AppHttpCodeEnum.PARAM_INVALID);}// 获取当前用户ApUser user = AppThreadLocalUtil.getUser();// 判断是否登录if(user == null) {return ResponseResult.errorResult(AppHttpCodeEnum.NEED_LOGIN);}// 删除mongoTemplate.remove(Query.query(Criteria.where("userId").is(user.getId()).and("id").is(dto.getId())), ApUserSearch.class);return ResponseResult.okResult(AppHttpCodeEnum.SUCCESS);
}

根据用户id和当前某个搜索记录的id进行删除

搜索联想词

搜索词(数据来源)

使用网上搜索频率较高的一些词:

  1. 自己维护联想词:通过分析用户搜索频率较高的词,按照排名作为搜索词
  2. 第三方获取:5118…

导入联想词

在这里插入图片描述

实现

正则表达式:
在这里插入图片描述

// 搜索联想词
@Override
public ResponseResult search(UserSearchDto dto) {// 1. 检查参数if(StringUtils.isBlank(dto.getSearchWords())) {return ResponseResult.errorResult(AppHttpCodeEnum.PARAM_INVALID);}// 2. 分页检查(最多只能查询20条)if(dto.getPageSize() > 20) {dto.setPageSize(20);}// 3. 执行模糊查询String regexStr = ".*?\\" + dto.getSearchWords() + ".*";Query query = Query.query(Criteria.where("associateWords").regex(regexStr)).limit(dto.getPageSize());List<ApAssociateWords> list = mongoTemplate.find(query, ApAssociateWords.class);return ResponseResult.okResult(list);
}

其实搜索联想词,就是提前先把词库导入到mongodb表中,用户在输入的时候,就会对这个表进行模糊查询,遇到符合条件的就立马匹配。


http://www.ppmy.cn/embedded/160702.html

相关文章

利用deepseek参与软件测试 基本架构如何 又该在什么环节接入deepseek

利用DeepSeek参与软件测试&#xff0c;可以考虑以下基本架构和接入环节&#xff1a; ### 基本架构 - **数据层** - **测试数据存储**&#xff1a;用于存放各种测试数据&#xff0c;包括正常输入数据、边界值数据、异常数据等&#xff0c;这些数据可以作为DeepSeek的输入&…

DeepSeek-R1:开源机器人智能控制系统的革命性突破

目录 引言 一、DeepSeek-R1 的概述 1.1 什么是 DeepSeek-R1&#xff1f; 1.2 DeepSeek-R1 的定位 二、DeepSeek-R1 的核心特性 2.1 实时控制能力 2.2 多传感器融合 2.3 路径规划与导航 2.4 人工智能集成 2.5 开源与模块化设计 2.6 跨平台支持 三、DeepSeek-R1 的技术…

Java并发编程笔记

Java并发基础知识补全 启动 启动线程的方式只有&#xff1a; 1、X extends Thread;&#xff0c;然后X.start 2、X implements Runnable&#xff1b;然后交给Thread运行 线程的状态 Java中线程的状态分为6种&#xff1a; 1. 初始(NEW)&#xff1a;新创建了一个线程对象&…

【QT】控件 -- 多元素类 | 容器类 | 布局类

&#x1f525; 目录 一、多元素类1. List Widget -- 列表2. Table Widget -- 表格3. Tree Widget -- 树形 二、容器类1. Group Box -- 分组框2. Tab Widget -- 标签页 三、布局类1. 垂直布局【使用 QVBoxLayout 管理多个控件】【创建两个 QVBoxLayout】 2. 水平布局【使用 QHBo…

在亚马逊云科技上云原生部署DeepSeek-R1模型(上)

DeepSeek-R1在开源版本发布的第二天就登陆了亚马逊云科技AWS云平台&#xff0c;这个速度另小李哥十分震惊。这又让我想起了在亚马逊云科技全球云计算大会re:Invent2025里&#xff0c;亚马逊CEO Andy Jassy说过的&#xff1a;随着目前生成式AI应用规模的扩大&#xff0c;云计算的…

[论文笔记] Deepseek-R1R1-zero技术报告阅读

启发: 1、SFT&RL的训练数据使用CoT输出的格式,先思考再回答,大大提升模型的数学与推理能力。 2、RL训练使用群体相对策略优化(GRPO),奖励模型是规则驱动,准确性奖励和格式化奖励。 1. 总体概述 背景与目标 报告聚焦于利用强化学习(RL)提升大型语言模型(LLMs)…

c++计算机教程

目的 做出-*/%计算机 要求 做出可以计算-*/%的计算机 实现 完整代码 #include<bits/stdc.h> int main() {std::cout<<"加 减- 乘* 除/ 取余% \没有了|(因为可以算三位)"<<"\n"<<"提示:每打完一个符号或打完一个数,\…

Java基础知识总结(四十八)--TCP传输、TCP客户端、TCP服务端

TCP传输&#xff1a;两个端点的建立连接后会有一个传输数据的通道&#xff0c;这通道称为流&#xff0c;而且是建立在网络基础上的流&#xff0c;称之为socket流。该流中既有读取&#xff0c;也有写入。 tcp的两个端点&#xff1a;一个是客户端&#xff0c;一个是服务端。 客户…