elasticsearch
安装好elasticsearch之后,我们可以通过postman访问9200端口查看一些信息,下面给出一些基本的命令
// 查看节点状态
localhost:9200/_cat/indices?v
// 向test索引中加入id为3的数据,在body中设置json格式数据
localhost:9200/test/_doc/3
// 删除test索引中id为1的数据
localhost:9200/test/_doc/1
// 在body中实现多条件查询
{
"query":{
"multi_match":{
"query":"互联网",
"fields":["title","content"]
}
}
}
配置
首先我们在pom.xml中引入相关依赖,但是每次引入都会报错,经过多次查询资料,发现是SSL证书的问题,具体可以参考博客
<!-- https://mvnrepository.com/artifact/org.springframework.boot/spring-boot-starter-data-elasticsearch -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-elasticsearch</artifactId>
</dependency>
然后我们需要在application.properties中配置节点名称和路径,在SpringBoot中我们使用的是9300端口
# ElasticSearchProperties
spring.data.elasticsearch.cluster-name=nowcoder
spring.data.elasticsearch.cluster-nodes=127.0.0.1:9300
这里要注意,elasticsearch和redis的网络部分都是基于netty实现的,因此我们要做一些配置避免发送冲突,具体就是在App启动的地方做一个配置(具体需要百度下。。。)
// 在构造器之前
@PostConstruct
public void init(){
// 解决netty启动冲突问题
// Netty4Utils.setAvailableProcessors()
System.setProperty("es.set.netty.runtime.available.processors","false");
}
entity
在Springboot中我们使用elasticsearch,实现要对能搜索的实体进行配置,使用@Document将实体和索引(也就是数据库中的表)相联系,使用@Id设置索引中哪个字段为id,@Field来设置索引中的字段。
@Document中indexName表示索引名字,type表示分隔符,shards表示分片,replicas表示副本数量。
@Field中type表示数据类型,analyzer表示解析器,ik_max_word表示将一句话拆分成尽可能多的词语,ik_smart表示符合尝试的解析器
@Document(indexName = "discusspost",type = "_doc",shards = 6, replicas = 3)
public class DiscussPost {
@Id
private int id;
@Field(type = FieldType.Integer)
private int userId;
// analyzer 解析器 对一句话尽可能拆分成多个词条 聪明的拆分。。
@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;
dao
有了对实体中字段的注解,在dao层我们只要继承ElasticsearchRepository,并指明实体类型和ID类型即可
@Repository
public interface DiscussPostRepository extends ElasticsearchRepository<DiscussPost, Integer> {
}
service
在service层,我们主要实现三个方法,分别是保存数据、删除数据和查询数据,其中保存和删除我们直接使用dao层的Repository即可,调用save和deleteById
public void saveDiscussPost(DiscussPost post){
discussPostRepository.save(post);
}
public void deleteDiscussPost(int id){
discussPostRepository.deleteById(id);
}
而搜索相对复杂一点。首先,我们要传入关键字、当前页和限制条数来构造搜索条件。
SearchQuery searchQuery = new NativeSearchQueryBuilder()
// 构造多字段查询
.withQuery(QueryBuilders.multiMatchQuery(keyword,"title","content"))
// 设置排序字段为type,score,createTime 倒序
.withSort(SortBuilders.fieldSort("type").order(SortOrder.DESC))
.withSort(SortBuilders.fieldSort("score").order(SortOrder.DESC))
.withSort(SortBuilders.fieldSort("createTime").order(SortOrder.DESC))
// 设置分页 page表示当前页
.withPageable(PageRequest.of(current,limit))
// 设置高亮的前标签和后标签
.withHighlightFields(
new HighlightBuilder.Field("title").preTags("<em>").postTags("</em>"),
new HighlightBuilder.Field("content").preTags("<em>").postTags("</em>")
).build();
有了搜索条件之后,我们就需要使用elasticsearchTemplate进行搜索了,通过使用queryForPage方法,queryForPage方法的返回类型是org.springframework.data.domain.Page<DiscussPost>,因此我们需要传入实体类.class和搜索条件,然后匿名实现一个SearchResultMapper()方法,用于对elasticsearchTemplate得到的结果进行相应的处理,然后再返回。
在匿名类中我们使用SearchResponse来接收elasticsearchTemplate返回的结果,在该方法中我们首先得到全部数据,如果数据不为空的话,我们遍历所有数据,对于每一条数据通过hit.getSourceAsMap().get(“xxx”)得到信息实例化一个post对象,然后得到高亮数据,通过hit.getHighlightFields().get(“title”)得到title的,content的也同理,然后如果高亮数据不为空,那么我们将添加了em标签的数据设置为post的title和content。最后我们使用一个AggregatedPageImpl的构造方法传入list和其他固定参数对结果进行返回。
// elasticsearchTemplate得到结果交给SearchResultMapper处理之后再返回
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();
// 得到每个被遍历数据的信息,然后设置post
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));
// 处理高亮结果
// 得到title字段的高亮数据
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.setTitle(contentField.getFragments()[0].toString());
}
list.add(post);
}
// 对返回数据进行初始化,传入list,其他都是固定的(目前)
return new AggregatedPageImpl(list,pageable,
hits.getTotalHits(), searchResponse.getAggregations(),
searchResponse.getScrollId(), hits.getMaxScore());
}
});
controller
在controller层我们首先处理发布帖子和修改帖子之后将帖子加入到elasticsearch服务器中的处理,这里我们使用消息队列来进行实现,同样的对于一个事件我们需要指定主题,触发事件的人和实体类型、实体id,然后使用eventProducer.fireEvent(event)发布事件。具体我们要在发布帖子和用户对帖子进行评论的时候加入事件触发。
// 触发发帖事件
Event event = new Event()
.setTopic(TOPIC_PUBLISH)
.setUserId(user.getId())
.setEntityType(ENTITY_TYPE_POST)
.setEntityId(discussPost.getId());
eventProducer.fireEvent(event);
if (comment.getEntityType() == ENTITY_TYPE_POST) {
// 触发发帖事件
event = new Event()
.setTopic(TOPIC_PUBLISH)
.setUserId(hostHolder.getUser().getId())
.setEntityType(ENTITY_TYPE_POST)
.setEntityId(discussPostId);
eventProducer.fireEvent(event);
}
同时,对于该类事件,我们需要加入一个新的消费者进行消费。
同样地通过设置主题,判断消息内容和格式,然后调用dao加入数据即可。
// 消费发帖事件
@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 = discussPostService.findDiscussPostByid(event.getEntityId());
elasticsearchService.saveDiscussPost(post);
}
最后就是对搜索结果进行处理的controller了
首先通过service得到搜索结果,然后同之间一样,根据帖子的作者id得到作者user实例,根据帖子id查询点赞数量,然后聚合到map中,再将map传入list中,最后将list传给model
@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)
public String search(String keyword, Page page, Model model){
// 搜索帖子
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";
}
}