用 Lucene 构建文档数据库

说到“档案”系统,选文档数据库再合适不过了。谈到文档数据库通常想到的是 MongoDB、CouchDB 之类的,可这里要说的不是这些,而是另外一个 NoSQL “文档数据库” —— Lucene。之因此要打引号,是由于暂时还没听到别人这样说。php

  1. 需求

最近公司要弄一个内部搜索,对比各类方案后,决定用 Lucene。当作出第一个原型后,考虑到公司另外几个项目未来也许用的上,而再写一遍代码可不是个人风格;又试用了开箱即用的 Solr,以为那也不是个人菜。由于我项目内已经有相似 Solr 的 Schame 的配置在用了,我打算复用这个模块;接口规范我也打算复用我现有的规范。html

基础的增删改查比较简单,很快就作出了原型。此时我想到公司另外一个大模块:档案(或叫简历)。这部分我已计划与另外一个项目的相似模块作整合,考虑用 MongoDB 重构。既然 Lucene 能够存取较复杂的数据结构,何不借此机会研究一下用 Lucene 做为档案系统的底层支撑呢。java

那这里说的档案是什么样子呢?举一个简单例子,一份我的简历:git

姓名:XXX
性别:男
照片:xxx/xxx.jpg
兴趣爱好
    兴趣:跑步、游泳、XX自定义
    简介:是浪费时间的服务吉林省地方就,受到法律书籍地方
教育经历
    经历1
        日期区间: 2014/1/1~2015/1/1
        学校: Jiali.Dun
        专业: 挖掘机
        学位:没士
    经历2……

大概的文档结构就是就是这样,字段、层级是不肯定的,须要保持此结构,能存、能取,大部分字段可查询、排序。github

  1. 结构化数据

总结以上档案结构,组成上可分为:mongodb

a. 基础板块(名字,性别,照片)
b. 其余板块(同上,但被区分开)
c. 列表板块(教育经历)

上面特地将基础信息称为基础“板块”,也就是说,通常状况下一份档案是由多个板块组成的。也许您的档案还会更复杂,好比兴趣爱好下再分为运动、娱乐,这种划分方式从存储上来讲与两层设计没什么区别,多了一个父级板块的指向而已,但这增长了展示的复杂度。如今你们都在谈“扁平化”,我所理解的扁平不只仅是把图标拍扁了,更是信息获取的渠道扁平了,能一下给我看的,不要让我点一层菜单进去又点一层;能用标签、搜索筛选的,不要让我点目录树查找。数据库

一个板块就是一组键值对,此处咱们将这一组规则称为表单。那么,列表板块就是由多个可重复表单组成的板块。apache

字段上能够有:json

a. 文本
b. 数字
c. 文件
d. 日期、时间(区间)
e. 单选、多选
f. 多条数据(文本、数字、日期等)

从 a~e 都是很常见的类型,文件能够转储到文件服务器上,这里只存 URL;日期、时间能够转换成时间戳。而 f 是指这个字段的值能够输入多个,一般用来记录一些须要多条记录东西,存储上与多选同样。服务器

Lucene 本来就是一个字段能够存多个值,这太妙了。

  1. 表单及验证

前面谈到我本身有一个数据校验模块,对数据结构的描述以下:

表单1
    字段1:类型,是否必填,是否重复,其余校验参数
    字段2……
枚举1
    取值1:名称
    取值2……

举一个栗子:

简历表单
    姓名:文本,必填,不重复,最大长度100
    性别:选项,必填,不重复,性别枚举
    照片:图片,选填,可重复,类型(jpg,png)
    兴趣爱好:表单,选填,不重复,兴趣爱好表单
    教育经历:表单,选填,可重复,教育经历表单
性别枚举
    0:女
    1:男
    2:中性
兴趣爱好表单
    兴趣:文本,必填,可重复,最大长度50
    简介:文本,选填,不重复,多行文本
教育经历表单
    日期区间:日期区间,必填,不重复
    学校:文本,必填,不重复
    专业:文本,必填,不重复

此表单描述上也是为了方便编辑和解析,设计成了 表单->字段 两层结构,未使用代码嵌套而是使用连接嵌套的方式。校验器在校验的时候,发现字段类型为表单,取出对应表单递归下去就好了。那这么多表单都堆积在一块儿,怎么解决命名空间的问题呢?我设计为每一个模块(同一应用主题)一个这样的配置,校验器在处理表单时若是没给出模块名(配置名),则取当前模块的指定名字的表单,有则取指定模块下的表单。

数据在校验成功后,会将数据清理为相似如下 JSON 的结构:

{
    "name": "XXX",
    "gender": 1,
    "photo": "upload/photo/xxxxxx.jpg",
    "hobby": {
        "interest": [
            "ljsdfsdfsd",
            "sldfj2ef"
        ],
        "comment": "sjldfjsldfsdlfjsldfsdfsdfsdfsdfsdf"
    },
    "education": [
        {
            "date": {
                "begin": Date(2014/1/1), 
                "end": Date(2015/1/1)
            },
            "university": "lwnfdsfwe",
            "professional": "slwef"
        }
    ]
}

输入的数据结构与此一致,对于使用 application/x-www-form-urlencoded 格式提交的数据,能够根据"."、"["和"]"解析成上面的数据结构,就像 PHP 的请求参数解析方式。

  1. 存储方式

