2019-12-09 03:15:35 +08:00
// Copyright 2019 The Gitea Authors. All rights reserved.
2022-11-27 13:20:29 -05:00
// SPDX-License-Identifier: MIT
2019-12-09 03:15:35 +08:00
2023-06-23 20:37:56 +08:00
package bleve
2019-12-09 03:15:35 +08:00
import (
2022-01-19 23:26:57 +00:00
"context"
2019-12-09 03:15:35 +08:00
"fmt"
2021-03-04 02:57:01 +00:00
"io"
2019-12-09 03:15:35 +08:00
"strconv"
"strings"
2020-02-20 21:53:55 +02:00
"time"
2019-12-09 03:15:35 +08:00
2021-12-10 09:27:50 +08:00
repo_model "code.gitea.io/gitea/models/repo"
2020-02-20 21:53:55 +02:00
"code.gitea.io/gitea/modules/analyze"
2019-12-09 03:15:35 +08:00
"code.gitea.io/gitea/modules/charset"
"code.gitea.io/gitea/modules/git"
2025-09-15 23:33:12 -07:00
"code.gitea.io/gitea/modules/git/gitcmd"
2025-10-07 02:06:51 -07:00
"code.gitea.io/gitea/modules/gitrepo"
2025-03-13 11:07:48 +08:00
"code.gitea.io/gitea/modules/indexer"
2024-10-11 20:35:04 -03:00
path_filter "code.gitea.io/gitea/modules/indexer/code/bleve/token/path"
2023-06-23 20:37:56 +08:00
"code.gitea.io/gitea/modules/indexer/code/internal"
indexer_internal "code.gitea.io/gitea/modules/indexer/internal"
inner_bleve "code.gitea.io/gitea/modules/indexer/internal/bleve"
2019-12-09 03:15:35 +08:00
"code.gitea.io/gitea/modules/setting"
2020-02-20 21:53:55 +02:00
"code.gitea.io/gitea/modules/timeutil"
2021-06-05 14:32:19 +02:00
"code.gitea.io/gitea/modules/typesniffer"
2025-03-15 02:06:31 +08:00
"code.gitea.io/gitea/modules/util"
2019-12-23 20:31:16 +08:00
2021-01-18 03:21:14 +02:00
"github.com/blevesearch/bleve/v2"
analyzer_custom "github.com/blevesearch/bleve/v2/analysis/analyzer/custom"
analyzer_keyword "github.com/blevesearch/bleve/v2/analysis/analyzer/keyword"
"github.com/blevesearch/bleve/v2/analysis/token/lowercase"
"github.com/blevesearch/bleve/v2/analysis/token/unicodenorm"
2024-11-06 17:51:20 -03:00
"github.com/blevesearch/bleve/v2/analysis/tokenizer/letter"
2021-01-18 03:21:14 +02:00
"github.com/blevesearch/bleve/v2/analysis/tokenizer/unicode"
"github.com/blevesearch/bleve/v2/mapping"
"github.com/blevesearch/bleve/v2/search/query"
2020-04-15 20:40:39 +03:00
"github.com/go-enry/go-enry/v2"
2019-12-09 03:15:35 +08:00
)
2022-01-20 18:46:10 +01:00
const (
unicodeNormalizeName = "unicodeNormalize"
maxBatchSize = 16
)
2019-12-09 03:15:35 +08:00
2019-12-23 20:31:16 +08:00
func addUnicodeNormalizeTokenFilter ( m * mapping . IndexMappingImpl ) error {
2023-07-04 20:36:08 +02:00
return m . AddCustomTokenFilter ( unicodeNormalizeName , map [ string ] any {
2019-12-23 20:31:16 +08:00
"type" : unicodenorm . Name ,
"form" : unicodenorm . NFC ,
} )
2019-12-09 03:15:35 +08:00
}
2019-12-23 20:31:16 +08:00
// RepoIndexerData data stored in the repo indexer
type RepoIndexerData struct {
2020-02-20 21:53:55 +02:00
RepoID int64
CommitID string
Content string
2024-10-11 20:35:04 -03:00
Filename string
2020-02-20 21:53:55 +02:00
Language string
UpdatedAt time . Time
2019-12-09 03:15:35 +08:00
}
2019-12-23 20:31:16 +08:00
// Type returns the document type, for bleve's mapping.Classifier interface.
func ( d * RepoIndexerData ) Type ( ) string {
return repoIndexerDocType
2019-12-09 03:15:35 +08:00
}
2019-12-23 20:31:16 +08:00
const (
repoIndexerAnalyzer = "repoIndexerAnalyzer"
2024-10-11 20:35:04 -03:00
filenameIndexerAnalyzer = "filenameIndexerAnalyzer"
filenameIndexerTokenizer = "filenameIndexerTokenizer"
2019-12-23 20:31:16 +08:00
repoIndexerDocType = "repoIndexerDocType"
2025-03-25 00:18:21 +08:00
repoIndexerLatestVersion = 9
2019-12-23 20:31:16 +08:00
)
2023-06-23 20:37:56 +08:00
// generateBleveIndexMapping generates a bleve index mapping for the repo indexer
func generateBleveIndexMapping ( ) ( mapping . IndexMapping , error ) {
2019-12-23 20:31:16 +08:00
docMapping := bleve . NewDocumentMapping ( )
numericFieldMapping := bleve . NewNumericFieldMapping ( )
numericFieldMapping . IncludeInAll = false
docMapping . AddFieldMappingsAt ( "RepoID" , numericFieldMapping )
textFieldMapping := bleve . NewTextFieldMapping ( )
textFieldMapping . IncludeInAll = false
docMapping . AddFieldMappingsAt ( "Content" , textFieldMapping )
2024-10-11 20:35:04 -03:00
fileNamedMapping := bleve . NewTextFieldMapping ( )
fileNamedMapping . IncludeInAll = false
fileNamedMapping . Analyzer = filenameIndexerAnalyzer
docMapping . AddFieldMappingsAt ( "Filename" , fileNamedMapping )
2020-02-20 21:53:55 +02:00
termFieldMapping := bleve . NewTextFieldMapping ( )
termFieldMapping . IncludeInAll = false
termFieldMapping . Analyzer = analyzer_keyword . Name
docMapping . AddFieldMappingsAt ( "Language" , termFieldMapping )
docMapping . AddFieldMappingsAt ( "CommitID" , termFieldMapping )
timeFieldMapping := bleve . NewDateTimeFieldMapping ( )
timeFieldMapping . IncludeInAll = false
docMapping . AddFieldMappingsAt ( "UpdatedAt" , timeFieldMapping )
2019-12-23 20:31:16 +08:00
mapping := bleve . NewIndexMapping ( )
2024-10-11 20:35:04 -03:00
2019-12-23 20:31:16 +08:00
if err := addUnicodeNormalizeTokenFilter ( mapping ) ; err != nil {
return nil , err
2023-07-04 20:36:08 +02:00
} else if err := mapping . AddCustomAnalyzer ( repoIndexerAnalyzer , map [ string ] any {
2020-02-20 21:53:55 +02:00
"type" : analyzer_custom . Name ,
2019-12-23 20:31:16 +08:00
"char_filters" : [ ] string { } ,
2024-11-06 17:51:20 -03:00
"tokenizer" : letter . Name ,
2025-03-25 00:18:21 +08:00
"token_filters" : [ ] string { unicodeNormalizeName , lowercase . Name } ,
2019-12-23 20:31:16 +08:00
} ) ; err != nil {
return nil , err
2019-12-09 03:15:35 +08:00
}
2024-10-11 20:35:04 -03:00
if err := mapping . AddCustomAnalyzer ( filenameIndexerAnalyzer , map [ string ] any {
"type" : analyzer_custom . Name ,
"char_filters" : [ ] string { } ,
"tokenizer" : unicode . Name ,
"token_filters" : [ ] string { unicodeNormalizeName , path_filter . Name , lowercase . Name } ,
} ) ; err != nil {
return nil , err
}
2019-12-23 20:31:16 +08:00
mapping . DefaultAnalyzer = repoIndexerAnalyzer
mapping . AddDocumentMapping ( repoIndexerDocType , docMapping )
mapping . AddDocumentMapping ( "_all" , bleve . NewDocumentDisabledMapping ( ) )
2023-06-23 20:37:56 +08:00
return mapping , nil
2019-12-23 20:31:16 +08:00
}
2023-06-23 20:37:56 +08:00
var _ internal . Indexer = & Indexer { }
2019-12-23 20:31:16 +08:00
2023-06-23 20:37:56 +08:00
// Indexer represents a bleve indexer implementation
type Indexer struct {
inner * inner_bleve . Indexer
indexer_internal . Indexer // do not composite inner_bleve.Indexer directly to avoid exposing too much
2019-12-23 20:31:16 +08:00
}
2025-03-13 11:07:48 +08:00
func ( b * Indexer ) SupportedSearchModes ( ) [ ] indexer . SearchMode {
return indexer . SearchModesExactWords ( )
}
2023-06-23 20:37:56 +08:00
// NewIndexer creates a new bleve local indexer
func NewIndexer ( indexDir string ) * Indexer {
inner := inner_bleve . NewIndexer ( indexDir , repoIndexerLatestVersion , generateBleveIndexMapping )
return & Indexer {
Indexer : inner ,
inner : inner ,
2019-12-23 20:31:16 +08:00
}
}
2026-01-09 05:37:36 +08:00
func ( b * Indexer ) addUpdate ( ctx context . Context , catFileBatch git . CatFileBatch , commitSha string ,
2023-06-23 20:37:56 +08:00
update internal . FileUpdate , repo * repo_model . Repository , batch * inner_bleve . FlushingBatch ,
2022-02-23 21:16:07 +01:00
) error {
2020-08-31 00:08:01 +08:00
// Ignore vendored files in code search
2021-04-01 18:41:09 +01:00
if setting . Indexer . ExcludeVendored && analyze . IsVendor ( update . Filename ) {
2020-08-31 00:08:01 +08:00
return nil
}
2021-02-17 21:32:25 +00:00
size := update . Size
2022-04-01 10:55:30 +08:00
var err error
2021-02-17 21:32:25 +00:00
if ! update . Sized {
2022-04-01 10:55:30 +08:00
var stdout string
2026-01-19 07:10:33 +08:00
stdout , _ , err = gitrepo . RunCmdString ( ctx , repo , gitcmd . NewCommand ( "cat-file" , "-s" ) . AddDynamicArguments ( update . BlobSha ) )
2021-02-17 21:32:25 +00:00
if err != nil {
return err
}
if size , err = strconv . ParseInt ( strings . TrimSpace ( stdout ) , 10 , 64 ) ; err != nil {
2024-03-16 11:32:45 +01:00
return fmt . Errorf ( "misformatted git cat-file output: %w" , err )
2021-02-17 21:32:25 +00:00
}
2020-08-31 00:08:01 +08:00
}
2021-02-17 21:32:25 +00:00
if size > setting . Indexer . MaxIndexerFileSize {
2020-08-31 00:08:01 +08:00
return b . addDelete ( update . Filename , repo , batch )
}
2026-01-09 05:37:36 +08:00
info , batchReader , err := catFileBatch . QueryContent ( update . BlobSha )
2021-03-04 02:57:01 +00:00
if err != nil {
return err
}
2026-01-09 05:37:36 +08:00
fileContents , err := io . ReadAll ( io . LimitReader ( batchReader , info . Size ) )
2020-08-31 00:08:01 +08:00
if err != nil {
return err
2021-06-05 14:32:19 +02:00
} else if ! typesniffer . DetectContentType ( fileContents ) . IsText ( ) {
2020-08-31 00:08:01 +08:00
// FIXME: UTF-16 files will probably fail here
2025-03-22 03:00:02 +08:00
// Even if the file is not recognized as a "text file", we could still put its name into the indexers to make the filename become searchable, while leave the content to empty.
fileContents = nil
2020-08-31 00:08:01 +08:00
}
2021-06-20 23:00:46 +01:00
if _ , err = batchReader . Discard ( 1 ) ; err != nil {
return err
}
2023-06-23 20:37:56 +08:00
id := internal . FilenameIndexerID ( repo . ID , update . Filename )
2020-08-31 00:08:01 +08:00
return batch . Index ( id , & RepoIndexerData {
RepoID : repo . ID ,
CommitID : commitSha ,
2024-10-11 20:35:04 -03:00
Filename : update . Filename ,
2025-12-13 05:54:03 -08:00
Content : string ( charset . ToUTF8DropErrors ( fileContents ) ) ,
2020-08-31 00:08:01 +08:00
Language : analyze . GetCodeLanguage ( update . Filename , fileContents ) ,
UpdatedAt : time . Now ( ) . UTC ( ) ,
} )
}
2023-06-23 20:37:56 +08:00
func ( b * Indexer ) addDelete ( filename string , repo * repo_model . Repository , batch * inner_bleve . FlushingBatch ) error {
id := internal . FilenameIndexerID ( repo . ID , filename )
2020-08-31 00:08:01 +08:00
return batch . Delete ( id )
}
2019-12-23 20:31:16 +08:00
// Index indexes the data
2023-06-23 20:37:56 +08:00
func ( b * Indexer ) Index ( ctx context . Context , repo * repo_model . Repository , sha string , changes * internal . RepoChanges ) error {
batch := inner_bleve . NewFlushingBatch ( b . inner . Indexer , maxBatchSize )
2021-03-04 02:57:01 +00:00
if len ( changes . Updates ) > 0 {
2025-12-29 10:19:42 -08:00
catfileBatch , err := gitrepo . NewBatch ( ctx , repo )
2024-08-21 01:04:57 +08:00
if err != nil {
return err
}
2025-12-29 10:19:42 -08:00
defer catfileBatch . Close ( )
2021-03-04 02:57:01 +00:00
for _ , update := range changes . Updates {
2025-12-29 10:19:42 -08:00
if err := b . addUpdate ( ctx , catfileBatch , sha , update , repo , batch ) ; err != nil {
2021-03-04 02:57:01 +00:00
return err
}
2019-12-09 03:15:35 +08:00
}
}
2019-12-23 20:31:16 +08:00
for _ , filename := range changes . RemovedFilenames {
2020-08-31 00:08:01 +08:00
if err := b . addDelete ( filename , repo , batch ) ; err != nil {
2019-12-23 20:31:16 +08:00
return err
}
}
2020-08-31 00:08:01 +08:00
return batch . Flush ( )
2019-12-23 20:31:16 +08:00
}
2019-12-09 03:15:35 +08:00
2019-12-23 20:31:16 +08:00
// Delete deletes indexes by ids
2023-06-23 20:37:56 +08:00
func ( b * Indexer ) Delete ( _ context . Context , repoID int64 ) error {
2023-07-31 14:28:53 +08:00
query := inner_bleve . NumericEqualityQuery ( repoID , "RepoID" )
2019-12-23 20:31:16 +08:00
searchRequest := bleve . NewSearchRequestOptions ( query , 2147483647 , 0 , false )
2023-06-23 20:37:56 +08:00
result , err := b . inner . Indexer . Search ( searchRequest )
2019-12-09 03:15:35 +08:00
if err != nil {
2019-12-23 20:31:16 +08:00
return err
}
2023-06-23 20:37:56 +08:00
batch := inner_bleve . NewFlushingBatch ( b . inner . Indexer , maxBatchSize )
2019-12-23 20:31:16 +08:00
for _ , hit := range result . Hits {
if err = batch . Delete ( hit . ID ) ; err != nil {
return err
}
2019-12-09 03:15:35 +08:00
}
2019-12-23 20:31:16 +08:00
return batch . Flush ( )
2019-12-09 03:15:35 +08:00
}
2019-12-23 20:31:16 +08:00
// Search searches for files in the specified repo.
// Returns the matching file-paths
2024-03-16 11:32:45 +01:00
func ( b * Indexer ) Search ( ctx context . Context , opts * internal . SearchOptions ) ( int64 , [ ] * internal . SearchResult , [ ] * internal . SearchResultLanguages , error ) {
2021-01-27 18:00:35 +08:00
var (
indexerQuery query . Query
keywordQuery query . Query
2025-02-16 18:28:06 +08:00
contentQuery query . Query
2021-01-27 18:00:35 +08:00
)
2024-10-11 20:35:04 -03:00
pathQuery := bleve . NewPrefixQuery ( strings . ToLower ( opts . Keyword ) )
pathQuery . FieldVal = "Filename"
pathQuery . SetBoost ( 10 )
2025-03-15 02:06:31 +08:00
searchMode := util . IfZero ( opts . SearchMode , b . SupportedSearchModes ( ) [ 0 ] . ModeValue )
if searchMode == indexer . SearchModeExact {
// 1.21 used NewPrefixQuery, but it seems not working well, and later releases changed to NewMatchPhraseQuery
2025-03-13 11:07:48 +08:00
q := bleve . NewMatchPhraseQuery ( opts . Keyword )
2025-03-15 02:06:31 +08:00
q . Analyzer = repoIndexerAnalyzer
2025-02-16 18:28:06 +08:00
q . FieldVal = "Content"
contentQuery = q
2025-03-13 11:07:48 +08:00
} else /* words */ {
2025-02-16 18:28:06 +08:00
q := bleve . NewMatchQuery ( opts . Keyword )
q . FieldVal = "Content"
2025-03-15 02:06:31 +08:00
q . Analyzer = repoIndexerAnalyzer
if searchMode == indexer . SearchModeFuzzy {
2025-03-13 11:07:48 +08:00
// this logic doesn't seem right, it is only used to pass the test-case `Keyword: "dESCRIPTION"`, which doesn't seem to be a real-life use-case.
2025-02-16 18:28:06 +08:00
q . Fuzziness = inner_bleve . GuessFuzzinessByKeyword ( opts . Keyword )
2025-03-13 11:07:48 +08:00
} else {
q . Operator = query . MatchQueryOperatorAnd
2025-02-16 18:28:06 +08:00
}
contentQuery = q
2021-01-27 18:00:35 +08:00
}
2019-12-23 20:31:16 +08:00
2024-10-11 20:35:04 -03:00
keywordQuery = bleve . NewDisjunctionQuery ( contentQuery , pathQuery )
2024-03-16 11:32:45 +01:00
if len ( opts . RepoIDs ) > 0 {
repoQueries := make ( [ ] query . Query , 0 , len ( opts . RepoIDs ) )
for _ , repoID := range opts . RepoIDs {
2023-07-31 14:28:53 +08:00
repoQueries = append ( repoQueries , inner_bleve . NumericEqualityQuery ( repoID , "RepoID" ) )
2019-12-09 03:15:35 +08:00
}
2019-12-15 09:51:28 +00:00
2019-12-23 20:31:16 +08:00
indexerQuery = bleve . NewConjunctionQuery (
bleve . NewDisjunctionQuery ( repoQueries ... ) ,
2021-01-27 18:00:35 +08:00
keywordQuery ,
2019-12-23 20:31:16 +08:00
)
} else {
2021-01-27 18:00:35 +08:00
indexerQuery = keywordQuery
2019-12-09 03:15:35 +08:00
}
2020-02-20 21:53:55 +02:00
// Save for reuse without language filter
facetQuery := indexerQuery
2024-03-16 11:32:45 +01:00
if len ( opts . Language ) > 0 {
languageQuery := bleve . NewMatchQuery ( opts . Language )
2020-02-20 21:53:55 +02:00
languageQuery . FieldVal = "Language"
languageQuery . Analyzer = analyzer_keyword . Name
indexerQuery = bleve . NewConjunctionQuery (
indexerQuery ,
languageQuery ,
)
}
2024-03-16 11:32:45 +01:00
from , pageSize := opts . GetSkipTake ( )
2019-12-23 20:31:16 +08:00
searchRequest := bleve . NewSearchRequestOptions ( indexerQuery , pageSize , from , false )
2024-10-11 20:35:04 -03:00
searchRequest . Fields = [ ] string { "Content" , "Filename" , "RepoID" , "Language" , "CommitID" , "UpdatedAt" }
2019-12-23 20:31:16 +08:00
searchRequest . IncludeLocations = true
2019-12-09 03:15:35 +08:00
2024-03-16 11:32:45 +01:00
if len ( opts . Language ) == 0 {
2020-02-20 21:53:55 +02:00
searchRequest . AddFacet ( "languages" , bleve . NewFacetRequest ( "Language" , 10 ) )
}
2024-09-28 17:13:55 -03:00
searchRequest . SortBy ( [ ] string { "-_score" , "UpdatedAt" } )
2023-06-23 20:37:56 +08:00
result , err := b . inner . Indexer . SearchInContext ( ctx , searchRequest )
2019-12-23 20:31:16 +08:00
if err != nil {
2020-02-20 21:53:55 +02:00
return 0 , nil , nil , err
2019-12-09 03:15:35 +08:00
}
2019-12-23 20:31:16 +08:00
2020-02-20 21:53:55 +02:00
total := int64 ( result . Total )
2023-06-23 20:37:56 +08:00
searchResults := make ( [ ] * internal . SearchResult , len ( result . Hits ) )
2019-12-23 20:31:16 +08:00
for i , hit := range result . Hits {
2022-06-20 12:02:49 +02:00
startIndex , endIndex := - 1 , - 1
2019-12-23 20:31:16 +08:00
for _ , locations := range hit . Locations [ "Content" ] {
location := locations [ 0 ]
locationStart := int ( location . Start )
locationEnd := int ( location . End )
if startIndex < 0 || locationStart < startIndex {
startIndex = locationStart
}
if endIndex < 0 || locationEnd > endIndex {
endIndex = locationEnd
}
}
2024-10-11 20:35:04 -03:00
if len ( hit . Locations [ "Filename" ] ) > 0 {
startIndex , endIndex = internal . FilenameMatchIndexPos ( hit . Fields [ "Content" ] . ( string ) )
}
2020-02-20 21:53:55 +02:00
language := hit . Fields [ "Language" ] . ( string )
var updatedUnix timeutil . TimeStamp
if t , err := time . Parse ( time . RFC3339 , hit . Fields [ "UpdatedAt" ] . ( string ) ) ; err == nil {
updatedUnix = timeutil . TimeStamp ( t . Unix ( ) )
}
2023-06-23 20:37:56 +08:00
searchResults [ i ] = & internal . SearchResult {
2020-02-20 21:53:55 +02:00
RepoID : int64 ( hit . Fields [ "RepoID" ] . ( float64 ) ) ,
StartIndex : startIndex ,
EndIndex : endIndex ,
2023-06-23 20:37:56 +08:00
Filename : internal . FilenameOfIndexerID ( hit . ID ) ,
2020-02-20 21:53:55 +02:00
Content : hit . Fields [ "Content" ] . ( string ) ,
CommitID : hit . Fields [ "CommitID" ] . ( string ) ,
UpdatedUnix : updatedUnix ,
Language : language ,
Color : enry . GetColor ( language ) ,
}
}
2023-06-23 20:37:56 +08:00
searchResultLanguages := make ( [ ] * internal . SearchResultLanguages , 0 , 10 )
2024-03-16 11:32:45 +01:00
if len ( opts . Language ) > 0 {
2020-02-20 21:53:55 +02:00
// Use separate query to go get all language counts
facetRequest := bleve . NewSearchRequestOptions ( facetQuery , 1 , 0 , false )
facetRequest . Fields = [ ] string { "Content" , "RepoID" , "Language" , "CommitID" , "UpdatedAt" }
facetRequest . IncludeLocations = true
facetRequest . AddFacet ( "languages" , bleve . NewFacetRequest ( "Language" , 10 ) )
2023-06-23 20:37:56 +08:00
if result , err = b . inner . Indexer . Search ( facetRequest ) ; err != nil {
2020-02-20 21:53:55 +02:00
return 0 , nil , nil , err
}
}
languagesFacet := result . Facets [ "languages" ]
2022-01-01 16:26:27 +08:00
for _ , term := range languagesFacet . Terms . Terms ( ) {
2020-02-20 21:53:55 +02:00
if len ( term . Term ) == 0 {
continue
2019-12-23 20:31:16 +08:00
}
2023-06-23 20:37:56 +08:00
searchResultLanguages = append ( searchResultLanguages , & internal . SearchResultLanguages {
2020-02-20 21:53:55 +02:00
Language : term . Term ,
Color : enry . GetColor ( term . Term ) ,
Count : term . Count ,
} )
2019-12-09 03:15:35 +08:00
}
2020-02-20 21:53:55 +02:00
return total , searchResults , searchResultLanguages , nil
2019-12-09 03:15:35 +08:00
}