【黑马头条】-day07APP端文章搜索-ES-mongoDB


文章目录

  • 今日内容
  • 1 搭建es环境
    • 1.1 拉取es镜像
    • 1.2 创建容器
    • 1.3 配置中文分词器ik
    • 1.4 测试
  • 2 app文章搜索
    • 2.1 需求说明
    • 2.2 思路分析
    • 2.3 创建索引和映射
      • 2.3.1 PUT请求添加映射
      • 2.3.2 其他操作
    • 2.4 初始化索引库数据
      • 2.4.1 导入es-init
      • 2.4.2 es-init配置
      • 2.4.3 导入数据
      • 2.4.4 查询已导入的文档
    • 2.5 接口定义
      • 2.5.1 UserSearchDto
    • 2.6 app端文章搜索项目准备
      • 2.6.1 导入heima-leadnews-search
      • 2.6.2 导入依赖
      • 2.6.3 导入配置
    • 2.7 实现app端文章搜索
      • 2.7.1 Controller接口定义
      • 2.7.2 业务层
      • 2.7.3 为app端文章搜索添加网关
      • 2.7.3 测试
  • 3 新增文章创建索引
    • 3.1 思路分析
    • 3.2 SearchArticleVo
    • 3.3 创建kafka的topic
    • 3.3 修改文章微服务配置
    • 3.4 静态文件路径生成后文章生产通知
    • 3.5 搜索微服务监听消息
      • 3.5.1 修改搜索微服务配置
      • 3.5.2 定义监听消息
    • 3.6 综合测试
  • 4 app端搜索-保存搜索记录
    • 4.1 需求说明
    • 4.2 安装mongoDB
      • 4.2.1 拉取镜像
      • 4.2.2 创建容器
      • 4.2.3 本地连接mongodb
    • 4.3 SpringBoot集成mongoDB
      • 4.3.1 导入资料中的mongo-demo
      • 4.3.2 导入依赖
      • 4.3.3. 配置mongoDB
      • 4.3.4 添加表映射
      • 4.3.5 核心方法
        • 4.3.5.1 保存
        • 4.3.5.2 查询
        • 4.3.5.3 条件查询
        • 4.3.5.4 删除
    • 4.4 保存搜索记录
      • 4.4.1 实现思路
      • 4.4.2 为搜索微服务添加mongoDB
      • 4.4.3 在nacos中配置mongoDB
      • 4.4.4 运行sql脚本
      • 4.4.5 导入对应的表的实体类
      • 4.4.6 Service
      • 4.4.7 过滤器解析token获取id放入头部,拦截器将id存入线程
      • 4.4.8 Search微服务也采用这样的方法
      • 4.4.9 异步调用保存搜索记录
    • 4.5 测试
  • 5 app端搜索-加载搜索历史
    • 5.1 接口
    • 5.2 Controller
    • 5.3 Service
    • 5.4 测试
  • 6 app端搜索-删除搜索历史
    • 6.1 接口
    • 6.2 Dto
    • 6.3 Controller
    • 6.4 Service
    • 6.5 测试
  • 7 app端搜索-关键字联想功能
    • 7.1 需求分析
    • 7.2 接口定义
    • 7.3 自动补全py插件
    • 7.4 自定义分词器


今日内容

在这里插入图片描述

在这里插入图片描述

1 搭建es环境

1.1 拉取es镜像

docker pull elasticsearch:7.4.0

1.2 创建容器

docker run -id \
--name es -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

1.3 配置中文分词器ik

把资料中的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容器

docker restart es

1.4 测试

发送post请求 192.168.204.129:9200/_analyze

在这里插入图片描述

{
    "analyzer":"ik_max_word",
    "text":"欢迎来到黑马学习java"
}

返回

{
    "tokens": [
        {
            "token": "欢迎",
            "start_offset": 0,
            "end_offset": 2,
            "type": "CN_WORD",
            "position": 0
        },
        {
            "token": "迎来",
            "start_offset": 1,
            "end_offset": 3,
            "type": "CN_WORD",
            "position": 1
        },
        {
            "token": "来到",
            "start_offset": 2,
            "end_offset": 4,
            "type": "CN_WORD",
            "position": 2
        },
        {
            "token": "黑马",
            "start_offset": 4,
            "end_offset": 6,
            "type": "CN_WORD",
            "position": 3
        },
        {
            "token": "学习",
            "start_offset": 6,
            "end_offset": 8,
            "type": "CN_WORD",
            "position": 4
        },
        {
            "token": "java",
            "start_offset": 8,
            "end_offset": 12,
            "type": "ENGLISH",
            "position": 5
        }
    ]
}

2 app文章搜索

2.1 需求说明

在这里插入图片描述

2.2 思路分析

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

2.3 创建索引和映射

2.3.1 PUT请求添加映射

PUT请求:192.168.204.129: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"
            }
        }
    }
}

返回:

{
    "acknowledged": true,
    "shards_acknowledged": true,
    "index": "app_info_article"
}

2.3.2 其他操作

GET请求查询映射:192.168.204.129:9200/app_info_article

{
    "app_info_article": {
        "aliases": {},
        "mappings": {
            "properties": {
                "authorId": {
                    "type": "long"
                },
                "authorName": {
                    "type": "text"
                },
                "content": {
                    "type": "text",
                    "analyzer": "ik_smart"
                },
                "id": {
                    "type": "long"
                },
                "images": {
                    "type": "keyword",
                    "index": false
                },
                "layout": {
                    "type": "integer"
                },
                "publishTime": {
                    "type": "date"
                },
                "staticUrl": {
                    "type": "keyword",
                    "index": false
                },
                "title": {
                    "type": "text",
                    "analyzer": "ik_smart"
                }
            }
        },
        "settings": {
            "index": {
                "creation_date": "1712574364409",
                "number_of_shards": "1",
                "number_of_replicas": "1",
                "uuid": "IPlVoSqUSOm5dRBfJmaV_A",
                "version": {
                    "created": "7040099"
                },
                "provided_name": "app_info_article"
            }
        }
    }
}

DELETE请求,删除索引及映射:192.168.204.129:9200/app_info_article

