用 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...

相關文章
相關標籤/搜索