上文中已經分析了索引的一部分,接下來將繼續學習索引的部分;web
// 歸併查找各個搜索鍵出現文檔的交集 // 從後向前查保證先輸出DocId較大文檔 indexPointers := make([]int, len(table)) for iTable := 0; iTable < len(table); iTable++ { indexPointers[iTable] = indexer.getIndexLength(table[iTable]) - 1 } // 平均文本關鍵詞長度,用於計算BM25 avgDocLength := indexer.totalTokenLength / float32(indexer.numDocuments) for ; indexPointers[0] >= 0; indexPointers[0]-- { // 以第一個搜索鍵出現的文檔做爲基準,並遍歷其餘搜索鍵搜索同一文檔 baseDocId := indexer.getDocId(table[0], indexPointers[0]) if docIds != nil { _, found := (*docIds)[baseDocId] if !found { continue } }
本段代碼中indexPointers相似指針並指向某個搜索鍵的對應文檔索引項,同時在後續搜索引擎的搜索環節的排序自評分階段將用到BM25(用於評比關鍵詞和文檔的相關狀況),因此BM25將在搜索排序階段給予詳細的公式講解;算法
iTable := 1 found := true for ; iTable < len(table); iTable++ { // 二分法比簡單的順序歸併效率高,也有更高效率的算法, // 但順序歸併也許是更好的選擇,考慮到未來須要用鏈表從新實現 // 以免反向表添加新文檔時的寫鎖。 // TODO: 進一步研究不一樣求交集算法的速度和可擴展性。 position, foundBaseDocId := indexer.searchIndex(table[iTable], 0, indexPointers[iTable], baseDocId) if foundBaseDocId { indexPointers[iTable] = position } else { if position == 0 { // 該搜索鍵中全部的文檔ID都比baseDocId大,所以已經沒有 // 繼續查找的必要。 return } else { // 繼續下一indexPointers[0]的查找 indexPointers[iTable] = position - 1 found = false break } }
本段代碼沒有采用二分法算法進行查找DocId,就如註釋說的那樣,未來如採用鏈表實現,因此用順序歸併進行查找,相關網上有這方面的關於索引的二分法的查找,有興趣的學習者,能夠自行進行嘗試實現;這裏是以baseDocId爲首選項進行查找的;數組
if found { indexedDoc := types.IndexedDocument{} // 當爲LocationsIndex時計算關鍵詞緊鄰距離 if indexer.initOptions.IndexType == types.LocationsIndex { // 計算有多少關鍵詞是帶有距離信息的 numTokensWithLocations := 0 for i, t := range table[:len(tokens)] { if len(t.locations[indexPointers[i]]) > 0 { numTokensWithLocations++ } } if numTokensWithLocations != len(tokens) { docs = append(docs, types.IndexedDocument{ DocId: baseDocId, }) break } // 計算搜索鍵在文檔中的緊鄰距離 tokenProximity, tokenLocations := computeTokenProximity(table[:len(tokens)], indexPointers, tokens) indexedDoc.TokenProximity = int32(tokenProximity) indexedDoc.TokenSnippetLocations = tokenLocations // 添加TokenLocations indexedDoc.TokenLocations = make([][]int, len(tokens)) for i, t := range table[:len(tokens)] { indexedDoc.TokenLocations[i] = t.locations[indexPointers[i]] } }
所謂關鍵詞緊鄰距離是一種衡量文檔和多個關鍵詞相關度的方法。緊鄰距離不能做爲給文檔排序的惟一指標,可是能夠經過閥值能夠過濾掉一部分的無關的結果;運用computeTokenProximity函數進行緊鄰距離的計算;安全
// 當爲LocationsIndex或者FrequenciesIndex時計算BM25 if indexer.initOptions.IndexType == types.LocationsIndex || indexer.initOptions.IndexType == types.FrequenciesIndex { bm25 := float32(0) d := indexer.docTokenLengths[baseDocId] for i, t := range table[:len(tokens)] { var frequency float32 if indexer.initOptions.IndexType == types.LocationsIndex { frequency = float32(len(t.locations[indexPointers[i]])) } else { frequency = t.frequencies[indexPointers[i]] } // 計算BM25 if len(t.docIds) > 0 && frequency > 0 && indexer.initOptions.BM25Parameters != nil && avgDocLength != 0 { // 帶平滑的idf idf := float32(math.Log2(float64(indexer.numDocuments)/float64(len(t.docIds)) + 1)) k1 := indexer.initOptions.BM25Parameters.K1 b := indexer.initOptions.BM25Parameters.B bm25 += idf * frequency * (k1 + 1) / (frequency + k1*(1-b+b*d/avgDocLength)) } } indexedDoc.BM25 = float32(bm25) } indexedDoc.DocId = baseDocId docs = append(docs, indexedDoc) } } return
計算BM25須要indexer.initOptions.IndexType = LocationsIndex或者FrequenciesIndex類型,同時本段代碼運用BM25計算公式:app
IDF * TF * (k1 + 1) BM25 = sum ---------------------------- TF + k1 * (1 - b + b * D / L)
其中sum對全部關鍵詞求和,TF(term frequency)爲某關鍵詞在該文檔中出現的詞頻,D爲該文檔的詞數,L爲全部文檔的平均詞數,k1和b爲常數,在悟空裏默認值爲2.0和0.75,不過能夠在引擎初始化的時候IDF(inverse document frequency)衡量關鍵詞是否常見,悟空引擎使用帶平滑的IDF公式函數
總文檔數目 IDF = log2( ------------------------ + 1 ) 出現該關鍵詞的文檔數目
// 二分法查找indices中某文檔的索引項// 第一個返回參數爲找到的位置或須要插入的位置 // 第二個返回參數標明是否找到 func (indexer *Indexer) searchIndex( indices *KeywordIndices, start int, end int, docId uint64) (int, bool) { // 特殊狀況 if indexer.getIndexLength(indices) == start { return start, false } if docId < indexer.getDocId(indices, start) { return start, false } else if docId == indexer.getDocId(indices, start) { return start, true } if docId > indexer.getDocId(indices, end) { return end + 1, false } else if docId == indexer.getDocId(indices, end) { return end, true } // 二分 var middle int for end-start > 1 { middle = (start + end) / 2 if docId == indexer.getDocId(indices, middle) { return middle, true } else if docId > indexer.getDocId(indices, middle) { start = middle } else { end = middle } } return end, false }
本段代碼中使用了二分法進行索引項的查找,上面咱們講述了用順序歸併法的例子,在索引器type Indexer struct {}定義中,爲了反向索引讀寫的安全,加了讀寫鎖sync.RWMutex; 學習
// 假定第 i 個搜索鍵首字節出如今文本中的位置爲 P_i,長度 L_i// 緊鄰距離計算公式爲 // // ArgMin(Sum(Abs(P_(i+1) - P_i - L_i))) // // 具體由動態規劃實現,依次計算前 i 個 token 在每一個出現位置的最優值。 // 選定的 P_i 經過 tokenLocations 參數傳回。 func computeTokenProximity(table []*KeywordIndices, indexPointers []int, tokens []string) ( minTokenProximity int, tokenLocations []int) { minTokenProximity = -1 tokenLocations = make([]int, len(tokens)) var ( currentLocations, nextLocations []int currentMinValues, nextMinValues []int path [][]int ) // 初始化路徑數組 path = make([][]int, len(tokens)) for i := 1; i < len(path); i++ { path[i] = make([]int, len(table[i].locations[indexPointers[i]])) } // 動態規劃 currentLocations = table[0].locations[indexPointers[0]] currentMinValues = make([]int, len(currentLocations)) for i := 1; i < len(tokens); i++ { nextLocations = table[i].locations[indexPointers[i]] nextMinValues = make([]int, len(nextLocations)) for j, _ := range nextMinValues { nextMinValues[j] = -1 } var iNext int for iCurrent, currentLocation := range currentLocations { if currentMinValues[iCurrent] == -1 { continue } for iNext+1 < len(nextLocations) && nextLocations[iNext+1] < currentLocation { iNext++ } update := func(from int, to int) { if to >= len(nextLocations) { return } value := currentMinValues[from] + utils.AbsInt(nextLocations[to]-currentLocations[from]-len(tokens[i-1])) if nextMinValues[to] == -1 || value < nextMinValues[to] { nextMinValues[to] = value path[i][to] = from } } // 最優解的狀態轉移只發生在左右最接近的位置 update(iCurrent, iNext) update(iCurrent, iNext+1) } currentLocations = nextLocations currentMinValues = nextMinValues } // 找出最優解 var cursor int for i, value := range currentMinValues { if value == -1 { continue } if minTokenProximity == -1 || value < minTokenProximity { minTokenProximity = value cursor = i } } // 從路徑倒推出最優解的位置 for i := len(tokens) - 1; i >= 0; i-- { if i != len(tokens)-1 { cursor = path[i+1][cursor] } tokenLocations[i] = table[i].locations[indexPointers[i]][cursor] } return }
本段代碼中關於minTokenProximity = -1,nextMinValues[j] = -1 對這個賦值,我難ui
以理解,同時難以理解的是爲何要進行update?功能沒有理解明白;搜索引擎
update := func(from int, to int) { if to >= len(nextLocations) { return } value := currentMinValues[from] + utils.AbsInt(nextLocations[to]-currentLocations[from]-len(tokens[i-1])) if nextMinValues[to] == -1 || value < nextMinValues[to] { nextMinValues[to] = value path[i][to] = from } }
沒看懂這個路徑組在推出最有路徑組方面的有何聯繫?spa
// 初始化路徑數組 path = make([][]int, len(tokens)) for i := 1; i < len(path); i++ { path[i] = make([]int, len(table[i].locations[indexPointers[i]])) }
根據公式,能夠比較理解,就是計算文本中多個搜索鍵之間的最小離,特別是首先以其中一個搜索鍵爲基準前提,逐次計算進行求和;
// 從KeywordIndices中獲得第i個文檔的DocIdfunc (indexer *Indexer) getDocId(ti *KeywordIndices, i int) uint64 { return ti.docIds[i] } // 獲得KeywordIndices中文檔總數 func (indexer *Indexer) getIndexLength(ti *KeywordIndices) int { return len(ti.docIds) }
這段代碼是進行的函數定義,是獲得索引器裏面的文檔參數;
總結:
本文是後續的索引部分,相對而言比較難以理解,須要結合搜索排序部分,用到的索引方法比較多,索引的分詞部分,將在單獨的sego項目講解到,利用了分詞字典環節,索引部分還容許用戶繞過悟空內置的分詞器直接進行輸入文檔關鍵詞,從而使得引擎外部分詞成爲可能;本文中出現一些比較重要和使用頻繁的概念,好比:table數組,keyWordIndices,searchIndex等等,因此瞭解索引部分,瞭解概念內涵及其之間的聯繫是相當重要的;同時本文用到的公式比較多,這主要爲後續的搜索的排序打分作鋪墊的,在後面的學習中將要說起; 本文最後說起的不理解的部分,暫時先進行擱置,在分析後面的部分再繼續跟蹤,也許也找到聯繫進而會有所悟;若是有了解的和懂的,能夠在評論中給予指出;