Test
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.

search.go 21KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606
  1. // Copyright 2012-present 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. package elastic
  5. import (
  6. "context"
  7. "encoding/json"
  8. "fmt"
  9. "net/url"
  10. "reflect"
  11. "strings"
  12. "github.com/olivere/elastic/uritemplates"
  13. )
  14. // Search for documents in Elasticsearch.
  15. type SearchService struct {
  16. client *Client
  17. searchSource *SearchSource
  18. source interface{}
  19. pretty bool
  20. filterPath []string
  21. searchType string
  22. index []string
  23. typ []string
  24. routing string
  25. preference string
  26. requestCache *bool
  27. ignoreUnavailable *bool
  28. allowNoIndices *bool
  29. expandWildcards string
  30. maxResponseSize int64
  31. }
  32. // NewSearchService creates a new service for searching in Elasticsearch.
  33. func NewSearchService(client *Client) *SearchService {
  34. builder := &SearchService{
  35. client: client,
  36. searchSource: NewSearchSource(),
  37. }
  38. return builder
  39. }
  40. // SearchSource sets the search source builder to use with this service.
  41. func (s *SearchService) SearchSource(searchSource *SearchSource) *SearchService {
  42. s.searchSource = searchSource
  43. if s.searchSource == nil {
  44. s.searchSource = NewSearchSource()
  45. }
  46. return s
  47. }
  48. // Source allows the user to set the request body manually without using
  49. // any of the structs and interfaces in Elastic.
  50. func (s *SearchService) Source(source interface{}) *SearchService {
  51. s.source = source
  52. return s
  53. }
  54. // FilterPath allows reducing the response, a mechanism known as
  55. // response filtering and described here:
  56. // https://www.elastic.co/guide/en/elasticsearch/reference/6.2/common-options.html#common-options-response-filtering.
  57. func (s *SearchService) FilterPath(filterPath ...string) *SearchService {
  58. s.filterPath = append(s.filterPath, filterPath...)
  59. return s
  60. }
  61. // Index sets the names of the indices to use for search.
  62. func (s *SearchService) Index(index ...string) *SearchService {
  63. s.index = append(s.index, index...)
  64. return s
  65. }
  66. // Types adds search restrictions for a list of types.
  67. func (s *SearchService) Type(typ ...string) *SearchService {
  68. s.typ = append(s.typ, typ...)
  69. return s
  70. }
  71. // Pretty enables the caller to indent the JSON output.
  72. func (s *SearchService) Pretty(pretty bool) *SearchService {
  73. s.pretty = pretty
  74. return s
  75. }
  76. // Timeout sets the timeout to use, e.g. "1s" or "1000ms".
  77. func (s *SearchService) Timeout(timeout string) *SearchService {
  78. s.searchSource = s.searchSource.Timeout(timeout)
  79. return s
  80. }
  81. // Profile sets the Profile API flag on the search source.
  82. // When enabled, a search executed by this service will return query
  83. // profiling data.
  84. func (s *SearchService) Profile(profile bool) *SearchService {
  85. s.searchSource = s.searchSource.Profile(profile)
  86. return s
  87. }
  88. // Collapse adds field collapsing.
  89. func (s *SearchService) Collapse(collapse *CollapseBuilder) *SearchService {
  90. s.searchSource = s.searchSource.Collapse(collapse)
  91. return s
  92. }
  93. // TimeoutInMillis sets the timeout in milliseconds.
  94. func (s *SearchService) TimeoutInMillis(timeoutInMillis int) *SearchService {
  95. s.searchSource = s.searchSource.TimeoutInMillis(timeoutInMillis)
  96. return s
  97. }
  98. // TerminateAfter specifies the maximum number of documents to collect for
  99. // each shard, upon reaching which the query execution will terminate early.
  100. func (s *SearchService) TerminateAfter(terminateAfter int) *SearchService {
  101. s.searchSource = s.searchSource.TerminateAfter(terminateAfter)
  102. return s
  103. }
  104. // SearchType sets the search operation type. Valid values are:
  105. // "dfs_query_then_fetch" and "query_then_fetch".
  106. // See https://www.elastic.co/guide/en/elasticsearch/reference/6.2/search-request-search-type.html
  107. // for details.
  108. func (s *SearchService) SearchType(searchType string) *SearchService {
  109. s.searchType = searchType
  110. return s
  111. }
  112. // Routing is a list of specific routing values to control the shards
  113. // the search will be executed on.
  114. func (s *SearchService) Routing(routings ...string) *SearchService {
  115. s.routing = strings.Join(routings, ",")
  116. return s
  117. }
  118. // Preference sets the preference to execute the search. Defaults to
  119. // randomize across shards ("random"). Can be set to "_local" to prefer
  120. // local shards, "_primary" to execute on primary shards only,
  121. // or a custom value which guarantees that the same order will be used
  122. // across different requests.
  123. func (s *SearchService) Preference(preference string) *SearchService {
  124. s.preference = preference
  125. return s
  126. }
  127. // RequestCache indicates whether the cache should be used for this
  128. // request or not, defaults to index level setting.
  129. func (s *SearchService) RequestCache(requestCache bool) *SearchService {
  130. s.requestCache = &requestCache
  131. return s
  132. }
  133. // Query sets the query to perform, e.g. MatchAllQuery.
  134. func (s *SearchService) Query(query Query) *SearchService {
  135. s.searchSource = s.searchSource.Query(query)
  136. return s
  137. }
  138. // PostFilter will be executed after the query has been executed and
  139. // only affects the search hits, not the aggregations.
  140. // This filter is always executed as the last filtering mechanism.
  141. func (s *SearchService) PostFilter(postFilter Query) *SearchService {
  142. s.searchSource = s.searchSource.PostFilter(postFilter)
  143. return s
  144. }
  145. // FetchSource indicates whether the response should contain the stored
  146. // _source for every hit.
  147. func (s *SearchService) FetchSource(fetchSource bool) *SearchService {
  148. s.searchSource = s.searchSource.FetchSource(fetchSource)
  149. return s
  150. }
  151. // FetchSourceContext indicates how the _source should be fetched.
  152. func (s *SearchService) FetchSourceContext(fetchSourceContext *FetchSourceContext) *SearchService {
  153. s.searchSource = s.searchSource.FetchSourceContext(fetchSourceContext)
  154. return s
  155. }
  156. // Highlight adds highlighting to the search.
  157. func (s *SearchService) Highlight(highlight *Highlight) *SearchService {
  158. s.searchSource = s.searchSource.Highlight(highlight)
  159. return s
  160. }
  161. // GlobalSuggestText defines the global text to use with all suggesters.
  162. // This avoids repetition.
  163. func (s *SearchService) GlobalSuggestText(globalText string) *SearchService {
  164. s.searchSource = s.searchSource.GlobalSuggestText(globalText)
  165. return s
  166. }
  167. // Suggester adds a suggester to the search.
  168. func (s *SearchService) Suggester(suggester Suggester) *SearchService {
  169. s.searchSource = s.searchSource.Suggester(suggester)
  170. return s
  171. }
  172. // Aggregation adds an aggreation to perform as part of the search.
  173. func (s *SearchService) Aggregation(name string, aggregation Aggregation) *SearchService {
  174. s.searchSource = s.searchSource.Aggregation(name, aggregation)
  175. return s
  176. }
  177. // MinScore sets the minimum score below which docs will be filtered out.
  178. func (s *SearchService) MinScore(minScore float64) *SearchService {
  179. s.searchSource = s.searchSource.MinScore(minScore)
  180. return s
  181. }
  182. // From index to start the search from. Defaults to 0.
  183. func (s *SearchService) From(from int) *SearchService {
  184. s.searchSource = s.searchSource.From(from)
  185. return s
  186. }
  187. // Size is the number of search hits to return. Defaults to 10.
  188. func (s *SearchService) Size(size int) *SearchService {
  189. s.searchSource = s.searchSource.Size(size)
  190. return s
  191. }
  192. // Explain indicates whether each search hit should be returned with
  193. // an explanation of the hit (ranking).
  194. func (s *SearchService) Explain(explain bool) *SearchService {
  195. s.searchSource = s.searchSource.Explain(explain)
  196. return s
  197. }
  198. // Version indicates whether each search hit should be returned with
  199. // a version associated to it.
  200. func (s *SearchService) Version(version bool) *SearchService {
  201. s.searchSource = s.searchSource.Version(version)
  202. return s
  203. }
  204. // Sort adds a sort order.
  205. func (s *SearchService) Sort(field string, ascending bool) *SearchService {
  206. s.searchSource = s.searchSource.Sort(field, ascending)
  207. return s
  208. }
  209. // SortWithInfo adds a sort order.
  210. func (s *SearchService) SortWithInfo(info SortInfo) *SearchService {
  211. s.searchSource = s.searchSource.SortWithInfo(info)
  212. return s
  213. }
  214. // SortBy adds a sort order.
  215. func (s *SearchService) SortBy(sorter ...Sorter) *SearchService {
  216. s.searchSource = s.searchSource.SortBy(sorter...)
  217. return s
  218. }
  219. // NoStoredFields indicates that no stored fields should be loaded, resulting in only
  220. // id and type to be returned per field.
  221. func (s *SearchService) NoStoredFields() *SearchService {
  222. s.searchSource = s.searchSource.NoStoredFields()
  223. return s
  224. }
  225. // StoredField adds a single field to load and return (note, must be stored) as
  226. // part of the search request. If none are specified, the source of the
  227. // document will be returned.
  228. func (s *SearchService) StoredField(fieldName string) *SearchService {
  229. s.searchSource = s.searchSource.StoredField(fieldName)
  230. return s
  231. }
  232. // StoredFields sets the fields to load and return as part of the search request.
  233. // If none are specified, the source of the document will be returned.
  234. func (s *SearchService) StoredFields(fields ...string) *SearchService {
  235. s.searchSource = s.searchSource.StoredFields(fields...)
  236. return s
  237. }
  238. // TrackScores is applied when sorting and controls if scores will be
  239. // tracked as well. Defaults to false.
  240. func (s *SearchService) TrackScores(trackScores bool) *SearchService {
  241. s.searchSource = s.searchSource.TrackScores(trackScores)
  242. return s
  243. }
  244. // SearchAfter allows a different form of pagination by using a live cursor,
  245. // using the results of the previous page to help the retrieval of the next.
  246. //
  247. // See https://www.elastic.co/guide/en/elasticsearch/reference/6.2/search-request-search-after.html
  248. func (s *SearchService) SearchAfter(sortValues ...interface{}) *SearchService {
  249. s.searchSource = s.searchSource.SearchAfter(sortValues...)
  250. return s
  251. }
  252. // IgnoreUnavailable indicates whether the specified concrete indices
  253. // should be ignored when unavailable (missing or closed).
  254. func (s *SearchService) IgnoreUnavailable(ignoreUnavailable bool) *SearchService {
  255. s.ignoreUnavailable = &ignoreUnavailable
  256. return s
  257. }
  258. // AllowNoIndices indicates whether to ignore if a wildcard indices
  259. // expression resolves into no concrete indices. (This includes `_all` string
  260. // or when no indices have been specified).
  261. func (s *SearchService) AllowNoIndices(allowNoIndices bool) *SearchService {
  262. s.allowNoIndices = &allowNoIndices
  263. return s
  264. }
  265. // ExpandWildcards indicates whether to expand wildcard expression to
  266. // concrete indices that are open, closed or both.
  267. func (s *SearchService) ExpandWildcards(expandWildcards string) *SearchService {
  268. s.expandWildcards = expandWildcards
  269. return s
  270. }
  271. // MaxResponseSize sets an upper limit on the response body size that we accept,
  272. // to guard against OOM situations.
  273. func (s *SearchService) MaxResponseSize(maxResponseSize int64) *SearchService {
  274. s.maxResponseSize = maxResponseSize
  275. return s
  276. }
  277. // buildURL builds the URL for the operation.
  278. func (s *SearchService) buildURL() (string, url.Values, error) {
  279. var err error
  280. var path string
  281. if len(s.index) > 0 && len(s.typ) > 0 {
  282. path, err = uritemplates.Expand("/{index}/{type}/_search", map[string]string{
  283. "index": strings.Join(s.index, ","),
  284. "type": strings.Join(s.typ, ","),
  285. })
  286. } else if len(s.index) > 0 {
  287. path, err = uritemplates.Expand("/{index}/_search", map[string]string{
  288. "index": strings.Join(s.index, ","),
  289. })
  290. } else if len(s.typ) > 0 {
  291. path, err = uritemplates.Expand("/_all/{type}/_search", map[string]string{
  292. "type": strings.Join(s.typ, ","),
  293. })
  294. } else {
  295. path = "/_search"
  296. }
  297. if err != nil {
  298. return "", url.Values{}, err
  299. }
  300. // Add query string parameters
  301. params := url.Values{}
  302. if s.pretty {
  303. params.Set("pretty", fmt.Sprintf("%v", s.pretty))
  304. }
  305. if s.searchType != "" {
  306. params.Set("search_type", s.searchType)
  307. }
  308. if s.routing != "" {
  309. params.Set("routing", s.routing)
  310. }
  311. if s.preference != "" {
  312. params.Set("preference", s.preference)
  313. }
  314. if s.requestCache != nil {
  315. params.Set("request_cache", fmt.Sprintf("%v", *s.requestCache))
  316. }
  317. if s.allowNoIndices != nil {
  318. params.Set("allow_no_indices", fmt.Sprintf("%v", *s.allowNoIndices))
  319. }
  320. if s.expandWildcards != "" {
  321. params.Set("expand_wildcards", s.expandWildcards)
  322. }
  323. if s.ignoreUnavailable != nil {
  324. params.Set("ignore_unavailable", fmt.Sprintf("%v", *s.ignoreUnavailable))
  325. }
  326. if len(s.filterPath) > 0 {
  327. params.Set("filter_path", strings.Join(s.filterPath, ","))
  328. }
  329. return path, params, nil
  330. }
  331. // Validate checks if the operation is valid.
  332. func (s *SearchService) Validate() error {
  333. return nil
  334. }
  335. // Do executes the search and returns a SearchResult.
  336. func (s *SearchService) Do(ctx context.Context) (*SearchResult, error) {
  337. // Check pre-conditions
  338. if err := s.Validate(); err != nil {
  339. return nil, err
  340. }
  341. // Get URL for request
  342. path, params, err := s.buildURL()
  343. if err != nil {
  344. return nil, err
  345. }
  346. // Perform request
  347. var body interface{}
  348. if s.source != nil {
  349. body = s.source
  350. } else {
  351. src, err := s.searchSource.Source()
  352. if err != nil {
  353. return nil, err
  354. }
  355. body = src
  356. }
  357. res, err := s.client.PerformRequest(ctx, PerformRequestOptions{
  358. Method: "POST",
  359. Path: path,
  360. Params: params,
  361. Body: body,
  362. MaxResponseSize: s.maxResponseSize,
  363. })
  364. if err != nil {
  365. return nil, err
  366. }
  367. // Return search results
  368. ret := new(SearchResult)
  369. if err := s.client.decoder.Decode(res.Body, ret); err != nil {
  370. return nil, err
  371. }
  372. return ret, nil
  373. }
  374. // SearchResult is the result of a search in Elasticsearch.
  375. type SearchResult struct {
  376. TookInMillis int64 `json:"took,omitempty"` // search time in milliseconds
  377. ScrollId string `json:"_scroll_id,omitempty"` // only used with Scroll and Scan operations
  378. Hits *SearchHits `json:"hits,omitempty"` // the actual search hits
  379. Suggest SearchSuggest `json:"suggest,omitempty"` // results from suggesters
  380. Aggregations Aggregations `json:"aggregations,omitempty"` // results from aggregations
  381. TimedOut bool `json:"timed_out,omitempty"` // true if the search timed out
  382. Error *ErrorDetails `json:"error,omitempty"` // only used in MultiGet
  383. Profile *SearchProfile `json:"profile,omitempty"` // profiling results, if optional Profile API was active for this search
  384. Shards *ShardsInfo `json:"_shards,omitempty"` // shard information
  385. }
  386. // TotalHits is a convenience function to return the number of hits for
  387. // a search result.
  388. func (r *SearchResult) TotalHits() int64 {
  389. if r.Hits != nil {
  390. return r.Hits.TotalHits
  391. }
  392. return 0
  393. }
  394. // Each is a utility function to iterate over all hits. It saves you from
  395. // checking for nil values. Notice that Each will ignore errors in
  396. // serializing JSON and hits with empty/nil _source will get an empty
  397. // value
  398. func (r *SearchResult) Each(typ reflect.Type) []interface{} {
  399. if r.Hits == nil || r.Hits.Hits == nil || len(r.Hits.Hits) == 0 {
  400. return nil
  401. }
  402. var slice []interface{}
  403. for _, hit := range r.Hits.Hits {
  404. v := reflect.New(typ).Elem()
  405. if hit.Source == nil {
  406. slice = append(slice, v.Interface())
  407. continue
  408. }
  409. if err := json.Unmarshal(*hit.Source, v.Addr().Interface()); err == nil {
  410. slice = append(slice, v.Interface())
  411. }
  412. }
  413. return slice
  414. }
  415. // SearchHits specifies the list of search hits.
  416. type SearchHits struct {
  417. TotalHits int64 `json:"total"` // total number of hits found
  418. MaxScore *float64 `json:"max_score,omitempty"` // maximum score of all hits
  419. Hits []*SearchHit `json:"hits,omitempty"` // the actual hits returned
  420. }
  421. // NestedHit is a nested innerhit
  422. type NestedHit struct {
  423. Field string `json:"field"`
  424. Offset int `json:"offset,omitempty"`
  425. Child *NestedHit `json:"_nested,omitempty"`
  426. }
  427. // SearchHit is a single hit.
  428. type SearchHit struct {
  429. Score *float64 `json:"_score,omitempty"` // computed score
  430. Index string `json:"_index,omitempty"` // index name
  431. Type string `json:"_type,omitempty"` // type meta field
  432. Id string `json:"_id,omitempty"` // external or internal
  433. Uid string `json:"_uid,omitempty"` // uid meta field (see MapperService.java for all meta fields)
  434. Routing string `json:"_routing,omitempty"` // routing meta field
  435. Parent string `json:"_parent,omitempty"` // parent meta field
  436. Version *int64 `json:"_version,omitempty"` // version number, when Version is set to true in SearchService
  437. Sort []interface{} `json:"sort,omitempty"` // sort information
  438. Highlight SearchHitHighlight `json:"highlight,omitempty"` // highlighter information
  439. Source *json.RawMessage `json:"_source,omitempty"` // stored document source
  440. Fields map[string]interface{} `json:"fields,omitempty"` // returned (stored) fields
  441. Explanation *SearchExplanation `json:"_explanation,omitempty"` // explains how the score was computed
  442. MatchedQueries []string `json:"matched_queries,omitempty"` // matched queries
  443. InnerHits map[string]*SearchHitInnerHits `json:"inner_hits,omitempty"` // inner hits with ES >= 1.5.0
  444. Nested *NestedHit `json:"_nested,omitempty"` // for nested inner hits
  445. // Shard
  446. // HighlightFields
  447. // SortValues
  448. // MatchedFilters
  449. }
  450. // SearchHitInnerHits is used for inner hits.
  451. type SearchHitInnerHits struct {
  452. Hits *SearchHits `json:"hits,omitempty"`
  453. }
  454. // SearchExplanation explains how the score for a hit was computed.
  455. // See https://www.elastic.co/guide/en/elasticsearch/reference/6.2/search-request-explain.html.
  456. type SearchExplanation struct {
  457. Value float64 `json:"value"` // e.g. 1.0
  458. Description string `json:"description"` // e.g. "boost" or "ConstantScore(*:*), product of:"
  459. Details []SearchExplanation `json:"details,omitempty"` // recursive details
  460. }
  461. // Suggest
  462. // SearchSuggest is a map of suggestions.
  463. // See https://www.elastic.co/guide/en/elasticsearch/reference/6.2/search-suggesters.html.
  464. type SearchSuggest map[string][]SearchSuggestion
  465. // SearchSuggestion is a single search suggestion.
  466. // See https://www.elastic.co/guide/en/elasticsearch/reference/6.2/search-suggesters.html.
  467. type SearchSuggestion struct {
  468. Text string `json:"text"`
  469. Offset int `json:"offset"`
  470. Length int `json:"length"`
  471. Options []SearchSuggestionOption `json:"options"`
  472. }
  473. // SearchSuggestionOption is an option of a SearchSuggestion.
  474. // See https://www.elastic.co/guide/en/elasticsearch/reference/6.2/search-suggesters.html.
  475. type SearchSuggestionOption struct {
  476. Text string `json:"text"`
  477. Index string `json:"_index"`
  478. Type string `json:"_type"`
  479. Id string `json:"_id"`
  480. Score float64 `json:"score"` // term and phrase suggesters uses "score" as of 6.2.4
  481. ScoreUnderscore float64 `json:"_score"` // completion and context suggesters uses "_score" as of 6.2.4
  482. Highlighted string `json:"highlighted"`
  483. CollateMatch bool `json:"collate_match"`
  484. Freq int `json:"freq"` // from TermSuggestion.Option in Java API
  485. Source *json.RawMessage `json:"_source"`
  486. }
  487. // SearchProfile is a list of shard profiling data collected during
  488. // query execution in the "profile" section of a SearchResult
  489. type SearchProfile struct {
  490. Shards []SearchProfileShardResult `json:"shards"`
  491. }
  492. // SearchProfileShardResult returns the profiling data for a single shard
  493. // accessed during the search query or aggregation.
  494. type SearchProfileShardResult struct {
  495. ID string `json:"id"`
  496. Searches []QueryProfileShardResult `json:"searches"`
  497. Aggregations []ProfileResult `json:"aggregations"`
  498. }
  499. // QueryProfileShardResult is a container class to hold the profile results
  500. // for a single shard in the request. It comtains a list of query profiles,
  501. // a collector tree and a total rewrite tree.
  502. type QueryProfileShardResult struct {
  503. Query []ProfileResult `json:"query,omitempty"`
  504. RewriteTime int64 `json:"rewrite_time,omitempty"`
  505. Collector []interface{} `json:"collector,omitempty"`
  506. }
  507. // CollectorResult holds the profile timings of the collectors used in the
  508. // search. Children's CollectorResults may be embedded inside of a parent
  509. // CollectorResult.
  510. type CollectorResult struct {
  511. Name string `json:"name,omitempty"`
  512. Reason string `json:"reason,omitempty"`
  513. Time string `json:"time,omitempty"`
  514. TimeNanos int64 `json:"time_in_nanos,omitempty"`
  515. Children []CollectorResult `json:"children,omitempty"`
  516. }
  517. // ProfileResult is the internal representation of a profiled query,
  518. // corresponding to a single node in the query tree.
  519. type ProfileResult struct {
  520. Type string `json:"type"`
  521. Description string `json:"description,omitempty"`
  522. NodeTime string `json:"time,omitempty"`
  523. NodeTimeNanos int64 `json:"time_in_nanos,omitempty"`
  524. Breakdown map[string]int64 `json:"breakdown,omitempty"`
  525. Children []ProfileResult `json:"children,omitempty"`
  526. }
  527. // Aggregations (see search_aggs.go)
  528. // Highlighting
  529. // SearchHitHighlight is the highlight information of a search hit.
  530. // See https://www.elastic.co/guide/en/elasticsearch/reference/6.2/search-request-highlighting.html
  531. // for a general discussion of highlighting.
  532. type SearchHitHighlight map[string][]string