第6章 Elasticsearch,分布式搜索引擎

6.1 Elasticsearch入门

image-20220725112019115

image-20220725112221624

https://www.elastic.co

  • 6.0之后索引对应表,文档对应行,字段对应列
# 6.0 之前
ES索引、类型、文档、字段和MySQL中的 数据库、表、行、列相对应。
ES中文档的数据通常采用的是JSON,JSON中的每一种属性叫字段。

但是在 ES6.0 之后的版本当中,这些关系逐渐要发生变化,主要集中在前两个部分,主要是它想废弃类型的这个概念,那么谁对应表呢:索引。所以,6.0 之后一个索引对应一张表,文档还是对应行,字段还是对应列。6.0 之后仍保留了类型,只不过类型一个固定的单词,而不是表名了,7.0之后就彻底废弃掉了。
  • 集群:一台或多台ES服务器组合在一起就是一个集群

  • 节点:集群当中的每一台服务器称为节点

  • 分片:一个索引也就是一个表,分片指的是对索引进一步的划分,一个索引在存的时候可以拆分成多个分片进

    ​ 行存储,这样的话并发能力就提高了。

  • 副本:副本是对分片的备份,一个分片可以包含多个副本,有了备份以后,万一说某一个副本数据丢了,挂

    ​ 了,那么还有其他的备份,这样的话提高系统的可用性。

安装ES

https://www.elastic.co/cn/elasticsearch/

image-20220725144000932

image-20220725145122997

image-20220725151659198

image-20220725151805253

将下载好的压缩包解压缩

解压好之后我们需要稍微改一下它的配置

image-20220725154354607

image-20220725174156933

然后我们来配置一下环境变量,因为一会我们会通过命令行的方式运行ES的常用命令,

image-20220725160254396

image-20220725160232299

然后我们还要装一个中文的分词插件,为什么呢,比如搜“互联网校招”,肯定是分成“互联网”和“校招”分别查询

这个分词插件在GitHub上

下载链接:

https://github.com/medcl/elasticsearch-analysis-ik/releases/download/v6.4.3/elasticsearch-analysis-ik-6.4.3.zip

image-20220725171044667

image-20220725171223844

image-20220725171405524

image-20220725171435951

下载好压缩包之后必须解压缩到固定的目录下,首先进入elasticsearch-6.4.3\plugins 目录下, 然后自己新建一个ik文件夹 我们在将elasticsearch ik解压到ik文件夹下 记住千万不要 将elasticsearch-analysis-ik-6.4.3这个文件夹放入到ik 我们只需要将 elasticsearch-analysis-ik-6.4.3 里的那些文件放入到 ik 即可

image-20220725174718408

如果有网络新词出现

image-20220725172013300

然后我们再安装一个文件

https://www.getpostman.com/

postman可以模拟web客户端, 说白了可以模仿网页,发送http请求,为什么需要这么一个工具呢,因为其实我们直接通过命令行去访问ES服务器,如果是查询某些东西还好,但是如果要往里面存东西,这个命令太长了,记不住,也很难写,因为ES支持用http方式去访问,如果我们有一个现成的网页,有一个框往里面填数据很方便,但是我们没有这个网页,那么这个 postman 就能替代那个网页,然后可以通过框构造一些数据提交给ES服务器,这样比较方便。即便是我们从ES中搜索数据,如果说我们搜索的规则比较复杂,那个时候命令也很难写,使用postman会比较方便。总之,为了提高入门ES使用的体验,我们使用postman。

演示使用ES

首先我们演示如何通过命令行的方式访问它,当然在访问之前,我们得将 elasticsearch 启动起来

image-20220725173653511

这里我们双击

image-20220725174939354

打开命令行工具cmd作为客户端访问一下服务器

# ES命令

查看ES集群的健康状况
curl -X GET "localhost:9200/_cat/health?v"       

查看集群中有什么节点
curl -X GET "localhost:9200/_cat/nodes?v"   

查看当前的ES服务器有多少个索引
curl -X GET "localhost:9200/_cat/indices?v" 

创建索引
curl -X PUT "localhost:9200/test" 

删除索引
curl -X DELETE "localhost:9200/test" 

image-20220725181142393

接下来演示一下使用postman来代替web客户端去访问ES

image-20220725182052331

image-20220725182210656

image-20220725182451548

如何往ES里面插入数据改数据插入数据是一样的,只不过是把提交的数据修改一下再插入,ES底层会先删再添加

