Commit da2b7e143ad480faf2d2cbd5b1efb9bf661bf734
Exists in
master
and in
21 other branches
Merge branch 'master' of github.com:ankane/searchkick into include-search-data
Showing
10 changed files
with
121 additions
and
25 deletions
Show diff stats
CHANGELOG.md
README.md
@@ -124,7 +124,7 @@ Boost important fields | @@ -124,7 +124,7 @@ Boost important fields | ||
124 | fields: ["title^10", "description"] | 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 | ```ruby | 129 | ```ruby |
130 | boost_by: [:orders_count] # give popular documents a little boost | 130 | boost_by: [:orders_count] # give popular documents a little boost |
@@ -553,7 +553,7 @@ Highlight the search query in the results. | @@ -553,7 +553,7 @@ Highlight the search query in the results. | ||
553 | bands = Band.search "cinema", fields: [:name], highlight: true | 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 | View the highlighted fields with: | 558 | View the highlighted fields with: |
559 | 559 | ||
@@ -569,6 +569,20 @@ To change the tag, use: | @@ -569,6 +569,20 @@ To change the tag, use: | ||
569 | Band.search "cinema", fields: [:name], highlight: {tag: "<strong>"} | 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 | ### Similar Items | 586 | ### Similar Items |
573 | 587 | ||
574 | Find similar items. | 588 | Find similar items. |
@@ -602,6 +616,20 @@ Bounded by a box | @@ -602,6 +616,20 @@ Bounded by a box | ||
602 | City.search "san", where: {location: {top_left: [38, -123], bottom_right: [37, -122]}} | 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 | ## Inheritance | 633 | ## Inheritance |
606 | 634 | ||
607 | Searchkick supports single table inheritance. | 635 | Searchkick supports single table inheritance. |
@@ -703,15 +731,20 @@ rake searchkick:reindex CLASS=Product | @@ -703,15 +731,20 @@ rake searchkick:reindex CLASS=Product | ||
703 | 731 | ||
704 | ### Performance | 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 | ```ruby | 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 | ### Automatic Failover | 749 | ### Automatic Failover |
717 | 750 |
lib/searchkick.rb
@@ -63,6 +63,10 @@ module Searchkick | @@ -63,6 +63,10 @@ module Searchkick | ||
63 | def self.callbacks? | 63 | def self.callbacks? |
64 | callbacks | 64 | callbacks |
65 | end | 65 | end |
66 | + | ||
67 | + def self.env | ||
68 | + @env ||= ENV["RAILS_ENV"] || ENV["RACK_ENV"] || "development" | ||
69 | + end | ||
66 | end | 70 | end |
67 | 71 | ||
68 | # TODO find better ActiveModel hook | 72 | # TODO find better ActiveModel hook |
lib/searchkick/model.rb
@@ -5,15 +5,14 @@ module Searchkick | @@ -5,15 +5,14 @@ module Searchkick | ||
5 | raise "Only call searchkick once per model" if respond_to?(:searchkick_index) | 5 | raise "Only call searchkick once per model" if respond_to?(:searchkick_index) |
6 | 6 | ||
7 | class_eval do | 7 | class_eval do |
8 | - cattr_reader :searchkick_options, :searchkick_env, :searchkick_klass | 8 | + cattr_reader :searchkick_options, :searchkick_klass |
9 | 9 | ||
10 | callbacks = options.has_key?(:callbacks) ? options[:callbacks] : true | 10 | callbacks = options.has_key?(:callbacks) ? options[:callbacks] : true |
11 | 11 | ||
12 | class_variable_set :@@searchkick_options, options.dup | 12 | class_variable_set :@@searchkick_options, options.dup |
13 | - class_variable_set :@@searchkick_env, ENV["RAILS_ENV"] || ENV["RACK_ENV"] || "development" | ||
14 | class_variable_set :@@searchkick_klass, self | 13 | class_variable_set :@@searchkick_klass, self |
15 | class_variable_set :@@searchkick_callbacks, callbacks | 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 | def self.searchkick_index | 17 | def self.searchkick_index |
19 | index = class_variable_get :@@searchkick_index | 18 | index = class_variable_get :@@searchkick_index |
lib/searchkick/query.rb
@@ -209,6 +209,21 @@ module Searchkick | @@ -209,6 +209,21 @@ module Searchkick | ||
209 | } | 209 | } |
210 | end | 210 | end |
211 | 211 | ||
212 | + boost_by_distance = options[:boost_by_distance] | ||
213 | + if boost_by_distance | ||
214 | + boost_by_distance = {function: :gauss, scale: "5mi"}.merge(boost_by_distance) | ||
215 | + if !boost_by_distance[:field] or !boost_by_distance[:origin] | ||
216 | + raise ArgumentError, "boost_by_distance requires :field and :origin" | ||
217 | + end | ||
218 | + function_params = boost_by_distance.select{|k,v| [:origin, :scale, :offset, :decay].include?(k) } | ||
219 | + function_params[:origin] = function_params[:origin].reverse | ||
220 | + custom_filters << { | ||
221 | + boost_by_distance[:function] => { | ||
222 | + boost_by_distance[:field] => function_params | ||
223 | + } | ||
224 | + } | ||
225 | + end | ||
226 | + | ||
212 | if custom_filters.any? | 227 | if custom_filters.any? |
213 | payload = { | 228 | payload = { |
214 | function_score: { | 229 | function_score: { |
@@ -320,9 +335,21 @@ module Searchkick | @@ -320,9 +335,21 @@ module Searchkick | ||
320 | payload[:highlight] = { | 335 | payload[:highlight] = { |
321 | fields: Hash[ fields.map{|f| [f, {}] } ] | 336 | fields: Hash[ fields.map{|f| [f, {}] } ] |
322 | } | 337 | } |
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</, "</")] | 338 | + |
339 | + if options[:highlight].is_a?(Hash) | ||
340 | + if tag = options[:highlight][:tag] | ||
341 | + payload[:highlight][:pre_tags] = [tag] | ||
342 | + payload[:highlight][:post_tags] = [tag.to_s.gsub(/\A</, "</")] | ||
343 | + end | ||
344 | + | ||
345 | + highlight_fields = options[:highlight][:fields] | ||
346 | + if highlight_fields | ||
347 | + payload[:highlight][:fields] = {} | ||
348 | + | ||
349 | + highlight_fields.each do |name, opts| | ||
350 | + payload[:highlight][:fields]["#{name}.analyzed"] = opts || {} | ||
351 | + end | ||
352 | + end | ||
326 | end | 353 | end |
327 | end | 354 | end |
328 | 355 |
lib/searchkick/reindex.rb
@@ -203,7 +203,7 @@ module Searchkick | @@ -203,7 +203,7 @@ module Searchkick | ||
203 | } | 203 | } |
204 | } | 204 | } |
205 | 205 | ||
206 | - if searchkick_env == "test" | 206 | + if Searchkick.env == "test" |
207 | settings.merge!(number_of_shards: 1, number_of_replicas: 0) | 207 | settings.merge!(number_of_shards: 1, number_of_replicas: 0) |
208 | end | 208 | end |
209 | 209 |
lib/searchkick/results.rb
@@ -26,17 +26,7 @@ module Searchkick | @@ -26,17 +26,7 @@ module Searchkick | ||
26 | if options[:includes] | 26 | if options[:includes] |
27 | records = records.includes(options[:includes]) | 27 | records = records.includes(options[:includes]) |
28 | end | 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 | end | 30 | end |
41 | 31 | ||
42 | # sort | 32 | # sort |
@@ -143,5 +133,21 @@ module Searchkick | @@ -143,5 +133,21 @@ module Searchkick | ||
143 | @response["hits"]["hits"] | 133 | @response["hits"]["hits"] |
144 | end | 134 | end |
145 | 135 | ||
136 | + private | ||
137 | + | ||
138 | + def results_query(records, grouped_hits) | ||
139 | + if records.respond_to?(:primary_key) and records.primary_key | ||
140 | + # ActiveRecord | ||
141 | + records.where(records.primary_key => grouped_hits.map{|hit| hit["_id"] }).to_a | ||
142 | + elsif records.respond_to?(:all) and records.all.respond_to?(:for_ids) | ||
143 | + # Mongoid 2 | ||
144 | + records.all.for_ids(grouped_hits.map{|hit| hit["_id"] }).to_a | ||
145 | + elsif records.respond_to?(:queryable) | ||
146 | + # Mongoid 3+ | ||
147 | + records.queryable.for_ids(grouped_hits.map{|hit| hit["_id"] }).to_a | ||
148 | + else | ||
149 | + raise "Not sure how to load records" | ||
150 | + end | ||
151 | + end | ||
146 | end | 152 | end |
147 | end | 153 | end |
lib/searchkick/version.rb
test/boost_test.rb
@@ -104,4 +104,13 @@ class TestBoost < Minitest::Test | @@ -104,4 +104,13 @@ class TestBoost < Minitest::Test | ||
104 | assert_first "tomato", "Tomato B", boost_where: {user_ids: {value: 2, factor: 10}} | 104 | assert_first "tomato", "Tomato B", boost_where: {user_ids: {value: 2, factor: 10}} |
105 | end | 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 | end | 116 | end |
test/highlight_test.rb
@@ -19,6 +19,18 @@ class TestHighlight < Minitest::Test | @@ -19,6 +19,18 @@ class TestHighlight < Minitest::Test | ||
19 | assert_equal "<em>Cinema</em> Orange", highlight[:color] | 19 | assert_equal "<em>Cinema</em> Orange", highlight[:color] |
20 | end | 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 | def test_multiple_words | 34 | def test_multiple_words |
23 | store_names ["Hello World Hello"] | 35 | store_names ["Hello World Hello"] |
24 | 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).with_details.first[1][:highlight][:name] |