GET请求,查询所有文档:192.168.204.129:9200/app_info_article/_search

2.4 初始化索引库数据

2.4.1 导入es-init

将es-init导入heima-leadnews-testk模块中

在这里插入图片描述

将其引入heima-leadnews-testk模块的pom文件中

<modules>
    <module>freemarker-demo</module>
    <module>minio-demo</module>
    <module>tess4j-demo</module>
    <module>kafka-demo</module>
    <module>es-init</module>
</modules>

2.4.2 es-init配置

在这里插入图片描述

通过配置类进行RestHighLevelClient的初始化

2.4.3 导入数据

在com.heima.es.ApArticleTest中编写测试方法导入数据

先创建BulkRequest,在把一条条数据组成IndexRequest再放到BulkRequest中,在用RestHighLevelClient的bulk方法批量添加

@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);
    }
}

2.4.4 查询已导入的文档

GET请求,查询所有文档:192.168.204.129:9200/app_info_article/_search,有23条数据

在这里插入图片描述

2.5 接口定义

在这里插入图片描述

2.5.1 UserSearchDto

@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);
    }
}

2.6 app端文章搜索项目准备

2.6.1 导入heima-leadnews-search

导入heima-leadnews-search到heima-leadnews-service中

在这里插入图片描述

引入heima-leadnews-search到pom文件中

2.6.2 导入依赖

在heima-leadnews-service的pom中添加依赖

<!--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>

2.6.3 导入配置

server:
  port: 51804
spring:
  application:
    name: leadnews-search
  cloud:
    nacos:
      discovery:
        server-addr: 192.168.204.129:8848
      config:
        server-addr: 192.168.204.129:8848
        file-extension: yml

这里并没有数据库相关配置,需要在nacos配置,在nacos中创建leadnews-search

因为暂时不需要数据库,所以取消掉DataSourceAutoConfiguration自动配置类

spring:
  autoconfigure:
    exclude: org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration
elasticsearch:
  host: 192.168.204.129
  port: 9200
minio:
  accessKey: minio
  secretKey: minio123
  bucket: leadnews
  endpoint: http://192.168.204.129:9000
  readPath: http://192.168.204.129:9000

在这里插入图片描述

2.7 实现app端文章搜索

2.7.1 Controller接口定义

创建com.heima.search.controller.v1.ArticleSearchController

@RestController
@RequestMapping("/api/v1/article/search")
public class ArticleSearchController {
    @Autowired
    private ArticleSearchService articleSearchService;
    @PostMapping("/search")
    public ResponseResult search(@RequestBody UserSearchDto userSearchDto) {
        return articleSearchService.search(userSearchDto);
    }
}

2.7.2 业务层

创建com.heima.search.service.ArticleSearchService接口

public interface ArticleSearchService {
    ResponseResult search(UserSearchDto userSearchDto);
}

实现

