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.

224 lines
6.4 KiB

  1. // Copyright 2017 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 indexer
  5. import (
  6. "strings"
  7. "code.gitea.io/gitea/modules/log"
  8. "code.gitea.io/gitea/modules/setting"
  9. "github.com/blevesearch/bleve"
  10. "github.com/blevesearch/bleve/analysis/analyzer/custom"
  11. "github.com/blevesearch/bleve/analysis/token/camelcase"
  12. "github.com/blevesearch/bleve/analysis/token/lowercase"
  13. "github.com/blevesearch/bleve/analysis/token/unique"
  14. "github.com/blevesearch/bleve/analysis/tokenizer/unicode"
  15. "github.com/blevesearch/bleve/search/query"
  16. "github.com/ethantkoenig/rupture"
  17. )
  18. const (
  19. repoIndexerAnalyzer = "repoIndexerAnalyzer"
  20. repoIndexerDocType = "repoIndexerDocType"
  21. repoIndexerLatestVersion = 1
  22. )
  23. // repoIndexer (thread-safe) index for repository contents
  24. var repoIndexer bleve.Index
  25. // RepoIndexerOp type of operation to perform on repo indexer
  26. type RepoIndexerOp int
  27. const (
  28. // RepoIndexerOpUpdate add/update a file's contents
  29. RepoIndexerOpUpdate = iota
  30. // RepoIndexerOpDelete delete a file
  31. RepoIndexerOpDelete
  32. )
  33. // RepoIndexerData data stored in the repo indexer
  34. type RepoIndexerData struct {
  35. RepoID int64
  36. Content string
  37. }
  38. // Type returns the document type, for bleve's mapping.Classifier interface.
  39. func (d *RepoIndexerData) Type() string {
  40. return repoIndexerDocType
  41. }
  42. // RepoIndexerUpdate an update to the repo indexer
  43. type RepoIndexerUpdate struct {
  44. Filepath string
  45. Op RepoIndexerOp
  46. Data *RepoIndexerData
  47. }
  48. // AddToFlushingBatch adds the update to the given flushing batch.
  49. func (update RepoIndexerUpdate) AddToFlushingBatch(batch rupture.FlushingBatch) error {
  50. id := filenameIndexerID(update.Data.RepoID, update.Filepath)
  51. switch update.Op {
  52. case RepoIndexerOpUpdate:
  53. return batch.Index(id, update.Data)
  54. case RepoIndexerOpDelete:
  55. return batch.Delete(id)
  56. default:
  57. log.Error(4, "Unrecognized repo indexer op: %d", update.Op)
  58. }
  59. return nil
  60. }
  61. // InitRepoIndexer initialize repo indexer
  62. func InitRepoIndexer(populateIndexer func() error) {
  63. var err error
  64. repoIndexer, err = openIndexer(setting.Indexer.RepoPath, repoIndexerLatestVersion)
  65. if err != nil {
  66. log.Fatal(4, "InitRepoIndexer: %v", err)
  67. }
  68. if repoIndexer != nil {
  69. return
  70. }
  71. if err = createRepoIndexer(); err != nil {
  72. log.Fatal(4, "CreateRepoIndexer: %v", err)
  73. }
  74. if err = populateIndexer(); err != nil {
  75. log.Fatal(4, "PopulateRepoIndex: %v", err)
  76. }
  77. }
  78. // createRepoIndexer create a repo indexer if one does not already exist
  79. func createRepoIndexer() error {
  80. var err error
  81. docMapping := bleve.NewDocumentMapping()
  82. numericFieldMapping := bleve.NewNumericFieldMapping()
  83. numericFieldMapping.IncludeInAll = false
  84. docMapping.AddFieldMappingsAt("RepoID", numericFieldMapping)
  85. textFieldMapping := bleve.NewTextFieldMapping()
  86. textFieldMapping.IncludeInAll = false
  87. docMapping.AddFieldMappingsAt("Content", textFieldMapping)
  88. mapping := bleve.NewIndexMapping()
  89. if err = addUnicodeNormalizeTokenFilter(mapping); err != nil {
  90. return err
  91. } else if err = mapping.AddCustomAnalyzer(repoIndexerAnalyzer, map[string]interface{}{
  92. "type": custom.Name,
  93. "char_filters": []string{},
  94. "tokenizer": unicode.Name,
  95. "token_filters": []string{unicodeNormalizeName, camelcase.Name, lowercase.Name, unique.Name},
  96. }); err != nil {
  97. return err
  98. }
  99. mapping.DefaultAnalyzer = repoIndexerAnalyzer
  100. mapping.AddDocumentMapping(repoIndexerDocType, docMapping)
  101. mapping.AddDocumentMapping("_all", bleve.NewDocumentDisabledMapping())
  102. repoIndexer, err = bleve.New(setting.Indexer.RepoPath, mapping)
  103. return err
  104. }
  105. func filenameIndexerID(repoID int64, filename string) string {
  106. return indexerID(repoID) + "_" + filename
  107. }
  108. func filenameOfIndexerID(indexerID string) string {
  109. index := strings.IndexByte(indexerID, '_')
  110. if index == -1 {
  111. log.Error(4, "Unexpected ID in repo indexer: %s", indexerID)
  112. }
  113. return indexerID[index+1:]
  114. }
  115. // RepoIndexerBatch batch to add updates to
  116. func RepoIndexerBatch() rupture.FlushingBatch {
  117. return rupture.NewFlushingBatch(repoIndexer, maxBatchSize)
  118. }
  119. // DeleteRepoFromIndexer delete all of a repo's files from indexer
  120. func DeleteRepoFromIndexer(repoID int64) error {
  121. query := numericEqualityQuery(repoID, "RepoID")
  122. searchRequest := bleve.NewSearchRequestOptions(query, 2147483647, 0, false)
  123. result, err := repoIndexer.Search(searchRequest)
  124. if err != nil {
  125. return err
  126. }
  127. batch := RepoIndexerBatch()
  128. for _, hit := range result.Hits {
  129. if err = batch.Delete(hit.ID); err != nil {
  130. return err
  131. }
  132. }
  133. return batch.Flush()
  134. }
  135. // RepoSearchResult result of performing a search in a repo
  136. type RepoSearchResult struct {
  137. RepoID int64
  138. StartIndex int
  139. EndIndex int
  140. Filename string
  141. Content string
  142. }
  143. // SearchRepoByKeyword searches for files in the specified repo.
  144. // Returns the matching file-paths
  145. func SearchRepoByKeyword(repoIDs []int64, keyword string, page, pageSize int) (int64, []*RepoSearchResult, error) {
  146. phraseQuery := bleve.NewMatchPhraseQuery(keyword)
  147. phraseQuery.FieldVal = "Content"
  148. phraseQuery.Analyzer = repoIndexerAnalyzer
  149. var indexerQuery query.Query
  150. if len(repoIDs) > 0 {
  151. var repoQueries = make([]query.Query, 0, len(repoIDs))
  152. for _, repoID := range repoIDs {
  153. repoQueries = append(repoQueries, numericEqualityQuery(repoID, "RepoID"))
  154. }
  155. indexerQuery = bleve.NewConjunctionQuery(
  156. bleve.NewDisjunctionQuery(repoQueries...),
  157. phraseQuery,
  158. )
  159. } else {
  160. indexerQuery = phraseQuery
  161. }
  162. from := (page - 1) * pageSize
  163. searchRequest := bleve.NewSearchRequestOptions(indexerQuery, pageSize, from, false)
  164. searchRequest.Fields = []string{"Content", "RepoID"}
  165. searchRequest.IncludeLocations = true
  166. result, err := repoIndexer.Search(searchRequest)
  167. if err != nil {
  168. return 0, nil, err
  169. }
  170. searchResults := make([]*RepoSearchResult, len(result.Hits))
  171. for i, hit := range result.Hits {
  172. var startIndex, endIndex int = -1, -1
  173. for _, locations := range hit.Locations["Content"] {
  174. location := locations[0]
  175. locationStart := int(location.Start)
  176. locationEnd := int(location.End)
  177. if startIndex < 0 || locationStart < startIndex {
  178. startIndex = locationStart
  179. }
  180. if endIndex < 0 || locationEnd > endIndex {
  181. endIndex = locationEnd
  182. }
  183. }
  184. searchResults[i] = &RepoSearchResult{
  185. RepoID: int64(hit.Fields["RepoID"].(float64)),
  186. StartIndex: startIndex,
  187. EndIndex: endIndex,
  188. Filename: filenameOfIndexerID(hit.ID),
  189. Content: hit.Fields["Content"].(string),
  190. }
  191. }
  192. return int64(result.Total), searchResults, nil
  193. }