Commit 29ab35b496ce3ec09572a600397de945492cceb6

Authored by Oskar Szrajer
2 parents 1569a146 71dac198

Merge remote-tracking branch 'upstream/master' into adv_facets

  1 +## 0.7.1
  2 +
  3 +- Fixed huge issue w/ zero-downtime reindexing on 0.90
  4 +
  5 +## 0.7.0
  6 +
  7 +- Added support for Elasticsearch 1.1
  8 +- Dropped support for Elasticsearch below 0.90.4 (unfortunate side effect of above)
  9 +
  10 +## 0.6.3
  11 +
  12 +- Removed patron since no support for Windows
  13 +- Added error if `searchkick` is called multiple times
  14 +
  15 +## 0.6.2
  16 +
  17 +- Added logging
  18 +- Fixed index_name option
  19 +- Added ability to use proc as the index name
  20 +
  21 +## 0.6.1
  22 +
  23 +- Fixed huge issue w/ zero-downtime reindexing on 0.90 and elasticsearch-ruby 1.0
  24 +- Restore load: false behavior
  25 +- Restore total_entries method
  26 +
  27 +## 0.6.0
  28 +
  29 +- Moved to elasticsearch-ruby
  30 +- Added support for modifying the query and viewing the response
  31 +- Added support for page_entries_info method
  32 +
1 ## 0.5.3 33 ## 0.5.3
2 34
3 - Fixed bug w/ word_* queries 35 - Fixed bug w/ word_* queries
@@ -147,7 +147,7 @@ Product.search "fresh honey" # fresh AND honey @@ -147,7 +147,7 @@ Product.search "fresh honey" # fresh AND honey
147 To change this, use: 147 To change this, use:
148 148
149 ```ruby 149 ```ruby
150 -Product.search "fresh honey", partial: true # fresh OR honey 150 +Product.search "fresh honey", operator: "or" # fresh OR honey
151 ``` 151 ```
152 152
153 By default, results must match the entire word - `back` will not match `backpack`. You can change this behavior with: 153 By default, results must match the entire word - `back` will not match `backpack`. You can change this behavior with:
@@ -504,7 +504,7 @@ And to search, use: @@ -504,7 +504,7 @@ And to search, use:
504 ```ruby 504 ```ruby
505 Animal.search "*" # all animals 505 Animal.search "*" # all animals
506 Dog.search "*" # just dogs 506 Dog.search "*" # just dogs
507 -Animal.search "*", type: [Dog, Cat] # just cats and dogs [master] 507 +Animal.search "*", type: [Dog, Cat] # just cats and dogs
508 ``` 508 ```
509 509
510 **Note:** The `suggest` option retrieves suggestions from the parent at the moment. 510 **Note:** The `suggest` option retrieves suggestions from the parent at the moment.
@@ -576,7 +576,13 @@ end @@ -576,7 +576,13 @@ end
576 And use the `query` option to search: 576 And use the `query` option to search:
577 577
578 ```ruby 578 ```ruby
579 -Product.search query: {match: {name: "milk"}} 579 +products = Product.search query: {match: {name: "milk"}}
  580 +```
  581 +
  582 +View the response with:
  583 +
  584 +```ruby
  585 +products.response
580 ``` 586 ```
581 587
582 To keep the mappings and settings generated by Searchkick, use: 588 To keep the mappings and settings generated by Searchkick, use:
@@ -587,9 +593,7 @@ class Product < ActiveRecord::Base @@ -587,9 +593,7 @@ class Product < ActiveRecord::Base
587 end 593 end
588 ``` 594 ```
589 595
590 -## Experimental [master]  
591 -  
592 -Modify the query generated by Searchkick. 596 +To modify the query generated by Searchkick, use:
593 597
594 ```ruby 598 ```ruby
595 query = Product.search "2% Milk", execute: false 599 query = Product.search "2% Milk", execute: false
@@ -597,15 +601,9 @@ query.body[:query] = {match_all: {}} @@ -597,15 +601,9 @@ query.body[:query] = {match_all: {}}
597 products = query.execute 601 products = query.execute
598 ``` 602 ```
599 603
600 -View the response with:  
601 -  
602 -```ruby  
603 -products.response  
604 -```  
605 -  
606 ## Reference 604 ## Reference
607 605
608 -Searchkick requires Elasticsearch `0.90.0` or higher. 606 +Searchkick requires Elasticsearch `0.90.4` or higher.
609 607
610 Reindex one record 608 Reindex one record
611 609
@@ -754,6 +752,10 @@ rake searchkick:reindex:all @@ -754,6 +752,10 @@ rake searchkick:reindex:all
754 752
755 4. Once it finishes, replace search calls w/ searchkick calls 753 4. Once it finishes, replace search calls w/ searchkick calls
756 754
  755 +## Note about 0.6.0 and 0.7.0
  756 +
  757 +If running Searchkick `0.6.0` or `0.7.0` and Elasticsearch `0.90`, we recommend upgrading to Searchkick `0.6.1` or `0.7.1` to fix an issue that causes downtime when reindexing.
  758 +
