// Copyright 2023 The Gitea Authors. All rights reserved. // SPDX-License-Identifier: MIT package issues import ( "context" "strconv" "strings" "sync" "time" "github.com/meilisearch/meilisearch-go" ) var _ Indexer = &MeilisearchIndexer{} // MeilisearchIndexer implements Indexer interface type MeilisearchIndexer struct { client *meilisearch.Client indexerName string available bool stopTimer chan struct{} lock sync.RWMutex } // MeilisearchIndexer creates a new meilisearch indexer func NewMeilisearchIndexer(url, apiKey, indexerName string) (*MeilisearchIndexer, error) { client := meilisearch.NewClient(meilisearch.ClientConfig{ Host: url, APIKey: apiKey, }) indexer := &MeilisearchIndexer{ client: client, indexerName: indexerName, available: true, stopTimer: make(chan struct{}), } ticker := time.NewTicker(10 * time.Second) go func() { for { select { case <-ticker.C: indexer.checkAvailability() case <-indexer.stopTimer: ticker.Stop() return } } }() return indexer, nil } // Init will initialize the indexer func (b *MeilisearchIndexer) Init() (bool, error) { _, err := b.client.GetIndex(b.indexerName) if err == nil { return true, nil } _, err = b.client.CreateIndex(&meilisearch.IndexConfig{ Uid: b.indexerName, PrimaryKey: "id", }) if err != nil { return false, b.checkError(err) } _, err = b.client.Index(b.indexerName).UpdateFilterableAttributes(&[]string{"repo_id"}) return false, b.checkError(err) } // Ping checks if meilisearch is available func (b *MeilisearchIndexer) Ping() bool { b.lock.RLock() defer b.lock.RUnlock() return b.available } // Index will save the index data func (b *MeilisearchIndexer) Index(issues []*IndexerData) error { if len(issues) == 0 { return nil } for _, issue := range issues { _, err := b.client.Index(b.indexerName).AddDocuments(issue) if err != nil { return b.checkError(err) } } // TODO: bulk send index data return nil } // Delete deletes indexes by ids func (b *MeilisearchIndexer) Delete(ids ...int64) error { if len(ids) == 0 { return nil } for _, id := range ids { _, err := b.client.Index(b.indexerName).DeleteDocument(strconv.FormatInt(id, 10)) if err != nil { return b.checkError(err) } } // TODO: bulk send deletes return nil } // Search searches for issues by given conditions. // Returns the matching issue IDs func (b *MeilisearchIndexer) Search(ctx context.Context, keyword string, repoIDs []int64, limit, start int) (*SearchResult, error) { repoFilters := make([]string, 0, len(repoIDs)) for _, repoID := range repoIDs { repoFilters = append(repoFilters, "repo_id = "+strconv.FormatInt(repoID, 10)) } filter := strings.Join(repoFilters, " OR ") searchRes, err := b.client.Index(b.indexerName).Search(keyword, &meilisearch.SearchRequest{ Filter: filter, Limit: int64(limit), Offset: int64(start), }) if err != nil { return nil, b.checkError(err) } hits := make([]Match, 0, len(searchRes.Hits)) for _, hit := range searchRes.Hits { hits = append(hits, Match{ ID: int64(hit.(map[string]any)["id"].(float64)), }) } return &SearchResult{ Total: searchRes.TotalHits, Hits: hits, }, nil } // Close implements indexer func (b *MeilisearchIndexer) Close() { select { case <-b.stopTimer: default: close(b.stopTimer) } } func (b *MeilisearchIndexer) checkError(err error) error { return err } func (b *MeilisearchIndexer) checkAvailability() { _, err := b.client.Health() if err != nil { b.setAvailability(false) return } b.setAvailability(true) } func (b *MeilisearchIndexer) setAvailability(available bool) { b.lock.Lock() defer b.lock.Unlock() if b.available == available { return } b.available = available }