集成Lucene和HBase

2012 年 3 月 29 日

在所有先进的应用程序中,不管是购物站点还是社交网络乃至风景名胜站点,搜索都扮演着关键的角色。Lucene 搜索程序库事实上已经成为实现搜索引擎的标准。苹果、IBM、Attlassian(Jira)、Wolfram 以及很多大家喜欢的公司【1】都使用了这种技术。因此,大家对任何能够提升 Lucene 的可伸缩性和性能的实现都很感兴趣。

Lucene 简介

Lucene 中可搜索的实体都表现为文档(document),它由字段(field)和值(value)组成。每个字段值都由一个或多个可搜索的元素——即词汇(term)——组成。Lucene 搜索基于反向索引,其中包含了关于可搜索文档的信息。在使用正常索引时,你可以搜索文档,以了解它包含哪些字段,但使用反向索引与之不同,你会搜索字段的词汇,以了解所有包括该词汇的文档。

图 1 显示的是高层次的 Lucene 架构【2】。它的主要组件包括 IndexSearcher、IndexReader、IndexWriter 和 Directory。IndexSearcher 实现了搜索逻辑。IndexWriter 为每个插入的文档写入反向索引。IndexReader 会在 IndexSearcher 的支持下读取索引的内容。IndexReader 和 IndexWriter 都依赖于 Directory,它会提供操作索引数据集的 API,而该 API 会直接模拟文件系统 API。

图 1: 高层次的 Lucene 架构

标准的 Lucene 分发包中有多个目录实现,包括基于文件系统和基于内存的 [1]

标准基于文件系统的后端的缺点在于,随着索引增加性能会下降。人们使用了各种不同的技术来解决这个问题,包括负载均衡和索引分片(index sharding)——在多个 Lucene 实例之间切分索引。尽管分片功能很强大,但它让总体的实现架构变得更复杂,并且需要大量对期望文档的预测知识,才能对 Lucene 索引进行合适地分片。

另一种不同的方法是,让索引后端自身对数据进行正确地分片,并基于这样的后端构建出实现。这种后端可以是 NoSQL 数据库。在本文中我们会描述基于 HBase 的实现【4】。

实现方法

正如在【3】中所说明的,在高层次上,Lucene 会操作两个单独的数据集:

  • 索引数据集中保存了所有字段 / 词汇对(还有其他信息,像术语频率、位置等),以及在恰当的字段包含这些词汇的文档。
  • 文档数据集中存储所有文档,包括存储的字段等。

正如我们已经在上面提到的,想要把 Lucene 移植到新的后端中,直接实现 directory 接口并不总会是最简单(最方便)的方法。所以,很多对 Lucene 的移植,包括从 Lucene 的 contrib.module 支持的优先内存索引、Lucandra【5】和 HBasene【6】分别采用了不同的方法【2】,不仅重写了 directory,还重写了高级的 Lucene 类——IndexReader 和 IndexWriter,从而绕开了 Directory 的 API(如图 2)。

图 2: 将 Lucene 和没有文件系统的后端整合

尽管这种方法通常需要更多工作【2】,但是它能够带来更强大的实现,让我们可以完全利用后端的本地功能。

文中所展现的实现 [2] 也遵循了这种方法。

总体架构

总体上的实现(如图 3)是在基于内存的后端之上构建的,并将其用作内存缓存、同步缓存和 HBase 后端的机制。

图 3: 基于 HBase 的 Lucene 实现的总体架构

该实现试图平衡两种相互冲突的需求,性能: 在内存中,缓存能够最小化 HBase 用于搜索和文件返回的数据读取量,从而极大提升性能;可伸缩性: 按照需要运行为多个 Lucene 示例以支持日益增长的搜索客户端的能力。后者需要最小化缓存的生命周期,从而和 HBase 实例(上面提到实例的副本)中的内容同步。通过为活动参数实现可配置的缓存时间,限制每个 Lucene 实例中展现的缓存,我们可以达成一种折中方案。

内存缓存中的底层数据模型

