You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
546 lines
15 KiB
546 lines
15 KiB
// 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 |
|
collapse *CollapseBuilder |
|
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 specifies the maximum number of documents to collect for |
|
// each shard, upon reaching which the query execution will terminate early. |
|
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 |
|
} |
|
|
|
// Collapse adds field collapsing. |
|
func (s *SearchSource) Collapse(collapse *CollapseBuilder) *SearchSource { |
|
s.collapse = collapse |
|
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.collapse != nil { |
|
src, err := s.collapse.Source() |
|
if err != nil { |
|
return nil, err |
|
} |
|
source["collapse"] = src |
|
} |
|
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 |
|
_ = m |
|
} |
|
} |
|
source["inner_hits"] = m |
|
} |
|
|
|
return source, nil |
|
}
|
|
|