576 lines
20 KiB
Markdown
576 lines
20 KiB
Markdown
![]() |
# 搜索
|
|||
|
|
|||
|
---
|
|||
|
|
|||
|
想要在 Elasticsearch 中搜索数据,需要把数据在 Elasticsearch 中再存一份
|
|||
|
|
|||
|

|
|||
|
|
|||
|
一个索引对应一个数据库(7.0 中索引对应表),一个类型对应一张表(7.0 已废弃该属性),一个文档对应表中的一行,一个字段对应表中的一列
|
|||
|
|
|||
|
## ElasticSearch 下载安装
|
|||
|
|
|||
|
注意,下载 ElasticSearch 版本一定要与你的 SpringBoot 版本内部规定的一致,我的是 SpringBoot 2.1.5
|
|||
|
|
|||
|

|
|||
|
|
|||
|
[Elasticsearch 6.4.3 下载地址](https://www.elastic.co/cn/downloads/past-releases/elasticsearch-6-4-3)
|
|||
|
|
|||
|
解压完毕后,需要配置一下:config/elasticsearch.yml
|
|||
|
|
|||
|
<img src="https://gitee.com/veal98/images/raw/master/img/20210129111542.png" style="zoom:50%;" />
|
|||
|
|
|||
|
配到环境变量中去:
|
|||
|
|
|||
|
<img src="https://gitee.com/veal98/images/raw/master/img/20210129112143.png" style="zoom:50%;" />
|
|||
|
|
|||
|
还需要安装一个**中文分词插件**(Elasticsearch 自带一个英文分词插件)[elasticsearch-analysis-ik 6.4.3 下载地址](https://github.com/medcl/elasticsearch-analysis-ik/releases/tag/v6.4.3)
|
|||
|
|
|||
|
注意:必须解压到你的 elasticsearch 安装目录的 plugins/ik 文件夹下(D:\elasticsearch-6.4.3\plugins\ik)
|
|||
|
|
|||
|
|
|||
|
|
|||
|
启动 elasticsearch
|
|||
|
|
|||
|
<img src="https://gitee.com/veal98/images/raw/master/img/20210129113947.png" style="zoom:50%;" />
|
|||
|
|
|||
|
常用命令:
|
|||
|
|
|||
|
```shell
|
|||
|
curl -X PUT "localhost:9200/test" 创建索引
|
|||
|
|
|||
|
curl -X GET "localhost:9200/_cat/indices?v" 查看索引
|
|||
|
|
|||
|
curl -X DELETE "localhost:9200/test" 删除索引
|
|||
|
```
|
|||
|
|
|||
|

|
|||
|
|
|||
|
可以使用 Postman 简化命令行的操作,比如创建索引:
|
|||
|
|
|||
|

|
|||
|
|
|||
|
增加一条数据:
|
|||
|
|
|||
|

|
|||
|
|
|||
|
查询:
|
|||
|
|
|||
|

|
|||
|
|
|||
|
删除:
|
|||
|
|
|||
|

|
|||
|
|
|||
|
搜索:搜索 title 中包含“互联网”的内容
|
|||
|
|
|||
|

|
|||
|
|
|||
|
复杂搜索:搜索 title 或 content 中包含 “互联网” 的内容
|
|||
|
|
|||
|

|
|||
|
|
|||
|
## Spring Boot 整合 Elasticsearch
|
|||
|
|
|||
|
### 引入依赖
|
|||
|
|
|||
|
```xml
|
|||
|
<!--Elasticsearch-->
|
|||
|
<dependency>
|
|||
|
<groupId>org.springframework.boot</groupId>
|
|||
|
<artifactId>spring-boot-starter-data-elasticsearch</artifactId>
|
|||
|
</dependency>
|
|||
|
```
|
|||
|
|
|||
|
### 配置 Elasticsearch
|
|||
|
|
|||
|
- cluster-name
|
|||
|
- cluster-nodes
|
|||
|
|
|||
|
```properties
|
|||
|
# Elasticsearch
|
|||
|
# 该字段见 Elasticsearch 安装包中的 elasticsearch.yml,可自行修改
|
|||
|
spring.data.elasticsearch.cluster-name = community
|
|||
|
spring.data.elasticsearch.cluster-nodes = 127.0.0.1:9300
|
|||
|
```
|
|||
|
|
|||
|
### 解决 Elasticsearch 和 Redis 底层的 Netty 启动冲突问题
|
|||
|
|
|||
|
```java
|
|||
|
@SpringBootApplication
|
|||
|
public class CommunityApplication {
|
|||
|
|
|||
|
/**
|
|||
|
* 解决 Elasticsearch 和 Redis 底层的 Netty 启动冲突问题
|
|||
|
*/
|
|||
|
@PostConstruct
|
|||
|
public void init() {
|
|||
|
System.setProperty("es.set.netty.runtime.available.processors", "false");
|
|||
|
}
|
|||
|
|
|||
|
public static void main(String[] args) {
|
|||
|
SpringApplication.run(CommunityApplication.class, args);
|
|||
|
}
|
|||
|
|
|||
|
}
|
|||
|
```
|
|||
|
|
|||
|
### Spring Data Elasticsearch
|
|||
|
|
|||
|
- ElasticsearchTemplate
|
|||
|
- ElasticsearchRepository(推荐)
|
|||
|
|
|||
|
首先,将实体类和 Elasticsearch 之间建立联系:
|
|||
|
|
|||
|
```java
|
|||
|
@Document(indexName = "discusspost", type = "_doc", shards = 6, replicas = 3)
|
|||
|
public class DiscussPost {
|
|||
|
```
|
|||
|
|
|||
|
type 类型在 elasticsearch 7 版本中已完全抛弃,6 也不太建议使用,所以我们就固定为 _doc,不用去管它
|
|||
|
|
|||
|
```java
|
|||
|
@Document(indexName = "discusspost", type = "_doc", shards = 6, replicas = 3)
|
|||
|
public class DiscussPost {
|
|||
|
|
|||
|
@Id
|
|||
|
private int id;
|
|||
|
|
|||
|
@Field(type = FieldType.Integer)
|
|||
|
private int userId;
|
|||
|
|
|||
|
@Field(type = FieldType.Text, analyzer = "ik_max_word", searchAnalyzer = "ik_smart")
|
|||
|
private String title;
|
|||
|
|
|||
|
@Field(type = FieldType.Text, analyzer = "ik_max_word", searchAnalyzer = "ik_smart")
|
|||
|
private String content;
|
|||
|
|
|||
|
@Field(type = FieldType.Integer)
|
|||
|
private int type;
|
|||
|
|
|||
|
@Field(type = FieldType.Integer)
|
|||
|
private int status;
|
|||
|
|
|||
|
@Field(type = FieldType.Date)
|
|||
|
private Date createTime;
|
|||
|
|
|||
|
@Field(type = FieldType.Integer)
|
|||
|
private int commentCount;
|
|||
|
|
|||
|
@Field(type = FieldType.Double)
|
|||
|
private double score;
|
|||
|
```
|
|||
|
|
|||
|
测试增删改查:
|
|||
|
|
|||
|
```java
|
|||
|
@RunWith(SpringRunner.class)
|
|||
|
@ContextConfiguration(classes = CommunityApplication.class)
|
|||
|
@SpringBootTest
|
|||
|
public class ElasticsearchTests {
|
|||
|
|
|||
|
@Autowired
|
|||
|
private DiscussPostMapper discussPostMapper;
|
|||
|
|
|||
|
@Autowired
|
|||
|
private DiscussPostRepository discussPostRepository;
|
|||
|
|
|||
|
@Autowired
|
|||
|
private ElasticsearchTemplate elasticsearchTemplate;
|
|||
|
|
|||
|
/**
|
|||
|
* 测试插入数据
|
|||
|
*/
|
|||
|
@Test
|
|||
|
public void testInsert() {
|
|||
|
discussPostRepository.save(discussPostMapper.selectDiscussPostById(241));
|
|||
|
discussPostRepository.save(discussPostMapper.selectDiscussPostById(242));
|
|||
|
discussPostRepository.save(discussPostMapper.selectDiscussPostById(243));
|
|||
|
}
|
|||
|
|
|||
|
/**
|
|||
|
* 测试批量插入数据
|
|||
|
*/
|
|||
|
@Test
|
|||
|
public void testInsetList() {
|
|||
|
discussPostRepository.saveAll(discussPostMapper.selectDiscussPosts(101, 0, 100));
|
|||
|
discussPostRepository.saveAll(discussPostMapper.selectDiscussPosts(102, 0, 100));
|
|||
|
discussPostRepository.saveAll(discussPostMapper.selectDiscussPosts(103, 0, 100));
|
|||
|
discussPostRepository.saveAll(discussPostMapper.selectDiscussPosts(111, 0, 100));
|
|||
|
discussPostRepository.saveAll(discussPostMapper.selectDiscussPosts(112, 0, 100));
|
|||
|
discussPostRepository.saveAll(discussPostMapper.selectDiscussPosts(131, 0, 100));
|
|||
|
discussPostRepository.saveAll(discussPostMapper.selectDiscussPosts(132, 0, 100));
|
|||
|
discussPostRepository.saveAll(discussPostMapper.selectDiscussPosts(133, 0, 100));
|
|||
|
discussPostRepository.saveAll(discussPostMapper.selectDiscussPosts(134, 0, 100));
|
|||
|
}
|
|||
|
|
|||
|
/**
|
|||
|
* 测试修改数据
|
|||
|
*/
|
|||
|
@Test
|
|||
|
public void testUpdate() {
|
|||
|
DiscussPost discussPost = discussPostMapper.selectDiscussPostById(231);
|
|||
|
discussPost.setContent("Great Elasticsearch");
|
|||
|
discussPostRepository.save(discussPost);
|
|||
|
}
|
|||
|
|
|||
|
/**
|
|||
|
* 测试删除数据(注意数据库中的数据并未被删除)
|
|||
|
*/
|
|||
|
@Test
|
|||
|
public void testDelete() {
|
|||
|
discussPostRepository.deleteById(231);
|
|||
|
// discussPostRepository.deleteAll();
|
|||
|
}
|
|||
|
|
|||
|
/**
|
|||
|
* 测试使用 ElasticsearchRepository 进行搜索
|
|||
|
*/
|
|||
|
@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();
|
|||
|
|
|||
|
// elasticsearchTemplate.queryForPage(searchQuery, class, SearchResultMapper);
|
|||
|
// 底层获取到了高亮显示的值,但是没有做处理(所以想要更加完善的话需要使用 ElasticsearchTemplate)
|
|||
|
|
|||
|
Page<DiscussPost> page = discussPostRepository.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);
|
|||
|
}
|
|||
|
}
|
|||
|
|
|||
|
/**
|
|||
|
* 测试使用 ElasticsearchTemplate 进行搜索
|
|||
|
*/
|
|||
|
@Test
|
|||
|
public void testSearchTemplate() {
|
|||
|
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();
|
|||
|
|
|||
|
Page<DiscussPost> page = elasticsearchTemplate.queryForPage(searchQuery, DiscussPost.class, new SearchResultMapper() {
|
|||
|
@Override
|
|||
|
public <T> AggregatedPage<T> mapResults(SearchResponse searchResponse, Class<T> aClass, Pageable pageable) {
|
|||
|
SearchHits hits = searchResponse.getHits();
|
|||
|
if (hits.getTotalHits() <= 0) {
|
|||
|
return null;
|
|||
|
}
|
|||
|
|
|||
|
List<DiscussPost> list = new ArrayList<>();
|
|||
|
|
|||
|
for (SearchHit hit : hits) {
|
|||
|
DiscussPost post = new DiscussPost();
|
|||
|
|
|||
|
String id = hit.getSourceAsMap().get("id").toString();
|
|||
|
post.setId(Integer.valueOf(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) {
|
|||
|
post.setTitle(titleField.getFragments()[0].toString());
|
|||
|
}
|
|||
|
|
|||
|
HighlightField contentField = hit.getHighlightFields().get("content");
|
|||
|
if (contentField != null) {
|
|||
|
post.setContent(contentField.getFragments()[0].toString());
|
|||
|
}
|
|||
|
|
|||
|
list.add(post);
|
|||
|
}
|
|||
|
|
|||
|
return new AggregatedPageImpl(list, pageable,
|
|||
|
hits.getTotalHits(), searchResponse.getAggregations(), searchResponse.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);
|
|||
|
}
|
|||
|
}
|
|||
|
|
|||
|
}
|
|||
|
```
|
|||
|
|
|||
|
🚨 注意这里面的 Page 是 Spring 提供的,而非我们自己开发的那个,它的 current 当前页码是从 0 开始计数的,而我们自己开发的那个 Page 是从 1 开始的,所以后续编码的时候记得稍微处理一下。
|
|||
|
|
|||
|
## 开发社区搜索功能
|
|||
|
|
|||
|
<img src="https://gitee.com/veal98/images/raw/master/img/20210129153411.png" style="zoom: 33%;" />
|
|||
|
|
|||
|
使用消息队列异步地(提高性能)将帖子提交到 Elasticsearch 提交到服务器
|
|||
|
|
|||
|
### Service
|
|||
|
|
|||
|
```java
|
|||
|
/**
|
|||
|
* 搜索相关
|
|||
|
*/
|
|||
|
@Service
|
|||
|
public class ElasticsearchService {
|
|||
|
|
|||
|
@Autowired
|
|||
|
private DiscussPostRepository discussPostRepository;
|
|||
|
|
|||
|
@Autowired
|
|||
|
private ElasticsearchTemplate elasticsearchTemplate;
|
|||
|
|
|||
|
/**
|
|||
|
* 将数据插入 Elasticsearch 服务器
|
|||
|
* @param post
|
|||
|
*/
|
|||
|
public void saveDiscusspost(DiscussPost post) {
|
|||
|
discussPostRepository.save(post);
|
|||
|
}
|
|||
|
|
|||
|
/**
|
|||
|
* 将数据从 Elasticsearch 服务器中删除
|
|||
|
* @param id
|
|||
|
*/
|
|||
|
public void deleteDiscusspost(int id) {
|
|||
|
discussPostRepository.deleteById(id);
|
|||
|
}
|
|||
|
|
|||
|
/**
|
|||
|
* 搜索
|
|||
|
* @param keyword 搜索的关键词
|
|||
|
* @param current 当前页码(这里的 Page 是 Spring 提供的,而非我们自己实现的那个)
|
|||
|
* @param limit 每页显示多少条数据
|
|||
|
* @return
|
|||
|
*/
|
|||
|
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();
|
|||
|
|
|||
|
return elasticsearchTemplate.queryForPage(searchQuery, DiscussPost.class, new SearchResultMapper() {
|
|||
|
@Override
|
|||
|
public <T> AggregatedPage<T> mapResults(SearchResponse searchResponse, Class<T> aClass, Pageable pageable) {
|
|||
|
// 获取命中的数据
|
|||
|
SearchHits hits = searchResponse.getHits();
|
|||
|
if (hits.getTotalHits() <= 0) {
|
|||
|
return null;
|
|||
|
}
|
|||
|
|
|||
|
// 处理命中的数据
|
|||
|
List<DiscussPost> list = new ArrayList<>();
|
|||
|
for (SearchHit hit : hits) {
|
|||
|
DiscussPost post = new DiscussPost();
|
|||
|
|
|||
|
String id = hit.getSourceAsMap().get("id").toString();
|
|||
|
post.setId(Integer.valueOf(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) {
|
|||
|
post.setTitle(titleField.getFragments()[0].toString());
|
|||
|
}
|
|||
|
|
|||
|
HighlightField contentField = hit.getHighlightFields().get("content");
|
|||
|
if (contentField != null) {
|
|||
|
post.setContent(contentField.getFragments()[0].toString());
|
|||
|
}
|
|||
|
|
|||
|
list.add(post);
|
|||
|
}
|
|||
|
|
|||
|
return new AggregatedPageImpl(list, pageable,
|
|||
|
hits.getTotalHits(), searchResponse.getAggregations(), searchResponse.getScrollId(), hits.getMaxScore());
|
|||
|
}
|
|||
|
});
|
|||
|
|
|||
|
}
|
|||
|
|
|||
|
}
|
|||
|
```
|
|||
|
|
|||
|
### Controller
|
|||
|
|
|||
|
- DiscusspostController
|
|||
|
|
|||
|
发帖成功后,通过消息队列将该帖子存入 Elasticsearch 服务器
|
|||
|
|
|||
|
<img src="https://gitee.com/veal98/images/raw/master/img/20210129155856.png" style="zoom: 50%;" />
|
|||
|
|
|||
|
- CommentController
|
|||
|
|
|||
|
对帖子添加评论成功后,通过消息队列将该帖子存入 Elasticsearch 服务器(不懂为啥要这样做,又不用查询评论)
|
|||
|
|
|||
|
<img src="https://gitee.com/veal98/images/raw/master/img/20210129160046.png" style="zoom:50%;" />
|
|||
|
|
|||
|
- 在消息队列消费者中添加一个消费发帖事件的方法
|
|||
|
|
|||
|
```java
|
|||
|
/**
|
|||
|
* 消费发帖事件
|
|||
|
*/
|
|||
|
@KafkaListener(topics = {TOPIC_PUBLISH})
|
|||
|
public void handlePublishMessage(ConsumerRecord record) {
|
|||
|
if (record == null || record.value() == null) {
|
|||
|
logger.error("消息的内容为空");
|
|||
|
return ;
|
|||
|
}
|
|||
|
Event event = JSONObject.parseObject(record.value().toString(), Event.class);
|
|||
|
if (event == null) {
|
|||
|
logger.error("消息格式错误");
|
|||
|
return ;
|
|||
|
}
|
|||
|
|
|||
|
DiscussPost post = discussPostSerivce.findDiscussPostById(event.getEntityId());
|
|||
|
elasticsearchService.saveDiscusspost(post);
|
|||
|
|
|||
|
}
|
|||
|
```
|
|||
|
|
|||
|
- **SearchController**
|
|||
|
|
|||
|
```java
|
|||
|
/**
|
|||
|
* 搜索
|
|||
|
*/
|
|||
|
@Controller
|
|||
|
public class SearchController implements CommunityConstant {
|
|||
|
|
|||
|
@Autowired
|
|||
|
private ElasticsearchService elasticsearchService;
|
|||
|
|
|||
|
@Autowired
|
|||
|
private UserService userService;
|
|||
|
|
|||
|
@Autowired
|
|||
|
private DiscussPostSerivce discussPostSerivce;
|
|||
|
|
|||
|
@Autowired
|
|||
|
private LikeService likeService;
|
|||
|
|
|||
|
// search?keword=xxx
|
|||
|
@GetMapping("/search")
|
|||
|
public String search(String keyword, Page page, Model model) {
|
|||
|
// 搜索帖子 (Spring 提供的 Page 当前页码从 0 开始计数)
|
|||
|
org.springframework.data.domain.Page<DiscussPost> searchResult =
|
|||
|
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";
|
|||
|
}
|
|||
|
|
|||
|
}
|
|||
|
```
|
|||
|
|
|||
|
### 前端
|
|||
|
|
|||
|
`index.html` 搜索框
|
|||
|
|
|||
|
```html
|
|||
|
<form method="get" th:action="@{/search}">
|
|||
|
<input name="keyword" th:value="${keyword}" />
|
|||
|
<button type="submit"> 搜索</button>
|
|||
|
</form>
|
|||
|
```
|
|||
|
|
|||
|
name = "keyword" 和 Controller 中参数的名称要一致
|
|||
|
|
|||
|
`search.html` 搜索详情页
|
|||
|
|
|||
|
```html
|
|||
|
<li th:each="map:${discussPosts}">
|
|||
|
<img th:src="${map.user.headerUrl}" alt="用户头像">
|
|||
|
<a th:href="@{|/discuss/detail/${map.post.id}|}" th:utext="${map.post.title}"></a>
|
|||
|
<div th:utext="${map.post.content}"></div>
|
|||
|
<u th:utext="${map.user.username}"></u>
|
|||
|
发布于 <b th:text="${#dates.format(map.post.createTime, 'yyyy-MM-dd HH:mm:ss')}"></b>
|
|||
|
<ul>
|
|||
|
<li>赞 <i th:text="${map.likeCount}"></i></li>
|
|||
|
<li>|</li>
|
|||
|
<li>回复 <i th:text="${map.post.commentCount}"></i></li>
|
|||
|
</ul>
|
|||
|
</li>
|
|||
|
```
|