正如之前所提到的,内部 Lucene 数据模型基于两种主要的数据集——索引和文档,它们会被实现为两种模型——IndexMemoryModel 和 DocumentMemoryModel。在我们的实现中,读写操作(IndexReader/IndexWriter)都是通过内存缓存完成的,但是它们的实现有很大区别。对于读取操作,缓存首先会检查所需要的数据是否在内存中,并且没有过期 [3] ,如果那样的话就会直接使用。否则缓存就会从 HBase 读取或者刷新数据,然后把它返回给 IndexReader。相反,对于写操作,数据会直接写入到 HBase,而不会在内存中存储。尽管这在实际的数据可用性方面会有延迟,但是它会让实现过程非常简单——我们不需要考虑把新建或者更新的数据发送给哪个缓存。这里的延迟可以通过设置合适的缓存过期时间来控制,从而符合业务需求。

IndexMemoryModel

索引内存模型的类图如图 4 所示。

图 4: IndexMemoryModel 类图

在这个实现中:

  • LuceneIndexMemoryModel 类包含了当前存在于内存中所有字段的 FieldTermDocuments 类。它还提供了所有对于实现 IndexReader 和 IndexWriter 必要的内部 API。
  • FieldTermDocuments 类会为每个字段值管理 TermDocuments。通常,对于可扫描的数据库,字段的列表和字段值的列表可以组合在可导航的字段 / 词汇值列表中。对于基于内存的缓存实现,我们已经把它们切分为两个独立的部分,从而让搜索的时间更可预测。
  • TermDocuments 类为每个文档 ID 包含了一系列 TermDocument 类。
  • TermDocument 类包含了针对给定文档在索引中存储的信息——文档使用频度和位置的数组。

DocumentMemoryModel

文档内存模型的类图如图 5 所示。

图 5: DocumentMemoryModel 类图

在这个实现中:

  • LuceneDocumentMemoryModel 类包含 DocumentStructure 类与每个被索引文档的映射关系。
  • DocumentStructure 类中包含了单个文档的信息。针对每个文档,它都包含了为每个索引后字段保存的字段和信息。
  • FieldData 类包含为存储字段(stored field)所保存的信息,包括字段名称、值和二进制或者字符串型的标识。
  • DocumentTermFrequency 类包含了关于每个被索引字段的信息,包括对相应索引结构(索引、词汇)的向后引用、文档中词汇使用频率、词汇在文档中的位置以及从文档开始的偏移量。

LuceneDocumentNormMemoryModel

正如在【9】中说明的,规范(norm)是用于表现文档或字段的加权因子,从而提供更好的搜索结果排序,这需要耗费大量内存。类的实现基于对映射的映射(map of maps),内部映射会存储规范和文档的映射关系,而外部映射会存储规范和字段的映射关系。

尽管规范信息的键值是字段名称,从而可以添加到 LuceneIndexMemoryModel 类中,但是我们还是决定把对规范的管理实现为单独的类——LuceneDocumentNormMemoryModel。这么做的原因在于,在 Lucene 使用规范是可选的操作。

IndexWriter

有了之前所描述的底层内存模型,实现索引写入程序就很简单了。因为 Lucene 不会定义 IndexWriter 接口,所以想要实现 IndexWriter,我们需要实现所有标准 Lucene 实现中的方法。这个类的主要内容在于 addDocument 方法。这个方法会遍历所有文档的字段。对于每个字段,方法都会检查它是否可以令牌化(tokenized),并使用特定的分析器来做到这一点。这个方法还会更新所有三种内存结构——索引、文档和(可选的)规范,它们会为新增的文档存储信息。

IndexReader

IndexReader 会实现 Lucene 核心所提供的 IndexReader 接口。因为 Hbase 中所获得的列表和单独的读操作相比要快很多,所以我们使用一些方法来扩展这个类,从而可以读取多个文档。类本身没有把更多的处理转交给几个类,它会对其进行管理:

  • 尽管文档 ID 通常是字符串,但 Lucene 内部还是对整型数操作。DocIDManager 这个类会负责管理从字符串到数字的转换。IndexReader 会以 ThreadLocalStorage 的形式使用这个类,从而可以在线程结束之后自动清理。
  • MemoryTermEnum 类扩展了 Lucene 提供的 TermEnum 类,负责扫描字段 / 词汇的值。
  • MemoryTermFrequencyVector 类会实现 Lucene 提供的 TermDocs 和 TermPositions 接口,负责为给定的字段 / 词汇对(field/tem pair)处理与文档相关的信息。
  • MemoryTermFrequencyVector 类实现了 Lucene 提供的 TermFreqVector 和 TermPositionVector 接口,负责针对给定的文档 ID 返回文档字段频率和位置信息。

