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