@Service
@Slf4j
public class ArticleSearchServiceImpl implements ArticleSearchService  {
    @Autowired
    private RestHighLevelClient restHighLevelClient;
    @Override
    public ResponseResult search(UserSearchDto userSearchDto) {
        //1. 检查参数
        if(userSearchDto == null|| StringUtils.isBlank(userSearchDto.getSearchWords())){
            return ResponseResult.errorResult(AppHttpCodeEnum.PARAM_INVALID,"参数不合法");
        }
        //2. 设置查询条件
        SearchRequest searchRequest = new SearchRequest("app_info_article");
        SearchSourceBuilder searchSourceBuilder = new SearchSourceBuilder();

        //复合查询需要使用boolQuery
        BoolQueryBuilder boolQueryBuilder = QueryBuilders.boolQuery();
        //2.1 关键词的分词之查询
        QueryStringQueryBuilder queryStringQueryBuilder = QueryBuilders.queryStringQuery(userSearchDto
                .getSearchWords())
                .field("title")
                .field("content")
                .defaultOperator(Operator.OR);
        boolQueryBuilder.must(queryStringQueryBuilder);
        //2.2 查询小于mindate的数据
        RangeQueryBuilder publishTime = QueryBuilders.rangeQuery("publishTime").lt(userSearchDto.getMinBehotTime().getTime());
        boolQueryBuilder.filter(publishTime);
        //2.3 分页查询
        searchSourceBuilder.from(0).size(userSearchDto.getPageSize());
        //2.4 按照时间倒序排序
        searchSourceBuilder.sort("publishTime", SortOrder.DESC);
        //2.5 设置高亮显示title
        HighlightBuilder highlightBuilder = new HighlightBuilder();
        highlightBuilder.field("title");
        highlightBuilder.preTags("<font style='color: red; font-size: inherit;'>");
        highlightBuilder.postTags("</font>");
        searchSourceBuilder.highlighter(highlightBuilder);

        //2.6 查询
        searchSourceBuilder.query(boolQueryBuilder);
        searchRequest.source(searchSourceBuilder);
        try {
            SearchResponse response = restHighLevelClient.search(searchRequest, RequestOptions.DEFAULT);
            //3. 结果封装
            List<Map> list= new ArrayList<>();
            SearchHits searchHits= response.getHits();
            SearchHit[] hits = searchHits.getHits();
            for (SearchHit hit : hits) {
                //3.1 获取source
                String json= hit.getSourceAsString();
                Map map = JSON.parseObject(json, Map.class);
                //3.2 获取高亮数据
                Map<String, HighlightField> highlightFields = hit.getHighlightFields();
                if(!CollectionUtils.isEmpty(highlightFields)){
                    HighlightField highlightField = highlightFields.get("title");
                    if(highlightField!=null){
                        //3.3 拿到所有高亮数据
                        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);
            }
            //4. 返回结果
            return ResponseResult.okResult(list);
        } catch (IOException e) {
            throw new RuntimeException(e);
        }
    }
}

2.7.3 为app端文章搜索添加网关

需要在app的网关中添加搜索微服务的路由配置

#搜索微服务
- id: leadnews-search
 uri: lb://leadnews-search
 predicates:
   - Path=/search/**
 filters:
   - StripPrefix= 1
spring:
  cloud:
    gateway:
      globalcors:
        add-to-simple-url-handler-mapping: true
        corsConfigurations:
          '[/**]':
            allowedHeaders: "*"
            allowedOrigins: "*"
            allowedMethods:
              - GET
              - POST
              - DELETE
              - PUT
              - OPTION
      routes:
        # 用户管理
        - id: user
          uri: lb://leadnews-user
          predicates:
            - Path=/user/**
          filters:
            - StripPrefix= 1
        # 文章管理
        - id: article
          uri: lb://leadnews-article
          predicates:
            - Path=/article/**
          filters:
            - StripPrefix= 1
        #搜索微服务
        - id: leadnews-search
          uri: lb://leadnews-search
          predicates:
            - Path=/search/**
          filters:
            - StripPrefix= 1

在这里插入图片描述

2.7.3 测试

启动对应微服务

在这里插入图片描述

打开 localhost:8801

在这里插入图片描述

数据显示没有问题,测试成功

在这里插入图片描述

3 新增文章创建索引

3.1 思路分析

在这里插入图片描述

3.2 SearchArticleVo

文章问微服务需要组装SearchArticleVo给搜索微服务

所以需要先定义SearchArticleVo,创建com.heima.model.search.SearchArticleVo类

@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;
}

3.3 创建kafka的topic

在com.heima.common.constants.ArticleConstants类中创建新的属性

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_INDEX_TOPIC = "article.es.syn.topic";

}

3.3 修改文章微服务配置

因为文章微服务相当于生产者,所以要更新文章微服务的nacos配置

spring:
  datasource:
    driver-class-name: com.mysql.jdbc.Driver
    url: jdbc:mysql://localhost:3306/leadnews_article?useUnicode=true&characterEncoding=UTF-8&serverTimezone=UTC&useSSL=false
    username: root
    password: 123sjbsjb
  kafka:
    bootstrap-servers: 192.168.204.129:9092
    producer:
      retries: 10
      key-serializer: org.apache.kafka.common.serialization.StringSerializer
      value-serializer: org.apache.kafka.common.serialization.StringSerializer
    consumer:
      group-id: ${spring.application.name}
      key-deserializer: org.apache.kafka.common.serialization.StringDeserializer
      value-deserializer: org.apache.kafka.common.serialization.StringDeserializer

# 设置Mapper接口所对应的XML文件位置,如果你在Mapper接口中有自定义方法,需要进行该配置
mybatis-plus:
  mapper-locations: classpath*:mapper/*.xml
  # 设置别名包扫描路径,通过该属性可以给包中的类注册别名
  type-aliases-package: com.heima.model.article.pojos
  global-config:
    datacenter-id: 1
    workerId: 1
minio:
  accessKey: minio
  secretKey: minio123
  bucket: leadnews
  endpoint: http://192.168.204.129:9000
  readPath: http://192.168.204.129:9000

在这里插入图片描述

3.4 静态文件路径生成后文章生产通知

在com.heima.article.service.impl.ArticleFreemarkerServiceImpl类中

最后生成

//4.把静态页面的路径保存到数据库
apArticleService.update(Wrappers
        .<ApArticle>lambdaUpdate()
        .eq(ApArticle::getId,apArticle.getId())
        .set(ApArticle::getStaticUrl,path));

//5. 发送消息到kafka,创建es索引
createArticleESIndex(apArticle,content,path);

创建es索引

@Autowired
private KafkaTemplate<String,String> kafkaTemplate;

/**
 * 创建文章索引
 * @param apArticle
 * @param content
 * @param path
 */
private void createArticleESIndex(ApArticle apArticle, String content, String path) {
    SearchArticleVo searchArticleVo = new SearchArticleVo();
    BeanUtils.copyProperties(apArticle,searchArticleVo);
    searchArticleVo.setContent(content);
    searchArticleVo.setStaticUrl(path);

    kafkaTemplate.send(ArticleConstants.ARTICLE_ES_INDEX_TOPIC, JSON.toJSONString(searchArticleVo));
}

3.5 搜索微服务监听消息

3.5.1 修改搜索微服务配置

搜索微服务作为kafka的消费者,进行nacos配置

spring:
  autoconfigure:
    exclude: org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration
  kafka:
    bootstrap-servers: 192.168.204.129:9092
    consumer:
      group-id: ${spring.application.name}
      key-deserializer: org.apache.kafka.common.serialization.StringDeserializer
      value-deserializer: org.apache.kafka.common.serialization.StringDeserializer
elasticsearch:
  host: 192.168.204.129
  port: 9200
minio:
  accessKey: minio
  secretKey: minio123
  bucket: leadnews
  endpoint: http://192.168.204.129:9000
  readPath: http://192.168.204.129:9000

在这里插入图片描述

3.5.2 定义监听消息

在heima-leadnews-search模块中创建com.heima.search.listen.SyncArticleListener类

@Component
@Slf4j
public class SyncArticleListener {
    @Autowired
    private RestHighLevelClient restHighLevelClient;
    @KafkaListener(topics = ArticleConstants.ARTICLE_ES_INDEX_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);
            }
        }
    }
}

3.6 综合测试

启动es、kafka、redis、rabbitmq、minio、nacos、zookeeper

在这里插入图片描述

再启动相应启动类

在这里插入图片描述

现在app端显示

在这里插入图片描述

在自媒体端添加测试:黑马测试搜索123

在这里插入图片描述

点击发布,自动审核成功

在这里插入图片描述

重新加载app端,此时审核已上架的文章已经显示

在这里插入图片描述

搜索栏中尝试搜搜

在这里插入图片描述

成功搜索到

查看SearchApplication的日志显示

