searchkick_bharthur 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
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