searchkick 1.3.4 → 1.3.5

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: e771f2aea318aada5efd68350c101abca94a77a8
4
- data.tar.gz: 4c3d8566133e79b6456609a9d50faf84be229454
3
+ metadata.gz: e69261f029a233983def6aaec3cd101621807fe8
4
+ data.tar.gz: 93d6d5e53193af4ae04537c012845d336a9b7197
5
5
  SHA512:
6
- metadata.gz: 67375bbcd44840170e0fc29eeb84308f4e6814b24c30e8c8e6d0ed177df04dc038f7d0f4084d9cba19a2f5116249bb8313a37b01707e4163a83aeccc89a812c1
7
- data.tar.gz: e31a38b61ed0ddf786f7aaf8be45eb39a938122305a9886d60fc33eaac335b267c468e1fb01511bf5863ca390bea86b216e822461422656e956c423948a3361c
6
+ metadata.gz: 2a2f79b352af2ae191c719480e648e07fc8083e3d6c836632e5f1f5245786386bad8e0397d15f3c13771a7197e92052a9c2ba269de23f3f5858fb528642eff26
7
+ data.tar.gz: 57972ac3b439dcbf77bef6d9f121383f93822ae3e3c0b6bbf64baedbe0f46a2dd1dd8c9ded33a0384bb80abb5922a33bd216cdeea9271c25f7dc754fd0f7c5a4
data/.travis.yml CHANGED
@@ -24,7 +24,7 @@ gemfile:
24
24
  - test/gemfiles/mongoid4.gemfile
25
25
  - test/gemfiles/mongoid5.gemfile
26
26
  env:
27
- - ELASTICSEARCH_VERSION=2.3.0
27
+ - ELASTICSEARCH_VERSION=2.4.0
28
28
  matrix:
29
29
  include:
30
30
  - gemfile: Gemfile
@@ -34,11 +34,11 @@ matrix:
34
34
  - gemfile: Gemfile
35
35
  env: ELASTICSEARCH_VERSION=2.0.0
36
36
  - gemfile: Gemfile
37
- env: ELASTICSEARCH_VERSION=5.0.0-alpha2
37
+ env: ELASTICSEARCH_VERSION=5.0.0-beta1
38
38
  # - gemfile: test/gemfiles/nobrainer.gemfile
39
39
  # env: NOBRAINER=true
40
40
  allow_failures:
41
41
  - gemfile: Gemfile
42
- env: ELASTICSEARCH_VERSION=5.0.0-alpha2
42
+ env: ELASTICSEARCH_VERSION=5.0.0-beta1
43
43
  # - gemfile: test/gemfiles/nobrainer.gemfile
44
44
  # env: NOBRAINER=true
data/CHANGELOG.md CHANGED
@@ -1,3 +1,9 @@
1
+ ## 1.3.5
2
+
3
+ - Added support for Elasticsearch 5.0 beta
4
+ - Added `request_params` option
5
+ - Added `filterable` option
6
+
1
7
  ## 1.3.4
2
8
 
3
9
  - Added `resume` option to reindex
data/Gemfile CHANGED
@@ -6,3 +6,4 @@ gemspec
6
6
  gem "sqlite3"
7
7
  gem "activerecord", "~> 5.0.0"
8
8
  gem "gemoji-parser"
9
+ gem "typhoeus"
data/README.md CHANGED
@@ -116,6 +116,12 @@ Limit / offset
116
116
  limit: 20, offset: 40
117
117
  ```
118
118
 
119
+ Select
120
+
121
+ ```ruby
122
+ select_v2: ["name"]
123
+ ```
124
+
119
125
  ### Results
120
126
 
121
127
  Searches return a `Searchkick::Results` object. This responds like an array to most methods.
@@ -127,6 +133,12 @@ results.any?
127
133
  results.each { |result| ... }
128
134
  ```
129
135
 
136
+ By default, ids are fetched from Elasticsearch and records are fetched from your database. To fetch everything from Elasticsearch, use:
137
+
138
+ ```ruby
139
+ Product.search("apples", load: false)
140
+ ```
141
+
130
142
  Get total results
131
143
 
132
144
  ```ruby
@@ -476,7 +488,7 @@ Next, add conversions to the index.
476
488
 
477
489
  ```ruby
478
490
  class Product < ActiveRecord::Base
