Elasticsearch入门
1. Elasticsearch简介
性能最好的搜索引擎
- 一个分布式的、Restful风格(请求标准的描述)的搜索引擎。
- 支持对各种类型的数据的检索。
- 搜索速度快,可以提供实时的搜索服务。
- 便于水平扩展,每秒可以处理PB级海量数据。
2. Elasticsearch术语
- 索引(对应数据库,最新版本对应一张表)、类型(对应表,最新版本被废弃)、文档(表里一行,通常采用json)、字段(一列)
- 集群(服务器组合在一起)、节点(集群中每台服务器)、分片(对索引的划分,提高并发能力)、副本(分片的备份,提高可用性)。
通过ES搜索的数据必须要在ES中转存一份,某种角度来说它是一个数据库。
3. Elasticsearch使用
下载版本6.4.3
- 安装、修改配置文件
- elasticsearch.yml文件,修改cluster.name,path.data,path.logs
- 配置环境变量
- 安装中文分词插件(ES仅支持中文分词):https://github.com/medcl/elasticsearch-analysis-ik
- ik插件安装到plugins文件夹下
- 安装postman(提交html数据给ES)模拟web客户端
- 启动ES:打开bin/elasticsearch.bat
- 查看集群健康状态:curl -X GET “localhost:9200/_cat/health?v”
- 查看节点:curl -X GET “localhost:9200/_cat/nodes?v”
- 查看索引:curl -X GET “localhost:9200/_cat/indices?v”
- 创建索引:curl -X PUT “localhost:9200/test”
- 删除索引:curl -X DELETE “localhost:9200/test”
- 使用postman查询
- 提交数据,PUT localhost:9200/test/_doc/1选择Body,raw,JSON
- Test: 索引 _doc:占位 1:id
- 搜索,GET localhost:9200/test/_search?q=title(/content):xxx(搜索title/content中包含xxx的)
- 搜索时ES对关键词进行了分词
- 通过请求体构造复杂搜索条件
- 提交数据,PUT localhost:9200/test/_doc/1选择Body,raw,JSON
Spring整合Elasticsearch
1. 引入依赖
- spring-boot-starter-data-elasticsearch
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-elasticsearch</artifactId>
</dependency>
2. 配置Elasticsearch
- cluster-name、cluster-nodes(集群的名字,节点)
- Redis和Es底层都用到了Netty,有启动冲突。解决:在CommunityApplication类加入初始化方法进行配置。
# ElasticsearchProperties
# 配置集群名字
spring.data.elasticsearch.cluster-name=nowcoder
# 9200是http默认访问的端口 9300是tcp访问的端口
# 配置节点
spring.data.elasticsearch.cluster-nodes=127.0.0.1:9300
还需解决冲突
es底层和redis都基于Netty,两者在启用Netty时有冲突
// 管理bean的生命周期,会在构造器被调用之后执行,通常是初始化方法
@PostConstruct
public void init() {
// 解决netty启动冲突问题
// see Netty4Utils.setAvailableProcessors()
System.setProperty("es.set.netty.runtime.available.processors", "false");
}
- Spring Data Elasticsearch(调用API)
- ElasticsearchTemplate(集成了Es的CRUD方法)
- ElasticsearchRepository(接口,底层为ElasticsearchTemplate,用起来更方便)
3. 需求
把数据库存的帖子传入服务器里,用服务器搜索帖子
4. 代码实现
- 把实体类和es之间建立关联
// 自动将实体数据与es映射
@Document(indexName = "discusspost", type = "_doc", shards = 6, replicas = 3)
public class DiscussPost {
@Id
private int id;
@Field(type = FieldType.Integer)
private int userId;
// analyze和searchAnalyzer是两个不同的分词器
// 存储数据时采用analyzer,拆分出更多的词,增加搜索范围;搜索时使用searchAnalyzer,方便搜索
@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;
}
- 定义repository接口
@Repository
public interface DiscussPostRepository extends ElasticsearchRepository<DiscussPost, Integer> {
}
- 创建测试类
- 增删改查
@RunWith(SpringRunner.class)
@SpringBootTest
// 在测试代码中启用CommunityApplication作为测试类
@ContextConfiguration(classes = CommunityApplication.class)
public class ElasticsearchTests {
@Autowired
private DiscussPostMapper discussMapper;
@Autowired
private DiscussPostRepository discussRepository;
@Autowired
private ElasticsearchTemplate elasticsearchTemplate;
// 插入一条数据
@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<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);
}
}
- 使用template方法进行搜索 进行高亮处理
@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<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();
Page<DiscussPost> page = elasticTemplate.queryForPage(searchQuery, DiscussPost.class, new SearchResultMapper() {
@Override
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();
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(), 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);
}
}
开发社区搜索功能
1. 搜索服务
- 将帖子保存至Elasticsearch服务器。
- 对贴子实体类DiscussPost用注解进行相关配置
// 自动将实体数据与es映射
@Document(indexName = "discusspost", type = "_doc", shards = 6, replicas = 3)
public class DiscussPost {
@Id
private int id;
@Field(type = FieldType.Integer)
private int userId;
// analyze和searchAnalyzer是两个不同的分词器
// 存储数据时采用analyzer,拆分出更多的词,增加搜索范围;搜索时使用searchAnalyzer,方便搜索
@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;
}
- 从Mybatis取数据存入
- 在dao层创建DiscussPostRepository类,继承ElasticsearchRepository接口即可,它集成了CRUD方法
- 从Elasticsearch服务器删除帖子。
- 从Elasticsearch服务器搜索帖子。
- Es可以在搜索到的词加标签,达到高亮显示
- 利用elasticTemplate.queryForPage()查询
同步:有多个请求需要被处理,需要一个请求接着一个请求被处理,当前请求不处理完成,下一个请求不能开始。
异步:有多个请求需要被处理,处理请求a,就接收这个请求a并返回一个响应,并开始执行下一个请求b,当前请求a只返回了一个响应,因为结果还需要处理,等结果处理完成后就可以返回结果给对应的请求a。
2. 发布事件
- 发布帖子时,将帖子异步的提交到Elasticsearch服务器。
- 新建ElasticsearchService类,定义CRUD和搜索方法。
@Service
public class ElasticsearchService {
@Autowired
private DiscussPostRepository discussRepository;
@Autowired
private ElasticsearchTemplate elasticTemplate;
/**
* 向es服务器提交新的帖子
*/
public void saveDiscussPost(DiscussPost post) {
discussRepository.save(post);
}
/**
* 删除帖子
*/
public void deleteDiscussPost(int id) {
discussRepository.deleteById(id);
}
/**
* 分页:current是当前页
*/
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(
// global标签中对<em>做了定义,<em>显示红色
new HighlightBuilder.Field("title").preTags("<em>").postTags("</em>"),
new HighlightBuilder.Field("content").preTags("<em>").postTags("</em>")
).build();
return elasticTemplate.queryForPage(searchQuery, DiscussPost.class, new SearchResultMapper() {
@Override
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();
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(), response.getAggregations(), response.getScrollId(), hits.getMaxScore());
}
});
}
}
- 在DiscussPostController类发帖时,定义和触发发帖事件(Event、eventProducer.fireEvent(event))
- 增加评论时,将帖子异步的提交到Elasticsearch服务器。
- 在CommentController类发表评论时,定义和触发发帖事件(需要先定义发帖事件常量)
// 触发发帖事件,把新发布的帖子存到es服务器
Event event = new Event()
.setTopic(TOPIC_PUBLISH)
.setUserId(user.getId())
.setEntityType(ENTITY_TYPE_POST)
.setEntityId(post.getId());
// 触发发帖事件
eventProducer.fireEvent(event);
- 在消费组件中增加一个方法,消费帖子发布事件。 - 在EventConsumer类增加消费发帖事件的方法
- 在事件中查询帖子,存到Es服务器
- 在CommentController类发表评论时,定义和触发发帖事件(需要先定义发帖事件常量)
/**
* 消费发帖事件
*/
@KafkaListener(topics = {TOPIC_PUBLISH})
public void handlePublishMessage(ConsumerRecord record) {
if (record == null || record.value() == null) {
logger.error("消息的内容为空!");
}
Event event = JSONObject.parseObject(record.value().toString(), Event.class);
if (event == null) {
logger.error("消息格式错误!");
return;
}
// 查询帖子,存进elasticsearch里
DiscussPost post = discussPostService.findDiscussPostById(event.getEntityId());
elasticsearchService.saveDiscussPost(post);
}
3. 显示结果
- 在控制器中处理搜索请求,在HTML上显示搜索结果。
- 新建SearchController类处理搜索请求
- 此时为GET请求,keyword的传入(search?keyword=xxx)
- 修改index.html,表单提交路径,文本框name=“keyword”
- 在search.html修改,遍历取到帖子。
// /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";
}