HBase 表

以上提出的解决方案基于两个主要的 HBase 表——Index 表(图 6)和 document 表(图 7)。

图 6: HBase 的 Index 表

(点击图像可以放大)

图7: HBase 的document 表

如果需要支持规范的话,可选择实现第三个表(图8)。

图8: HBase 的norm 表

HBase 的 Index 表(图 6)会完成实现的主要工作。这个表对每个 Lucene 实例所知道的字段 / 词汇组合都设置了入口,其中包含一个列族(column family)——documents 族。这个列族为包含这个字段或词汇的所有文档都包含了一列(名称是文档的 ID)。每个列的内容都是 TermDocument 类的值。

HBase 的 document 表(图 7)存储了文档本身、对索引或规范的向后引用,它会为文档处理引用这些文档以及一些 Lucene 使用的附加信息。它对所有 Lucene 实例知道的文档都设置了入口(row)。每个文档都通过文档 ID(键值)唯一标识,并包含两个列族——字段族和索引族。字段列族针对所有存储在 Lucene 中的文档字段包含一列(名称为字段的名称)。列的值是值的类型(字符串或者字符数组)和值本身的组合。索引列族为每个引用这个文档的索引包含了一列(名称是字段或者术语)。列的值包括给定字段 / 词汇在文档的使用频率、位置和偏移量。

HBase 的 norm 表(图 8)为每个字段存储了文档的规范。它对所有 Lucene 实例知道的每个字段(键值)都设置了入口(行)。每行都只包含一个列族——规范族。这个族对每个需要存储给定字段规范的文档都有一列(名称是文档 ID)。

数据格式

最终的设计方案确定了在 HBase 中存储数据的数据格式。对于这个实现,我们基于性能、结果数据的最小规模以及和 Hadoop 的紧密整合程度选择了 Avro【10】。

实现主要使用的数据结构是 TermDocument(代码 1)、文档的 FieldData(代码 2)和 DocumentTermFrequency(代码 3)。

复制代码
{
"type" : "record",
"name" : "TermDocument",
"namespace" : "com.navteq.lucene.hbase.document",
"fields" : [ {
"name" : "docFrequency",
"type" : "int"
}, {
"name" : "docPositions",
"type" : ["null", {
"type" : "array",
"items" : "<u>int</u>"
}]
} ]
}

代码 1 词汇文档 AVRO 定义

复制代码
{
"type" : "record",
"name" : "FieldsData",
"<u>namespace</u>" : "com.navteq.lucene.hbase.document",
"fields" : [ {
"name" : "fieldsArray",
"type" : {
"type" : "array",
"items" : {
"type" : "record",
"name" : "singleField",
"fields" : [ {
"name" : "binary",
"type" : "boolean"
}, {
"name" : "data",
"type" : [ "string", "bytes" ]
} ]
}
}
} ]
}

代码 2 字段数据 AVRO 定义

复制代码
{
"type" : "record",
"name" : "TermDocumentFrequency",
"<u>namespace</u>" : "com.navteq.lucene.hbase.document",
"fields" : [ {
"name" : "docFrequency",
"type" : "<u>int</u>"
}, {
"name" : "docPositions",
"type" : ["null",{
"type" : "array",
"items" : "<u>int</u>"
}]
}, {
"name" : "docOffsets",
"type" : ["null",{
"type" : "array",
"items" : {
"type" : "record",
"name" : "TermsOffset",
"fields" : [ {
"name" : "startOffset",
"type" : "<u>int</u>"
}, {
"name" : "endOffset",
"type" : "<u>int</u>"
} ]
}
}]
} ]
}

代码 3 TermDocumentFrequency 的 AVRO 定义

结论

本文中描述的简单实现完全支持所有 Lucene 功能,针对 Lucene 核心和普通模块的单元测试都验证了这一点。我们可以将它作为基础,构建可扩展性很强的搜索实现,支持 HBase 固有的可扩展性以及完全对称的设计,让我们可以添加任意数量服务于 HBase 数据的进程。它还可以避免需要关闭打开状态的 Lucene 索引读取程序,就可以包含新的索引数据,那会经过一定延迟之后为用户所用,而延迟是通过活动参数的缓存时间所控制的。在下一篇文章中我们会展示如何扩展这个实现,以包含地理搜索支持。

