大多数使用Apache Lucene的搜索应用程序都会为每个索引文档分配唯一的ID(即主键)。 尽管Lucene本身不需要这样做(它可能不太在乎!),但应用程序通常需要它以后通过其外部ID替换,删除或检索该文档。 大多数在Lucene之上构建的服务器,例如Elasticsearch和Solr ,都需要一个唯一的ID,如果不提供,则可以自动生成一个ID。
有时,您的ID值已经预先定义,例如,如果外部数据库或内容管理系统分配了ID ,或者您必须使用URI ,但是如果您可以自由分配自己的ID,那么哪种方法最适合Lucene?
一个明显的选择是Java的UUID类,该类生成版本4的通用唯一标识符 ,但事实证明,这是性能上最糟糕的选择:它比最快的速度慢4倍。 要了解原因,需要对Lucene如何找到术语有所了解。
BlockTree术语词典
术语词典的目的是存储在索引期间看到的所有唯一术语,并将每个术语映射到其元数据( docFreq
, totalTermFreq
等 )以及totalTermFreq
(文档,偏移量,投递和有效载荷)。 当请求一个术语时,术语词典必须在磁盘索引中找到它并返回其元数据。
默认编解码器使用BlockTree术语词典 ,该词典以排序的二进制顺序存储每个字段的所有术语,并将这些术语分配到共享公共前缀的块中。 默认情况下,每个块包含25到48个词。 它使用内存中的前缀三叉戟索引结构( FST )将每个前缀快速映射到相应的磁盘块,并在查找时首先根据请求的术语的前缀检查索引,然后在-disk块并扫描以查找术语。
在某些情况下,当段中的术语具有可预测的模式时,术语索引可以知道请求的术语不能存在于磁盘上。 这种快速匹配测试可以带来可观的性能提升,尤其是当索引很冷(操作系统的IO缓存不缓存页面)时,因为它避免了昂贵的磁盘搜寻。 由于Lucene是基于段的,因此单个id查找必须访问每个段直到找到匹配项,因此快速排除一个或多个段可能是一个大胜利。 保持细分受众群的数量尽可能少也很重要!
鉴于此,完全随机的id(例如UUID V4 )应该会表现最差,因为它们击败了术语索引快速匹配测试,并且需要对每个段进行磁盘搜索。 具有可预测的每段模式的ID(例如顺序分配的值或时间戳)应发挥最佳作用,因为它们将使术语索引快速匹配测试的收益最大化。
测试性能
我创建了一个简单的性能测试器来验证这一点。 完整的源代码在这里 。 该测试首先将1亿个ID索引到具有7/7/8段结构(7个大段,7个中段,8个小段)的索引中,然后搜索200万个ID的随机子集,记录最佳时间5次。 我在Ubuntu 14.04上使用Java 1.7.0_55,以及3.5 GHz Ivy Bridge Core i7 3770K。
由于Lucene的术语从4.0开始是完全二进制的 ,因此存储任何值的最紧凑的方法是二进制形式,其中每个字节使用所有256个值。 然后,一个128位ID值需要16个字节。
我测试了以下标识符来源:
- 顺序ID(0、1、2,...),采用二进制编码。
- 零填充的顺序ID(00000000、00000001等),采用二进制编码。
- 纳米时间,二进制编码。 但是请记住, 纳米时间是棘手的 。
- 使用此实现从时间戳,nodeID和序列计数器派生的UUID V1 。
- UUID V4 ,使用Java的
UUID.randomUUID()
随机生成。 - 片状ID ,使用此实现 。
对于UUID和Flake ID,除了标准(16或36基)编码之外,我还测试了二进制编码。 请注意,我仅使用一个线程测试了查找速度,但是在添加线程时,结果应该线性扩展(在足够并行的硬件上)。
用二进制编码的零填充顺序ID最快,比非零填充顺序ID快很多。 UUID V4(使用Java的UUID.randomUUID()
)慢了约4倍。
但是对于大多数应用程序来说,顺序编号是不实际的。 第二快的是UUID V1 ,以二进制编码。 令我惊讶的是,这比Flake ID快得多,因为Flake ID使用相同的原始信息源(时间,节点ID,序列),但是以不同的方式对位进行混洗以保留总体顺序。 我怀疑问题在于,在获取不同文档之间的数字之前,必须在片状ID中遍历的通用前导数字的数目,因为64位时间戳的高位在先,而UUID V1则在低位位首先是64位时间戳。 当一个字段中的所有术语共享一个公共前缀时,术语索引也许可以优化这种情况。
我还分别测试了10、16、36、64、256的基数,通常对于非随机ID,较高的基数更快。 我对此感到惊讶,因为我希望与BlockTree块大小(25到48)匹配的基数最好。
此测试有一些重要警告(欢迎补丁)! 一个真正的应用程序显然比简单地查找id还要做更多的工作,并且结果可能会有所不同,因为热点必须编译更多活动的代码。 在我的测试中,该索引非常热(有大量RAM可以容纳整个索引); 对于冷索引,我希望结果会更加鲜明,因为避免磁盘搜索变得非常重要。 在实际应用中,使用时间戳的id在时间上会更加分散; 我可以通过伪造更大范围的时间戳来“模拟”自己。 也许这会缩小UUID V1和Flake ID之间的差距? 我在索引编制过程中只使用了一个线程,但是具有多个索引编制线程的实际应用程序会将ID一次分散到多个段中。
我使用了Lucene的默认TieredMergePolicy ,但是有一种更聪明的合并策略可能会支持那些ID更加“相似”的合并段,从而可能会产生更好的结果。 该测试不执行任何删除/更新操作,这将在查找期间需要进行更多工作,因为如果给定的ID已被更新(除其中一个以外的所有其他ID),则该ID可能位于多个段中。
最后,我使用了Lucene的默认编解码器,但是当您愿意将RAM换成更快的查询时,我们有不错的发布格式针对主键查找进行了优化,例如去年的Google夏季代码项目和MemoryPostingsFormat 。 这些可能会提供可观的性能提升!
翻译自: https://www.javacodegeeks.com/2014/05/choosing-a-fast-unique-identifier-uuid-for-lucene.html