searchkick 0.9.1 → 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (45) hide show
  1. checksums.yaml +4 -4
  2. data/.travis.yml +10 -9
  3. data/CHANGELOG.md +14 -0
  4. data/README.md +190 -14
  5. data/lib/searchkick.rb +1 -0
  6. data/lib/searchkick/index.rb +8 -8
  7. data/lib/searchkick/logging.rb +4 -2
  8. data/lib/searchkick/model.rb +13 -10
  9. data/lib/searchkick/query.rb +107 -39
  10. data/lib/searchkick/reindex_job.rb +0 -2
  11. data/lib/searchkick/reindex_v2_job.rb +0 -1
  12. data/lib/searchkick/results.rb +18 -1
  13. data/lib/searchkick/tasks.rb +0 -2
  14. data/lib/searchkick/version.rb +1 -1
  15. data/test/aggs_test.rb +102 -0
  16. data/test/autocomplete_test.rb +1 -3
  17. data/test/boost_test.rb +1 -3
  18. data/{ci → test/ci}/before_install.sh +4 -3
  19. data/test/facets_test.rb +4 -6
  20. data/{gemfiles → test/gemfiles}/activerecord31.gemfile +1 -1
  21. data/{gemfiles → test/gemfiles}/activerecord32.gemfile +1 -1
  22. data/{gemfiles → test/gemfiles}/activerecord40.gemfile +1 -1
  23. data/{gemfiles → test/gemfiles}/activerecord41.gemfile +1 -1
  24. data/{gemfiles → test/gemfiles}/mongoid2.gemfile +1 -1
  25. data/{gemfiles → test/gemfiles}/mongoid3.gemfile +1 -1
  26. data/{gemfiles → test/gemfiles}/mongoid4.gemfile +1 -1
  27. data/test/gemfiles/mongoid5.gemfile +7 -0
  28. data/{gemfiles → test/gemfiles}/nobrainer.gemfile +1 -1
  29. data/test/highlight_test.rb +2 -4
  30. data/test/index_test.rb +20 -9
  31. data/test/inheritance_test.rb +1 -3
  32. data/test/match_test.rb +8 -7
  33. data/test/model_test.rb +2 -4
  34. data/test/query_test.rb +1 -3
  35. data/test/records_test.rb +0 -2
  36. data/test/reindex_job_test.rb +1 -3
  37. data/test/reindex_v2_job_test.rb +1 -3
  38. data/test/routing_test.rb +4 -3
  39. data/test/should_index_test.rb +1 -3
  40. data/test/similar_test.rb +1 -3
  41. data/test/sql_test.rb +7 -9
  42. data/test/suggest_test.rb +1 -3
  43. data/test/synonyms_test.rb +1 -3
  44. data/test/test_helper.rb +23 -10
  45. metadata +24 -11
@@ -92,24 +92,37 @@ module Searchkick
92
92
  shared_options = {
93
93
  query: term,
94
94
  operator: operator,
95
- boost: factor
95
+ boost: 10 * factor
96
96
  }
97
97
 
98
- if field == "_all" || field.end_with?(".analyzed")
99
- misspellings = options.key?(:misspellings) ? options[:misspellings] : options[:mispellings] # why not?
100
- if misspellings != false
101
- edit_distance = (misspellings.is_a?(Hash) && (misspellings[:edit_distance] || misspellings[:distance])) || 1
102
- transpositions = (misspellings.is_a?(Hash) && misspellings[:transpositions] == true) ? {fuzzy_transpositions: true} : {}
103
- prefix_length = (misspellings.is_a?(Hash) && misspellings[:prefix_length]) || 0
104
- qs.concat [
105
- shared_options.merge(fuzziness: edit_distance, prefix_length: prefix_length, max_expansions: 3, analyzer: "searchkick_search").merge(transpositions),
106
- shared_options.merge(fuzziness: edit_distance, prefix_length: prefix_length, max_expansions: 3, analyzer: "searchkick_search2").merge(transpositions)
107
- ]
98
+ misspellings =
99
+ if options.key?(:misspellings)
100
+ options[:misspellings]
101
+ elsif options.key?(:mispellings)
102
+ options[:mispellings] # why not?
103
+ else
104
+ true
108
105
  end