关于作者

Boris Lublinsky是 NAVTEQ 的首席架构师,他的工作是为大型数据管理和处理、SOA 以及实现各种 NAVTEQ 项目定义架构的愿景。他还是 InfoQ 的 SOA 编辑,并且参与了 OASIS 的 SOA RA 工作组。Boris 是一位作者,并经常发表演讲,他最新的著作是《Applied SOA》。

Michael Segel拥有二十多年和客户协作的经验,和他们一起确定并解决业务问题。Michael 曾经在多个领域以多种角色工作过。他是一位独立顾问,期望解决任何有挑战性的问题。Michael 拥有俄亥俄州立大学的软件工程学位。

参考文献

  1. Lucene-java Wiki
  2. Animesh Kumar. Apache Lucene and Cassandra
    1. Animesh Kumar. Lucandra - an inside story!
  3. HBase
  4. Lucandra
  5. HBasene
  6. Bigtable
  7. Cassandra
  8. Michael McCandless, Erik Hatcher, Otis Gospodnetic. Lucene in Action, Second Edition.
  9. Boris Lublinsky. Using Apache Avro .

[1] 附加的 Lucene 程序包含了为 Berkley DB 构建的 DB 目录。

[2] 这个实现受到了 Lusandra 源代码【3】的启发。

[3] 没有在内存中存在太长时间。

查看英文原文: Integrating Lucene with HBase


给 InfoQ 中文站投稿或者参与内容翻译工作,请邮件至 editors@cn.infoq.com 。也欢迎大家通过新浪微博( @InfoQ )或者腾讯微博( @InfoQ )关注我们,并与我们的编辑和其他读者朋友交流。

2012 年 3 月 29 日 00:0013293
用户头像

发布了 340 篇内容, 共 111.1 次阅读, 收获喜欢 2 次。

关注

评论

发布
暂无评论
发现更多内容

Skywalking Php注册不上问题排查

心平气和

php Skywalking 全链路追踪

第一周总结

睁眼看世界

极客大学架构师训练营

有了数据湖探索服务,企业决策“新”中有数

华为云开发者社区

Serverless 数据湖 数据分析 云原生 华为云

架构师训练营-week01-作业

大刘

极客大学架构师训练营

oeasy 教您玩转 linux 之 010301 电子宠物 pet

o

第一周作业

alpha

极客大学架构师训练营

架构师训练营-大作业

连增申

“锟斤拷”的前世今生

Java旅途

Unicode 编码 ASCII 锟斤拷

大作业二

嘻哈

技术解析丨C++元编程之Parser Combinator

华为云开发者社区

c++ 字符串 Parser Combinator Parser 元编程

期末作业-达通快递

森林

C++隐式推导-auto关键词

良知犹存

c++

LeetCode题解:66. 加一,BigInt,JavaScript,详细注释

Lee Chen

LeetCode 前端进阶训练营

架構師訓練營第1期-第01周總結

Panda

架构师训练营大作业

Bruce Xiong

架构师 0 期大作业(二)

何伟敏

架构师 0 期第十三周命题作业

何伟敏

Golang领域模型-聚合根

奔奔奔跑

golang 架构 微服务 领域驱动设计 DDD

互联网架构师能力图谱

dony.zhang

架构师 架构师技能

拖动旋转的 3D 骰子效果

Clloz

CSS transform rotate3d

架构师训练营大作业

方堃

大作业-同城快递

林毋梦

架构师训练营大作业二(架构思维导图)

吴建中

使用枚举的正确姿势

Java旅途

Java 单例 枚举

轻松的可贵

谷鱼

回忆 转折

架构师训练营第 1 期第一次作业

强风

在进行廋身之前,对你来说是想要找到问题的真相?或是解决当下的问题?

叶小鍵

心理学 基思·斯坦诺维奇

系统架构师训练营大作业(一)-同城物流快递业务系统架构设计

吴建中

LeetCode题解:84. 柱状图中最大的矩形,使用栈,JavaScript,详细注释

Lee Chen

LeetCode 前端进阶训练营

食堂就餐系统 UML 图

睁眼看世界

极客大学架构师训练营 食堂就餐系统

思维导图

集成Lucene和HBase-InfoQ