searchkick_bharthur 0.0.1

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.
Files changed (61) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +22 -0
  3. data/.travis.yml +44 -0
  4. data/CHANGELOG.md +360 -0
  5. data/Gemfile +8 -0
  6. data/LICENSE.txt +22 -0
  7. data/README.md +1443 -0
  8. data/Rakefile +8 -0
  9. data/lib/searchkick/index.rb +662 -0
  10. data/lib/searchkick/logging.rb +185 -0
  11. data/lib/searchkick/middleware.rb +12 -0
  12. data/lib/searchkick/model.rb +105 -0
  13. data/lib/searchkick/query.rb +845 -0
  14. data/lib/searchkick/reindex_job.rb +26 -0
  15. data/lib/searchkick/reindex_v2_job.rb +23 -0
  16. data/lib/searchkick/results.rb +211 -0
  17. data/lib/searchkick/tasks.rb +33 -0
  18. data/lib/searchkick/version.rb +3 -0
  19. data/lib/searchkick.rb +159 -0
  20. data/searchkick.gemspec +28 -0
  21. data/test/aggs_test.rb +115 -0
  22. data/test/autocomplete_test.rb +65 -0
  23. data/test/boost_test.rb +144 -0
  24. data/test/callbacks_test.rb +27 -0
  25. data/test/ci/before_install.sh +21 -0
  26. data/test/dangerous_reindex_test.rb +27 -0
  27. data/test/facets_test.rb +90 -0
  28. data/test/gemfiles/activerecord31.gemfile +7 -0
  29. data/test/gemfiles/activerecord32.gemfile +7 -0
  30. data/test/gemfiles/activerecord40.gemfile +8 -0
  31. data/test/gemfiles/activerecord41.gemfile +8 -0
  32. data/test/gemfiles/activerecord50.gemfile +7 -0
  33. data/test/gemfiles/apartment.gemfile +8 -0
  34. data/test/gemfiles/mongoid2.gemfile +7 -0
  35. data/test/gemfiles/mongoid3.gemfile +6 -0
  36. data/test/gemfiles/mongoid4.gemfile +7 -0
  37. data/test/gemfiles/mongoid5.gemfile +7 -0
  38. data/test/gemfiles/nobrainer.gemfile +6 -0
  39. data/test/highlight_test.rb +63 -0
  40. data/test/index_test.rb +120 -0
  41. data/test/inheritance_test.rb +78 -0
  42. data/test/match_test.rb +227 -0
  43. data/test/misspellings_test.rb +46 -0
  44. data/test/model_test.rb +42 -0
  45. data/test/multi_search_test.rb +22 -0
  46. data/test/multi_tenancy_test.rb +22 -0
  47. data/test/order_test.rb +44 -0
  48. data/test/pagination_test.rb +53 -0
  49. data/test/query_test.rb +13 -0
  50. data/test/records_test.rb +8 -0
  51. data/test/reindex_job_test.rb +31 -0
  52. data/test/reindex_v2_job_test.rb +32 -0
  53. data/test/routing_test.rb +13 -0
  54. data/test/should_index_test.rb +32 -0
  55. data/test/similar_test.rb +28 -0
  56. data/test/sql_test.rb +196 -0
  57. data/test/suggest_test.rb +80 -0
  58. data/test/synonyms_test.rb +54 -0
  59. data/test/test_helper.rb +361 -0
  60. data/test/where_test.rb +171 -0
  61. metadata +231 -0
