Elasticsearch,分布式搜索引擎
一:Elasticsearch简介
Elasticsearch是目前性能最好、最流行的搜索引擎,像FaceBook、百度、维基百科、GitHub,很多知名的大厂都在使用Elasticsearch搜索引擎。
- 一个分布式的、Restful风格的搜索引擎。
- 支持对各种类型的数据的检索。
- 搜索速度快,可以提供实时的搜索服务。
- 便于水平扩展,每秒可以处理PB级海量数据。
所谓搜索引擎,其实会有以下两个步骤:
1、提交数据:我们首先是要把数据先存一份到搜索引擎中,按照这个角度,我们其实可以把搜索引擎看成是一个特殊的数据库。
2、分词、建立索引:在存数据时,搜索引擎会进行分词,建立索引,从而提供搜索功能。
但是有些搜索引擎在提交数据阶段就会很慢,有的搜索引擎在分词、建立索引阶段会很慢,从而使他们并不能做到提供实时的搜索服务
而Elasticsearch可以,这足以体现出Elasticsearch的强大之处
二:Elasticsearch术语
既然我们可以把搜索引擎看成是一个特殊的数据库
那么我们就可以类比数据库中的一些术语。
- 索引、类型、文档、字段。
1、索引—> 与数据库中的database数据库相对应;一个索引就相当于一个库
2、类型—> 与数据库中的table表相对应;一个类型就相当于一张表
3、文档—> 相当于table表中的一条数据。JSON结构
4、字段—> 就相当于table中的一个字段,一列数据
注意: 在6.0版本之后,类型的概念逐渐的被废弃,之后索引就代表的是一张表,文档代表的还是一条数据,字段还是相当于一个字段。
- 集群、节点、分片、副本。
1、分片—> 对索引的进一步划分,一个索引在进行存储的时候可以划分成多个分片进行存储,大大提高并发能力。
2、副本—> 对各个分片进行备份数据,大大提高可用性。
三:SpringBoot整合Elasticsearch
由于SpringBoot中的父pom.xml文件中适配的Elasticsearch版本为6.4.3
所以建议采用6.4.3版本,而不要使用最新的7.0版本,因为从6.0版本到7.0版本变化还是很大的,会造成兼容性问题。
1、导入依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-elasticsearch</artifactId>
</dependency>
2、配置Elasticsearch
# ElasticsearchProperties
spring.data.elasticsearch.cluster-name=nowcoder #自己随意起的名字
spring.data.elasticsearch.cluster-nodes=127.0.0.1:9300
3、解决冲突
因为Elasticsearch和Redis的底层都是基于Netty的,所以他们两个在同时使用时就会产生冲突。
详情见源码:
在启动类上设置System.setProperty(“es.set.netty.runtime.available.processors”, “false”);
@PostConstruct
public void init() {
// 解决netty启动冲突问题
// see Netty4Utils.setAvailableProcessors()
System.setProperty("es.set.netty.runtime.available.processors", "false");
}
4、向Elasticsearch提交数据
我们只需要在要搜索数据的数据表的实体类上加上一些说明的注解,让他能够进行对应的存储。
例如:查询讨论区的贴子
shards —>分片数量
replicas —>副本数量
analyzer —> 存储时的解析器。
例如:我们要往Elasticsearch存入"互联网校招",那么采用ik_max_word这个解析器那么在存数据时,就会尽可能多的对数据进行分词,建立尽可能多的词条以便于之后的搜索服务,可能"互联网校招"就会被拆分成"互联"、“联网”、“互联网”、“网校”、"校招"这么多分词。
ik代表是中文的分词器。
searchAnalyzer—> 搜索时的解析器。
例如:我们在搜索"互联网校招"时,我们没有必要再把它拆分出很多的词条,我们应该希望Elasticsearch能够智能的理解它的意思,进行智能的分词,那么可能只会拆分出"互联网"、"校招"这两个词条,这样就会便于查找相应的内容,所以采用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;
@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;
5、创建Elasticsearch的API接口
1、ElasticsearchTemplate
需要这个接口来补充ElasticsearchRepository接口不能进行的操作。
2、ElasticsearchRepository
这个接口操作起来比较简单。
@Repository
public interface DiscussPostRepository extends ElasticsearchRepository<DiscussPost, Integer> {
}
6、CURD、搜索操作测试代码
@RunWith(SpringRunner.class)
@SpringBootTest
@ContextConfiguration(classes = CommunityApplication.class)
public class ElasticsearchTests {
@Autowired
private DiscussPostMapper discussMapper;
@Autowired
private DiscussPostRepository discussRepository;
@Autowired
private ElasticsearchTemplate elasticTemplate;
@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();
// ElasticsearchRepository底层使用的方法,高亮的样式没有返回,这是一个缺点,我们可以采用ElasticsearchTemplate来解决
// 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);
}
}
}