indexer.go 12 KB

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