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 | 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 | ... | ... |
lib/searchkick.rb
lib/searchkick/model.rb
... | ... | @@ -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["RAILS_ENV"] || ENV["RACK_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 | ... | ... |
lib/searchkick/query.rb
... | ... | @@ -209,6 +209,21 @@ module Searchkick |
209 | 209 | } |
210 | 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 | 227 | if custom_filters.any? |
213 | 228 | payload = { |
214 | 229 | function_score: { |
... | ... | @@ -320,9 +335,21 @@ module Searchkick |
320 | 335 | payload[:highlight] = { |
321 | 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 | 353 | end |
327 | 354 | end |
328 | 355 | ... | ... |
lib/searchkick/reindex.rb
lib/searchkick/results.rb
... | ... | @@ -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 |
... | ... | @@ -143,5 +133,21 @@ module Searchkick |
143 | 133 | @response["hits"]["hits"] |
144 | 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 | 152 | end |
147 | 153 | end | ... | ... |
lib/searchkick/version.rb
test/boost_test.rb
... | ... | @@ -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 | ... | ... |
test/highlight_test.rb
... | ... | @@ -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] | ... | ... |