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

打印 上一主题 下一主题

主题 879|帖子 879|积分 2637

需求

用户输入关键字时,可以检索出结果,

并且可以查察汗青搜索环境,

还可以进行联想词展示。

ElasticSearch(搜索)

预备工作


  • 使用docker安装es,配置ik分词器
  • 重新建一个search模块,用来写搜索微服务的业务代码
  • 导入es的依靠
  • 配置RestHighLevelClient
  1. @Getter
  2. @Setter
  3. @Configuration
  4. @ConfigurationProperties(prefix = "elasticsearch")
  5. public class ElasticSearchConfig {
  6.     private String host;
  7.     private int port;
  8.     @Bean
  9.     public RestHighLevelClient client(){
  10.         System.out.println(host);
  11.         System.out.println(port);
  12.         return new RestHighLevelClient(RestClient.builder(
  13.                 new HttpHost(
  14.                         host,
  15.                         port,
  16.                         "http"
  17.                 )
  18.         ));
  19.     }
  20. }
复制代码
  1. spring:
  2.   autoconfigure:
  3.     exclude: org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration
  4. elasticsearch:
  5.   host: 192.168.140.102
  6.   port: 9200
复制代码

  • 初始化索引库数据(项目上线之前必要批量导入):
  1. @Autowired
  2. private ApArticleMapper apArticleMapper;
  3.   @Autowired
  4.   private RestHighLevelClient restHighLevelClient;
  5.   /**
  6.    * 注意:数据量的导入,如果数据量过大,需要分页导入
  7.    * @throws Exception
  8.    */
  9.   @Test
  10.   public void init() throws Exception {
  11.       // 1. 查询所有符合条件的文章数据
  12.       List<SearchArticleVo> searchArticleVos = apArticleMapper.loadArticleList();
  13.       // 2. 批量导入es索引库中
  14.       BulkRequest bulkRequest = new BulkRequest("app_info_article");
  15.       for (SearchArticleVo searchArticleVo : searchArticleVos) {
  16.           IndexRequest indexRequest = new IndexRequest().id(searchArticleVo.getId().toString())
  17.                   .source(JSON.toJSONString(searchArticleVo), XContentType.JSON);
  18.           bulkRequest.add(indexRequest); // 批量添加数据
  19.       }
  20.       restHighLevelClient.bulk(bulkRequest, RequestOptions.DEFAULT);
  21.   }
复制代码
文章搜索


  • 单一条件查询:直接放入SearchSourceBuilder
    如果查询逻辑简单,只有一个独立条件,可以直接将条件放入SearchSourceBuilder的query方法中
  1. SearchSourceBuilder sourceBuilder = new SearchSourceBuilder();
  2. sourceBuilder.query(QueryBuilders.termQuery("status", "active"));
复制代码

  • 组合多个条件:必须使用BoolQueryBuilder,当必要组合多个条件(如 AND/OR/NOT 逻辑)时,必须显式使用 BoolQueryBuilder。
范例作用是否影响评分是否可缓存must子条件,必须满足,雷同逻辑 AND✅ 是❌ 否filter子条件 必须满足,但不参与相关性评分❌ 否✅ 是(可缓存)
  1. BoolQueryBuilder boolQuery = QueryBuilders.boolQuery()
  2.     .must(QueryBuilders.termQuery("status", "active")) // AND 条件
  3.     .must(QueryBuilders.rangeQuery("age").gte(18)) // 另一个 AND 条件
  4.     .should(QueryBuilders.termQuery("tag", "urgent")) // OR 条件
  5.     .mustNot(QueryBuilders.termQuery("deleted", true)); // NOT 条件
  6. SearchSourceBuilder sourceBuilder = new SearchSourceBuilder();
  7. sourceBuilder.query(boolQuery);