2024-04-09 13:41:44.683  INFO 16292 --- [ntainer#0-0-C-1] c.h.search.listen.SyncArticleListener    : SyncArticleListener,message={"authorId":1102,"authorName":"admin","content":"[{\"type\":\"image\",\"value\":\"http://192.168.204.129:9000/leadnews/2024/03/25/40ecf5d3f9084e1094b5469fe48f7603.jpg\"},{\"type\":\"text\",\"value\":\"请在这里输入正文\"}]","id":1777572583391236097,"images":"http://192.168.204.129:9000/leadnews/2024/03/25/40ecf5d3f9084e1094b5469fe48f7603.jpg","publishTime":1712641152000,"staticUrl":"http://192.168.204.129:9000/leadnews/2024/04/09/1777572583391236097.html","title":"黑马测试搜索123"}

说明我们的测试非常成功。

4 app端搜索-保存搜索记录

4.1 需求说明

在这里插入图片描述

在这里插入图片描述

4.2 安装mongoDB

4.2.1 拉取镜像

docker pull mongo

4.2.2 创建容器

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

4.2.3 本地连接mongodb

使用navicat,,同时创建leadnews-history数据库

在这里插入图片描述

在这里插入图片描述

4.3 SpringBoot集成mongoDB

4.3.1 导入资料中的mongo-demo

导入资料中的mongo-demo并将其导入test模块中的pom文件里

在这里插入图片描述

4.3.2 导入依赖

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-mongodb</artifactId>
</dependency>

4.3.3. 配置mongoDB

创建application.yaml

server:
  port: 9998
spring:
  data:
    mongodb:
      host: 192.168.204.129
      port: 27017
      database: leadnews-history

4.3.4 添加表映射

@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;

}

4.3.5 核心方法

在这里插入图片描述

com.itheima.mongo.test.MongoTest测试方法

@SpringBootTest(classes = MongoApplication.class)
@RunWith(SpringRunner.class)
public class MongoTest {


    @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("6614e7011f52f0112ac7df19", 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);
    }
}
4.3.5.1 保存
//保存
@Test
public void saveTest(){
    ApAssociateWords apAssociateWords = new ApAssociateWords();
    apAssociateWords.setAssociateWords("黑马头条");
    apAssociateWords.setCreatedTime(new Date());
    mongoTemplate.save(apAssociateWords);
}

在这里插入图片描述

4.3.5.2 查询
//查询一个
@Test
public void saveFindOne(){
    ApAssociateWords apAssociateWords = mongoTemplate.findById("6614e7011f52f0112ac7df19", ApAssociateWords.class);
    System.out.println(apAssociateWords);
}
2024-04-09 15:04:51.338  INFO 13360 --- [           main] o.m.d.connection                         : Opened connection [connectionId{localValue:2, serverValue:16}] to 192.168.204.129:27017
[ApAssociateWords(id=6614e7011f52f0112ac7df19, associateWords=黑马头条, createdTime=Tue Apr 09 14:58:08 CST 2024)]
4.3.5.3 条件查询
//条件查询
@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);
}
2024-04-09 15:06:09.968  INFO 13672 --- [           main] o.m.d.connection                         : Opened connection [connectionId{localValue:2, serverValue:18}] to 192.168.204.129:27017
[ApAssociateWords(id=6614e7011f52f0112ac7df19, associateWords=黑马头条, createdTime=Tue Apr 09 14:58:08 CST 2024)]
4.3.5.4 删除
//删除
@Test
public void testDel(){
    mongoTemplate.remove(Query.query(Criteria.where("associateWords").is("黑马头条")),ApAssociateWords.class);
}

4.4 保存搜索记录

4.4.1 实现思路

在这里插入图片描述

在这里插入图片描述

4.4.2 为搜索微服务添加mongoDB

为heima-leadnews-search搜索微服务添加mongoDB

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-mongodb</artifactId>
</dependency>

4.4.3 在nacos中配置mongoDB

spring:
  data:
   mongodb:
    host: 192.168.204.129
    port: 27017
    database: leadnews-history
  kafka:
    bootstrap-servers: 192.168.204.129:9092
    producer:
      retries: 10
      key-serializer: org.apache.kafka.common.serialization.StringSerializer
      value-serializer: org.apache.kafka.common.serialization.StringSerializer
    consumer:
      group-id: ${spring.application.name}
      key-deserializer: org.apache.kafka.common.serialization.StringDeserializer
      value-deserializer: org.apache.kafka.common.serialization.StringDeserializer
  autoconfigure:
    exclude: org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration
elasticsearch:
  host: 192.168.204.129
  port: 9200
minio:
  accessKey: minio
  secretKey: minio123
  bucket: leadnews
  endpoint: http://192.168.204.129:9000
  readPath: http://192.168.204.129:9000

在这里插入图片描述

4.4.4 运行sql脚本

在这里插入图片描述

4.4.5 导入对应的表的实体类

@Data
@Document("ap_user_search")
public class ApUserSearch implements Serializable {
    private static final long serialVersionUID = 1L;
    /**
     * 主键
     */
    private String id;
    /**
     * 用户ID
     */
    private Integer userId;
    /**
     * 搜索词
     */
    private String keyword;
    /**
     * 创建时间
     */
    private Date createdTime;
}
@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;
}

4.4.6 Service

接口

public interface ApUserSearchService {
    /**
     * 保存用户搜索历史记录
     * @param keyword
     * @param userId
     */
    void insert(String keyword,Integer userId);
}

实现

@Service
@Slf4j
public class ApUserSearchServiceImpl implements ApUserSearchService {
    @Autowired
    private MongoTemplate mongoTemplate;
    @Override
    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条,超过则删除最早的一条
        long count = mongoTemplate.count(Query.query(Criteria.where("userId").is(userId)), ApUserSearch.class);
        if(count >= 10) {
            Query query1 = Query.query(Criteria.where("userId").is(userId))
                    .with(Sort.by(Sort.Order.asc("createdTime"))).limit(1);
            ApUserSearch apUserSearch1 = mongoTemplate.findOne(query1, ApUserSearch.class);
            mongoTemplate.remove(apUserSearch1);
        }
        else{
            ApUserSearch apUserSearch1 = new ApUserSearch();
            apUserSearch1.setUserId(userId);
            apUserSearch1.setKeyword(keyword);
            apUserSearch1.setCreatedTime(new Date());
            mongoTemplate.save(apUserSearch1);
        }
    }
}

4.4.7 过滤器解析token获取id放入头部,拦截器将id存入线程

针对每个用户的保存历史记录,需要放在对应的userid下

在gateway网关中通过过滤器fliter解析token获取的jwt令牌获取用户id