image-20220725183258377

查询某条数据

image-20220725183611502

删除某条数据

image-20220725184342523

ES存在的价值在于它里面的数据能够被我们搜索,这个搜索不是像查数据库,而是说提供一句话,需要分词,再去库里去匹配,匹配的时候还不是固定的某一个字段,既想搜title,又想搜content,是全文的匹配,那么数据库这一点是做不到的,它不能分词,接下来演示一下如何实现这种搜索,我们先往里面存三条数据,

image-20220725184921335

image-20220725185029229

image-20220725185210212

三条数据都插入了,接下来是搜索

localhost:9200/test/_search
没有加条件就是全部搜索

image-20220725185716247

搜索标题title,查询标题有“互联网”字样的
localhost:9200/test/_search?q=title:互联网

image-20220725185926351

搜索内容中含有"运营实习"字样的
localhost:9200/test/_search?q=content:运营实习

image-20220725190252999

既要搜title又要搜content,只要包含这个词条都显示出来,这时候搜的逻辑就有点复杂了,通过路径搞不定了,

路径
localhost:9200/test/_search
具体的搜索条件在Body中提交
{
    "query": {
        "multi_match": {
            "query": "互联网",
            "fields": ["title", "content"]
        }
    }
}

image-20220725191151632

6.2 Spring整合Elasticsearch

image-20220726070407515

引入依赖

注意:我的SpringBoot父版本是2.1.5.RELEASE,对于高版本的elasticsearch配置可能会有所不同

<dependency>
  <groupId>org.springframework.boot</groupId>
  <artifactId>spring-boot-starter-data-elasticsearch</artifactId>
</dependency>
配置Elasticsearch

application.properties 配置文件中进行配置

注:我这里使用的elasticsearch是低版本的,高版本的配置可能会略有不同。

127.0.0.0localhost 等价

es中9200是http访问的端口,9300是tcp端口也是默认启用的,我们应用服务通常会用9300端口tcp去访问它。

#ElasticsearchProperties
# 配置集群名字,以前我们在es配置文件里改过es集群的名字
spring.data.elasticsearch.cluster-name=nowcoder
# 配置集群中各个结点(当然,我们这里只有一个结点)
spring.data.elasticsearch.cluster-nodes=127.0.0.1:9300

image-20220726075255192

elasticsearch底层是基于netty,我们之前安装的redis底层也是基于netty,这两者在启用netty的时候有冲突,主要体现在es底层代码上,我们需要稍微做一个变通。

在项目的启动入口类CommunityApplication里面

@PostConstruct
public void init(){
   // 解决Netty启动冲突问题(看NettyRuntime中setAvailableProcessors方法和Netty4Utils类)
   System.setProperty("es.set.netty.runtime.available.processors", "false");
}

image-20220726075457260

Spring Data Elasticsearch

我们现在要把数据库里存的帖子存到es服务器里,然后我们去es服务器搜索这个帖子,我们可以使用 ElasticsearchTemplateElasticsearchRepository 去做这个事情。

ElasticsearchRepository

ElasticsearchRepository简单,我们先用这种方案,当有些需求它不好解决的时候再用ElasticsearchTemplate

在使用 ElasticsearchRepository 之前我们需要做一个配置,需要告诉它帖子这个表和es里要存的那个索引之间是什么样的对应关系,这个表存到es里变成索引的时候每个字段对应是什么样的类型,用什么方式搜素,这些都要做配置,这个配置呢不需要我们写xml文件,我们通过注解就可以,实体类上要加上这个注解,因为我们是针对帖子的操作。

类上加上注解,映射到哪个索引上去,映射到什么类型上去,创建几个分片几个副本,日后调用api时,如果没有索引会自动创建索引,然后没有分片、副本,会自动根据配置创建,然后再往索引里插入数据。然后为了让实体中的属性索引中的字段对应,所以我们在属性上也需要加上注解配置。

我们搜帖子主要就是搜标题内容

analyzer存储的时候的解析器,会将搜索的词拆分成更多的词然后与这句话建立一个索引与这句话匹配,增大搜索范围

searchAnalyzer搜索的时候的解析器,聪明的分词器,拆分出少的但是符合预期的词

image-20220726083615035

下面我们定义ElasticsearchRepository接口

泛型里面写要处理的实体类和主键是什么类型

@Repository         // es可以被看成一个特殊的数据库
public interface DiscussPostRepository extends ElasticsearchRepository<DiscussPost, Integer> {

}