复制代码
  固然技能上可以将所有查询都包装成 BoolQuery,但直接使用单一条件更简便
  1. private final RestHighLevelClient restHighLevelClient;
  2. @Override
  3. public ResponseResult search(UserSearchDto dto) throws IOException {
  4.     // 1. 检查参数
  5.     if(dto == null || StringUtils.isBlank(dto.getSearchWords())) {
  6.         return ResponseResult.errorResult(AppHttpCodeEnum.PARAM_INVALID);
  7.     }
  8.     // 2. 设置查询条件
  9.     SearchRequest searchRequest = new SearchRequest("app_info_article");
  10.     // searchSourceBuilder主要是对查询结果处理(分页、排序、高亮),不参与查询逻辑的构建
  11.     SearchSourceBuilder searchSourceBuilder = new SearchSourceBuilder();
  12.     // boolQuery主要是构建复杂的查询逻辑
  13.     BoolQueryBuilder boolQuery = QueryBuilders.boolQuery(); // 布尔查询
  14.     // 2-1. 关键词分词后查询
  15.     QueryStringQueryBuilder queryStringQueryBuilder = QueryBuilders.queryStringQuery(dto.getSearchWords()) // 分词之后再查询
  16.             .field("title") // 对标题分词
  17.             .field("content") // 对内容分词
  18.             .defaultOperator(Operator.OR);// 分词之后的条件(或的关系)
  19.     boolQuery.must(queryStringQueryBuilder); // 2-1. 放入布尔查询中(must:参与算分)
  20.     // 2-2. 查询小于minBehotTime的数据
  21.     RangeQueryBuilder rangeQueryBuilder = QueryBuilders.rangeQuery("publishTime") // 发布时间
  22.             .lt(dto.getMinBehotTime().getTime());// 小于minBehotTime
  23.     boolQuery.filter(rangeQueryBuilder); // 2-2. 放入布尔查询中(filter:不参与算分)
  24.     // 2-3. 分页查询
  25.     searchSourceBuilder.from(0);
  26.     searchSourceBuilder.size(dto.getPageSize());
  27.     // 2-4. 按照发布时间倒叙查询
  28.     searchSourceBuilder.sort("publishTime", SortOrder.DESC);
  29.     // 2-5. 设置高亮
  30.     HighlightBuilder highlightBuilder = new HighlightBuilder();
  31.     highlightBuilder.field("title");// 哪个字段高亮
  32.     highlightBuilder.preTags("<font style='color: red; font-size: inherit;'>"); // 高亮字段前缀
  33.     highlightBuilder.postTags("</font>"); // 高亮字段的后缀
  34.     searchSourceBuilder.highlighter(highlightBuilder);
  35.     searchSourceBuilder.query(boolQuery);
  36.     searchRequest.source(searchSourceBuilder);
  37.     SearchResponse searchResponse = restHighLevelClient.search(searchRequest, RequestOptions.DEFAULT);
  38.     // 3. 结果封装返回
  39.     SearchHit[] hits = searchResponse.getHits().getHits();
  40.     List<Map> list = new ArrayList<>();
  41.     for (SearchHit hit : hits) {
  42.         String json = hit.getSourceAsString();
  43.         Map map = JSON.parseObject(json, Map.class);
  44.         // 处理高亮
  45.         if(hit.getHighlightFields() != null && hit.getHighlightFields().size() > 0) {
  46.             Text[] titles = hit.getHighlightFields().get("title").getFragments();
  47.             String title = StringUtils.join(titles); // 高亮之后的title
  48.             map.put("h_title", title); // 设置高亮标题
  49.         }else {
  50.             map.put("h_title", map.get("title")); // 没有设置高亮,就把原本的标题放入h_title中
  51.         }
  52.         list.add(map);
  53.     }
  54.     return ResponseResult.okResult(list);
  55. }
复制代码
新增文章创建索引


思绪:文章审核成功后使用kafka发送消息,文章微服务是消息的生产者;搜索微服务吸收到消息后,添加数据到索引库,搜索微服务是消息的消费者。

  • 文章微服务(生产者)
到yml中配置生产者:
  1. spring:
  2.   kafka:
  3.         bootstrap-servers: 192.168.140.102:9092
  4.         producer:
  5.           # 重试次数
  6.           retries: 10
  7.           # key、value的序列化器
  8.           key-serializer: org.apache.kafka.common.serialization.StringSerializer
  9.           value-serializer: org.apache.kafka.common.serialization.StringSerializer
复制代码
往消息队列中发送消息:
  1. // 发送消息,创建索引
  2. SearchArticleVo searchArticleVo = new SearchArticleVo();
  3. BeanUtils.copyProperties(article, searchArticleVo);
  4. searchArticleVo.setContent(dto.getContent());
  5. searchArticleVo.setStaticUrl(path);
  6. kafkaTemplate.send(ArticleConstants.ARTICLE_ES_SYNC_TOPIC, JSON.toJSONString(searchArticleVo));
复制代码

  • 搜索微服务(消费者)
到yml中配置消费者:
  1. spring:
  2.   kafka:
  3.     bootstrap-servers: 192.168.140.102:9092
  4.     consumer:
  5.       # 消费组
  6.       group-id: ${spring.application.name}
  7.       # key、value的反序列化器
  8.       key-deserializer: org.apache.kafka.common.serialization.StringDeserializer
  9.       value-deserializer: org.apache.kafka.common.serialization.StringDeserializer
复制代码
mongodb(搜索记载)

