indexer.go 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400
  1. package core
  2. import (
  3. "github.com/huichen/wukong/types"
  4. "github.com/huichen/wukong/utils"
  5. "log"
  6. "math"
  7. "sync"
  8. )
  9. // 索引器
  10. type Indexer struct {
  11. // 从搜索键到文档列表的反向索引
  12. // 加了读写锁以保证读写安全
  13. tableLock struct {
  14. sync.RWMutex
  15. table map[string]*KeywordIndices
  16. }
  17. initOptions types.IndexerInitOptions
  18. initialized bool
  19. // 这实际上是总文档数的一个近似
  20. numDocuments uint64
  21. // 所有被索引文本的总关键词数
  22. totalTokenLength float32
  23. // 每个文档的关键词长度
  24. docTokenLengths map[uint64]float32
  25. }
  26. // 反向索引表的一行,收集了一个搜索键出现的所有文档,按照DocId从小到大排序。
  27. type KeywordIndices struct {
  28. // 下面的切片是否为空,取决于初始化时IndexType的值
  29. docIds []uint64 // 全部类型都有
  30. frequencies []float32 // IndexType == FrequenciesIndex
  31. locations [][]int // IndexType == LocationsIndex
  32. }
  33. // 初始化索引器
  34. func (indexer *Indexer) Init(options types.IndexerInitOptions) {
  35. if indexer.initialized == true {
  36. log.Fatal("索引器不能初始化两次")
  37. }
  38. indexer.initialized = true
  39. indexer.tableLock.table = make(map[string]*KeywordIndices)
  40. indexer.initOptions = options
  41. indexer.docTokenLengths = make(map[uint64]float32)
  42. }
  43. // 向反向索引表中加入一个文档
  44. func (indexer *Indexer) AddDocument(document *types.DocumentIndex) {
  45. if indexer.initialized == false {
  46. log.Fatal("索引器尚未初始化")
  47. }
  48. indexer.tableLock.Lock()
  49. defer indexer.tableLock.Unlock()
  50. // 更新文档关键词总长度
  51. if document.TokenLength != 0 {
  52. originalLength, found := indexer.docTokenLengths[document.DocId]
  53. indexer.docTokenLengths[document.DocId] = float32(document.TokenLength)
  54. if found {
  55. indexer.totalTokenLength += document.TokenLength - originalLength
  56. } else {
  57. indexer.totalTokenLength += document.TokenLength
  58. }
  59. }
  60. docIdIsNew := true
  61. for _, keyword := range document.Keywords {
  62. indices, foundKeyword := indexer.tableLock.table[keyword.Text]
  63. if !foundKeyword {
  64. // 如果没找到该搜索键则加入
  65. ti := KeywordIndices{}
  66. switch indexer.initOptions.IndexType {
  67. case types.LocationsIndex:
  68. ti.locations = [][]int{keyword.Starts}
  69. case types.FrequenciesIndex:
  70. ti.frequencies = []float32{keyword.Frequency}
  71. }
  72. ti.docIds = []uint64{document.DocId}
  73. indexer.tableLock.table[keyword.Text] = &ti
  74. continue
  75. }
  76. // 查找应该插入的位置
  77. position, found := indexer.searchIndex(
  78. indices, 0, indexer.getIndexLength(indices)-1, document.DocId)
  79. if found {
  80. docIdIsNew = false
  81. // 覆盖已有的索引项
  82. switch indexer.initOptions.IndexType {
  83. case types.LocationsIndex:
  84. indices.locations[position] = keyword.Starts
  85. case types.FrequenciesIndex:
  86. indices.frequencies[position] = keyword.Frequency
  87. }
  88. continue
  89. }
  90. // 当索引不存在时,插入新索引项
  91. switch indexer.initOptions.IndexType {
  92. case types.LocationsIndex:
  93. indices.locations = append(indices.locations, []int{})
  94. copy(indices.locations[position+1:], indices.locations[position:])
  95. indices.locations[position] = keyword.Starts
  96. case types.FrequenciesIndex:
  97. indices.frequencies = append(indices.frequencies, float32(0))
  98. copy(indices.frequencies[position+1:], indices.frequencies[position:])
  99. indices.frequencies[position] = keyword.Frequency
  100. }
  101. indices.docIds = append(indices.docIds, 0)
  102. copy(indices.docIds[position+1:], indices.docIds[position:])
  103. indices.docIds[position] = document.DocId
  104. }
  105. // 更新文章总数
  106. if docIdIsNew {
  107. indexer.numDocuments++
  108. }
  109. }
  110. // 查找包含全部搜索键(AND操作)的文档
  111. // 当docIds不为nil时仅从docIds指定的文档中查找
  112. func (indexer *Indexer) Lookup(
  113. tokens []string, labels []string, docIds *map[uint64]bool) (docs []types.IndexedDocument) {
  114. if indexer.initialized == false {
  115. log.Fatal("索引器尚未初始化")
  116. }
  117. if indexer.numDocuments == 0 {
  118. return
  119. }
  120. // 合并关键词和标签为搜索键
  121. keywords := make([]string, len(tokens)+len(labels))
  122. copy(keywords, tokens)
  123. copy(keywords[len(tokens):], labels)
  124. indexer.tableLock.RLock()
  125. defer indexer.tableLock.RUnlock()
  126. table := make([]*KeywordIndices, len(keywords))
  127. for i, keyword := range keywords {
  128. indices, found := indexer.tableLock.table[keyword]
  129. if !found {
  130. // 当反向索引表中无此搜索键时直接返回
  131. return
  132. } else {
  133. // 否则加入反向表中
  134. table[i] = indices
  135. }
  136. }
  137. // 当没有找到时直接返回
  138. if len(table) == 0 {
  139. return
  140. }
  141. // 归并查找各个搜索键出现文档的交集
  142. // 从后向前查保证先输出DocId较大文档
  143. indexPointers := make([]int, len(table))
  144. for iTable := 0; iTable < len(table); iTable++ {
  145. indexPointers[iTable] = indexer.getIndexLength(table[iTable]) - 1
  146. }
  147. // 平均文本关键词长度,用于计算BM25
  148. avgDocLength := indexer.totalTokenLength / float32(indexer.numDocuments)
  149. for ; indexPointers[0] >= 0; indexPointers[0]-- {
  150. // 以第一个搜索键出现的文档作为基准,并遍历其他搜索键搜索同一文档
  151. baseDocId := indexer.getDocId(table[0], indexPointers[0])
  152. if docIds != nil {
  153. _, found := (*docIds)[baseDocId]
  154. if !found {
  155. continue
  156. }
  157. }
  158. iTable := 1
  159. found := true
  160. for ; iTable < len(table); iTable++ {
  161. // 二分法比简单的顺序归并效率高,也有更高效率的算法,
  162. // 但顺序归并也许是更好的选择,考虑到将来需要用链表重新实现
  163. // 以避免反向表添加新文档时的写锁。
  164. // TODO: 进一步研究不同求交集算法的速度和可扩展性。
  165. position, foundBaseDocId := indexer.searchIndex(table[iTable],
  166. 0, indexPointers[iTable], baseDocId)
  167. if foundBaseDocId {
  168. indexPointers[iTable] = position
  169. } else {
  170. if position == 0 {
  171. // 该搜索键中所有的文档ID都比baseDocId大,因此已经没有
  172. // 继续查找的必要。
  173. return
  174. } else {
  175. // 继续下一indexPointers[0]的查找
  176. indexPointers[iTable] = position - 1
  177. found = false
  178. break
  179. }
  180. }
  181. }
  182. if found {
  183. indexedDoc := types.IndexedDocument{}
  184. // 当为LocationsIndex时计算关键词紧邻距离
  185. if indexer.initOptions.IndexType == types.LocationsIndex {
  186. // 计算有多少关键词是带有距离信息的
  187. numTokensWithLocations := 0
  188. for i, t := range table[:len(tokens)] {
  189. if len(t.locations[indexPointers[i]]) > 0 {
  190. numTokensWithLocations++
  191. }
  192. }
  193. if numTokensWithLocations != len(tokens) {
  194. docs = append(docs, types.IndexedDocument{
  195. DocId: baseDocId,
  196. })
  197. break
  198. }
  199. // 计算搜索键在文档中的紧邻距离
  200. tokenProximity, tokenLocations := computeTokenProximity(table[:len(tokens)], indexPointers, tokens)
  201. indexedDoc.TokenProximity = int32(tokenProximity)
  202. indexedDoc.TokenSnippetLocations = tokenLocations
  203. // 添加TokenLocations
  204. indexedDoc.TokenLocations = make([][]int, len(tokens))
  205. for i, t := range table[:len(tokens)] {
  206. indexedDoc.TokenLocations[i] = t.locations[indexPointers[i]]
  207. }
  208. }
  209. // 当为LocationsIndex或者FrequenciesIndex时计算BM25
  210. if indexer.initOptions.IndexType == types.LocationsIndex ||
  211. indexer.initOptions.IndexType == types.FrequenciesIndex {
  212. bm25 := float32(0)
  213. d := indexer.docTokenLengths[baseDocId]
  214. for i, t := range table[:len(tokens)] {
  215. var frequency float32
  216. if indexer.initOptions.IndexType == types.LocationsIndex {
  217. frequency = float32(len(t.locations[indexPointers[i]]))
  218. } else {
  219. frequency = t.frequencies[indexPointers[i]]
  220. }
  221. // 计算BM25
  222. if len(t.docIds) > 0 && frequency > 0 && indexer.initOptions.BM25Parameters != nil && avgDocLength != 0 {
  223. // 带平滑的idf
  224. idf := float32(math.Log2(float64(indexer.numDocuments)/float64(len(t.docIds)) + 1))
  225. k1 := indexer.initOptions.BM25Parameters.K1
  226. b := indexer.initOptions.BM25Parameters.B
  227. bm25 += idf * frequency * (k1 + 1) / (frequency + k1*(1-b+b*d/avgDocLength))
  228. }
  229. }
  230. indexedDoc.BM25 = float32(bm25)
  231. }
  232. indexedDoc.DocId = baseDocId
  233. docs = append(docs, indexedDoc)
  234. }
  235. }
  236. return
  237. }
  238. // 二分法查找indices中某文档的索引项
  239. // 第一个返回参数为找到的位置或需要插入的位置
  240. // 第二个返回参数标明是否找到
  241. func (indexer *Indexer) searchIndex(
  242. indices *KeywordIndices, start int, end int, docId uint64) (int, bool) {
  243. // 特殊情况
  244. if indexer.getIndexLength(indices) == start {
  245. return start, false
  246. }
  247. if docId < indexer.getDocId(indices, start) {
  248. return start, false
  249. } else if docId == indexer.getDocId(indices, start) {
  250. return start, true
  251. }
  252. if docId > indexer.getDocId(indices, end) {
  253. return end + 1, false
  254. } else if docId == indexer.getDocId(indices, end) {
  255. return end, true
  256. }
  257. // 二分
  258. var middle int
  259. for end-start > 1 {
  260. middle = (start + end) / 2
  261. if docId == indexer.getDocId(indices, middle) {
  262. return middle, true
  263. } else if docId > indexer.getDocId(indices, middle) {
  264. start = middle
  265. } else {
  266. end = middle
  267. }
  268. }
  269. return end, false
  270. }
  271. // 计算搜索键在文本中的紧邻距离
  272. //
  273. // 假定第 i 个搜索键首字节出现在文本中的位置为 P_i,长度 L_i
  274. // 紧邻距离计算公式为
  275. //
  276. // ArgMin(Sum(Abs(P_(i+1) - P_i - L_i)))
  277. //
  278. // 具体由动态规划实现,依次计算前 i 个 token 在每个出现位置的最优值。
  279. // 选定的 P_i 通过 tokenLocations 参数传回。
  280. func computeTokenProximity(table []*KeywordIndices, indexPointers []int, tokens []string) (
  281. minTokenProximity int, tokenLocations []int) {
  282. minTokenProximity = -1
  283. tokenLocations = make([]int, len(tokens))
  284. var (
  285. currentLocations, nextLocations []int
  286. currentMinValues, nextMinValues []int
  287. path [][]int
  288. )
  289. // 初始化路径数组
  290. path = make([][]int, len(tokens))
  291. for i := 1; i < len(path); i++ {
  292. path[i] = make([]int, len(table[i].locations[indexPointers[i]]))
  293. }
  294. // 动态规划
  295. currentLocations = table[0].locations[indexPointers[0]]
  296. currentMinValues = make([]int, len(currentLocations))
  297. for i := 1; i < len(tokens); i++ {
  298. nextLocations = table[i].locations[indexPointers[i]]
  299. nextMinValues = make([]int, len(nextLocations))
  300. for j, _ := range nextMinValues {
  301. nextMinValues[j] = -1
  302. }
  303. var iNext int
  304. for iCurrent, currentLocation := range currentLocations {
  305. if currentMinValues[iCurrent] == -1 {
  306. continue
  307. }
  308. for iNext+1 < len(nextLocations) && nextLocations[iNext+1] < currentLocation {
  309. iNext++
  310. }
  311. update := func(from int, to int) {
  312. if to >= len(nextLocations) {
  313. return
  314. }
  315. value := currentMinValues[from] + utils.AbsInt(nextLocations[to]-currentLocations[from]-len(tokens[i-1]))
  316. if nextMinValues[to] == -1 || value < nextMinValues[to] {
  317. nextMinValues[to] = value
  318. path[i][to] = from
  319. }
  320. }
  321. // 最优解的状态转移只发生在左右最接近的位置
  322. update(iCurrent, iNext)
  323. update(iCurrent, iNext+1)
  324. }
  325. currentLocations = nextLocations
  326. currentMinValues = nextMinValues
  327. }
  328. // 找出最优解
  329. var cursor int
  330. for i, value := range currentMinValues {
  331. if value == -1 {
  332. continue
  333. }
  334. if minTokenProximity == -1 || value < minTokenProximity {
  335. minTokenProximity = value
  336. cursor = i
  337. }
  338. }
  339. // 从路径倒推出最优解的位置
  340. for i := len(tokens) - 1; i >= 0; i-- {
  341. if i != len(tokens)-1 {
  342. cursor = path[i+1][cursor]
  343. }
  344. tokenLocations[i] = table[i].locations[indexPointers[i]][cursor]
  345. }
  346. return
  347. }
  348. // 从KeywordIndices中得到第i个文档的DocId
  349. func (indexer *Indexer) getDocId(ti *KeywordIndices, i int) uint64 {
  350. return ti.docIds[i]
  351. }
  352. // 得到KeywordIndices中文档总数
  353. func (indexer *Indexer) getIndexLength(ti *KeywordIndices) int {
  354. return len(ti.docIds)
  355. }