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