You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

bleve.go 9.5KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345
  1. // Copyright 2019 The Gitea Authors. All rights reserved.
  2. // Use of this source code is governed by a MIT-style
  3. // license that can be found in the LICENSE file.
  4. package code
  5. import (
  6. "fmt"
  7. "os"
  8. "strconv"
  9. "strings"
  10. "code.gitea.io/gitea/models"
  11. "code.gitea.io/gitea/modules/base"
  12. "code.gitea.io/gitea/modules/charset"
  13. "code.gitea.io/gitea/modules/git"
  14. "code.gitea.io/gitea/modules/log"
  15. "code.gitea.io/gitea/modules/setting"
  16. "github.com/blevesearch/bleve"
  17. "github.com/blevesearch/bleve/analysis/analyzer/custom"
  18. "github.com/blevesearch/bleve/analysis/token/lowercase"
  19. "github.com/blevesearch/bleve/analysis/token/unicodenorm"
  20. "github.com/blevesearch/bleve/analysis/tokenizer/unicode"
  21. "github.com/blevesearch/bleve/index/upsidedown"
  22. "github.com/blevesearch/bleve/mapping"
  23. "github.com/blevesearch/bleve/search/query"
  24. "github.com/ethantkoenig/rupture"
  25. )
  26. const unicodeNormalizeName = "unicodeNormalize"
  27. const maxBatchSize = 16
  28. // indexerID a bleve-compatible unique identifier for an integer id
  29. func indexerID(id int64) string {
  30. return strconv.FormatInt(id, 36)
  31. }
  32. // numericEqualityQuery a numeric equality query for the given value and field
  33. func numericEqualityQuery(value int64, field string) *query.NumericRangeQuery {
  34. f := float64(value)
  35. tru := true
  36. q := bleve.NewNumericRangeInclusiveQuery(&f, &f, &tru, &tru)
  37. q.SetField(field)
  38. return q
  39. }
  40. func addUnicodeNormalizeTokenFilter(m *mapping.IndexMappingImpl) error {
  41. return m.AddCustomTokenFilter(unicodeNormalizeName, map[string]interface{}{
  42. "type": unicodenorm.Name,
  43. "form": unicodenorm.NFC,
  44. })
  45. }
  46. // openIndexer open the index at the specified path, checking for metadata
  47. // updates and bleve version updates. If index needs to be created (or
  48. // re-created), returns (nil, nil)
  49. func openIndexer(path string, latestVersion int) (bleve.Index, error) {
  50. _, err := os.Stat(path)
  51. if err != nil && os.IsNotExist(err) {
  52. return nil, nil
  53. } else if err != nil {
  54. return nil, err
  55. }
  56. metadata, err := rupture.ReadIndexMetadata(path)
  57. if err != nil {
  58. return nil, err
  59. }
  60. if metadata.Version < latestVersion {
  61. // the indexer is using a previous version, so we should delete it and
  62. // re-populate
  63. return nil, os.RemoveAll(path)
  64. }
  65. index, err := bleve.Open(path)
  66. if err != nil && err == upsidedown.IncompatibleVersion {
  67. // the indexer was built with a previous version of bleve, so we should
  68. // delete it and re-populate
  69. return nil, os.RemoveAll(path)
  70. } else if err != nil {
  71. return nil, err
  72. }
  73. return index, nil
  74. }
  75. // RepoIndexerData data stored in the repo indexer
  76. type RepoIndexerData struct {
  77. RepoID int64
  78. Content string
  79. }
  80. // Type returns the document type, for bleve's mapping.Classifier interface.
  81. func (d *RepoIndexerData) Type() string {
  82. return repoIndexerDocType
  83. }
  84. func addUpdate(update fileUpdate, repo *models.Repository, batch rupture.FlushingBatch) error {
  85. stdout, err := git.NewCommand("cat-file", "-s", update.BlobSha).
  86. RunInDir(repo.RepoPath())
  87. if err != nil {
  88. return err
  89. }
  90. if size, err := strconv.Atoi(strings.TrimSpace(stdout)); err != nil {
  91. return fmt.Errorf("Misformatted git cat-file output: %v", err)
  92. } else if int64(size) > setting.Indexer.MaxIndexerFileSize {
  93. return addDelete(update.Filename, repo, batch)
  94. }
  95. fileContents, err := git.NewCommand("cat-file", "blob", update.BlobSha).
  96. RunInDirBytes(repo.RepoPath())
  97. if err != nil {
  98. return err
  99. } else if !base.IsTextFile(fileContents) {
  100. // FIXME: UTF-16 files will probably fail here
  101. return nil
  102. }
  103. id := filenameIndexerID(repo.ID, update.Filename)
  104. return batch.Index(id, &RepoIndexerData{
  105. RepoID: repo.ID,
  106. Content: string(charset.ToUTF8DropErrors(fileContents)),
  107. })
  108. }
  109. func addDelete(filename string, repo *models.Repository, batch rupture.FlushingBatch) error {
  110. id := filenameIndexerID(repo.ID, filename)
  111. return batch.Delete(id)
  112. }
  113. const (
  114. repoIndexerAnalyzer = "repoIndexerAnalyzer"
  115. repoIndexerDocType = "repoIndexerDocType"
  116. repoIndexerLatestVersion = 4
  117. )
  118. // createRepoIndexer create a repo indexer if one does not already exist
  119. func createRepoIndexer(path string, latestVersion int) (bleve.Index, error) {
  120. docMapping := bleve.NewDocumentMapping()
  121. numericFieldMapping := bleve.NewNumericFieldMapping()
  122. numericFieldMapping.IncludeInAll = false
  123. docMapping.AddFieldMappingsAt("RepoID", numericFieldMapping)
  124. textFieldMapping := bleve.NewTextFieldMapping()
  125. textFieldMapping.IncludeInAll = false
  126. docMapping.AddFieldMappingsAt("Content", textFieldMapping)
  127. mapping := bleve.NewIndexMapping()
  128. if err := addUnicodeNormalizeTokenFilter(mapping); err != nil {
  129. return nil, err
  130. } else if err := mapping.AddCustomAnalyzer(repoIndexerAnalyzer, map[string]interface{}{
  131. "type": custom.Name,
  132. "char_filters": []string{},
  133. "tokenizer": unicode.Name,
  134. "token_filters": []string{unicodeNormalizeName, lowercase.Name},
  135. }); err != nil {
  136. return nil, err
  137. }
  138. mapping.DefaultAnalyzer = repoIndexerAnalyzer
  139. mapping.AddDocumentMapping(repoIndexerDocType, docMapping)
  140. mapping.AddDocumentMapping("_all", bleve.NewDocumentDisabledMapping())
  141. indexer, err := bleve.New(path, mapping)
  142. if err != nil {
  143. return nil, err
  144. }
  145. if err = rupture.WriteIndexMetadata(path, &rupture.IndexMetadata{
  146. Version: latestVersion,
  147. }); err != nil {
  148. return nil, err
  149. }
  150. return indexer, nil
  151. }
  152. func filenameIndexerID(repoID int64, filename string) string {
  153. return indexerID(repoID) + "_" + filename
  154. }
  155. func filenameOfIndexerID(indexerID string) string {
  156. index := strings.IndexByte(indexerID, '_')
  157. if index == -1 {
  158. log.Error("Unexpected ID in repo indexer: %s", indexerID)
  159. }
  160. return indexerID[index+1:]
  161. }
  162. var (
  163. _ Indexer = &BleveIndexer{}
  164. )
  165. // BleveIndexer represents a bleve indexer implementation
  166. type BleveIndexer struct {
  167. indexDir string
  168. indexer bleve.Index
  169. }
  170. // NewBleveIndexer creates a new bleve local indexer
  171. func NewBleveIndexer(indexDir string) (*BleveIndexer, bool, error) {
  172. indexer := &BleveIndexer{
  173. indexDir: indexDir,
  174. }
  175. created, err := indexer.init()
  176. return indexer, created, err
  177. }
  178. // init init the indexer
  179. func (b *BleveIndexer) init() (bool, error) {
  180. var err error
  181. b.indexer, err = openIndexer(b.indexDir, repoIndexerLatestVersion)
  182. if err != nil {
  183. return false, err
  184. }
  185. if b.indexer != nil {
  186. return false, nil
  187. }
  188. b.indexer, err = createRepoIndexer(b.indexDir, repoIndexerLatestVersion)
  189. if err != nil {
  190. return false, err
  191. }
  192. return true, nil
  193. }
  194. // Close close the indexer
  195. func (b *BleveIndexer) Close() {
  196. log.Debug("Closing repo indexer")
  197. if b.indexer != nil {
  198. err := b.indexer.Close()
  199. if err != nil {
  200. log.Error("Error whilst closing the repository indexer: %v", err)
  201. }
  202. }
  203. log.Info("PID: %d Repository Indexer closed", os.Getpid())
  204. }
  205. // Index indexes the data
  206. func (b *BleveIndexer) Index(repoID int64) error {
  207. repo, err := models.GetRepositoryByID(repoID)
  208. if err != nil {
  209. return err
  210. }
  211. sha, err := getDefaultBranchSha(repo)
  212. if err != nil {
  213. return err
  214. }
  215. changes, err := getRepoChanges(repo, sha)
  216. if err != nil {
  217. return err
  218. } else if changes == nil {
  219. return nil
  220. }
  221. batch := rupture.NewFlushingBatch(b.indexer, maxBatchSize)
  222. for _, update := range changes.Updates {
  223. if err := addUpdate(update, repo, batch); err != nil {
  224. return err
  225. }
  226. }
  227. for _, filename := range changes.RemovedFilenames {
  228. if err := addDelete(filename, repo, batch); err != nil {
  229. return err
  230. }
  231. }
  232. if err = batch.Flush(); err != nil {
  233. return err
  234. }
  235. return repo.UpdateIndexerStatus(models.RepoIndexerTypeCode, sha)
  236. }
  237. // Delete deletes indexes by ids
  238. func (b *BleveIndexer) Delete(repoID int64) error {
  239. query := numericEqualityQuery(repoID, "RepoID")
  240. searchRequest := bleve.NewSearchRequestOptions(query, 2147483647, 0, false)
  241. result, err := b.indexer.Search(searchRequest)
  242. if err != nil {
  243. return err
  244. }
  245. batch := rupture.NewFlushingBatch(b.indexer, maxBatchSize)
  246. for _, hit := range result.Hits {
  247. if err = batch.Delete(hit.ID); err != nil {
  248. return err
  249. }
  250. }
  251. return batch.Flush()
  252. }
  253. // Search searches for files in the specified repo.
  254. // Returns the matching file-paths
  255. func (b *BleveIndexer) Search(repoIDs []int64, keyword string, page, pageSize int) (int64, []*SearchResult, error) {
  256. phraseQuery := bleve.NewMatchPhraseQuery(keyword)
  257. phraseQuery.FieldVal = "Content"
  258. phraseQuery.Analyzer = repoIndexerAnalyzer
  259. var indexerQuery query.Query
  260. if len(repoIDs) > 0 {
  261. var repoQueries = make([]query.Query, 0, len(repoIDs))
  262. for _, repoID := range repoIDs {
  263. repoQueries = append(repoQueries, numericEqualityQuery(repoID, "RepoID"))
  264. }
  265. indexerQuery = bleve.NewConjunctionQuery(
  266. bleve.NewDisjunctionQuery(repoQueries...),
  267. phraseQuery,
  268. )
  269. } else {
  270. indexerQuery = phraseQuery
  271. }
  272. from := (page - 1) * pageSize
  273. searchRequest := bleve.NewSearchRequestOptions(indexerQuery, pageSize, from, false)
  274. searchRequest.Fields = []string{"Content", "RepoID"}
  275. searchRequest.IncludeLocations = true
  276. result, err := b.indexer.Search(searchRequest)
  277. if err != nil {
  278. return 0, nil, err
  279. }
  280. searchResults := make([]*SearchResult, len(result.Hits))
  281. for i, hit := range result.Hits {
  282. var startIndex, endIndex int = -1, -1
  283. for _, locations := range hit.Locations["Content"] {
  284. location := locations[0]
  285. locationStart := int(location.Start)
  286. locationEnd := int(location.End)
  287. if startIndex < 0 || locationStart < startIndex {
  288. startIndex = locationStart
  289. }
  290. if endIndex < 0 || locationEnd > endIndex {
  291. endIndex = locationEnd
  292. }
  293. }
  294. searchResults[i] = &SearchResult{
  295. RepoID: int64(hit.Fields["RepoID"].(float64)),
  296. StartIndex: startIndex,
  297. EndIndex: endIndex,
  298. Filename: filenameOfIndexerID(hit.ID),
  299. Content: hit.Fields["Content"].(string),
  300. }
  301. }
  302. return int64(result.Total), searchResults, nil
  303. }