目录
一、Elasticsearch入门
简介、术语
二、Elasticsearch下载及配置
ES下载和配置、中文分词插件ik下载、Postman下载
三、Elasticsearch启动与测试
命令行启动与测试、Postman访问ES服务器
四、Spring整合Elasticsearch
导包、配置、冲突解决、向ES服务器映射数据、ES的API接口、测试CRUD(搜索条件)
【Elasticsearch简介】
Elasticsearch,简称ES,是一个分布式、高扩展、高实时、以及 Restful 风格的搜索引擎。它能很方便的使大量数据具有搜索、分析和探索的能力,支持对各种类型的数据的检索,且搜索速度快,可以提供实时的搜索服务。并且充分利用Elasticsearch的水平伸缩性,能使数据在生产环境变得更有价值,每秒可以处理PB级海量数据。
Elasticsearch 的实现原理:主要分为以下几个步骤,首先用户将数据提交到 Elasticsearch 数据库中,再通过分词控制器去将对应的语句分词,将其权重和分词结果一并存入数据,当用户搜索数据时候,再根据权重将结果排名,打分,再将返回结果呈现给用户。
Elasticsearch 是目前性能最好、最流行的一个搜索引擎,像大名鼎鼎的FaceBook、GitHub、 维基百科、百度等等都使用Elasticsearch搜索引擎完成搜索功能。
【Elasticsearch术语】
注: 在6.0版本之后,类型的概念逐渐的被废弃,之后索引就代表的是一张表,文档还是相当于一条数据,字段还是相当于一列数据。
【下载Elasticsearch】
由于我使用的是2.1.5.RELEASE版本的SpringBoot中的父pom.xml文件中适配的Elasticsearch版本为6.4.3,所以我们下载Elasticsearch 6.4.3版本,下载地址:Elasticsearch 6.4.3 | Elastic
注:父 pom.xml 文件需要使用 2.1.5.RELEASE 版本才能够适应后面Spring整合ES时步骤。
根据系统点击链接即可下载。之后解压到某一文件夹即可.(配置环境变量略)
【配置Elasticsearch】
配置 /config/elasticsearch.yml 文件:
以上名字和路径可以根据自己的需求更改,这里仅作参考。大家在更改配置的时候不要加注释,我这里加注释只是为的方便大家观看,否则容易出问题。若出问题参考:elasticsearch常见问题总结
【下载中文分词插件ik】
ES默认会对英文进行分词,若想要对中文进行分词,还要下载中文分词的插件,下载地址:
可以进入Github官网中按照以下步骤操作进行下载,也可以直接点击此链接下载安装包
下载好的压缩包需要解压到固定的目录下:即刚才安装的elasticsearch目录下的plugins目录下的ik目录下
解压后的目录如下:
config目录下的文件都是对关键词的配置
如果有想要添加的网络新词,可以在 /config/IKAnalyzer.cfg.xml 文件中进行配置:即在config目录下新建 .dic 文件,然后配置到 /config/IKAnalyzer.cfg.xml 文件中即可
【下载postman】
postman(可选) 能够模拟web客户端,即能够模拟网页发送http请求,使用此插件可以增强我们入门体验感,也可以不使用或使用其他类似的插件
下载地址:Download Postman | Get Started for Free
下载完成后,双击开始自动安装,免费注册后即可使用
【启动Elasticsearch服务器】:可以通过命令行启动,也可直接双击启动
【测试】
配置过环境变量后可以直接在命令行中测试:
// 查看集群的健康状况
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"
// 创建名为test的索引
curl -X PUT "localhost:9200/test"
// 删除名为test的索引
curl -X DELETE "localhost:9200/test"
【使用Postman访问ES服务器】
如果使用 Postman 对ES服务器访问,则去掉命令前面的curl -X,方式选择对应方式,后面的命令不变即可。
向 test 索引(表)中添加一个文档(一行数据),id为1,格式为JSON字符串:
更新 test 索引中的数据,只需再重新添加一个相同id文档即可,底层默认会先删除、再建立一个新的文档
搜索 content 中指定词条的信息:
注意:在分词器的作用下,这里会把"运营实习"再分成"运营"和"实习"两个词条进行搜索
搜索多个字段匹配的复杂信息(既在title中搜索、也在content中搜索):
【版本问题】
以下配置和整合都依赖于 spring 的 2.1.5.RELEASE 版本,如果使用高版本,会有很多配置和属性无法使用的问题。
【在pom.xml中导入依赖】
org.springframework.boot
spring-boot-starter-data-elasticsearch
【在application.properties中配置】
如果spring不是2.1.5.RELEASE版本,则会报红
# ElasticsearchPropertices
spring.data.elasticsearch.cluster-name=nowcoder # 自定义集群名称
spring.data.elasticsearch.cluster-nodes=127.0.0.1:9300 # TCP协议的接口
【解决冲突】
这里有个坑:ES和redis的底层都依赖netty,如果我们同时使用就会产生冲突,为解决冲突我们可以在项目启动入口类中初始化 Netty4Utils 包中的一个开关。
解决办法: 在项目启动入口类中初始化 Netty4Utils 包中的key
@PostConstruct
public void init() {
// 解决netty启动冲突的问题
// 在Netty4Utils.setAvailableProcessors()
System.setProperty("es.set.netty.runtime.available.processors", "false");
}
【向Elasticsearch服务器映射数据】
spring整合的ES提供的一项技术:spring底层在访问ES服务器时会自动地将实体数据和ES服务器里面的索引相互映射,但我们需要在实体类中加上一些注解,以便告知其映射后的索引名称(indexName)、类型名称(type)以及分片个数(shards)、副本个数(replicas)等。
analyzer:存储时的解析器。
例如:我们要往Elasticsearch存入"互联网校招",那么采用ik_max_word这个解析器那么在存数据时,就会尽可能多的对数据进行分词,建立尽可能多的词条以便于之后的搜索服务,可能"互联网校招"就会被拆分成"互联"、“联网”、“互联网”、“网校”、"校招"这么多分词。(ik为中文分词插件)
searchAnalyzer:搜索时的解析器。
例如:我们在搜索"互联网校招"时,我们没有必要再把它拆分出很多的词条,我们应该希望Elasticsearch能够智能的理解它的意思,进行智能的分词,那么可能只会拆分出"互联网"、"校招"这两个词条,这样就会便于查找相应的内容,所以采用ik_smart分词器。
以DiscussPost(帖子)实体为例:
@Document(indexName = "discusspost", type = "_doc", shards = 6, replicas = 3)
public class DiscussPost {
@Id // 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;
}
【Elasticsearch的API接口】
ES有两个API接口:
在接口中,我们只需要继承 ElasticsearchRepository 类即可,在此类中已经事先定义好了增删改查的操作,同时,我们还需在泛型中表明实体类型和主键类型
@Repository
public interface DiscussPostRepository extends ElasticsearchRepository {
}
【测试CRUD操作数据】
操作数据前需要引入依赖:
// 从MySQL中取数据,以便转存至ES服务器
@Autowired
private DiscussPostMapper discussMapper;
// ElasticsearchRepository接口
@Autowired
private DiscussPostRepository discussRepository;
// ElasticsearchRepository接口解决不了的,使用ElasticsearchTemplate来解决
@Autowired
private ElasticsearchTemplate elasticTemplate;
一次插入一条数据 save( ):
@Test
public void testInsert() {
discussRepository.save(discussMapper.selectDiscussPostById(241));
discussRepository.save(discussMapper.selectDiscussPostById(242));
discussRepository.save(discussMapper.selectDiscussPostById(243));
}
一次插入多条数据 saveAll( ):
@Test
public void testInsertList() {
discussRepository.saveAll(discussMapper.selectDiscussPosts(101,0,100));
}
更新一条数据 save( ):
@Test
public void testUpdate() {
// 从Mysql中查询,更改后存入ES,即可覆盖原来的数据,达到更新的目的
DiscussPost post = discussMapper.selectDiscussPostById(231);
post.setContent("我是新人,使劲灌水.");
discussRepository.save(post);
}
删除一条 deleteById( ) 或多条 deleteAll( ) 数据:
@Test
public void testDelete() {
discussRepository.deleteById(231);
discussRepository.deleteAll();
}
通过 ElasticsearchRepository 接口搜索 search( ):
构造搜索条件需要构造一系列的内容,比如:关键词(withQuery)、排序方式(withSort)、分页方式(withPageable)、高亮显示(withHighlightFields)等等
高亮显示是通过对关键词加标签,然后在css中添加样式的机制来实现的,通过此接口无法获取高亮显示的数据。
@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("").postTags(""),
new HighlightBuilder.Field("content").preTags("").postTags("<.em>")
).build();
// elasticTemplate.queryForPage(SearchQuery, class,SearchResultMapper);
// elasticRepository.search() 方法底层调用了上一行的方法, 获取到了高亮显示的值, 但是没有返回
// ElasticsearchRepository接口的查询操作
Page 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);
}
}
通过 ElasticsearchTemplate 接口搜索 queryForPage( ):
elasticsearchRepository.search( ) 方法底层调用了 elasticsearchTemplate.queryForPage(SearchQuery对象, class,SearchResultMapper); 方法, 获取到了高亮显示的值, 但是没有返回,返回的仍是原始的没有高亮标签的数据。如果想要获取到带有高亮标签的数据,需要对两份数据进行整合,即在调用 elasticsearchRepository.search( ) 方法前重写 elasticsearchTemplate 的方法,这样会很麻烦,不如直接使用elasticsearchTemplate 的方法。
构造查询条件不变,使用elasticsearchTemplate 接口的查询操作
@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("").postTags(""),
new HighlightBuilder.Field("content").preTags("").postTags("")
).build();
// ElasticsearchTemplate接口的查询操作
Page page = elasticTemplate.queryForPage(searchQuery, DiscussPost.class, new SearchResultMapper() {
@Override
public AggregatedPage mapResults(SearchResponse searchResponse, Class aClass, Pageable pageable) {
// 获取本次搜索的数据
SearchHits hits = searchResponse.getHits();
// 如果未查询到数据, 直接返回
if(hits.getTotalHits() <= 0) {
return null;
}
// 将数据存入List集合中
List 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);
}
}