Elasticsearch学习初篇

工作了好几年,一直也都没有用上Elasticsearch,周边的同事有的已经用了,但我们却没有机会用。

自己在做的网上商城有商品搜索功能,所以我希望借此机会好好地学习一下Elasticsearch。

官网的教程还是挺细致的,没有必要在这里重新写一遍了,就是下面参考资料的权威指南。

可以选择docker安装,也可以直接在主机下载压缩包安装。压缩包里有启动脚本,单机的比较好操作。当然单机只适合学习,因为它本身就是一个分布式存储引擎,在实际应用中不可能用单机的。

ES的高性能主要得益于其特殊的数据存储结构:倒排索引,一种单词-文档的数据结构。文档是ES中比较基础的内容,相当于一条记录的数据。

这里说下倒排索引的产生过程。比如说,现在某个索引下的文档数据如下:

文档1 年度目标
文档2 AI技术生态部的年度目标
文档3 AI市场的年度目标

其产生的倒排索引如下:

单词ID 单词 逆向文档频率 倒排列表
1 目标 3 (1;1;<1>),(2;1;<5>),(3;1;<4>)
2 年度 3 (1;1;<2>),(2;1;<4>),(3;1;<3>)
3 AI 2 (2;1;<1>),(3;1;<1>)
4 技术 1 (2;1;<2>)
5 生态 1 (2;1;<3>)
6 市场 1 (3;1;<2>)

倒排列表的含义就是 在第几个文档,出现过几次,在第几个单词。

搜索后,会根据匹配情况,返回命中的结果。比如:

curl -XPOST http://localhost:9200/haibo/_search?pretty=true  -H 'Content-Type:application/json' -d' {
    "query" : { "match" : { "content" : "向中国" }},
    "highlight" : {
        "pre_tags" : ["<tag1>", "<tag2>"],
        "post_tags" : ["</tag1>", "</tag2>"],
        "fields" : {
            "content" : {}
        }
    }
}
'
{
  "took" : 4,
  "timed_out" : false,
  "_shards" : {
    "total" : 1,
    "successful" : 1,
    "skipped" : 0,
    "failed" : 0
  },
  "hits" : {
    "total" : {
      "value" : 2,
      "relation" : "eq"
    },
    "max_score" : 1.315783,
    "hits" : [
      {
        "_index" : "haibo",
        "_type" : "_doc",
        "_id" : "3",
        "_score" : 1.315783,
        "_source" : {
          "content" : "如果有一天DD有国家向中国发起了战争,一定会有很多人勇往直前"
        },
        "highlight" : {
          "content" : [
            "如果有一天DD有国家<tag1>向</tag1><tag1>中国</tag1>发起了战争,一定会有很多人勇往直前"
          ]
        }
      },
      {
        "_index" : "haibo",
        "_type" : "_doc",
        "_id" : "2",
        "_score" : 0.9186288,
        "_source" : {
          "content" : "中国是个好地方"
        },
        "highlight" : {
          "content" : [
            "<tag1>中国</tag1>是个好地方"
          ]
        }
      }
    ]
  }
}

搜索的结果中包含了匹配的结果,每个数据都有评分,倒序排列。

上面所说的倒排索引使得基于Lucene的ES查找速度更快。简单说它的思想是尽量将数据放在内存中,减少磁盘读取此书,并通过各种压缩技术,提高内存使用率。

上面的例子中,单词是Term,而后面的列表存储的是文档列表,其被称为Posting list。

此外,还有一个Term Dictorary的概念,其目的就是将Term排序,提高检索效率。其存储的就是元数据信息。

为了提高查询效率,希望将Term Dictorary放到内存中,然而实际上,Term Dictorary可能很大,没办法全部都放到内存中,因为出现了Term Index,其存储了倒排索引文件。Term Index在内存中是以FST(finite state transducers)存储实现的,思想就是使用字节存储所有的term index,非常节省内存,具体的可以看下面的参考资料。

目前,对于ES的客户端很多,官方对于不同语言都有相应的推荐,甚至都可以自己写,因为可以通过http访问。对于Java,使用官方推荐的 high level rest client,(不用springboot的spring-data-redis-elasticsearch)。

就介绍简单的使用。

引入:

 <dependency>
            <groupId>org.elasticsearch.client</groupId>
            <artifactId>elasticsearch-rest-client</artifactId>
            <version>7.8.0</version>
        </dependency>
        <dependency>
            <groupId>org.elasticsearch</groupId>
            <artifactId>elasticsearch</artifactId>
            <version>7.8.0</version>
        </dependency>
        <dependency>
            <groupId>org.elasticsearch.client</groupId>
            <artifactId>elasticsearch-rest-high-level-client</artifactId>
            <version>7.8.0</version>
        </dependency>

就算使用高阶API client,也要引入其他两个,因为高阶的也依赖基础的操作。

基本操作:

@Configuration
public class EsConfiguration {

    @Value("${spring.elasticsearch.host}")
    public String host;

    @Value(("${spring.elasticsearch.port}"))
    public int port;

    @Value("${spring.elasticsearch.scheme}")
    public String scheme;


    @Bean
    public RestHighLevelClient restHighLevelClient(){
        return new RestHighLevelClient(
                RestClient.builder(
                        new HttpHost(host, port, scheme)
                )
        );
    }

}





@Component
public class HighRestClientDao {


    @Autowired
    RestHighLevelClient restHighLevelClient;


