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.
- checksums.yaml +7 -0
- data/.gitignore +22 -0
- data/.travis.yml +44 -0
- data/CHANGELOG.md +360 -0
- data/Gemfile +8 -0
- data/LICENSE.txt +22 -0
- data/README.md +1443 -0
- data/Rakefile +8 -0
- data/lib/searchkick/index.rb +662 -0
- data/lib/searchkick/logging.rb +185 -0
- data/lib/searchkick/middleware.rb +12 -0
- data/lib/searchkick/model.rb +105 -0
- data/lib/searchkick/query.rb +845 -0
- data/lib/searchkick/reindex_job.rb +26 -0
- data/lib/searchkick/reindex_v2_job.rb +23 -0
- data/lib/searchkick/results.rb +211 -0
- data/lib/searchkick/tasks.rb +33 -0
- data/lib/searchkick/version.rb +3 -0
- data/lib/searchkick.rb +159 -0
- data/searchkick.gemspec +28 -0
- data/test/aggs_test.rb +115 -0
- data/test/autocomplete_test.rb +65 -0
- data/test/boost_test.rb +144 -0
- data/test/callbacks_test.rb +27 -0
- data/test/ci/before_install.sh +21 -0
- data/test/dangerous_reindex_test.rb +27 -0
- data/test/facets_test.rb +90 -0
- data/test/gemfiles/activerecord31.gemfile +7 -0
- data/test/gemfiles/activerecord32.gemfile +7 -0
- data/test/gemfiles/activerecord40.gemfile +8 -0
- data/test/gemfiles/activerecord41.gemfile +8 -0
- data/test/gemfiles/activerecord50.gemfile +7 -0
- data/test/gemfiles/apartment.gemfile +8 -0
- data/test/gemfiles/mongoid2.gemfile +7 -0
- data/test/gemfiles/mongoid3.gemfile +6 -0
- data/test/gemfiles/mongoid4.gemfile +7 -0
- data/test/gemfiles/mongoid5.gemfile +7 -0
- data/test/gemfiles/nobrainer.gemfile +6 -0
- data/test/highlight_test.rb +63 -0
- data/test/index_test.rb +120 -0
- data/test/inheritance_test.rb +78 -0
- data/test/match_test.rb +227 -0
- data/test/misspellings_test.rb +46 -0
- data/test/model_test.rb +42 -0
- data/test/multi_search_test.rb +22 -0
- data/test/multi_tenancy_test.rb +22 -0
- data/test/order_test.rb +44 -0
- data/test/pagination_test.rb +53 -0
- data/test/query_test.rb +13 -0
- data/test/records_test.rb +8 -0
- data/test/reindex_job_test.rb +31 -0
- data/test/reindex_v2_job_test.rb +32 -0
- data/test/routing_test.rb +13 -0
- data/test/should_index_test.rb +32 -0
- data/test/similar_test.rb +28 -0
- data/test/sql_test.rb +196 -0
- data/test/suggest_test.rb +80 -0
- data/test/synonyms_test.rb +54 -0
- data/test/test_helper.rb +361 -0
- data/test/where_test.rb +171 -0
- 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
|
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)
|
data/searchkick.gemspec
ADDED
@@ -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
|