image-20220726084938878

接下来我们来测试一下 ElasticsearchRepositoryElasticsearchTemplate

注意:测试的时候一定要将 Elasticsearch 服务 和 Kafka(还有zookeeper,因为项目用到了这些服务)打开

测试类

@RunWith(SpringRunner.class)
@SpringBootTest
@ContextConfiguration(classes = CommunityApplication.class)
public class ElasticsearchTests {

    @Autowired
    private DiscussPostMapper discussMapper;    // 先从mysql取出数据

    @Autowired
    private DiscussPostRepository discussRepository;   // 注入刚才的那个接口以便于将数据存到es查询

    @Autowired
    private ElasticsearchTemplate elasticTemplate;    // 有些情况DiscussPostRepository解决不了就用这个

    @Test
    public void testInsert() {
        // 插入数据
        discussRepository.save(discussMapper.selectDiscussPostById(241));
        discussRepository.save(discussMapper.selectDiscussPostById(242));
        discussRepository.save(discussMapper.selectDiscussPostById(243));
    }

    @Test
    public void testInsertList() {
        discussRepository.saveAll(discussMapper.selectDiscussPosts(101, 0, 100));
        discussRepository.saveAll(discussMapper.selectDiscussPosts(102, 0, 100));
        discussRepository.saveAll(discussMapper.selectDiscussPosts(103, 0, 100));
        discussRepository.saveAll(discussMapper.selectDiscussPosts(111, 0, 100));
        discussRepository.saveAll(discussMapper.selectDiscussPosts(112, 0, 100));
        discussRepository.saveAll(discussMapper.selectDiscussPosts(131, 0, 100));
        discussRepository.saveAll(discussMapper.selectDiscussPosts(132, 0, 100));
        discussRepository.saveAll(discussMapper.selectDiscussPosts(133, 0, 100));
        discussRepository.saveAll(discussMapper.selectDiscussPosts(134, 0, 100));
    }

    @Test
    public void testUpdate() {
        DiscussPost post = discussMapper.selectDiscussPostById(231);
        post.setContent("我是新人,使劲灌水.");
        discussRepository.save(post);
    }

    @Test
    public void testDelete() {
         //discussRepository.deleteById(231);
        discussRepository.deleteAll();
    }
    // 搜索功能
    @Test
    public void testSearchByRepository() {
        // 构造搜索条件:要不要排序、分页并且搜索结果要不要高亮显示等
        SearchQuery searchQuery = new NativeSearchQueryBuilder()
                .withQuery(QueryBuilders.multiMatchQuery("互联网寒冬", "title", "content"))//搜索的关键词并且在哪搜
                .withSort(SortBuilders.fieldSort("type").order(SortOrder.DESC))        // 排序方式:倒序
                .withSort(SortBuilders.fieldSort("score").order(SortOrder.DESC))       // 排序方式:倒序
                .withSort(SortBuilders.fieldSort("createTime").order(SortOrder.DESC))  // 排序方式:倒序
                .withPageable(PageRequest.of(0, 10))                        // 分页方式
                .withHighlightFields(       // 指定哪些字段要高亮显示,怎么高亮显示
                        new HighlightBuilder.Field("title").preTags("<em>").postTags("</em>"),  // 高亮显示
                        new HighlightBuilder.Field("content").preTags("<em>").postTags("</em>") // 高亮显示
                ).build();

        // elasticTemplate.queryForPage(searchQuery, class, SearchResultMapper)
        // 底层获取得到了高亮显示的值, 但是没有返回.

        // 这个Page不是我们自己写的那个实体类,而是java提供的
        Page<DiscussPost> page = discussRepository.search(searchQuery);
        System.out.println(page.getTotalElements());        // 一共有多少条数据匹配
        System.out.println(page.getTotalPages());           // 一共有多少页
        System.out.println(page.getNumber());               // 当前处在第几页
        System.out.println(page.getSize());                 // 每一页最多显示几条数据
        for (DiscussPost post : page) {                     // 查看查询到的数据
            System.out.println(post);
        }
    }