106
+
107
+ if misspellings != false
108
+ edit_distance = (misspellings.is_a?(Hash) && (misspellings[:edit_distance] || misspellings[:distance])) || 1
109
+ transpositions =
110
+ if misspellings.is_a?(Hash) && misspellings.key?(:transpositions)
111
+ {fuzzy_transpositions: misspellings[:transpositions]}
112
+ elsif below14?
113
+ {}
114
+ else
115
+ {fuzzy_transpositions: true}
116
+ end
117
+ prefix_length = (misspellings.is_a?(Hash) && misspellings[:prefix_length]) || 0
118
+ max_expansions = (misspellings.is_a?(Hash) && misspellings[:max_expansions]) || 3
119
+ end
120
+
121
+ if field == "_all" || field.end_with?(".analyzed")
109
122
  shared_options[:cutoff_frequency] = 0.001 unless operator == "and" || misspellings == false
110
123
  qs.concat [
111
- shared_options.merge(boost: 10 * factor, analyzer: "searchkick_search"),
112
- shared_options.merge(boost: 10 * factor, analyzer: "searchkick_search2")
124
+ shared_options.merge(analyzer: "searchkick_search"),
125
+ shared_options.merge(analyzer: "searchkick_search2")
113
126
  ]
114
127
  elsif field.end_with?(".exact")
115
128
  f = field.split(".")[0..-2].join(".")
@@ -119,6 +132,10 @@ module Searchkick
119
132
  qs << shared_options.merge(analyzer: analyzer)
120
133
  end
121
134
 
135
+ if misspellings != false
136
+ qs.concat qs.map { |q| q.except(:cutoff_frequency).merge(fuzziness: edit_distance, prefix_length: prefix_length, max_expansions: max_expansions, boost: factor).merge(transpositions) }
137
+ end
138
+
122
139
  queries.concat(qs.map { |q| {match: {field => q}} })
123
140
  end
124
141
 
@@ -135,7 +152,7 @@ module Searchkick
135
152
  if below12?
136
153
  {script_score: {script: "doc['count'].value"}}
137
154
  else
138
- {field_value_factor: {field: "count"}}
155
+ {field_value_factor: {field: "#{conversions_field}.count"}}
139
156
  end
140
157
 
