searchkick 0.8.5 → 0.8.6
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 +4 -4
- data/.travis.yml +6 -1
- data/CHANGELOG.md +9 -0
- data/Gemfile +1 -2
- data/README.md +33 -22
- data/ci/before_install.sh +14 -0
- data/gemfiles/activerecord41.gemfile +8 -0
- data/gemfiles/nobrainer.gemfile +6 -0
- data/lib/searchkick.rb +2 -2
- data/lib/searchkick/index.rb +405 -6
- data/lib/searchkick/model.rb +57 -47
- data/lib/searchkick/query.rb +18 -10
- data/lib/searchkick/results.rb +8 -1
- data/lib/searchkick/tasks.rb +1 -1
- data/lib/searchkick/version.rb +1 -1
- data/test/boost_test.rb +13 -3
- data/test/facets_test.rb +1 -0
- data/test/sql_test.rb +1 -1
- data/test/suggest_test.rb +7 -1
- data/test/test_helper.rb +42 -1
- metadata +6 -5
- data/lib/searchkick/reindex.rb +0 -339
- data/lib/searchkick/similar.rb +0 -19
data/lib/searchkick/model.rb
CHANGED
@@ -1,9 +1,13 @@
|
|
1
1
|
module Searchkick
|
2
|
+
module Reindex; end # legacy for Searchjoy
|
3
|
+
|
2
4
|
module Model
|
3
5
|
|
4
6
|
def searchkick(options = {})
|
5
7
|
raise "Only call searchkick once per model" if respond_to?(:searchkick_index)
|
6
8
|
|
9
|
+
Searchkick.models << self
|
10
|
+
|
7
11
|
class_eval do
|
8
12
|
cattr_reader :searchkick_options, :searchkick_klass
|
9
13
|
|
@@ -14,32 +18,51 @@ module Searchkick
|
|
14
18
|
class_variable_set :@@searchkick_callbacks, callbacks
|
15
19
|
class_variable_set :@@searchkick_index, options[:index_name] || [options[:index_prefix], model_name.plural, Searchkick.env].compact.join("_")
|
16
20
|
|
17
|
-
|
18
|
-
|
19
|
-
index = index.call if index.respond_to? :call
|
20
|
-
Searchkick::Index.new(index)
|
21
|
+
define_singleton_method(Searchkick.search_method_name) do |term = nil, options={}, &block|
|
22
|
+
searchkick_index.search_model(self, term, options, &block)
|
21
23
|
end
|
24
|
+
extend Searchkick::Reindex # legacy for Searchjoy
|
22
25
|
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
26
|
+
class << self
|
27
|
+
|
28
|
+
def searchkick_index
|
29
|
+
index = class_variable_get :@@searchkick_index
|
30
|
+
index = index.call if index.respond_to? :call
|
31
|
+
Searchkick::Index.new(index, searchkick_options)
|
27
32
|
end
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
query.execute
|
33
|
+
|
34
|
+
def enable_search_callbacks
|
35
|
+
class_variable_set :@@searchkick_callbacks, true
|
32
36
|
end
|
33
|
-
end
|
34
|
-
extend Searchkick::Reindex
|
35
|
-
include Searchkick::Similar
|
36
37
|
|
37
|
-
|
38
|
-
|
39
|
-
Searchkick::ReindexV2Job.perform_later(self.class.name, id.to_s)
|
40
|
-
else
|
41
|
-
Delayed::Job.enqueue Searchkick::ReindexJob.new(self.class.name, id.to_s)
|
38
|
+
def disable_search_callbacks
|
39
|
+
class_variable_set :@@searchkick_callbacks, false
|
42
40
|
end
|
41
|
+
|
42
|
+
def search_callbacks?
|
43
|
+
class_variable_get(:@@searchkick_callbacks) && Searchkick.callbacks?
|
44
|
+
end
|
45
|
+
|
46
|
+
def reindex(options = {})
|
47
|
+
searchkick_index.reindex_scope(searchkick_klass, options)
|
48
|
+
end
|
49
|
+
|
50
|
+
def clean_indices
|
51
|
+
searchkick_index.clean_indices
|
52
|
+
end
|
53
|
+
|
54
|
+
def searchkick_import(options = {})
|
55
|
+
(options[:index] || searchkick_index).import_scope(searchkick_klass)
|
56
|
+
end
|
57
|
+
|
58
|
+
def searchkick_create_index
|
59
|
+
searchkick_index.create_index
|
60
|
+
end
|
61
|
+
|
62
|
+
def searchkick_index_options
|
63
|
+
searchkick_index.index_options
|
64
|
+
end
|
65
|
+
|
43
66
|
end
|
44
67
|
|
45
68
|
if callbacks
|
@@ -52,38 +75,25 @@ module Searchkick
|
|
52
75
|
end
|
53
76
|
end
|
54
77
|
|
55
|
-
def
|
56
|
-
|
57
|
-
end
|
58
|
-
|
59
|
-
def self.disable_search_callbacks
|
60
|
-
class_variable_set :@@searchkick_callbacks, false
|
61
|
-
end
|
62
|
-
|
63
|
-
def self.search_callbacks?
|
64
|
-
class_variable_get(:@@searchkick_callbacks) && Searchkick.callbacks?
|
65
|
-
end
|
78
|
+
def reindex
|
79
|
+
self.class.searchkick_index.reindex_record(self)
|
80
|
+
end unless method_defined?(:reindex)
|
66
81
|
|
67
|
-
def
|
68
|
-
|
69
|
-
end
|
82
|
+
def reindex_async
|
83
|
+
self.class.searchkick_index.reindex_record_async(self)
|
84
|
+
end unless method_defined?(:reindex_async)
|
70
85
|
|
71
|
-
def
|
72
|
-
|
73
|
-
|
74
|
-
begin
|
75
|
-
index.remove self
|
76
|
-
rescue Elasticsearch::Transport::Transport::Errors::NotFound
|
77
|
-
# do nothing
|
78
|
-
end
|
79
|
-
else
|
80
|
-
index.store self
|
81
|
-
end
|
82
|
-
end
|
86
|
+
def similar(options = {})
|
87
|
+
self.class.searchkick_index.similar_record(self, options)
|
88
|
+
end unless method_defined?(:similar)
|
83
89
|
|
84
90
|
def search_data
|
85
91
|
respond_to?(:to_hash) ? to_hash : serializable_hash
|
86
|
-
end
|
92
|
+
end unless method_defined?(:search_data)
|
93
|
+
|
94
|
+
def should_index?
|
95
|
+
true
|
96
|
+
end unless method_defined?(:should_index?)
|
87
97
|
|
88
98
|
end
|
89
99
|
end
|
data/lib/searchkick/query.rb
CHANGED
@@ -28,7 +28,7 @@ module Searchkick
|
|
28
28
|
k, v = value.is_a?(Hash) ? value.to_a.first : [value, :word]
|
29
29
|
k2, boost = k.to_s.split("^", 2)
|
30
30
|
field = "#{k2}.#{v == :word ? "analyzed" : v}"
|
31
|
-
boost_fields[field] = boost.
|
31
|
+
boost_fields[field] = boost.to_f if boost
|
32
32
|
field
|
33
33
|
end
|
34
34
|
end
|
@@ -195,20 +195,21 @@ module Searchkick
|
|
195
195
|
boost_where[personalize_field] = options[:user_id]
|
196
196
|
end
|
197
197
|
if options[:personalize]
|
198
|
-
boost_where.merge
|
198
|
+
boost_where = boost_where.merge(options[:personalize])
|
199
199
|
end
|
200
200
|
boost_where.each do |field, value|
|
201
|
-
if value.is_a?(Hash)
|
201
|
+
if value.is_a?(Array) and value.first.is_a?(Hash)
|
202
|
+
value.each do |value_factor|
|
203
|
+
value, factor = value_factor[:value], value_factor[:factor]
|
204
|
+
custom_filters << custom_filter(field, value, factor)
|
205
|
+
end
|
206
|
+
elsif value.is_a?(Hash)
|
202
207
|
value, factor = value[:value], value[:factor]
|
208
|
+
custom_filters << custom_filter(field, value, factor)
|
203
209
|
else
|
204
210
|
factor = 1000
|
211
|
+
custom_filters << custom_filter(field, value, factor)
|
205
212
|
end
|
206
|
-
custom_filters << {
|
207
|
-
filter: {
|
208
|
-
term: {field => value}
|
209
|
-
},
|
210
|
-
boost_factor: factor
|
211
|
-
}
|
212
213
|
end
|
213
214
|
|
214
215
|
boost_by_distance = options[:boost_by_distance]
|
@@ -317,7 +318,7 @@ module Searchkick
|
|
317
318
|
|
318
319
|
# intersection
|
319
320
|
if options[:fields]
|
320
|
-
suggest_fields = suggest_fields & options[:fields].map{|v| (v.is_a?(Hash) ? v.keys.first : v).to_s }
|
321
|
+
suggest_fields = suggest_fields & options[:fields].map{|v| (v.is_a?(Hash) ? v.keys.first : v).to_s.split("^", 2).first }
|
321
322
|
end
|
322
323
|
|
323
324
|
if suggest_fields.any?
|
@@ -535,5 +536,12 @@ module Searchkick
|
|
535
536
|
end
|
536
537
|
end
|
537
538
|
|
539
|
+
def custom_filter(field, value, factor)
|
540
|
+
{
|
541
|
+
filter: term_filters(field, value),
|
542
|
+
boost_factor: factor
|
543
|
+
}
|
544
|
+
end
|
545
|
+
|
538
546
|
end
|
539
547
|
end
|
data/lib/searchkick/results.rb
CHANGED
@@ -24,7 +24,11 @@ module Searchkick
|
|
24
24
|
hits.group_by{|hit, i| hit["_type"] }.each do |type, grouped_hits|
|
25
25
|
records = type.camelize.constantize
|
26
26
|
if options[:includes]
|
27
|
-
records
|
27
|
+
if defined?(NoBrainer::Document) and records < NoBrainer::Document
|
28
|
+
records = records.preload(options[:includes])
|
29
|
+
else
|
30
|
+
records = records.includes(options[:includes])
|
31
|
+
end
|
28
32
|
end
|
29
33
|
results[type] = results_query(records, grouped_hits)
|
30
34
|
end
|
@@ -143,6 +147,9 @@ module Searchkick
|
|
143
147
|
elsif records.respond_to?(:queryable)
|
144
148
|
# Mongoid 3+
|
145
149
|
records.queryable.for_ids(grouped_hits.map{|hit| hit["_id"] }).to_a
|
150
|
+
elsif records.respond_to?(:unscoped) and records.all.respond_to?(:preload)
|
151
|
+
# Nobrainer
|
152
|
+
records.unscoped.where(:id.in => grouped_hits.map{|hit| hit["_id"] }).to_a
|
146
153
|
else
|
147
154
|
raise "Not sure how to load records"
|
148
155
|
end
|
data/lib/searchkick/tasks.rb
CHANGED
@@ -22,7 +22,7 @@ namespace :searchkick do
|
|
22
22
|
desc "reindex all models"
|
23
23
|
task :all => :environment do
|
24
24
|
Rails.application.eager_load!
|
25
|
-
|
25
|
+
Searchkick.models.each do |model|
|
26
26
|
puts "Reindexing #{model.name}..."
|
27
27
|
model.reindex
|
28
28
|
end
|
data/lib/searchkick/version.rb
CHANGED
data/test/boost_test.rb
CHANGED
@@ -75,6 +75,14 @@ class TestBoost < Minitest::Test
|
|
75
75
|
assert_order "red", ["Red", "White"], fields: ["name^10", "color"]
|
76
76
|
end
|
77
77
|
|
78
|
+
def test_boost_fields_decimal
|
79
|
+
store [
|
80
|
+
{name: "Red", color: "White"},
|
81
|
+
{name: "White", color: "Red Red Red"}
|
82
|
+
]
|
83
|
+
assert_order "red", ["Red", "White"], fields: ["name^10.5", "color"]
|
84
|
+
end
|
85
|
+
|
78
86
|
def test_boost_fields_word_start
|
79
87
|
store [
|
80
88
|
{name: "Red", color: "White"},
|
@@ -96,12 +104,14 @@ class TestBoost < Minitest::Test
|
|
96
104
|
def test_boost_where
|
97
105
|
store [
|
98
106
|
{name: "Tomato A"},
|
99
|
-
{name: "Tomato B", user_ids: [1, 2
|
100
|
-
{name: "Tomato C"}
|
101
|
-
{name: "Tomato D"}
|
107
|
+
{name: "Tomato B", user_ids: [1, 2]},
|
108
|
+
{name: "Tomato C", user_ids: [3]}
|
102
109
|
]
|
103
110
|
assert_first "tomato", "Tomato B", boost_where: {user_ids: 2}
|
111
|
+
assert_first "tomato", "Tomato B", boost_where: {user_ids: [1, 4]}
|
104
112
|
assert_first "tomato", "Tomato B", boost_where: {user_ids: {value: 2, factor: 10}}
|
113
|
+
assert_first "tomato", "Tomato B", boost_where: {user_ids: {value: [1, 4], factor: 10}}
|
114
|
+
assert_order "tomato", ["Tomato C", "Tomato B", "Tomato A"], boost_where: {user_ids: [{value: 1, factor: 10}, {value: 3, factor: 20}]}
|
105
115
|
end
|
106
116
|
|
107
117
|
def test_boost_by_distance
|
data/test/facets_test.rb
CHANGED
data/test/sql_test.rb
CHANGED
@@ -312,7 +312,7 @@ class TestSql < Minitest::Test
|
|
312
312
|
end
|
313
313
|
|
314
314
|
# TODO see if Mongoid is loaded
|
315
|
-
|
315
|
+
unless defined?(Mongoid) or defined?(NoBrainer)
|
316
316
|
def test_include
|
317
317
|
store_names ["Product A"]
|
318
318
|
assert Product.search("product", include: [:store]).first.association(:store).loaded?
|
data/test/suggest_test.rb
CHANGED
@@ -18,6 +18,7 @@ class TestSuggest < Minitest::Test
|
|
18
18
|
end
|
19
19
|
|
20
20
|
def test_without_option
|
21
|
+
store_names ["hi"] # needed to prevent ElasticsearchException - seed 668
|
21
22
|
assert_raises(RuntimeError){ Product.search("hi").suggestions }
|
22
23
|
end
|
23
24
|
|
@@ -57,11 +58,16 @@ class TestSuggest < Minitest::Test
|
|
57
58
|
assert_suggest "shar", "shark", fields: [:name, :unknown]
|
58
59
|
end
|
59
60
|
|
60
|
-
def
|
61
|
+
def test_fields_partial_match
|
61
62
|
store_names ["Great White Shark", "Hammerhead Shark", "Tiger Shark"]
|
62
63
|
assert_suggest "How Big is a Tigre Shar", "how big is a tiger shark", fields: [{name: :word_start}]
|
63
64
|
end
|
64
65
|
|
66
|
+
def test_fields_partial_match_boost
|
67
|
+
store_names ["Great White Shark", "Hammerhead Shark", "Tiger Shark"]
|
68
|
+
assert_suggest "How Big is a Tigre Shar", "how big is a tiger shark", fields: [{"name^2" => :word_start}]
|
69
|
+
end
|
70
|
+
|
65
71
|
protected
|
66
72
|
|
67
73
|
def assert_suggest(term, expected, options = {})
|
data/test/test_helper.rb
CHANGED
@@ -71,6 +71,45 @@ if defined?(Mongoid)
|
|
71
71
|
class Dog < Animal
|
72
72
|
end
|
73
73
|
|
74
|
+
class Cat < Animal
|
75
|
+
end
|
76
|
+
elsif defined?(NoBrainer)
|
77
|
+
NoBrainer.configure do |config|
|
78
|
+
config.app_name = :searchkick
|
79
|
+
config.environment = :test
|
80
|
+
end
|
81
|
+
|
82
|
+
class Product
|
83
|
+
include NoBrainer::Document
|
84
|
+
include NoBrainer::Document::Timestamps
|
85
|
+
|
86
|
+
field :name, type: String
|
87
|
+
field :store_id, type: Integer
|
88
|
+
field :in_stock, type: Boolean
|
89
|
+
field :backordered, type: Boolean
|
90
|
+
field :orders_count, type: Integer
|
91
|
+
field :price, type: Integer
|
92
|
+
field :color, type: String
|
93
|
+
field :latitude
|
94
|
+
field :longitude
|
95
|
+
field :description, type: String
|
96
|
+
end
|
97
|
+
|
98
|
+
class Store
|
99
|
+
include NoBrainer::Document
|
100
|
+
|
101
|
+
field :name, type: String
|
102
|
+
end
|
103
|
+
|
104
|
+
class Animal
|
105
|
+
include NoBrainer::Document
|
106
|
+
|
107
|
+
field :name, type: String
|
108
|
+
end
|
109
|
+
|
110
|
+
class Dog < Animal
|
111
|
+
end
|
112
|
+
|
74
113
|
class Cat < Animal
|
75
114
|
end
|
76
115
|
else
|
@@ -86,6 +125,8 @@ else
|
|
86
125
|
# migrations
|
87
126
|
ActiveRecord::Base.establish_connection adapter: "sqlite3", database: ":memory:"
|
88
127
|
|
128
|
+
ActiveRecord::Base.raise_in_transactional_callbacks = true if ActiveRecord::Base.respond_to?(:raise_in_transactional_callbacks=)
|
129
|
+
|
89
130
|
ActiveRecord::Migration.create_table :products do |t|
|
90
131
|
t.string :name
|
91
132
|
t.integer :store_id
|
@@ -97,7 +138,7 @@ else
|
|
97
138
|
t.decimal :latitude, precision: 10, scale: 7
|
98
139
|
t.decimal :longitude, precision: 10, scale: 7
|
99
140
|
t.text :description
|
100
|
-
t.timestamps
|
141
|
+
t.timestamps null: true
|
101
142
|
end
|
102
143
|
|
103
144
|
ActiveRecord::Migration.create_table :stores do |t|
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: searchkick
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.8.
|
4
|
+
version: 0.8.6
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Andrew Kane
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date:
|
11
|
+
date: 2015-02-11 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: activemodel
|
@@ -108,22 +108,23 @@ files:
|
|
108
108
|
- LICENSE.txt
|
109
109
|
- README.md
|
110
110
|
- Rakefile
|
111
|
+
- ci/before_install.sh
|
111
112
|
- gemfiles/activerecord31.gemfile
|
112
113
|
- gemfiles/activerecord32.gemfile
|
113
114
|
- gemfiles/activerecord40.gemfile
|
115
|
+
- gemfiles/activerecord41.gemfile
|
114
116
|
- gemfiles/mongoid2.gemfile
|
115
117
|
- gemfiles/mongoid3.gemfile
|
116
118
|
- gemfiles/mongoid4.gemfile
|
119
|
+
- gemfiles/nobrainer.gemfile
|
117
120
|
- lib/searchkick.rb
|
118
121
|
- lib/searchkick/index.rb
|
119
122
|
- lib/searchkick/logging.rb
|
120
123
|
- lib/searchkick/model.rb
|
121
124
|
- lib/searchkick/query.rb
|
122
|
-
- lib/searchkick/reindex.rb
|
123
125
|
- lib/searchkick/reindex_job.rb
|
124
126
|
- lib/searchkick/reindex_v2_job.rb
|
125
127
|
- lib/searchkick/results.rb
|
126
|
-
- lib/searchkick/similar.rb
|
127
128
|
- lib/searchkick/tasks.rb
|
128
129
|
- lib/searchkick/version.rb
|
129
130
|
- searchkick.gemspec
|
@@ -164,7 +165,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
164
165
|
version: '0'
|
165
166
|
requirements: []
|
166
167
|
rubyforge_project:
|
167
|
-
rubygems_version: 2.
|
168
|
+
rubygems_version: 2.4.5
|
168
169
|
signing_key:
|
169
170
|
specification_version: 4
|
170
171
|
summary: Searchkick learns what your users are looking for. As more people search,
|
data/lib/searchkick/reindex.rb
DELETED
@@ -1,339 +0,0 @@
|
|
1
|
-
module Searchkick
|
2
|
-
module Reindex
|
3
|
-
|
4
|
-
# https://gist.github.com/jarosan/3124884
|
5
|
-
# http://www.elasticsearch.org/blog/changing-mapping-with-zero-downtime/
|
6
|
-
def reindex(options = {})
|
7
|
-
skip_import = options[:import] == false
|
8
|
-
|
9
|
-
alias_name = searchkick_index.name
|
10
|
-
new_name = "#{alias_name}_#{Time.now.strftime('%Y%m%d%H%M%S%L')}"
|
11
|
-
index = Searchkick::Index.new(new_name)
|
12
|
-
|
13
|
-
clean_indices
|
14
|
-
|
15
|
-
index.create searchkick_index_options
|
16
|
-
|
17
|
-
# check if alias exists
|
18
|
-
if Searchkick.client.indices.exists_alias(name: alias_name)
|
19
|
-
# import before swap
|
20
|
-
searchkick_import(index: index) unless skip_import
|
21
|
-
|
22
|
-
# get existing indices to remove
|
23
|
-
old_indices = Searchkick.client.indices.get_alias(name: alias_name).keys
|
24
|
-
actions = old_indices.map{|name| {remove: {index: name, alias: alias_name}} } + [{add: {index: new_name, alias: alias_name}}]
|
25
|
-
Searchkick.client.indices.update_aliases body: {actions: actions}
|
26
|
-
clean_indices
|
27
|
-
else
|
28
|
-
searchkick_index.delete if searchkick_index.exists?
|
29
|
-
Searchkick.client.indices.update_aliases body: {actions: [{add: {index: new_name, alias: alias_name}}]}
|
30
|
-
|
31
|
-
# import after swap
|
32
|
-
searchkick_import(index: index) unless skip_import
|
33
|
-
end
|
34
|
-
|
35
|
-
index.refresh
|
36
|
-
|
37
|
-
true
|
38
|
-
end
|
39
|
-
|
40
|
-
# remove old indices that start w/ index_name
|
41
|
-
def clean_indices
|
42
|
-
all_indices = Searchkick.client.indices.get_aliases
|
43
|
-
indices = all_indices.select{|k, v| (v.empty? || v["aliases"].empty?) && k =~ /\A#{Regexp.escape(searchkick_index.name)}_\d{14,17}\z/ }.keys
|
44
|
-
indices.each do |index|
|
45
|
-
Searchkick::Index.new(index).delete
|
46
|
-
end
|
47
|
-
indices
|
48
|
-
end
|
49
|
-
|
50
|
-
def self.extended(klass)
|
51
|
-
@descendents ||= []
|
52
|
-
@descendents << klass unless @descendents.include?(klass)
|
53
|
-
end
|
54
|
-
|
55
|
-
def searchkick_import(options = {})
|
56
|
-
index = options[:index] || searchkick_index
|
57
|
-
batch_size = searchkick_options[:batch_size] || 1000
|
58
|
-
|
59
|
-
# use scope for import
|
60
|
-
scope = searchkick_klass
|
61
|
-
scope = scope.search_import if scope.respond_to?(:search_import)
|
62
|
-
if scope.respond_to?(:find_in_batches)
|
63
|
-
scope.find_in_batches batch_size: batch_size do |batch|
|
64
|
-
index.import batch.select{|item| item.should_index? }
|
65
|
-
end
|
66
|
-
else
|
67
|
-
# https://github.com/karmi/tire/blob/master/lib/tire/model/import.rb
|
68
|
-
# use cursor for Mongoid
|
69
|
-
items = []
|
70
|
-
scope.all.each do |item|
|
71
|
-
items << item if item.should_index?
|
72
|
-
if items.length == batch_size
|
73
|
-
index.import items
|
74
|
-
items = []
|
75
|
-
end
|
76
|
-
end
|
77
|
-
index.import items
|
78
|
-
end
|
79
|
-
end
|
80
|
-
|
81
|
-
def searchkick_index_options
|
82
|
-
options = searchkick_options
|
83
|
-
|
84
|
-
if options[:mappings] and !options[:merge_mappings]
|
85
|
-
settings = options[:settings] || {}
|
86
|
-
mappings = options[:mappings]
|
87
|
-
else
|
88
|
-
settings = {
|
89
|
-
analysis: {
|
90
|
-
analyzer: {
|
91
|
-
searchkick_keyword: {
|
92
|
-
type: "custom",
|
93
|
-
tokenizer: "keyword",
|
94
|
-
filter: ["lowercase", "searchkick_stemmer"]
|
95
|
-
},
|
96
|
-
default_index: {
|
97
|
-
type: "custom",
|
98
|
-
tokenizer: "standard",
|
99
|
-
# synonym should come last, after stemming and shingle
|
100
|
-
# shingle must come before searchkick_stemmer
|
101
|
-
filter: ["standard", "lowercase", "asciifolding", "searchkick_index_shingle", "searchkick_stemmer"]
|
102
|
-
},
|
103
|
-
searchkick_search: {
|
104
|
-
type: "custom",
|
105
|
-
tokenizer: "standard",
|
106
|
-
filter: ["standard", "lowercase", "asciifolding", "searchkick_search_shingle", "searchkick_stemmer"]
|
107
|
-
},
|
108
|
-
searchkick_search2: {
|
109
|
-
type: "custom",
|
110
|
-
tokenizer: "standard",
|
111
|
-
filter: ["standard", "lowercase", "asciifolding", "searchkick_stemmer"]
|
112
|
-
},
|
113
|
-
# https://github.com/leschenko/elasticsearch_autocomplete/blob/master/lib/elasticsearch_autocomplete/analyzers.rb
|
114
|
-
searchkick_autocomplete_index: {
|
115
|
-
type: "custom",
|
116
|
-
tokenizer: "searchkick_autocomplete_ngram",
|
117
|
-
filter: ["lowercase", "asciifolding"]
|
118
|
-
},
|
119
|
-
searchkick_autocomplete_search: {
|
120
|
-
type: "custom",
|
121
|
-
tokenizer: "keyword",
|
122
|
-
filter: ["lowercase", "asciifolding"]
|
123
|
-
},
|
124
|
-
searchkick_word_search: {
|
125
|
-
type: "custom",
|
126
|
-
tokenizer: "standard",
|
127
|
-
filter: ["lowercase", "asciifolding"]
|
128
|
-
},
|
129
|
-
searchkick_suggest_index: {
|
130
|
-
type: "custom",
|
131
|
-
tokenizer: "standard",
|
132
|
-
filter: ["lowercase", "asciifolding", "searchkick_suggest_shingle"]
|
133
|
-
},
|
134
|
-
searchkick_text_start_index: {
|
135
|
-
type: "custom",
|
136
|
-
tokenizer: "keyword",
|
137
|
-
filter: ["lowercase", "asciifolding", "searchkick_edge_ngram"]
|
138
|
-
},
|
139
|
-
searchkick_text_middle_index: {
|
140
|
-
type: "custom",
|
141
|
-
tokenizer: "keyword",
|
142
|
-
filter: ["lowercase", "asciifolding", "searchkick_ngram"]
|
143
|
-
},
|
144
|
-
searchkick_text_end_index: {
|
145
|
-
type: "custom",
|
146
|
-
tokenizer: "keyword",
|
147
|
-
filter: ["lowercase", "asciifolding", "reverse", "searchkick_edge_ngram", "reverse"]
|
148
|
-
},
|
149
|
-
searchkick_word_start_index: {
|
150
|
-
type: "custom",
|
151
|
-
tokenizer: "standard",
|
152
|
-
filter: ["lowercase", "asciifolding", "searchkick_edge_ngram"]
|
153
|
-
},
|
154
|
-
searchkick_word_middle_index: {
|
155
|
-
type: "custom",
|
156
|
-
tokenizer: "standard",
|
157
|
-
filter: ["lowercase", "asciifolding", "searchkick_ngram"]
|
158
|
-
},
|
159
|
-
searchkick_word_end_index: {
|
160
|
-
type: "custom",
|
161
|
-
tokenizer: "standard",
|
162
|
-
filter: ["lowercase", "asciifolding", "reverse", "searchkick_edge_ngram", "reverse"]
|
163
|
-
}
|
164
|
-
},
|
165
|
-
filter: {
|
166
|
-
searchkick_index_shingle: {
|
167
|
-
type: "shingle",
|
168
|
-
token_separator: ""
|
169
|
-
},
|
170
|
-
# lucky find http://web.archiveorange.com/archive/v/AAfXfQ17f57FcRINsof7
|
171
|
-
searchkick_search_shingle: {
|
172
|
-
type: "shingle",
|
173
|
-
token_separator: "",
|
174
|
-
output_unigrams: false,
|
175
|
-
output_unigrams_if_no_shingles: true
|
176
|
-
},
|
177
|
-
searchkick_suggest_shingle: {
|
178
|
-
type: "shingle",
|
179
|
-
max_shingle_size: 5
|
180
|
-
},
|
181
|
-
searchkick_edge_ngram: {
|
182
|
-
type: "edgeNGram",
|
183
|
-
min_gram: 1,
|
184
|
-
max_gram: 50
|
185
|
-
},
|
186
|
-
searchkick_ngram: {
|
187
|
-
type: "nGram",
|
188
|
-
min_gram: 1,
|
189
|
-
max_gram: 50
|
190
|
-
},
|
191
|
-
searchkick_stemmer: {
|
192
|
-
type: "snowball",
|
193
|
-
language: options[:language] || "English"
|
194
|
-
}
|
195
|
-
},
|
196
|
-
tokenizer: {
|
197
|
-
searchkick_autocomplete_ngram: {
|
198
|
-
type: "edgeNGram",
|
199
|
-
min_gram: 1,
|
200
|
-
max_gram: 50
|
201
|
-
}
|
202
|
-
}
|
203
|
-
}
|
204
|
-
}
|
205
|
-
|
206
|
-
if Searchkick.env == "test"
|
207
|
-
settings.merge!(number_of_shards: 1, number_of_replicas: 0)
|
208
|
-
end
|
209
|
-
|
210
|
-
settings.deep_merge!(options[:settings] || {})
|
211
|
-
|
212
|
-
# synonyms
|
213
|
-
synonyms = options[:synonyms] || []
|
214
|
-
if synonyms.any?
|
215
|
-
settings[:analysis][:filter][:searchkick_synonym] = {
|
216
|
-
type: "synonym",
|
217
|
-
synonyms: synonyms.select{|s| s.size > 1 }.map{|s| s.join(",") }
|
218
|
-
}
|
219
|
-
# choosing a place for the synonym filter when stemming is not easy
|
220
|
-
# https://groups.google.com/forum/#!topic/elasticsearch/p7qcQlgHdB8
|
221
|
-
# TODO use a snowball stemmer on synonyms when creating the token filter
|
222
|
-
|
223
|
-
# http://elasticsearch-users.115913.n3.nabble.com/synonym-multi-words-search-td4030811.html
|
224
|
-
# I find the following approach effective if you are doing multi-word synonyms (synonym phrases):
|
225
|
-
# - Only apply the synonym expansion at index time
|
226
|
-
# - Don't have the synonym filter applied search
|
227
|
-
# - Use directional synonyms where appropriate. You want to make sure that you're not injecting terms that are too general.
|
228
|
-
settings[:analysis][:analyzer][:default_index][:filter].insert(4, "searchkick_synonym")
|
229
|
-
settings[:analysis][:analyzer][:default_index][:filter] << "searchkick_synonym"
|
230
|
-
end
|
231
|
-
|
232
|
-
if options[:wordnet]
|
233
|
-
settings[:analysis][:filter][:searchkick_wordnet] = {
|
234
|
-
type: "synonym",
|
235
|
-
format: "wordnet",
|
236
|
-
synonyms_path: Searchkick.wordnet_path
|
237
|
-
}
|
238
|
-
|
239
|
-
settings[:analysis][:analyzer][:default_index][:filter].insert(4, "searchkick_wordnet")
|
240
|
-
settings[:analysis][:analyzer][:default_index][:filter] << "searchkick_wordnet"
|
241
|
-
end
|
242
|
-
|
243
|
-
if options[:special_characters] == false
|
244
|
-
settings[:analysis][:analyzer].each do |analyzer, analyzer_settings|
|
245
|
-
analyzer_settings[:filter].reject!{|f| f == "asciifolding" }
|
246
|
-
end
|
247
|
-
end
|
248
|
-
|
249
|
-
mapping = {}
|
250
|
-
|
251
|
-
# conversions
|
252
|
-
if options[:conversions]
|
253
|
-
mapping[:conversions] = {
|
254
|
-
type: "nested",
|
255
|
-
properties: {
|
256
|
-
query: {type: "string", analyzer: "searchkick_keyword"},
|
257
|
-
count: {type: "integer"}
|
258
|
-
}
|
259
|
-
}
|
260
|
-
end
|
261
|
-
|
262
|
-
mapping_options = Hash[
|
263
|
-
[:autocomplete, :suggest, :text_start, :text_middle, :text_end, :word_start, :word_middle, :word_end, :highlight]
|
264
|
-
.map{|type| [type, (options[type] || []).map(&:to_s)] }
|
265
|
-
]
|
266
|
-
|
267
|
-
mapping_options.values.flatten.uniq.each do |field|
|
268
|
-
field_mapping = {
|
269
|
-
type: "multi_field",
|
270
|
-
fields: {
|
271
|
-
field => {type: "string", index: "not_analyzed"},
|
272
|
-
"analyzed" => {type: "string", index: "analyzed"}
|
273
|
-
# term_vector: "with_positions_offsets" for fast / correct highlighting
|
274
|
-
# http://www.elasticsearch.org/guide/en/elasticsearch/reference/current/search-request-highlighting.html#_fast_vector_highlighter
|
275
|
-
}
|
276
|
-
}
|
277
|
-
|
278
|
-
mapping_options.except(:highlight).each do |type, fields|
|
279
|
-
if fields.include?(field)
|
280
|
-
field_mapping[:fields][type] = {type: "string", index: "analyzed", analyzer: "searchkick_#{type}_index"}
|
281
|
-
end
|
282
|
-
end
|
283
|
-
|
284
|
-
if mapping_options[:highlight].include?(field)
|
285
|
-
field_mapping[:fields]["analyzed"][:term_vector] = "with_positions_offsets"
|
286
|
-
end
|
287
|
-
|
288
|
-
mapping[field] = field_mapping
|
289
|
-
end
|
290
|
-
|
291
|
-
(options[:locations] || []).map(&:to_s).each do |field|
|
292
|
-
mapping[field] = {
|
293
|
-
type: "geo_point"
|
294
|
-
}
|
295
|
-
end
|
296
|
-
|
297
|
-
(options[:unsearchable] || []).map(&:to_s).each do |field|
|
298
|
-
mapping[field] = {
|
299
|
-
type: "string",
|
300
|
-
index: "no"
|
301
|
-
}
|
302
|
-
end
|
303
|
-
|
304
|
-
mappings = {
|
305
|
-
_default_: {
|
306
|
-
properties: mapping,
|
307
|
-
# https://gist.github.com/kimchy/2898285
|
308
|
-
dynamic_templates: [
|
309
|
-
{
|
310
|
-
string_template: {
|
311
|
-
match: "*",
|
312
|
-
match_mapping_type: "string",
|
313
|
-
mapping: {
|
314
|
-
# http://www.elasticsearch.org/guide/reference/mapping/multi-field-type/
|
315
|
-
type: "multi_field",
|
316
|
-
fields: {
|
317
|
-
# analyzed field must be the default field for include_in_all
|
318
|
-
# http://www.elasticsearch.org/guide/reference/mapping/multi-field-type/
|
319
|
-
# however, we can include the not_analyzed field in _all
|
320
|
-
# and the _all index analyzer will take care of it
|
321
|
-
"{name}" => {type: "string", index: "not_analyzed"},
|
322
|
-
"analyzed" => {type: "string", index: "analyzed"}
|
323
|
-
}
|
324
|
-
}
|
325
|
-
}
|
326
|
-
}
|
327
|
-
]
|
328
|
-
}
|
329
|
-
}.deep_merge(options[:mappings] || {})
|
330
|
-
end
|
331
|
-
|
332
|
-
{
|
333
|
-
settings: settings,
|
334
|
-
mappings: mappings
|
335
|
-
}
|
336
|
-
end
|
337
|
-
|
338
|
-
end
|
339
|
-
end
|