757 ## Elasticsearch Gotchas 759 ## Elasticsearch Gotchas
758 760
759 ### Inconsistent Scores 761 ### Inconsistent Scores
@@ -770,7 +772,7 @@ For convenience, this is set by default in the test environment. @@ -770,7 +772,7 @@ For convenience, this is set by default in the test environment.
770 772
771 ## Thanks 773 ## Thanks
772 774
773 -Thanks to Karel Minarik for [Tire](https://github.com/karmi/tire), Jaroslav Kalistsuk for [zero downtime reindexing](https://gist.github.com/jarosan/3124884), and Alex Leschenko for [Elasticsearch autocomplete](https://github.com/leschenko/elasticsearch_autocomplete). 775 +Thanks to Karel Minarik for [Elasticsearch Ruby](https://github.com/elasticsearch/elasticsearch-ruby) and [Tire](https://github.com/karmi/tire), Jaroslav Kalistsuk for [zero downtime reindexing](https://gist.github.com/jarosan/3124884), and Alex Leschenko for [Elasticsearch autocomplete](https://github.com/leschenko/elasticsearch_autocomplete).
774 776
775 ## Roadmap 777 ## Roadmap
776 778
@@ -797,7 +799,5 @@ Everyone is encouraged to help improve this project. Here are a few ways you can @@ -797,7 +799,5 @@ Everyone is encouraged to help improve this project. Here are a few ways you can
797 To get started with development and testing: 799 To get started with development and testing:
798 800
799 1. Clone the repo 801 1. Clone the repo
800 -2. Install PostgreSQL and create a database called `searchkick_test` (`psql -d postgres -c "create database searchkick_test"`)  
801 -3. Install Elasticsearch  
802 -4. `bundle`  
803 -5. `rake test` 802 +2. `bundle`
  803 +3. `rake test`
gemfiles/mongoid4.gemfile
@@ -3,4 +3,4 @@ source 'https://rubygems.org' @@ -3,4 +3,4 @@ source 'https://rubygems.org'
3 # Specify your gem's dependencies in searchkick.gemspec 3 # Specify your gem's dependencies in searchkick.gemspec
4 gemspec path: "../" 4 gemspec path: "../"
5 5
6 -gem "mongoid", github: "mongoid/mongoid" 6 +gem "mongoid", "4.0.0.beta1"
lib/searchkick.rb
1 -require "tire" 1 +require "active_model"
  2 +require "elasticsearch"
  3 +require "hashie"
2 require "searchkick/version" 4 require "searchkick/version"
  5 +require "searchkick/index"
3 require "searchkick/reindex" 6 require "searchkick/reindex"
4 require "searchkick/results" 7 require "searchkick/results"
5 require "searchkick/query" 8 require "searchkick/query"
@@ -7,9 +10,14 @@ require "searchkick/search" @@ -7,9 +10,14 @@ require "searchkick/search"
7 require "searchkick/similar" 10 require "searchkick/similar"
8 require "searchkick/model" 11 require "searchkick/model"
9 require "searchkick/tasks" 12 require "searchkick/tasks"
10 -require "searchkick/logger" if defined?(Rails) 13 +require "searchkick/logging" if defined?(Rails)
11 14
12 module Searchkick 15 module Searchkick
  16 +
  17 + def self.client
  18 + @client ||= Elasticsearch::Client.new(url: ENV["ELASTICSEARCH_URL"])
  19 + end
  20 +
13 @callbacks = true 21 @callbacks = true
14 22
15 def self.enable_callbacks 23 def self.enable_callbacks
lib/searchkick/index.rb 0 โ†’ 100644
@@ -0,0 +1,137 @@ @@ -0,0 +1,137 @@
  1 +module Searchkick
  2 + class Index
  3 + attr_reader :name
  4 +
  5 + def initialize(name)
  6 + @name = name
  7 + end
  8 +
  9 + def create(options = {})
  10 + client.indices.create index: name, body: options
  11 + end
  12 +
  13 + def delete
  14 + client.indices.delete index: name
  15 + end
  16 +
  17 + def exists?
  18 + client.indices.exists index: name
  19 + end
  20 +
  21 + def refresh
  22 + client.indices.refresh index: name
  23 + end
  24 +
  25 + def store(record)
  26 + client.index(
  27 + index: name,
  28 + type: document_type(record),
  29 + id: record.id,
  30 + body: search_data(record)
  31 + )
  32 + end
  33 +
  34 + def remove(record)
  35 + client.delete(
  36 + index: name,
  37 + type: document_type(record),
  38 + id: record.id
  39 + )
  40 + end
  41 +
  42 + def import(records)
  43 + if records.any?
  44 + client.bulk(
  45 + index: name,
  46 + type: document_type(records.first),
  47 + body: records.map{|r| data = search_data(r); {index: {_id: data["_id"] || data["id"] || r.id, data: data}} }
  48 + )
  49 + end
  50 + end
  51 +
  52 + def retrieve(record)
  53 + client.get(
  54 + index: name,
  55 + type: document_type(record),
  56 + id: record.id
  57 + )["_source"]
  58 + end
  59 +
  60 + def klass_document_type(klass)
  61 + klass.model_name.to_s.underscore
  62 + end
  63 +
  64 + protected
  65 +
  66 + def client
  67 + Searchkick.client
  68 + end
  69 +
  70 + def document_type(record)
  71 + klass_document_type(record.class)
  72 + end
  73 +
  74 + def search_data(record)
  75 + source = record.search_data
  76 +
  77 + # stringify fields
  78 + source = source.inject({}){|memo,(k,v)| memo[k.to_s] = v; memo}
  79 +
  80 + # Mongoid 4 hack
  81 + if defined?(BSON::ObjectId) and source["_id"].is_a?(BSON::ObjectId)
  82 + source["_id"] = source["_id"].to_s
  83 + end
  84 +
  85 + options = record.class.searchkick_options
  86 +
  87 + # conversions
  88 + conversions_field = options[:conversions]
  89 + if conversions_field and source[conversions_field]
  90 + source[conversions_field] = source[conversions_field].map{|k, v| {query: k, count: v} }
  91 + end
  92 +
  93 + # hack to prevent generator field doesn't exist error
  94 + (options[:suggest] || []).map(&:to_s).each do |field|
  95 + source[field] = nil if !source[field]
  96 + end
  97 +
  98 + # locations
  99 + (options[:locations] || []).map(&:to_s).each do |field|
  100 + if source[field]
  101 + if source[field].first.is_a?(Array) # array of arrays
  102 + source[field] = source[field].map{|a| a.map(&:to_f).reverse }
  103 + else
  104 + source[field] = source[field].map(&:to_f).reverse
  105 + end
  106 + end
  107 + end
  108 +
  109 + cast_big_decimal(source)
  110 +
  111 + # p search_data
  112 +
  113 + source.as_json
  114 + end
  115 +
  116 + # change all BigDecimal values to floats due to
  117 + # https://github.com/rails/rails/issues/6033
  118 + # possible loss of precision :/
  119 + def cast_big_decimal(obj)
  120 + case obj
  121 + when BigDecimal
  122 + obj.to_f
  123 + when Hash
  124 + obj.each do |k, v|
  125 + obj[k] = cast_big_decimal(v)
  126 + end
  127 + when Enumerable
  128 + obj.map do |v|
  129 + cast_big_decimal(v)
  130 + end
  131 + else
  132 + obj
  133 + end
  134 + end
  135 +
  136 + end
  137 +end
lib/searchkick/logger.rb
@@ -1,19 +0,0 @@ @@ -1,19 +0,0 @@
1 -require "tire/rails/logger"  
2 -require "tire/rails/logger/log_subscriber"  
3 -  
4 -class Tire::Rails::LogSubscriber  
5 -  
6 - # better output format  
7 - def search(event)  
8 - self.class.runtime += event.duration  
9 - return unless logger.debug?  
10 -  
11 - payload = event.payload  
12 -  
13 - name = "%s (%.1fms)" % [payload[:name], event.duration]  
14 - query = payload[:search].to_s  
15 -  
16 - debug " #{color(name, YELLOW, true)} #{query}"  
17 - end  
18 -  
19 -end  
lib/searchkick/logging.rb 0 โ†’ 100644
@@ -0,0 +1,89 @@ @@ -0,0 +1,89 @@
  1 +# based on https://gist.github.com/mnutt/566725
  2 +
  3 +module Searchkick
  4 + class Query
  5 + def execute_with_instrumentation
  6 + event = {
  7 + name: "#{searchkick_klass.name} Search",
  8 + query: params
  9 + }
  10 + ActiveSupport::Notifications.instrument("search.searchkick", event) do
  11 + execute_without_instrumentation
  12 + end
  13 + end
  14 +
  15 + alias_method_chain :execute, :instrumentation
  16 + end
  17 +
  18 + # https://github.com/rails/rails/blob/master/activerecord/lib/active_record/log_subscriber.rb
  19 + class LogSubscriber < ActiveSupport::LogSubscriber
  20 + def self.runtime=(value)
  21 + Thread.current[:searchkick_runtime] = value
  22 + end
  23 +
  24 + def self.runtime
  25 + Thread.current[:searchkick_runtime] ||= 0
  26 + end
  27 +
  28 + def self.reset_runtime
  29 + rt, self.runtime = runtime, 0
  30 + rt
  31 + end
  32 +
  33 + def search(event)
  34 + self.class.runtime += event.duration
  35 + return unless logger.debug?
  36 +
  37 + payload = event.payload
  38 + name = "#{payload[:name]} (#{event.duration.round(1)}ms)"
  39 + type = payload[:query][:type]
  40 +
  41 + # no easy way to tell which host the client will use
  42 + host = Searchkick.client.transport.hosts.first
  43 + debug " #{color(name, YELLOW, true)} curl #{host[:protocol]}://#{host[:host]}:#{host[:port]}/#{CGI.escape(payload[:query][:index])}#{type ? "/#{type.map{|t| CGI.escape(t) }.join(",")}" : ""}/_search?pretty -d '#{payload[:query][:body].to_json}'"
  44 + end
  45 + end
  46 +
  47 + # https://github.com/rails/rails/blob/master/activerecord/lib/active_record/railties/controller_runtime.rb
  48 + module ControllerRuntime
  49 + extend ActiveSupport::Concern
  50 +
  51 + protected
  52 +
  53 + attr_internal :searchkick_runtime
  54 +
  55 + def process_action(action, *args)
  56 + # We also need to reset the runtime before each action
  57 + # because of queries in middleware or in cases we are streaming
  58 + # and it won't be cleaned up by the method below.
  59 + Searchkick::LogSubscriber.reset_runtime
  60 + super
  61 + end
  62 +
  63 + def cleanup_view_runtime
  64 + searchkick_rt_before_render = Searchkick::LogSubscriber.reset_runtime
  65 + runtime = super
  66 + searchkick_rt_after_render = Searchkick::LogSubscriber.reset_runtime
  67 + self.searchkick_runtime = searchkick_rt_before_render + searchkick_rt_after_render
  68 + runtime - searchkick_rt_after_render
  69 + end
  70 +
  71 + def append_info_to_payload(payload)
  72 + super
  73 + payload[:searchkick_runtime] = (searchkick_runtime || 0) + Searchkick::LogSubscriber.reset_runtime
  74 + end
  75 +
  76 + module ClassMethods
  77 + def log_process_action(payload)
  78 + messages, runtime = super, payload[:searchkick_runtime]
  79 + messages << ("Searchkick: %.1fms" % runtime.to_f) if runtime.to_f > 0
  80 + messages
  81 + end
  82 + end
  83 + end
  84 +end
  85 +
  86 +Searchkick::LogSubscriber.attach_to :searchkick
  87 +ActiveSupport.on_load(:action_controller) do
  88 + include Searchkick::ControllerRuntime
  89 +end
lib/searchkick/model.rb
@@ -2,18 +2,22 @@ module Searchkick @@ -2,18 +2,22 @@ module Searchkick
2 module Model 2 module Model
3 3
4 def searchkick(options = {}) 4 def searchkick(options = {})
  5 + raise "Only call searchkick once per model" if respond_to?(:searchkick_index)
  6 +
5 class_eval do 7 class_eval do
6 - cattr_reader :searchkick_options, :searchkick_env, :searchkick_klass, :searchkick_index 8 + cattr_reader :searchkick_options, :searchkick_env, :searchkick_klass
7 9
8 class_variable_set :@@searchkick_options, options.dup 10 class_variable_set :@@searchkick_options, options.dup
9 class_variable_set :@@searchkick_env, ENV["RACK_ENV"] || ENV["RAILS_ENV"] || "development" 11 class_variable_set :@@searchkick_env, ENV["RACK_ENV"] || ENV["RAILS_ENV"] || "development"
10 class_variable_set :@@searchkick_klass, self 12 class_variable_set :@@searchkick_klass, self
11 class_variable_set :@@searchkick_callbacks, options[:callbacks] != false 13 class_variable_set :@@searchkick_callbacks, options[:callbacks] != false
  14 + class_variable_set :@@searchkick_index, options[:index_name] || [options[:index_prefix], model_name.plural, searchkick_env].compact.join("_")
12 15
13 - # set index name  
14 - # TODO support proc  
15 - index_name = options[:index_name] || [options[:index_prefix], model_name.plural, searchkick_env].compact.join("_")  
16 - class_variable_set :@@searchkick_index, Tire::Index.new(index_name) 16 + def self.searchkick_index
  17 + index = class_variable_get :@@searchkick_index
  18 + index = index.call if index.respond_to? :call
  19 + Searchkick::Index.new(index)
  20 + end
17 21
18 extend Searchkick::Search 22 extend Searchkick::Search
19 extend Searchkick::Reindex 23 extend Searchkick::Reindex
@@ -45,7 +49,11 @@ module Searchkick @@ -45,7 +49,11 @@ module Searchkick
45 def reindex 49 def reindex
46 index = self.class.searchkick_index 50 index = self.class.searchkick_index
47 if destroyed? or !should_index? 51 if destroyed? or !should_index?
48 - index.remove self 52 + begin
  53 + index.remove self
  54 + rescue Elasticsearch::Transport::Transport::Errors::NotFound
  55 + # do nothing
  56 + end
49 else 57 else
50 index.store self 58 index.store self
51 end 59 end
@@ -55,79 +63,6 @@ module Searchkick @@ -55,79 +63,6 @@ module Searchkick
55 respond_to?(:to_hash) ? to_hash : serializable_hash 63 respond_to?(:to_hash) ? to_hash : serializable_hash
56 end 64 end
57 65
58 - def to_indexed_json  
59 - source = search_data  
60 -  
61 - # stringify fields  
62 - source = source.inject({}){|memo,(k,v)| memo[k.to_s] = v; memo}  
63 -  
64 - # Mongoid 4 hack  
65 - if defined?(BSON::ObjectId) and source["_id"].is_a?(BSON::ObjectId)  
66 - source["_id"] = source["_id"].to_s  
67 - end  
68 -  
69 - options = self.class.searchkick_options  
70 -  
71 - # conversions  
72 - conversions_field = options[:conversions]  
73 - if conversions_field and source[conversions_field]  
74 - source[conversions_field] = source[conversions_field].map{|k, v| {query: k, count: v} }  
75 - end  
76 -  
77 - # hack to prevent generator field doesn't exist error  
78 - (options[:suggest] || []).map(&:to_s).each do |field|  
79 - source[field] = nil if !source[field]  
80 - end  
81 -  
82 - # locations  
83 - (options[:locations] || []).map(&:to_s).each do |field|  
84 - if source[field]  
85 - if source[field].first.is_a?(Array) # array of arrays  
86 - source[field] = source[field].map{|a| a.map(&:to_f).reverse }  
87 - else  
88 - source[field] = source[field].map(&:to_f).reverse  
89 - end  
90 - end  
91 - end  
92 -  
93 - # change all BigDecimal values to floats due to  
94 - # https://github.com/rails/rails/issues/6033  
95 - # possible loss of precision :/  
96 - cast_big_decimal =  
97 - proc do |obj|  
98 - case obj  
99 - when BigDecimal  
100 - obj.to_f  
101 - when Hash  
102 - obj.each do |k, v|  
103 - obj[k] = cast_big_decimal.call(v)  
104 - end  
105 - when Enumerable  
106 - obj.map! do |v|  
107 - cast_big_decimal.call(v)  
108 - end  
109 - else  
110 - obj  
111 - end  
112 - end  
113 -  
114 - cast_big_decimal.call(source)  
115 -  
116 - # p search_data  
117 -  
118 - source.to_json  
119 - end  
120 -  
121 - # TODO remove  
122 -  
123 - def self.document_type  
124 - model_name.to_s.underscore  
125 - end  
126 -  
127 - def document_type  
128 - self.class.document_type  
129 - end  
130 -  
131 end 66 end
132 end 67 end
133 68
lib/searchkick/query.rb
@@ -35,15 +35,10 @@ module Searchkick @@ -35,15 +35,10 @@ module Searchkick
35 35
36 operator = options[:operator] || (options[:partial] ? "or" : "and") 36 operator = options[:operator] || (options[:partial] ? "or" : "and")
37 37
38 - # model and eagar loading  
39 - load = options[:load].nil? ? true : options[:load]  
40 - load = (options[:include] ? {include: options[:include]} : true) if load  
41 -  
42 # pagination 38 # pagination
43 page = [options[:page].to_i, 1].max 39 page = [options[:page].to_i, 1].max
44 per_page = (options[:limit] || options[:per_page] || 100000).to_i 40 per_page = (options[:limit] || options[:per_page] || 100000).to_i
45 offset = options[:offset] || (page - 1) * per_page 41 offset = options[:offset] || (page - 1) * per_page
46 - index_name = options[:index_name] || searchkick_index.name  
47 42
48 conversions_field = searchkick_options[:conversions] 43 conversions_field = searchkick_options[:conversions]
49 personalize_field = searchkick_options[:personalize] 44 personalize_field = searchkick_options[:personalize]
@@ -83,9 +78,9 @@ module Searchkick @@ -83,9 +78,9 @@ module Searchkick
83 fields: [field], 78 fields: [field],
84 query: term, 79 query: term,
85 use_dis_max: false, 80 use_dis_max: false,
86 - operator: operator,  
87 - cutoff_frequency: 0.001 81 + operator: operator
88 } 82 }
  83 + shared_options[:cutoff_frequency] = 0.001 unless operator == "and"
