code.gitea.io/gitea@v1.19.3/modules/indexer/issues/elastic_search.go (about) 1 // Copyright 2019 The Gitea Authors. All rights reserved. 2 // SPDX-License-Identifier: MIT 3 4 package issues 5 6 import ( 7 "context" 8 "errors" 9 "fmt" 10 "net" 11 "strconv" 12 "sync" 13 "time" 14 15 "code.gitea.io/gitea/modules/graceful" 16 "code.gitea.io/gitea/modules/log" 17 18 "github.com/olivere/elastic/v7" 19 ) 20 21 var _ Indexer = &ElasticSearchIndexer{} 22 23 // ElasticSearchIndexer implements Indexer interface 24 type ElasticSearchIndexer struct { 25 client *elastic.Client 26 indexerName string 27 available bool 28 availabilityCallback func(bool) 29 stopTimer chan struct{} 30 lock sync.RWMutex 31 } 32 33 type elasticLogger struct { 34 log.LevelLogger 35 } 36 37 func (l elasticLogger) Printf(format string, args ...interface{}) { 38 _ = l.Log(2, l.GetLevel(), format, args...) 39 } 40 41 // NewElasticSearchIndexer creates a new elasticsearch indexer 42 func NewElasticSearchIndexer(url, indexerName string) (*ElasticSearchIndexer, error) { 43 opts := []elastic.ClientOptionFunc{ 44 elastic.SetURL(url), 45 elastic.SetSniff(false), 46 elastic.SetHealthcheckInterval(10 * time.Second), 47 elastic.SetGzip(false), 48 } 49 50 logger := elasticLogger{log.GetLogger(log.DEFAULT)} 51 52 if logger.GetLevel() == log.TRACE || logger.GetLevel() == log.DEBUG { 53 opts = append(opts, elastic.SetTraceLog(logger)) 54 } else if logger.GetLevel() == log.ERROR || logger.GetLevel() == log.CRITICAL || logger.GetLevel() == log.FATAL { 55 opts = append(opts, elastic.SetErrorLog(logger)) 56 } else if logger.GetLevel() == log.INFO || logger.GetLevel() == log.WARN { 57 opts = append(opts, elastic.SetInfoLog(logger)) 58 } 59 60 client, err := elastic.NewClient(opts...) 61 if err != nil { 62 return nil, err 63 } 64 65 indexer := &ElasticSearchIndexer{ 66 client: client, 67 indexerName: indexerName, 68 available: true, 69 stopTimer: make(chan struct{}), 70 } 71 72 ticker := time.NewTicker(10 * time.Second) 73 go func() { 74 for { 75 select { 76 case <-ticker.C: 77 indexer.checkAvailability() 78 case <-indexer.stopTimer: 79 ticker.Stop() 80 return 81 } 82 } 83 }() 84 85 return indexer, nil 86 } 87 88 const ( 89 defaultMapping = `{ 90 "mappings": { 91 "properties": { 92 "id": { 93 "type": "integer", 94 "index": true 95 }, 96 "repo_id": { 97 "type": "integer", 98 "index": true 99 }, 100 "title": { 101 "type": "text", 102 "index": true 103 }, 104 "content": { 105 "type": "text", 106 "index": true 107 }, 108 "comments": { 109 "type" : "text", 110 "index": true 111 } 112 } 113 } 114 }` 115 ) 116 117 // Init will initialize the indexer 118 func (b *ElasticSearchIndexer) Init() (bool, error) { 119 ctx := graceful.GetManager().HammerContext() 120 exists, err := b.client.IndexExists(b.indexerName).Do(ctx) 121 if err != nil { 122 return false, b.checkError(err) 123 } 124 125 if !exists { 126 mapping := defaultMapping 127 128 createIndex, err := b.client.CreateIndex(b.indexerName).BodyString(mapping).Do(ctx) 129 if err != nil { 130 return false, b.checkError(err) 131 } 132 if !createIndex.Acknowledged { 133 return false, errors.New("init failed") 134 } 135 136 return false, nil 137 } 138 return true, nil 139 } 140 141 // SetAvailabilityChangeCallback sets callback that will be triggered when availability changes 142 func (b *ElasticSearchIndexer) SetAvailabilityChangeCallback(callback func(bool)) { 143 b.lock.Lock() 144 defer b.lock.Unlock() 145 b.availabilityCallback = callback 146 } 147 148 // Ping checks if elastic is available 149 func (b *ElasticSearchIndexer) Ping() bool { 150 b.lock.RLock() 151 defer b.lock.RUnlock() 152 return b.available 153 } 154 155 // Index will save the index data 156 func (b *ElasticSearchIndexer) Index(issues []*IndexerData) error { 157 if len(issues) == 0 { 158 return nil 159 } else if len(issues) == 1 { 160 issue := issues[0] 161 _, err := b.client.Index(). 162 Index(b.indexerName). 163 Id(fmt.Sprintf("%d", issue.ID)). 164 BodyJson(map[string]interface{}{ 165 "id": issue.ID, 166 "repo_id": issue.RepoID, 167 "title": issue.Title, 168 "content": issue.Content, 169 "comments": issue.Comments, 170 }). 171 Do(graceful.GetManager().HammerContext()) 172 return b.checkError(err) 173 } 174 175 reqs := make([]elastic.BulkableRequest, 0) 176 for _, issue := range issues { 177 reqs = append(reqs, 178 elastic.NewBulkIndexRequest(). 179 Index(b.indexerName). 180 Id(fmt.Sprintf("%d", issue.ID)). 181 Doc(map[string]interface{}{ 182 "id": issue.ID, 183 "repo_id": issue.RepoID, 184 "title": issue.Title, 185 "content": issue.Content, 186 "comments": issue.Comments, 187 }), 188 ) 189 } 190 191 _, err := b.client.Bulk(). 192 Index(b.indexerName). 193 Add(reqs...). 194 Do(graceful.GetManager().HammerContext()) 195 return b.checkError(err) 196 } 197 198 // Delete deletes indexes by ids 199 func (b *ElasticSearchIndexer) Delete(ids ...int64) error { 200 if len(ids) == 0 { 201 return nil 202 } else if len(ids) == 1 { 203 _, err := b.client.Delete(). 204 Index(b.indexerName). 205 Id(fmt.Sprintf("%d", ids[0])). 206 Do(graceful.GetManager().HammerContext()) 207 return b.checkError(err) 208 } 209 210 reqs := make([]elastic.BulkableRequest, 0) 211 for _, id := range ids { 212 reqs = append(reqs, 213 elastic.NewBulkDeleteRequest(). 214 Index(b.indexerName). 215 Id(fmt.Sprintf("%d", id)), 216 ) 217 } 218 219 _, err := b.client.Bulk(). 220 Index(b.indexerName). 221 Add(reqs...). 222 Do(graceful.GetManager().HammerContext()) 223 return b.checkError(err) 224 } 225 226 // Search searches for issues by given conditions. 227 // Returns the matching issue IDs 228 func (b *ElasticSearchIndexer) Search(ctx context.Context, keyword string, repoIDs []int64, limit, start int) (*SearchResult, error) { 229 kwQuery := elastic.NewMultiMatchQuery(keyword, "title", "content", "comments") 230 query := elastic.NewBoolQuery() 231 query = query.Must(kwQuery) 232 if len(repoIDs) > 0 { 233 repoStrs := make([]interface{}, 0, len(repoIDs)) 234 for _, repoID := range repoIDs { 235 repoStrs = append(repoStrs, repoID) 236 } 237 repoQuery := elastic.NewTermsQuery("repo_id", repoStrs...) 238 query = query.Must(repoQuery) 239 } 240 searchResult, err := b.client.Search(). 241 Index(b.indexerName). 242 Query(query). 243 Sort("_score", false). 244 From(start).Size(limit). 245 Do(ctx) 246 if err != nil { 247 return nil, b.checkError(err) 248 } 249 250 hits := make([]Match, 0, limit) 251 for _, hit := range searchResult.Hits.Hits { 252 id, _ := strconv.ParseInt(hit.Id, 10, 64) 253 hits = append(hits, Match{ 254 ID: id, 255 }) 256 } 257 258 return &SearchResult{ 259 Total: searchResult.TotalHits(), 260 Hits: hits, 261 }, nil 262 } 263 264 // Close implements indexer 265 func (b *ElasticSearchIndexer) Close() { 266 select { 267 case <-b.stopTimer: 268 default: 269 close(b.stopTimer) 270 } 271 } 272 273 func (b *ElasticSearchIndexer) checkError(err error) error { 274 var opErr *net.OpError 275 if !(elastic.IsConnErr(err) || (errors.As(err, &opErr) && (opErr.Op == "dial" || opErr.Op == "read"))) { 276 return err 277 } 278 279 b.setAvailability(false) 280 281 return err 282 } 283 284 func (b *ElasticSearchIndexer) checkAvailability() { 285 if b.Ping() { 286 return 287 } 288 289 // Request cluster state to check if elastic is available again 290 _, err := b.client.ClusterState().Do(graceful.GetManager().ShutdownContext()) 291 if err != nil { 292 b.setAvailability(false) 293 return 294 } 295 296 b.setAvailability(true) 297 } 298 299 func (b *ElasticSearchIndexer) setAvailability(available bool) { 300 b.lock.Lock() 301 defer b.lock.Unlock() 302 303 if b.available == available { 304 return 305 } 306 307 b.available = available 308 if b.availabilityCallback != nil { 309 // Call the callback from within the lock to ensure that the ordering remains correct 310 b.availabilityCallback(b.available) 311 } 312 }