Bleve程式碼閱讀(一)——新建索引
引言
Bleve是Golang實現的一個全文檢索庫,類似Lucene之於Java。在這裡通過閱讀其程式碼,來學習如何使用及定製檢索功能。也是為了通過閱讀程式碼,學習在具體環境下Golang的一些使用方式。程式碼的路徑在github上https://github.com/blevesearch/bleve。
1 新建索引
下面的程式碼摘自Bleve的"Hello World"示例。
// open a new index mapping := bleve.NewIndexMapping() index, err := bleve.New("example.bleve", mapping) if err != nil { fmt.Println(err) return }
1.1和1.2兩節是對上面邏輯的展開介紹,1.3節是在閱讀程式碼中遇到的一些Golang特性的介紹,1.4節是當我們在使用bleve新建索引時可能會怎麼做。
1.1 新建一個IndexMapping
下面這段程式碼是Bleve的"Hello World"示例的第一條語句,表示開啟一個新索引。
// open a new index
mapping := bleve.NewIndexMapping()
這個函式的定義位於bleve目錄下的mapping.go檔案
func NewIndexMapping() *mapping.IndexMappingImpl { return mapping.NewIndexMapping() }
可以看出它是一個封裝函式,呼叫了mapping package的NewIndexMapping函式。該函式的定義位於bleve/mapping/index.go檔案內,屬於github.com/blevesearch/bleve/mapping包。
// NewIndexMapping creates a new IndexMapping that will use all the default indexing rules func NewIndexMapping() *IndexMappingImpl { return &IndexMappingImpl{ TypeMapping: make(map[string]*DocumentMapping), DefaultMapping: NewDocumentMapping(), TypeField: defaultTypeField, DefaultType: defaultType, DefaultAnalyzer: defaultAnalyzer, DefaultDateTimeParser: defaultDateTimeParser, DefaultField: defaultField, IndexDynamic: IndexDynamic, StoreDynamic: StoreDynamic, DocValuesDynamic: DocValuesDynamic, CustomAnalysis: newCustomAnalysis(), cache: registry.NewCache(), } }
建立了一個IndexMappingImpl結構並返回其指標,該IndexMappingImpl的所有成員均以預設方式初始化。接下來看一下IndexMappingImpl結構的定義,該結構同樣屬於github.com/blevesearch/bleve/mapping包,並位於bleve/mapping/index.go檔案內。
// An IndexMappingImpl controls how objects are placed
// into an index.
// First the type of the object is determined.
// Once the type is know, the appropriate
// DocumentMapping is selected by the type.
// If no mapping was determined for that type,
// a DefaultMapping will be used.
type IndexMappingImpl struct {
TypeMapping map[string]*DocumentMapping `json:"types,omitempty"`
DefaultMapping *DocumentMapping `json:"default_mapping"`
TypeField string `json:"type_field"`
DefaultType string `json:"default_type"`
DefaultAnalyzer string `json:"default_analyzer"`
DefaultDateTimeParser string `json:"default_datetime_parser"`
DefaultField string `json:"default_field"`
StoreDynamic bool `json:"store_dynamic"`
IndexDynamic bool `json:"index_dynamic"`
DocValuesDynamic bool `json:"docvalues_dynamic,omitempty"`
CustomAnalysis *customAnalysis `json:"analysis,omitempty"`
cache *registry.Cache
}
從註釋可以看出,IndexMappingImpl結構是用來控制每一個物件應該被如何放入Index中,其各欄位也是圍繞這個目標展開的。
TypeMapping :一個map型別,Key是string型別,表示文件的型別。Value是DocumentMapping型別的指標,表示該型別文件對應的DocumentMapping。
DefaultMapping:一個DocumentMapping型別的指標。當文件的型別未知時,使用的預設DocumentMapping。
函式bleve.NewIndexMapping()
僅僅返回了一個結構,這個結構用來控制所有文件應該被如何建立索引。
1.2 新建一個索引檔案
index, err := bleve.New("example.bleve", mapping)
示例的第二條語句,將IndexMappingImpl結構與一個具體路徑結合起來,新建一個索引。這個函式定義在bleve/index.go檔案內,在bleve包內。
// New index at the specified path, must not exist.
// The provided mapping will be used for all
// Index/Search operations.
func New(path string, mapping mapping.IndexMapping) (Index, error) {
return newIndexUsing(path, mapping, Config.DefaultIndexType, Config.DefaultKVStore, nil)
}
這個函式呼叫了newIndexUsing函式,前兩個引數就是New函式傳進來的,第3個和第4個引數通過 Config變數給出。接下來的主要邏輯都在newIndexUsing函式實現,本節剩餘部分都在newIndexUsing函式內部討論。該函式位於index_impl.go檔案內,在bleve包裡。
首先,總體說一下newIndexUsing函式的功能。校驗引數的合法性,根據引數從物理層面開啟一個具體型別的index,儲存元資料,關聯統計模組。
err := mapping.Validate()
if err != nil {
return nil, err
}
校驗indexMapping的合法性。具體在bleve/mapping.go中實現,主要是檢查analyzer和DocumentMapping可以被建立。
if kvconfig == nil {
kvconfig = map[string]interface{}{}
}
if kvstore == "" {
return nil, fmt.Errorf("bleve not configured for file based indexing")
}
必須給出kv儲存的具體方式,預設是用boltdb,一個golang實現的kv儲存。
rv := indexImpl{
path: path,
name: path,
m: mapping,
meta: newIndexMeta(indexType, kvstore, kvconfig),
}
rv.stats = &IndexStat{i: &rv}
初始化一個indexImpl結構,注意此結構還不是具體的index,而是包含index及其meta資料,還有統計資訊的一個結構。
if path != "" {
err = rv.meta.Save(path)
if err != nil {
return nil, err
}
kvconfig["create_if_missing"] = true
kvconfig["error_if_exists"] = true
kvconfig["path"] = indexStorePath(path)
} else {
kvconfig["path"] = ""
}
儲存索引的meta資料,這個meta資料只是包含了index的具體型別,底層kv儲存的具體型別。index的元資料如mapping不在這裡儲存。
indexTypeConstructor := registry.IndexTypeConstructorByName(rv.meta.IndexType)
if indexTypeConstructor == nil {
return nil, ErrorUnknownIndexType
}
獲取index的Constructor,index型別預設是upsitedown。這個Constructor是一個函式,是在upsidedown包的init函式初始化設定的。在bleve/registry/index_type.go檔案中定義。
rv.i, err = indexTypeConstructor(rv.meta.Storage, kvconfig, Config.analysisQueue)
if err != nil {
return nil, err
}
呼叫Index的Constructor,如果是upsidedown,函式是upsidedown.go檔案的NewUpsideDownCouch函式。返回一個index.Index介面,如果是upsidedown,則也是UpsideDownCouch結構。
err = rv.i.Open()
if err != nil {
if err == index.ErrorUnknownStorageType {
return nil, ErrorUnknownStorageType
}
return nil, err
}
開啟上一步建立的index.Index介面。包括開啟kv儲存,初始化reader等,具體細節沒有往下深究。
mappingBytes, err := json.Marshal(mapping)
if err != nil {
return nil, err
}
err = rv.i.SetInternal(mappingInternalKey, mappingBytes)
if err != nil {
return nil, err
}
將indexMapping序列化後儲存至剛開啟的index。
indexStats.Register(&rv)
註冊index的統計資訊。
1.3 相關Golang特性說明
1.3.1 init()函式
這裡要額外說一下1.2中Config這個變數。通過查詢,可以看到Config是一個package內的全域性變數。
var Config *configuration
然而這個變數是一個指向configuration結構的指標,它是通過init()函式初始化的。
func init() {
bootStart := time.Now()
// build the default configuration
Config = newConfiguration()
// set the default highlighter
Config.DefaultHighlighter = html.Name
// default kv store
Config.DefaultKVStore = ""
// default mem only kv store
Config.DefaultMemKVStore = gtreap.Name
// default index
Config.DefaultIndexType = upsidedown.Name
bootDuration := time.Since(bootStart)
bleveExpVar.Add("bootDuration", int64(bootDuration))
indexStats = NewIndexStats()
bleveExpVar.Set("indexes", indexStats)
initDisk()
}
對於init函式的解釋,來自知乎五分鐘理解golang的init函式。init()函式是Golang的一個特性,它先於main函式執行。init函式的主要作用:
- 初始化不能採用初始化表示式初始化的變數。
- 程式執行前的註冊。
- 實現sync.Once功能。
- 其他
init函式的主要特點有:
- init函式先於main函式自動執行,不能被其他函式呼叫;
- init函式沒有輸入引數、返回值;
- 每個包可以有多個init函式;
- 包的每個原始檔也可以有多個init函式,這點比較特殊;
- 同一個包的init執行順序,golang沒有明確定義,程式設計時要注意程式不要依賴這個執行順序。
- 不同包的init函式按照包匯入的依賴關係決定執行順序。
golang程式初始化:
- 初始化匯入的包(包的初始化順序並不是按匯入順序(“從上到下”)執行的,runtime需要解析包依賴關係,沒有依賴的包最先初始化,與變數初始化依賴關係類似;
- 初始化包作用域的變數;
- 執行包的init函式。
1.3.2 struct型別的tag
在1.1節IndexMappingImpl結構體定義中,我們看到定義結構體的每個成員時用到了三個欄位,前兩個欄位是成員變數名和型別,第三個欄位就是struct的Tag。
TypeMapping map[string]*DocumentMapping `json:"types,omitempty"`
DefaultMapping *DocumentMapping `json:"default_mapping"`
struct的Tag是用雙引號或反向單引號括起來的字串,可以通過reflect包來訪問和獲取。
// ojb是indexMappingImpl結構的一個例項
t := reflect.TypeOf(obj)
for i := 0; i < t.NumField(); i++ {
if t.Field(i).CanInterface(){
fmt.Printf("%s %s = %v -tag:%s \n",
t.Field(i).Name,
t.Field(i).Type,
v.Field(i).Interface(),
t.Field(i).Tag)
}
}
在我們的例子中,Tag的內容是json:後跟一個雙引號括起來的value列表,這表示json在marshel這個結構體是,對應的成員應該如何處理。json:"default_mapping"
這個tag,表示json在marshel這個結構體時,該成員應該以default_mapping為key,unmarshel時遇到default_mapping這個key,也會解碼到對應的成員。json:"types,omitempty"
第二個引數omitempty的含義是,當該成員為empty值(0、nil等)時忽略該成員。
其他的例子還包括bison和protobuf,功能類似。
type User struct {
Name string `json:"name,omitempty" bson:"name,omitempty" protobuf:"1"`
Secret string `json:"-,omitempty" bson:"secret,omitempty" protobuf:"2"`
}
1.4 實踐落地
在實踐中,如果我們要使用bleve,肯定會新建一個索引。最簡單的新建索引的方式,已經在本文最開頭的程式碼中給出。
mapping := bleve.NewIndexMapping()
index, err := bleve.New("example.bleve", mapping)
if err != nil {
fmt.Println(err)
return
}
但是,實際使用中我們可能會對不同的域有不同的檢索需求,還可能會使用不同的Analyzer。根據不同的個性化需求,我們需要使用更具體的介面來進行初始化,目前對這一塊還不是特別瞭解。但是,總體來說新建一個index應該包含上步。第一,建立一個IndexMapping然後個性化配置;第二,在一個檔案上開啟索引檔案,這一步可能需要使用一些更具體的介面來進行配置。