OK,上面已经扯了不少了,这开始进入正题了。数据都清理好了,但是这样一个结构的数据怎么存到 Lucene 检索库里呢?Lucene 可不是 MongoDB 能存储 BSON 那样的复杂结构呀。难道像设计关系数据库的 ERM 同样,建几个索引目录当表使,而后用外键作关联,而后本身实现关联查询。或者,把整个数据序列化扔到一个字段里,本身写 Filter 、Query 来实现对复杂结构的查询?

我可不想这么费劲。

为解决这些问题,先梳理一下,Lucene 的基本字段类型有:

StringField: 基础文本字段,可指定是否索引
StoredField: 仅存储不索引(也就是不能搜索、查询只能跟着文档取出来看)
TextField  : 会在这上面应用分词器,用来作全文检索的

还有其余的 IntField,FloatField…… 能够存数字的(关键的是能够按数字值大小来排序),ByteField 存二进制数据等。还有,Lucene 支持一个字段存储多个值,当只须要一个值得时候拿一个就是了,须要多个就取多个值。

如今,我能够假定默认的状况下基础数据要能独立索引以方便查询的,他们用单独的字段存放。其余数据能够在字段名上用一个分隔符链接板块名和字段名。若是这些字段的字段名是不重复的(好比随机生成的),直接用字段名便可。这样作的好处是展示和存储分离,当一个字段的数据从A板块迁移到B板块时,不用去修改过去已经存储的数据,由于这个迁移仅仅是视觉上的迁移而已。目前我用 RDMS 实现的一套档案系统就是这么干的。

比较麻烦的是列表板块。

若是不须要对这部分的数据作查询,那就直接序列化存起来。

若是须要对里面独立的字段作搜索和排序,那就再序列化的基础上,多加一个字段独立存储要索引的字段。好比添加字段 教育经历-学校,就能够对曾就读过某个学校的档案作搜索了。

若是还想完成需求:查询某个日期范围内就读某某学校的档案,仍是另行存储吧。查询时能够用外键关联,查出一个再 IN 去查另外一个(注:Lucene没有IN的操做,须要联合使用MUST和SHOULD)。能够另外做为一个档案存在当前索引目录内,更好的方式是独立开个附属目录存储,这样作能够确保主数据更干净。

完整的存储结构为:

主要数据存储
    记录ID
    字段1:值1,值2……
    字段2……
列表数据存储
    主记录ID
    行记录ID
    序号
    字段1:值1,值2……
    字段2……
  1. 查询规则

我有一套已经应用在 RDBMS 模型上的查询规则,须要作的是将规则解析成 Lucene 的 Query。查询规则以下:

{
    "id": "xxx",       // 等于
    "star": [1, 2],    // IN, Lucene 的 Must + Should
    "f1": {
        "-gt": 18,     // 大于
        "-le": 35      // 小于或等于
    },
    "f2": {
        "-ne": "zzz"   // 不等于
    },
    "f3": {
        "-or": "zzz"   // OR, 对应 Lucene 的 Should
    },
    "f4": {
        "-ni": [3, 4]  // NOT IN, 对应 Lucene 的 Must_Not
    },
    "f5": {
        "-ai": [1, 2]  // ALL IN, 对应 Lucene 的 Must
    },
    "f6": {
        "-oi": [5, 6]  // OR IN, 对应 Lucene 的 Should
    }
}

用 application/x-form-urlencode 可表示为:

id=xxx&star[]=1&star[]=2&f1[-gt]=18&f1[-le]=35&f6[-oi][]=5&f6[-oi][]=6

系统会以相似 PHP 的请求参数解析方式解析相似上面 JSON 的数据结构。为了方便看和写,也可支持将[]换成.,如:f6.-oi.=6 与 f6[-oi][]=6 是相同的。

熟悉 MongoDB 的人看这个会很眼熟,没错,这就是从 MongoDB 借鉴过来,并用在个人关系数据库查询上。这里的 -or 和 -oi 是 Lucene 特有的,能够影响到排序,这对搜索那些无关紧要的字段颇有帮助。-ai 相似于 Mongo 的 containsAll。

注:[2015/12/01] 以上"-"已换成"!"符号。

  1. 接口规范

接口的主要目是为了传递数据,数据结构已经在上面给出。接口以 REST 风格给出,请求数据支持 application/x-form-urlencode,json,返回数据为 json。

若是你熟悉 Protobuf,也许意识到了上面的表单跟 proto 的描述很像,没错,这也是借鉴的。只是 Protobuf 无法加更多的描述,因此我没去用。这里的表单配置能够转换为 proto 描述。为便于不一样系统、不一样终端的数据交换,protobuf 也将(应当)在接口支持以内。

  1. 后注

若是不去考虑 Lucene 写锁的“问题”,我真心以为这是个至关不错的嵌入式文档数据库;虽然用 Lucene 存储复杂结构数据的可行性还有待商榷,但折腾一下对了解 Lucene 仍是有价值的。没必要强求必须用什么语言、框架或工具才能完成某件事,其实能办成一件事的途径有不少,多尝试一下思路就更清晰一点。

我在 github 上有个项目,不过尚未搭建演示,往后有了再将连接添加到这里。

部分代码:

Lucene CRUD 封装:https://github.com/ihongs/Hon...
表单校验程序:https://github.com/ihongs/Hon...
表单配置规范:https://github.com/ihongs/Hon...

参考资料:

MongoDB 查询:http://docs.mongodb.org/manua...
Lucene 查询:https://lucene.apache.org/cor...
REST 简介:http://baike.baidu.com/view/5...
PHP 请求参数解析(见第一条 Note):http://php.net/manual/zh/rese...

相关文章
相关标签/搜索