    @Test
    public void testSearchByTemplate() {
        // 构造搜索条件:要不要排序、分页并且搜索结果要不要高亮显示等
        SearchQuery searchQuery = new NativeSearchQueryBuilder()
                .withQuery(QueryBuilders.multiMatchQuery("互联网寒冬", "title", "content"))//搜索的关键词并且在哪搜
                .withSort(SortBuilders.fieldSort("type").order(SortOrder.DESC))        // 排序方式:倒序
                .withSort(SortBuilders.fieldSort("score").order(SortOrder.DESC))       // 排序方式:倒序
                .withSort(SortBuilders.fieldSort("createTime").order(SortOrder.DESC))  // 排序方式:倒序
                .withPageable(PageRequest.of(0, 10))                        // 分页方式
                .withHighlightFields(       // 指定哪些字段要高亮显示,怎么高亮显示
                        new HighlightBuilder.Field("title").preTags("<em>").postTags("</em>"),  // 高亮显示
                        new HighlightBuilder.Field("content").preTags("<em>").postTags("</em>") // 高亮显示
                ).build();
        // 参数1:搜索条件      参数2:实体类型    参数3:SearchResultMapper接口(实现一个匿名内部类或者传一个实现类)
        Page<DiscussPost> page = elasticTemplate.queryForPage(searchQuery, DiscussPost.class, new SearchResultMapper() {
            @Override            // queryForPage得到结果然后交给mapResults处理,然后通过SearchResponse参数处理
            public <T> AggregatedPage<T> mapResults(SearchResponse response, Class<T> aClass, Pageable pageable) {
                SearchHits hits = response.getHits();   // 先取到这次搜索命令的数据(里面可以是多条数据)
                if (hits.getTotalHits() <= 0) {         // 判断有没有数据
                    return null;
                }
                // 执行到这里说明有数据
                List<DiscussPost> list = new ArrayList<>();
                for (SearchHit hit : hits) {        // 遍历命中的数据将其放在集合里
                    DiscussPost post = new DiscussPost();   // 将命中的数据包装到实体类中
                    // hit里面是将数据封装成了map并且里面key和value都是String类型,我们可以从中取值
                    String id = hit.getSourceAsMap().get("id").toString();
                    post.setId(Integer.valueOf(id)); // 将字符类型的数转成整数存入实体类的id属性

                    String userId = hit.getSourceAsMap().get("userId").toString();
                    post.setUserId(Integer.valueOf(userId));

                    String title = hit.getSourceAsMap().get("title").toString();
                    post.setTitle(title);

                    String content = hit.getSourceAsMap().get("content").toString();
                    post.setContent(content);

                    String status = hit.getSourceAsMap().get("status").toString();
                    post.setStatus(Integer.valueOf(status));

                    String createTime = hit.getSourceAsMap().get("createTime").toString();
                    post.setCreateTime(new Date(Long.valueOf(createTime)));

                    String commentCount = hit.getSourceAsMap().get("commentCount").toString();
                    post.setCommentCount(Integer.valueOf(commentCount));

                    // 处理高亮显示的结果
                    HighlightField titleField = hit.getHighlightFields().get("title");
                    if (titleField != null) {
                        // getFragments()返回的是一个数组,因为匹配的词条有可能是多个,我们只将第一个设置成高亮即可
                        post.setTitle(titleField.getFragments()[0].toString());
                    }

                    HighlightField contentField = hit.getHighlightFields().get("content");
                    if (contentField != null) {
                        post.setContent(contentField.getFragments()[0].toString());
                    }

                    list.add(post);
                }
                // AggregatedPageImpl   参数1:集合 参数2:方法参数pageable 参数3:一共多少条数据
                //                      参数4:     参数5:                 参数6:
                return new AggregatedPageImpl(list, pageable,
                        hits.getTotalHits(), response.getAggregations(), response.getScrollId(), hits.getMaxScore());
            }
        });

        System.out.println(page.getTotalElements());
        System.out.println(page.getTotalPages());
        System.out.println(page.getNumber());
        System.out.println(page.getSize());
        for (DiscussPost post : page) {
            System.out.println(post);
        }
    }
}

6.3 开发社区搜索功能

image-20220726131945759

业务层

发布一个帖子的时候应该1.将帖子存到Elasticsearch服务器2.删帖子我们也应该从Elasticsearch服务器删去(当然现在删帖的功能还没有实现,但是我们在开发搜索服务的时候先把从Elasticsearch服务器删除帖子的方法先准备好,以后呢可以直接调用),然后重点就是我们要在组件里提供搜索的服务去3.搜索帖子