89 queries.concat [ 84 queries.concat [
90 {multi_match: shared_options.merge(boost: 10, analyzer: "searchkick_search")}, 85 {multi_match: shared_options.merge(boost: 10, analyzer: "searchkick_search")},
91 {multi_match: shared_options.merge(boost: 10, analyzer: "searchkick_search2")} 86 {multi_match: shared_options.merge(boost: 10, analyzer: "searchkick_search2")}
@@ -126,13 +121,16 @@ module Searchkick @@ -126,13 +121,16 @@ module Searchkick
126 path: conversions_field, 121 path: conversions_field,
127 score_mode: "total", 122 score_mode: "total",
128 query: { 123 query: {
129 - custom_score: { 124 + function_score: {
  125 + boost_mode: "replace",
130 query: { 126 query: {
131 match: { 127 match: {
132 query: term 128 query: term
133 } 129 }
134 }, 130 },
135 - script: "doc['count'].value" 131 + script_score: {
  132 + script: "doc['count'].value"
  133 + }
136 } 134 }
137 } 135 }
138 } 136 }
@@ -151,7 +149,9 @@ module Searchkick @@ -151,7 +149,9 @@ module Searchkick
151 field: options[:boost] 149 field: options[:boost]
152 } 150 }
153 }, 151 },
154 - script: "log(doc['#{options[:boost]}'].value + 2.718281828)" 152 + script_score: {
  153 + script: "log(doc['#{options[:boost]}'].value + 2.718281828)"
  154 + }