    public boolean createIndex(String indexName, Map<String,Map<String,Object>> properties) throws Exception {
//        XContentBuilder xContentBuilder = XContentFactory.jsonBuilder();
//        xContentBuilder.startObject();
//        xContentBuilder
//                .startObject("mapping")
//                    .field("properties",properties)
//                .endObject()
////                .startObject("settings")
//        ;
//        xContentBuilder.endObject();
//        CreateIndexRequest createIndexRequest = new CreateIndexRequest(indexName).source(xContentBuilder);
        CreateIndexRequest createIndexRequest = new CreateIndexRequest(indexName);
        Map<String,Object> mapping = new HashMap<>();
        mapping.put("properties",properties);
        createIndexRequest.mapping(mapping);
        CreateIndexResponse createIndexResponse = restHighLevelClient.indices().create(createIndexRequest, RequestOptions.DEFAULT);
        log.info("get create index,request:{} response:{}",createIndexRequest,createIndexResponse);
        return createIndexResponse.isAcknowledged();
    }

    public boolean isExistIndex(String indexName) throws Exception{
        GetIndexRequest getIndexRequest = new GetIndexRequest(indexName);
        getIndexRequest.humanReadable(true);
        getIndexRequest.includeDefaults(false);
        getIndexRequest.local(false);
        return restHighLevelClient.indices().exists(getIndexRequest,RequestOptions.DEFAULT);
    }

    public boolean deleteIndex(String indexName) throws Exception {
        DeleteIndexRequest deleteIndexRequest = new DeleteIndexRequest(indexName);
        deleteIndexRequest.timeout("1m");
        return restHighLevelClient.indices().delete(deleteIndexRequest,RequestOptions.DEFAULT).isAcknowledged();
    }

}

关于文档的删除操作:

Elasticsearch中的文档是不可变的,因此不能被删除或者改动以展示其变更。但是API是有删除或改动的。
磁盘上的每个段都有一个相应的.del文件。当删除请求发送后,文档并没有真的被删除,而是在.del文件中被标记为删除。该文档依然能匹配查询,但是会在结果中被过滤掉。当段合并时,在.del文件中被标记为删除的文档将不会被写入新段。
对于更新操作。在新的文档被创建时,Elasticsearch会为该文档指定一个版本号。当执行更新时,旧版本的文档在.del文件中被标记为删除,新版本的文档被索引到一个新段。旧版本的文档依然能匹配查询,但是会在结果中被过滤掉。

刚开始学习,看到number_of_shards以及replicas的概念,不是很懂。看了文档才理解。

在Lucence中,分片是index的实例,一个索引会有一个或者多个分片。

通过在集群中设置多个分片,有点分而治之的意思,在进行查询时,多个分片会同时进行,最后将所有分片的数据进行整合。分片数默认是5个。当一个节点的数据量过多时,ES会自动做负载均衡,将数据分配在不同的分片上。然而,ES的一个好处是,多个分片对于用户是无感知的,不用考虑其复杂性。

作者将分片数量设置5个也是综合考量了数据增长的可能性与分片查询带来的开销。

副本:就是主分片的副本,主分片挂掉后,副本会自动成为主分片。

正常情况下,一个集群的节点数量是 分片数 * (副本数+1),因为副本和主分片一样,占用资源。

对于副本,在写入的时候,可以设置所有副本都同步成功了,才返回给客户端;在请求的时候,根据轮询算法,在主分片和副本中选择一个进行读请求,从而实现负载均衡。

有了分片,带来一个最直观的问题就是查询时,正常会查询所有分片,然后等待所有分片查询后,将所有结果进行合并。这势必带来了一定的开销。因此ES中有一个概念叫做路由。

如果我们在插入时,指定了使用什么路由,那么在查询时,也可以根据我们设定的路由来查询,根据路由,ES会到指定的shard上进行查询,而不用查询所有分片。

分片默认算法:

shard_num = hash(_routing) % num_primary_shards

_routing可以指定。

再说下ES的近乎实时的搜索机制,实时搜索指的是数据写入之后可近乎实时搜索到数据。

ES在写入时数据会先写入到进程buffer里,随后数据会copy到PageCache中,随后1s中会定时刷盘。

此外,为了避免宕机后,数据丢失,ES增加了一个用于崩溃回复的日志,transferlog。ES使用translog来记录所有的操作,我们称之为WAL,当新增一条记录时,es会把数据写到translog和buffer中。类是一个索引分片层级的组件,这个日志的作用就是将没有提交的索引操作记录下来。但是,这并不能说明,有了tanslog就不会造成数据丢失,这还要和操作系统的pagecache有关。因为数据首先会写入到pagecache,然后定时(默认5s)刷盘的,因此如果写入到pageCache后,如果在刷盘前断电了,还是会出现数据丢失的,但这个问题是所有文件写入都会遇到的,这个是不可控的。所以tansferlog在一定程度上避免因为ES进程本身挂掉导致的数据丢失。

下面示意图是ES的一个写入流程图:

参考资料:

Elasticsearch: 权威指南

Apache Lucene Core

全文搜索引擎ElasticSearch介绍

Elasticsearch源码解读六:ES中的倒排索引

Elasticsearch原理学习--为什么Elasticsearch/Lucene检索可以比MySQL快?

Day 7 - Elasticsearch中数据是如何存储的

ES从入门到实战】完整合集版,带思维导图

掌握它才说明你真正懂 Elasticsearch - ES(三)

ES基本原理介绍

--------EOF---------
微信分享/微信扫码阅读