Commit c72ef2c16f74ab468bfce4445747c62d9e747368

Authored by Andrew Kane
2 parents 6a71a686 2a43d660

Test mongoid release

README.md
... ... @@ -568,7 +568,13 @@ end
568 568 And use the `query` option to search:
569 569  
570 570 ```ruby
571   -Product.search query: {match: {name: "milk"}}
  571 +products = Product.search query: {match: {name: "milk"}}
  572 +```
  573 +
  574 +View the response with:
  575 +
  576 +```ruby
  577 +products.response
572 578 ```
573 579  
574 580 To keep the mappings and settings generated by Searchkick, use:
... ... @@ -579,7 +585,7 @@ class Product < ActiveRecord::Base
579 585 end
580 586 ```
581 587  
582   -## Experimental [master]
  588 +## Experimental
583 589  
584 590 Modify the query generated by Searchkick.
585 591  
... ...
gemfiles/mongoid4.gemfile
... ... @@ -3,4 +3,4 @@ source 'https://rubygems.org'
3 3 # Specify your gem's dependencies in searchkick.gemspec
4 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 "patron"
  3 +require "elasticsearch"
2 4 require "searchkick/version"
  5 +require "searchkick/index"
3 6 require "searchkick/reindex"
4 7 require "searchkick/results"
5 8 require "searchkick/query"
... ... @@ -7,9 +10,15 @@ require "searchkick/search"
7 10 require "searchkick/similar"
8 11 require "searchkick/model"
9 12 require "searchkick/tasks"
10   -require "searchkick/logger" if defined?(Rails)
  13 +# TODO add logger
  14 +# require "searchkick/logger" if defined?(Rails)
11 15  
12 16 module Searchkick
  17 +
  18 + def self.client
  19 + @client ||= Elasticsearch::Client.new(url: ENV["ELASTICSEARCH_URL"])
  20 + end
  21 +
13 22 @callbacks = true
14 23  
15 24 def self.enable_callbacks
... ...
lib/searchkick/index.rb 0 โ†’ 100644
... ... @@ -0,0 +1,67 @@
  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: record.document_type,
  29 + id: record.id,
  30 + body: record.as_indexed_json
  31 + )
  32 + end
  33 +
  34 + def remove(record)
  35 + client.delete(
  36 + index: name,
  37 + type: record.document_type,
  38 + id: record.id
  39 + )
  40 + end
  41 +
  42 + def import(records)
  43 + if records.any?
  44 + client.bulk(
  45 + index: name,
  46 + type: records.first.document_type,
  47 + body: records.map{|r| data = r.as_indexed_json; {index: {_id: data["_id"] || data["id"] || r.id, data: data}} }
  48 + )
  49 + end
  50 + end
  51 +
  52 + def retrieve(document_type, id)
  53 + client.get_source(
  54 + index: name,
  55 + type: document_type,
  56 + id: id
  57 + )
  58 + end
  59 +
  60 + protected
  61 +
  62 + def client
  63 + Searchkick.client
  64 + end
  65 +
  66 + end
  67 +end
... ...
lib/searchkick/logger.rb
... ... @@ -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/model.rb
... ... @@ -13,7 +13,7 @@ module Searchkick
13 13 # set index name
14 14 # TODO support proc
15 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 + class_variable_set :@@searchkick_index, Searchkick::Index.new(index_name)
17 17  
18 18 extend Searchkick::Search
19 19 extend Searchkick::Reindex
... ... @@ -45,7 +45,11 @@ module Searchkick
45 45 def reindex
46 46 index = self.class.searchkick_index
47 47 if destroyed? or !should_index?
48   - index.remove self
  48 + begin
  49 + index.remove self
  50 + rescue Elasticsearch::Transport::Transport::Errors::NotFound
  51 + # do nothing
  52 + end
49 53 else
50 54 index.store self
51 55 end
... ... @@ -55,7 +59,7 @@ module Searchkick
55 59 respond_to?(:to_hash) ? to_hash : serializable_hash
56 60 end
57 61  
58   - def to_indexed_json
  62 + def as_indexed_json
59 63 source = search_data
60 64  
61 65 # stringify fields
... ... @@ -115,7 +119,7 @@ module Searchkick
115 119  
116 120 # p search_data
117 121  
118   - source.to_json
  122 + source.as_json
119 123 end
120 124  
121 125 # TODO remove
... ...
lib/searchkick/query.rb
... ... @@ -35,10 +35,6 @@ module Searchkick
35 35  
36 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 38 # pagination
43 39 page = [options[:page].to_i, 1].max
44 40 per_page = (options[:limit] || options[:per_page] || 100000).to_i
... ... @@ -278,18 +274,22 @@ module Searchkick
278 274 end
279 275 end
280 276  
  277 + # model and eagar loading
  278 + load = options[:load].nil? ? true : options[:load]
  279 +
281 280 # An empty array will cause only the _id and _type for each hit to be returned
282 281 # http://www.elasticsearch.org/guide/reference/api/search/fields/
283 282 payload[:fields] = [] if load
284 283  
285   - tire_options = {load: load, size: per_page, from: offset}
286 284 if options[:type] or klass != searchkick_klass
287   - tire_options[:type] = [options[:type] || klass].flatten.map(&:document_type)
  285 + @type = [options[:type] || klass].flatten.map(&:document_type)
288 286 end
289 287  
290   - @search = Tire::Search::Search.new(index_name, tire_options)
291 288 @body = payload
292 289 @facet_limits = facet_limits
  290 + @page = page
  291 + @per_page = per_page
  292 + @load = load
293 293 end
294 294  
295 295 def searchkick_index
... ... @@ -309,14 +309,18 @@ module Searchkick
309 309 end
310 310  
311 311 def execute
312   - @search.options[:payload] = body
  312 + params = {
  313 + index: searchkick_index.name,
  314 + body: body
  315 + }
  316 + params.merge!(type: @type) if @type
313 317 begin
314   - response = @search.json
315   - rescue Tire::Search::SearchRequestFailed => e
  318 + response = Searchkick.client.search(params)
  319 + rescue => e # TODO rescue type
316 320 status_code = e.message[1..3].to_i
317 321 if status_code == 404
318 322 raise "Index missing - run #{searchkick_klass.name}.reindex"
319   - elsif status_code == 500 and (e.message.include?("IllegalArgumentException[minimumSimilarity >= 1]") or e.message.include?("No query registered for [multi_match]"))
  323 + elsif status_code == 500 and (e.message.include?("IllegalArgumentException[minimumSimilarity >= 1]") or e.message.include?("No query registered for [multi_match]") or e.message.include?("[match] query does not support [cutoff_frequency]]"))
320 324 raise "Upgrade Elasticsearch to 0.90.0 or greater"
321 325 else
322 326 raise e
... ... @@ -332,7 +336,13 @@ module Searchkick
332 336 response["facets"][field]["other"] = facet["total"] - facet["terms"].sum{|term| term["count"] }
333 337 end
334 338  
335   - Searchkick::Results.new(response, @search.options.merge(term: term, model_name: searchkick_klass.model_name))
  339 + opts = {
  340 + page: @page,
  341 + per_page: @per_page,
  342 + load: @load,
  343 + includes: options[:include] || options[:includes]
  344 + }
  345 + Searchkick::Results.new(searchkick_klass, response, opts)
336 346 end
337 347  
338 348 private
... ...
lib/searchkick/reindex.rb
... ... @@ -2,36 +2,28 @@ module Searchkick
2 2 module Reindex
3 3  
4 4 # https://gist.github.com/jarosan/3124884
  5 + # http://www.elasticsearch.org/blog/changing-mapping-with-zero-downtime/
5 6 def reindex
6 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 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 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 24 else
31 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 27 searchkick_import(index) # import after swap
36 28 end
37 29  
... ... @@ -42,10 +34,10 @@ module Searchkick
42 34  
43 35 # remove old indices that start w/ index_name
44 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 38 indices = all_indices.select{|k, v| v["aliases"].empty? && k =~ /\A#{Regexp.escape(searchkick_index.name)}_\d{14,17}\z/ }.keys
47 39 indices.each do |index|
48   - Tire::Index.new(index).delete
  40 + Searchkick::Index.new(index).delete
49 41 end
50 42 indices
51 43 end
... ... @@ -73,7 +65,7 @@ module Searchkick
73 65 items = []
74 66 scope.all.each do |item|
75 67 items << item if item.should_index?
76   - if items.length % batch_size == 0
  68 + if items.length == batch_size
77 69 index.import items
78 70 items = []
79 71 end
... ...
lib/searchkick/results.rb
1 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
  29 + end
  30 + end
  31 + end
4 32  
5 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 36 else
9 37 raise "Pass `suggest: true` to the search method for suggestions"
10 38 end
11 39 end
12 40  
  41 + def each_with_hit(&block)
  42 + results.zip(hits).each(&block)
  43 + end
  44 +
13 45 def with_details
14 46 each_with_hit.map do |model, hit|
15 47 details = {}
... ... @@ -20,13 +52,50 @@ module Searchkick
20 52 end
21 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 57 end
27 58  
28 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 +
  67 + def current_page
  68 + options[:page]
  69 + end
  70 +
  71 + def per_page
  72 + options[:per_page]
  73 + end
  74 +
  75 + def total_pages
  76 + (total_count / per_page.to_f).ceil
  77 + end
  78 +
  79 + def limit_value
  80 + per_page
  81 + end
  82 +
  83 + def offset_value
  84 + current_page * per_page
  85 + end
  86 +
  87 + def previous_page
  88 + current_page > 1 ? (current_page - 1) : nil
  89 + end
  90 +
  91 + def next_page
  92 + current_page < total_pages ? (current_page + 1) : nil
  93 + end
  94 +
  95 + protected
  96 +
  97 + def hits
  98 + @response["hits"]["hits"]
30 99 end
31 100  
32 101 end
... ...
lib/searchkick/similar.rb
... ... @@ -3,7 +3,7 @@ module Searchkick
3 3  
4 4 def similar(options = {})
5 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)) }
  6 + .keep_if{|k,v| !options[:fields] || options[:fields].map(&:to_s).include?(k) }
7 7 .values.compact.join(" ")
8 8  
9 9 # TODO deep merge method
... ...
searchkick.gemspec
... ... @@ -18,8 +18,9 @@ Gem::Specification.new do |spec|
18 18 spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
19 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"
  23 + spec.add_dependency "patron" # persistent http connections for performance
23 24  
24 25 spec.add_development_dependency "bundler", "~> 1.3"
25 26 spec.add_development_dependency "rake"
... ...
test/index_test.rb
... ... @@ -3,8 +3,11 @@ require_relative &quot;test_helper&quot;
3 3 class TestIndex < Minitest::Unit::TestCase
4 4  
5 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 12 # create indexes
10 13 old_index.create
... ... @@ -18,7 +21,7 @@ class TestIndex &lt; Minitest::Unit::TestCase
18 21 end
19 22  
20 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 25 old_index.create
23 26  
24 27 Product.clean_indices
... ...
test/sql_test.rb
... ... @@ -26,6 +26,8 @@ class TestSql &lt; Minitest::Unit::TestCase
26 26 assert_equal 2, products.per_page
27 27 assert_equal 3, products.total_pages
28 28 assert_equal 5, products.total_count
  29 + assert_equal 2, products.limit_value
  30 + assert_equal 4, products.offset_value
29 31 end
30 32  
31 33 def test_pagination_nil_page
... ... @@ -245,12 +247,12 @@ class TestSql &lt; Minitest::Unit::TestCase
245 247  
246 248 def test_load_false
247 249 store_names ["Product A"]
248   - assert_kind_of Tire::Results::Item, Product.search("product", load: false).first
  250 + assert_kind_of Hash, Product.search("product", load: false).first
249 251 end
250 252  
251 253 def test_load_false_with_include
252 254 store_names ["Product A"]
253   - assert_kind_of Tire::Results::Item, Product.search("product", load: false, include: [:store]).first
  255 + assert_kind_of Hash, Product.search("product", load: false, include: [:store]).first
254 256 end
255 257  
256 258 # TODO see if Mongoid is loaded
... ...
test/test_helper.rb
... ... @@ -2,14 +2,12 @@ require &quot;bundler/setup&quot;
2 2 Bundler.require(:default)
3 3 require "minitest/autorun"
4 4 require "minitest/pride"
  5 +require "logger"
5 6  
6 7 ENV["RACK_ENV"] = "test"
7 8  
8 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 12 if defined?(Mongoid)
15 13 Mongoid.configure do |config|
... ...