155 } 155 }
156 end 156 end
157 157
@@ -162,7 +162,7 @@ module Searchkick @@ -162,7 +162,7 @@ module Searchkick
162 personalize_field => options[:user_id] 162 personalize_field => options[:user_id]
163 } 163 }
164 }, 164 },
165 - boost: 100 165 + boost_factor: 100
166 } 166 }
167 end 167 end
168 168
@@ -171,16 +171,16 @@ module Searchkick @@ -171,16 +171,16 @@ module Searchkick
171 filter: { 171 filter: {
172 term: options[:personalize] 172 term: options[:personalize]
173 }, 173 },
174 - boost: 100 174 + boost_factor: 100
175 } 175 }
176 end 176 end
177 177
178 if custom_filters.any? 178 if custom_filters.any?
179 payload = { 179 payload = {
180 - custom_filters_score: { 180 + function_score: {
  181 + functions: custom_filters,
181 query: payload, 182 query: payload,
182 - filters: custom_filters,  
183 - score_mode: "total" 183 + score_mode: "sum"
184 } 184 }
185 } 185 }
186 end 186 end
@@ -279,18 +279,22 @@ module Searchkick @@ -279,18 +279,22 @@ module Searchkick
279 end 279 end
280 end 280 end
281 281
  282 + # model and eagar loading
  283 + load = options[:load].nil? ? true : options[:load]
  284 +
