searchkick 2.0.0 → 2.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml 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