Search: use only bluge-based search (#48968)
This commit is contained in:
+189
-126
@@ -30,145 +30,212 @@ const (
|
|||||||
documentFieldInternalID = "__internal_id" // only for migrations! (indexed as a string)
|
documentFieldInternalID = "__internal_id" // only for migrations! (indexed as a string)
|
||||||
)
|
)
|
||||||
|
|
||||||
func initBlugeIndex(dashboards []dashboard, logger log.Logger) (*bluge.Reader, error) {
|
func initIndex(dashboards []dashboard, logger log.Logger) (*bluge.Reader, *bluge.Writer, error) {
|
||||||
config := bluge.InMemoryOnlyConfig()
|
writer, err := bluge.OpenWriter(bluge.InMemoryOnlyConfig())
|
||||||
|
|
||||||
// open an index writer using the configuration
|
|
||||||
writer, err := bluge.OpenWriter(config)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("error opening writer: %v", err)
|
return nil, nil, fmt.Errorf("error opening writer: %v", err)
|
||||||
}
|
}
|
||||||
defer func() {
|
// Not closing Writer here since we use it later while processing dashboard change events.
|
||||||
err = writer.Close()
|
|
||||||
if err != nil {
|
|
||||||
logger.Error("Error closing bluge writer", "error", err)
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
|
|
||||||
start := time.Now()
|
|
||||||
|
|
||||||
logger.Info("Loading dashboards for bluge index", "elapsed", time.Since(start), "numDashboards", len(dashboards))
|
|
||||||
label := time.Now()
|
|
||||||
|
|
||||||
batch := bluge.NewBatch()
|
batch := bluge.NewBatch()
|
||||||
|
|
||||||
// First index the folders
|
start := time.Now()
|
||||||
|
label := start
|
||||||
|
|
||||||
|
// First index the folders to construct folderIdLookup.
|
||||||
folderIdLookup := make(map[int64]string, 50)
|
folderIdLookup := make(map[int64]string, 50)
|
||||||
for _, dashboard := range dashboards {
|
for _, dash := range dashboards {
|
||||||
if !dashboard.isFolder {
|
if !dash.isFolder {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
uid := dashboard.uid
|
doc := getFolderDashboardDoc(dash)
|
||||||
url := fmt.Sprintf("/dashboards/f/%s/%s", dashboard.uid, dashboard.slug)
|
batch.Insert(doc)
|
||||||
|
uid := dash.uid
|
||||||
if uid == "" {
|
if uid == "" {
|
||||||
uid = "general"
|
uid = "general"
|
||||||
url = "/dashboards"
|
|
||||||
dashboard.info.Title = "General"
|
|
||||||
dashboard.info.Description = ""
|
|
||||||
|
|
||||||
// ARRRG, why is this not in the final index?!!
|
|
||||||
}
|
}
|
||||||
|
folderIdLookup[dash.id] = uid
|
||||||
doc := bluge.NewDocument(uid).
|
|
||||||
AddField(bluge.NewKeywordField(documentFieldKind, string(entityKindFolder)).Aggregatable().StoreValue()).
|
|
||||||
AddField(bluge.NewKeywordField(documentFieldURL, url).StoreValue()).
|
|
||||||
AddField(bluge.NewTextField(documentFieldName, dashboard.info.Title).StoreValue().SearchTermPositions()).
|
|
||||||
AddField(bluge.NewTextField(documentFieldDescription, dashboard.info.Description).SearchTermPositions())
|
|
||||||
|
|
||||||
batch.Insert(doc)
|
|
||||||
|
|
||||||
folderIdLookup[dashboard.id] = uid
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Then each dashboard
|
// Then each dashboard.
|
||||||
for _, dashboard := range dashboards {
|
for _, dash := range dashboards {
|
||||||
if dashboard.isFolder {
|
if dash.isFolder {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
folderUID := folderIdLookup[dash.folderID]
|
||||||
url := fmt.Sprintf("/d/%s/%s", dashboard.uid, dashboard.slug)
|
|
||||||
folderUID := folderIdLookup[dashboard.folderID]
|
|
||||||
location := folderUID
|
location := folderUID
|
||||||
|
doc := getNonFolderDashboardDoc(dash, location)
|
||||||
// Dashboard document
|
|
||||||
doc := bluge.NewDocument(dashboard.uid).
|
|
||||||
AddField(bluge.NewKeywordField(documentFieldKind, string(entityKindDashboard)).Aggregatable().StoreValue()).
|
|
||||||
AddField(bluge.NewKeywordField(documentFieldURL, url).StoreValue()).
|
|
||||||
AddField(bluge.NewKeywordField(documentFieldLocation, location).Aggregatable().StoreValue()).
|
|
||||||
AddField(bluge.NewTextField(documentFieldName, dashboard.info.Title).StoreValue().SearchTermPositions()).
|
|
||||||
AddField(bluge.NewTextField(documentFieldDescription, dashboard.info.Description).SearchTermPositions())
|
|
||||||
|
|
||||||
// Add legacy ID (for lookup by internal ID)
|
|
||||||
doc.AddField(bluge.NewKeywordField(documentFieldInternalID, fmt.Sprintf("%d", dashboard.id)))
|
|
||||||
|
|
||||||
for _, tag := range dashboard.info.Tags {
|
|
||||||
doc.AddField(bluge.NewKeywordField(documentFieldTag, tag).
|
|
||||||
StoreValue().
|
|
||||||
Aggregatable().
|
|
||||||
SearchTermPositions())
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, ds := range dashboard.info.Datasource {
|
|
||||||
if ds.UID != "" {
|
|
||||||
doc.AddField(bluge.NewKeywordField(documentFieldDSUID, ds.UID).
|
|
||||||
StoreValue().
|
|
||||||
Aggregatable().
|
|
||||||
SearchTermPositions())
|
|
||||||
}
|
|
||||||
if ds.Type != "" {
|
|
||||||
doc.AddField(bluge.NewKeywordField(documentFieldDSType, ds.Type).
|
|
||||||
StoreValue().
|
|
||||||
Aggregatable().
|
|
||||||
SearchTermPositions())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO: enterprise, add dashboard sorting fields
|
|
||||||
|
|
||||||
batch.Insert(doc)
|
batch.Insert(doc)
|
||||||
|
|
||||||
location += "/" + dashboard.uid
|
// Index each panel in dashboard.
|
||||||
|
location += "/" + dash.uid
|
||||||
// Now add a doc for each panel
|
docs := getDashboardPanelDocs(dash, location)
|
||||||
for _, panel := range dashboard.info.Panels {
|
for _, panelDoc := range docs {
|
||||||
uid := dashboard.uid + "#" + strconv.FormatInt(panel.ID, 10)
|
batch.Insert(panelDoc)
|
||||||
purl := url
|
|
||||||
if panel.Type != "row" {
|
|
||||||
purl = fmt.Sprintf("%s?viewPanel=%d", url, panel.ID)
|
|
||||||
}
|
|
||||||
|
|
||||||
doc := bluge.NewDocument(uid).
|
|
||||||
AddField(bluge.NewKeywordField(documentFieldURL, purl).StoreValue()).
|
|
||||||
AddField(bluge.NewTextField(documentFieldName, panel.Title).StoreValue().SearchTermPositions()).
|
|
||||||
AddField(bluge.NewTextField(documentFieldDescription, panel.Description).SearchTermPositions()).
|
|
||||||
AddField(bluge.NewKeywordField(documentFieldPanelType, panel.Type).Aggregatable().StoreValue()).
|
|
||||||
AddField(bluge.NewKeywordField(documentFieldLocation, location).Aggregatable().StoreValue()).
|
|
||||||
AddField(bluge.NewKeywordField(documentFieldKind, string(entityKindPanel)).Aggregatable().StoreValue()) // likely want independent index for this
|
|
||||||
|
|
||||||
batch.Insert(doc)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
logger.Info("Finish inserting docs into batch", "elapsed", time.Since(label))
|
||||||
logger.Info("Inserting documents into bluge batch", "elapsed", time.Since(label))
|
|
||||||
label = time.Now()
|
label = time.Now()
|
||||||
|
|
||||||
err = writer.Batch(batch)
|
err = writer.Batch(batch)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, nil, err
|
||||||
}
|
}
|
||||||
|
logger.Info("Finish writing batch", "elapsed", time.Since(label))
|
||||||
|
|
||||||
reader, err := writer.Reader()
|
reader, err := writer.Reader()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
logger.Info("Inserting batch into bluge writer", "elapsed", time.Since(label))
|
logger.Info("Finish building index", "totalElapsed", time.Since(start))
|
||||||
logger.Info("Finish building bluge index", "totalElapsed", time.Since(start))
|
return reader, writer, err
|
||||||
return reader, err
|
}
|
||||||
|
|
||||||
|
func getFolderDashboardDoc(dash dashboard) *bluge.Document {
|
||||||
|
uid := dash.uid
|
||||||
|
url := fmt.Sprintf("/dashboards/f/%s/%s", dash.uid, dash.slug)
|
||||||
|
if uid == "" {
|
||||||
|
uid = "general"
|
||||||
|
url = "/dashboards"
|
||||||
|
dash.info.Title = "General"
|
||||||
|
dash.info.Description = ""
|
||||||
|
}
|
||||||
|
|
||||||
|
return bluge.NewDocument(uid).
|
||||||
|
AddField(bluge.NewKeywordField(documentFieldKind, string(entityKindFolder)).Aggregatable().StoreValue()).
|
||||||
|
AddField(bluge.NewKeywordField(documentFieldURL, url).StoreValue()).
|
||||||
|
AddField(bluge.NewTextField(documentFieldName, dash.info.Title).StoreValue().SearchTermPositions()).
|
||||||
|
AddField(bluge.NewTextField(documentFieldDescription, dash.info.Description).SearchTermPositions()).
|
||||||
|
// Add legacy ID (for lookup by internal ID)
|
||||||
|
AddField(bluge.NewKeywordField(documentFieldInternalID, fmt.Sprintf("%d", dash.id)).Aggregatable().StoreValue())
|
||||||
|
}
|
||||||
|
|
||||||
|
func getNonFolderDashboardDoc(dash dashboard, location string) *bluge.Document {
|
||||||
|
url := fmt.Sprintf("/d/%s/%s", dash.uid, dash.slug)
|
||||||
|
|
||||||
|
// Dashboard document
|
||||||
|
doc := bluge.NewDocument(dash.uid).
|
||||||
|
AddField(bluge.NewKeywordField(documentFieldKind, string(entityKindDashboard)).Aggregatable().StoreValue()).
|
||||||
|
AddField(bluge.NewKeywordField(documentFieldURL, url).StoreValue()).
|
||||||
|
AddField(bluge.NewKeywordField(documentFieldLocation, location).Aggregatable().StoreValue()).
|
||||||
|
AddField(bluge.NewTextField(documentFieldName, dash.info.Title).StoreValue().SearchTermPositions()).
|
||||||
|
AddField(bluge.NewTextField(documentFieldDescription, dash.info.Description).SearchTermPositions())
|
||||||
|
|
||||||
|
// Add legacy ID (for lookup by internal ID)
|
||||||
|
doc.AddField(bluge.NewKeywordField(documentFieldInternalID, fmt.Sprintf("%d", dash.id)))
|
||||||
|
|
||||||
|
for _, tag := range dash.info.Tags {
|
||||||
|
doc.AddField(bluge.NewKeywordField(documentFieldTag, tag).
|
||||||
|
StoreValue().
|
||||||
|
Aggregatable().
|
||||||
|
SearchTermPositions())
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, ds := range dash.info.Datasource {
|
||||||
|
if ds.UID != "" {
|
||||||
|
doc.AddField(bluge.NewKeywordField(documentFieldDSUID, ds.UID).
|
||||||
|
StoreValue().
|
||||||
|
Aggregatable().
|
||||||
|
SearchTermPositions())
|
||||||
|
}
|
||||||
|
if ds.Type != "" {
|
||||||
|
doc.AddField(bluge.NewKeywordField(documentFieldDSType, ds.Type).
|
||||||
|
StoreValue().
|
||||||
|
Aggregatable().
|
||||||
|
SearchTermPositions())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: enterprise, add dashboard sorting fields
|
||||||
|
|
||||||
|
return doc
|
||||||
|
}
|
||||||
|
|
||||||
|
func getDashboardPanelDocs(dash dashboard, location string) []*bluge.Document {
|
||||||
|
var docs []*bluge.Document
|
||||||
|
url := fmt.Sprintf("/d/%s/%s", dash.uid, dash.slug)
|
||||||
|
for _, panel := range dash.info.Panels {
|
||||||
|
uid := dash.uid + "#" + strconv.FormatInt(panel.ID, 10)
|
||||||
|
purl := url
|
||||||
|
if panel.Type != "row" {
|
||||||
|
purl = fmt.Sprintf("%s?viewPanel=%d", url, panel.ID)
|
||||||
|
}
|
||||||
|
|
||||||
|
doc := bluge.NewDocument(uid).
|
||||||
|
AddField(bluge.NewKeywordField(documentFieldURL, purl).StoreValue()).
|
||||||
|
AddField(bluge.NewKeywordField(documentFieldDSUID, dash.uid).StoreValue()).
|
||||||
|
AddField(bluge.NewTextField(documentFieldName, panel.Title).StoreValue().SearchTermPositions()).
|
||||||
|
AddField(bluge.NewTextField(documentFieldDescription, panel.Description).SearchTermPositions()).
|
||||||
|
AddField(bluge.NewKeywordField(documentFieldPanelType, panel.Type).Aggregatable().StoreValue()).
|
||||||
|
AddField(bluge.NewKeywordField(documentFieldLocation, location).Aggregatable().StoreValue()).
|
||||||
|
AddField(bluge.NewKeywordField(documentFieldKind, string(entityKindPanel)).Aggregatable().StoreValue()) // likely want independent index for this
|
||||||
|
|
||||||
|
docs = append(docs, doc)
|
||||||
|
}
|
||||||
|
return docs
|
||||||
|
}
|
||||||
|
|
||||||
|
func getDashboardFolderUID(reader *bluge.Reader, folderID int64) (string, error) {
|
||||||
|
fullQuery := bluge.NewBooleanQuery()
|
||||||
|
fullQuery.AddMust(bluge.NewTermQuery(strconv.FormatInt(folderID, 10)).SetField(documentFieldInternalID))
|
||||||
|
fullQuery.AddMust(bluge.NewTermQuery(string(entityKindFolder)).SetField(documentFieldKind))
|
||||||
|
req := bluge.NewAllMatches(fullQuery)
|
||||||
|
req.WithStandardAggregations()
|
||||||
|
documentMatchIterator, err := reader.Search(context.Background(), req)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
var uid string
|
||||||
|
match, err := documentMatchIterator.Next()
|
||||||
|
for err == nil && match != nil {
|
||||||
|
// load the identifier for this match
|
||||||
|
err = match.VisitStoredFields(func(field string, value []byte) bool {
|
||||||
|
if field == documentFieldUID {
|
||||||
|
uid = string(value)
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
// load the next document match
|
||||||
|
match, err = documentMatchIterator.Next()
|
||||||
|
}
|
||||||
|
return uid, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func getDashboardPanelIDs(reader *bluge.Reader, dashboardUID string) ([]string, error) {
|
||||||
|
var panelIDs []string
|
||||||
|
fullQuery := bluge.NewBooleanQuery()
|
||||||
|
fullQuery.AddMust(bluge.NewTermQuery(dashboardUID).SetField(documentFieldDSUID))
|
||||||
|
fullQuery.AddMust(bluge.NewTermQuery(string(entityKindPanel)).SetField(documentFieldKind))
|
||||||
|
req := bluge.NewAllMatches(fullQuery)
|
||||||
|
req.WithStandardAggregations()
|
||||||
|
documentMatchIterator, err := reader.Search(context.Background(), req)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
match, err := documentMatchIterator.Next()
|
||||||
|
for err == nil && match != nil {
|
||||||
|
// load the identifier for this match
|
||||||
|
err = match.VisitStoredFields(func(field string, value []byte) bool {
|
||||||
|
if field == documentFieldUID {
|
||||||
|
panelIDs = append(panelIDs, string(value))
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
// load the next document match
|
||||||
|
match, err = documentMatchIterator.Next()
|
||||||
|
}
|
||||||
|
return panelIDs, err
|
||||||
}
|
}
|
||||||
|
|
||||||
//nolint: gocyclo
|
//nolint: gocyclo
|
||||||
func doBlugeQuery(ctx context.Context, s *StandardSearchService, reader *bluge.Reader, filter ResourceFilter, q DashboardQuery) *backend.DataResponse {
|
func doSearchQuery(ctx context.Context, logger log.Logger, reader *bluge.Reader, filter ResourceFilter, q DashboardQuery) *backend.DataResponse {
|
||||||
response := &backend.DataResponse{}
|
response := &backend.DataResponse{}
|
||||||
|
|
||||||
// Folder listing structure
|
// Folder listing structure
|
||||||
@@ -194,7 +261,7 @@ func doBlugeQuery(ctx context.Context, s *StandardSearchService, reader *bluge.R
|
|||||||
|
|
||||||
hasConstraints := false
|
hasConstraints := false
|
||||||
fullQuery := bluge.NewBooleanQuery()
|
fullQuery := bluge.NewBooleanQuery()
|
||||||
fullQuery.AddMust(newPermissionFilter(filter, s.logger))
|
fullQuery.AddMust(newPermissionFilter(filter, logger))
|
||||||
|
|
||||||
// Only show dashboard / folders
|
// Only show dashboard / folders
|
||||||
if len(q.Kind) > 0 {
|
if len(q.Kind) > 0 {
|
||||||
@@ -208,19 +275,12 @@ func doBlugeQuery(ctx context.Context, s *StandardSearchService, reader *bluge.R
|
|||||||
|
|
||||||
// Explicit UID lookup (stars etc)
|
// Explicit UID lookup (stars etc)
|
||||||
if len(q.UIDs) > 0 {
|
if len(q.UIDs) > 0 {
|
||||||
|
count := len(q.UIDs) + 3
|
||||||
bq := bluge.NewBooleanQuery()
|
bq := bluge.NewBooleanQuery()
|
||||||
for _, v := range q.UIDs {
|
for i, v := range q.UIDs {
|
||||||
bq.AddShould(bluge.NewTermQuery(v).SetField(documentFieldUID))
|
bq.AddShould(bluge.NewTermQuery(v).
|
||||||
}
|
SetField(documentFieldUID).
|
||||||
fullQuery.AddMust(bq)
|
SetBoost(float64(count - i)))
|
||||||
hasConstraints = true
|
|
||||||
}
|
|
||||||
|
|
||||||
// Legacy lookup by internal ID
|
|
||||||
if len(q.IDs) > 0 {
|
|
||||||
bq := bluge.NewBooleanQuery()
|
|
||||||
for _, v := range q.IDs {
|
|
||||||
bq.AddShould(bluge.NewTermQuery(fmt.Sprintf("%d", v)).SetField(documentFieldInternalID))
|
|
||||||
}
|
}
|
||||||
fullQuery.AddMust(bq)
|
fullQuery.AddMust(bq)
|
||||||
hasConstraints = true
|
hasConstraints = true
|
||||||
@@ -293,7 +353,7 @@ func doBlugeQuery(ctx context.Context, s *StandardSearchService, reader *bluge.R
|
|||||||
// execute this search on the reader
|
// execute this search on the reader
|
||||||
documentMatchIterator, err := reader.Search(ctx, req)
|
documentMatchIterator, err := reader.Search(ctx, req)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
s.logger.Error("error executing search: %v", err)
|
logger.Error("error executing search: %v", err)
|
||||||
response.Error = err
|
response.Error = err
|
||||||
return response
|
return response
|
||||||
}
|
}
|
||||||
@@ -330,9 +390,9 @@ func doBlugeQuery(ctx context.Context, s *StandardSearchService, reader *bluge.R
|
|||||||
fTags.Name = "tags"
|
fTags.Name = "tags"
|
||||||
fExplain.Name = "explain"
|
fExplain.Name = "explain"
|
||||||
|
|
||||||
frame := data.NewFrame("Query results", fScore, fKind, fUID, fName, fPType, fURL, fTags, fDSUIDs, fLocation)
|
frame := data.NewFrame("Query results", fKind, fUID, fName, fPType, fURL, fTags, fDSUIDs, fLocation)
|
||||||
if q.Explain {
|
if q.Explain {
|
||||||
frame.Fields = append(frame.Fields, fExplain)
|
frame.Fields = append(frame.Fields, fScore, fExplain)
|
||||||
}
|
}
|
||||||
|
|
||||||
locationItems := make(map[string]bool, 50)
|
locationItems := make(map[string]bool, 50)
|
||||||
@@ -385,12 +445,11 @@ func doBlugeQuery(ctx context.Context, s *StandardSearchService, reader *bluge.R
|
|||||||
return true
|
return true
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
s.logger.Error("error loading stored fields: %v", err)
|
logger.Error("error loading stored fields: %v", err)
|
||||||
response.Error = err
|
response.Error = err
|
||||||
return response
|
return response
|
||||||
}
|
}
|
||||||
|
|
||||||
fScore.Append(match.Score)
|
|
||||||
fKind.Append(kind)
|
fKind.Append(kind)
|
||||||
fUID.Append(uid)
|
fUID.Append(uid)
|
||||||
fPType.Append(ptype)
|
fPType.Append(ptype)
|
||||||
@@ -422,6 +481,7 @@ func doBlugeQuery(ctx context.Context, s *StandardSearchService, reader *bluge.R
|
|||||||
}
|
}
|
||||||
|
|
||||||
if q.Explain {
|
if q.Explain {
|
||||||
|
fScore.Append(match.Score)
|
||||||
if match.Explanation != nil {
|
if match.Explanation != nil {
|
||||||
js, _ := json.Marshal(&match.Explanation)
|
js, _ := json.Marshal(&match.Explanation)
|
||||||
jsb := json.RawMessage(js)
|
jsb := json.RawMessage(js)
|
||||||
@@ -439,9 +499,12 @@ func doBlugeQuery(ctx context.Context, s *StandardSearchService, reader *bluge.R
|
|||||||
aggs := documentMatchIterator.Aggregations()
|
aggs := documentMatchIterator.Aggregations()
|
||||||
|
|
||||||
header := &customMeta{
|
header := &customMeta{
|
||||||
Count: aggs.Count(), // Total cound
|
Count: aggs.Count(), // Total cound
|
||||||
MaxScore: aggs.Metric("max_score"),
|
|
||||||
}
|
}
|
||||||
|
if q.Explain {
|
||||||
|
header.MaxScore = aggs.Metric("max_score")
|
||||||
|
}
|
||||||
|
|
||||||
if len(locationItems) > 0 && !q.SkipLocation {
|
if len(locationItems) > 0 && !q.SkipLocation {
|
||||||
header.Locations = getLocationLookupInfo(ctx, reader, locationItems)
|
header.Locations = getLocationLookupInfo(ctx, reader, locationItems)
|
||||||
}
|
}
|
||||||
|
|||||||
+160
-118
@@ -31,15 +31,6 @@ type eventStore interface {
|
|||||||
GetAllEventsAfter(ctx context.Context, id int64) ([]*store.EntityEvent, error)
|
GetAllEventsAfter(ctx context.Context, id int64) ([]*store.EntityEvent, error)
|
||||||
}
|
}
|
||||||
|
|
||||||
type dashboardIndex struct {
|
|
||||||
mu sync.RWMutex
|
|
||||||
loader dashboardLoader
|
|
||||||
dashboards map[int64][]dashboard // orgId -> []dashboards
|
|
||||||
reader map[int64]*bluge.Reader // orgId -> bluge index
|
|
||||||
eventStore eventStore
|
|
||||||
logger log.Logger
|
|
||||||
}
|
|
||||||
|
|
||||||
type dashboard struct {
|
type dashboard struct {
|
||||||
id int64
|
id int64
|
||||||
uid string
|
uid string
|
||||||
@@ -51,13 +42,24 @@ type dashboard struct {
|
|||||||
info *extract.DashboardInfo
|
info *extract.DashboardInfo
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type dashboardIndex struct {
|
||||||
|
mu sync.RWMutex
|
||||||
|
loader dashboardLoader
|
||||||
|
perOrgReader map[int64]*bluge.Reader // orgId -> bluge reader
|
||||||
|
perOrgWriter map[int64]*bluge.Writer // orgId -> bluge writer
|
||||||
|
eventStore eventStore
|
||||||
|
logger log.Logger
|
||||||
|
buildSignals chan int64
|
||||||
|
}
|
||||||
|
|
||||||
func newDashboardIndex(dashLoader dashboardLoader, evStore eventStore) *dashboardIndex {
|
func newDashboardIndex(dashLoader dashboardLoader, evStore eventStore) *dashboardIndex {
|
||||||
return &dashboardIndex{
|
return &dashboardIndex{
|
||||||
loader: dashLoader,
|
loader: dashLoader,
|
||||||
eventStore: evStore,
|
eventStore: evStore,
|
||||||
dashboards: map[int64][]dashboard{},
|
perOrgReader: map[int64]*bluge.Reader{},
|
||||||
reader: map[int64]*bluge.Reader{},
|
perOrgWriter: map[int64]*bluge.Writer{},
|
||||||
logger: log.New("dashboardIndex"),
|
logger: log.New("dashboardIndex"),
|
||||||
|
buildSignals: make(chan int64),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -79,19 +81,26 @@ func (i *dashboardIndex) run(ctx context.Context) error {
|
|||||||
|
|
||||||
// Build on start for orgID 1 but keep lazy for others.
|
// Build on start for orgID 1 but keep lazy for others.
|
||||||
started := time.Now()
|
started := time.Now()
|
||||||
dashboards, err := i.getDashboards(ctx, 1)
|
numDashboards, err := i.buildOrgIndex(ctx, 1)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("can't build dashboard search index for org ID 1: %w", err)
|
return fmt.Errorf("can't build dashboard search index for org ID 1: %w", err)
|
||||||
}
|
}
|
||||||
i.logger.Info("Indexing for main org finished", "mainOrgIndexElapsed", time.Since(started), "numDashboards", len(dashboards))
|
i.logger.Info("Indexing for main org finished", "mainOrgIndexElapsed", time.Since(started), "numDashboards", numDashboards)
|
||||||
|
|
||||||
// build bluge index on startup (will catch panics)
|
|
||||||
go i.reIndexFromScratchBluge(ctx)
|
|
||||||
|
|
||||||
for {
|
for {
|
||||||
select {
|
select {
|
||||||
case <-partialUpdateTicker.C:
|
case <-partialUpdateTicker.C:
|
||||||
lastEventID = i.applyIndexUpdates(ctx, lastEventID)
|
lastEventID = i.applyIndexUpdates(ctx, lastEventID)
|
||||||
|
case orgID := <-i.buildSignals:
|
||||||
|
i.mu.RLock()
|
||||||
|
_, ok := i.perOrgWriter[orgID]
|
||||||
|
if ok {
|
||||||
|
// Index for org already exists, do nothing.
|
||||||
|
i.mu.RUnlock()
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
i.mu.RUnlock()
|
||||||
|
_, _ = i.buildOrgIndex(ctx, orgID)
|
||||||
case <-fullReIndexTicker.C:
|
case <-fullReIndexTicker.C:
|
||||||
started := time.Now()
|
started := time.Now()
|
||||||
i.reIndexFromScratch(ctx)
|
i.reIndexFromScratch(ctx)
|
||||||
@@ -102,82 +111,67 @@ func (i *dashboardIndex) run(ctx context.Context) error {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (i *dashboardIndex) reIndexFromScratch(ctx context.Context) {
|
func (i *dashboardIndex) buildOrgIndex(ctx context.Context, orgID int64) (int, error) {
|
||||||
i.mu.RLock()
|
started := time.Now()
|
||||||
orgIDs := make([]int64, 0, len(i.dashboards))
|
ctx, cancel := context.WithTimeout(ctx, time.Minute)
|
||||||
for orgID := range i.dashboards {
|
defer cancel()
|
||||||
orgIDs = append(orgIDs, orgID)
|
|
||||||
}
|
|
||||||
i.mu.RUnlock()
|
|
||||||
|
|
||||||
for _, orgID := range orgIDs {
|
i.logger.Info("Start building org index", "orgId", orgID)
|
||||||
started := time.Now()
|
dashboards, err := i.loader.LoadDashboards(ctx, orgID, "")
|
||||||
ctx, cancel := context.WithTimeout(ctx, time.Minute)
|
if err != nil {
|
||||||
dashboards, err := i.loader.LoadDashboards(ctx, orgID, "")
|
return 0, fmt.Errorf("error loading dashboards: %w", err)
|
||||||
if err != nil {
|
|
||||||
cancel()
|
|
||||||
i.logger.Error("Error re-indexing dashboards for organization", "orgId", orgID, "error", err)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
cancel()
|
|
||||||
i.logger.Info("Re-indexed dashboards for organization", "orgId", orgID, "orgReIndexElapsed", time.Since(started))
|
|
||||||
i.mu.Lock()
|
|
||||||
i.dashboards[orgID] = dashboards
|
|
||||||
i.mu.Unlock()
|
|
||||||
}
|
}
|
||||||
|
orgSearchIndexLoadTime := time.Since(started)
|
||||||
|
i.logger.Info("Finish loading org dashboards", "elapsed", orgSearchIndexLoadTime, "orgId", orgID)
|
||||||
|
|
||||||
|
reader, writer, err := initIndex(dashboards, i.logger)
|
||||||
|
if err != nil {
|
||||||
|
return 0, fmt.Errorf("error initializing index: %w", err)
|
||||||
|
}
|
||||||
|
orgSearchIndexTotalTime := time.Since(started)
|
||||||
|
orgSearchIndexBuildTime := orgSearchIndexTotalTime - orgSearchIndexLoadTime
|
||||||
|
|
||||||
|
i.logger.Info("Re-indexed dashboards for organization",
|
||||||
|
"orgId", orgID,
|
||||||
|
"orgSearchIndexLoadTime", orgSearchIndexLoadTime,
|
||||||
|
"orgSearchIndexBuildTime", orgSearchIndexBuildTime,
|
||||||
|
"orgSearchIndexTotalTime", orgSearchIndexTotalTime)
|
||||||
|
|
||||||
|
i.mu.Lock()
|
||||||
|
i.perOrgReader[orgID] = reader
|
||||||
|
i.perOrgWriter[orgID] = writer
|
||||||
|
i.mu.Unlock()
|
||||||
|
return len(dashboards), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Variation of the above function that builds bluge index from scratch
|
func (i *dashboardIndex) getOrgReader(orgID int64) (*bluge.Reader, bool) {
|
||||||
// Once the frontend is wired up, we should switch to this one
|
|
||||||
func (i *dashboardIndex) reIndexFromScratchBluge(ctx context.Context) {
|
|
||||||
// Catch Panic (just in case)
|
|
||||||
defer func() {
|
|
||||||
recv := recover()
|
|
||||||
if recv != nil {
|
|
||||||
i.logger.Error("panic in search runner", "recv", recv) // REMVOE after we are sure it works!
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
|
|
||||||
i.mu.RLock()
|
i.mu.RLock()
|
||||||
orgIDs := make([]int64, 0, len(i.dashboards))
|
defer i.mu.RUnlock()
|
||||||
for orgID := range i.dashboards {
|
r, ok := i.perOrgReader[orgID]
|
||||||
|
return r, ok
|
||||||
|
}
|
||||||
|
|
||||||
|
func (i *dashboardIndex) getOrgWriter(orgID int64) (*bluge.Writer, bool) {
|
||||||
|
i.mu.RLock()
|
||||||
|
defer i.mu.RUnlock()
|
||||||
|
w, ok := i.perOrgWriter[orgID]
|
||||||
|
return w, ok
|
||||||
|
}
|
||||||
|
|
||||||
|
func (i *dashboardIndex) reIndexFromScratch(ctx context.Context) {
|
||||||
|
i.mu.RLock()
|
||||||
|
orgIDs := make([]int64, 0, len(i.perOrgWriter))
|
||||||
|
for orgID := range i.perOrgWriter {
|
||||||
orgIDs = append(orgIDs, orgID)
|
orgIDs = append(orgIDs, orgID)
|
||||||
}
|
}
|
||||||
i.mu.RUnlock()
|
i.mu.RUnlock()
|
||||||
if len(orgIDs) < 1 {
|
|
||||||
orgIDs = append(orgIDs, int64(1)) // make sure we index
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, orgID := range orgIDs {
|
for _, orgID := range orgIDs {
|
||||||
started := time.Now()
|
_, err := i.buildOrgIndex(ctx, orgID)
|
||||||
ctx, cancel := context.WithTimeout(ctx, time.Minute)
|
|
||||||
|
|
||||||
dashboards, err := i.loader.LoadDashboards(ctx, orgID, "")
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
cancel()
|
|
||||||
i.logger.Error("Error re-indexing dashboards for organization", "orgId", orgID, "error", err)
|
i.logger.Error("Error re-indexing dashboards for organization", "orgId", orgID, "error", err)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
orgSearchIndexLoadTime := time.Since(started)
|
|
||||||
|
|
||||||
reader, err := initBlugeIndex(dashboards, i.logger)
|
|
||||||
if err != nil {
|
|
||||||
cancel()
|
|
||||||
i.logger.Error("Error re-indexing dashboards for organization", "orgId", orgID, "error", err)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
orgSearchIndexTotalTime := time.Since(started)
|
|
||||||
orgSearchIndexBuildTime := orgSearchIndexTotalTime - orgSearchIndexLoadTime
|
|
||||||
|
|
||||||
cancel()
|
|
||||||
i.logger.Info("Re-indexed dashboards for organization (bluge)",
|
|
||||||
"orgId", orgID,
|
|
||||||
"orgSearchIndexLoadTime", orgSearchIndexLoadTime,
|
|
||||||
"orgSearchIndexBuildTime", orgSearchIndexBuildTime,
|
|
||||||
"orgSearchIndexTotalTime", orgSearchIndexTotalTime)
|
|
||||||
i.mu.Lock()
|
|
||||||
i.reader[orgID] = reader
|
|
||||||
i.mu.Unlock()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -231,7 +225,7 @@ func (i *dashboardIndex) applyEventOnIndex(ctx context.Context, e *store.EntityE
|
|||||||
|
|
||||||
func (i *dashboardIndex) applyDashboardEvent(ctx context.Context, orgID int64, dashboardUID string, _ store.EntityEventType) error {
|
func (i *dashboardIndex) applyDashboardEvent(ctx context.Context, orgID int64, dashboardUID string, _ store.EntityEventType) error {
|
||||||
i.mu.Lock()
|
i.mu.Lock()
|
||||||
_, ok := i.dashboards[orgID]
|
_, ok := i.perOrgWriter[orgID]
|
||||||
if !ok {
|
if !ok {
|
||||||
// Skip event for org not yet indexed.
|
// Skip event for org not yet indexed.
|
||||||
i.mu.Unlock()
|
i.mu.Unlock()
|
||||||
@@ -247,64 +241,112 @@ func (i *dashboardIndex) applyDashboardEvent(ctx context.Context, orgID int64, d
|
|||||||
i.mu.Lock()
|
i.mu.Lock()
|
||||||
defer i.mu.Unlock()
|
defer i.mu.Unlock()
|
||||||
|
|
||||||
dashboards, ok := i.dashboards[orgID]
|
writer, ok := i.perOrgWriter[orgID]
|
||||||
|
if !ok {
|
||||||
|
// Skip event for org not yet fully indexed.
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
reader, ok := i.perOrgReader[orgID]
|
||||||
if !ok {
|
if !ok {
|
||||||
// Skip event for org not yet fully indexed.
|
// Skip event for org not yet fully indexed.
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var newReader *bluge.Reader
|
||||||
|
|
||||||
// In the future we can rely on operation types to reduce work here.
|
// In the future we can rely on operation types to reduce work here.
|
||||||
if len(dbDashboards) == 0 {
|
if len(dbDashboards) == 0 {
|
||||||
// Delete.
|
newReader, err = i.removeDashboard(writer, reader, dashboardUID)
|
||||||
i.dashboards[orgID] = removeDashboard(dashboards, dashboardUID)
|
|
||||||
} else {
|
} else {
|
||||||
updated := false
|
newReader, err = i.updateDashboard(writer, reader, dbDashboards[0])
|
||||||
for i, d := range dashboards {
|
|
||||||
if d.uid == dashboardUID {
|
|
||||||
// Update.
|
|
||||||
dashboards[i] = dbDashboards[0]
|
|
||||||
updated = true
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if !updated {
|
|
||||||
// Create.
|
|
||||||
dashboards = append(dashboards, dbDashboards...)
|
|
||||||
}
|
|
||||||
i.dashboards[orgID] = dashboards
|
|
||||||
}
|
}
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
i.perOrgReader[orgID] = newReader
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func removeDashboard(dashboards []dashboard, dashboardUID string) []dashboard {
|
func (i *dashboardIndex) removeDashboard(writer *bluge.Writer, reader *bluge.Reader, dashboardUID string) (*bluge.Reader, error) {
|
||||||
k := 0
|
// Find all panel docs to remove with dashboard.
|
||||||
for _, d := range dashboards {
|
panelIDs, err := getDashboardPanelIDs(reader, dashboardUID)
|
||||||
if d.uid != dashboardUID {
|
if err != nil {
|
||||||
dashboards[k] = d
|
return nil, err
|
||||||
k++
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
return dashboards[:k]
|
|
||||||
|
batch := bluge.NewBatch()
|
||||||
|
batch.Delete(bluge.NewDocument(dashboardUID).ID())
|
||||||
|
for _, panelID := range panelIDs {
|
||||||
|
batch.Delete(bluge.NewDocument(panelID).ID())
|
||||||
|
}
|
||||||
|
|
||||||
|
err = writer.Batch(batch)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return writer.Reader()
|
||||||
}
|
}
|
||||||
|
|
||||||
func (i *dashboardIndex) getDashboards(ctx context.Context, orgId int64) ([]dashboard, error) {
|
func stringInSlice(str string, slice []string) bool {
|
||||||
var dashboards []dashboard
|
for _, s := range slice {
|
||||||
|
if s == str {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
i.mu.Lock()
|
func (i *dashboardIndex) updateDashboard(writer *bluge.Writer, reader *bluge.Reader, dash dashboard) (*bluge.Reader, error) {
|
||||||
defer i.mu.Unlock()
|
batch := bluge.NewBatch()
|
||||||
|
|
||||||
if cachedDashboards, ok := i.dashboards[orgId]; ok {
|
var doc *bluge.Document
|
||||||
dashboards = cachedDashboards
|
if dash.isFolder {
|
||||||
|
doc = getFolderDashboardDoc(dash)
|
||||||
} else {
|
} else {
|
||||||
// Load and parse all dashboards for given orgId.
|
var folderUID string
|
||||||
var err error
|
if dash.folderID == 0 {
|
||||||
dashboards, err = i.loader.LoadDashboards(ctx, orgId, "")
|
folderUID = "general"
|
||||||
|
} else {
|
||||||
|
var err error
|
||||||
|
folderUID, err = getDashboardFolderUID(reader, dash.folderID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
location := folderUID
|
||||||
|
doc = getNonFolderDashboardDoc(dash, location)
|
||||||
|
|
||||||
|
var actualPanelIDs []string
|
||||||
|
|
||||||
|
location += "/" + dash.uid
|
||||||
|
panelDocs := getDashboardPanelDocs(dash, location)
|
||||||
|
for _, panelDoc := range panelDocs {
|
||||||
|
actualPanelIDs = append(actualPanelIDs, string(panelDoc.ID().Term()))
|
||||||
|
batch.Update(panelDoc.ID(), panelDoc)
|
||||||
|
}
|
||||||
|
|
||||||
|
indexedPanelIDs, err := getDashboardPanelIDs(reader, dash.uid)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
i.dashboards[orgId] = dashboards
|
|
||||||
|
for _, panelID := range indexedPanelIDs {
|
||||||
|
if !stringInSlice(panelID, actualPanelIDs) {
|
||||||
|
batch.Delete(bluge.NewDocument(panelID).ID())
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return dashboards, nil
|
|
||||||
|
batch.Update(doc.ID(), doc)
|
||||||
|
|
||||||
|
err := writer.Batch(batch)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return writer.Reader()
|
||||||
}
|
}
|
||||||
|
|
||||||
type sqlDashboardLoader struct {
|
type sqlDashboardLoader struct {
|
||||||
|
|||||||
@@ -2,9 +2,16 @@ package searchV2
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"flag"
|
||||||
|
"path/filepath"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
|
"github.com/grafana/grafana/pkg/infra/log"
|
||||||
|
"github.com/grafana/grafana/pkg/services/searchV2/extract"
|
||||||
"github.com/grafana/grafana/pkg/services/store"
|
"github.com/grafana/grafana/pkg/services/store"
|
||||||
|
|
||||||
|
"github.com/blugelabs/bluge"
|
||||||
|
"github.com/grafana/grafana-plugin-sdk-go/experimental"
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -12,83 +19,123 @@ type testDashboardLoader struct {
|
|||||||
dashboards []dashboard
|
dashboards []dashboard
|
||||||
}
|
}
|
||||||
|
|
||||||
func (t *testDashboardLoader) LoadDashboards(ctx context.Context, orgID int64, dashboardUID string) ([]dashboard, error) {
|
func (t *testDashboardLoader) LoadDashboards(_ context.Context, _ int64, _ string) ([]dashboard, error) {
|
||||||
return t.dashboards, nil
|
return t.dashboards, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestDashboardIndexCreate(t *testing.T) {
|
var testLogger = log.New("index-test-logger")
|
||||||
|
|
||||||
|
var testAllowAllFilter = func(uid string) bool {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
var testDisallowAllFilter = func(uid string) bool {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
var update = flag.Bool("update", false, "update golden files")
|
||||||
|
|
||||||
|
func initTestIndexFromDashes(t *testing.T, dashboards []dashboard) (*dashboardIndex, *bluge.Reader, *bluge.Writer) {
|
||||||
|
t.Helper()
|
||||||
dashboardLoader := &testDashboardLoader{
|
dashboardLoader := &testDashboardLoader{
|
||||||
dashboards: []dashboard{
|
dashboards: dashboards,
|
||||||
{
|
|
||||||
uid: "1",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
index := newDashboardIndex(dashboardLoader, &store.MockEntityEventsService{})
|
index := newDashboardIndex(dashboardLoader, &store.MockEntityEventsService{})
|
||||||
require.NotNil(t, index)
|
require.NotNil(t, index)
|
||||||
dashboards, err := index.getDashboards(context.Background(), 1)
|
numDashboards, err := index.buildOrgIndex(context.Background(), 1)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
require.Len(t, dashboards, 1)
|
require.Equal(t, len(dashboardLoader.dashboards), numDashboards)
|
||||||
|
reader, ok := index.getOrgReader(1)
|
||||||
|
require.True(t, ok)
|
||||||
|
writer, ok := index.getOrgWriter(1)
|
||||||
|
require.True(t, ok)
|
||||||
|
return index, reader, writer
|
||||||
|
}
|
||||||
|
|
||||||
dashboardLoader.dashboards = []dashboard{
|
func checkSearchResponse(t *testing.T, fileName string, reader *bluge.Reader, filter ResourceFilter, query DashboardQuery) {
|
||||||
{
|
t.Helper()
|
||||||
|
resp := doSearchQuery(context.Background(), testLogger, reader, filter, query)
|
||||||
|
goldenFile := filepath.Join("testdata", fileName)
|
||||||
|
err := experimental.CheckGoldenDataResponse(goldenFile, resp, *update)
|
||||||
|
require.NoError(t, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
var testDashboards = []dashboard{
|
||||||
|
{
|
||||||
|
id: 1,
|
||||||
|
uid: "1",
|
||||||
|
info: &extract.DashboardInfo{
|
||||||
|
Title: "test",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 2,
|
||||||
|
uid: "2",
|
||||||
|
info: &extract.DashboardInfo{
|
||||||
|
Title: "boom",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestDashboardIndex(t *testing.T) {
|
||||||
|
t.Run("basic-search", func(t *testing.T) {
|
||||||
|
_, reader, _ := initTestIndexFromDashes(t, testDashboards)
|
||||||
|
checkSearchResponse(t, filepath.Base(t.Name())+".txt", reader, testAllowAllFilter,
|
||||||
|
DashboardQuery{Query: "boom"},
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("basic-filter", func(t *testing.T) {
|
||||||
|
_, reader, _ := initTestIndexFromDashes(t, testDashboards)
|
||||||
|
checkSearchResponse(t, filepath.Base(t.Name())+".txt", reader, testDisallowAllFilter,
|
||||||
|
DashboardQuery{Query: "boom"},
|
||||||
|
)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestDashboardIndexUpdates(t *testing.T) {
|
||||||
|
t.Run("dashboard-delete", func(t *testing.T) {
|
||||||
|
index, reader, writer := initTestIndexFromDashes(t, testDashboards)
|
||||||
|
|
||||||
|
newReader, err := index.removeDashboard(writer, reader, "2")
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
checkSearchResponse(t, filepath.Base(t.Name())+".txt", newReader, testAllowAllFilter,
|
||||||
|
DashboardQuery{Query: "boom"},
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("dashboard-create", func(t *testing.T) {
|
||||||
|
index, reader, writer := initTestIndexFromDashes(t, testDashboards)
|
||||||
|
|
||||||
|
newReader, err := index.updateDashboard(writer, reader, dashboard{
|
||||||
|
id: 3,
|
||||||
|
uid: "3",
|
||||||
|
info: &extract.DashboardInfo{
|
||||||
|
Title: "created",
|
||||||
|
},
|
||||||
|
})
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
checkSearchResponse(t, filepath.Base(t.Name())+".txt", newReader, testAllowAllFilter,
|
||||||
|
DashboardQuery{Query: "created"},
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("dashboard-update", func(t *testing.T) {
|
||||||
|
index, reader, writer := initTestIndexFromDashes(t, testDashboards)
|
||||||
|
|
||||||
|
newReader, err := index.updateDashboard(writer, reader, dashboard{
|
||||||
|
id: 2,
|
||||||
uid: "2",
|
uid: "2",
|
||||||
},
|
info: &extract.DashboardInfo{
|
||||||
}
|
Title: "nginx",
|
||||||
err = index.applyDashboardEvent(context.Background(), 1, "2", "")
|
|
||||||
require.NoError(t, err)
|
|
||||||
dashboards, err = index.getDashboards(context.Background(), 1)
|
|
||||||
require.NoError(t, err)
|
|
||||||
require.Len(t, dashboards, 2)
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestDashboardIndexUpdate(t *testing.T) {
|
|
||||||
dashboardLoader := &testDashboardLoader{
|
|
||||||
dashboards: []dashboard{
|
|
||||||
{
|
|
||||||
uid: "1",
|
|
||||||
slug: "test",
|
|
||||||
},
|
},
|
||||||
},
|
})
|
||||||
}
|
require.NoError(t, err)
|
||||||
index := newDashboardIndex(dashboardLoader, nil)
|
|
||||||
require.NotNil(t, index)
|
|
||||||
dashboards, err := index.getDashboards(context.Background(), 1)
|
|
||||||
require.NoError(t, err)
|
|
||||||
require.Len(t, dashboards, 1)
|
|
||||||
|
|
||||||
dashboardLoader.dashboards = []dashboard{
|
checkSearchResponse(t, filepath.Base(t.Name())+".txt", newReader, testAllowAllFilter,
|
||||||
{
|
DashboardQuery{Query: "nginx"},
|
||||||
uid: "1",
|
)
|
||||||
slug: "updated",
|
})
|
||||||
},
|
|
||||||
}
|
|
||||||
err = index.applyDashboardEvent(context.Background(), 1, "1", "")
|
|
||||||
require.NoError(t, err)
|
|
||||||
dashboards, err = index.getDashboards(context.Background(), 1)
|
|
||||||
require.NoError(t, err)
|
|
||||||
require.Len(t, dashboards, 1)
|
|
||||||
require.Equal(t, "updated", dashboards[0].slug)
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestDashboardIndexDelete(t *testing.T) {
|
|
||||||
dashboardLoader := &testDashboardLoader{
|
|
||||||
dashboards: []dashboard{
|
|
||||||
{
|
|
||||||
uid: "1",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
index := newDashboardIndex(dashboardLoader, nil)
|
|
||||||
require.NotNil(t, index)
|
|
||||||
dashboards, err := index.getDashboards(context.Background(), 1)
|
|
||||||
require.NoError(t, err)
|
|
||||||
require.Len(t, dashboards, 1)
|
|
||||||
|
|
||||||
dashboardLoader.dashboards = []dashboard{}
|
|
||||||
err = index.applyDashboardEvent(context.Background(), 1, "1", "")
|
|
||||||
require.NoError(t, err)
|
|
||||||
dashboards, err = index.getDashboards(context.Background(), 1)
|
|
||||||
require.NoError(t, err)
|
|
||||||
require.Len(t, dashboards, 0)
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,23 +2,18 @@ package searchV2
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"encoding/json"
|
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
|
||||||
"strconv"
|
|
||||||
|
|
||||||
"github.com/grafana/grafana/pkg/infra/log"
|
"github.com/grafana/grafana/pkg/infra/log"
|
||||||
"github.com/grafana/grafana/pkg/models"
|
"github.com/grafana/grafana/pkg/models"
|
||||||
"github.com/grafana/grafana/pkg/registry"
|
"github.com/grafana/grafana/pkg/registry"
|
||||||
"github.com/grafana/grafana/pkg/services/accesscontrol"
|
"github.com/grafana/grafana/pkg/services/accesscontrol"
|
||||||
"github.com/grafana/grafana/pkg/services/featuremgmt"
|
"github.com/grafana/grafana/pkg/services/featuremgmt"
|
||||||
"github.com/grafana/grafana/pkg/services/searchV2/extract"
|
|
||||||
"github.com/grafana/grafana/pkg/services/sqlstore"
|
"github.com/grafana/grafana/pkg/services/sqlstore"
|
||||||
"github.com/grafana/grafana/pkg/services/store"
|
"github.com/grafana/grafana/pkg/services/store"
|
||||||
"github.com/grafana/grafana/pkg/setting"
|
"github.com/grafana/grafana/pkg/setting"
|
||||||
|
|
||||||
"github.com/grafana/grafana-plugin-sdk-go/backend"
|
"github.com/grafana/grafana-plugin-sdk-go/backend"
|
||||||
"github.com/grafana/grafana-plugin-sdk-go/data"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
type StandardSearchService struct {
|
type StandardSearchService struct {
|
||||||
@@ -105,9 +100,9 @@ func (s *StandardSearchService) getUser(ctx context.Context, backendUser *backen
|
|||||||
return user, nil
|
return user, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *StandardSearchService) DoDashboardQuery(ctx context.Context, user *backend.User, orgId int64, q DashboardQuery) *backend.DataResponse {
|
func (s *StandardSearchService) DoDashboardQuery(ctx context.Context, user *backend.User, orgID int64, q DashboardQuery) *backend.DataResponse {
|
||||||
rsp := &backend.DataResponse{}
|
rsp := &backend.DataResponse{}
|
||||||
signedInUser, err := s.getUser(ctx, user, orgId)
|
signedInUser, err := s.getUser(ctx, user, orgID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
rsp.Error = err
|
rsp.Error = err
|
||||||
return rsp
|
return rsp
|
||||||
@@ -119,219 +114,14 @@ func (s *StandardSearchService) DoDashboardQuery(ctx context.Context, user *back
|
|||||||
return rsp
|
return rsp
|
||||||
}
|
}
|
||||||
|
|
||||||
reader := s.dashboardIndex.reader[orgId]
|
reader, ok := s.dashboardIndex.getOrgReader(orgID)
|
||||||
if reader != nil && q.Query != "" { // frontend initializes with empty string
|
if !ok {
|
||||||
return doBlugeQuery(ctx, s, reader, filter, q)
|
go func() {
|
||||||
}
|
s.dashboardIndex.buildSignals <- orgID
|
||||||
|
}()
|
||||||
dashboards, err := s.dashboardIndex.getDashboards(ctx, orgId)
|
rsp.Error = errors.New("search index is not ready, try again later")
|
||||||
if err != nil {
|
|
||||||
rsp.Error = err
|
|
||||||
return rsp
|
return rsp
|
||||||
}
|
}
|
||||||
|
|
||||||
dashboards = s.applyAuthFilter(filter, dashboards)
|
return doSearchQuery(ctx, s.logger, reader, filter, q)
|
||||||
|
|
||||||
rsp.Frames = metaToFrame(dashboards)
|
|
||||||
|
|
||||||
return rsp
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *StandardSearchService) applyAuthFilter(filter ResourceFilter, dashboards []dashboard) []dashboard {
|
|
||||||
// create a list of all viewable dashboards for this user.
|
|
||||||
res := make([]dashboard, 0, len(dashboards))
|
|
||||||
for _, dash := range dashboards {
|
|
||||||
if filter(dash.uid) || (dash.isFolder && dash.uid == "") { // include the "General" folder
|
|
||||||
res = append(res, dash)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return res
|
|
||||||
}
|
|
||||||
|
|
||||||
type simpleCounter struct {
|
|
||||||
values map[string]int64
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *simpleCounter) add(key string) {
|
|
||||||
v, ok := c.values[key]
|
|
||||||
if !ok {
|
|
||||||
v = 0
|
|
||||||
}
|
|
||||||
c.values[key] = v + 1
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *simpleCounter) toFrame(name string) *data.Frame {
|
|
||||||
key := data.NewFieldFromFieldType(data.FieldTypeString, 0)
|
|
||||||
val := data.NewFieldFromFieldType(data.FieldTypeInt64, 0)
|
|
||||||
for k, v := range c.values {
|
|
||||||
key.Append(k)
|
|
||||||
val.Append(v)
|
|
||||||
}
|
|
||||||
return data.NewFrame(name, key, val)
|
|
||||||
}
|
|
||||||
|
|
||||||
// UGLY... but helpful for now
|
|
||||||
func metaToFrame(meta []dashboard) data.Frames {
|
|
||||||
folderID := data.NewFieldFromFieldType(data.FieldTypeInt64, 0)
|
|
||||||
folderUID := data.NewFieldFromFieldType(data.FieldTypeString, 0)
|
|
||||||
folderName := data.NewFieldFromFieldType(data.FieldTypeString, 0)
|
|
||||||
folderDashCount := data.NewFieldFromFieldType(data.FieldTypeInt64, 0)
|
|
||||||
|
|
||||||
folderID.Name = "id"
|
|
||||||
folderUID.Name = "uid"
|
|
||||||
folderName.Name = "name"
|
|
||||||
folderDashCount.Name = "DashCount"
|
|
||||||
|
|
||||||
dashID := data.NewFieldFromFieldType(data.FieldTypeInt64, 0)
|
|
||||||
dashUID := data.NewFieldFromFieldType(data.FieldTypeString, 0)
|
|
||||||
dashURL := data.NewFieldFromFieldType(data.FieldTypeString, 0)
|
|
||||||
dashFolderID := data.NewFieldFromFieldType(data.FieldTypeInt64, 0)
|
|
||||||
dashName := data.NewFieldFromFieldType(data.FieldTypeString, 0)
|
|
||||||
dashDescr := data.NewFieldFromFieldType(data.FieldTypeString, 0)
|
|
||||||
dashCreated := data.NewFieldFromFieldType(data.FieldTypeTime, 0)
|
|
||||||
dashUpdated := data.NewFieldFromFieldType(data.FieldTypeTime, 0)
|
|
||||||
dashSchemaVersion := data.NewFieldFromFieldType(data.FieldTypeInt64, 0)
|
|
||||||
dashTags := data.NewFieldFromFieldType(data.FieldTypeNullableString, 0)
|
|
||||||
dashPanelCount := data.NewFieldFromFieldType(data.FieldTypeInt64, 0)
|
|
||||||
dashVarCount := data.NewFieldFromFieldType(data.FieldTypeInt64, 0)
|
|
||||||
dashDSList := data.NewFieldFromFieldType(data.FieldTypeNullableString, 0)
|
|
||||||
|
|
||||||
dashID.Name = "id"
|
|
||||||
dashUID.Name = "uid"
|
|
||||||
dashFolderID.Name = "folderID"
|
|
||||||
dashName.Name = "name"
|
|
||||||
dashDescr.Name = "description"
|
|
||||||
dashTags.Name = "tags"
|
|
||||||
dashSchemaVersion.Name = "SchemaVersion"
|
|
||||||
dashCreated.Name = "Created"
|
|
||||||
dashUpdated.Name = "Updated"
|
|
||||||
dashURL.Name = "url"
|
|
||||||
dashURL.Config = &data.FieldConfig{
|
|
||||||
Links: []data.DataLink{
|
|
||||||
{Title: "link", URL: "${__value.text}"},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
dashPanelCount.Name = "panelCount"
|
|
||||||
dashVarCount.Name = "varCount"
|
|
||||||
dashDSList.Name = "datasource"
|
|
||||||
|
|
||||||
dashTags.Config = &data.FieldConfig{
|
|
||||||
Custom: map[string]interface{}{
|
|
||||||
// Table panel default styling
|
|
||||||
"displayMode": "json-view",
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
panelDashID := data.NewFieldFromFieldType(data.FieldTypeInt64, 0)
|
|
||||||
panelID := data.NewFieldFromFieldType(data.FieldTypeInt64, 0)
|
|
||||||
panelName := data.NewFieldFromFieldType(data.FieldTypeString, 0)
|
|
||||||
panelDescr := data.NewFieldFromFieldType(data.FieldTypeString, 0)
|
|
||||||
panelType := data.NewFieldFromFieldType(data.FieldTypeString, 0)
|
|
||||||
|
|
||||||
panelDashID.Name = "dashboardID"
|
|
||||||
panelID.Name = "id"
|
|
||||||
panelName.Name = "name"
|
|
||||||
panelDescr.Name = "description"
|
|
||||||
panelType.Name = "type"
|
|
||||||
|
|
||||||
panelTypeCounter := simpleCounter{
|
|
||||||
values: make(map[string]int64, 30),
|
|
||||||
}
|
|
||||||
|
|
||||||
schemaVersionCounter := simpleCounter{
|
|
||||||
values: make(map[string]int64, 30),
|
|
||||||
}
|
|
||||||
|
|
||||||
folderCounter := make(map[int64]int64, 20)
|
|
||||||
|
|
||||||
for _, row := range meta {
|
|
||||||
if row.isFolder {
|
|
||||||
folderID.Append(row.id)
|
|
||||||
folderUID.Append(row.uid)
|
|
||||||
folderName.Append(row.info.Title)
|
|
||||||
folderDashCount.Append(int64(0)) // filled in later
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
dashID.Append(row.id)
|
|
||||||
dashUID.Append(row.uid)
|
|
||||||
dashFolderID.Append(row.folderID)
|
|
||||||
dashName.Append(row.info.Title)
|
|
||||||
dashDescr.Append(row.info.Title)
|
|
||||||
dashSchemaVersion.Append(row.info.SchemaVersion)
|
|
||||||
dashCreated.Append(row.created)
|
|
||||||
dashUpdated.Append(row.updated)
|
|
||||||
|
|
||||||
// Increment the folder counter
|
|
||||||
fcount, ok := folderCounter[row.folderID]
|
|
||||||
if !ok {
|
|
||||||
fcount = 0
|
|
||||||
}
|
|
||||||
folderCounter[row.folderID] = fcount + 1
|
|
||||||
|
|
||||||
url := fmt.Sprintf("/d/%s/%s", row.uid, row.slug)
|
|
||||||
dashURL.Append(url)
|
|
||||||
|
|
||||||
// stats
|
|
||||||
schemaVersionCounter.add(strconv.FormatInt(row.info.SchemaVersion, 10))
|
|
||||||
|
|
||||||
dashTags.Append(toJSONString(row.info.Tags))
|
|
||||||
dashPanelCount.Append(int64(len(row.info.Panels)))
|
|
||||||
dashVarCount.Append(int64(len(row.info.TemplateVars)))
|
|
||||||
dashDSList.Append(dsAsJSONString(row.info.Datasource))
|
|
||||||
|
|
||||||
// Row for each panel
|
|
||||||
for _, panel := range row.info.Panels {
|
|
||||||
panelDashID.Append(row.id)
|
|
||||||
panelID.Append(panel.ID)
|
|
||||||
panelName.Append(panel.Title)
|
|
||||||
panelDescr.Append(panel.Description)
|
|
||||||
panelType.Append(panel.Type)
|
|
||||||
panelTypeCounter.add(panel.Type)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Update the folder counts
|
|
||||||
for i := 0; i < folderID.Len(); i++ {
|
|
||||||
id, ok := folderID.At(i).(int64)
|
|
||||||
if ok {
|
|
||||||
folderDashCount.Set(i, folderCounter[id])
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return data.Frames{
|
|
||||||
data.NewFrame("folders", folderID, folderUID, folderName, folderDashCount),
|
|
||||||
data.NewFrame("dashboards", dashID, dashUID, dashURL, dashFolderID,
|
|
||||||
dashName, dashDescr, dashTags,
|
|
||||||
dashSchemaVersion,
|
|
||||||
dashPanelCount, dashVarCount, dashDSList,
|
|
||||||
dashCreated, dashUpdated),
|
|
||||||
data.NewFrame("panels", panelDashID, panelID, panelName, panelDescr, panelType),
|
|
||||||
panelTypeCounter.toFrame("panel-type-counts"),
|
|
||||||
schemaVersionCounter.toFrame("schema-version-counts"),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func toJSONString(vals []string) *string {
|
|
||||||
if len(vals) < 1 {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
b, err := json.Marshal(vals)
|
|
||||||
if err == nil {
|
|
||||||
s := string(b)
|
|
||||||
return &s
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func dsAsJSONString(vals []extract.DataSourceRef) *string {
|
|
||||||
if len(vals) < 1 {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
b, err := json.Marshal(vals)
|
|
||||||
if err == nil {
|
|
||||||
s := string(b)
|
|
||||||
return &s
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
}
|
||||||
|
|||||||
+20
@@ -0,0 +1,20 @@
|
|||||||
|
🌟 This was machine generated. Do not edit. 🌟
|
||||||
|
|
||||||
|
Frame[0] {
|
||||||
|
"type": "search-results",
|
||||||
|
"custom": {
|
||||||
|
"count": 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Name: Query results
|
||||||
|
Dimensions: 8 Fields by 0 Rows
|
||||||
|
+----------------+----------------+----------------+------------------+----------------+--------------------------+--------------------------+----------------+
|
||||||
|
| Name: kind | Name: uid | Name: name | Name: panel_type | Name: url | Name: tags | Name: ds_uid | Name: location |
|
||||||
|
| Labels: | Labels: | Labels: | Labels: | Labels: | Labels: | Labels: | Labels: |
|
||||||
|
| Type: []string | Type: []string | Type: []string | Type: []string | Type: []string | Type: []*json.RawMessage | Type: []*json.RawMessage | Type: []string |
|
||||||
|
+----------------+----------------+----------------+------------------+----------------+--------------------------+--------------------------+----------------+
|
||||||
|
+----------------+----------------+----------------+------------------+----------------+--------------------------+--------------------------+----------------+
|
||||||
|
|
||||||
|
|
||||||
|
====== TEST DATA RESPONSE (arrow base64) ======
|
||||||
|
FRAME=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cmwiOiIke19fdmFsdWUudGV4dH0ifV19AAAAAAYAAABjb25maWcAAAAAAACI/v//AwAAAHVybAD2/v//FAAAAEAAAABAAAAAAAAABTwAAAABAAAABAAAAOT+//8IAAAAFAAAAAoAAABwYW5lbF90eXBlAAAEAAAAbmFtZQAAAAAAAAAA4P7//woAAABwYW5lbF90eXBlAABW////FAAAADwAAAA8AAAAAAAABTgAAAABAAAABAAAAET///8IAAAAEAAAAAQAAABuYW1lAAAAAAQAAABuYW1lAAAAAAAAAAA8////BAAAAG5hbWUAAAAArv///xQAAAA4AAAAOAAAAAAAAAU0AAAAAQAAAAQAAACc////CAAAAAwAAAADAAAAdWlkAAQAAABuYW1lAAAAAAAAAACQ////AwAAAHVpZAAAABIAGAAUAAAAEwAMAAAACAAEABIAAAAUAAAARAAAAEgAAAAAAAAFRAAAAAEAAAAMAAAACAAMAAgABAAIAAAACAAAABAAAAAEAAAAa2luZAAAAAAEAAAAbmFtZQAAAAAAAAAABAAEAAQAAAAEAAAAa2luZAAAAABgBAAAQVJST1cx
|
||||||
+21
@@ -0,0 +1,21 @@
|
|||||||
|
🌟 This was machine generated. Do not edit. 🌟
|
||||||
|
|
||||||
|
Frame[0] {
|
||||||
|
"type": "search-results",
|
||||||
|
"custom": {
|
||||||
|
"count": 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Name: Query results
|
||||||
|
Dimensions: 8 Fields by 1 Rows
|
||||||
|
+----------------+----------------+----------------+------------------+----------------+--------------------------+--------------------------+----------------+
|
||||||
|
| Name: kind | Name: uid | Name: name | Name: panel_type | Name: url | Name: tags | Name: ds_uid | Name: location |
|
||||||
|
| Labels: | Labels: | Labels: | Labels: | Labels: | Labels: | Labels: | Labels: |
|
||||||
|
| Type: []string | Type: []string | Type: []string | Type: []string | Type: []string | Type: []*json.RawMessage | Type: []*json.RawMessage | Type: []string |
|
||||||
|
+----------------+----------------+----------------+------------------+----------------+--------------------------+--------------------------+----------------+
|
||||||
|
| dashboard | 2 | boom | | /d/2/ | null | null | |
|
||||||
|
+----------------+----------------+----------------+------------------+----------------+--------------------------+--------------------------+----------------+
|
||||||
|
|
||||||
|
|
||||||
|
====== TEST DATA RESPONSE (arrow base64) ======
|
||||||
|
FRAME=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
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
🌟 This was machine generated. Do not edit. 🌟
|
||||||
|
|
||||||
|
Frame[0] {
|
||||||
|
"type": "search-results",
|
||||||
|
"custom": {
|
||||||
|
"count": 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Name: Query results
|
||||||
|
Dimensions: 8 Fields by 1 Rows
|
||||||
|
+----------------+----------------+----------------+------------------+----------------+--------------------------+--------------------------+----------------+
|
||||||
|
| Name: kind | Name: uid | Name: name | Name: panel_type | Name: url | Name: tags | Name: ds_uid | Name: location |
|
||||||
|
| Labels: | Labels: | Labels: | Labels: | Labels: | Labels: | Labels: | Labels: |
|
||||||
|
| Type: []string | Type: []string | Type: []string | Type: []string | Type: []string | Type: []*json.RawMessage | Type: []*json.RawMessage | Type: []string |
|
||||||
|
+----------------+----------------+----------------+------------------+----------------+--------------------------+--------------------------+----------------+
|
||||||
|
| dashboard | 3 | created | | /d/3/ | null | null | general |
|
||||||
|
+----------------+----------------+----------------+------------------+----------------+--------------------------+--------------------------+----------------+
|
||||||
|
|
||||||
|
|
||||||
|
====== TEST DATA RESPONSE (arrow base64) ======
|
||||||
|
FRAME=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
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
🌟 This was machine generated. Do not edit. 🌟
|
||||||
|
|
||||||
|
Frame[0] {
|
||||||
|
"type": "search-results",
|
||||||
|
"custom": {
|
||||||
|
"count": 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Name: Query results
|
||||||
|
Dimensions: 8 Fields by 0 Rows
|
||||||
|
+----------------+----------------+----------------+------------------+----------------+--------------------------+--------------------------+----------------+
|
||||||
|
| Name: kind | Name: uid | Name: name | Name: panel_type | Name: url | Name: tags | Name: ds_uid | Name: location |
|
||||||
|
| Labels: | Labels: | Labels: | Labels: | Labels: | Labels: | Labels: | Labels: |
|
||||||
|
| Type: []string | Type: []string | Type: []string | Type: []string | Type: []string | Type: []*json.RawMessage | Type: []*json.RawMessage | Type: []string |
|
||||||
|
+----------------+----------------+----------------+------------------+----------------+--------------------------+--------------------------+----------------+
|
||||||
|
+----------------+----------------+----------------+------------------+----------------+--------------------------+--------------------------+----------------+
|
||||||
|
|
||||||
|
|
||||||
|
====== TEST DATA RESPONSE (arrow base64) ======
|
||||||
|
FRAME=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cmwiOiIke19fdmFsdWUudGV4dH0ifV19AAAAAAYAAABjb25maWcAAAAAAACI/v//AwAAAHVybAD2/v//FAAAAEAAAABAAAAAAAAABTwAAAABAAAABAAAAOT+//8IAAAAFAAAAAoAAABwYW5lbF90eXBlAAAEAAAAbmFtZQAAAAAAAAAA4P7//woAAABwYW5lbF90eXBlAABW////FAAAADwAAAA8AAAAAAAABTgAAAABAAAABAAAAET///8IAAAAEAAAAAQAAABuYW1lAAAAAAQAAABuYW1lAAAAAAAAAAA8////BAAAAG5hbWUAAAAArv///xQAAAA4AAAAOAAAAAAAAAU0AAAAAQAAAAQAAACc////CAAAAAwAAAADAAAAdWlkAAQAAABuYW1lAAAAAAAAAACQ////AwAAAHVpZAAAABIAGAAUAAAAEwAMAAAACAAEABIAAAAUAAAARAAAAEgAAAAAAAAFRAAAAAEAAAAMAAAACAAMAAgABAAIAAAACAAAABAAAAAEAAAAa2luZAAAAAAEAAAAbmFtZQAAAAAAAAAABAAEAAQAAAAEAAAAa2luZAAAAABgBAAAQVJST1cx
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
🌟 This was machine generated. Do not edit. 🌟
|
||||||
|
|
||||||
|
Frame[0] {
|
||||||
|
"type": "search-results",
|
||||||
|
"custom": {
|
||||||
|
"count": 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Name: Query results
|
||||||
|
Dimensions: 8 Fields by 1 Rows
|
||||||
|
+----------------+----------------+----------------+------------------+----------------+--------------------------+--------------------------+----------------+
|
||||||
|
| Name: kind | Name: uid | Name: name | Name: panel_type | Name: url | Name: tags | Name: ds_uid | Name: location |
|
||||||
|
| Labels: | Labels: | Labels: | Labels: | Labels: | Labels: | Labels: | Labels: |
|
||||||
|
| Type: []string | Type: []string | Type: []string | Type: []string | Type: []string | Type: []*json.RawMessage | Type: []*json.RawMessage | Type: []string |
|
||||||
|
+----------------+----------------+----------------+------------------+----------------+--------------------------+--------------------------+----------------+
|
||||||
|
| dashboard | 2 | nginx | | /d/2/ | null | null | general |
|
||||||
|
+----------------+----------------+----------------+------------------+----------------+--------------------------+--------------------------+----------------+
|
||||||
|
|
||||||
|
|
||||||
|
====== TEST DATA RESPONSE (arrow base64) ======
|
||||||
|
FRAME=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
|
||||||
@@ -21,7 +21,6 @@ type DashboardQuery struct {
|
|||||||
Tags []string `json:"tags,omitempty"`
|
Tags []string `json:"tags,omitempty"`
|
||||||
Kind []string `json:"kind,omitempty"`
|
Kind []string `json:"kind,omitempty"`
|
||||||
UIDs []string `json:"uid,omitempty"`
|
UIDs []string `json:"uid,omitempty"`
|
||||||
IDs []int64 `json:"id,omitempty"` // deprecated -- but will convert internal ID to UIDs
|
|
||||||
Explain bool `json:"explain,omitempty"` // adds details on why document matched
|
Explain bool `json:"explain,omitempty"` // adds details on why document matched
|
||||||
Facet []FacetField `json:"facet,omitempty"`
|
Facet []FacetField `json:"facet,omitempty"`
|
||||||
SkipLocation bool `json:"skipLocation,omitempty"`
|
SkipLocation bool `json:"skipLocation,omitempty"`
|
||||||
|
|||||||
Reference in New Issue
Block a user