愛在花開的季節 发表于 2025-2-18 21:14:53

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

需求

用户输入关键字时,可以检索出结果,
https://i-blog.csdnimg.cn/direct/ee0c455b29834a0dac6a1f3bca7f5f13.png
并且可以查察汗青搜索环境,https://i-blog.csdnimg.cn/direct/c083ab7ec07b45f29138c03c84473962.png
还可以进行联想词展示。https://i-blog.csdnimg.cn/direct/7af43012fc05432eb5b85eeba9507993.png
ElasticSearch(搜索)

预备工作


[*]使用docker安装es,配置ik分词器
[*]重新建一个search模块,用来写搜索微服务的业务代码
[*]导入es的依靠
[*]配置RestHighLevelClient
@Getter
@Setter
@Configuration
@ConfigurationProperties(prefix = "elasticsearch")
public class ElasticSearchConfig {
    private String host;
    private int port;

    @Bean
    public 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.102
port: 9200

[*]初始化索引库数据(项目上线之前必要批量导入):
@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);
}
文章搜索


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

[*]组合多个条件:必须使用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());// 小于minBehotTime
    boolQuery.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); // 高亮之后的title
            map.put("h_title", title); // 设置高亮标题
      }else {
            map.put("h_title", map.get("title")); // 没有设置高亮,就把原本的标题放入h_title中
      }
      list.add(map);
    }
    return ResponseResult.okResult(list);
}
新增文章创建索引

https://i-blog.csdnimg.cn/direct/1e09ed1db8a94619a815d21d0217d189.png
思绪:文章审核成功后使用kafka发送消息,文章微服务是消息的生产者;搜索微服务吸收到消息后,添加数据到索引库,搜索微服务是消息的消费者。

[*]文章微服务(生产者)
到yml中配置生产者:
spring:
kafka:
        bootstrap-servers: 192.168.140.102:9092
        producer:
          # 重试次数
          retries: 10
          # key、value的序列化器
          key-serializer: org.apache.kafka.common.serialization.StringSerializer
          value-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));

[*]搜索微服务(消费者)
到yml中配置消费者:
spring:
kafka:
    bootstrap-servers: 192.168.140.102:9092
    consumer:
      # 消费组
      group-id: ${spring.application.name}
      # key、value的反序列化器
      key-deserializer: org.apache.kafka.common.serialization.StringDeserializer
      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:
docker run -di \
--name mongo-service \
--restart=always \
-p 27017:27017 \
-v ~/data/mongodata:/data \
mongo
2. springboot集成mongodb


[*]添加mongodb依靠:
<dependency>
        <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-mongodb</artifactId>
</dependency>

[*]配置mongodb:
spring:
data:
    mongodb:
      host: 192.168.140.102
      port: 27017
      database: leadnews-history

[*]映射
@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;

}

[*]焦点方法


[*]生存或修改:
@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);
}
生存搜索记载

https://i-blog.csdnimg.cn/direct/4f4e55d94e6c4d40976c242fdb2934d3.png
   用户搜索后,为了让用户能更快的得到搜索的结果,异步发送请求记载关键字。
https://i-blog.csdnimg.cn/direct/c42987c8d6384195bb3e03f1230f1741.png
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. 不存在 - 判断该用户的当前历史总数量是否 > 10
    apUserSearch = 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进行删除
搜索联想词

搜索词(数据泉源)

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

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

https://i-blog.csdnimg.cn/direct/e1b5de45335d4987ab59025fbed1288c.png
实现

正则表达式:
https://i-blog.csdnimg.cn/direct/488c069a0ae94adea4bdc16960f1bc9d.png
// 搜索联想词
@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表中,用户在输入的时间,就会对这个表进行模糊查询,碰到符合条件的就立马匹配。

免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!更多信息从访问主页:qidao123.com:ToB企服之家,中国第一个企服评测及商务社交产业平台。
页: [1]
查看完整版本: 【搜索文章】:搜索(es)+ 搜索记载(mongodb)+ 搜索联想词