@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是否有效
        //5.1 解析token
        try{
            Claims body = AppJwtUtil.getClaimsBody(token);
            //5.2 判断token是否有效
            int result = AppJwtUtil.verifyToken(body);
            if(result == 1||result == 2) {
                //5.3 token过期
                response.setStatusCode(HttpStatus.UNAUTHORIZED);
                return response.setComplete();
            }
            //获取用户信息
            Integer userId = (Integer) body.get("id");
            //将用户信息放入到header中
            ServerHttpRequest serverHttpRequest = request.mutate().headers(httpHeaders -> {
                httpHeaders.add("userId", userId + "");
            }).build();
            //重置请求
            exchange.mutate().request(serverHttpRequest);
        }catch (Exception e) {
            e.printStackTrace();
            //5.4 token无效
            response.setStatusCode(HttpStatus.UNAUTHORIZED);
            return response.setComplete();
        }
        //6.放行
        return chain.filter(exchange);
    }

解析完token后,将其发在httpheaders的头部,重置请求request

然后再通过拦截器解析request获取userid,再存入线程中

public class WmTokenInterceptor implements HandlerInterceptor {
    /**
     * 拦截器的前置方法,得到header中的用户信息,存入到当前线程中
     * @param request
     * @param response
     * @param handler
     * @return
     * @throws Exception
     */
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        String userId = request.getHeader("userId");
        if(userId != null){
            //存入当前线程
            WmUser wmUser = new WmUser();
            wmUser.setId(Integer.valueOf(userId));
            WmThreadLocalUtil.setUser(wmUser);
        }
        return true;
    }

    /**
     * 后置方法,清除当前线程中的用户信息
     * @param request
     * @param response
     * @param handler
     * @param ex
     * @throws Exception
     */
    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
        WmThreadLocalUtil.clear();
    }
}

4.4.8 Search微服务也采用这样的方法

在WmTokenInterceptor中存放userid的是WmThreadLocalUtil,search微服务也需要一个线程空间来存放userid,自然创建AppThreadLocalUtil,在heima-leadnews-utils模块下创建com.heima.utils.thread.AppThreadLocalUtil类

public class AppThreadLocalUtil {
    private static final ThreadLocal<ApUser> APP_USER_THREAD_LOCAL = new ThreadLocal<>();
    public static void setUser(ApUser user) {
        APP_USER_THREAD_LOCAL.set(user);
    }
    public static ApUser getUser() {
        return APP_USER_THREAD_LOCAL.get();
    }
    public static void clear() {
        APP_USER_THREAD_LOCAL.remove();
    }
}

在heima-leadnews-search模块中添加com.heima.search.interceptor.AppTokenInterceptor类

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();
    }
}

要想使拦截器AppTokenInterceptor生效,需要将拦截器载入拦截器注册器里,在heima-leadnews-search模块中添加com.heima.search.config.WebMvcConfig类

@Configuration
public class WebMvcConfig implements WebMvcConfigurer {
    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(new AppTokenInterceptor()).addPathPatterns("/**");
    }
}

将拦截器AppTokenInterceptor加入到拦截器注册器里,拦截所有path

4.4.9 异步调用保存搜索记录

在搜索之前就可以进行保存了,所以在com.heima.search.service.impl.ArticleSearchServiceImpl检查完参数之后就可以写进mongoDB里了,并且为apUserSearchService.insert(userSearchDto.getSearchWords(), AppThreadLocalUtil.getUser().getId());添加异步调用方法@Async

userSearchDto.getFromIndex()==0是因为只有在第一页的时候才保存,你都翻到第二页了,不用再保存一次关键字了。

