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 13KB

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