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