searchkick 2.0.0 → 2.0.1

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 06b1b68ac3cb57317a8fdd0c94e3988ffed37852
4
- data.tar.gz: b5a51c3c2126b88749bd114c0355a9e593f2cdfa
3
+ metadata.gz: 17c7f39c1470d38bc637340ae0031997a0b0134c
4
+ data.tar.gz: 0167c2d1448536bf5b2b48120f56e992c22aed8e
5
5
  SHA512:
6
- metadata.gz: b520ec977fec7ac5a65aa6a5aacd9fb4c6142a079385bd63dce20b28ccd59f7cf84993daa4e1f68f8befba383ceb2246ce2a78b38846ec43e7d4432bb9587b40
7
- data.tar.gz: b0032add9b2f7ed647ecbc9f2db958f5f240a0725b3990ea24697290e8b61c5e2d6afdf8777742fafaa2fbb33407f0d62f6480239522099869a9bccaf4b9092e
6
+ metadata.gz: a6bf61245813c31fc922a29c5aa4998df13c9c1f51eba6c248ea1bfbba435f048de37a069d23cad6fc041618a235974895cb7466c08ee24cb14eb39296fc908d
7
+ data.tar.gz: b50909d603658acd1c9562470c2fd1de404cffd2f93a22c1ca926003afaaabcf33ea70a14ba69d96e3b94b50af241b91cbd267492c079416e2b9ae500c6cf32e
data/.travis.yml CHANGED
@@ -26,7 +26,7 @@ matrix:
26
26
  env: ELASTICSEARCH_VERSION=2.0.0
27
27
  jdk: oraclejdk7
28
28
  - gemfile: Gemfile
29
- env: ELASTICSEARCH_VERSION=2.4.1
29
+ env: ELASTICSEARCH_VERSION=2.4.3
30
30
  jdk: oraclejdk7
31
31
  - gemfile: Gemfile
32
32
  env: ELASTICSEARCH_VERSION=5.0.1
data/CHANGELOG.md CHANGED
@@ -1,3 +1,8 @@
1
+ ## 2.0.1
2
+
3
+ - Added `search_hit` and `search_highlights` methods to models
4
+ - Improved reindex performance
5
+
1
6
  ## 2.0.0
2
7
 
3
8
  - Added support for `reindex` on associations
data/README.md CHANGED
@@ -254,7 +254,7 @@ Available options are:
254
254
  ### Exact Matches
255
255
 
256
256
  ```ruby
257
- User.search params[:q], fields: [{email: :exact}, :name]
257
+ User.search query, fields: [{email: :exact}, :name]
258
258
  ```
259
259
 
260
260
  ### Phrase Matches
@@ -431,7 +431,7 @@ Product.reindex(resume: true)
431
431
 
432
432
  #### No need to reindex
433
433
 
434
- - App starts
434
+ - app starts
435
435
 
436
436
  ### Stay Synced
437
437
 
@@ -784,8 +784,8 @@ bands = Band.search "cinema", fields: [:name], highlight: true
784
784
  View the highlighted fields with:
785
785
 
786
786
  ```ruby
787
- bands.with_details.each do |band, details|
788
- puts details[:highlight][:name] # "Two Door <em>Cinema</em> Club"
787
+ bands.each do |band|
788
+ band.search_highlights[:name] # "Two Door <em>Cinema</em> Club"
789
789
  end
790
790
  ```
791
791
 
@@ -1013,7 +1013,7 @@ heroku run rake searchkick:reindex CLASS=Product
1013
1013
  Include `elasticsearch 1.0.15` or greater in your Gemfile.
1014
1014
 
1015
1015
  ```ruby
1016
- gem "elasticsearch", ">= 1.0.15"
1016
+ gem 'elasticsearch', '>= 1.0.15'
1017
1017
  ```
1018
1018
 
1019
1019
  Create an initializer `config/initializers/elasticsearch.rb` with:
@@ -1088,15 +1088,23 @@ See [Production Rails](https://github.com/ankane/production_rails) for other goo
1088
1088
 
1089
1089
  ## Performance
1090
1090
 
1091
+ ### JSON Generation
1092
+
1093
+ Significantly increase performance with faster JSON generation. Add [Oj](https://github.com/ohler55/oj) to your Gemfile.
1094
+
1095
+ ```ruby
1096
+ gem 'oj'
1097
+ ```
1098
+
1091
1099
  ### Persistent HTTP Connections
1092
1100
 
1093
- For the best performance, add [Typhoeus](https://github.com/typhoeus/typhoeus) to your Gemfile.
1101
+ Significantly increase performance with persistent HTTP connections. Add [Typhoeus](https://github.com/typhoeus/typhoeus) to your Gemfile.
1094
1102
 
1095
1103
  ```ruby
1096
1104
  gem 'typhoeus'
1097
1105
  ```
1098
1106
 
1099
- And create an initializer to reduce log noise with:
1107
+ To reduce log noise, create an initializer with:
1100
1108
 
1101
1109
  ```ruby
1102
1110
  Ethon.logger = Logger.new("/dev/null")
@@ -1126,7 +1134,7 @@ end
1126
1134
 
1127
1135
  ### Routing
1128
1136
 
1129
- Searchkick supports [Elasticsearch’s routing feature](https://www.elastic.co/blog/customizing-your-document-routing).
1137
+ Searchkick supports [Elasticsearch’s routing feature](https://www.elastic.co/blog/customizing-your-document-routing), which can significantly speed up searches.
1130
1138
 
1131
1139
  ```ruby
1132
1140
  class Business < ActiveRecord::Base
@@ -1245,24 +1253,22 @@ User.search "san", fields: ["address.city"], where: {"address.zip_code" => 12345
1245
1253
  Reindex one record
1246
1254
 
1247
1255
  ```ruby
1248
- product = Product.find 10
1256
+ product = Product.find(1)
1249
1257
  product.reindex
1250
1258
  # or to reindex in the background
1251
1259
  product.reindex_async
1252
1260
  ```
1253
1261
 
1254
- Reindex more than one record without recreating the index
1262
+ Reindex multiple records
1255
1263
 
1256
1264
  ```ruby
1257
- some_company.products.reindex
1265
+ Product.where(store_id: 1).reindex
1258
1266
  ```
1259
1267
 
1260
- Reindex large set of records in batches
1268
+ Reindex associations
1261
1269
 
1262
1270
  ```ruby
1263
- Product.where("id > 100000").find_in_batches do |batch|
1264
- Product.searchkick_index.import(batch)
1265
- end
1271
+ store.products.reindex
1266
1272
  ```
1267
1273
 
1268
1274
  Reindex a subset of attributes (partial reindex)
data/Rakefile CHANGED
@@ -6,3 +6,7 @@ Rake::TestTask.new do |t|
6
6
  t.libs << "test"
7
7
  t.pattern = "test/**/*_test.rb"
8
8
  end
9
+
10
+ task :benchmark do
11
+ require_relative "benchmark/benchmark"
12
+ end
data/benchmark/Gemfile ADDED
@@ -0,0 +1,19 @@
1
+ source "https://rubygems.org"
2
+
3
+ # Specify your gem's dependencies in searchkick.gemspec
4
+ gemspec path: "../"
5
+
6
+ gem "sqlite3"
7
+ gem "activerecord", "~> 5.0.0"
8
+ gem "activerecord-import"
9
+
10
+ # performance
11
+ gem "typhoeus"
12
+ gem "oj"
13
+
14
+ # profiling
15
+ gem "ruby-prof"
16
+ gem "allocation_stats"
17
+ gem "get_process_mem"
18
+ gem "memory_profiler"
19
+ gem "allocation_tracer"
@@ -0,0 +1,63 @@
1
+ require "bundler/setup"
2
+ Bundler.require(:default)
3
+ require "active_record"
4
+ require "benchmark"
5
+
6
+ ActiveRecord::Base.default_timezone = :utc
7
+ ActiveRecord::Base.time_zone_aware_attributes = true
8
+ ActiveRecord::Base.establish_connection adapter: "sqlite3", database: ":memory:"
9
+
10
+ ActiveRecord::Migration.create_table :products do |t|
11
+ t.string :name
12
+ t.string :color
13
+ t.integer :store_id
14
+ end
15
+
16
+ class Product < ActiveRecord::Base
17
+ searchkick batch_size: 100
18
+
19
+ def search_data
20
+ {
21
+ name: name,
22
+ color: color,
23
+ store_id: store_id
24
+ }
25
+ end
26
+ end
27
+
28
+ Product.import ["name", "color", "store_id"], 20000.times.map { |i| ["Product #{i}", ["red", "blue"].sample, rand(10)] }
29
+
30
+ puts "Imported"
31
+
32
+ result = nil
33
+ report = nil
34
+ stats = nil
35
+
36
+ # p GetProcessMem.new.mb
37
+
38
+ time =
39
+ Benchmark.realtime do
40
+ # result = RubyProf.profile do
41
+ # report = MemoryProfiler.report do
42
+ # stats = AllocationStats.trace do
43
+ Product.reindex
44
+ # end
45
+ end
46
+
47
+ # p GetProcessMem.new.mb
48
+
49
+ puts time.round(1)
50
+ puts Product.searchkick_index.total_docs
51
+
52
+ if result
53
+ printer = RubyProf::GraphPrinter.new(result)
54
+ printer.print(STDOUT, min_percent: 5)
55
+ end
56
+
57
+ if report
58
+ puts report.pretty_print
59
+ end
60
+
61
+ if stats
62
+ puts result.allocations(alias_paths: true).group_by(:sourcefile, :class).to_text
63
+ end
@@ -7,6 +7,7 @@ module Searchkick
7
7
  def initialize(name, options = {})
8
8
  @name = name
9
9
  @options = options
10
+ @klass_document_type = {} # cache
10
11
  end
11
12
 
12
13
  def create(body = {})
@@ -270,10 +271,12 @@ module Searchkick
270
271
  end
271
272
 
272
273
  def klass_document_type(klass)
273
- if klass.respond_to?(:document_type)
274
- klass.document_type
275
- else
276
- klass.model_name.to_s.underscore
274
+ @klass_document_type[klass] ||= begin
275
+ if klass.respond_to?(:document_type)
276
+ klass.document_type
277
+ else
278
+ klass.model_name.to_s.underscore
279
+ end
277
280
  end
278
281
  end
279
282
 
@@ -296,42 +299,48 @@ module Searchkick
296
299
  id.is_a?(Numeric) ? id : id.to_s
297
300
  end
298
301
 
302
+ EXCLUDED_ATTRIBUTES = ["_id", "_type"]
303
+
299
304
  def search_data(record, method_name = nil)
300
305
  partial_reindex = !method_name.nil?
301
- source = record.send(method_name || :search_data)
302
306
  options = record.class.searchkick_options
303
307
 
304
- # stringify fields
305
308
  # remove _id since search_id is used instead
306
- source = source.each_with_object({}) { |(k, v), memo| memo[k.to_s] = v; memo }.except("_id", "_type")
309
+ source = record.send(method_name || :search_data).each_with_object({}) { |(k, v), memo| memo[k.to_s] = v; memo }.except(*EXCLUDED_ATTRIBUTES)
307
310
 
308
311
  # conversions
309
- Array(options[:conversions]).map(&:to_s).each do |conversions_field|
310
- if source[conversions_field]
311
- source[conversions_field] = source[conversions_field].map { |k, v| {query: k, count: v} }
312
+ if options[:conversions]
313
+ Array(options[:conversions]).map(&:to_s).each do |conversions_field|
314
+ if source[conversions_field]
315
+ source[conversions_field] = source[conversions_field].map { |k, v| {query: k, count: v} }
316
+ end
312
317
  end
313
318
  end
314
319
 
315
320
  # hack to prevent generator field doesn't exist error
316
- (options[:suggest] || []).map(&:to_s).each do |field|
317
- source[field] = nil if !source[field] && !partial_reindex
321
+ if options[:suggest]
322
+ options[:suggest].map(&:to_s).each do |field|
323
+ source[field] = nil if !source[field] && !partial_reindex
324
+ end
318
325
  end
319
326
 
320
327
  # locations
321
- (options[:locations] || []).map(&:to_s).each do |field|
322
- if source[field]
323
- if !source[field].is_a?(Hash) && (source[field].first.is_a?(Array) || source[field].first.is_a?(Hash))
324
- # multiple locations
325
- source[field] = source[field].map { |a| location_value(a) }
326
- else
327
- source[field] = location_value(source[field])
328
+ if options[:locations]
329
+ options[:locations].map(&:to_s).each do |field|
330
+ if source[field]
331
+ if !source[field].is_a?(Hash) && (source[field].first.is_a?(Array) || source[field].first.is_a?(Hash))
332
+ # multiple locations
333
+ source[field] = source[field].map { |a| location_value(a) }
334
+ else
335
+ source[field] = location_value(source[field])
336
+ end
328
337
  end
329
338
  end
330
339
  end
331
340
 
332
341
  cast_big_decimal(source)
333
342
 
334
- source.as_json
343
+ source
335
344
  end
336
345
 
337
346
  def location_value(value)
@@ -252,15 +252,7 @@ module Searchkick
252
252
  end
253
253
  end
254
254
 
255
- mapping[field] =
256
- if below50
257
- {
258
- type: "multi_field",
259
- fields: fields
260
- }
261
- elsif fields[field]
262
- fields[field].merge(fields: fields.except(field))
263
- end
255
+ mapping[field] = fields[field].merge(fields: fields.except(field))
264
256
  end
265
257
 
266
258
  (options[:locations] || []).map(&:to_s).each do |field|
@@ -307,15 +299,7 @@ module Searchkick
307
299
  end
308
300
 
309
301
  # http://www.elasticsearch.org/guide/reference/mapping/multi-field-type/
310
- multi_field =
311
- if below50
312
- {
313
- type: "multi_field",
314
- fields: dynamic_fields
315
- }
316
- else
317
- dynamic_fields["{name}"].merge(fields: dynamic_fields.except("{name}"))
318
- end
302
+ multi_field = dynamic_fields["{name}"].merge(fields: dynamic_fields.except("{name}"))
319
303
 
320
304
  all_enabled = !options[:searchable] || options[:searchable].to_a.include?("_all")
321
305
 
@@ -32,7 +32,22 @@ module Searchkick
32
32
 
33
33
  # sort
34
34
  hits.map do |hit|
35
- results[hit["_type"]][hit["_id"].to_s]
35
+ result = results[hit["_type"]][hit["_id"].to_s]
36
+ if result
37
+ unless result.respond_to?(:search_hit)
38
+ result.define_singleton_method(:search_hit) do
39
+ hit
40
+ end
41
+ end
42
+
43
+ if hit["highlight"] && !result.respond_to?(:search_highlights)
44
+ highlights = Hash[hit["highlight"].map { |k, v| [(options[:json] ? k : k.sub(/\.#{@options[:match_suffix]}\z/, "")).to_sym, v.first] }]
45
+ result.define_singleton_method(:search_highlights) do
46
+ highlights
47
+ end
48
+ end
49
+ end
50
+ result
36
51
  end.compact
37
52
  else
38
53
  hits.map do |hit|
@@ -187,21 +202,21 @@ module Searchkick
187
202
  end
188
203
  end
189
204
 
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) && [:preload, :eager_load].any? { |m| records.all.respond_to?(m) }
200
- # Nobrainer
201
- records.unscoped.where(:id.in => ids)
202
- else
203
- raise "Not sure how to load records"
204
- end
205
+ records =
206
+ if records.respond_to?(:primary_key)
207
+ # ActiveRecord
208
+ records.where(records.primary_key => ids) if records.primary_key
209
+ elsif records.respond_to?(:queryable)
210
+ # Mongoid 3+
211
+ records.queryable.for_ids(ids)
212
+ elsif records.respond_to?(:unscoped) && :id.respond_to?(:in)
213
+ # Nobrainer
214
+ records.unscoped.where(:id.in => ids)
215
+ end
216
+
217
+ raise Searchkick::Error, "Not sure how to load records" if !records
218
+
219
+ records
205
220
  end
206
221
 
207
222
  def base_field(k)
@@ -1,3 +1,3 @@
1
1
  module Searchkick
2
- VERSION = "2.0.0"
2
+ VERSION = "2.0.1"
3
3
  end
data/searchkick.gemspec CHANGED
@@ -15,7 +15,7 @@ Gem::Specification.new do |spec|
15
15
 
16
16
  spec.files = `git ls-files`.split($/)
17
17
  spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
18
- spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
18
+ spec.test_files = spec.files.grep(%r{^(test|spec|features|benchmark)/})
19
19
  spec.require_paths = ["lib"]
20
20
 
21
21
  spec.add_dependency "activemodel", ">= 4.1"
data/test/aggs_test.rb CHANGED
@@ -98,19 +98,22 @@ class AggsTest < Minitest::Test
98
98
 
99
99
  def test_aggs_group_by_date
100
100
  store [{name: "Old Product", created_at: 3.years.ago}]
101
- aggs = Product.search(
102
- "Product",
103
- aggs: {
104
- products_per_year: {
105
- date_histogram: {
106
- field: :created_at,
107
- interval: :year
108
- }
109
- }
110
- }
111
- ).aggs
112
-
113
- assert_equal 4, aggs["products_per_year"]["buckets"].size
101
+ products =
102
+ Product.search("Product", {
103
+ where: {
104
+ created_at: {lt: Time.now}
105
+ },
106
+ aggs: {
107
+ products_per_year: {
108
+ date_histogram: {
109
+ field: :created_at,
110
+ interval: :year
111
+ }
112
+ }
113
+ }
114
+ })
115
+
116
+ assert_equal 4, products.aggs["products_per_year"]["buckets"].size
114
117
  end
115
118
 
116
119
  protected
@@ -3,42 +3,42 @@ require_relative "test_helper"
3
3
  class HighlightTest < Minitest::Test
4
4
  def test_basic
5
5
  store_names ["Two Door Cinema Club"]
6
- assert_equal "Two Door <em>Cinema</em> Club", Product.search("cinema", fields: [:name], highlight: true).with_details.first[1][:highlight][:name]
6
+ assert_equal "Two Door <em>Cinema</em> Club", Product.search("cinema", fields: [:name], highlight: true).first.search_highlights[:name]
7
7
  end
8
8
 
9
9
  def test_tag
10
10
  store_names ["Two Door Cinema Club"]
11
- assert_equal "Two Door <strong>Cinema</strong> Club", Product.search("cinema", fields: [:name], highlight: {tag: "<strong>"}).with_details.first[1][:highlight][:name]
11
+ assert_equal "Two Door <strong>Cinema</strong> Club", Product.search("cinema", fields: [:name], highlight: {tag: "<strong>"}).first.search_highlights[:name]
12
12
  end
13
13
 
14
14
  def test_multiple_fields
15
15
  store [{name: "Two Door Cinema Club", color: "Cinema Orange"}]
16
- highlight = Product.search("cinema", fields: [:name, :color], highlight: true).with_details.first[1][:highlight]
17
- assert_equal "Two Door <em>Cinema</em> Club", highlight[:name]
18
- assert_equal "<em>Cinema</em> Orange", highlight[:color]
16
+ highlights = Product.search("cinema", fields: [:name, :color], highlight: true).first.search_highlights
17
+ assert_equal "Two Door <em>Cinema</em> Club", highlights[:name]
18
+ assert_equal "<em>Cinema</em> Orange", highlights[:color]
19
19
  end
20
20
 
21
21
  def test_fields
22
22
  store [{name: "Two Door Cinema Club", color: "Cinema Orange"}]
23
- highlight = Product.search("cinema", fields: [:name, :color], highlight: {fields: [:name]}).with_details.first[1][:highlight]
24
- assert_equal "Two Door <em>Cinema</em> Club", highlight[:name]
25
- assert_nil highlight[:color]
23
+ highlights = Product.search("cinema", fields: [:name, :color], highlight: {fields: [:name]}).first.search_highlights
24
+ assert_equal "Two Door <em>Cinema</em> Club", highlights[:name]
25
+ assert_nil highlights[:color]
26
26
  end
27
27
 
28
28
  def test_field_options
29
29
  store_names ["Two Door Cinema Club are a Northern Irish indie rock band"]
30
30
  fragment_size = ENV["MATCH"] == "word_start" ? 26 : 20
31
- assert_equal "Two Door <em>Cinema</em> Club are", Product.search("cinema", fields: [:name], highlight: {fields: {name: {fragment_size: fragment_size}}}).with_details.first[1][:highlight][:name]
31
+ assert_equal "Two Door <em>Cinema</em> Club are", Product.search("cinema", fields: [:name], highlight: {fields: {name: {fragment_size: fragment_size}}}).first.search_highlights[:name]
32
32
  end
33
33
 
34
34
  def test_multiple_words
35
35
  store_names ["Hello World Hello"]
36
- assert_equal "<em>Hello</em> World <em>Hello</em>", Product.search("hello", fields: [:name], highlight: true).with_details.first[1][:highlight][:name]
36
+ assert_equal "<em>Hello</em> World <em>Hello</em>", Product.search("hello", fields: [:name], highlight: true).first.search_highlights[:name]
37
37
  end
38
38
 
39
39
  def test_encoder
40
40
  store_names ["<b>Hello</b>"]
41
- assert_equal "&lt;b&gt;<em>Hello</em>&lt;&#x2F;b&gt;", Product.search("hello", fields: [:name], highlight: {encoder: "html"}, misspellings: false).with_details.first[1][:highlight][:name]
41
+ assert_equal "&lt;b&gt;<em>Hello</em>&lt;&#x2F;b&gt;", Product.search("hello", fields: [:name], highlight: {encoder: "html"}, misspellings: false).first.search_highlights[:name]
42
42
  end
43
43
 
44
44
  def test_body
@@ -58,6 +58,11 @@ class HighlightTest < Minitest::Test
58
58
  }
59
59
  }
60
60
  }
61
- assert_equal "Two Door <strong>Cinema</strong> Club", Product.search(body: body).with_details.first[1][:highlight][:"name.analyzed"]
61
+ assert_equal "Two Door <strong>Cinema</strong> Club", Product.search(body: body).first.search_highlights[:"name.analyzed"]
62
+ end
63
+
64
+ def test_legacy
65
+ store_names ["Two Door Cinema Club"]
66
+ assert_equal "Two Door <em>Cinema</em> Club", Product.search("cinema", fields: [:name], highlight: true).with_details.first[1][:highlight][:name]
62
67
  end
63
68
  end
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: 2.0.0
4
+ version: 2.0.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Andrew Kane
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2016-12-29 00:00:00.000000000 Z
11
+ date: 2016-12-31 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: activemodel
@@ -108,6 +108,8 @@ files:
108
108
  - LICENSE.txt
109
109
  - README.md
110
110
  - Rakefile
111
+ - benchmark/Gemfile
112
+ - benchmark/benchmark.rb
111
113
  - lib/searchkick.rb
112
114
  - lib/searchkick/index.rb
113
115
  - lib/searchkick/index_options.rb
@@ -190,6 +192,8 @@ summary: Searchkick learns what your users are looking for. As more people searc
190
192
  it gets smarter and the results get better. It’s friendly for developers - and magical
191
193
  for your users.
192
194
  test_files:
195
+ - benchmark/Gemfile
196
+ - benchmark/benchmark.rb
193
197
  - test/aggs_test.rb
194
198
  - test/autocomplete_test.rb
195
199
  - test/boost_test.rb