479
- has_many :searches, class_name: "Searchjoy::Search"
491
+ has_many :searches, class_name: "Searchjoy::Search", as: :convertable
480
492
 
481
493
  searchkick conversions: ["conversions"] # name of field
482
494
 
@@ -593,7 +605,7 @@ products.suggestions # ["peanut butter"]
593
605
 
594
606
  ### Aggregations
595
607
 
596
- [Aggregations](https://www.elastic.co/guide/en/elasticsearch/reference/current/search-facets.html) provide aggregated search data.
608
+ [Aggregations](https://www.elastic.co/guide/en/elasticsearch/reference/current/search-aggregations.html) provide aggregated search data.
597
609
 
598
610
  ![Aggregations](https://raw.githubusercontent.com/ankane/searchkick/gh-pages/facets.png)
599
611
 
@@ -1121,7 +1133,7 @@ products =
1121
1133
  end
1122
1134
  ```
1123
1135
 
1124
- ### Multi Search
1136
+ ## Multi Search
1125
1137
 
1126
1138
  To batch search requests for performance, use:
1127
1139
 
@@ -1135,7 +1147,7 @@ Then use `fresh_products` and `frozen_products` as typical results.
1135
1147
 
1136
1148
  **Note:** Errors are not raised as with single requests. Use the `error` method on each query to check for errors. Also, if you use the `below` option for misspellings, misspellings will be disabled.
1137
1149
 
1138
- ### Multiple Indices
1150
+ ## Multiple Indices
1139
1151
 
1140
1152
  Search across multiple indices with:
1141
1153
 
@@ -1149,6 +1161,14 @@ Boost specific indices with:
1149
1161
  indices_boost: {Category => 2, Product => 1}
1150
1162
  ```
1151
1163
 
1164
+ ## Nested Data
1165
+
1166
+ To query nested data, use dot notation.
1167
+
1168
+ ```ruby
1169
+ User.search "*", where: {"address.zip_code" => 12345}
1170
+ ```
1171
+
1152
1172
  ## Reference
1153
1173
 
1154
1174
  Reindex one record
@@ -1267,13 +1287,7 @@ Searchkick.search_method_name = :lookup
1267
1287
  Eager load associations
1268
1288
 
1269
1289
  ```ruby
1270
- Product.search "milk", include: [:brand, :stores]
1271
- ```
1272
-
1273
- Do not load models
1274
-
1275
- ```ruby
1276
- Product.search "milk", load: false
1290
+ Product.search "milk", includes: [:brand, :stores]
1277
1291
  ```
1278
1292
 
1279
1293
  Turn off special characters
@@ -1314,6 +1328,12 @@ products = Product.search("carrots", execute: false)
1314
1328
  products.each { ... } # search not executed until here
1315
1329
  ```
1316
1330
 
1331
+ Add [request parameters](https://www.elastic.co/guide/en/elasticsearch/reference/current/search-uri-request.html), like `search_type` and `query_cache`
1332
+
1333
+ ```ruby
1334
+ Product.search("carrots", request_params: {search_type: "dfs_query_then_fetch"})
1335
+ ```
1336
+
1317
1337
  Make fields unsearchable but include in the source
1318
1338
 
1319
1339
  ```ruby
data/lib/searchkick.rb CHANGED
@@ -2,6 +2,7 @@ require "active_model"
2
2
  require "elasticsearch"
3
3
  require "hashie"
4
4
  require "searchkick/version"
5
+ require "searchkick/index_options"
5
6
  require "searchkick/index"
6
7
  require "searchkick/results"
7
8
  require "searchkick/query"
@@ -11,14 +12,6 @@ require "searchkick/tasks"
11
12
  require "searchkick/middleware"
12
13
  require "searchkick/logging" if defined?(ActiveSupport::Notifications)
13
14
 
14
- # background jobs
15
- begin
16
- require "active_job"
17
- rescue LoadError
18
- # do nothing
19
- end
20
- require "searchkick/reindex_v2_job" if defined?(ActiveJob)
21
-
22
15
  module Searchkick
23
16
  class Error < StandardError; end
24
17
  class MissingIndexError < Error; end
@@ -37,13 +30,16 @@ module Searchkick
37
30
  self.models = []
38
31
 
39
32
  def self.client
40
- @client ||=
33
+ @client ||= begin
34
+ require "typhoeus/adapters/faraday" if defined?(Typhoeus)
35
+
41
36
  Elasticsearch::Client.new(
42
37
  url: ENV["ELASTICSEARCH_URL"],
43
- transport_options: {request: {timeout: timeout}}
38
+ transport_options: {request: {timeout: timeout}, headers: {content_type: "application/json"}}
44
39
  ) do |f|
45
40
  f.use Searchkick::Middleware
46
41
  end
42
+ end
47
43
  end
48
44
 
49
45
  def self.env
@@ -156,6 +152,13 @@ module Searchkick
156
152
  end
157
153
  end
158
154
 
155
+ ActiveSupport.on_load(:active_job) do
156
+ require "searchkick/reindex_v2_job"
157
+ end
158
+
159
159
  # TODO find better ActiveModel hook
160
160
  ActiveModel::Callbacks.send(:include, Searchkick::Model)
161
- ActiveRecord::Base.send(:extend, Searchkick::Model) if defined?(ActiveRecord)
161
+
162
+ ActiveSupport.on_load(:active_record) do
163
+ ActiveRecord::Base.send(:extend, Searchkick::Model)
164
+ end
@@ -1,5 +1,7 @@
1
1
  module Searchkick
2
2
  class Index
3
+ include IndexOptions
4
+
3
5
  attr_reader :name, :options
4
6
 
5
7
  def initialize(name, options = {})
@@ -95,8 +97,10 @@ module Searchkick
95
97
  if Searchkick.callbacks_value.nil?
96
98
  if defined?(Searchkick::ReindexV2Job)
97
99
  Searchkick::ReindexV2Job.perform_later(record.class.name, record.id.to_s)
98
- else
100
+ elsif defined?(Delayed::Job)
99
101
  Delayed::Job.enqueue Searchkick::ReindexJob.new(record.class.name, record.id.to_s)
102
+ else
103
+ raise Searchkick::Error, "Job adapter not found"
100
104
  end
101
105
  else
102
106
  reindex_record(record)
@@ -123,7 +127,7 @@ module Searchkick
123
127
 
124
128
  def search_model(searchkick_klass, term = nil, options = {}, &block)
125
129
  query = Searchkick::Query.new(searchkick_klass, term, options)
126
- block.call(query.body) if block
130
+ yield(query.body) if block
127
131
  if options[:execute] == false
128
132
  query
129
133
  else
@@ -147,8 +151,8 @@ module Searchkick
147
151
  rescue Elasticsearch::Transport::Transport::Errors::NotFound
148
152
  {}
149
153
  end
150
- indices = indices.select { |k, v| v.empty? || v["aliases"].empty? } if options[:unaliased]
151
- indices.select { |k, v| k =~ /\A#{Regexp.escape(name)}_\d{14,17}\z/ }.keys
154
+ indices = indices.select { |_k, v| v.empty? || v["aliases"].empty? } if options[:unaliased]
155
+ indices.select { |k, _v| k =~ /\A#{Regexp.escape(name)}_\d{14,17}\z/ }.keys
152
156
  end
153
157
 
154
158
  # remove old indices that start w/ index_name
@@ -244,357 +248,6 @@ module Searchkick
244
248
  end
245
249
  end
246
250
 
247
- def index_options
248
- options = @options
249
- language = options[:language]
250
- language = language.call if language.respond_to?(:call)
251
-
252
- if options[:mappings] && !options[:merge_mappings]
253
- settings = options[:settings] || {}
254
- mappings = options[:mappings]
255
- else
256
- below22 = Searchkick.server_below?("2.2.0")
257
- below50 = Searchkick.server_below?("5.0.0-alpha1")
258
- default_type = below50 ? "string" : "text"
259
- default_analyzer = below50 ? :default_index : :default
260
- keyword_mapping =
261
- if below50
262
- {
263
- type: default_type,
264
- index: "not_analyzed"
265
- }
266
- else
267
- {
268
- type: "keyword"
269
- }
270
- end
271
-
272
- keyword_mapping[:ignore_above] = 256 unless below22
273
-
274
- settings = {
275
- analysis: {
276
- analyzer: {
277
- searchkick_keyword: {
278
- type: "custom",
279
- tokenizer: "keyword",
280
- filter: ["lowercase"] + (options[:stem_conversions] == false ? [] : ["searchkick_stemmer"])
281
- },
282
- default_analyzer => {
283
- type: "custom",
284
- # character filters -> tokenizer -> token filters
285
- # https://www.elastic.co/guide/en/elasticsearch/guide/current/analysis-intro.html
286
- char_filter: ["ampersand"],
287
- tokenizer: "standard",
288
- # synonym should come last, after stemming and shingle
289
- # shingle must come before searchkick_stemmer
290
- filter: ["standard", "lowercase", "asciifolding", "searchkick_index_shingle", "searchkick_stemmer"]
291
- },
292
- searchkick_search: {
293
- type: "custom",
294
- char_filter: ["ampersand"],
295
- tokenizer: "standard",
296
- filter: ["standard", "lowercase", "asciifolding", "searchkick_search_shingle", "searchkick_stemmer"]
297
- },
298
- searchkick_search2: {
299
- type: "custom",
300
- char_filter: ["ampersand"],
301
- tokenizer: "standard",
302
- filter: ["standard", "lowercase", "asciifolding", "searchkick_stemmer"]
303
- },
304
- # https://github.com/leschenko/elasticsearch_autocomplete/blob/master/lib/elasticsearch_autocomplete/analyzers.rb
305
- searchkick_autocomplete_index: {
306
- type: "custom",
307
- tokenizer: "searchkick_autocomplete_ngram",
308
- filter: ["lowercase", "asciifolding"]
309
- },
310
- searchkick_autocomplete_search: {
311
- type: "custom",
312
- tokenizer: "keyword",
313
- filter: ["lowercase", "asciifolding"]
314
- },
315
- searchkick_word_search: {
316
- type: "custom",
317
- tokenizer: "standard",
318
- filter: ["lowercase", "asciifolding"]
319
- },
320
- searchkick_suggest_index: {
321
- type: "custom",
322
- tokenizer: "standard",
323
- filter: ["lowercase", "asciifolding", "searchkick_suggest_shingle"]
324
- },
325
- searchkick_text_start_index: {
326
- type: "custom",
327
- tokenizer: "keyword",
328
- filter: ["lowercase", "asciifolding", "searchkick_edge_ngram"]
329
- },
330
- searchkick_text_middle_index: {
331
- type: "custom",
332
- tokenizer: "keyword",
333
- filter: ["lowercase", "asciifolding", "searchkick_ngram"]
334
- },
335
- searchkick_text_end_index: {
336
- type: "custom",
337
- tokenizer: "keyword",
338
- filter: ["lowercase", "asciifolding", "reverse", "searchkick_edge_ngram", "reverse"]
339
- },
340
- searchkick_word_start_index: {
341
- type: "custom",
342
- tokenizer: "standard",
343
- filter: ["lowercase", "asciifolding", "searchkick_edge_ngram"]
344
- },
345
- searchkick_word_middle_index: {
346
- type: "custom",
347
- tokenizer: "standard",
348
- filter: ["lowercase", "asciifolding", "searchkick_ngram"]
349
- },
350
- searchkick_word_end_index: {
351
- type: "custom",
352
- tokenizer: "standard",
353
- filter: ["lowercase", "asciifolding", "reverse", "searchkick_edge_ngram", "reverse"]
354
- }
355
- },
356
- filter: {
357
- searchkick_index_shingle: {
358
- type: "shingle",
359
- token_separator: ""
360
- },
361
- # lucky find http://web.archiveorange.com/archive/v/AAfXfQ17f57FcRINsof7
362
- searchkick_search_shingle: {
363
- type: "shingle",
364
- token_separator: "",
365
- output_unigrams: false,
366
- output_unigrams_if_no_shingles: true
367
- },
368
- searchkick_suggest_shingle: {
369
- type: "shingle",
370
- max_shingle_size: 5
371
- },
372
- searchkick_edge_ngram: {
373
- type: "edgeNGram",
374
- min_gram: 1,
375
- max_gram: 50
376
- },
377
- searchkick_ngram: {
378
- type: "nGram",
379
- min_gram: 1,
380
- max_gram: 50
381
- },
382
- searchkick_stemmer: {
383
- # use stemmer if language is lowercase, snowball otherwise
384
- # TODO deprecate language option in favor of stemmer
385
- type: language == language.to_s.downcase ? "stemmer" : "snowball",
386
- language: language || "English"
387
- }
388
- },
389
- char_filter: {
390
- # https://www.elastic.co/guide/en/elasticsearch/guide/current/custom-analyzers.html
391
- # &_to_and
392
- ampersand: {
393
- type: "mapping",
394
- mappings: ["&=> and "]
395
- }
396
- },
397
- tokenizer: {
398
- searchkick_autocomplete_ngram: {
399
- type: "edgeNGram",
400
- min_gram: 1,
401
- max_gram: 50
402
- }
403
- }
404
- }
405
- }
406
-
407
- if Searchkick.env == "test"
408
- settings.merge!(number_of_shards: 1, number_of_replicas: 0)
409
- end
410
-
411
- if options[:similarity]
412
- settings[:similarity] = {default: {type: options[:similarity]}}
413
- end
414
-
415
- settings.deep_merge!(options[:settings] || {})
416
-
417
- # synonyms
418
- synonyms = options[:synonyms] || []
419
-
420
- synonyms = synonyms.call if synonyms.respond_to?(:call)
421
-
422
- if synonyms.any?
423
- settings[:analysis][:filter][:searchkick_synonym] = {
424
- type: "synonym",
425
- synonyms: synonyms.select { |s| s.size > 1 }.map { |s| s.join(",") }
426
- }
427
- # choosing a place for the synonym filter when stemming is not easy
428
- # https://groups.google.com/forum/#!topic/elasticsearch/p7qcQlgHdB8
429
- # TODO use a snowball stemmer on synonyms when creating the token filter
430
-
431
- # http://elasticsearch-users.115913.n3.nabble.com/synonym-multi-words-search-td4030811.html
432
- # I find the following approach effective if you are doing multi-word synonyms (synonym phrases):
433
- # - Only apply the synonym expansion at index time
434
- # - Don't have the synonym filter applied search
435
- # - Use directional synonyms where appropriate. You want to make sure that you're not injecting terms that are too general.
436
- settings[:analysis][:analyzer][default_analyzer][:filter].insert(4, "searchkick_synonym")
437
- settings[:analysis][:analyzer][default_analyzer][:filter] << "searchkick_synonym"
438
-
439
- %w(word_start word_middle word_end).each do |type|
440
- settings[:analysis][:analyzer]["searchkick_#{type}_index".to_sym][:filter].insert(2, "searchkick_synonym")
441
- end
442
- end
443
-
444
- if options[:wordnet]
445
- settings[:analysis][:filter][:searchkick_wordnet] = {
446
- type: "synonym",
447
- format: "wordnet",
448
- synonyms_path: Searchkick.wordnet_path
449
- }
450
-
451
- settings[:analysis][:analyzer][default_analyzer][:filter].insert(4, "searchkick_wordnet")
452
- settings[:analysis][:analyzer][default_analyzer][:filter] << "searchkick_wordnet"
453
-
454
- %w(word_start word_middle word_end).each do |type|
455
- settings[:analysis][:analyzer]["searchkick_#{type}_index".to_sym][:filter].insert(2, "searchkick_wordnet")
456
- end
457
- end
458
-
459
- if options[:special_characters] == false
460
- settings[:analysis][:analyzer].each do |_, analyzer_settings|
461
- analyzer_settings[:filter].reject! { |f| f == "asciifolding" }
462
- end
463
- end
464
-
465
- mapping = {}
466
-
467
- # conversions
468
- Array(options[:conversions]).each do |conversions_field|
469
- mapping[conversions_field] = {
470
- type: "nested",
471
- properties: {
472
- query: {type: default_type, analyzer: "searchkick_keyword"},
473
- count: {type: "integer"}
474
- }
475
- }
476
- end
477
-
478
- mapping_options = Hash[
479
- [:autocomplete, :suggest, :word, :text_start, :text_middle, :text_end, :word_start, :word_middle, :word_end, :highlight, :searchable, :only_analyzed]
480
- .map { |type| [type, (options[type] || []).map(&:to_s)] }
481
- ]
482
-
483
- word = options[:word] != false && (!options[:match] || options[:match] == :word)
484
-
485
- mapping_options.values.flatten.uniq.each do |field|
486
- fields = {}
487
-
488
- if mapping_options[:only_analyzed].include?(field)
489
- fields[field] = {type: default_type, index: "no"}
490
- else
491
- fields[field] = keyword_mapping
492
- end
493
-
494
- if !options[:searchable] || mapping_options[:searchable].include?(field)
495
- if word
496
- fields["analyzed"] = {type: default_type, index: "analyzed", analyzer: default_analyzer}
497
-
498
- if mapping_options[:highlight].include?(field)
499
- fields["analyzed"][:term_vector] = "with_positions_offsets"
500
- end
501
- end
502
-
503
- mapping_options.except(:highlight, :searchable, :only_analyzed).each do |type, f|
504
- if options[:match] == type || f.include?(field)
505
- fields[type] = {type: default_type, index: "analyzed", analyzer: "searchkick_#{type}_index"}
506
- end
507
- end
508
- end
509
-
510
- mapping[field] =
511
- if below50
512
- {
513
- type: "multi_field",
514
- fields: fields
515
- }
516
- elsif fields[field]
517
- fields[field].merge(fields: fields.except(field))
518
- end
519
- end
520
-
521
- (options[:locations] || []).map(&:to_s).each do |field|
522
- mapping[field] = {
523
- type: "geo_point"
524
- }
525
- end
526
-
527
- (options[:unsearchable] || []).map(&:to_s).each do |field|
528
- mapping[field] = {
529
- type: default_type,
530
- index: "no"
531
- }
532
- end
533
-
534
- routing = {}
535
- if options[:routing]
536
- routing = {required: true}
537
- unless options[:routing] == true
538
- routing[:path] = options[:routing].to_s
539
- end
540
- end
541
-
542
- dynamic_fields = {
543
- # analyzed field must be the default field for include_in_all
544
- # http://www.elasticsearch.org/guide/reference/mapping/multi-field-type/
545
- # however, we can include the not_analyzed field in _all
546
- # and the _all index analyzer will take care of it
547
- "{name}" => keyword_mapping.merge(include_in_all: !options[:searchable])
548
- }
549
-
550
- dynamic_fields["{name}"][:ignore_above] = 256 unless below22
551
-
552
- unless options[:searchable]
553
- if options[:match] && options[:match] != :word
554
- dynamic_fields[options[:match]] = {type: default_type, index: "analyzed", analyzer: "searchkick_#{options[:match]}_index"}
555
- end
556
-
557
- if word
558
- dynamic_fields["analyzed"] = {type: default_type, index: "analyzed"}
559
- end
560
- end
561
-
562
- # http://www.elasticsearch.org/guide/reference/mapping/multi-field-type/
563
- multi_field =
564
- if below50
565
- {
566
- type: "multi_field",
567
- fields: dynamic_fields
568
- }
569
- else
570
- dynamic_fields["{name}"].merge(fields: dynamic_fields.except("{name}"))
571
- end
572
-
573
- mappings = {
574
- _default_: {
575
- _all: {type: default_type, index: "analyzed", analyzer: default_analyzer},
576
- properties: mapping,
577
- _routing: routing,
578
- # https://gist.github.com/kimchy/2898285
579
- dynamic_templates: [
580
- {
581
- string_template: {
582
- match: "*",
583
- match_mapping_type: "string",
584
- mapping: multi_field
585
- }
586
- }
587
- ]
588
- }
589
- }.deep_merge(options[:mappings] || {})
590
- end
591
-
592
- {
593
- settings: settings,
594
- mappings: mappings
595
- }
596
- end
597
-
598
251
  # other
599
252
 
600
253
  def tokens(text, options = {})
@@ -634,10 +287,10 @@ module Searchkick
634
287
 
635
288
  # stringify fields
636
289
  # remove _id since search_id is used instead
637
- source = source.inject({}) { |memo, (k, v)| memo[k.to_s] = v; memo }.except("_id")
290
+ source = source.each_with_object({}) { |(k, v), memo| memo[k.to_s] = v; memo }.except("_id")
638
291
 
639
292
  # conversions
640
- Array(options[:conversions]).each do |conversions_field|
293
+ Array(options[:conversions]).map(&:to_s).each do |conversions_field|
641
294
  if source[conversions_field]
642
295
  source[conversions_field] = source[conversions_field].map { |k, v| {query: k, count: v} }
643
296
  end