This change adds `access` format support for notifications to a Elasticsearch server, and it refactors `namespace` format support. In the case of `access` format, for each event in Minio, a JSON document is inserted into Elasticsearch with its timestamp set to the event's timestamp, and with the ID generated automatically by elasticsearch. No events are modified or deleted in this mode. In the case of `namespace` format, for each event in Minio, a JSON document is keyed together by the bucket and object name is updated in Elasticsearch. In the case of an object being created or over-written in Minio, a new document or an existing document is inserted into the Elasticsearch index. If an object is deleted in Minio, the corresponding document is deleted from the Elasticsearch index. Additionally, this change upgrades Elasticsearch support to the 5.x series. This is a breaking change, and users of previous elasticsearch versions should upgrade. Also updates documentation on Elasticsearch notification target usage and has a link to an elasticsearch upgrade guide. This is the last patch that finally resolves #3928.
531 lines
14 KiB
Go
531 lines
14 KiB
Go
// Copyright 2012-present Oliver Eilhard. All rights reserved.
|
|
// Use of this source code is governed by a MIT-license.
|
|
// See http://olivere.mit-license.org/license.txt for details.
|
|
|
|
package elastic
|
|
|
|
import (
|
|
"fmt"
|
|
)
|
|
|
|
// SearchSource enables users to build the search source.
|
|
// It resembles the SearchSourceBuilder in Elasticsearch.
|
|
type SearchSource struct {
|
|
query Query
|
|
postQuery Query
|
|
sliceQuery Query
|
|
from int
|
|
size int
|
|
explain *bool
|
|
version *bool
|
|
sorters []Sorter
|
|
trackScores bool
|
|
searchAfterSortValues []interface{}
|
|
minScore *float64
|
|
timeout string
|
|
terminateAfter *int
|
|
storedFieldNames []string
|
|
docvalueFields []string
|
|
scriptFields []*ScriptField
|
|
fetchSourceContext *FetchSourceContext
|
|
aggregations map[string]Aggregation
|
|
highlight *Highlight
|
|
globalSuggestText string
|
|
suggesters []Suggester
|
|
rescores []*Rescore
|
|
defaultRescoreWindowSize *int
|
|
indexBoosts map[string]float64
|
|
stats []string
|
|
innerHits map[string]*InnerHit
|
|
profile bool
|
|
}
|
|
|
|
// NewSearchSource initializes a new SearchSource.
|
|
func NewSearchSource() *SearchSource {
|
|
return &SearchSource{
|
|
from: -1,
|
|
size: -1,
|
|
trackScores: false,
|
|
aggregations: make(map[string]Aggregation),
|
|
indexBoosts: make(map[string]float64),
|
|
innerHits: make(map[string]*InnerHit),
|
|
}
|
|
}
|
|
|
|
// Query sets the query to use with this search source.
|
|
func (s *SearchSource) Query(query Query) *SearchSource {
|
|
s.query = query
|
|
return s
|
|
}
|
|
|
|
// Profile specifies that this search source should activate the
|
|
// Profile API for queries made on it.
|
|
func (s *SearchSource) Profile(profile bool) *SearchSource {
|
|
s.profile = profile
|
|
return s
|
|
}
|
|
|
|
// PostFilter will be executed after the query has been executed and
|
|
// only affects the search hits, not the aggregations.
|
|
// This filter is always executed as the last filtering mechanism.
|
|
func (s *SearchSource) PostFilter(postFilter Query) *SearchSource {
|
|
s.postQuery = postFilter
|
|
return s
|
|
}
|
|
|
|
// Slice allows partitioning the documents in multiple slices.
|
|
// It is e.g. used to slice a scroll operation, supported in
|
|
// Elasticsearch 5.0 or later.
|
|
// See https://www.elastic.co/guide/en/elasticsearch/reference/5.2/search-request-scroll.html#sliced-scroll
|
|
// for details.
|
|
func (s *SearchSource) Slice(sliceQuery Query) *SearchSource {
|
|
s.sliceQuery = sliceQuery
|
|
return s
|
|
}
|
|
|
|
// From index to start the search from. Defaults to 0.
|
|
func (s *SearchSource) From(from int) *SearchSource {
|
|
s.from = from
|
|
return s
|
|
}
|
|
|
|
// Size is the number of search hits to return. Defaults to 10.
|
|
func (s *SearchSource) Size(size int) *SearchSource {
|
|
s.size = size
|
|
return s
|
|
}
|
|
|
|
// MinScore sets the minimum score below which docs will be filtered out.
|
|
func (s *SearchSource) MinScore(minScore float64) *SearchSource {
|
|
s.minScore = &minScore
|
|
return s
|
|
}
|
|
|
|
// Explain indicates whether each search hit should be returned with
|
|
// an explanation of the hit (ranking).
|
|
func (s *SearchSource) Explain(explain bool) *SearchSource {
|
|
s.explain = &explain
|
|
return s
|
|
}
|
|
|
|
// Version indicates whether each search hit should be returned with
|
|
// a version associated to it.
|
|
func (s *SearchSource) Version(version bool) *SearchSource {
|
|
s.version = &version
|
|
return s
|
|
}
|
|
|
|
// Timeout controls how long a search is allowed to take, e.g. "1s" or "500ms".
|
|
func (s *SearchSource) Timeout(timeout string) *SearchSource {
|
|
s.timeout = timeout
|
|
return s
|
|
}
|
|
|
|
// TimeoutInMillis controls how many milliseconds a search is allowed
|
|
// to take before it is canceled.
|
|
func (s *SearchSource) TimeoutInMillis(timeoutInMillis int) *SearchSource {
|
|
s.timeout = fmt.Sprintf("%dms", timeoutInMillis)
|
|
return s
|
|
}
|
|
|
|
// TerminateAfter allows the request to stop after the given number
|
|
// of search hits are collected.
|
|
func (s *SearchSource) TerminateAfter(terminateAfter int) *SearchSource {
|
|
s.terminateAfter = &terminateAfter
|
|
return s
|
|
}
|
|
|
|
// Sort adds a sort order.
|
|
func (s *SearchSource) Sort(field string, ascending bool) *SearchSource {
|
|
s.sorters = append(s.sorters, SortInfo{Field: field, Ascending: ascending})
|
|
return s
|
|
}
|
|
|
|
// SortWithInfo adds a sort order.
|
|
func (s *SearchSource) SortWithInfo(info SortInfo) *SearchSource {
|
|
s.sorters = append(s.sorters, info)
|
|
return s
|
|
}
|
|
|
|
// SortBy adds a sort order.
|
|
func (s *SearchSource) SortBy(sorter ...Sorter) *SearchSource {
|
|
s.sorters = append(s.sorters, sorter...)
|
|
return s
|
|
}
|
|
|
|
func (s *SearchSource) hasSort() bool {
|
|
return len(s.sorters) > 0
|
|
}
|
|
|
|
// TrackScores is applied when sorting and controls if scores will be
|
|
// tracked as well. Defaults to false.
|
|
func (s *SearchSource) TrackScores(trackScores bool) *SearchSource {
|
|
s.trackScores = trackScores
|
|
return s
|
|
}
|
|
|
|
// SearchAfter allows a different form of pagination by using a live cursor,
|
|
// using the results of the previous page to help the retrieval of the next.
|
|
//
|
|
// See https://www.elastic.co/guide/en/elasticsearch/reference/5.2/search-request-search-after.html
|
|
func (s *SearchSource) SearchAfter(sortValues ...interface{}) *SearchSource {
|
|
s.searchAfterSortValues = append(s.searchAfterSortValues, sortValues...)
|
|
return s
|
|
}
|
|
|
|
// Aggregation adds an aggreation to perform as part of the search.
|
|
func (s *SearchSource) Aggregation(name string, aggregation Aggregation) *SearchSource {
|
|
s.aggregations[name] = aggregation
|
|
return s
|
|
}
|
|
|
|
// DefaultRescoreWindowSize sets the rescore window size for rescores
|
|
// that don't specify their window.
|
|
func (s *SearchSource) DefaultRescoreWindowSize(defaultRescoreWindowSize int) *SearchSource {
|
|
s.defaultRescoreWindowSize = &defaultRescoreWindowSize
|
|
return s
|
|
}
|
|
|
|
// Highlight adds highlighting to the search.
|
|
func (s *SearchSource) Highlight(highlight *Highlight) *SearchSource {
|
|
s.highlight = highlight
|
|
return s
|
|
}
|
|
|
|
// Highlighter returns the highlighter.
|
|
func (s *SearchSource) Highlighter() *Highlight {
|
|
if s.highlight == nil {
|
|
s.highlight = NewHighlight()
|
|
}
|
|
return s.highlight
|
|
}
|
|
|
|
// GlobalSuggestText defines the global text to use with all suggesters.
|
|
// This avoids repetition.
|
|
func (s *SearchSource) GlobalSuggestText(text string) *SearchSource {
|
|
s.globalSuggestText = text
|
|
return s
|
|
}
|
|
|
|
// Suggester adds a suggester to the search.
|
|
func (s *SearchSource) Suggester(suggester Suggester) *SearchSource {
|
|
s.suggesters = append(s.suggesters, suggester)
|
|
return s
|
|
}
|
|
|
|
// Rescorer adds a rescorer to the search.
|
|
func (s *SearchSource) Rescorer(rescore *Rescore) *SearchSource {
|
|
s.rescores = append(s.rescores, rescore)
|
|
return s
|
|
}
|
|
|
|
// ClearRescorers removes all rescorers from the search.
|
|
func (s *SearchSource) ClearRescorers() *SearchSource {
|
|
s.rescores = make([]*Rescore, 0)
|
|
return s
|
|
}
|
|
|
|
// FetchSource indicates whether the response should contain the stored
|
|
// _source for every hit.
|
|
func (s *SearchSource) FetchSource(fetchSource bool) *SearchSource {
|
|
if s.fetchSourceContext == nil {
|
|
s.fetchSourceContext = NewFetchSourceContext(fetchSource)
|
|
} else {
|
|
s.fetchSourceContext.SetFetchSource(fetchSource)
|
|
}
|
|
return s
|
|
}
|
|
|
|
// FetchSourceContext indicates how the _source should be fetched.
|
|
func (s *SearchSource) FetchSourceContext(fetchSourceContext *FetchSourceContext) *SearchSource {
|
|
s.fetchSourceContext = fetchSourceContext
|
|
return s
|
|
}
|
|
|
|
// NoStoredFields indicates that no fields should be loaded, resulting in only
|
|
// id and type to be returned per field.
|
|
func (s *SearchSource) NoStoredFields() *SearchSource {
|
|
s.storedFieldNames = nil
|
|
return s
|
|
}
|
|
|
|
// StoredField adds a single field to load and return (note, must be stored) as
|
|
// part of the search request. If none are specified, the source of the
|
|
// document will be returned.
|
|
func (s *SearchSource) StoredField(storedFieldName string) *SearchSource {
|
|
s.storedFieldNames = append(s.storedFieldNames, storedFieldName)
|
|
return s
|
|
}
|
|
|
|
// StoredFields sets the fields to load and return as part of the search request.
|
|
// If none are specified, the source of the document will be returned.
|
|
func (s *SearchSource) StoredFields(storedFieldNames ...string) *SearchSource {
|
|
s.storedFieldNames = append(s.storedFieldNames, storedFieldNames...)
|
|
return s
|
|
}
|
|
|
|
// DocvalueField adds a single field to load from the field data cache
|
|
// and return as part of the search request.
|
|
func (s *SearchSource) DocvalueField(fieldDataField string) *SearchSource {
|
|
s.docvalueFields = append(s.docvalueFields, fieldDataField)
|
|
return s
|
|
}
|
|
|
|
// DocvalueFields adds one or more fields to load from the field data cache
|
|
// and return as part of the search request.
|
|
func (s *SearchSource) DocvalueFields(docvalueFields ...string) *SearchSource {
|
|
s.docvalueFields = append(s.docvalueFields, docvalueFields...)
|
|
return s
|
|
}
|
|
|
|
// ScriptField adds a single script field with the provided script.
|
|
func (s *SearchSource) ScriptField(scriptField *ScriptField) *SearchSource {
|
|
s.scriptFields = append(s.scriptFields, scriptField)
|
|
return s
|
|
}
|
|
|
|
// ScriptFields adds one or more script fields with the provided scripts.
|
|
func (s *SearchSource) ScriptFields(scriptFields ...*ScriptField) *SearchSource {
|
|
s.scriptFields = append(s.scriptFields, scriptFields...)
|
|
return s
|
|
}
|
|
|
|
// IndexBoost sets the boost that a specific index will receive when the
|
|
// query is executed against it.
|
|
func (s *SearchSource) IndexBoost(index string, boost float64) *SearchSource {
|
|
s.indexBoosts[index] = boost
|
|
return s
|
|
}
|
|
|
|
// Stats group this request will be aggregated under.
|
|
func (s *SearchSource) Stats(statsGroup ...string) *SearchSource {
|
|
s.stats = append(s.stats, statsGroup...)
|
|
return s
|
|
}
|
|
|
|
// InnerHit adds an inner hit to return with the result.
|
|
func (s *SearchSource) InnerHit(name string, innerHit *InnerHit) *SearchSource {
|
|
s.innerHits[name] = innerHit
|
|
return s
|
|
}
|
|
|
|
// Source returns the serializable JSON for the source builder.
|
|
func (s *SearchSource) Source() (interface{}, error) {
|
|
source := make(map[string]interface{})
|
|
|
|
if s.from != -1 {
|
|
source["from"] = s.from
|
|
}
|
|
if s.size != -1 {
|
|
source["size"] = s.size
|
|
}
|
|
if s.timeout != "" {
|
|
source["timeout"] = s.timeout
|
|
}
|
|
if s.terminateAfter != nil {
|
|
source["terminate_after"] = *s.terminateAfter
|
|
}
|
|
if s.query != nil {
|
|
src, err := s.query.Source()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
source["query"] = src
|
|
}
|
|
if s.postQuery != nil {
|
|
src, err := s.postQuery.Source()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
source["post_filter"] = src
|
|
}
|
|
if s.sliceQuery != nil {
|
|
src, err := s.sliceQuery.Source()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
source["slice"] = src
|
|
}
|
|
if s.minScore != nil {
|
|
source["min_score"] = *s.minScore
|
|
}
|
|
if s.version != nil {
|
|
source["version"] = *s.version
|
|
}
|
|
if s.explain != nil {
|
|
source["explain"] = *s.explain
|
|
}
|
|
if s.profile {
|
|
source["profile"] = s.profile
|
|
}
|
|
if s.fetchSourceContext != nil {
|
|
src, err := s.fetchSourceContext.Source()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
source["_source"] = src
|
|
}
|
|
|
|
if s.storedFieldNames != nil {
|
|
switch len(s.storedFieldNames) {
|
|
case 1:
|
|
source["stored_fields"] = s.storedFieldNames[0]
|
|
default:
|
|
source["stored_fields"] = s.storedFieldNames
|
|
}
|
|
}
|
|
|
|
if len(s.docvalueFields) > 0 {
|
|
source["docvalue_fields"] = s.docvalueFields
|
|
}
|
|
|
|
if len(s.scriptFields) > 0 {
|
|
sfmap := make(map[string]interface{})
|
|
for _, scriptField := range s.scriptFields {
|
|
src, err := scriptField.Source()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
sfmap[scriptField.FieldName] = src
|
|
}
|
|
source["script_fields"] = sfmap
|
|
}
|
|
|
|
if len(s.sorters) > 0 {
|
|
var sortarr []interface{}
|
|
for _, sorter := range s.sorters {
|
|
src, err := sorter.Source()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
sortarr = append(sortarr, src)
|
|
}
|
|
source["sort"] = sortarr
|
|
}
|
|
|
|
if s.trackScores {
|
|
source["track_scores"] = s.trackScores
|
|
}
|
|
|
|
if len(s.searchAfterSortValues) > 0 {
|
|
source["search_after"] = s.searchAfterSortValues
|
|
}
|
|
|
|
if len(s.indexBoosts) > 0 {
|
|
source["indices_boost"] = s.indexBoosts
|
|
}
|
|
|
|
if len(s.aggregations) > 0 {
|
|
aggsMap := make(map[string]interface{})
|
|
for name, aggregate := range s.aggregations {
|
|
src, err := aggregate.Source()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
aggsMap[name] = src
|
|
}
|
|
source["aggregations"] = aggsMap
|
|
}
|
|
|
|
if s.highlight != nil {
|
|
src, err := s.highlight.Source()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
source["highlight"] = src
|
|
}
|
|
|
|
if len(s.suggesters) > 0 {
|
|
suggesters := make(map[string]interface{})
|
|
for _, s := range s.suggesters {
|
|
src, err := s.Source(false)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
suggesters[s.Name()] = src
|
|
}
|
|
if s.globalSuggestText != "" {
|
|
suggesters["text"] = s.globalSuggestText
|
|
}
|
|
source["suggest"] = suggesters
|
|
}
|
|
|
|
if len(s.rescores) > 0 {
|
|
// Strip empty rescores from request
|
|
var rescores []*Rescore
|
|
for _, r := range s.rescores {
|
|
if !r.IsEmpty() {
|
|
rescores = append(rescores, r)
|
|
}
|
|
}
|
|
|
|
if len(rescores) == 1 {
|
|
rescores[0].defaultRescoreWindowSize = s.defaultRescoreWindowSize
|
|
src, err := rescores[0].Source()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
source["rescore"] = src
|
|
} else {
|
|
var slice []interface{}
|
|
for _, r := range rescores {
|
|
r.defaultRescoreWindowSize = s.defaultRescoreWindowSize
|
|
src, err := r.Source()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
slice = append(slice, src)
|
|
}
|
|
source["rescore"] = slice
|
|
}
|
|
}
|
|
|
|
if len(s.stats) > 0 {
|
|
source["stats"] = s.stats
|
|
}
|
|
|
|
if len(s.innerHits) > 0 {
|
|
// Top-level inner hits
|
|
// See http://www.elastic.co/guide/en/elasticsearch/reference/1.5/search-request-inner-hits.html#top-level-inner-hits
|
|
// "inner_hits": {
|
|
// "<inner_hits_name>": {
|
|
// "<path|type>": {
|
|
// "<path-to-nested-object-field|child-or-parent-type>": {
|
|
// <inner_hits_body>,
|
|
// [,"inner_hits" : { [<sub_inner_hits>]+ } ]?
|
|
// }
|
|
// }
|
|
// },
|
|
// [,"<inner_hits_name_2>" : { ... } ]*
|
|
// }
|
|
m := make(map[string]interface{})
|
|
for name, hit := range s.innerHits {
|
|
if hit.path != "" {
|
|
src, err := hit.Source()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
path := make(map[string]interface{})
|
|
path[hit.path] = src
|
|
m[name] = map[string]interface{}{
|
|
"path": path,
|
|
}
|
|
} else if hit.typ != "" {
|
|
src, err := hit.Source()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
typ := make(map[string]interface{})
|
|
typ[hit.typ] = src
|
|
m[name] = map[string]interface{}{
|
|
"type": typ,
|
|
}
|
|
} else {
|
|
// TODO the Java client throws here, because either path or typ must be specified
|
|
}
|
|
}
|
|
source["inner_hits"] = m
|
|
}
|
|
|
|
return source, nil
|
|
}
|