searchkick 1.3.4 → 1.3.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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