@Override
public ResponseResult search(UserSearchDto userSearchDto) {
    //1. 检查参数
    if(userSearchDto == null|| StringUtils.isBlank(userSearchDto.getSearchWords())){
        return ResponseResult.errorResult(AppHttpCodeEnum.PARAM_INVALID,"参数不合法");
    }
    ApUser user=AppThreadLocalUtil.getUser();
    //1.1 异步调用保存搜索记录
    if(user!=null&& userSearchDto.getFromIndex()==0){
        apUserSearchService.insert(userSearchDto.getSearchWords(), AppThreadLocalUtil.getUser().getId());
    }
    //2. 设置查询条件
    SearchRequest searchRequest = new SearchRequest("app_info_article");
    SearchSourceBuilder searchSourceBuilder = new SearchSourceBuilder();

apUserSearchService.insert(userSearchDto.getSearchWords(), AppThreadLocalUtil.getUser().getId());添加异步调用方法@Async

@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. 存在则更新时间

并且在com.heima.search.SearchApplication启动类中添加启动异步方法@EnableAsync

@SpringBootApplication
@EnableDiscoveryClient
@EnableAsync
public class SearchApplication {

    public static void main(String[] args) {
        SpringApplication.run(SearchApplication.class,args);
    }
}

4.5 测试

启动

在这里插入图片描述

搜索黑马

在这里插入图片描述

查看mongoDB,说明插入成功

在这里插入图片描述

测试10个会不会删除

在这里插入图片描述

需求满足

5 app端搜索-加载搜索历史

5.1 接口

在这里插入图片描述

5.2 Controller

创建com.heima.search.controller.v1.ApUserSearchController

@RestController
@RequestMapping("/api/v1/history")
public class ApUserSearchController {
    @Autowired
    private ApUserSearchService apUserSearchService;
    @PostMapping("/load")
    public ResponseResult load() {
        return apUserSearchService.load();
    }
}

5.3 Service

接口:

public interface ApUserSearchService {
    public void insert(String keyword, Integer userId);

    public ResponseResult load();

}

实现

@Override
public ResponseResult load() {
    //1. 获取当前用户
    ApUser user = AppThreadLocalUtil.getUser();
    if(user == null) {
        return ResponseResult.errorResult(400, "请先登录");
    }
    //2. 查询用户搜索历史记录
    Query query = Query.query(Criteria.where("userId").is(user.getId()))
            .with(Sort.by(Sort.Order.desc("createdTime")));
    List<ApUserSearch> apUserSearches = mongoTemplate.find(query, ApUserSearch.class);
    return ResponseResult.okResult(apUserSearches);

}

5.4 测试

在这里插入图片描述

探花都在

6 app端搜索-删除搜索历史

6.1 接口

在这里插入图片描述

6.2 Dto

创建com.heima.model.search.dtos.HistorySearchDto类接收历史记录id

public class HistorySearchDto {
    /**
     * 接收历史记录id
     */
    String id;
}

6.3 Controller

@PostMapping("/del")
public ResponseResult del(@RequestBody HistorySearchDto historySearchDto) {
    return apUserSearchService.deleteHistorySearch(historySearchDto);
}

6.4 Service

接口:

ResponseResult deleteHistorySearch(HistorySearchDto historySearchDto);

实现:

@Override
public ResponseResult deleteHistorySearch(HistorySearchDto historySearchDto) {
    //1. 获取当前用户
    ApUser user = AppThreadLocalUtil.getUser();
    if(user == null) {
        return ResponseResult.errorResult(400, "请先登录");
    }
    //2. 删除用户搜索历史记录
    Query query = Query.query(Criteria.where("userId").is(user.getId())
            .and("id").is(historySearchDto.getId()));
    mongoTemplate.remove(query, ApUserSearch.class);
    return ResponseResult.okResult(AppHttpCodeEnum.SUCCESS);
}

6.5 测试

在这里插入图片描述

探花7已经没有了

查看数据库

在这里插入图片描述

永失我探花7,测试成功

7 app端搜索-关键字联想功能

7.1 需求分析

es的补全使这个数据本身在es的数据库里就有,才能补全,而这个是不管有没有都能补全。

这是与es的补全不同的地方。

我觉得老师这个不好,因为ES补全的是可以搜索到文章的,我要用ES实现。

7.2 接口定义

在这里插入图片描述

在这里插入图片描述

7.3 自动补全py插件

要实现根据字母自动补全,就必须对文章按照拼音分词。在GitHub上恰好有es的拼音分词插件,版本是7.4.0。

将py插件放到es的插件库里

在这里插入图片描述

重启es

docker restart es

测试

在这里插入图片描述

{
    "analyzer":"pinyin",
    "text":"欢迎来到黑马学习java"
}

响应

{
    "tokens": [
        {
            "token": "huan",
            "start_offset": 0,
            "end_offset": 0,
            "type": "word",
            "position": 0
        },
        {
            "token": "hyldhmxxjava",
            "start_offset": 0,
            "end_offset": 0,
            "type": "word",
            "position": 0
        },
        {
            "token": "ying",
            "start_offset": 0,
            "end_offset": 0,
            "type": "word",
            "position": 1
        },
        {
            "token": "lai",
            "start_offset": 0,
            "end_offset": 0,
            "type": "word",
            "position": 2
        },
        {
            "token": "dao",
            "start_offset": 0,
            "end_offset": 0,
            "type": "word",
            "position": 3
        },
        {
            "token": "hei",
            "start_offset": 0,
            "end_offset": 0,
            "type": "word",
            "position": 4
        },
        {
            "token": "ma",
            "start_offset": 0,
            "end_offset": 0,
            "type": "word",
            "position": 5
        },
        {
            "token": "xue",
            "start_offset": 0,
            "end_offset": 0,
            "type": "word",
            "position": 6
        },
        {
            "token": "xi",
            "start_offset": 0,
            "end_offset": 0,
            "type": "word",
            "position": 7
        },
        {
            "token": "ja",
            "start_offset": 0,
            "end_offset": 0,
            "type": "word",
            "position": 8
        },
        {
            "token": "v",
            "start_offset": 0,
            "end_offset": 0,
            "type": "word",
            "position": 9
        },
        {
            "token": "a",
            "start_offset": 0,
            "end_offset": 0,
            "type": "word",
            "position": 10
        }
    ]
}

7.4 自定义分词器

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

删除以前索引和文档

创建自定义分词器以及新的索引

在这里插入图片描述

{
  "settings": {
    "analysis": {
      "analyzer": { 
        "my_analyzer": { 
          "tokenizer": "ik_max_word",
          "filter": "py"
        },
        "completion_analyzer": {
            "tokenizer": "keyword",
          	"filter": "py"
        }
      },
      "filter": {
        "py": { 
          "type": "pinyin",
          "keep_full_pinyin": false,
          "keep_joined_full_pinyin": true,
          "keep_original": true,
          "limit_first_letter_length": 16,
          "remove_duplicated_term": true,
          "none_chinese_pinyin_tokenize": false
        }
      }
    }
  },
  "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":"my_analyzer",
                "search_analyzer":"ik_smart",
                "copy_to": "all"
            },
            "content":{
                "type":"text",
                "analyzer":"my_analyzer"
            },
            "suggestion":{
                "type":"completion",
                "analyzer":"completion_analyzer"    
            }
        }
    }
} 

因为多了suggestion字段,因此创建一个实体类接收com.heima.model.search.vos.SearchArticlewithSuggestion

@Data
public class SearchArticlewithSuggestion {

    // 文章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;
    //建议
    private List<String> suggestion;
    SearchArticlewithSuggestion(SearchArticleVo vo){
        BeanUtils.copyProperties(vo,this);
        this.suggestion= Arrays.asList(this.title);
    }

}

响应

{
    "acknowledged": true,
    "shards_acknowledged": true,
    "index": "app_info_article"
}

导入数据,创建新的单元测试

@Test
void testBatchInsertIndexDocument2() throws IOException {

    ///1. 查询文章列表
    List<SearchArticleVo> searchArticleVos = apArticleMapper.loadArticleList();
    //2. 将数据导入到es中
    BulkRequest bulkRequest = new BulkRequest("app_info_article");
    for(SearchArticleVo searchArticleVo : searchArticleVos){
        SearchArticlewithSuggestion searchArticleVoWithSuggestion = new SearchArticlewithSuggestion(searchArticleVo);
        IndexRequest indexRequest = new IndexRequest()
                .id(searchArticleVoWithSuggestion.getId().toString())
                .source(JSON.toJSONString(searchArticleVoWithSuggestion), XContentType.JSON);
        //添加到批量请求中
        bulkRequest.add(indexRequest);
    }
    restHighLevelClient.bulk(bulkRequest, RequestOptions.DEFAULT);
}