282 # An empty array will cause only the _id and _type for each hit to be returned 285 # An empty array will cause only the _id and _type for each hit to be returned
283 # http://www.elasticsearch.org/guide/reference/api/search/fields/ 286 # http://www.elasticsearch.org/guide/reference/api/search/fields/
284 payload[:fields] = [] if load 287 payload[:fields] = [] if load
285 288
286 - tire_options = {load: load, size: per_page, from: offset}  
287 if options[:type] or klass != searchkick_klass 289 if options[:type] or klass != searchkick_klass
288 - tire_options[:type] = [options[:type] || klass].flatten.map(&:document_type) 290 + @type = [options[:type] || klass].flatten.map{|v| searchkick_index.klass_document_type(v) }
289 end 291 end
290 292
291 - @search = Tire::Search::Search.new(index_name, tire_options)  
292 @body = payload 293 @body = payload
293 @facet_limits = facet_limits 294 @facet_limits = facet_limits
  295 + @page = page
  296 + @per_page = per_page
  297 + @load = load
294 end 298 end
295 299
296 def searchkick_index 300 def searchkick_index
@@ -305,20 +309,30 @@ module Searchkick @@ -305,20 +309,30 @@ module Searchkick
305 klass.searchkick_klass 309 klass.searchkick_klass
306 end 310 end
307 311
308 - def document_type  
309 - klass.document_type 312 + def params
  313 + params = {
  314 + index: options[:index_name] || searchkick_index.name,
  315 + body: body
  316 + }
  317 + params.merge!(type: @type) if @type
  318 + params
310 end 319 end
311 320
312 def execute 321 def execute
313 - @search.options[:payload] = body  
314 begin 322 begin
315 - response = @search.json  
316 - rescue Tire::Search::SearchRequestFailed => e 323 + response = Searchkick.client.search(params)
  324 + rescue => e # TODO rescue type
