searchkick 0.8.3 → 0.8.5

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: 8b8b951d29bd106ae56b6ca7d2738574430f76ae
4
- data.tar.gz: a56ba366f77bfb6f0378e8f5711704bec31ef2cf
3
+ metadata.gz: c50ef339d04f201523ff1f41aad11277d7168dab
4
+ data.tar.gz: d7f79bf282327a781e23475f64b7530dfac6beab
5
5
  SHA512:
6
- metadata.gz: 7f532ecc2e7bb89ad4a705400c89151d7b5ab114efc7c8d4fa9771578de22a3e3ed5361863b6cea9cf56ea9ab6cf4b3379899168af619242844035197c1b294a
7
- data.tar.gz: 37c8d654358714fc2a5389190940fdf5f9714b60ded559364ec28ea6a639ed69570f80495e004e2d17c3354e8aada69b7f5e9cf062176f595d4e626024921b1c
6
+ metadata.gz: 61072c05e3aa82903b699e2062dc0a2944f7fcec20545709d2b83f1f0a760778adffbc7cf7002815c56c2ba6f9484de13f35134c484842ebbf56785d549a6883
7
+ data.tar.gz: 5c95dd8df5ead5050d2b24175e01b6ad197d7f430f92554f2330cb3b0976360ab4182d956e62ae734ce38b77985eb84f6c0e7e95a0d147d18f061e0964ee61e6
data/CHANGELOG.md CHANGED
@@ -1,3 +1,16 @@
1
+ ## 0.8.5
2
+
3
+ - Added support for Elasticsearch 1.4
4
+ - Added `unsearchable` option
5
+ - Added `select: true` option
6
+ - Added `body` option
7
+
8
+ ## 0.8.4
9
+
10
+ - Added `boost_by_distance`
11
+ - More flexible highlight options
12
+ - Better `env` logic
13
+
1
14
  ## 0.8.3
2
15
 
3
16
  - Added support for ActiveJob
data/README.md CHANGED
@@ -124,7 +124,7 @@ Boost important fields
124
124
  fields: ["title^10", "description"]
125
125
  ```
126
126
 
127
- Boost by the value of a field
127
+ Boost by the value of a field (field must be numeric)
128
128
 
129
129
  ```ruby
130
130
  boost_by: [:orders_count] # give popular documents a little boost
@@ -553,7 +553,7 @@ Highlight the search query in the results.
553
553
  bands = Band.search "cinema", fields: [:name], highlight: true
554
554
  ```
555
555
 
556
- **Note:** The `fields` option is required.
556
+ **Note:** The `fields` option is required, unless highlight options are given - see below.
557
557
 
558
558
  View the highlighted fields with:
559
559
 
@@ -569,6 +569,20 @@ To change the tag, use:
569
569
  Band.search "cinema", fields: [:name], highlight: {tag: "<strong>"}
570
570
  ```
571
571
 
