说到“档案”系统,选文档数据库再合适不过了。谈到文档数据库一般想到的是 MongoDB、CouchDB 之类的,可这里要说的不是这些,而是另一个 NoSQL “文档数据库” —— Lucene。之所以要打引号,是因为暂时还没听到别人这样说。
需求
最近公司要弄一个内部搜索,对比各种方案后,决定用 Lucene。当做出第一个原型后,考虑到公司另外几个项目将来也许用的上,而再写一遍代码可不是我的风格;又试用了开箱即用的 Solr,觉得那也不是我的菜。因为我项目内已经有类似 Solr 的 Schame 的配置在用了,我打算复用这个模块;接口规范我也打算复用我现有的规范。
基础的增删改查比较简单,很快就做出了原型。此时我想到公司另一个大模块:档案(或叫简历)。这部分我已计划与另一个项目的类似模块做整合,考虑用 MongoDB 重构。既然 Lucene 可以存取较复杂的数据结构,何不借此机会研究一下用 Lucene 作为档案系统的底层支撑呢。
那这里说的档案是什么样子呢?举一个简单例子,一份个人简历:
姓名:XXX
性别:男
照片:xxx/xxx.jpg
兴趣爱好
兴趣:跑步、游泳、XX自定义
简介:是浪费时间的服务吉林省地方就,受到法律书籍地方
教育经历
经历1
日期区间: 2014/1/1~2015/1/1
学校: Jiali.Dun
专业: 挖掘机
学位:没士
经历2……
大概的文档结构就是就是这样,字段、层级是不确定的,需要保持此结构,能存、能取,大部分字段可查询、排序。
结构化数据
总结以上档案结构,组成上可分为:
a. 基础板块(名字,性别,照片)
b. 其他板块(同上,但被区分开)
c. 列表板块(教育经历)
上面特意将基础信息称为基础“板块”,也就是说,一般情况下一份档案是由多个板块组成的。也许您的档案还会更复杂,比如兴趣爱好下再分为运动、娱乐,这种划分方式从存储上来说与两层设计没什么区别,多了一个父级板块的指向而已,但这增加了展现的复杂度。现在大家都在谈“扁平化”,我所理解的扁平不仅仅是把图标拍扁了,更是信息获取的渠道扁平了,能一下给我看的,不要让我点一层菜单进去又点一层;能用标签、搜索筛选的,不要让我点目录树查找。
一个板块就是一组键值对,此处我们将这一组规则称为表单。那么,列表板块就是由多个可重复表单组成的板块。
字段上可以有:
a. 文本
b. 数字
c. 文件
d. 日期、时间(区间)
e. 单选、多选
f. 多条数据(文本、数字、日期等)
从 a~e 都是很常见的类型,文件可以转储到文件服务器上,这里只存 URL;日期、时间可以转换成时间戳。而 f 是指这个字段的值可以输入多个,通常用来记录一些需要多条记录东西,存储上与多选一样。
Lucene 原本就是一个字段可以存多个值,这太妙了。
表单及验证
前面谈到我自己有一个数据校验模块,对数据结构的描述如下:
表单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 的请求参数解析方式。
存储方式
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……
查询规则
我有一套已经应用在 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] 以上”-“已换成”!”符号。
接口规范
接口的主要目是为了传递数据,数据结构已经在上面给出。接口以 REST 风格给出,请求数据支持 application/x-form-urlencode,json,返回数据为 json。
如果你熟悉 Protobuf,也许意识到了上面的表单跟 proto 的描述很像,没错,这也是借鉴的。只是 Protobuf 没法加更多的描述,所以我没去用。这里的表单配置可以转换为 proto 描述。为便于不同系统、不同终端的数据交换,protobuf 也将(应当)在接口支持之内。
后注
如果不去考虑 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…