1. 程式人生 > >Bleve程式碼閱讀(一)——新建索引

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程式初始化:

  1. 初始化匯入的包(包的初始化順序並不是按匯入順序(“從上到下”)執行的,runtime需要解析包依賴關係,沒有依賴的包最先初始化,與變數初始化依賴關係類似;
  2. 初始化包作用域的變數;
  3. 執行包的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然後個性化配置;第二,在一個檔案上開啟索引檔案,這一步可能需要使用一些更具體的介面來進行配置。