必要给每个用户生存一份搜索记载,数据量大,要求加载速率快,通常如许的数据存储到mongodb更符合,不建议存到mysql中。

  • mongodb:

    • 支持分片,恰当存储用户搜索日记这种连续写入的场景
    • 基于磁盘存储,成本低

  • mysql:

    • 对高频写入(如每秒数千次插入)的支持较弱
    • 搜索记载通常是半结构化或非结构化数据,需频仍变更表结构来适应新字段

  • redis:

    • redis基于内存的,内存成本高,恰当存储热数据(如缓存)
    • Redis 的 RDB 快照和 AOF 日记是异步持久化机制,在宕机时可能丢失部分数据
    • 数据量过大时,从磁盘加载备份到内存的恢复过程耗时较长

   

  • MongoDB:得看成为主存储,满足海量数据、机动查询、低成本持久化的焦点需求。
  • Redis:得看成为缓存层,加快近期数据的访问,但无法替代 MongoDB 的长期存储脚色。
  • MySQL:不恰当高频写入和非结构化日记场景。
  
预备工作

1. 配置环境

使用docker安装mongodb:
  1. docker run -di \
  2. --name mongo-service \
  3. --restart=always \
  4. -p 27017:27017 \
  5. -v ~/data/mongodata:/data \
  6. mongo
复制代码
2. springboot集成mongodb


  • 添加mongodb依靠:
  1. <dependency>
  2.         <groupId>org.springframework.boot</groupId>
  3.     <artifactId>spring-boot-starter-data-mongodb</artifactId>
  4. </dependency>
复制代码

  • 配置mongodb:
  1. spring:
  2.   data:
  3.     mongodb:
  4.       host: 192.168.140.102
  5.       port: 27017
  6.       database: leadnews-history
复制代码

  • 映射
  1. @Data
  2. @Document("ap_associate_words") // 映射哪个集合【mongodb表名】
  3. public class ApAssociateWords implements Serializable {
  4.     private static final long serialVersionUID = 1L;
  5.     private String id;
  6.     /**
  7.      * 联想词
  8.      */
  9.     private String associateWords;
  10.     /**
  11.      * 创建时间
  12.      */
  13.     private Date createdTime;
  14. }
复制代码

  • 焦点方法


  • 生存或修改:
  1. @Autowired
  2. private MongoTemplate mongoTemplate;
  3. //保存
  4. @Test
  5. public void saveTest(){
  6.     ApAssociateWords apAssociateWords = new ApAssociateWords();
  7.     apAssociateWords.setAssociateWords("黑马头条");
  8.     apAssociateWords.setCreatedTime(new Date());
  9.     mongoTemplate.save(apAssociateWords);
  10. }
复制代码


  • 查询一个对象
  1. @Test
  2. public void saveFindOne(){
  3.     ApAssociateWords apAssociateWords = mongoTemplate.findById("67a330c35faec30826dcbe8e", ApAssociateWords.class);
  4.     System.out.println(apAssociateWords);
  5. }
复制代码


  • 多条件查询
  1. @Test
  2. public void testQuery(){
  3.     Query query = Query.query(Criteria.where("associateWords").is("黑马头条"))
  4.             .with(Sort.by(Sort.Direction.DESC,"createdTime"));
  5.     List<ApAssociateWords> apAssociateWordsList = mongoTemplate.find(query, ApAssociateWords.class);
  6.     System.out.println(apAssociateWordsList);
  7. }
复制代码


  • 删除
  1. @Test
  2. public void testDel(){
  3.     mongoTemplate.remove(Query.query(Criteria.where("associateWords").is("黑马头条")),ApAssociateWords.class);
  4. }
复制代码
生存搜索记载


   用户搜索后,为了让用户能更快的得到搜索的结果,异步发送请求记载关键字。

  1. private final MongoTemplate mongoTemplate;
  2. // 保存搜索记录
  3. @Override
  4. @Async
  5. public void save(String keyword, Integer userId) {
  6.     // 1. 查询当前用户搜索关键字
  7.     Query query = Query.query(Criteria.where("userId").is(userId)
  8.                                         .and("keyword").is(keyword));
  9.     ApUserSearch apUserSearch = mongoTemplate.findOne(query, ApUserSearch.class);
  10.     // 2. 存在 - 更新时间
  11.     if(apUserSearch != null) {
  12.         apUserSearch.setCreatedTime(new Date());
  13.         mongoTemplate.save(apUserSearch); // 有id-修改、没有id-新增
  14.         return;
  15.     }
  16.     // 3. 不存在 - 判断该用户的当前历史总数量是否 > 10
  17.     apUserSearch = new ApUserSearch();
  18.     apUserSearch.setUserId(userId);
  19.     apUserSearch.setKeyword(keyword);
  20.     apUserSearch.setCreatedTime(new Date());
  21.     // 4. 当前用户的当前历史总数量 < 10 - 直接保存
  22.     Query query1 = Query.query(Criteria.where("userId").is(userId));
  23.     query1.with(Sort.by(Sort.Direction.DESC, "createdTime")); // 按照时间倒序排列
  24.     List<ApUserSearch> apUserSearches = mongoTemplate.find(query1, ApUserSearch.class);
  25.     if(apUserSearches == null || apUserSearches.size() < 10) {
  26.         mongoTemplate.save(apUserSearch); // 直接保存
  27.     }else {
  28.         // 5. 当前用户的当前历史总数量 >= 10 - 替换最后一条记录
  29.         ApUserSearch lastUserSearch = apUserSearches.get(apUserSearches.size() - 1);
  30.         mongoTemplate.findAndReplace(Query.query(Criteria.where("id").is(lastUserSearch.getId())), apUserSearch); // 修改最后一条记录
  31.     }
  32. }