317 status_code = e.message[1..3].to_i 325 status_code = e.message[1..3].to_i
318 if status_code == 404 326 if status_code == 404
319 raise "Index missing - run #{searchkick_klass.name}.reindex" 327 raise "Index missing - run #{searchkick_klass.name}.reindex"
320 - elsif status_code == 500 and (e.message.include?("IllegalArgumentException[minimumSimilarity >= 1]") or e.message.include?("No query registered for [multi_match]"))  
321 - raise "Upgrade Elasticsearch to 0.90.0 or greater" 328 + elsif status_code == 500 and (
  329 + e.message.include?("IllegalArgumentException[minimumSimilarity >= 1]") or
  330 + e.message.include?("No query registered for [multi_match]") or
  331 + e.message.include?("[match] query does not support [cutoff_frequency]]") or
  332 + e.message.include?("No query registered for [function_score]]")
  333 + )
  334 +
  335 + raise "This version of Searchkick requires Elasticsearch 0.90.4 or greater"
322 else 336 else
323 raise e 337 raise e
324 end 338 end
@@ -333,7 +347,13 @@ module Searchkick @@ -333,7 +347,13 @@ module Searchkick
333 response["facets"][field]["other"] = facet["total"] - facet["terms"].sum{|term| term["count"] } 347 response["facets"][field]["other"] = facet["total"] - facet["terms"].sum{|term| term["count"] }
334 end 348 end
335 349
336 - Searchkick::Results.new(response, @search.options.merge(term: term, model_name: searchkick_klass.model_name)) 350 + opts = {
  351 + page: @page,
  352 + per_page: @per_page,
  353 + load: @load,
  354 + includes: options[:include] || options[:includes]
  355 + }
  356 + Searchkick::Results.new(searchkick_klass, response, opts)
337 end 357 end
338 358
339 private 359 private
lib/searchkick/reindex.rb
@@ -2,36 +2,28 @@ module Searchkick @@ -2,36 +2,28 @@ module Searchkick
2 module Reindex 2 module Reindex
3 3
4 # https://gist.github.com/jarosan/3124884 4 # https://gist.github.com/jarosan/3124884
  5 + # http://www.elasticsearch.org/blog/changing-mapping-with-zero-downtime/
5 def reindex 6 def reindex
6 alias_name = searchkick_index.name 7 alias_name = searchkick_index.name
7 - new_index = alias_name + "_" + Time.now.strftime("%Y%m%d%H%M%S%L")  
8 - index = Tire::Index.new(new_index) 8 + new_name = alias_name + "_" + Time.now.strftime("%Y%m%d%H%M%S%L")
  9 + index = Searchkick::Index.new(new_name)
9 10
10 clean_indices 11 clean_indices
11 12
12 - success = index.create searchkick_index_options  
13 - raise index.response.to_s if !success 13 + index.create searchkick_index_options
14 14
15 - if a = Tire::Alias.find(alias_name) 15 + # check if alias exists
  16 + if Searchkick.client.indices.exists_alias(name: alias_name)
16 searchkick_import(index) # import before swap 17 searchkick_import(index) # import before swap
17 18
18 - a.indices.each do |i|  
19 - a.indices.delete i  
20 - end  
21 -  
22 - a.indices.add new_index  
23 - response = a.save  
24 -  
25 - if response.success?  
26 - clean_indices  
27 - else  
28 - raise response.to_s  
29 - end 19 + # get existing indices to remove
  20 + old_indices = Searchkick.client.indices.get_alias(name: alias_name).keys
  21 + actions = old_indices.map{|name| {remove: {index: name, alias: alias_name}} } + [{add: {index: new_name, alias: alias_name}}]
  22 + Searchkick.client.indices.update_aliases body: {actions: actions}
  23 + clean_indices