572
+ To highlight and search different fields, use:
573
+
574
+ ```ruby
575
+ Band.search "cinema", fields: [:name], highlight: {fields: [:description]}
576
+ ```
577
+
578
+ Additional options, including fragment size, can be specified for each field:
579
+
580
+ ```ruby
581
+ Band.search "cinema", fields: [:name], highlight: {fields: {name: {fragment_size: 200}}}
582
+ ```
583
+
584
+ You can find available highlight options in the [Elasticsearch reference](http://www.elasticsearch.org/guide/en/elasticsearch/reference/current/search-request-highlighting.html#_highlighted_fragments).
585
+
572
586
  ### Similar Items
573
587
 
574
588
  Find similar items.
@@ -602,6 +616,20 @@ Bounded by a box
602
616
  City.search "san", where: {location: {top_left: [38, -123], bottom_right: [37, -122]}}
603
617
  ```
604
618
 
619
+ ### Boost By Distance
620
+
621
+ Boost results by distance - closer results are boosted more
622
+
623
+ ```ruby
624
+ City.search "san", boost_by_distance: {field: :location, origin: [37, -122]}
625
+ ```
626
+
627
+ Also supports [additional options](http://www.elasticsearch.org/guide/en/elasticsearch/reference/current/query-dsl-function-score-query.html#_decay_functions)
628
+
629
+ ```ruby
630
+ City.search "san", boost_by_distance: {field: :location, origin: [37, -122], function: :linear, scale: "30mi", decay: 0.5}
631
+ ```
632
+
605
633
  ## Inheritance
606
634
 
607
635
  Searchkick supports single table inheritance.
@@ -703,15 +731,20 @@ rake searchkick:reindex CLASS=Product
703
731
 
704
732
  ### Performance
705
733
 
706
- For the best performance, add [Patron](https://github.com/toland/patron) to your Gemfile.
734
+ For the best performance, add [Typhoeus](https://github.com/typhoeus/typhoeus) to your Gemfile.
707
735
 
708
736
  ```ruby
709
- gem 'patron'
737
+ gem 'typhoeus'
710
738
  ```
711
739
 
712
- Searchkick will automatically use it.
740
+ And create an initializer with:
741
+
742
+ ```ruby
743
+ require "typhoeus/adapters/faraday"
744
+ Ethon.logger = Logger.new("/dev/null")
745
+ ```
713
746
 
714
- **Note:** Patron is not available for Windows.
747
+ **Note:** Typhoeus is not available for Windows.
715
748
 
716
749
  ### Automatic Failover
717
750
 
@@ -723,10 +756,26 @@ Searchkick.client = Elasticsearch::Client.new(hosts: ["localhost:9200", "localho
723
756
 
724
757
  See [elasticsearch-transport](https://github.com/elasticsearch/elasticsearch-ruby/blob/master/elasticsearch-transport) for a complete list of options.
725
758
 
759
+ ### Lograge
760
+
761
+ Add the following to `config/environments/production.rb`:
762
+
763
+ ```ruby
764
+ config.lograge.custom_options = lambda do |event|
765
+ options = {}
766
+ options[:search] = event.payload[:searchkick_runtime] if event.payload[:searchkick_runtime].to_f > 0
767
+ options
768
+ end
769
+ ```
770
+
771
+ See [Production Rails](https://github.com/ankane/production_rails) for other good practices.
772
+
726
773
  ## Advanced
727
774
 
728
775
  Prefer to use the [Elasticsearch DSL](http://www.elasticsearch.org/guide/en/elasticsearch/reference/current/query-dsl-queries.html) but still want awesome features like zero-downtime reindexing?
729
776
 
777
+ ### Advanced Mapping
778
+
730
779
  Create a custom mapping:
731
780
 
732
781
  ```ruby
@@ -741,32 +790,35 @@ class Product < ActiveRecord::Base
741
790
  end
742
791
  ```
743
792
 
744
- And use the `query` option to search:
793
+ To keep the mappings and settings generated by Searchkick, use:
745
794
 
746
795
  ```ruby
747
- products = Product.search query: {match: {name: "milk"}}
796
+ class Product < ActiveRecord::Base
797
+ searchkick merge_mappings: true, mappings: {...}
798
+ end
748
799
  ```
749
800
 
750
- View the response with:
801
+ ### Advanced Search
802
+
803
+ And use the `body` option to search:
751
804
 
752
805
  ```ruby
753
- products.response
806
+ products = Product.search body: {match: {name: "milk"}}
754
807
  ```
755
808
 
756
- To keep the mappings and settings generated by Searchkick, use:
809
+ View the response with:
757
810
 
758
811
  ```ruby
759
- class Product < ActiveRecord::Base
760
- searchkick merge_mappings: true, mappings: {...}
761
- end
812
+ products.response
762
813
  ```
763
814
 
764
815
  To modify the query generated by Searchkick, use:
765
816
 
766
817
  ```ruby
767
- query = Product.search "2% Milk", execute: false
768
- query.body[:query] = {match_all: {}}
769
- products = query.execute
818
+ products =
819
+ Product.search "2% Milk" do |body|
820
+ body[:query] = {match_all: {}}
821
+ end
770
822
  ```
771
823
 
772
824
  ## Reference
@@ -850,6 +902,14 @@ class Product < ActiveRecord::Base
850
902
  end
851
903
  ```
852
904
 
905
+ Make fields unsearchable but include in the source
906
+
907
+ ```ruby
908
+ class Product < ActiveRecord::Base
909
+ searchkick unsearchable: [:color]
910
+ end
911
+ ```
912
+
853
913
  Reindex asynchronously
854
914
 
855
915
  ```ruby
@@ -5,15 +5,14 @@ module Searchkick
5
5
  raise "Only call searchkick once per model" if respond_to?(:searchkick_index)
6
6
 
7
7
  class_eval do
8
- cattr_reader :searchkick_options, :searchkick_env, :searchkick_klass
8
+ cattr_reader :searchkick_options, :searchkick_klass
9
9
 
10
10
  callbacks = options.has_key?(:callbacks) ? options[:callbacks] : true
11
11
 
12
12
  class_variable_set :@@searchkick_options, options.dup
13
- class_variable_set :@@searchkick_env, ENV["RACK_ENV"] || ENV["RAILS_ENV"] || "development"
14
13
  class_variable_set :@@searchkick_klass, self
15
14
  class_variable_set :@@searchkick_callbacks, callbacks
16
- class_variable_set :@@searchkick_index, options[:index_name] || [options[:index_prefix], model_name.plural, searchkick_env].compact.join("_")
15
+ class_variable_set :@@searchkick_index, options[:index_name] || [options[:index_prefix], model_name.plural, Searchkick.env].compact.join("_")
17
16
 
18
17
  def self.searchkick_index
19
18
  index = class_variable_get :@@searchkick_index
@@ -21,8 +20,11 @@ module Searchkick
21
20
  Searchkick::Index.new(index)
22
21
  end
23
22
 
24
- define_singleton_method(Searchkick.search_method_name) do |term = nil, options={}|
23
+ define_singleton_method(Searchkick.search_method_name) do |term = nil, options={}, &block|
25
24
  query = Searchkick::Query.new(self, term, options)
25
+ if block
26
+ block.call(query.body)
27
+ end
26
28
  if options[:execute] == false
27
29
  query
28
30
  else
@@ -16,6 +16,7 @@ module Searchkick
16
16
  @options = options
17
17
 
18
18
  below12 = Gem::Version.new(Searchkick.server_version) < Gem::Version.new("1.2")
19
+ below14 = Gem::Version.new(Searchkick.server_version) < Gem::Version.new("1.4")
19
20
 
20
21
  boost_fields = {}
21
22
  fields =
@@ -56,6 +57,7 @@ module Searchkick
56
57
  all = term == "*"
57
58
  facet_limits = {}
58
59
 
60
+ options[:json] ||= options[:body]
59
61
  if options[:json]
60
62
  payload = options[:json]
61
63
  else
@@ -209,6 +211,21 @@ module Searchkick
209
211
  }
210
212
  end
211
213
 
214
+ boost_by_distance = options[:boost_by_distance]
215
+ if boost_by_distance
216
+ boost_by_distance = {function: :gauss, scale: "5mi"}.merge(boost_by_distance)
217
+ if !boost_by_distance[:field] or !boost_by_distance[:origin]
218
+ raise ArgumentError, "boost_by_distance requires :field and :origin"
219
+ end
220
+ function_params = boost_by_distance.select{|k,v| [:origin, :scale, :offset, :decay].include?(k) }
221
+ function_params[:origin] = function_params[:origin].reverse
222
+ custom_filters << {
223
+ boost_by_distance[:function] => {
224
+ boost_by_distance[:field] => function_params
225
+ }
226
+ }
227
+ end
228
+
212
229
  if custom_filters.any?
213
230
  payload = {
214
231
  function_score: {
@@ -264,7 +281,7 @@ module Searchkick
264
281
  payload[:facets][field] = {
265
282
  terms_stats: {
266
283
  key_field: field,
267
- value_script: "doc.score",
284
+ value_script: below14 ? "doc.score" : "_score",
268
285
  size: size
269
286
  }
270
287
  }
@@ -320,18 +337,30 @@ module Searchkick
320
337
  payload[:highlight] = {
321
338
  fields: Hash[ fields.map{|f| [f, {}] } ]
322
339
  }
323
- if options[:highlight].is_a?(Hash) and tag = options[:highlight][:tag]
324
- payload[:highlight][:pre_tags] = [tag]
325
- payload[:highlight][:post_tags] = [tag.to_s.gsub(/\A</, "</")]
340
+
341
+ if options[:highlight].is_a?(Hash)
342
+ if tag = options[:highlight][:tag]
343
+ payload[:highlight][:pre_tags] = [tag]
344
+ payload[:highlight][:post_tags] = [tag.to_s.gsub(/\A</, "</")]
345
+ end
346
+
347
+ highlight_fields = options[:highlight][:fields]
348
+ if highlight_fields
349
+ payload[:highlight][:fields] = {}
350
+
351
+ highlight_fields.each do |name, opts|
352
+ payload[:highlight][:fields]["#{name}.analyzed"] = opts || {}
353
+ end
354
+ end
326
355
  end
327
356
  end
328
357
 
329
358
  # An empty array will cause only the _id and _type for each hit to be returned
330
359
  # http://www.elasticsearch.org/guide/reference/api/search/fields/
331
- if load
360
+ if options[:select]
361
+ payload[:fields] = options[:select] if options[:select] != true
362
+ elsif load
332
363
  payload[:fields] = []
333
- elsif options[:select]
334
- payload[:fields] = options[:select]
335
364
  end
336
365
 
337
366
  if options[:type] or klass != searchkick_klass
@@ -7,7 +7,7 @@ module Searchkick
7
7
  skip_import = options[:import] == false
8
8
 
9
9
  alias_name = searchkick_index.name
10
- new_name = alias_name + "_" + Time.now.strftime("%Y%m%d%H%M%S%L")
10
+ new_name = "#{alias_name}_#{Time.now.strftime('%Y%m%d%H%M%S%L')}"
11
11
  index = Searchkick::Index.new(new_name)
12
12
 
13
13
  clean_indices
@@ -40,7 +40,7 @@ module Searchkick
40
40
  # remove old indices that start w/ index_name
41
41
  def clean_indices
42
42
  all_indices = Searchkick.client.indices.get_aliases
43
- indices = all_indices.select{|k, v| v["aliases"].empty? && k =~ /\A#{Regexp.escape(searchkick_index.name)}_\d{14,17}\z/ }.keys
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
44
  indices.each do |index|
45
45
  Searchkick::Index.new(index).delete
46
46
  end
@@ -203,7 +203,7 @@ module Searchkick
203
203
  }
204
204
  }
205
205
 
206
- if searchkick_env == "test"
206
+ if Searchkick.env == "test"
207
207
  settings.merge!(number_of_shards: 1, number_of_replicas: 0)
208
208
  end
209
209
 
@@ -288,12 +288,19 @@ module Searchkick
288
288
  mapping[field] = field_mapping
289
289
  end
290
290
 
291
- (options[:locations] || []).each do |field|
291
+ (options[:locations] || []).map(&:to_s).each do |field|
292
292
  mapping[field] = {
293
293
  type: "geo_point"
294
294
  }
295
295
  end
296
296
 
297
+ (options[:unsearchable] || []).map(&:to_s).each do |field|
298
+ mapping[field] = {
299
+ type: "string",
300
+ index: "no"
301
+ }
302
+ end
303
+
297
304
  mappings = {
298
305
  _default_: {
299
306
  properties: mapping,
@@ -26,17 +26,7 @@ module Searchkick
26
26
  if options[:includes]
27
27
  records = records.includes(options[:includes])
28
28
  end
29
- results[type] =
30
- if records.respond_to?(:primary_key) and records.primary_key
31
- # ActiveRecord
32
- records.where(records.primary_key => grouped_hits.map{|hit| hit["_id"] }).to_a
33
- elsif records.respond_to?(:all) and records.all.respond_to?(:for_ids)
34
- # Mongoid 2
35
- records.all.for_ids(grouped_hits.map{|hit| hit["_id"] }).to_a
36
- else
37
- # Mongoid 3+
38
- records.queryable.for_ids(grouped_hits.map{|hit| hit["_id"] }).to_a
39
- end
29
+ results[type] = results_query(records, grouped_hits)
40
30
  end
41
31
 
42
32
  # sort
@@ -137,11 +127,25 @@ module Searchkick
137
127
  next_page.nil?
138
128
  end
139
129
 
140
- protected
141
-
142
130
  def hits
143
131
  @response["hits"]["hits"]
144
132
  end
145
133
 
134
+ private
135
+
136
+ def results_query(records, grouped_hits)
137
+ if records.respond_to?(:primary_key) and records.primary_key
138
+ # ActiveRecord
139
+ records.where(records.primary_key => grouped_hits.map{|hit| hit["_id"] }).to_a
140
+ elsif records.respond_to?(:all) and records.all.respond_to?(:for_ids)
141
+ # Mongoid 2
142
+ records.all.for_ids(grouped_hits.map{|hit| hit["_id"] }).to_a
143
+ elsif records.respond_to?(:queryable)
144
+ # Mongoid 3+
145
+ records.queryable.for_ids(grouped_hits.map{|hit| hit["_id"] }).to_a
146
+ else
147
+ raise "Not sure how to load records"
148
+ end
149
+ end
146
150
  end
147
151
  end
@@ -1,3 +1,3 @@
1
1
  module Searchkick
2
- VERSION = "0.8.3"
2
+ VERSION = "0.8.5"
3
3
  end
data/lib/searchkick.rb CHANGED
@@ -63,6 +63,10 @@ module Searchkick
63
63
  def self.callbacks?
64
64
  callbacks
65
65
  end
66
+
67
+ def self.env
68
+ @env ||= ENV["RAILS_ENV"] || ENV["RACK_ENV"] || "development"
69
+ end
66
70
  end
67
71
 
68
72
  # TODO find better ActiveModel hook
data/test/boost_test.rb CHANGED
@@ -104,4 +104,13 @@ class TestBoost < Minitest::Test
104
104
  assert_first "tomato", "Tomato B", boost_where: {user_ids: {value: 2, factor: 10}}
105
105
  end
106
106
 
107
+ def test_boost_by_distance
108
+ store [
109
+ {name: "San Francisco", latitude: 37.7833, longitude: -122.4167},
110
+ {name: "San Antonio", latitude: 29.4167, longitude: -98.5000},
111
+ {name: "San Marino", latitude: 43.9333, longitude: 12.4667}
112
+ ]
113
+ assert_order "san", ["San Francisco", "San Antonio", "San Marino"], boost_by_distance: {field: :location, origin: [37, -122], scale: "1000mi"}
114
+ end
115
+
107
116
  end
@@ -19,6 +19,18 @@ class TestHighlight < Minitest::Test
19
19
  assert_equal "<em>Cinema</em> Orange", highlight[:color]
20
20
  end
21
21
 
22
+ def test_fields
23
+ store [{name: "Two Door Cinema Club", color: "Cinema Orange"}]
24
+ highlight = Product.search("cinema", fields: [:name, :color], highlight: {fields: [:name]}).with_details.first[1][:highlight]
25
+ assert_equal "Two Door <em>Cinema</em> Club", highlight[:name]
26
+ assert_equal nil, highlight[:color]
27
+ end
28
+
29
+ def test_field_options
30
+ store_names ["Two Door Cinema Club are a Northern Irish indie rock band"]
31
+ assert_equal "Two Door <em>Cinema</em> Club are", Product.search("cinema", fields: [:name], highlight: {fields: {name: {fragment_size: 20}}}).with_details.first[1][:highlight][:name]
32
+ end
33
+
22
34
  def test_multiple_words
23
35
  store_names ["Hello World Hello"]
24
36
  assert_equal "<em>Hello</em> World <em>Hello</em>", Product.search("hello", fields: [:name], highlight: true).with_details.first[1][:highlight][:name]
data/test/index_test.rb CHANGED
@@ -41,6 +41,21 @@ class TestIndex < Minitest::Test
41
41
  assert_equal ["Dollar Tree"], Store.search(json: {query: {match: {name: "Dollar Tree"}}}, load: false).map(&:name)
42
42
  end
43
43
 
44
+ def test_body
45
+ store_names ["Dollar Tree"], Store
46
+ assert_equal [], Store.search(query: {match: {name: "dollar"}}).map(&:name)
47
+ assert_equal ["Dollar Tree"], Store.search(body: {query: {match: {name: "Dollar Tree"}}}, load: false).map(&:name)
48
+ end
49
+
50
+ def test_block
51
+ store_names ["Dollar Tree"]
52
+ products =
53
+ Product.search "boom" do |body|
54
+ body[:query] = {match_all: {}}
55
+ end
56
+ assert_equal ["Dollar Tree"], products.map(&:name)
57
+ end
58
+
44
59
  def test_tokens
45
60
  assert_equal ["dollar", "dollartre", "tree"], Product.searchkick_index.tokens("Dollar Tree")
46
61
  end
data/test/match_test.rb CHANGED
@@ -153,4 +153,11 @@ class TestMatch < Minitest::Test
153
153
  assert_search "to be", ["to be or not to be"]
154
154
  end
155
155
 
156
+ def test_unsearchable
157
+ store [
158
+ {name: "Unsearchable", description: "Almond"}
159
+ ]
160
+ assert_search "almond", []
161
+ end
162
+
156
163
  end
data/test/sql_test.rb CHANGED
@@ -298,6 +298,13 @@ class TestSql < Minitest::Test
298
298
  assert_equal [1, 2], result.user_ids
299
299
  end
300
300
 
301
+ def test_select_all
302
+ store [{name: "Product A", user_ids: [1, 2]}]
303
+ hit = Product.search("product", select: true).hits.first
304
+ assert_equal hit["_source"]["name"], "Product A"
305
+ assert_equal hit["_source"]["user_ids"], [1, 2]
306
+ end
307
+
301
308
  def test_nested_object
302
309
  aisle = {"id" => 1, "name" => "Frozen"}
303
310
  store [{name: "Product A", aisle: aisle}]
data/test/test_helper.rb CHANGED
@@ -53,6 +53,7 @@ if defined?(Mongoid)
53
53
  field :color
54
54
  field :latitude, type: BigDecimal
55
55
  field :longitude, type: BigDecimal
56
+ field :description
56
57
  end
57
58
 
58
59
  class Store
@@ -95,6 +96,7 @@ else
95
96
  t.string :color
96
97
  t.decimal :latitude, precision: 10, scale: 7
97
98
  t.decimal :longitude, precision: 10, scale: 7
99
+ t.text :description
98
100
  t.timestamps
99
101
  end
100
102
 
@@ -146,7 +148,8 @@ class Product
146
148
  word_start: [:name],
147
149
  word_middle: [:name],
148
150
  word_end: [:name],
149
- highlight: [:name]
151
+ highlight: [:name],
152
+ unsearchable: [:description]
150
153
 
151
154
  attr_accessor :conversions, :user_ids, :aisle
152
155
 
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.3
4
+ version: 0.8.5
5
5
  platform: ruby
6
6
  authors:
7
7
  - Andrew Kane
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2014-09-21 00:00:00.000000000 Z
11
+ date: 2014-11-11 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: activemodel
@@ -188,4 +188,3 @@ test_files:
188
188
  - test/suggest_test.rb
189
189
  - test/synonyms_test.rb
190
190
  - test/test_helper.rb
191
- has_rdoc: