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.

424 lines
12 KiB

  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. "time"
  11. "code.gitea.io/gitea/models"
  12. "code.gitea.io/gitea/modules/analyze"
  13. "code.gitea.io/gitea/modules/base"
  14. "code.gitea.io/gitea/modules/charset"
  15. "code.gitea.io/gitea/modules/git"
  16. "code.gitea.io/gitea/modules/log"
  17. "code.gitea.io/gitea/modules/setting"
  18. "code.gitea.io/gitea/modules/timeutil"
  19. "code.gitea.io/gitea/modules/util"
  20. "github.com/blevesearch/bleve"
  21. analyzer_custom "github.com/blevesearch/bleve/analysis/analyzer/custom"
  22. analyzer_keyword "github.com/blevesearch/bleve/analysis/analyzer/keyword"
  23. "github.com/blevesearch/bleve/analysis/token/lowercase"
  24. "github.com/blevesearch/bleve/analysis/token/unicodenorm"
  25. "github.com/blevesearch/bleve/analysis/tokenizer/unicode"
  26. "github.com/blevesearch/bleve/index/upsidedown"
  27. "github.com/blevesearch/bleve/mapping"
  28. "github.com/blevesearch/bleve/search/query"
  29. "github.com/ethantkoenig/rupture"
  30. "github.com/go-enry/go-enry/v2"
  31. )
  32. const unicodeNormalizeName = "unicodeNormalize"
  33. const maxBatchSize = 16
  34. // indexerID a bleve-compatible unique identifier for an integer id
  35. func indexerID(id int64) string {
  36. return strconv.FormatInt(id, 36)
  37. }
  38. // numericEqualityQuery a numeric equality query for the given value and field
  39. func numericEqualityQuery(value int64, field string) *query.NumericRangeQuery {
  40. f := float64(value)
  41. tru := true
  42. q := bleve.NewNumericRangeInclusiveQuery(&f, &f, &tru, &tru)
  43. q.SetField(field)
  44. return q
  45. }
  46. func addUnicodeNormalizeTokenFilter(m *mapping.IndexMappingImpl) error {
  47. return m.AddCustomTokenFilter(unicodeNormalizeName, map[string]interface{}{
  48. "type": unicodenorm.Name,
  49. "form": unicodenorm.NFC,
  50. })
  51. }
  52. // openIndexer open the index at the specified path, checking for metadata
  53. // updates and bleve version updates. If index needs to be created (or
  54. // re-created), returns (nil, nil)
  55. func openIndexer(path string, latestVersion int) (bleve.Index, error) {
  56. _, err := os.Stat(path)
  57. if err != nil && os.IsNotExist(err) {
  58. return nil, nil
  59. } else if err != nil {
  60. return nil, err
  61. }
  62. metadata, err := rupture.ReadIndexMetadata(path)
  63. if err != nil {
  64. return nil, err
  65. }
  66. if metadata.Version < latestVersion {
  67. // the indexer is using a previous version, so we should delete it and
  68. // re-populate
  69. return nil, util.RemoveAll(path)
  70. }
  71. index, err := bleve.Open(path)
  72. if err != nil && err == upsidedown.IncompatibleVersion {
  73. // the indexer was built with a previous version of bleve, so we should
  74. // delete it and re-populate
  75. return nil, util.RemoveAll(path)
  76. } else if err != nil {
  77. return nil, err
  78. }
  79. return index, nil
  80. }
  81. // RepoIndexerData data stored in the repo indexer
  82. type RepoIndexerData struct {
  83. RepoID int64
  84. CommitID string
  85. Content string
  86. Language string
  87. UpdatedAt time.Time
  88. }
  89. // Type returns the document type, for bleve's mapping.Classifier interface.
  90. func (d *RepoIndexerData) Type() string {
  91. return repoIndexerDocType
  92. }
  93. func addUpdate(commitSha string, update fileUpdate, repo *models.Repository, batch rupture.FlushingBatch) error {
  94. // Ignore vendored files in code search
  95. if setting.Indexer.ExcludeVendored && enry.IsVendor(update.Filename) {
  96. return nil
  97. }
  98. stdout, err := git.NewCommand("cat-file", "-s", update.BlobSha).
  99. RunInDir(repo.RepoPath())
  100. if err != nil {
  101. return err
  102. }
  103. if size, err := strconv.Atoi(strings.TrimSpace(stdout)); err != nil {
  104. return fmt.Errorf("Misformatted git cat-file output: %v", err)
  105. } else if int64(size) > setting.Indexer.MaxIndexerFileSize {
  106. return addDelete(update.Filename, repo, batch)
  107. }
  108. fileContents, err := git.NewCommand("cat-file", "blob", update.BlobSha).
  109. RunInDirBytes(repo.RepoPath())
  110. if err != nil {
  111. return err
  112. } else if !base.IsTextFile(fileContents) {
  113. // FIXME: UTF-16 files will probably fail here
  114. return nil
  115. }
  116. id := filenameIndexerID(repo.ID, update.Filename)
  117. return batch.Index(id, &RepoIndexerData{
  118. RepoID: repo.ID,
  119. CommitID: commitSha,
  120. Content: string(charset.ToUTF8DropErrors(fileContents)),
  121. Language: analyze.GetCodeLanguage(update.Filename, fileContents),
  122. UpdatedAt: time.Now().UTC(),
  123. })
  124. }
  125. func addDelete(filename string, repo *models.Repository, batch rupture.FlushingBatch) error {
  126. id := filenameIndexerID(repo.ID, filename)
  127. return batch.Delete(id)
  128. }
  129. const (
  130. repoIndexerAnalyzer = "repoIndexerAnalyzer"
  131. repoIndexerDocType = "repoIndexerDocType"
  132. repoIndexerLatestVersion = 5
  133. )
  134. // createRepoIndexer create a repo indexer if one does not already exist
  135. func createRepoIndexer(path string, latestVersion int) (bleve.Index, error) {
  136. docMapping := bleve.NewDocumentMapping()
  137. numericFieldMapping := bleve.NewNumericFieldMapping()
  138. numericFieldMapping.IncludeInAll = false
  139. docMapping.AddFieldMappingsAt("RepoID", numericFieldMapping)
  140. textFieldMapping := bleve.NewTextFieldMapping()
  141. textFieldMapping.IncludeInAll = false
  142. docMapping.AddFieldMappingsAt("Content", textFieldMapping)
  143. termFieldMapping := bleve.NewTextFieldMapping()
  144. termFieldMapping.IncludeInAll = false
  145. termFieldMapping.Analyzer = analyzer_keyword.Name
  146. docMapping.AddFieldMappingsAt("Language", termFieldMapping)
  147. docMapping.AddFieldMappingsAt("CommitID", termFieldMapping)
  148. timeFieldMapping := bleve.NewDateTimeFieldMapping()
  149. timeFieldMapping.IncludeInAll = false
  150. docMapping.AddFieldMappingsAt("UpdatedAt", timeFieldMapping)
  151. mapping := bleve.NewIndexMapping()
  152. if err := addUnicodeNormalizeTokenFilter(mapping); err != nil {
  153. return nil, err
  154. } else if err := mapping.AddCustomAnalyzer(repoIndexerAnalyzer, map[string]interface{}{
  155. "type": analyzer_custom.Name,
  156. "char_filters": []string{},
  157. "tokenizer": unicode.Name,
  158. "token_filters": []string{unicodeNormalizeName, lowercase.Name},
  159. }); err != nil {
  160. return nil, err
  161. }
  162. mapping.DefaultAnalyzer = repoIndexerAnalyzer
  163. mapping.AddDocumentMapping(repoIndexerDocType, docMapping)
  164. mapping.AddDocumentMapping("_all", bleve.NewDocumentDisabledMapping())
  165. indexer, err := bleve.New(path, mapping)
  166. if err != nil {
  167. return nil, err
  168. }
  169. if err = rupture.WriteIndexMetadata(path, &rupture.IndexMetadata{
  170. Version: latestVersion,
  171. }); err != nil {
  172. return nil, err
  173. }
  174. return indexer, nil
  175. }
  176. func filenameIndexerID(repoID int64, filename string) string {
  177. return indexerID(repoID) + "_" + filename
  178. }
  179. func filenameOfIndexerID(indexerID string) string {
  180. index := strings.IndexByte(indexerID, '_')
  181. if index == -1 {
  182. log.Error("Unexpected ID in repo indexer: %s", indexerID)
  183. }
  184. return indexerID[index+1:]
  185. }
  186. var (
  187. _ Indexer = &BleveIndexer{}
  188. )
  189. // BleveIndexer represents a bleve indexer implementation
  190. type BleveIndexer struct {
  191. indexDir string
  192. indexer bleve.Index
  193. }
  194. // NewBleveIndexer creates a new bleve local indexer
  195. func NewBleveIndexer(indexDir string) (*BleveIndexer, bool, error) {
  196. indexer := &BleveIndexer{
  197. indexDir: indexDir,
  198. }
  199. created, err := indexer.init()
  200. return indexer, created, err
  201. }
  202. // init init the indexer
  203. func (b *BleveIndexer) init() (bool, error) {
  204. var err error
  205. b.indexer, err = openIndexer(b.indexDir, repoIndexerLatestVersion)
  206. if err != nil {
  207. return false, err
  208. }
  209. if b.indexer != nil {
  210. return false, nil
  211. }
  212. b.indexer, err = createRepoIndexer(b.indexDir, repoIndexerLatestVersion)
  213. if err != nil {
  214. return false, err
  215. }
  216. return true, nil
  217. }
  218. // Close close the indexer
  219. func (b *BleveIndexer) Close() {
  220. log.Debug("Closing repo indexer")
  221. if b.indexer != nil {
  222. err := b.indexer.Close()
  223. if err != nil {
  224. log.Error("Error whilst closing the repository indexer: %v", err)
  225. }
  226. }
  227. log.Info("PID: %d Repository Indexer closed", os.Getpid())
  228. }
  229. // Index indexes the data
  230. func (b *BleveIndexer) Index(repoID int64) error {
  231. repo, err := models.GetRepositoryByID(repoID)
  232. if err != nil {
  233. return err
  234. }
  235. sha, err := getDefaultBranchSha(repo)
  236. if err != nil {
  237. return err
  238. }
  239. changes, err := getRepoChanges(repo, sha)
  240. if err != nil {
  241. return err
  242. } else if changes == nil {
  243. return nil
  244. }
  245. batch := rupture.NewFlushingBatch(b.indexer, maxBatchSize)
  246. for _, update := range changes.Updates {
  247. if err := addUpdate(sha, update, repo, batch); err != nil {
  248. return err
  249. }
  250. }
  251. for _, filename := range changes.RemovedFilenames {
  252. if err := addDelete(filename, repo, batch); err != nil {
  253. return err
  254. }
  255. }
  256. if err = batch.Flush(); err != nil {
  257. return err
  258. }
  259. return repo.UpdateIndexerStatus(models.RepoIndexerTypeCode, sha)
  260. }
  261. // Delete deletes indexes by ids
  262. func (b *BleveIndexer) Delete(repoID int64) error {
  263. query := numericEqualityQuery(repoID, "RepoID")
  264. searchRequest := bleve.NewSearchRequestOptions(query, 2147483647, 0, false)
  265. result, err := b.indexer.Search(searchRequest)
  266. if err != nil {
  267. return err
  268. }
  269. batch := rupture.NewFlushingBatch(b.indexer, maxBatchSize)
  270. for _, hit := range result.Hits {
  271. if err = batch.Delete(hit.ID); err != nil {
  272. return err
  273. }
  274. }
  275. return batch.Flush()
  276. }
  277. // Search searches for files in the specified repo.
  278. // Returns the matching file-paths
  279. func (b *BleveIndexer) Search(repoIDs []int64, language, keyword string, page, pageSize int) (int64, []*SearchResult, []*SearchResultLanguages, error) {
  280. phraseQuery := bleve.NewMatchPhraseQuery(keyword)
  281. phraseQuery.FieldVal = "Content"
  282. phraseQuery.Analyzer = repoIndexerAnalyzer
  283. var indexerQuery query.Query
  284. if len(repoIDs) > 0 {
  285. var repoQueries = make([]query.Query, 0, len(repoIDs))
  286. for _, repoID := range repoIDs {
  287. repoQueries = append(repoQueries, numericEqualityQuery(repoID, "RepoID"))
  288. }
  289. indexerQuery = bleve.NewConjunctionQuery(
  290. bleve.NewDisjunctionQuery(repoQueries...),
  291. phraseQuery,
  292. )
  293. } else {
  294. indexerQuery = phraseQuery
  295. }
  296. // Save for reuse without language filter
  297. facetQuery := indexerQuery
  298. if len(language) > 0 {
  299. languageQuery := bleve.NewMatchQuery(language)
  300. languageQuery.FieldVal = "Language"
  301. languageQuery.Analyzer = analyzer_keyword.Name
  302. indexerQuery = bleve.NewConjunctionQuery(
  303. indexerQuery,
  304. languageQuery,
  305. )
  306. }
  307. from := (page - 1) * pageSize
  308. searchRequest := bleve.NewSearchRequestOptions(indexerQuery, pageSize, from, false)
  309. searchRequest.Fields = []string{"Content", "RepoID", "Language", "CommitID", "UpdatedAt"}
  310. searchRequest.IncludeLocations = true
  311. if len(language) == 0 {
  312. searchRequest.AddFacet("languages", bleve.NewFacetRequest("Language", 10))
  313. }
  314. result, err := b.indexer.Search(searchRequest)
  315. if err != nil {
  316. return 0, nil, nil, err
  317. }
  318. total := int64(result.Total)
  319. searchResults := make([]*SearchResult, len(result.Hits))
  320. for i, hit := range result.Hits {
  321. var startIndex, endIndex int = -1, -1
  322. for _, locations := range hit.Locations["Content"] {
  323. location := locations[0]
  324. locationStart := int(location.Start)
  325. locationEnd := int(location.End)
  326. if startIndex < 0 || locationStart < startIndex {
  327. startIndex = locationStart
  328. }
  329. if endIndex < 0 || locationEnd > endIndex {
  330. endIndex = locationEnd
  331. }
  332. }
  333. language := hit.Fields["Language"].(string)
  334. var updatedUnix timeutil.TimeStamp
  335. if t, err := time.Parse(time.RFC3339, hit.Fields["UpdatedAt"].(string)); err == nil {
  336. updatedUnix = timeutil.TimeStamp(t.Unix())
  337. }
  338. searchResults[i] = &SearchResult{
  339. RepoID: int64(hit.Fields["RepoID"].(float64)),
  340. StartIndex: startIndex,
  341. EndIndex: endIndex,
  342. Filename: filenameOfIndexerID(hit.ID),
  343. Content: hit.Fields["Content"].(string),
  344. CommitID: hit.Fields["CommitID"].(string),
  345. UpdatedUnix: updatedUnix,
  346. Language: language,
  347. Color: enry.GetColor(language),
  348. }
  349. }
  350. searchResultLanguages := make([]*SearchResultLanguages, 0, 10)
  351. if len(language) > 0 {
  352. // Use separate query to go get all language counts
  353. facetRequest := bleve.NewSearchRequestOptions(facetQuery, 1, 0, false)
  354. facetRequest.Fields = []string{"Content", "RepoID", "Language", "CommitID", "UpdatedAt"}
  355. facetRequest.IncludeLocations = true
  356. facetRequest.AddFacet("languages", bleve.NewFacetRequest("Language", 10))
  357. if result, err = b.indexer.Search(facetRequest); err != nil {
  358. return 0, nil, nil, err
  359. }
  360. }
  361. languagesFacet := result.Facets["languages"]
  362. for _, term := range languagesFacet.Terms {
  363. if len(term.Term) == 0 {
  364. continue
  365. }
  366. searchResultLanguages = append(searchResultLanguages, &SearchResultLanguages{
  367. Language: term.Term,
  368. Color: enry.GetColor(term.Term),
  369. Count: term.Count,
  370. })
  371. }
  372. return total, searchResults, searchResultLanguages, nil
  373. }