导入后查看数据,成功有suggestion

在这里插入图片描述

测试自动补全

{
  "suggest": {
    "title_suggest": {
      "text": "h", 
      "completion": {
        "field": "suggestion", 
        "skip_duplicates": false, 
        "size": 10 
      }
    }
  }
}

在这里插入图片描述

{
    "took": 30,
    "timed_out": false,
    "_shards": {
        "total": 1,
        "successful": 1,
        "skipped": 0,
        "failed": 0
    },
    "hits": {
        "total": {
            "value": 0,
            "relation": "eq"
        },
        "max_score": null,
        "hits": []
    },
    "suggest": {
        "title_suggest": [
            {
                "text": "h",
                "offset": 0,
                "length": 1,
                "options": [
                    {
                        "text": "黄龄工作室发视频回应",
                        "_index": "app_info_article",
                        "_type": "_doc",
                        "_id": "1302977754114826241",
                        "_score": 1.0,
                        "_source": {
                            "authorId": 4,
                            "authorName": "admin",
                            "content": "[{\"type\":\"text\",\"value\":\"3黄龄工作室发视频回应黄龄工作室发视频回应黄龄工作室发视频回应黄龄工作室发视频回应黄龄工作室发视频回应黄龄工作室发视频回应黄龄工作室发视频回应黄龄工作室发视频回应黄龄工作室发视频回应黄龄工作室发视频回应黄龄工作室发视频回应黄龄工作室发视频回应黄龄工作室发视频回应黄龄工作室发视频回应黄龄工作室发视频回应\"},{\"type\":\"image\",\"value\":\"http://192.168.200.130/group1/M00/00/00/wKjIgl892vuAXr_MAASCMYD0yzc919.jpg\"},{\"type\":\"text\",\"value\":\"请在这里输入正文黄龄工作室发视频回应黄龄工作室发视频回应黄龄工作室发视频回应黄龄工作室发视频回应黄龄工作室发视频回应黄龄工作室发视频回应黄龄工作室发视频回应黄龄工作室发视频回应黄龄工作室发视频回应黄龄工作室发视频回应黄龄工作室发视频回应黄龄工作室发视频回应\"}]",
                            "id": 1302977754114826241,
                            "images": "http://192.168.204.129:9000/leadnews/2024/03/25/71413acd3df847759c70b121ed526ff9.jpg",
                            "layout": 1,
                            "publishTime": 1599489079000,
                            "suggestion": [
                                "黄龄工作室发视频回应"
                            ],
                            "title": "黄龄工作室发视频回应"
                        }
                    },
                    {
                        "text": "黑马测试搜索123",
                        "_index": "app_info_article",
                        "_type": "_doc",
                        "_id": "1777572583391236097",
                        "_score": 1.0,
                        "_source": {
                            "authorId": 1102,
                            "authorName": "admin",
                            "content": "[{\"type\":\"image\",\"value\":\"http://192.168.204.129:9000/leadnews/2024/03/25/40ecf5d3f9084e1094b5469fe48f7603.jpg\"},{\"type\":\"text\",\"value\":\"请在这里输入正文\"}]",
                            "id": 1777572583391236097,
                            "images": "http://192.168.204.129:9000/leadnews/2024/03/25/40ecf5d3f9084e1094b5469fe48f7603.jpg",
                            "layout": 1,
                            "publishTime": 1712641152000,
                            "staticUrl": "http://192.168.204.129:9000/leadnews/2024/04/09/1777572583391236097.html",
                            "suggestion": [
                                "黑马测试搜索123"
                            ],
                            "title": "黑马测试搜索123"
                        }
                    }
                ]
            }
        ]
    }
}

只能匹配到第一个字。

分析:如果想要句中也能匹配,就要把completion_analyzer的tokenizer变成ik_max_word,保证都能覆盖到,而不是keyword作为一个整体。

在这里插入图片描述

如果想要做成黑马旅游那样,就要改前端了,这不是本节课的重点,我也就不进行了。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/530209.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

idea中MySQL数据库的配置

在IntelliJ IDEA中配置数据库可以通过以下步骤进行&#xff1a; 打开IntelliJ IDEA&#xff0c;在菜单栏中选择"View" -> "Tool Windows" -> "Database"&#xff0c;打开Database工具窗口。 在Database工具窗口上方&#xff0c;点击"…

springboot+vue2+elementui+mybatis- 批量导出导入

全部导出 批量导出 报错问题分析 经过排查&#xff0c;原因是因为在发起 axios 请求的时候&#xff0c;没有指定响应的数据类型&#xff08;这里需要指定响应的数据类型为 blob 二进制文件&#xff09; 当响应数据回来后&#xff0c;会执行 axios 后置拦截器的代码&#xff0…

相机模型浅析

相机模型 文章目录 相机模型四个坐标系针孔相机模型世界坐标系到相机坐标系相机坐标系到图像坐标系图像坐标到像素坐标 四个坐标系 ①世界坐标系&#xff1a;是客观三维世界的绝对坐标系&#xff0c;也称客观坐标系。因为数码相机安放在三维空间中&#xff0c;我们需要世界坐标…

主流排序简单集合

排序算法集合 选择排序 图解&#xff1a;以此类推直至 /*选择排序*/ void select_sort(vector<int>& nums) {/*选取一个基准元素逐个与后面的比较*/for (int i 0; i < nums.size() - 1-1; i) {int min i;/*定义随之变化的基准元素*/for (int j i 1; j <…

华为 2024 届校园招聘-硬件通⽤/单板开发——第一套(部分题目分享,完整版带答案,共十套)

华为 2024 届校园招聘-硬件通⽤/单板开发——第一套 部分题目分享&#xff0c;完整版带答案(有答案和解析&#xff0c;答案非官方&#xff0c;未仔细校正&#xff0c;仅供参考&#xff09;&#xff08;共十套&#xff09;获取&#xff08;WX:didadidadidida313&#xff0c;加我…

GEE:研究区(Polygon)样式设置