表现层

  1. 发布帖子时,采用异步的方式将帖子提交到Elasticsearch服务器

  2. 增加评论时,帖子的评论数量就会发生变化,这个时候我们也将帖子异步地提交到Elasticsearch服务器,相当于这是修改帖子

  3. 异步的方式主要是为了提高性能,当发了帖子以后,只要把事件丢到消息队列里,我们就可以继续处理下一个类似的请求,不用等待,所以说异步可以并行的处理一些事情,这样比较好。既然是异步的话,我们在发布帖子时、增加评论时触发了这样一个事件,我们需要在消费者组件里加一个方法来消费这个事件

当把数据同步到了ES服务器以后,剩下的就是查询了,查询的时候我们要想显示出搜索结果,我们需要在controller里处理搜索请求,然后在对应的html里显示结果。

首先先解决一个之前遗留的小问题

image-20220726150038650

然后正式开发刚才所述内容

事务层(Service)

新建一个 ElasticsearchService 处理业务层

@Service
public class ElasticsearchService {

    @Autowired
    private DiscussPostRepository discussRepository;    // 往ES里存、修改、删除数据、搜索可以用到

    @Autowired
    private ElasticsearchTemplate elasticTemplate;      // 这个的搜索方法可以做到高亮显示

    // 往ES里存数据(再存一次就是修改)
    public void saveDiscussPost(DiscussPost post) {
        discussRepository.save(post);
    }

    // 从ES里删除数据
    public void deleteDiscussPost(int id) {
        discussRepository.deleteById(id);
    }

    // 提供搜索方法并高亮显示  参数1:搜索的关键字, 搜索支持分页,传入分页条件 参数2:当前要显示第几页 参数3:每页显示多少条数据
    // Page是Spring提供的,不是我们自己写的实体类
    public Page<DiscussPost> searchDiscussPost(String keyword, int current, int limit) {
        SearchQuery searchQuery = new NativeSearchQueryBuilder()
                .withQuery(QueryBuilders.multiMatchQuery(keyword, "title", "content"))//搜索的关键词并且在哪个字段搜
                .withSort(SortBuilders.fieldSort("type").order(SortOrder.DESC))       // 排序方式:倒序
                .withSort(SortBuilders.fieldSort("score").order(SortOrder.DESC))      // 排序方式:倒序
                .withSort(SortBuilders.fieldSort("createTime").order(SortOrder.DESC)) // 排序方式:倒序
                .withPageable(PageRequest.of(current, limit))                         // 分页方式
                .withHighlightFields(    // 指定哪些字段要高亮显示,怎么高亮显示
                        new HighlightBuilder.Field("title").preTags("<em>").postTags("</em>"),  // 高亮显示
                        new HighlightBuilder.Field("content").preTags("<em>").postTags("</em>") // 高亮显示
                ).build();
        // 参数1:搜索条件      参数2:实体类型    参数3:SearchResultMapper接口(实现一个匿名内部类或者传一个实现类)
        return elasticTemplate.queryForPage(searchQuery, DiscussPost.class, new SearchResultMapper() {
            @Override               // queryForPage得到结果然后交给mapResults处理,然后通过SearchResponse参数处理
            public <T> AggregatedPage<T> mapResults(SearchResponse response, Class<T> aClass, Pageable pageable) {
                SearchHits hits = response.getHits();           // 先取到这次搜索命令的数据(里面可以是多条数据)
                if (hits.getTotalHits() <= 0) {                 // 判断有没有数据
                    return null;
                }

                List<DiscussPost> list = new ArrayList<>();
                for (SearchHit hit : hits) {                    // 遍历命中的数据将其放在集合里
                    DiscussPost post = new DiscussPost();       // 将命中的数据包装到实体类中
                    // hit里面是将数据封装成了map并且里面key和value都是String类型,我们可以从中取值
                    String id = hit.getSourceAsMap().get("id").toString();
                    post.setId(Integer.valueOf(id));            // 将字符类型的数转成整数存入实体类的id属性

                    String userId = hit.getSourceAsMap().get("userId").toString();
                    post.setUserId(Integer.valueOf(userId));

                    String title = hit.getSourceAsMap().get("title").toString();
                    post.setTitle(title);

                    String content = hit.getSourceAsMap().get("content").toString();
                    post.setContent(content);

                    String status = hit.getSourceAsMap().get("status").toString();
                    post.setStatus(Integer.valueOf(status));

                    String createTime = hit.getSourceAsMap().get("createTime").toString();
                    post.setCreateTime(new Date(Long.valueOf(createTime)));

                    String commentCount = hit.getSourceAsMap().get("commentCount").toString();
                    post.setCommentCount(Integer.valueOf(commentCount));

                    // 处理高亮显示的结果
                    HighlightField titleField = hit.getHighlightFields().get("title");
                    if (titleField != null) {
                        // getFragments()返回的是一个数组,因为匹配的词条有可能是多个,我们只将第一个设置成高亮即可
                        post.setTitle(titleField.getFragments()[0].toString());
                    }

                    HighlightField contentField = hit.getHighlightFields().get("content");
                    if (contentField != null) {
                        post.setContent(contentField.getFragments()[0].toString());
                    }

                    list.add(post);
                }
                // AggregatedPageImpl   参数1:集合 参数2:方法参数pageable 参数3:一共多少条数据
                //                      参数4:     参数5:                 参数6:
                return new AggregatedPageImpl(list, pageable,
                        hits.getTotalHits(), response.getAggregations(), response.getScrollId(), hits.getMaxScore());
            }
        });
    }
}