141
158
  payload = {
@@ -144,13 +161,13 @@ module Searchkick
144
161
  should: {
145
162
  nested: {
146
163
  path: conversions_field,
147
- score_mode: "total",
164
+ score_mode: "sum",
148
165
  query: {
149
166
  function_score: {
150
167
  boost_mode: "replace",
151
168
  query: {
152
169
  match: {
153
- query: term
170
+ "#{conversions_field}.query" => term
154
171
  }
155
172
  }
156
173
  }.merge(script_score)
@@ -170,11 +187,9 @@ module Searchkick
170
187
  if boost_by.is_a?(Array)
171
188
  boost_by = Hash[boost_by.map { |f| [f, {factor: 1}] }]
172
189
  elsif boost_by.is_a?(Hash)
173
- multiply_by, boost_by = boost_by.partition { |k,v| v[:boost_mode] == "multiply" }.map{ |i| Hash[i] }
174
- end
175
- if options[:boost]
176
- boost_by[options[:boost]] = {factor: 1}
190
+ multiply_by, boost_by = boost_by.partition { |_, v| v[:boost_mode] == "multiply" }.map { |i| Hash[i] }
177
191
  end
192
+ boost_by[options[:boost]] = {factor: 1} if options[:boost]
178
193
 
179
194
  custom_filters.concat boost_filters(boost_by, log: true)
180
195
  multiply_filters.concat boost_filters(multiply_by || {})
@@ -189,12 +204,10 @@ module Searchkick
189
204
  boost_where.each do |field, value|
190
205
  if value.is_a?(Array) && value.first.is_a?(Hash)
191
206
  value.each do |value_factor|
192
- value, factor = value_factor[:value], value_factor[:factor]
193
- custom_filters << custom_filter(field, value, factor)
207
+ custom_filters << custom_filter(field, value_factor[:value], value_factor[:factor])
194
208
  end
195
209
  elsif value.is_a?(Hash)
196
- value, factor = value[:value], value[:factor]
197
- custom_filters << custom_filter(field, value, factor)
210
+ custom_filters << custom_filter(field, value[:value], value[:factor])
198
211
  else
199
212
  factor = 1000
200
213
  custom_filters << custom_filter(field, value, factor)
@@ -207,7 +220,7 @@ module Searchkick
207
220
  if !boost_by_distance[:field] || !boost_by_distance[:origin]
208
221
  raise ArgumentError, "boost_by_distance requires :field and :origin"
209
222
  end
210
- function_params = boost_by_distance.select { |k, v| [:origin, :scale, :offset, :decay].include?(k) }
223
+ function_params = boost_by_distance.select { |k, _| [:origin, :scale, :offset, :decay].include?(k) }
211
224
  function_params[:origin] = function_params[:origin].reverse
212
225
  custom_filters << {
213
226
  boost_by_distance[:function] => {
@@ -253,7 +266,7 @@ module Searchkick
253
266
  # filters
254
267
  filters = where_filters(options[:where])
255
268
  if filters.any?
256
- if options[:facets]
269
+ if options[:facets] || options[:aggs]
257
270
  payload[:filter] = {
258
271
  and: filters
259
272
  }
@@ -273,9 +286,7 @@ module Searchkick
273
286
  # facets
274
287
  if options[:facets]
275
288
  facets = options[:facets] || {}
276
- if facets.is_a?(Array) # convert to more advanced syntax
277
- facets = Hash[facets.map { |f| [f, {}] }]
278
- end
289
+ facets = Hash[facets.map { |f| [f, {}] }] if facets.is_a?(Array) # convert to more advanced syntax
279
290
 
280
291
  payload[:facets] = {}
281
292
  facets.each do |field, facet_options|
@@ -311,7 +322,7 @@ module Searchkick
311
322
  # offset is not possible
312
323
  # http://elasticsearch-users.115913.n3.nabble.com/Is-pagination-possible-in-termsStatsFacet-td3422943.html
313
324
 
314
- facet_options.deep_merge!(where: options[:where].reject { |k| k == field }) if options[:smart_facets] == true
325
+ facet_options.deep_merge!(where: options.fetch(:where, {}).reject { |k| k == field }) if options[:smart_facets] == true
315
326
  facet_filters = where_filters(facet_options[:where])
316
327
  if facet_filters.any?
317
328
  payload[:facets][field][:facet_filter] = {
@@ -323,6 +334,57 @@ module Searchkick
323
334
  end
324
335
  end
325
336
 
337
+ # aggregations
338
+ if options[:aggs]
339
+ aggs = options[:aggs]
340
+ payload[:aggs] = {}
341
+
342
+ aggs = aggs.map { |f| [f, {}] }.to_h if aggs.is_a?(Array) # convert to more advanced syntax
343
+
344
+ aggs.each do |field, agg_options|
345
+ size = agg_options[:limit] ? agg_options[:limit] : 100_000
346
+
347
+ if agg_options[:ranges]
348
+ payload[:aggs][field] = {
349
+ range: {
350
+ field: agg_options[:field] || field,
351
+ ranges: agg_options[:ranges]
352
+ }
353
+ }
354
+ elsif agg_options[:date_ranges]
355
+ payload[:aggs][field] = {
356
+ date_range: {
357
+ field: agg_options[:field] || field,
358
+ ranges: agg_options[:date_ranges]
359
+ }
360
+ }
361
+ else
362
+ payload[:aggs][field] = {
363
+ terms: {
364
+ field: agg_options[:field] || field,
365
+ size: size
366
+ }
367
+ }
368
+ end
369
+
370
+ where = {}
371
+ where = (options[:where] || {}).reject { |k| k == field } unless options[:smart_aggs] == false
372
+ agg_filters = where_filters(where.merge(agg_options[:where] || {}))
373
+ if agg_filters.any?
374
+ payload[:aggs][field] = {
375
+ filter: {
376
+ bool: {
377
+ must: agg_filters
378
+ }
379
+ },
380
+ aggs: {
381
+ field => payload[:aggs][field]
382
+ }
383
+ }
384
+ end
385
+ end
386
+ end
387
+
326
388
  # suggestions
327
389
  if options[:suggest]
328
390
  suggest_fields = (searchkick_options[:suggest] || []).map(&:to_s)
@@ -356,6 +418,10 @@ module Searchkick
356
418
  payload[:highlight][:post_tags] = [tag.to_s.gsub(/\A</, "</")]
357
419
  end
358
420
 
421
+ if (fragment_size = options[:highlight][:fragment_size])
422
+ payload[:highlight][:fragment_size] = fragment_size
423
+ end
424
+
359
425
  highlight_fields = options[:highlight][:fields]
360
426
  if highlight_fields
361
427
  payload[:highlight][:fields] = {}
@@ -380,9 +446,7 @@ module Searchkick
380
446
  end
381
447
 
382
448
  # routing
383
- if options[:routing]
384
- @routing = options[:routing]
385
- end
449
+ @routing = options[:routing] if options[:routing]
386
450
  end
387
451
 
388
452
  @body = payload
@@ -478,9 +542,7 @@ module Searchkick
478
542
  value = {gte: value.first, (value.exclude_end? ? :lt : :lte) => value.last}
479
543
  end
480
544
 
481
- if value.is_a?(Array)
482
- value = {in: value}
483
- end
545
+ value = {in: value} if value.is_a?(Array)
484
546
 
485
547
  if value.is_a?(Hash)
486
548
  value.each do |op, op_value|
@@ -506,9 +568,11 @@ module Searchkick
506
568
  when :regexp # support for regexp queries without using a regexp ruby object
507
569
  filters << {regexp: {field => {value: op_value}}}
508
570
  when :not # not equal
509
- filters << {not: term_filters(field, op_value)}
571
+ filters << {not: {filter: term_filters(field, op_value)}}
510
572
  when :all
511
- filters << {terms: {field => op_value, execution: "and"}}
573
+ op_value.each do |value|
574
+ filters << term_filters(field, value)
575
+ end
512
576
  when :in
513
577
  filters << term_filters(field, op_value)
514
578
  else
@@ -560,7 +624,7 @@ module Searchkick
560
624
  def custom_filter(field, value, factor)
561
625
  {
562
626
  filter: {
563
- and: where_filters({field => value})
627
+ and: where_filters(field => value)
564
628
  },
565
629
  boost_factor: factor
566
630
  }
@@ -596,6 +660,10 @@ module Searchkick
596
660
  below_version?("1.4.0")
597
661
  end
598
662
 
663
+ def below20?
664
+ below_version?("2.0.0")
665
+ end
666
+
599
667
  def below_version?(version)
600
668
  Gem::Version.new(Searchkick.server_version) < Gem::Version.new(version)
601
669
  end
@@ -1,6 +1,5 @@
1
1
  module Searchkick
2
2
  class ReindexJob
3
-
4
3
  def initialize(klass, id)
5
4
  @klass = klass
6
5
  @id = id
@@ -23,6 +22,5 @@ module Searchkick
23
22
  index.store record
24
23
  end
25
24
  end
26
-
27
25
  end
28
26
  end
@@ -19,6 +19,5 @@ module Searchkick
19
19
  index.store record
20
20
  end
21
21
  end
22
-
23
22
  end
24
23
  end
@@ -26,7 +26,7 @@ module Searchkick
26
26
  # results can have different types
27
27
  results = {}
28
28
 
29
- hits.group_by { |hit, i| hit["_type"] }.each do |type, grouped_hits|
29
+ hits.group_by { |hit, _| hit["_type"] }.each do |type, grouped_hits|
30
30
  results[type] = results_query(type.camelize.constantize, grouped_hits).to_a.index_by { |r| r.id.to_s }
31
31
  end
32
32
 
@@ -75,6 +75,23 @@ module Searchkick
75
75
  response["facets"]
76
76
  end
77
77
 
78
+ def aggs
79
+ @aggs ||= begin
80
+ response["aggregations"].dup.each do |field, filtered_agg|
81
+ buckets = filtered_agg[field]
82
+ # move the buckets one level above into the field hash
83
+ if buckets
84
+ filtered_agg.delete(field)
85
+ filtered_agg.merge!(buckets)
86
+ end
87
+ end
88
+ end
89
+ end
90
+
91
+ def took
92
+ response["took"]
93
+ end
94
+
78
95
  def model_name
79
96
  klass.model_name
80
97
  end
@@ -1,7 +1,6 @@
1
1
  require "rake"
2
2
 
3
3
  namespace :searchkick do
4
-
5
4
  desc "reindex model"
6
5
  task reindex: :environment do
7
6
  if ENV["CLASS"]
@@ -31,5 +30,4 @@ namespace :searchkick do
31
30
  end
32
31
 
33
32
  end
34
-
35
33
  end
@@ -1,3 +1,3 @@
1
1
  module Searchkick
2
- VERSION = "0.9.1"
2
+ VERSION = "1.0.0"
3
3
  end
@@ -0,0 +1,102 @@
1
+ require_relative "test_helper"
2
+
3
+ class AggsTest < Minitest::Test
4
+ def setup
5
+ super
6
+ store [
7
+ {name: "Product Show", latitude: 37.7833, longitude: 12.4167, store_id: 1, in_stock: true, color: "blue", price: 21, created_at: 2.days.ago},
8
+ {name: "Product Hide", latitude: 29.4167, longitude: -98.5000, store_id: 2, in_stock: false, color: "green", price: 25, created_at: 2.days.from_now},
9
+ {name: "Product B", latitude: 43.9333, longitude: -122.4667, store_id: 2, in_stock: false, color: "red", price: 5},
10
+ {name: "Foo", latitude: 43.9333, longitude: 12.4667, store_id: 3, in_stock: false, color: "yellow", price: 15}
11
+ ]
12
+ end
13
+
14
+ def test_basic
15
+ assert_equal ({1 => 1, 2 => 2}), store_agg(aggs: [:store_id])
16
+ end
17
+
18
+ def test_where
19
+ assert_equal ({1 => 1}), store_agg(aggs: {store_id: {where: {in_stock: true}}})
20
+ end
21
+
22
+ def test_field
23
+ assert_equal ({1 => 1, 2 => 2}), store_agg(aggs: {store_id: {}})
24
+ assert_equal ({1 => 1, 2 => 2}), store_agg(aggs: {store_id: {field: "store_id"}})
25
+ assert_equal ({1 => 1, 2 => 2}), store_agg({aggs: {store_id_new: {field: "store_id"}}}, "store_id_new")
26
+ end
27
+
28
+ def test_limit
29
+ agg = Product.search("Product", aggs: {store_id: {limit: 1}}).aggs["store_id"]
30
+ assert_equal 1, agg["buckets"].size
31
+ # assert_equal 3, agg["doc_count"]
32
+ assert_equal(1, agg["sum_other_doc_count"]) if Gem::Version.new(Searchkick.server_version) >= Gem::Version.new("1.4.0")
33
+ end
34
+
35
+ def test_ranges
36
+ price_ranges = [{to: 10}, {from: 10, to: 20}, {from: 20}]
37
+ agg = Product.search("Product", aggs: {price: {ranges: price_ranges}}).aggs["price"]
38
+
39
+ assert_equal 3, agg["buckets"].size
40
+ assert_equal 10.0, agg["buckets"][0]["to"]
41
+ assert_equal 20.0, agg["buckets"][2]["from"]
42
+ assert_equal 1, agg["buckets"][0]["doc_count"]
43
+ assert_equal 0, agg["buckets"][1]["doc_count"]
44
+ assert_equal 2, agg["buckets"][2]["doc_count"]
45
+ end
46
+
47
+ def test_date_ranges
48
+ ranges = [{to: 1.day.ago}, {from: 1.day.ago, to: 1.day.from_now}, {from: 1.day.from_now}]
49
+ agg = Product.search("Product", aggs: {created_at: {date_ranges: ranges}}).aggs["created_at"]
50
+
51
+ assert_equal 1, agg["buckets"][0]["doc_count"]
52
+ assert_equal 1, agg["buckets"][1]["doc_count"]
53
+ assert_equal 1, agg["buckets"][2]["doc_count"]
54
+ end
55
+
56
+ def test_query_where
57
+ assert_equal ({1 => 1}), store_agg(where: {in_stock: true}, aggs: [:store_id])
58
+ end
59
+
60
+ def test_two_wheres
61
+ assert_equal ({2 => 1}), store_agg(where: {color: "red"}, aggs: {store_id: {where: {in_stock: false}}})
62
+ end
63
+
64
+ def test_where_override
65
+ assert_equal ({}), store_agg(where: {color: "red"}, aggs: {store_id: {where: {in_stock: false, color: "blue"}}})
66
+ assert_equal ({2 => 1}), store_agg(where: {color: "blue"}, aggs: {store_id: {where: {in_stock: false, color: "red"}}})
67
+ end
68
+
69
+ def test_skip
70
+ assert_equal ({1 => 1, 2 => 2}), store_agg(where: {store_id: 2}, aggs: [:store_id])
71
+ end
72
+
73
+ def test_skip_complex
74
+ assert_equal ({1 => 1, 2 => 1}), store_agg(where: {store_id: 2, price: {gt: 5}}, aggs: [:store_id])
75
+ end
76
+
77
+ def test_multiple
78
+ assert_equal ({"store_id" => {1 => 1, 2 => 2}, "color" => {"blue" => 1, "green" => 1, "red" => 1}}), store_multiple_aggs(aggs: [:store_id, :color])
79
+ end
80
+
81
+ def test_smart_aggs_false
82
+ assert_equal ({2 => 2}), store_agg(where: {color: "red"}, aggs: {store_id: {where: {in_stock: false}}}, smart_aggs: false)
83
+ assert_equal ({2 => 2}), store_agg(where: {color: "blue"}, aggs: {store_id: {where: {in_stock: false}}}, smart_aggs: false)
84
+ end
85
+
86
+ protected
87
+
88
+ def buckets_as_hash(agg)
89
+ agg["buckets"].map { |v| [v["key"], v["doc_count"]] }.to_h
90
+ end
91
+
92
+ def store_agg(options, agg_key = "store_id")
93
+ buckets = Product.search("Product", options).aggs[agg_key]
94
+ buckets_as_hash(buckets)
95
+ end
96
+
97
+ def store_multiple_aggs(options)
98
+ Product.search("Product", options).aggs.map do |field, filtered_agg|
99
+ [field, buckets_as_hash(filtered_agg)]
100
+ end.to_h
101
+ end
102
+ end
@@ -1,7 +1,6 @@
1
1
  require_relative "test_helper"
2
2
 
3
- class TestAutocomplete < Minitest::Test
4
-
3
+ class AutocompleteTest < Minitest::Test
5
4
  def test_autocomplete
6
5
  store_names ["Hummus"]
7
6
  assert_search "hum", ["Hummus"], autocomplete: true
@@ -63,5 +62,4 @@ class TestAutocomplete < Minitest::Test
63
62
  store_names ["hi@example.org"]
64
63
  assert_search "hi@example.org", ["hi@example.org"], fields: [{name: :exact}]
65
64
  end
66
-
67
65
  end