@@ -0,0 +1,26 @@
1
+ module Searchkick
2
+ class ReindexJob
3
+ def initialize(klass, id)
4
+ @klass = klass
5
+ @id = id
6
+ end
7
+
8
+ def perform
9
+ model = @klass.constantize
10
+ record = model.find(@id) rescue nil # TODO fix lazy coding
11
+ index = model.searchkick_index
12
+ if !record || !record.should_index?
13
+ # hacky
14
+ record ||= model.new
15
+ record.id = @id
16
+ begin
17
+ index.remove record
18
+ rescue Elasticsearch::Transport::Transport::Errors::NotFound
19
+ # do nothing
20
+ end
21
+ else
22
+ index.store record
23
+ end
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,23 @@
1
+ module Searchkick
2
+ class ReindexV2Job < ActiveJob::Base
3
+ queue_as :searchkick
4
+
5
+ def perform(klass, id)
6
+ model = klass.constantize
7
+ record = model.find(id) rescue nil # TODO fix lazy coding
8
+ index = model.searchkick_index
9
+ if !record || !record.should_index?
10
+ # hacky
11
+ record ||= model.new
12
+ record.id = id
13
+ begin
14
+ index.remove record
15
+ rescue Elasticsearch::Transport::Transport::Errors::NotFound
16
+ # do nothing
17
+ end
18
+ else
19
+ index.store record
20
+ end
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,211 @@
1
+ require "forwardable"
2
+
3
+ module Searchkick
4
+ class Results
5
+ include Enumerable
6
+ extend Forwardable
7
+
8
+ attr_reader :klass, :response, :options
9
+
10
+ def_delegators :results, :each, :any?, :empty?, :size, :length, :slice, :[], :to_ary
11
+
12
+ def initialize(klass, response, options = {})
13
+ @klass = klass
14
+ @response = response
15
+ @options = options
16
+ end
17
+
18
+ # experimental: may not make next release
19
+ def records
20
+ @records ||= results_query(klass, hits)
21
+ end
22
+
23
+ def results
24
+ @results ||= begin
25
+ if options[:load]
26
+ # results can have different types
27
+ results = {}
28
+
29
+ hits.group_by { |hit, _| hit["_type"] }.each do |type, grouped_hits|
30
+ results[type] = results_query(type.camelize.constantize, grouped_hits).to_a.index_by { |r| r.id.to_s }
31
+ end
32
+
33
+ # sort
34
+ hits.map do |hit|
35
+ results[hit["_type"]][hit["_id"].to_s]
36
+ end.compact
37
+ else
38
+ hits.map do |hit|
39
+ result =
40
+ if hit["_source"]
41
+ hit.except("_source").merge(hit["_source"])
42
+ elsif hit["fields"]
43
+ hit.except("fields").merge(hit["fields"])
44
+ else
45
+ hit
46
+ end
47
+
48
+ if hit["highlight"]
49
+ highlight = Hash[hit["highlight"].map { |k, v| [base_field(k), v.first] }]
50
+ options[:highlighted_fields].map { |k| base_field(k) }.each do |k|
51
+ result["highlighted_#{k}"] ||= (highlight[k] || result[k])
52
+ end
53
+ end
54
+
55
+ result["id"] ||= result["_id"] # needed for legacy reasons
56
+ Hashie::Mash.new(result)
57
+ end
58
+ end
59
+ end
60
+ end
61
+
62
+ def suggestions
63
+ if response["suggest"]
64
+ response["suggest"].values.flat_map { |v| v.first["options"] }.sort_by { |o| -o["score"] }.map { |o| o["text"] }.uniq
65
+ else
66
+ raise "Pass `suggest: true` to the search method for suggestions"
67
+ end
68
+ end
69
+
70
+ def each_with_hit(&block)
71
+ results.zip(hits).each(&block)
72
+ end
73
+
74
+ def with_details
75
+ each_with_hit.map do |model, hit|
76
+ details = {}
77
+ if hit["highlight"]
78
+ details[:highlight] = Hash[hit["highlight"].map { |k, v| [(options[:json] ? k : k.sub(/\.#{@options[:match_suffix]}\z/, "")).to_sym, v.first] }]
79
+ end
80
+ [model, details]
81
+ end
82
+ end
83
+
84
+ def facets
85
+ response["facets"]
86
+ end
87
+
88
+ def aggregations
89
+ response["aggregations"]
90
+ end
91
+
92
+ def aggs
93
+ @aggs ||= begin
94
+ if aggregations
95
+ aggregations.dup.each do |field, filtered_agg|
96
+ buckets = filtered_agg[field]
97
+ # move the buckets one level above into the field hash
98
+ if buckets
99
+ filtered_agg.delete(field)
100
+ filtered_agg.merge!(buckets)
101
+ end
102
+ end
103
+ end
104
+ end
105
+ end
106
+
107
+ def took
108
+ response["took"]
109
+ end
110
+
111
+ def error
112
+ response["error"]
113
+ end
114
+
115
+ def model_name
116
+ klass.model_name
117
+ end
118
+
119
+ def entry_name
120
+ model_name.human.downcase
121
+ end
122
+
123
+ def total_count
124
+ response["hits"]["total"]
125
+ end
126
+ alias_method :total_entries, :total_count
127
+
128
+ def current_page
129
+ options[:page]
130
+ end
131
+
132
+ def per_page
133
+ options[:per_page]
134
+ end
135
+ alias_method :limit_value, :per_page
136
+
137
+ def padding
138
+ options[:padding]
139
+ end
140
+
141
+ def total_pages
142
+ (total_count / per_page.to_f).ceil
143
+ end
144
+ alias_method :num_pages, :total_pages
145
+
146
+ def offset_value
147
+ (current_page - 1) * per_page + padding
148
+ end
149
+ alias_method :offset, :offset_value
150
+
151
+ def previous_page
152
+ current_page > 1 ? (current_page - 1) : nil
153
+ end
154
+ alias_method :prev_page, :previous_page
155
+
156
+ def next_page
157
+ current_page < total_pages ? (current_page + 1) : nil
158
+ end
159
+
160
+ def first_page?
161
+ previous_page.nil?
162
+ end
163
+
164
+ def last_page?
165
+ next_page.nil?
166
+ end
167
+
168
+ def out_of_range?
169
+ current_page > total_pages
170
+ end
171
+
172
+ def hits
173
+ @response["hits"]["hits"]
174
+ end
175
+
176
+ private
177
+
178
+ def results_query(records, hits)
179
+ ids = hits.map { |hit| hit["_id"] }
180
+
181
+ if options[:includes]
182
+ records =
183
+ if defined?(NoBrainer::Document) && records < NoBrainer::Document
184
+ records.preload(options[:includes])
185
+ else
186
+ records.includes(options[:includes])
187
+ end
188
+ end
189
+
190
+ if records.respond_to?(:primary_key) && records.primary_key
191
+ # ActiveRecord
192
+ records.where(records.primary_key => ids)
193
+ elsif records.respond_to?(:all) && records.all.respond_to?(:for_ids)
194
+ # Mongoid 2
195
+ records.all.for_ids(ids)
196
+ elsif records.respond_to?(:queryable)
197
+ # Mongoid 3+
198
+ records.queryable.for_ids(ids)
199
+ elsif records.respond_to?(:unscoped) && records.all.respond_to?(:preload)
200
+ # Nobrainer
201
+ records.unscoped.where(:id.in => ids)
202
+ else
203
+ raise "Not sure how to load records"
204
+ end
205
+ end
206
+
207
+ def base_field(k)
208
+ k.sub(/\.(analyzed|word_start|word_middle|word_end|text_start|text_middle|text_end|exact)\z/, "")
209
+ end
210
+ end
211
+ end
@@ -0,0 +1,33 @@
1
+ require "rake"
2
+
3
+ namespace :searchkick do
4
+ desc "reindex model"
5
+ task reindex: :environment do
6
+ if ENV["CLASS"]
7
+ klass = ENV["CLASS"].constantize rescue nil
8
+ if klass
9
+ klass.reindex
10
+ else
11
+ abort "Could not find class: #{ENV['CLASS']}"
12
+ end
13
+ else
14
+ abort "USAGE: rake searchkick:reindex CLASS=Product"
15
+ end
16
+ end
17
+
18
+ if defined?(Rails)
19
+
20
+ namespace :reindex do
21
+ desc "reindex all models"
22
+ task all: :environment do
23
+ Rails.application.eager_load!
24
+ Searchkick.models.each do |model|
25
+ puts "Reindexing #{model.name}..."
26
+ model.reindex
27
+ end
28
+ puts "Reindex complete"
29
+ end
30
+ end
31
+
32
+ end
33
+ end
@@ -0,0 +1,3 @@
1
+ module Searchkick
2
+ VERSION = "0.0.1"
3
+ end
data/lib/searchkick.rb ADDED
@@ -0,0 +1,159 @@
1
+ require "active_model"
2
+ require "elasticsearch"
3
+ require "hashie"
4
+ require "searchkick/version"
5
+ require "searchkick/index"
6
+ require "searchkick/results"
7
+ require "searchkick/query"
8
+ require "searchkick/reindex_job"
9
+ require "searchkick/model"
10
+ require "searchkick/tasks"
11
+ require "searchkick/middleware"
12
+ require "searchkick/logging" if defined?(ActiveSupport::Notifications)
13
+
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
+ module Searchkick
23
+ class Error < StandardError; end
24
+ class MissingIndexError < Error; end
25
+ class UnsupportedVersionError < Error; end
26
+ class InvalidQueryError < Elasticsearch::Transport::Transport::Errors::BadRequest; end
27
+ class DangerousOperation < Error; end
28
+ class ImportError < Error; end
29
+
30
+ class << self
31
+ attr_accessor :search_method_name, :wordnet_path, :timeout, :models
32
+ attr_writer :client, :env, :search_timeout
33
+ end
34
+ self.search_method_name = :search
35
+ self.wordnet_path = "/var/lib/wn_s.pl"
36
+ self.timeout = 10
37
+ self.models = []
38
+
39
+ def self.client
40
+ @client ||=
41
+ Elasticsearch::Client.new(
42
+ url: ENV["ELASTICSEARCH_URL"],
43
+ transport_options: {request: {timeout: timeout}}
44
+ ) do |f|
45
+ f.use Searchkick::Middleware
46
+ end
47
+ end
48
+
49
+ def self.env
50
+ @env ||= ENV["RAILS_ENV"] || ENV["RACK_ENV"] || "development"
51
+ end
52
+
53
+ def self.search_timeout
54
+ @search_timeout || timeout
55
+ end
56
+
57
+ def self.server_version
58
+ @server_version ||= client.info["version"]["number"]
59
+ end
60
+
61
+ def self.server_below?(version)
62
+ Gem::Version.new(server_version.sub("-", ".")) < Gem::Version.new(version.sub("-", "."))
63
+ end
64
+
65
+ def self.enable_callbacks
66
+ self.callbacks_value = nil
67
+ end
68
+
69
+ def self.disable_callbacks
70
+ self.callbacks_value = false
71
+ end
72
+
73
+ def self.callbacks?
74
+ Thread.current[:searchkick_callbacks_enabled].nil? || Thread.current[:searchkick_callbacks_enabled]
75
+ end
76
+
77
+ def self.callbacks(value)
78
+ if block_given?
79
+ previous_value = callbacks_value
80
+ begin
81
+ self.callbacks_value = value
82
+ yield
83
+ perform_bulk if callbacks_value == :bulk
84
+ ensure
85
+ self.callbacks_value = previous_value
86
+ end
87
+ else
88
+ self.callbacks_value = value
89
+ end
90
+ end
91
+
92
+ # private
93
+ def self.queue_items(items)
94
+ queued_items.concat(items)
95
+ perform_bulk unless callbacks_value == :bulk
96
+ end
97
+
98
+ # private
99
+ def self.perform_bulk
100
+ items = queued_items
101
+ clear_queued_items
102
+ perform_items(items)
103
+ end
104
+
105
+ # private
106
+ def self.perform_items(items)
107
+ if items.any?
108
+ response = client.bulk(body: items)
109
+ if response["errors"]
110
+ first_item = response["items"].first
111
+ raise Searchkick::ImportError, (first_item["index"] || first_item["delete"])["error"]
112
+ end
113
+ end
114
+ end
115
+
116
+ # private
117
+ def self.queued_items
118
+ Thread.current[:searchkick_queued_items] ||= []
119
+ end
120
+
121
+ # private
122
+ def self.clear_queued_items
123
+ Thread.current[:searchkick_queued_items] = []
124
+ end
125
+
126
+ # private
127
+ def self.callbacks_value
128
+ Thread.current[:searchkick_callbacks_enabled]
129
+ end
130
+
131
+ # private
132
+ def self.callbacks_value=(value)
133
+ Thread.current[:searchkick_callbacks_enabled] = value
134
+ end
135
+
136
+ def self.search(term = nil, options = {}, &block)
137
+ query = Searchkick::Query.new(nil, term, options)
138
+ block.call(query.body) if block
139
+ if options[:execute] == false
140
+ query
141
+ else
142
+ query.execute
143
+ end
144
+ end
145
+
146
+ def self.multi_search(queries)
147
+ if queries.any?
148
+ responses = client.msearch(body: queries.flat_map { |q| [q.params.except(:body), q.body] })["responses"]
149
+ queries.each_with_index do |query, i|
150
+ query.handle_response(responses[i])
151
+ end
152
+ end
153
+ nil
154
+ end
155
+ end
156
+
157
+ # TODO find better ActiveModel hook
158
+ ActiveModel::Callbacks.send(:include, Searchkick::Model)
159
+ ActiveRecord::Base.send(:extend, Searchkick::Model) if defined?(ActiveRecord)
@@ -0,0 +1,28 @@
1
+ # coding: utf-8
2
+ lib = File.expand_path("../lib", __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require "searchkick/version"
5
+
6
+ Gem::Specification.new do |spec|
7
+ spec.name = "searchkick_bharthur"
8
+ spec.version = Searchkick::VERSION
9
+ spec.authors = ["Andrew Kane", "Shiv Bharthur (Customization)"]
10
+ spec.email = ["andrew@chartkick.com", "shiv.bharthur@gmail.com"]
11
+ spec.description = "Intelligent search made easy"
12
+ spec.summary = "Searchkick learns what your users are looking for. As more people search, it gets smarter and the results get better. It’s friendly for developers - and magical for your users."
13
+ spec.homepage = "https://github.com/ankane/searchkick"
14
+ spec.license = "MIT"
15
+
16
+ spec.files = `git ls-files`.split($INPUT_RECORD_SEPARATOR)
17
+ spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
18
+ spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
19
+ spec.require_paths = ["lib"]
20
+
21
+ spec.add_dependency "activemodel"
22
+ spec.add_dependency "elasticsearch", ">= 1"
23
+ spec.add_dependency "hashie"
24
+
25
+ spec.add_development_dependency "bundler", "~> 1.6"
26
+ spec.add_development_dependency "rake"
27
+ spec.add_development_dependency "minitest"
28
+ end
data/test/aggs_test.rb ADDED
@@ -0,0 +1,115 @@
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_order
23
+ agg = Product.search("Product", aggs: {color: {order: {"_term" => "desc"}}}).aggs["color"]
24
+ assert_equal %w(red green blue), agg["buckets"].map { |b| b["key"] }
25
+ end
26
+
27
+ def test_field
28
+ assert_equal ({1 => 1, 2 => 2}), store_agg(aggs: {store_id: {}})
29
+ assert_equal ({1 => 1, 2 => 2}), store_agg(aggs: {store_id: {field: "store_id"}})
30
+ assert_equal ({1 => 1, 2 => 2}), store_agg({aggs: {store_id_new: {field: "store_id"}}}, "store_id_new")
31
+ end
32
+
33
+ def test_min_doc_count
34
+ assert_equal ({2 => 2}), store_agg(aggs: {store_id: {min_doc_count: 2}})
35
+ end
36
+
37
+ def test_no_aggs
38
+ assert_nil Product.search("*").aggs
39
+ end
40
+
41
+ def test_limit
42
+ agg = Product.search("Product", aggs: {store_id: {limit: 1}}).aggs["store_id"]
43
+ assert_equal 1, agg["buckets"].size
44
+ # assert_equal 3, agg["doc_count"]
45
+ assert_equal(1, agg["sum_other_doc_count"]) unless Searchkick.server_below?("1.4.0")
46
+ end
47
+
48
+ def test_ranges
49
+ price_ranges = [{to: 10}, {from: 10, to: 20}, {from: 20}]
50
+ agg = Product.search("Product", aggs: {price: {ranges: price_ranges}}).aggs["price"]
51
+
52
+ assert_equal 3, agg["buckets"].size
53
+ assert_equal 10.0, agg["buckets"][0]["to"]
54
+ assert_equal 20.0, agg["buckets"][2]["from"]
55
+ assert_equal 1, agg["buckets"][0]["doc_count"]
56
+ assert_equal 0, agg["buckets"][1]["doc_count"]
57
+ assert_equal 2, agg["buckets"][2]["doc_count"]
58
+ end
59
+
60
+ def test_date_ranges
61
+ ranges = [{to: 1.day.ago}, {from: 1.day.ago, to: 1.day.from_now}, {from: 1.day.from_now}]
62
+ agg = Product.search("Product", aggs: {created_at: {date_ranges: ranges}}).aggs["created_at"]
63
+
64
+ assert_equal 1, agg["buckets"][0]["doc_count"]
65
+ assert_equal 1, agg["buckets"][1]["doc_count"]
66
+ assert_equal 1, agg["buckets"][2]["doc_count"]
67
+ end
68
+
69
+ def test_query_where
70
+ assert_equal ({1 => 1}), store_agg(where: {in_stock: true}, aggs: [:store_id])
71
+ end
72
+
73
+ def test_two_wheres
74
+ assert_equal ({2 => 1}), store_agg(where: {color: "red"}, aggs: {store_id: {where: {in_stock: false}}})
75
+ end
76
+
77
+ def test_where_override
78
+ assert_equal ({}), store_agg(where: {color: "red"}, aggs: {store_id: {where: {in_stock: false, color: "blue"}}})
79
+ assert_equal ({2 => 1}), store_agg(where: {color: "blue"}, aggs: {store_id: {where: {in_stock: false, color: "red"}}})
80
+ end
81
+
82
+ def test_skip
83
+ assert_equal ({1 => 1, 2 => 2}), store_agg(where: {store_id: 2}, aggs: [:store_id])
84
+ end
85
+
86
+ def test_skip_complex
87
+ assert_equal ({1 => 1, 2 => 1}), store_agg(where: {store_id: 2, price: {gt: 5}}, aggs: [:store_id])
88
+ end
89
+
90
+ def test_multiple
91
+ assert_equal ({"store_id" => {1 => 1, 2 => 2}, "color" => {"blue" => 1, "green" => 1, "red" => 1}}), store_multiple_aggs(aggs: [:store_id, :color])
92
+ end
93
+
94
+ def test_smart_aggs_false
95
+ assert_equal ({2 => 2}), store_agg(where: {color: "red"}, aggs: {store_id: {where: {in_stock: false}}}, smart_aggs: false)
96
+ assert_equal ({2 => 2}), store_agg(where: {color: "blue"}, aggs: {store_id: {where: {in_stock: false}}}, smart_aggs: false)
97
+ end
98
+
99
+ protected
100
+
101
+ def buckets_as_hash(agg)
102
+ Hash[agg["buckets"].map { |v| [v["key"], v["doc_count"]] }]
103
+ end
104
+
105
+ def store_agg(options, agg_key = "store_id")
106
+ buckets = Product.search("Product", options).aggs[agg_key]
107
+ buckets_as_hash(buckets)
108
+ end
109
+
110
+ def store_multiple_aggs(options)
111
+ Hash[Product.search("Product", options).aggs.map do |field, filtered_agg|
112
+ [field, buckets_as_hash(filtered_agg)]
113
+ end]
114
+ end
115
+ end
@@ -0,0 +1,65 @@
1
+ require_relative "test_helper"
2
+
3
+ class AutocompleteTest < Minitest::Test
4
+ def test_autocomplete
5
+ store_names ["Hummus"]
6
+ assert_search "hum", ["Hummus"], autocomplete: true
7
+ end
8
+
9
+ def test_autocomplete_two_words
10
+ store_names ["Organic Hummus"]
11
+ assert_search "hum", [], autocomplete: true
12
+ end
13
+
14
+ def test_autocomplete_fields
15
+ store_names ["Hummus"]
16
+ assert_search "hum", ["Hummus"], autocomplete: true, fields: [:name]
17
+ end
18
+
19
+ def test_text_start
20
+ store_names ["Where in the World is Carmen San Diego"]
21
+ assert_search "where in the world is", ["Where in the World is Carmen San Diego"], fields: [{name: :text_start}]
22
+ assert_search "in the world", [], fields: [{name: :text_start}]
23
+ end
24
+
25
+ def test_text_middle
26
+ store_names ["Where in the World is Carmen San Diego"]
27
+ assert_search "where in the world is", ["Where in the World is Carmen San Diego"], fields: [{name: :text_middle}]
28
+ assert_search "n the wor", ["Where in the World is Carmen San Diego"], fields: [{name: :text_middle}]
29
+ assert_search "men san diego", ["Where in the World is Carmen San Diego"], fields: [{name: :text_middle}]
30
+ assert_search "world carmen", [], fields: [{name: :text_middle}]
31
+ end
32
+
33
+ def test_text_end
34
+ store_names ["Where in the World is Carmen San Diego"]
35
+ assert_search "men san diego", ["Where in the World is Carmen San Diego"], fields: [{name: :text_end}]
36
+ assert_search "carmen san", [], fields: [{name: :text_end}]
37
+ end
38
+
39
+ def test_word_start
40
+ store_names ["Where in the World is Carmen San Diego"]
41
+ assert_search "car san wor", ["Where in the World is Carmen San Diego"], fields: [{name: :word_start}]
42
+ end
43
+
44
+ def test_word_middle
45
+ store_names ["Where in the World is Carmen San Diego"]
46
+ assert_search "orl", ["Where in the World is Carmen San Diego"], fields: [{name: :word_middle}]
47
+ end
48
+
49
+ def test_word_end
50
+ store_names ["Where in the World is Carmen San Diego"]
51
+ assert_search "rld men ego", ["Where in the World is Carmen San Diego"], fields: [{name: :word_end}]
52
+ end
53
+
54
+ def test_word_start_multiple_words
55
+ store_names ["Dark Grey", "Dark Blue"]
56
+ assert_search "dark grey", ["Dark Grey"], fields: [{name: :word_start}]
57
+ end
58
+
59
+ # TODO find a better place
60
+
61
+ def test_exact
62
+ store_names ["hi@example.org"]
63
+ assert_search "hi@example.org", ["hi@example.org"], fields: [{name: :exact}]
64
+ end
65
+ end