this repo has no description
1// heavily inspired by gitea's model (basically copy-pasted) 2package issues_indexer 3 4import ( 5 "context" 6 "errors" 7 "log" 8 "os" 9 10 "github.com/blevesearch/bleve/v2" 11 "github.com/blevesearch/bleve/v2/index/upsidedown" 12 "github.com/blevesearch/bleve/v2/search/query" 13 "tangled.org/core/appview/db" 14 "tangled.org/core/appview/indexer/base36" 15 "tangled.org/core/appview/indexer/bleve" 16 "tangled.org/core/appview/models" 17 "tangled.org/core/appview/pagination" 18 tlog "tangled.org/core/log" 19) 20 21type Indexer struct { 22 indexer bleve.Index 23 path string 24} 25 26func NewIndexer(indexDir string) *Indexer { 27 return &Indexer{ 28 path: indexDir, 29 } 30} 31 32// Init initializes the indexer 33func (ix *Indexer) Init(ctx context.Context, e db.Execer) { 34 l := tlog.FromContext(ctx) 35 existed, err := ix.intialize(ctx) 36 if err != nil { 37 log.Fatalln("failed to initialize issue indexer", err) 38 } 39 if !existed { 40 l.Debug("Populating the issue indexer") 41 err := PopulateIndexer(ctx, ix, e) 42 if err != nil { 43 log.Fatalln("failed to populate issue indexer", err) 44 } 45 } 46 l.Info("Initialized the issue indexer") 47} 48 49func (ix *Indexer) intialize(ctx context.Context) (bool, error) { 50 if ix.indexer != nil { 51 return false, errors.New("indexer is already initialized") 52 } 53 54 indexer, err := openIndexer(ctx, ix.path) 55 if err != nil { 56 return false, err 57 } 58 if indexer != nil { 59 ix.indexer = indexer 60 return true, nil 61 } 62 63 mapping := bleve.NewIndexMapping() 64 indexer, err = bleve.New(ix.path, mapping) 65 if err != nil { 66 return false, err 67 } 68 69 ix.indexer = indexer 70 71 return false, nil 72} 73 74func openIndexer(ctx context.Context, path string) (bleve.Index, error) { 75 l := tlog.FromContext(ctx) 76 indexer, err := bleve.Open(path) 77 if err != nil { 78 if errors.Is(err, upsidedown.IncompatibleVersion) { 79 l.Info("Indexer was built with a previous version of bleve, deleting and rebuilding") 80 return nil, os.RemoveAll(path) 81 } 82 return nil, nil 83 } 84 return indexer, nil 85} 86 87func PopulateIndexer(ctx context.Context, ix *Indexer, e db.Execer) error { 88 l := tlog.FromContext(ctx) 89 count := 0 90 err := pagination.IterateAll( 91 func(page pagination.Page) ([]models.Issue, error) { 92 return db.GetIssuesPaginated(e, page) 93 }, 94 func(issues []models.Issue) error { 95 count += len(issues) 96 return ix.Index(ctx, issues...) 97 }, 98 ) 99 l.Info("issues indexed", "count", count) 100 return err 101} 102 103// issueData data stored and will be indexed 104type issueData struct { 105 ID int64 `json:"id"` 106 RepoAt string `json:"repo_at"` 107 IssueID int `json:"issue_id"` 108 Title string `json:"title"` 109 Body string `json:"body"` 110 111 IsOpen bool `json:"is_open"` 112 Comments []IssueCommentData `json:"comments"` 113} 114 115func makeIssueData(issue *models.Issue) *issueData { 116 return &issueData{ 117 ID: issue.Id, 118 RepoAt: issue.RepoAt.String(), 119 IssueID: issue.IssueId, 120 Title: issue.Title, 121 Body: issue.Body, 122 IsOpen: issue.Open, 123 } 124} 125 126type IssueCommentData struct { 127 Body string `json:"body"` 128} 129 130type SearchResult struct { 131 Hits []int64 132 Total uint64 133} 134 135const maxBatchSize = 20 136 137func (ix *Indexer) Index(ctx context.Context, issues ...models.Issue) error { 138 batch := bleveutil.NewFlushingBatch(ix.indexer, maxBatchSize) 139 for _, issue := range issues { 140 issueData := makeIssueData(&issue) 141 if err := batch.Index(base36.Encode(issue.Id), issueData); err != nil { 142 return err 143 } 144 } 145 return batch.Flush() 146} 147 148// Search searches for issues 149func (ix *Indexer) Search(ctx context.Context, opts models.IssueSearchOptions) (*SearchResult, error) { 150 var queries []query.Query 151 152 if opts.Keyword != "" { 153 queries = append(queries, bleve.NewDisjunctionQuery( 154 bleveutil.MatchAndQuery("title", opts.Keyword), 155 bleveutil.MatchAndQuery("body", opts.Keyword), 156 )) 157 } 158 queries = append(queries, bleveutil.KeywordFieldQuery("repo_at", opts.RepoAt)) 159 queries = append(queries, bleveutil.BoolFieldQuery("is_open", opts.IsOpen)) 160 // TODO: append more queries 161 162 var indexerQuery query.Query = bleve.NewConjunctionQuery(queries...) 163 searchReq := bleve.NewSearchRequestOptions(indexerQuery, opts.Page.Limit, opts.Page.Offset, false) 164 res, err := ix.indexer.SearchInContext(ctx, searchReq) 165 if err != nil { 166 return nil, nil 167 } 168 ret := &SearchResult{ 169 Total: res.Total, 170 Hits: make([]int64, len(res.Hits)), 171 } 172 for i, hit := range res.Hits { 173 id, err := base36.Decode(hit.ID) 174 if err != nil { 175 return nil, err 176 } 177 ret.Hits[i] = id 178 } 179 return ret, nil 180}