复制代码
  在之前写的文章搜索的业务代码中,异步调用“生存搜索记载”的方法。
此中:userId通过app网关的过滤器拦截到前端发过来的userId,并把userId放到请求头中传给搜索微服务,搜索微服务的拦截器获取app网关发来的userId,存到ThreadLocal中。
注意:由于是异步调用save方法,是又开了一个线程,此时这个线程是没办法从ThreadLocal中获取到userId,只能通过主线程传过来。
  查询搜索汗青

  1. public ResponseResult findUserSearch() {
  2.         // 获取当前用户
  3.         ApUser user = AppThreadLocalUtil.getUser();
  4.         // 根据用户查询当前数据(按照时间倒叙)
  5.         if(user == null) {
  6.             return ResponseResult.errorResult(AppHttpCodeEnum.NEED_LOGIN);
  7.         }
  8.         List<ApUserSearch> list = mongoTemplate.find(Query.query(Criteria.where("userId").is(user.getId()))
  9.                                                 .with(Sort.by(Sort.Direction.DESC, "createdTime")), ApUserSearch.class);
  10.         return ResponseResult.okResult(list);
  11.     }
复制代码
  根据用户id和当前某个用户的id查找记载,并按照创建时间降序分列。
  删除某一个汗青记载

  1. public ResponseResult delUserSearch(HistorySearchDto dto) {
  2.     // 检查参数
  3.     if (dto.getId() == null) {
  4.         return ResponseResult.errorResult(AppHttpCodeEnum.PARAM_INVALID);
  5.     }
  6.     // 获取当前用户
  7.     ApUser user = AppThreadLocalUtil.getUser();
  8.     // 判断是否登录
  9.     if(user == null) {
  10.         return ResponseResult.errorResult(AppHttpCodeEnum.NEED_LOGIN);
  11.     }
  12.     // 删除
  13.     mongoTemplate.remove(Query.query(Criteria.where("userId").is(user.getId())
  14.                                             .and("id").is(dto.getId())), ApUserSearch.class);
  15.     return ResponseResult.okResult(AppHttpCodeEnum.SUCCESS);
  16. }
复制代码
  根据用户id和当前某个搜索记载的id进行删除
  搜索联想词

搜索词(数据泉源)

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

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


实现

正则表达式:

  1. // 搜索联想词
  2. @Override
  3. public ResponseResult search(UserSearchDto dto) {
  4.     // 1. 检查参数
  5.     if(StringUtils.isBlank(dto.getSearchWords())) {
  6.         return ResponseResult.errorResult(AppHttpCodeEnum.PARAM_INVALID);
  7.     }
  8.     // 2. 分页检查(最多只能查询20条)
  9.     if(dto.getPageSize() > 20) {
  10.         dto.setPageSize(20);
  11.     }
  12.     // 3. 执行模糊查询
  13.     String regexStr = ".*?\" + dto.getSearchWords() + ".*";
  14.     Query query = Query.query(Criteria.where("associateWords")
  15.                         .regex(regexStr))
  16.                         .limit(dto.getPageSize());
  17.     List<ApAssociateWords> list = mongoTemplate.find(query, ApAssociateWords.class);
  18.     return ResponseResult.okResult(list);
  19. }
复制代码
  实在搜索联想词,就是提前先把词库导入到mongodb表中,用户在输入的时间,就会对这个表进行模糊查询,碰到符合条件的就立马匹配。

免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!更多信息从访问主页:qidao123.com:ToB企服之家,中国第一个企服评测及商务社交产业平台。

本帖子中包含更多资源

您需要 登录 才可以下载或查看,没有账号?立即注册

x
回复

使用道具 举报

0 个回复

倒序浏览

快速回复

您需要登录后才可以回帖 登录 or 立即注册

本版积分规则

愛在花開的季節

金牌会员
这个人很懒什么都没写!

标签云

快速回复 返回顶部 返回列表