1// Copyright 2012-2015 Oliver Eilhard. All rights reserved.
2// Use of this source code is governed by a MIT-license.
3// See http://olivere.mit-license.org/license.txt for details.
4
5package elastic
6
7import (
8	"fmt"
9)
10
11// SearchSource enables users to build the search source.
12// It resembles the SearchSourceBuilder in Elasticsearch.
13type SearchSource struct {
14	query                    Query
15	postFilter               Filter
16	from                     int
17	size                     int
18	explain                  *bool
19	version                  *bool
20	sorts                    []SortInfo
21	sorters                  []Sorter
22	trackScores              bool
23	minScore                 *float64
24	timeout                  string
25	fieldNames               []string
26	fieldDataFields          []string
27	scriptFields             []*ScriptField
28	partialFields            []*PartialField
29	fetchSourceContext       *FetchSourceContext
30	facets                   map[string]Facet
31	aggregations             map[string]Aggregation
32	highlight                *Highlight
33	globalSuggestText        string
34	suggesters               []Suggester
35	rescores                 []*Rescore
36	defaultRescoreWindowSize *int
37	indexBoosts              map[string]float64
38	stats                    []string
39	innerHits                map[string]*InnerHit
40}
41
42// NewSearchSource initializes a new SearchSource.
43func NewSearchSource() *SearchSource {
44	return &SearchSource{
45		from:            -1,
46		size:            -1,
47		trackScores:     false,
48		sorts:           make([]SortInfo, 0),
49		sorters:         make([]Sorter, 0),
50		fieldDataFields: make([]string, 0),
51		scriptFields:    make([]*ScriptField, 0),
52		partialFields:   make([]*PartialField, 0),
53		facets:          make(map[string]Facet),
54		aggregations:    make(map[string]Aggregation),
55		rescores:        make([]*Rescore, 0),
56		indexBoosts:     make(map[string]float64),
57		stats:           make([]string, 0),
58		innerHits:       make(map[string]*InnerHit),
59	}
60}
61
62// Query sets the query to use with this search source.
63func (s *SearchSource) Query(query Query) *SearchSource {
64	s.query = query
65	return s
66}
67
68// PostFilter will be executed after the query has been executed and
69// only affects the search hits, not the aggregations.
70// This filter is always executed as the last filtering mechanism.
71func (s *SearchSource) PostFilter(postFilter Filter) *SearchSource {
72	s.postFilter = postFilter
73	return s
74}
75
76// From index to start the search from. Defaults to 0.
77func (s *SearchSource) From(from int) *SearchSource {
78	s.from = from
79	return s
80}
81
82// Size is the number of search hits to return. Defaults to 10.
83func (s *SearchSource) Size(size int) *SearchSource {
84	s.size = size
85	return s
86}
87
88// MinScore sets the minimum score below which docs will be filtered out.
89func (s *SearchSource) MinScore(minScore float64) *SearchSource {
90	s.minScore = &minScore
91	return s
92}
93
94// Explain indicates whether each search hit should be returned with
95// an explanation of the hit (ranking).
96func (s *SearchSource) Explain(explain bool) *SearchSource {
97	s.explain = &explain
98	return s
99}
100
101// Version indicates whether each search hit should be returned with
102// a version associated to it.
103func (s *SearchSource) Version(version bool) *SearchSource {
104	s.version = &version
105	return s
106}
107
108// Timeout controls how long a search is allowed to take, e.g. "1s" or "500ms".
109func (s *SearchSource) Timeout(timeout string) *SearchSource {
110	s.timeout = timeout
111	return s
112}
113
114// TimeoutInMillis controls how many milliseconds a search is allowed
115// to take before it is canceled.
116func (s *SearchSource) TimeoutInMillis(timeoutInMillis int) *SearchSource {
117	s.timeout = fmt.Sprintf("%dms", timeoutInMillis)
118	return s
119}
120
121// Sort adds a sort order.
122func (s *SearchSource) Sort(field string, ascending bool) *SearchSource {
123	s.sorts = append(s.sorts, SortInfo{Field: field, Ascending: ascending})
124	return s
125}
126
127// SortWithInfo adds a sort order.
128func (s *SearchSource) SortWithInfo(info SortInfo) *SearchSource {
129	s.sorts = append(s.sorts, info)
130	return s
131}
132
133// SortBy	adds a sort order.
134func (s *SearchSource) SortBy(sorter ...Sorter) *SearchSource {
135	s.sorters = append(s.sorters, sorter...)
136	return s
137}
138
139func (s *SearchSource) hasSort() bool {
140	return len(s.sorts) > 0 || len(s.sorters) > 0
141}
142
143// TrackScores is applied when sorting and controls if scores will be
144// tracked as well. Defaults to false.
145func (s *SearchSource) TrackScores(trackScores bool) *SearchSource {
146	s.trackScores = trackScores
147	return s
148}
149
150// Facet adds a facet to perform as part of the search.
151func (s *SearchSource) Facet(name string, facet Facet) *SearchSource {
152	s.facets[name] = facet
153	return s
154}
155
156// Aggregation adds an aggreation to perform as part of the search.
157func (s *SearchSource) Aggregation(name string, aggregation Aggregation) *SearchSource {
158	s.aggregations[name] = aggregation
159	return s
160}
161
162// DefaultRescoreWindowSize sets the rescore window size for rescores
163// that don't specify their window.
164func (s *SearchSource) DefaultRescoreWindowSize(defaultRescoreWindowSize int) *SearchSource {
165	s.defaultRescoreWindowSize = &defaultRescoreWindowSize
166	return s
167}
168
169// Highlight adds highlighting to the search.
170func (s *SearchSource) Highlight(highlight *Highlight) *SearchSource {
171	s.highlight = highlight
172	return s
173}
174
175// Highlighter returns the highlighter.
176func (s *SearchSource) Highlighter() *Highlight {
177	if s.highlight == nil {
178		s.highlight = NewHighlight()
179	}
180	return s.highlight
181}
182
183// GlobalSuggestText defines the global text to use with all suggesters.
184// This avoids repetition.
185func (s *SearchSource) GlobalSuggestText(text string) *SearchSource {
186	s.globalSuggestText = text
187	return s
188}
189
190// Suggester adds a suggester to the search.
191func (s *SearchSource) Suggester(suggester Suggester) *SearchSource {
192	s.suggesters = append(s.suggesters, suggester)
193	return s
194}
195
196// AddRescorer adds a rescorer to the search.
197func (s *SearchSource) AddRescore(rescore *Rescore) *SearchSource {
198	s.rescores = append(s.rescores, rescore)
199	return s
200}
201
202// ClearRescorers removes all rescorers from the search.
203func (s *SearchSource) ClearRescores() *SearchSource {
204	s.rescores = make([]*Rescore, 0)
205	return s
206}
207
208// FetchSource indicates whether the response should contain the stored
209// _source for every hit.
210func (s *SearchSource) FetchSource(fetchSource bool) *SearchSource {
211	if s.fetchSourceContext == nil {
212		s.fetchSourceContext = NewFetchSourceContext(fetchSource)
213	} else {
214		s.fetchSourceContext.SetFetchSource(fetchSource)
215	}
216	return s
217}
218
219// FetchSourceContext indicates how the _source should be fetched.
220func (s *SearchSource) FetchSourceContext(fetchSourceContext *FetchSourceContext) *SearchSource {
221	s.fetchSourceContext = fetchSourceContext
222	return s
223}
224
225// Fields	sets the fields to load and return as part of the search request.
226// If none are specified, the source of the document will be returned.
227func (s *SearchSource) Fields(fieldNames ...string) *SearchSource {
228	if s.fieldNames == nil {
229		s.fieldNames = make([]string, 0)
230	}
231	s.fieldNames = append(s.fieldNames, fieldNames...)
232	return s
233}
234
235// Field adds a single field to load and return (note, must be stored) as
236// part of the search request. If none are specified, the source of the
237// document will be returned.
238func (s *SearchSource) Field(fieldName string) *SearchSource {
239	if s.fieldNames == nil {
240		s.fieldNames = make([]string, 0)
241	}
242	s.fieldNames = append(s.fieldNames, fieldName)
243	return s
244}
245
246// NoFields indicates that no fields should be loaded, resulting in only
247// id and type to be returned per field.
248func (s *SearchSource) NoFields() *SearchSource {
249	s.fieldNames = make([]string, 0)
250	return s
251}
252
253// FieldDataFields adds one or more fields to load from the field data cache
254// and return as part of the search request.
255func (s *SearchSource) FieldDataFields(fieldDataFields ...string) *SearchSource {
256	s.fieldDataFields = append(s.fieldDataFields, fieldDataFields...)
257	return s
258}
259
260// FieldDataField adds a single field to load from the field data cache
261// and return as part of the search request.
262func (s *SearchSource) FieldDataField(fieldDataField string) *SearchSource {
263	s.fieldDataFields = append(s.fieldDataFields, fieldDataField)
264	return s
265}
266
267// ScriptFields adds one or more script fields with the provided scripts.
268func (s *SearchSource) ScriptFields(scriptFields ...*ScriptField) *SearchSource {
269	s.scriptFields = append(s.scriptFields, scriptFields...)
270	return s
271}
272
273// ScriptField adds a single script field with the provided script.
274func (s *SearchSource) ScriptField(scriptField *ScriptField) *SearchSource {
275	s.scriptFields = append(s.scriptFields, scriptField)
276	return s
277}
278
279// PartialFields adds partial fields.
280func (s *SearchSource) PartialFields(partialFields ...*PartialField) *SearchSource {
281	s.partialFields = append(s.partialFields, partialFields...)
282	return s
283}
284
285// PartialField adds a partial field.
286func (s *SearchSource) PartialField(partialField *PartialField) *SearchSource {
287	s.partialFields = append(s.partialFields, partialField)
288	return s
289}
290
291// IndexBoost sets the boost that a specific index will receive when the
292// query is executed against it.
293func (s *SearchSource) IndexBoost(index string, boost float64) *SearchSource {
294	s.indexBoosts[index] = boost
295	return s
296}
297
298// Stats group this request will be aggregated under.
299func (s *SearchSource) Stats(statsGroup ...string) *SearchSource {
300	s.stats = append(s.stats, statsGroup...)
301	return s
302}
303
304// InnerHit adds an inner hit to return with the result.
305func (s *SearchSource) InnerHit(name string, innerHit *InnerHit) *SearchSource {
306	s.innerHits[name] = innerHit
307	return s
308}
309
310// Source returns the serializable JSON for the source builder.
311func (s *SearchSource) Source() interface{} {
312	source := make(map[string]interface{})
313
314	if s.from != -1 {
315		source["from"] = s.from
316	}
317	if s.size != -1 {
318		source["size"] = s.size
319	}
320	if s.timeout != "" {
321		source["timeout"] = s.timeout
322	}
323	if s.query != nil {
324		source["query"] = s.query.Source()
325	}
326	if s.postFilter != nil {
327		source["post_filter"] = s.postFilter.Source()
328	}
329	if s.minScore != nil {
330		source["min_score"] = *s.minScore
331	}
332	if s.version != nil {
333		source["version"] = *s.version
334	}
335	if s.explain != nil {
336		source["explain"] = *s.explain
337	}
338	if s.fetchSourceContext != nil {
339		source["_source"] = s.fetchSourceContext.Source()
340	}
341
342	if s.fieldNames != nil {
343		switch len(s.fieldNames) {
344		case 1:
345			source["fields"] = s.fieldNames[0]
346		default:
347			source["fields"] = s.fieldNames
348		}
349	}
350
351	if len(s.fieldDataFields) > 0 {
352		source["fielddata_fields"] = s.fieldDataFields
353	}
354
355	if len(s.partialFields) > 0 {
356		pfmap := make(map[string]interface{})
357		for _, partialField := range s.partialFields {
358			pfmap[partialField.Name] = partialField.Source()
359		}
360		source["partial_fields"] = pfmap
361	}
362
363	if len(s.scriptFields) > 0 {
364		sfmap := make(map[string]interface{})
365		for _, scriptField := range s.scriptFields {
366			sfmap[scriptField.FieldName] = scriptField.Source()
367		}
368		source["script_fields"] = sfmap
369	}
370
371	if len(s.sorters) > 0 {
372		sortarr := make([]interface{}, 0)
373		for _, sorter := range s.sorters {
374			sortarr = append(sortarr, sorter.Source())
375		}
376		source["sort"] = sortarr
377	} else if len(s.sorts) > 0 {
378		sortarr := make([]interface{}, 0)
379		for _, sort := range s.sorts {
380			sortarr = append(sortarr, sort.Source())
381		}
382		source["sort"] = sortarr
383	}
384
385	if s.trackScores {
386		source["track_scores"] = s.trackScores
387	}
388
389	if len(s.indexBoosts) > 0 {
390		source["indices_boost"] = s.indexBoosts
391	}
392
393	if len(s.facets) > 0 {
394		facetsMap := make(map[string]interface{})
395		for field, facet := range s.facets {
396			facetsMap[field] = facet.Source()
397		}
398		source["facets"] = facetsMap
399	}
400
401	if len(s.aggregations) > 0 {
402		aggsMap := make(map[string]interface{})
403		for name, aggregate := range s.aggregations {
404			aggsMap[name] = aggregate.Source()
405		}
406		source["aggregations"] = aggsMap
407	}
408
409	if s.highlight != nil {
410		source["highlight"] = s.highlight.Source()
411	}
412
413	if len(s.suggesters) > 0 {
414		suggesters := make(map[string]interface{})
415		for _, s := range s.suggesters {
416			suggesters[s.Name()] = s.Source(false)
417		}
418		if s.globalSuggestText != "" {
419			suggesters["text"] = s.globalSuggestText
420		}
421		source["suggest"] = suggesters
422	}
423
424	if len(s.rescores) > 0 {
425		// Strip empty rescores from request
426		rescores := make([]*Rescore, 0)
427		for _, r := range s.rescores {
428			if !r.IsEmpty() {
429				rescores = append(rescores, r)
430			}
431		}
432
433		if len(rescores) == 1 {
434			rescores[0].defaultRescoreWindowSize = s.defaultRescoreWindowSize
435			source["rescore"] = rescores[0].Source()
436		} else {
437			slice := make([]interface{}, 0)
438			for _, r := range rescores {
439				r.defaultRescoreWindowSize = s.defaultRescoreWindowSize
440				slice = append(slice, r.Source())
441			}
442			source["rescore"] = slice
443		}
444	}
445
446	if len(s.stats) > 0 {
447		source["stats"] = s.stats
448	}
449
450	if len(s.innerHits) > 0 {
451		// Top-level inner hits
452		// See http://www.elastic.co/guide/en/elasticsearch/reference/1.5/search-request-inner-hits.html#top-level-inner-hits
453		// "inner_hits": {
454		//   "<inner_hits_name>": {
455		//     "<path|type>": {
456		//       "<path-to-nested-object-field|child-or-parent-type>": {
457		//         <inner_hits_body>,
458		//         [,"inner_hits" : { [<sub_inner_hits>]+ } ]?
459		//       }
460		//     }
461		//   },
462		//   [,"<inner_hits_name_2>" : { ... } ]*
463		// }
464		m := make(map[string]interface{})
465		for name, hit := range s.innerHits {
466			if hit.path != "" {
467				path := make(map[string]interface{})
468				path[hit.path] = hit.Source()
469				m[name] = map[string]interface{}{
470					"path": path,
471				}
472			} else if hit.typ != "" {
473				typ := make(map[string]interface{})
474				typ[hit.typ] = hit.Source()
475				m[name] = map[string]interface{}{
476					"type": typ,
477				}
478			} else {
479				// TODO the Java client throws here, because either path or typ must be specified
480			}
481		}
482		source["inner_hits"] = m
483	}
484
485	return source
486}
487
488// -- Script Field --
489
490type ScriptField struct {
491	FieldName string
492
493	script string
494	lang   string
495	params map[string]interface{}
496}
497
498func NewScriptField(fieldName, script, lang string, params map[string]interface{}) *ScriptField {
499	return &ScriptField{fieldName, script, lang, params}
500}
501
502func (f *ScriptField) Source() interface{} {
503	source := make(map[string]interface{})
504	source["script"] = f.script
505	if f.lang != "" {
506		source["lang"] = f.lang
507	}
508	if f.params != nil && len(f.params) > 0 {
509		source["params"] = f.params
510	}
511	return source
512}
513
514// -- Partial Field --
515
516type PartialField struct {
517	Name     string
518	includes []string
519	excludes []string
520}
521
522func NewPartialField(name string, includes, excludes []string) *PartialField {
523	return &PartialField{name, includes, excludes}
524}
525
526func (f *PartialField) Source() interface{} {
527	source := make(map[string]interface{})
528
529	if f.includes != nil {
530		switch len(f.includes) {
531		case 0:
532		case 1:
533			source["include"] = f.includes[0]
534		default:
535			source["include"] = f.includes
536		}
537	}
538
539	if f.excludes != nil {
540		switch len(f.excludes) {
541		case 0:
542		case 1:
543			source["exclude"] = f.excludes[0]
544		default:
545			source["exclude"] = f.excludes
546		}
547	}
548
549	return source
550}
551