作者:CSDN @ _养乐多_ 本文将介绍在 Google Earth Engine (GEE)平台上为 polygon (面)数据设置样式的方法和代码,polygon 可以设置成任何颜色,以增加可视化效果更好理解数据分布。 结果如下图所示, 文章目录 一、统一样式1.1 示例代码1.2 示例代码链接二、根据区域名…

基于SpringBoot2.x、SpringCloud和SpringCloudAlibaba并采用前后端分离的企业级微服务多租户系统架构

简介 基于SpringBoot2.x、SpringCloud和SpringCloudAlibaba并采用前后端分离的企业级微服务多租户系统架构。并引入组件化的思想实现高内聚低耦合并且高度可配置化&#xff0c;适合学习和企业中使用。 真正实现了基于RBAC、jwt和oauth2的无状态统一权限认证的解决方案&#x…

element UI table合并单元格方法

废话不多讲&#xff0c;直接上代码&#xff0c;希望能帮到需要的朋友 // 合并单元格function spanMethod({ row, column, rowIndex, columnIndex }) {//定义需要合并的列字段&#xff0c;有哪些列需要合并&#xff0c;就自定义添加字段即可const fields [declareRegion] // …

hive-3.1.2分布式搭建与hive的三种交互方式

hive-3.1.2分布式搭建&#xff1a; 一、上传解压配置环境变量 在官网或者镜像站下载驱动包 华为云镜像站地址&#xff1a; hive&#xff1a;Index of apache-local/hive/hive-3.1.2 mysql驱动包&#xff1a;Index of mysql-local/Downloads/Connector-J # 1、解压 tar -zx…

采用Flink CDC操作SQL Server数据库获取增量变更数据

采用Flink CDC操作SQL Server数据库获取增量变更数据 Flink CDC 1.12版本引入了对SQL Server的支持&#xff0c;包括SqlServerCatalog和SqlServerTable。在SqlServerCatalog中&#xff0c;你可以根据表名获取对应的字段和字段类型。 SQL Server 2008 开始支持变更数据捕获 (C…

李廉洋:4.10黄金原油走势最新分析及策略。

美联储博斯蒂克重申了他对今年降息一次的预期&#xff0c;但他补充说&#xff0c;如果经济形势发生变化&#xff0c;他对推迟降息或进一步降息持开放态度。博斯蒂克强调了美国经济和劳动力市场的持续强劲&#xff0c;但表示就业市场的疲软迹象将促使他考虑比目前预期的更早和更…

GFS部署实验

目录 1、部署环境 ​编辑 2、更改节点名称 3、准备环境 4、磁盘分区&#xff0c;并挂载 5. 做主机映射--/etc/hosts/ 6. 复制脚本文件 7. 执行脚本完成分区 8. 安装客户端软件 1. 安装解压源包 2. 创建gfs 3. 安装 gfs 4. 开启服务 9、 添加节点到存储信任池中 1…

SVM向量支持机

1.通俗理解 svm&#xff1a;support vector machine目标&#xff1a;利用超平面将两类数据分割开来&#xff0c;这个超平面就是我们要设计的对象 如何设计&#xff1f;我们设计之后会有间隔&#xff0c;间隔越大分类效果就越好&#xff1b;距离决策边界最近的点我们成为支持向…

40.Python从入门到精通—Python3 JSON 数据解析 Python3 日期和时间 什么是时间元组? 获取当前时间 获取格式化的时间

40.Python从入门到精通—Python3 JSON 数据解析 Python3 日期和时间 什么是时间元组&#xff1f; 获取当前时间 获取格式化的时间 Python3 JSON 数据解析Python3 日期和时间什么是时间元组&#xff1f;获取当前时间获取格式化的时间 Python3 JSON 数据解析 Python3 中可以使用…

使用MongoDB 构建AI:轻松应对从预测式AI到生成式AI

毫无疑问&#xff0c;如今从生成式AI (GenAI )中获益最大的&#xff0c;是那些早已运用预测式AI (Predictive AI )的组织。 2023年6月&#xff0c;麦肯锡在2023年6月发布的《生成式人工智能的经济潜力》研究中也得出了与此相同的结论。 原因主要有以下几点&#xff1a; 内部文…

顺序表(C语言实现)

什么是顺序表 顺序表和数组的区别 顺序表本质就是数组 结构体初阶进阶 系统化的学习-CSDN博客 简单解释一下&#xff0c;就像大家去吃饭&#xff0c;然后左边是苍蝇馆子&#xff0c;右边是修饰过的苍蝇馆子&#xff0c;但是那个好看的苍蝇馆子一看&#xff0c;这不行啊&a…

Harmony鸿蒙南向驱动开发-MIPI CSI

CSI&#xff08;Camera Serial Interface&#xff09;是由MIPI联盟下Camera工作组指定的接口标准。CSI-2是MIPI CSI第二版&#xff0c;主要由应用层、协议层、物理层组成&#xff0c;最大支持4通道数据传输、单线传输速度高达1Gb/s。 物理层支持HS&#xff08;High Speed&…

kettle经验篇:取出一个字符串中的两个数值

项目场景 在一个数据清洗、同步的需求中&#xff1b;有一个要求是判断两个数值是否在正常范围内&#xff0c;并根据判断结果给出异常标记。但这两个数值是以XML的格式存储在Oracle的CLOB字段中&#xff0c;且在同一个XML节点中。该节点的内容如下: "OD: 17.4 mmHg O…

2024Peak码支付系统网站源码

系统简介 Peak码支付-是专为个人站长打造的聚合免签系统&#xff0c;拥有卓越的性能和丰富的功能。用全新轻量化的界面UI&#xff0c;让您可以更加方便快捷地解决知识付费和运营赞助的难题。同时&#xff0c;它基于高性能SpeedPHPLayuiPearAdmin架构&#xff0c;提供实时监控和…

https的配置和使用(以腾讯云为例)

1、注册域名 2、获取证书 3、下载证书 下载下来的证书所有格式 4、在服务器上下载nginx并配置 nginx的配置文件 如下 server {listen 80;listen 443 ssl;server_name delegate.letspiu.net.cn;ssl on; #开启ssl#指定证书位置ssl_certificate /etc/ss…