1、前言
之前在与研发进行 ES 使用优化的过程中,研发的同事饶有兴致的在会议后问了我这么一个问题:我们写入 ES 的 _id 字段和 lucene 中使用的 docid 是一个内容么?
两者有什么关联么?
当时对 Lucene 没有太多了解的我只能实话实说:两者应该不是一个概念,但是具体是否有关联我这边也没有梳理清楚,后面有结论了可以再进行沟通。
现在,我们针对这个问题梳理一下吧。
2、Lucene 的 docid
首先来看看 Lucene 官方对 docid 的定义。
Lucene's index is composed of segments, each of which contains a subset of all the documents in the index, and is a complete searchable index in itself, over that subset. As documents are written to the index, new segments are created and flushed to directory storage. Segments are immutable; updates and deletions may only create new segments and do not modify existing ones. Over time, the writer merges groups of smaller segments into single larger ones in order to maintain an index that is efficient to search, and to reclaim dead space left behind by deleted (and updated) documents.
Each document is identified by a 32-bit number, its "docid," and is composed of a collection of Field values of diverse types (postings, stored fields, doc values, and points). Docids come in two flavors: global and per-segment. A document's global docid is just the sum of its per-segment docid and that segment's base docid offset. External, high-level APIs only handle global docids, but internal APIs that reference a LeafReader, which is a reader for a single segment, deal in per-segment docids.
Docids are assigned sequentially within each segment (starting at 0). Thus the number of documents in a segment is the same as its maximum docid; some may be deleted, but their docids are retained until the segment is merged. When segments merge, their documents are assigned new sequential docids. Accordingly, docid values must always be treated as internal implementation, not exposed as part of an application, nor stored or referenced outside of Lucene's internal APIs.
最直接的定义:
1.docid 是 32 位的数字,只是用于标记文档;
2.docid 由不同类型的Field值(发布、存储字段、文档值和点)的集合组成;
3.docid 有两种类型:全局的和每段的。文档的全局文档值只是其每段文档值和该段的 docid 偏移量的总和。
4.docid 在每个段内按顺序分配(从 0 开始)。
5.docid 只在 segment merge 的时候回收,然后被重新分配。
最后,官方强调 docid 只能作为 Lucene 的内部实现,并不适合外部应用调用,也不能在 Lucene 的内部 api 之外存储或引用。
虽然官方对 docid 的生成和使用都介绍的很详细,但是最后一句概括了所有,docid 只是 Lucene 内部对文档进行标记的实现方式,与外部无关。
3、ES 的 _id
下面我们看看 ES 的 _id 字段。
Each document has an _id that uniquely identifies it, which is indexed so that documents can be looked up either with the GET API or the ids query. The _id can either be assigned at indexing time, or a unique _id can be generated by Elasticsearch. This field is not configurable in the mappings.
这里 ES 对 _id 字段的定义就很清晰,它是文档唯一性标记,它可以在写入的时候可以被指定值,如果不指定 ES 会自己生成一个值。即 _id 是 ES 文档数据的唯一性主键,即 uuid。
ES 这么做的原因是 Lucene 不强要求写入的数据需要带 uuid,这在 Lucene 的主要开发者 Michael McCandless 博客里得到了印证,Lucene 并不要求写入的数据具有唯一性主键。原文如下
Most search applications using Apache Lucene assign a unique id, or primary key, to each indexed document. While Lucene itself does not require this (it could care less!), the application usually needs it to later replace, delete or retrieve that one document by its external id.
而从 Lucene 的角度看,像 _id 这样的元信息字段,只是 ES 对 Lucene 定制的一个字段,和其他字段没有本质的区别。
public AbstractIdFieldType() {super(NAME, isIndexed:true, isStored:true, hasDocValue:false, TextSearchInfo.SIMPLE_MATCH_ONLY, Collections.emptyMap());}
从 ES 源码可以看到,_id 字段是一个被倒排索引,被 store 了,但是没有生成列存 docvalue 的特殊字段。
而我们日常用 _id 在 ES 进行 GET 操作获取文档数据,与使用 ES search API 进行 _id 查询获取数据,对于 Lucene 来说是一样的,都是对 _id 字段进行检索。
# get 操作
GET my-index-000001/_doc/2
# search 操作
GET my-index-000001/_search
{
"query": {
"terms": {
"_id": [ "2" ]
}
}
}
4、小结
综上,我们不难看出,虽然 _id 字段和 docid 都是用与标记文档的,但两者并无耦合性关系。
_id 字段是 ES 用来做数据唯一性主键的特殊字段。docid 则是 Lucene 的内部实现,服务于 Lucene 字段查找的过程。
当然,ES 实现数据多版本的唯一性,不仅仅依靠 _id 字段,更多的信息在
《Elasticsearch内核解析 - 数据模型篇》:
https://zhuanlan.zhihu.com/p/34680841,
而对 _id 字段优化使用有兴趣的同学可以参考
《Choosing a fast unique identifier (UUID) for Lucene》:
https://blog.mikemccandless.com/2014/05/choosing-fast-unique-identifier-uuid.html
作者介绍
金多安,Elastic 认证专家,Elastic 资深运维工程师,死磕 Elasticsearch 知识星球嘉宾,星球Top活跃技术专家,搜索客社区日报责任编辑