Elasticsearch(一个基于分布式的搜索引擎)

目录

一、Elasticsearch入门

        简介、术语

二、Elasticsearch下载及配置

        ES下载和配置、中文分词插件ik下载、Postman下载

三、Elasticsearch启动与测试

        命令行启动与测试、Postman访问ES服务器

四、Spring整合Elasticsearch

        导包、配置、冲突解决、向ES服务器映射数据、ES的API接口、测试CRUD(搜索条件)


一、Elasticsearch入门

【Elasticsearch简介】

Elasticsearch,简称ES,是一个分布式、高扩展、高实时、以及 Restful 风格的搜索引擎。它能很方便的使大量数据具有搜索、分析和探索的能力,支持对各种类型的数据的检索,且搜索速度快,可以提供实时的搜索服务。并且充分利用Elasticsearch的水平伸缩性,能使数据在生产环境变得更有价值,每秒可以处理PB级海量数据。

Elasticsearch 的实现原理:主要分为以下几个步骤,首先用户将数据提交到 Elasticsearch 数据库中,再通过分词控制器去将对应的语句分词,将其权重和分词结果一并存入数据,当用户搜索数据时候,再根据权重将结果排名,打分,再将返回结果呈现给用户。

Elasticsearch 是目前性能最好、最流行的一个搜索引擎,像大名鼎鼎的FaceBook、GitHub、 维基百科、百度等等都使用Elasticsearch搜索引擎完成搜索功能。

【Elasticsearch术语】

  • 索引:与数据库中的database数据库相对应。一个索引就相当于一个库
  • 类型:与数据库中的table表相对应。一个类型就相当于一张表
  • 文档: 相当于table表中的一条数据。通常采用JSON结构
  • 字段: 就相当于table中的一个字段。一列数据

注: 在6.0版本之后,类型的概念逐渐的被废弃,之后索引就代表的是一张表,文档还是相当于一条数据,字段还是相当于一列数据。

  • 集群:分布式部署中一台或多台ES服务器组合在一起就是一个集群
  • 节点:集群当中的每一个服务器称为一个结点
  • 分片:对索引(表)的进一步划分。一个索引划分成若干个分片,可以提高并发的能力
  • 副本:对分片数据的备份。一个分片可以包含多个副本,提高系统的容错性

二、Elasticsearch下载及配置

【下载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官网中按照以下步骤操作进行下载,也可以直接点击此链接下载安装包

进入界面下拉找到Install

找到与ES版本相对应的6.4.3版本下载即可

下载好的压缩包需要解压到固定的目录下:即刚才安装的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启动与测试

【启动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 为 1 的数据:

删除 test 索引之下,id 为 1 的数据:

更新 test 索引中的数据,只需再重新添加一个相同id文档即可,底层默认会先删除、再建立一个新的文档

搜索全部信息:

搜索 title 中指定词条的信息:

搜索  content 中指定词条的信息:

       注意:在分词器的作用下,这里会把"运营实习"再分成"运营"和"实习"两个词条进行搜索

搜索多个字段匹配的复杂信息(既在title中搜索、也在content中搜索):


四、Spring整合Elasticsearch

【版本问题】

以下配置和整合都依赖于 spring 的 2.1.5.RELEASE 版本,如果使用高版本,会有很多配置和属性无法使用的问题。

【在pom.xml中导入依赖】

Spring Boot Starter Data Elasticsearch

<!-- 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中配置】

如果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 :这个接口操作起来比较简单
  • ElasticsearchTemplate :需要这个接口来补充ElasticsearchRepository接口不能进行的操作

在接口中,我们只需要继承 ElasticsearchRepository 类即可,在此类中已经事先定义好了增删改查的操作,同时,我们还需在泛型中表明实体类型主键类型

@Repository
public interface DiscussPostRepository extends ElasticsearchRepository<DiscussPost, Integer> {
    
}

【测试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("<em>").postTags("</em>"),
                    new HighlightBuilder.Field("content").preTags("<em>").postTags("<.em>")
            ).build();

    // elasticTemplate.queryForPage(SearchQuery, class,SearchResultMapper);
    // elasticRepository.search() 方法底层调用了上一行的方法, 获取到了高亮显示的值, 但是没有返回

    // ElasticsearchRepository接口的查询操作
    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);
    }
}

通过 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("<em>").postTags("</em>"),
                    new HighlightBuilder.Field("content").preTags("<em>").postTags("</em>")
            ).build();

    // ElasticsearchTemplate接口的查询操作
    Page<DiscussPost> page = elasticTemplate.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集合中
            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);
    }
}

  • 3
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 2
    评论
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

李巴巴

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值