2024-10-08 09:43:23 -04:00
|
|
|
package resource
|
|
|
|
|
|
|
|
|
|
import (
|
|
|
|
|
"context"
|
|
|
|
|
"encoding/json"
|
|
|
|
|
"fmt"
|
2024-10-10 11:34:57 -06:00
|
|
|
golog "log"
|
2024-10-08 09:43:23 -04:00
|
|
|
"os"
|
2024-10-09 11:20:05 -06:00
|
|
|
"strings"
|
2024-10-08 09:43:23 -04:00
|
|
|
|
|
|
|
|
"github.com/blevesearch/bleve/v2"
|
2024-10-08 13:09:56 -04:00
|
|
|
"github.com/blevesearch/bleve/v2/analysis/lang/en"
|
2024-10-08 09:43:23 -04:00
|
|
|
"github.com/blevesearch/bleve/v2/mapping"
|
|
|
|
|
"github.com/google/uuid"
|
2024-10-10 11:34:57 -06:00
|
|
|
"github.com/grafana/grafana/pkg/infra/log"
|
2024-10-09 11:20:05 -06:00
|
|
|
"golang.org/x/exp/slices"
|
2024-10-08 09:43:23 -04:00
|
|
|
)
|
|
|
|
|
|
|
|
|
|
type Shard struct {
|
|
|
|
|
index bleve.Index
|
|
|
|
|
path string
|
|
|
|
|
batch *bleve.Batch
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
type Index struct {
|
|
|
|
|
shards map[string]Shard
|
|
|
|
|
opts Opts
|
|
|
|
|
s *server
|
2024-10-10 11:34:57 -06:00
|
|
|
log log.Logger
|
2024-10-08 09:43:23 -04:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func NewIndex(s *server, opts Opts) *Index {
|
|
|
|
|
idx := &Index{
|
|
|
|
|
s: s,
|
|
|
|
|
opts: opts,
|
|
|
|
|
shards: make(map[string]Shard),
|
2024-10-10 11:34:57 -06:00
|
|
|
log: log.New("unifiedstorage.search.index"),
|
2024-10-08 09:43:23 -04:00
|
|
|
}
|
|
|
|
|
return idx
|
|
|
|
|
}
|
|
|
|
|
|
2024-10-11 02:13:10 -06:00
|
|
|
func (i *Index) IndexBatch(list *ListResponse, kind string) error {
|
|
|
|
|
for _, obj := range list.Items {
|
|
|
|
|
res, err := getResource(obj.Value)
|
2024-10-08 09:43:23 -04:00
|
|
|
if err != nil {
|
|
|
|
|
return err
|
|
|
|
|
}
|
|
|
|
|
|
2024-10-11 02:13:10 -06:00
|
|
|
shard, err := i.getShard(tenant(res))
|
|
|
|
|
if err != nil {
|
|
|
|
|
return err
|
|
|
|
|
}
|
|
|
|
|
i.log.Debug("initial indexing resources batch", "count", len(list.Items), "kind", kind, "tenant", tenant(res))
|
2024-10-08 09:43:23 -04:00
|
|
|
|
2024-10-11 02:13:10 -06:00
|
|
|
var jsonDoc interface{}
|
|
|
|
|
err = json.Unmarshal(obj.Value, &jsonDoc)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return err
|
|
|
|
|
}
|
|
|
|
|
err = shard.batch.Index(res.Metadata.Uid, jsonDoc)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return err
|
|
|
|
|
}
|
|
|
|
|
}
|
2024-10-08 13:09:56 -04:00
|
|
|
|
2024-10-11 02:13:10 -06:00
|
|
|
for _, shard := range i.shards {
|
|
|
|
|
err := shard.index.Batch(shard.batch)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return err
|
|
|
|
|
}
|
|
|
|
|
shard.batch.Reset()
|
|
|
|
|
}
|
2024-10-10 11:34:57 -06:00
|
|
|
|
2024-10-11 02:13:10 -06:00
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (i *Index) Init(ctx context.Context) error {
|
|
|
|
|
resourceTypes := fetchResourceTypes()
|
|
|
|
|
for _, rt := range resourceTypes {
|
|
|
|
|
i.log.Info("indexing resource", "kind", rt.Key.Resource)
|
|
|
|
|
r := &ListRequest{Options: rt, Limit: 100}
|
|
|
|
|
|
|
|
|
|
// Paginate through the list of resources and index each page
|
|
|
|
|
for {
|
|
|
|
|
list, err := i.s.List(ctx, r)
|
2024-10-08 13:09:56 -04:00
|
|
|
if err != nil {
|
|
|
|
|
return err
|
|
|
|
|
}
|
2024-10-11 02:13:10 -06:00
|
|
|
|
|
|
|
|
// Index current page
|
|
|
|
|
err = i.IndexBatch(list, rt.Key.Resource)
|
2024-10-08 09:43:23 -04:00
|
|
|
if err != nil {
|
|
|
|
|
return err
|
|
|
|
|
}
|
|
|
|
|
|
2024-10-11 02:13:10 -06:00
|
|
|
if list.NextPageToken == "" {
|
|
|
|
|
break
|
2024-10-08 09:43:23 -04:00
|
|
|
}
|
2024-10-11 02:13:10 -06:00
|
|
|
|
|
|
|
|
r.NextPageToken = list.NextPageToken
|
2024-10-08 09:43:23 -04:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (i *Index) Index(ctx context.Context, data *Data) error {
|
|
|
|
|
res, err := getResource(data.Value.Value)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return err
|
|
|
|
|
}
|
|
|
|
|
tenant := tenant(res)
|
2024-10-11 02:13:10 -06:00
|
|
|
i.log.Debug("indexing resource for tenant", "res", res, "tenant", tenant)
|
2024-10-08 09:43:23 -04:00
|
|
|
shard, err := i.getShard(tenant)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return err
|
|
|
|
|
}
|
2024-10-08 16:17:31 -04:00
|
|
|
var jsonDoc interface{}
|
|
|
|
|
err = json.Unmarshal(data.Value.Value, &jsonDoc)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return err
|
|
|
|
|
}
|
|
|
|
|
err = shard.index.Index(res.Metadata.Uid, jsonDoc)
|
2024-10-08 09:43:23 -04:00
|
|
|
if err != nil {
|
|
|
|
|
return err
|
|
|
|
|
}
|
|
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (i *Index) Delete(ctx context.Context, uid string, key *ResourceKey) error {
|
|
|
|
|
shard, err := i.getShard(key.Namespace)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return err
|
|
|
|
|
}
|
|
|
|
|
err = shard.index.Delete(uid)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return err
|
|
|
|
|
}
|
|
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
|
2024-10-09 11:20:05 -06:00
|
|
|
func (i *Index) Search(ctx context.Context, tenant string, query string, limit int, offset int) ([]SearchSummary, error) {
|
2024-10-08 13:09:56 -04:00
|
|
|
if tenant == "" {
|
|
|
|
|
tenant = "default"
|
|
|
|
|
}
|
|
|
|
|
shard, err := i.getShard(tenant)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return nil, err
|
|
|
|
|
}
|
2024-10-10 11:34:57 -06:00
|
|
|
docCount, err := shard.index.DocCount()
|
|
|
|
|
if err != nil {
|
|
|
|
|
return nil, err
|
|
|
|
|
}
|
|
|
|
|
i.log.Info("got index for tenant", "tenant", tenant, "docCount", docCount)
|
2024-10-09 11:20:05 -06:00
|
|
|
|
|
|
|
|
// use 10 as a default limit for now
|
|
|
|
|
if limit <= 0 {
|
|
|
|
|
limit = 10
|
|
|
|
|
}
|
|
|
|
|
|
2024-10-08 13:09:56 -04:00
|
|
|
req := bleve.NewSearchRequest(bleve.NewQueryStringQuery(query))
|
2024-10-09 11:20:05 -06:00
|
|
|
req.From = offset
|
|
|
|
|
req.Size = limit
|
|
|
|
|
|
|
|
|
|
req.Fields = []string{"*"} // return all indexed fields in search results
|
2024-10-08 13:09:56 -04:00
|
|
|
|
2024-10-10 11:34:57 -06:00
|
|
|
i.log.Info("searching index", "query", query, "tenant", tenant)
|
2024-10-08 13:09:56 -04:00
|
|
|
res, err := shard.index.Search(req)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return nil, err
|
|
|
|
|
}
|
|
|
|
|
hits := res.Hits
|
2024-10-09 11:20:05 -06:00
|
|
|
|
2024-10-10 11:34:57 -06:00
|
|
|
i.log.Info("got search results", "hits", hits)
|
|
|
|
|
|
2024-10-09 11:20:05 -06:00
|
|
|
results := make([]SearchSummary, len(hits))
|
|
|
|
|
for resKey, hit := range hits {
|
|
|
|
|
searchSummary := SearchSummary{}
|
|
|
|
|
|
|
|
|
|
// add common fields to search results
|
|
|
|
|
searchSummary.Kind = hit.Fields["kind"].(string)
|
|
|
|
|
searchSummary.Metadata.CreationTimestamp = hit.Fields["metadata.creationTimestamp"].(string)
|
|
|
|
|
searchSummary.Metadata.Uid = hit.Fields["metadata.uid"].(string)
|
|
|
|
|
|
|
|
|
|
// add allowed indexed spec fields to search results
|
|
|
|
|
specResult := map[string]interface{}{}
|
|
|
|
|
for k, v := range hit.Fields {
|
|
|
|
|
if strings.HasPrefix(k, "spec.") {
|
|
|
|
|
mappedFields := specFieldMappings(searchSummary.Kind)
|
|
|
|
|
// should only include spec fields we care about in search results
|
|
|
|
|
if slices.Contains(mappedFields, k) {
|
|
|
|
|
specKey := strings.TrimPrefix(k, "spec.")
|
|
|
|
|
specResult[specKey] = v
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
searchSummary.Spec = specResult
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
results[resKey] = searchSummary
|
2024-10-08 13:09:56 -04:00
|
|
|
}
|
2024-10-09 11:20:05 -06:00
|
|
|
|
2024-10-08 13:09:56 -04:00
|
|
|
return results, nil
|
|
|
|
|
}
|
|
|
|
|
|
2024-10-08 09:43:23 -04:00
|
|
|
func tenant(res *Resource) string {
|
|
|
|
|
return res.Metadata.Namespace
|
|
|
|
|
}
|
|
|
|
|
|
2024-10-09 11:20:05 -06:00
|
|
|
type SearchSummary struct {
|
|
|
|
|
Kind string `json:"kind"`
|
|
|
|
|
Metadata `json:"metadata"`
|
|
|
|
|
Spec map[string]interface{} `json:"spec"`
|
|
|
|
|
}
|
|
|
|
|
|
2024-10-08 09:43:23 -04:00
|
|
|
type Metadata struct {
|
|
|
|
|
Name string
|
|
|
|
|
Namespace string
|
2024-10-09 11:20:05 -06:00
|
|
|
Uid string `json:"uid"`
|
|
|
|
|
CreationTimestamp string `json:"creationTimestamp"`
|
2024-10-08 09:43:23 -04:00
|
|
|
Labels map[string]string
|
|
|
|
|
Annotations map[string]string
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
type Resource struct {
|
|
|
|
|
Kind string
|
|
|
|
|
ApiVersion string
|
|
|
|
|
Metadata Metadata
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
type Opts struct {
|
|
|
|
|
Workers int // This controls how many goroutines are used to index objects
|
|
|
|
|
BatchSize int // This is the batch size for how many objects to add to the index at once
|
|
|
|
|
Concurrent bool
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func createFileIndex() (bleve.Index, string, error) {
|
|
|
|
|
indexPath := fmt.Sprintf("%s%s.bleve", os.TempDir(), uuid.New().String())
|
|
|
|
|
index, err := bleve.New(indexPath, createIndexMappings())
|
|
|
|
|
if err != nil {
|
2024-10-10 11:34:57 -06:00
|
|
|
golog.Fatalf("Failed to create index: %v", err)
|
2024-10-08 09:43:23 -04:00
|
|
|
}
|
|
|
|
|
return index, indexPath, err
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func createIndexMappings() *mapping.IndexMappingImpl {
|
2024-10-09 11:20:05 -06:00
|
|
|
//Create mapping for the creationTimestamp field in the metadata
|
2024-10-08 09:43:23 -04:00
|
|
|
creationTimestampFieldMapping := bleve.NewDateTimeFieldMapping()
|
2024-10-09 11:20:05 -06:00
|
|
|
uidMapping := bleve.NewTextFieldMapping()
|
2024-10-08 09:43:23 -04:00
|
|
|
metaMapping := bleve.NewDocumentMapping()
|
|
|
|
|
metaMapping.AddFieldMappingsAt("creationTimestamp", creationTimestampFieldMapping)
|
2024-10-09 11:20:05 -06:00
|
|
|
metaMapping.AddFieldMappingsAt("uid", uidMapping)
|
2024-10-08 09:43:23 -04:00
|
|
|
metaMapping.Dynamic = false
|
2024-10-08 13:09:56 -04:00
|
|
|
metaMapping.Enabled = true
|
|
|
|
|
|
2024-10-09 11:20:05 -06:00
|
|
|
// Spec is different for all resources, so we create a dynamic mapping for it to index all fields (for now)
|
2024-10-08 13:09:56 -04:00
|
|
|
specMapping := bleve.NewDocumentMapping()
|
2024-10-09 11:20:05 -06:00
|
|
|
specMapping.Dynamic = true
|
2024-10-08 13:09:56 -04:00
|
|
|
specMapping.Enabled = true
|
2024-10-08 09:43:23 -04:00
|
|
|
|
|
|
|
|
//Create a sub-document mapping for the metadata field
|
|
|
|
|
objectMapping := bleve.NewDocumentMapping()
|
|
|
|
|
objectMapping.AddSubDocumentMapping("metadata", metaMapping)
|
2024-10-08 13:09:56 -04:00
|
|
|
objectMapping.AddSubDocumentMapping("spec", specMapping)
|
2024-10-09 11:20:05 -06:00
|
|
|
objectMapping.Dynamic = true
|
2024-10-08 13:09:56 -04:00
|
|
|
objectMapping.Enabled = true
|
|
|
|
|
|
|
|
|
|
// a generic reusable mapping for english text
|
|
|
|
|
englishTextFieldMapping := bleve.NewTextFieldMapping()
|
|
|
|
|
englishTextFieldMapping.Analyzer = en.AnalyzerName
|
2024-10-08 09:43:23 -04:00
|
|
|
|
|
|
|
|
// Map top level fields - just kind for now
|
2024-10-08 13:09:56 -04:00
|
|
|
objectMapping.AddFieldMappingsAt("kind", englishTextFieldMapping)
|
2024-10-08 09:43:23 -04:00
|
|
|
objectMapping.Dynamic = false
|
|
|
|
|
|
|
|
|
|
// Create the index mapping
|
|
|
|
|
indexMapping := bleve.NewIndexMapping()
|
|
|
|
|
indexMapping.DefaultMapping = objectMapping
|
|
|
|
|
|
|
|
|
|
return indexMapping
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func getResource(data []byte) (*Resource, error) {
|
|
|
|
|
res := &Resource{}
|
|
|
|
|
err := json.Unmarshal(data, res)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return nil, err
|
|
|
|
|
}
|
|
|
|
|
return res, nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (i *Index) getShard(tenant string) (Shard, error) {
|
|
|
|
|
shard, ok := i.shards[tenant]
|
|
|
|
|
if ok {
|
|
|
|
|
return shard, nil
|
|
|
|
|
}
|
|
|
|
|
index, path, err := createFileIndex()
|
|
|
|
|
if err != nil {
|
|
|
|
|
return Shard{}, err
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
shard = Shard{
|
|
|
|
|
index: index,
|
|
|
|
|
path: path,
|
|
|
|
|
batch: index.NewBatch(),
|
|
|
|
|
}
|
|
|
|
|
// TODO: do we need to lock this?
|
|
|
|
|
i.shards[tenant] = shard
|
|
|
|
|
return shard, nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// TODO - fetch from api
|
|
|
|
|
func fetchResourceTypes() []*ListOptions {
|
|
|
|
|
items := []*ListOptions{}
|
|
|
|
|
items = append(items, &ListOptions{
|
|
|
|
|
Key: &ResourceKey{
|
|
|
|
|
Group: "playlist.grafana.app",
|
|
|
|
|
Resource: "playlists",
|
|
|
|
|
},
|
|
|
|
|
})
|
|
|
|
|
return items
|
|
|
|
|
}
|
2024-10-09 11:20:05 -06:00
|
|
|
|
|
|
|
|
func specFieldMappings(kind string) []string {
|
|
|
|
|
mappedFields := map[string][]string{
|
|
|
|
|
"Playlist": {
|
|
|
|
|
"spec.title",
|
|
|
|
|
"spec.interval",
|
|
|
|
|
},
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return mappedFields[kind]
|
|
|
|
|
}
|