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  }