30 else 24 else
31 searchkick_index.delete if searchkick_index.exists? 25 searchkick_index.delete if searchkick_index.exists?
32 - response = Tire::Alias.create(name: alias_name, indices: [new_index])  
33 - raise response.to_s if !response.success?  
34 - 26 + Searchkick.client.indices.update_aliases body: {actions: [{add: {index: new_name, alias: alias_name}}]}
35 searchkick_import(index) # import after swap 27 searchkick_import(index) # import after swap
36 end 28 end
37 29
@@ -42,10 +34,10 @@ module Searchkick @@ -42,10 +34,10 @@ module Searchkick
42 34
43 # remove old indices that start w/ index_name 35 # remove old indices that start w/ index_name
44 def clean_indices 36 def clean_indices
45 - all_indices = JSON.parse(Tire::Configuration.client.get("#{Tire::Configuration.url}/_aliases").body) 37 + all_indices = Searchkick.client.indices.get_aliases
46 indices = all_indices.select{|k, v| v["aliases"].empty? && k =~ /\A#{Regexp.escape(searchkick_index.name)}_\d{14,17}\z/ }.keys 38 indices = all_indices.select{|k, v| v["aliases"].empty? && k =~ /\A#{Regexp.escape(searchkick_index.name)}_\d{14,17}\z/ }.keys
47 indices.each do |index| 39 indices.each do |index|
48 - Tire::Index.new(index).delete 40 + Searchkick::Index.new(index).delete
49 end 41 end
50 indices 42 indices
51 end 43 end
@@ -73,7 +65,7 @@ module Searchkick @@ -73,7 +65,7 @@ module Searchkick
73 items = [] 65 items = []
74 scope.all.each do |item| 66 scope.all.each do |item|
75 items << item if item.should_index? 67 items << item if item.should_index?
76 - if items.length % batch_size == 0 68 + if items.length == batch_size
77 index.import items 69 index.import items
78 items = [] 70 items = []
79 end 71 end
lib/searchkick/results.rb
1 module Searchkick 1 module Searchkick
2 - class Results < Tire::Results::Collection  
3 - attr_reader :response 2 + class Results
  3 + include Enumerable
  4 + extend Forwardable
  5 +
  6 + attr_reader :klass, :response, :options
  7 +
  8 + def_delegators :results, :each, :empty?, :size, :slice, :[], :to_ary
  9 +
  10 + def initialize(klass, response, options = {})
  11 + @klass = klass
  12 + @response = response
  13 + @options = options
  14 + end
  15 +
  16 + def results
  17 + @results ||= begin
  18 + if options[:load]
  19 + hit_ids = hits.map{|hit| hit["_id"] }
  20 + records = klass
  21 + if options[:includes]
  22 + records = records.includes(options[:includes])
  23 + end
  24 + records = records.find(hit_ids)
  25 + hit_ids = hit_ids.map(&:to_s)
  26 + records.sort_by{|r| hit_ids.index(r.id.to_s) }
  27 + else
  28 + hits.map{|hit| Hashie::Mash.new(hit["_source"]) }
  29 + end
  30 + end
  31 + end
4 32
5 def suggestions 33 def suggestions
6 - if @response["suggest"]  
7 - @response["suggest"].values.flat_map{|v| v.first["options"] }.sort_by{|o| -o["score"] }.map{|o| o["text"] }.uniq 34 + if response["suggest"]
  35 + response["suggest"].values.flat_map{|v| v.first["options"] }.sort_by{|o| -o["score"] }.map{|o| o["text"] }.uniq
8 else 36 else
9 raise "Pass `suggest: true` to the search method for suggestions" 37 raise "Pass `suggest: true` to the search method for suggestions"
10 end 38 end
11 end 39 end
12 40
  41 + def each_with_hit(&block)
  42 + results.zip(hits).each(&block)
  43 + end
  44 +
13 def with_details 45 def with_details
14 each_with_hit.map do |model, hit| 46 each_with_hit.map do |model, hit|
15 details = {} 47 details = {}
@@ -20,13 +52,48 @@ module Searchkick @@ -20,13 +52,48 @@ module Searchkick
20 end 52 end
21 end 53 end
22 54
23 - # fixes deprecation warning  
24 - def __find_records_by_ids(klass, ids)  
25 - @options[:load] === true ? klass.find(ids) : klass.includes(@options[:load][:include]).find(ids) 55 + def facets
  56 + response["facets"]
26 end 57 end
27 58
28 def model_name 59 def model_name
29 - @options[:model_name] 60 + klass.model_name
  61 + end
  62 +
  63 + def total_count
  64 + response["hits"]["total"]
  65 + end
  66 + alias_method :total_entries, :total_count
  67 +
  68 + def current_page
  69 + options[:page]
  70 + end
  71 +
  72 + def per_page
  73 + options[:per_page]
  74 + end
  75 + alias_method :limit_value, :per_page
  76 +
  77 + def total_pages
  78 + (total_count / per_page.to_f).ceil
  79 + end
  80 +
  81 + def offset_value
  82 + current_page * per_page
  83 + end
  84 +
  85 + def previous_page
  86 + current_page > 1 ? (current_page - 1) : nil
  87 + end
  88 +
  89 + def next_page
  90 + current_page < total_pages ? (current_page + 1) : nil
  91 + end
  92 +
  93 + protected
  94 +
  95 + def hits
  96 + @response["hits"]["hits"]
30 end 97 end
31 98
32 end 99 end
lib/searchkick/similar.rb
@@ -2,8 +2,8 @@ module Searchkick @@ -2,8 +2,8 @@ module Searchkick
2 module Similar 2 module Similar
3 3
4 def similar(options = {}) 4 def similar(options = {})
5 - like_text = self.class.searchkick_index.retrieve(document_type, id).to_hash  
6 - .keep_if{|k,v| k[0] != "_" and (!options[:fields] or options[:fields].map(&:to_sym).include?(k)) } 5 + like_text = self.class.searchkick_index.retrieve(self).to_hash
  6 + .keep_if{|k,v| !options[:fields] || options[:fields].map(&:to_s).include?(k) }
7 .values.compact.join(" ") 7 .values.compact.join(" ")
8 8
9 # TODO deep merge method 9 # TODO deep merge method
lib/searchkick/version.rb
1 module Searchkick 1 module Searchkick
2 - VERSION = "0.5.3" 2 + VERSION = "0.7.1"
3 end 3 end
searchkick.gemspec
@@ -8,8 +8,8 @@ Gem::Specification.new do |spec| @@ -8,8 +8,8 @@ Gem::Specification.new do |spec|
8 spec.version = Searchkick::VERSION 8 spec.version = Searchkick::VERSION
9 spec.authors = ["Andrew Kane"] 9 spec.authors = ["Andrew Kane"]
10 spec.email = ["andrew@chartkick.com"] 10 spec.email = ["andrew@chartkick.com"]
11 - spec.description = %q{Search made easy}  
12 - spec.summary = %q{Search made easy} 11 + spec.description = %q{Intelligent search made easy}
  12 + spec.summary = %q{Searchkick learns what your users are looking for. As more people search, it gets smarter and the results get better. Itโ€™s friendly for developers - and magical for your users.}
13 spec.homepage = "https://github.com/ankane/searchkick" 13 spec.homepage = "https://github.com/ankane/searchkick"
14 spec.license = "MIT" 14 spec.license = "MIT"
15 15
@@ -18,8 +18,9 @@ Gem::Specification.new do |spec| @@ -18,8 +18,9 @@ Gem::Specification.new do |spec|
18 spec.test_files = spec.files.grep(%r{^(test|spec|features)/}) 18 spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
19 spec.require_paths = ["lib"] 19 spec.require_paths = ["lib"]
20 20
21 - spec.add_dependency "tire"  
22 - spec.add_dependency "tire-contrib" 21 + spec.add_dependency "activemodel"
  22 + spec.add_dependency "elasticsearch", "~> 0.4.11"
  23 + spec.add_dependency "hashie"
23 24
24 spec.add_development_dependency "bundler", "~> 1.3" 25 spec.add_development_dependency "bundler", "~> 1.3"
25 spec.add_development_dependency "rake" 26 spec.add_development_dependency "rake"
test/index_test.rb
@@ -3,8 +3,11 @@ require_relative &quot;test_helper&quot; @@ -3,8 +3,11 @@ require_relative &quot;test_helper&quot;
3 class TestIndex < Minitest::Unit::TestCase 3 class TestIndex < Minitest::Unit::TestCase
4 4
5 def test_clean_indices 5 def test_clean_indices
6 - old_index = Tire::Index.new("products_test_20130801000000000")  
7 - different_index = Tire::Index.new("items_test_20130801000000000") 6 + old_index = Searchkick::Index.new("products_test_20130801000000000")
  7 + different_index = Searchkick::Index.new("items_test_20130801000000000")
  8 +
  9 + old_index.delete if old_index.exists?
  10 + different_index.delete if different_index.exists?
8 11
9 # create indexes 12 # create indexes
10 old_index.create 13 old_index.create
@@ -18,7 +21,7 @@ class TestIndex &lt; Minitest::Unit::TestCase @@ -18,7 +21,7 @@ class TestIndex &lt; Minitest::Unit::TestCase
18 end 21 end
19 22
20 def test_clean_indices_old_format 23 def test_clean_indices_old_format
21 - old_index = Tire::Index.new("products_test_20130801000000") 24 + old_index = Searchkick::Index.new("products_test_20130801000000")
22 old_index.create 25 old_index.create
23 26
24 Product.clean_indices 27 Product.clean_indices
test/inheritance_test.rb
@@ -10,7 +10,7 @@ class TestInheritance &lt; Minitest::Unit::TestCase @@ -10,7 +10,7 @@ class TestInheritance &lt; Minitest::Unit::TestCase
10 end 10 end
11 11
12 def test_child_index_name 12 def test_child_index_name
13 - assert_equal "animals_test", Dog.searchkick_index.name 13 + assert_equal "animals-#{Date.today.year}", Dog.searchkick_index.name
14 end 14 end
15 15
16 def test_child_search 16 def test_child_search
test/sql_test.rb
@@ -26,6 +26,9 @@ class TestSql &lt; Minitest::Unit::TestCase @@ -26,6 +26,9 @@ class TestSql &lt; Minitest::Unit::TestCase
26 assert_equal 2, products.per_page 26 assert_equal 2, products.per_page
27 assert_equal 3, products.total_pages 27 assert_equal 3, products.total_pages
28 assert_equal 5, products.total_count 28 assert_equal 5, products.total_count
  29 + assert_equal 5, products.total_entries
  30 + assert_equal 2, products.limit_value
  31 + assert_equal 4, products.offset_value
29 end 32 end
30 33
31 def test_pagination_nil_page 34 def test_pagination_nil_page
@@ -245,12 +248,17 @@ class TestSql &lt; Minitest::Unit::TestCase @@ -245,12 +248,17 @@ class TestSql &lt; Minitest::Unit::TestCase
245 248
246 def test_load_false 249 def test_load_false
247 store_names ["Product A"] 250 store_names ["Product A"]
248 - assert_kind_of Tire::Results::Item, Product.search("product", load: false).first 251 + assert_kind_of Hash, Product.search("product", load: false).first
  252 + end
  253 +
  254 + def test_load_false_methods
  255 + store_names ["Product A"]
  256 + assert_equal "Product A", Product.search("product", load: false).first.name
249 end 257 end
250 258
251 def test_load_false_with_include 259 def test_load_false_with_include
252 store_names ["Product A"] 260 store_names ["Product A"]
253 - assert_kind_of Tire::Results::Item, Product.search("product", load: false, include: [:store]).first 261 + assert_kind_of Hash, Product.search("product", load: false, include: [:store]).first
254 end 262 end
255 263
256 # TODO see if Mongoid is loaded 264 # TODO see if Mongoid is loaded
test/test_helper.rb
@@ -2,14 +2,12 @@ require &quot;bundler/setup&quot; @@ -2,14 +2,12 @@ require &quot;bundler/setup&quot;
2 Bundler.require(:default) 2 Bundler.require(:default)
3 require "minitest/autorun" 3 require "minitest/autorun"
4 require "minitest/pride" 4 require "minitest/pride"
  5 +require "logger"
5 6
6 ENV["RACK_ENV"] = "test" 7 ENV["RACK_ENV"] = "test"
7 8
8 File.delete("elasticsearch.log") if File.exists?("elasticsearch.log") 9 File.delete("elasticsearch.log") if File.exists?("elasticsearch.log")
9 -Tire.configure do  
10 - logger "elasticsearch.log", :level => "debug"  
11 - pretty true  
12 -end 10 +Searchkick.client.transport.logger = Logger.new("elasticsearch.log")
13 11
14 if defined?(Mongoid) 12 if defined?(Mongoid)
15 Mongoid.configure do |config| 13 Mongoid.configure do |config|
@@ -145,7 +143,7 @@ class Store @@ -145,7 +143,7 @@ class Store
145 end 143 end
146 144
147 class Animal 145 class Animal
148 - searchkick autocomplete: [:name], suggest: [:name] 146 + searchkick autocomplete: [:name], suggest: [:name], index_name: -> { "#{self.name.tableize}-#{Date.today.year}" }
149 end 147 end
150 148
151 Product.searchkick_index.delete if Product.searchkick_index.exists? 149 Product.searchkick_index.delete if Product.searchkick_index.exists?