image-20220726160645620

image-20220726160807283

image-20220726161015082

触发事件(生产者)

用异步的方式去向ES服务器当中同步数据,我们是在发布帖子增加评论这两个地方同步数据(删除帖子目前还没做),我们在这两个点触发一个发帖事件

发帖事件先定义一个常量

CommunityConstant

/**
  * 主题: 发帖
  */
String TOPIC_PUBLISH = "publish";

image-20220726161100517

发布帖子时:

DiscussPostController

image-20220726161242572

image-20220726161323865

发布评论

CommentController

评论帖子以后帖子的评论数量,帖子就变了,这个时候需要触发一次事件把ES里的数据覆盖掉,其实是一个修改的行为。

image-20220726161500397

接下来我们需要去做的事情就是去消费这个事件

EventConsumer

消费者

image-20220726163037105

image-20220726163132084

表现层

最后就是展现,当我发一个帖子,这个帖子能够同步到ES服务器里,那就能搜到它,下面我们做的就是展现

新建一个 SearchController

@Controller
public class SearchController implements CommunityConstant {

    @Autowired
    private ElasticsearchService elasticsearchService;      // 用于查询

    @Autowired
    private UserService userService;                        // 搜到帖子以后还要展现作者

    @Autowired
    private LikeService likeService;                        // 搜到帖子以后还要展现帖子点赞的数量

    // search?keyword=xxx
    @RequestMapping(path = "/search", method = RequestMethod.GET)
    //                                  这里的Page是我们自己写的实体类
    //  参数1:搜索的关键字    参数2:传入分页的条件(我们封装的Page接收)   参数3:用于向模板传数据
    public String search(String keyword, Page page, Model model) {
        // 搜索帖子
        // 因为和实体类冲突了,所以会自动带上包名,泛型里写DiscussPost
        org.springframework.data.domain.Page<DiscussPost> searchResult =
                // 参数1:关键词          参数2:当前是第几页(方法要求从0开始)        参数3:每页显示多少条
                elasticsearchService.searchDiscussPost(keyword, page.getCurrent() - 1, page.getLimit());
        // 聚合数据(用户名、帖子点赞数量)
        List<Map<String, Object>> discussPosts = new ArrayList<>();
        if (searchResult != null) {
            for (DiscussPost post : searchResult) {
                Map<String, Object> map = new HashMap<>();
                // 帖子
                map.put("post", post);
                // 作者
                map.put("user", userService.findUserById(post.getUserId()));
                // 点赞数量
                map.put("likeCount", likeService.findEntityLikeCount(ENTITY_TYPE_POST, post.getId()));

                discussPosts.add(map);
            }
        }
        model.addAttribute("discussPosts", discussPosts);
        model.addAttribute("keyword", keyword);

        // 分页信息
        page.setPath("/search?keyword=" + keyword);         // 设置路径
        page.setRows(searchResult == null ? 0 : (int) searchResult.getTotalElements()); // 总共多少条数据

        return "/site/search";
    }

}

image-20220726170106830

image-20220726170257553

最后就是处理 html 了,首先我们需要处理的是搜索框,我们可以处理 首页index.html搜索框,其他页面复用这个index.htmlheader就可以复用这个代码了,所以我们首先要处理index.html

image-20220726171549322

最后是 search.html 好显示搜索的结果

image-20220726175451653

image-20220726175550122

image-20220726175821041

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值