slingshot-rb 0.0.8 → 0.0.9

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.
Files changed (65) hide show
  1. data/.gitignore +1 -0
  2. data/README.markdown +276 -50
  3. data/examples/rails-application-template.rb +144 -0
  4. data/examples/slingshot-dsl.rb +272 -102
  5. data/lib/slingshot.rb +13 -0
  6. data/lib/slingshot/client.rb +10 -1
  7. data/lib/slingshot/dsl.rb +17 -1
  8. data/lib/slingshot/index.rb +109 -7
  9. data/lib/slingshot/model/callbacks.rb +23 -0
  10. data/lib/slingshot/model/import.rb +18 -0
  11. data/lib/slingshot/model/indexing.rb +50 -0
  12. data/lib/slingshot/model/naming.rb +30 -0
  13. data/lib/slingshot/model/persistence.rb +34 -0
  14. data/lib/slingshot/model/persistence/attributes.rb +60 -0
  15. data/lib/slingshot/model/persistence/finders.rb +61 -0
  16. data/lib/slingshot/model/persistence/storage.rb +75 -0
  17. data/lib/slingshot/model/search.rb +97 -0
  18. data/lib/slingshot/results/collection.rb +35 -10
  19. data/lib/slingshot/results/item.rb +10 -7
  20. data/lib/slingshot/results/pagination.rb +30 -0
  21. data/lib/slingshot/rubyext/symbol.rb +11 -0
  22. data/lib/slingshot/search.rb +3 -2
  23. data/lib/slingshot/search/facet.rb +8 -6
  24. data/lib/slingshot/search/filter.rb +7 -8
  25. data/lib/slingshot/search/highlight.rb +1 -3
  26. data/lib/slingshot/search/query.rb +4 -0
  27. data/lib/slingshot/search/sort.rb +5 -0
  28. data/lib/slingshot/tasks.rb +88 -0
  29. data/lib/slingshot/version.rb +1 -1
  30. data/slingshot.gemspec +17 -4
  31. data/test/integration/active_model_searchable_test.rb +80 -0
  32. data/test/integration/active_record_searchable_test.rb +193 -0
  33. data/test/integration/highlight_test.rb +1 -1
  34. data/test/integration/index_mapping_test.rb +1 -1
  35. data/test/integration/index_store_test.rb +27 -0
  36. data/test/integration/persistent_model_test.rb +35 -0
  37. data/test/integration/query_string_test.rb +3 -3
  38. data/test/integration/sort_test.rb +2 -2
  39. data/test/models/active_model_article.rb +31 -0
  40. data/test/models/active_model_article_with_callbacks.rb +49 -0
  41. data/test/models/active_model_article_with_custom_index_name.rb +5 -0
  42. data/test/models/active_record_article.rb +12 -0
  43. data/test/models/persistent_article.rb +11 -0
  44. data/test/models/persistent_articles_with_custom_index_name.rb +10 -0
  45. data/test/models/supermodel_article.rb +22 -0
  46. data/test/models/validated_model.rb +11 -0
  47. data/test/test_helper.rb +4 -0
  48. data/test/unit/active_model_lint_test.rb +17 -0
  49. data/test/unit/client_test.rb +4 -0
  50. data/test/unit/configuration_test.rb +4 -0
  51. data/test/unit/index_test.rb +240 -17
  52. data/test/unit/model_callbacks_test.rb +90 -0
  53. data/test/unit/model_import_test.rb +71 -0
  54. data/test/unit/model_persistence_test.rb +400 -0
  55. data/test/unit/model_search_test.rb +289 -0
  56. data/test/unit/results_collection_test.rb +69 -7
  57. data/test/unit/results_item_test.rb +8 -14
  58. data/test/unit/rubyext_hash_test.rb +19 -0
  59. data/test/unit/search_facet_test.rb +25 -7
  60. data/test/unit/search_filter_test.rb +3 -0
  61. data/test/unit/search_query_test.rb +11 -0
  62. data/test/unit/search_sort_test.rb +8 -0
  63. data/test/unit/search_test.rb +14 -0
  64. data/test/unit/slingshot_test.rb +38 -0
  65. metadata +133 -26
@@ -0,0 +1,61 @@
1
+ module Slingshot
2
+ module Model
3
+
4
+ module Persistence
5
+
6
+ module Finders
7
+
8
+ module ClassMethods
9
+
10
+ def find *args
11
+ # TODO: Options like `sort`
12
+ old_wrapper = Slingshot::Configuration.wrapper
13
+ Slingshot::Configuration.wrapper self
14
+ options = args.pop if args.last.is_a?(Hash)
15
+ args.flatten!
16
+ if args.size > 1
17
+ Slingshot::Search::Search.new(index.name).query do |query|
18
+ query.ids(args, document_type)
19
+ end.perform.results
20
+ else
21
+ case args = args.pop
22
+ when Fixnum, String
23
+ Index.new(index_name).retrieve document_type, args
24
+ when :all, :first
25
+ send(args)
26
+ else
27
+ raise ArgumentError, "Please pass either ID as Fixnum or String, or :all, :first as an argument"
28
+ end
29
+ end
30
+ ensure
31
+ Slingshot::Configuration.wrapper old_wrapper
32
+ end
33
+
34
+ def all
35
+ # TODO: Options like `sort`; Possibly `filters`
36
+ old_wrapper = Slingshot::Configuration.wrapper
37
+ Slingshot::Configuration.wrapper self
38
+ s = Slingshot::Search::Search.new(index_name).query { all }
39
+ s.perform.results
40
+ ensure
41
+ Slingshot::Configuration.wrapper old_wrapper
42
+ end
43
+
44
+ def first
45
+ # TODO: Options like `sort`; Possibly `filters`
46
+ old_wrapper = Slingshot::Configuration.wrapper
47
+ Slingshot::Configuration.wrapper self
48
+ s = Slingshot::Search::Search.new(index_name).query { all }.size(1)
49
+ s.perform.results.first
50
+ ensure
51
+ Slingshot::Configuration.wrapper old_wrapper
52
+ end
53
+
54
+ end
55
+
56
+ end
57
+
58
+ end
59
+
60
+ end
61
+ end
@@ -0,0 +1,75 @@
1
+ module Slingshot
2
+ module Model
3
+
4
+ module Persistence
5
+
6
+ module Storage
7
+
8
+ def self.included(base)
9
+
10
+ base.class_eval do
11
+ extend ClassMethods
12
+ include InstanceMethods
13
+ end
14
+
15
+ end
16
+
17
+ module ClassMethods
18
+
19
+ def create(args={})
20
+ document = new(args)
21
+ return false unless document.valid?
22
+ document.save
23
+ document
24
+ end
25
+
26
+ def index
27
+ @index = Index.new(index_name)
28
+ end
29
+
30
+ end
31
+
32
+ module InstanceMethods
33
+
34
+ def update_attribute(name, value)
35
+ send("#{name}=", value)
36
+ save
37
+ end
38
+
39
+ def update_attributes(attributes={})
40
+ attributes.each do |name, value|
41
+ send("#{name}=", value)
42
+ end
43
+ save
44
+ end
45
+
46
+ def save
47
+ return false unless valid?
48
+ run_callbacks :save do
49
+ # Document#id is set in the +update_elastic_search_index+ method,
50
+ # where we have access to the JSON response
51
+ end
52
+ self
53
+ end
54
+
55
+ def destroy
56
+ run_callbacks :destroy do
57
+ @destroyed = true
58
+ end
59
+ self.freeze
60
+ end
61
+
62
+ # TODO: Implement `new_record?` and clean up
63
+
64
+ def destroyed?; !!@destroyed; end
65
+
66
+ def persisted?; !!id; end
67
+
68
+ end
69
+
70
+ end
71
+
72
+ end
73
+
74
+ end
75
+ end
@@ -0,0 +1,97 @@
1
+ module Slingshot
2
+ module Model
3
+
4
+ module Search
5
+
6
+ def self.included(base)
7
+ base.class_eval do
8
+ extend Slingshot::Model::Naming::ClassMethods
9
+ include Slingshot::Model::Naming::InstanceMethods
10
+
11
+ extend Slingshot::Model::Indexing::ClassMethods
12
+ extend Slingshot::Model::Import::ClassMethods
13
+
14
+ extend ClassMethods
15
+ include InstanceMethods
16
+
17
+ ['_score', '_type', '_index', '_version', 'sort', 'highlight'].each do |attr|
18
+ # TODO: Find a sane way to add attributes like _score for ActiveRecord -
19
+ # `define_attribute_methods [attr]` does not work in AR.
20
+ define_method("#{attr}=") { |value| @attributes ||= {}; @attributes[attr] = value }
21
+ define_method("#{attr}") { @attributes[attr] }
22
+ end
23
+ end
24
+ end
25
+
26
+ module ClassMethods
27
+
28
+ def search(query=nil, options={}, &block)
29
+ old_wrapper = Slingshot::Configuration.wrapper
30
+ Slingshot::Configuration.wrapper self
31
+ sort = options[:order] || options[:sort]
32
+ sort = Array(sort)
33
+ unless block_given?
34
+ s = Slingshot::Search::Search.new(index.name, options)
35
+ s.query { string query }
36
+ s.sort do
37
+ sort.each do |t|
38
+ field_name, direction = t.split(' ')
39
+ field_name.include?('.') ? field(field_name, direction) : send(field_name, direction)
40
+ end
41
+ end unless sort.empty?
42
+ s.size( options[:per_page].to_i ) if options[:per_page]
43
+ s.from( options[:page].to_i <= 1 ? 0 : (options[:per_page].to_i * (options[:page].to_i-1)) ) if options[:page] && options[:per_page]
44
+ s.perform.results
45
+ else
46
+ s = Slingshot::Search::Search.new(index.name, options)
47
+ block.arity < 1 ? s.instance_eval(&block) : block.call(s)
48
+ s.perform.results
49
+ end
50
+ ensure
51
+ Slingshot::Configuration.wrapper old_wrapper
52
+ end
53
+
54
+ def index
55
+ @index = Index.new(index_name)
56
+ end
57
+
58
+ end
59
+
60
+ module InstanceMethods
61
+
62
+ def score
63
+ attributes['_score']
64
+ end
65
+
66
+ def index
67
+ self.class.index
68
+ end
69
+
70
+ def update_elastic_search_index
71
+ if destroyed?
72
+ self.class.index.remove document_type, self
73
+ else
74
+ response = self.class.index.store document_type, self
75
+ self.id ||= response['_id'] if self.respond_to?(:id=)
76
+ self
77
+ end
78
+ end
79
+
80
+ def to_indexed_json
81
+ if self.class.mapping.empty?
82
+ self.serializable_hash.
83
+ to_json
84
+ else
85
+ self.serializable_hash.
86
+ reject { |key, value| ! self.class.mapping.keys.map(&:to_s).include?(key.to_s) }.
87
+ to_json
88
+ end
89
+ end
90
+
91
+ end
92
+
93
+ extend ClassMethods
94
+ end
95
+
96
+ end
97
+ end
@@ -3,19 +3,32 @@ module Slingshot
3
3
 
4
4
  class Collection
5
5
  include Enumerable
6
- attr_reader :time, :total, :results, :facets
6
+ include Pagination
7
7
 
8
- def initialize(response)
9
- @time = response['took']
10
- @total = response['hits']['total']
8
+ attr_reader :time, :total, :options, :results, :facets
9
+
10
+ def initialize(response, options={})
11
+ @options = options
12
+ @time = response['took'].to_i
13
+ @total = response['hits']['total'].to_i
11
14
  @results = response['hits']['hits'].map do |h|
12
- if Configuration.wrapper == Hash
13
- h
15
+ if Configuration.wrapper == Hash then h
14
16
  else
15
- document = h['_source'] ? h['_source'] : h['fields']
16
- document['highlight'] = h['highlight'] if h['highlight']
17
- h.update document if document
18
- Configuration.wrapper.new(h)
17
+ document = {}
18
+
19
+ # Update the document with content and ID
20
+ document = h['_source'] ? document.update( h['_source'] || {} ) : document.update( h['fields'] || {} )
21
+ document.update( {'id' => h['_id']} )
22
+
23
+ # Update the document with meta information
24
+ ['_score', '_version', 'sort', 'highlight'].each { |key| document.update( {key => h[key]} || {} ) }
25
+
26
+ object = Configuration.wrapper.new(document)
27
+ # TODO: Figure out how to circumvent mass assignment protection for id in ActiveRecord
28
+ object.id = h['_id'] if object.respond_to?(:id=)
29
+ # TODO: Figure out how mark record as "not new record" in ActiveRecord
30
+ object.instance_variable_set(:@new_record, false) if object.respond_to?(:new_record?)
31
+ object
19
32
  end
20
33
  end
21
34
  @facets = response['facets']
@@ -25,6 +38,18 @@ module Slingshot
25
38
  @results.each(&block)
26
39
  end
27
40
 
41
+ def empty?
42
+ @results.empty?
43
+ end
44
+
45
+ def size
46
+ @results.size
47
+ end
48
+
49
+ def to_ary
50
+ self
51
+ end
52
+
28
53
  end
29
54
 
30
55
  end
@@ -7,13 +7,9 @@ module Slingshot
7
7
  # and leaving everything else alone.
8
8
  #
9
9
  def initialize(args={})
10
- if args.is_a? Hash
11
- args.each_pair do |key, value|
12
- self[key.to_sym] = value.is_a?(Hash) ? self.class.new(value) : value
13
- end
14
- super.replace self
15
- else
16
- super
10
+ raise ArgumentError, "Please pass a Hash-like object" unless args.respond_to?(:each_pair)
11
+ args.each_pair do |key, value|
12
+ self[key.to_sym] = value.respond_to?(:to_hash) ? self.class.new(value) : value
17
13
  end
18
14
  end
19
15
 
@@ -24,12 +20,19 @@ module Slingshot
24
20
  self.has_key?(method_name.to_sym) ? self[method_name.to_sym] : nil
25
21
  end
26
22
 
23
+ # Get ID
24
+ #
25
+ def id
26
+ self[:id]
27
+ end
28
+
27
29
  def inspect
28
30
  s = []; self.each { |k,v| s << "#{k}: #{v.inspect}" }
29
31
  %Q|<Item #{s.join(', ')}>|
30
32
  end
31
33
 
32
34
  alias_method :to_indexed_json, :to_json
35
+
33
36
  end
34
37
 
35
38
  end
@@ -0,0 +1,30 @@
1
+ module Slingshot
2
+ module Results
3
+
4
+ module Pagination
5
+
6
+ def total_entries
7
+ @total
8
+ end
9
+
10
+ def total_pages
11
+ result = @total.to_f / (@options[:per_page] ? @options[:per_page].to_i : 10 )
12
+ result < 1 ? 1 : result.round
13
+ end
14
+
15
+ def current_page
16
+ @options[:page].to_i
17
+ end
18
+
19
+ def previous_page
20
+ @options[:page].to_i - 1
21
+ end
22
+
23
+ def next_page
24
+ @options[:page].to_i + 1
25
+ end
26
+
27
+ end
28
+
29
+ end
30
+ end
@@ -0,0 +1,11 @@
1
+ # ActiveModel::Serialization Ruby < 1.9.x compatibility
2
+
3
+ class Symbol
4
+ def <=> other
5
+ self.to_s <=> other.to_s
6
+ end unless method_defined?(:'<=>')
7
+
8
+ def capitalize
9
+ to_s.capitalize
10
+ end unless method_defined?(:capitalize)
11
+ end
@@ -16,7 +16,8 @@ module Slingshot
16
16
  end
17
17
 
18
18
  def query(&block)
19
- @query = Query.new(&block)
19
+ @query = Query.new
20
+ block.arity < 1 ? @query.instance_eval(&block) : block.call(@query)
20
21
  self
21
22
  end
22
23
 
@@ -65,7 +66,7 @@ module Slingshot
65
66
  @url = "#{Configuration.url}/#{indices.join(',')}/_search"
66
67
  @response = Configuration.client.post(@url, self.to_json)
67
68
  @json = Yajl::Parser.parse(@response.body)
68
- @results = Results::Collection.new(@json)
69
+ @results = Results::Collection.new(@json, @options)
69
70
  self
70
71
  rescue Exception => error
71
72
  STDERR.puts "[REQUEST FAILED] #{self.to_curl}\n"
@@ -14,12 +14,15 @@ module Slingshot
14
14
  self.instance_eval(&block) if block_given?
15
15
  end
16
16
 
17
- def terms(field, size=10, options={})
18
- @value = { :terms => { :field => field, :size => size } }.update(options)
17
+ def terms(field, options={})
18
+ size = options.delete(:size) || 10
19
+ all_terms = options.delete(:all_terms) || false
20
+ @value = { :terms => { :field => field, :size => size, :all_terms => all_terms } }.update(options)
19
21
  self
20
22
  end
21
23
 
22
- def date(field, interval='day', options={})
24
+ def date(field, options={})
25
+ interval = options.delete(:interval) || 'day'
23
26
  @value = { :date_histogram => { :field => field, :interval => interval } }.update(options)
24
27
  self
25
28
  end
@@ -29,9 +32,8 @@ module Slingshot
29
32
  end
30
33
 
31
34
  def to_hash
32
- h = { @name => @value }
33
- h[@name].update @options
34
- return h
35
+ @value.update @options
36
+ { @name => @value }
35
37
  end
36
38
  end
37
39
 
@@ -7,8 +7,12 @@ module Slingshot
7
7
  class Filter
8
8
 
9
9
  def initialize(type, *options)
10
- @type = type
11
- @options = options || []
10
+ value = if options.size < 2
11
+ options.first || {}
12
+ else
13
+ options # An +or+ filter encodes multiple filters as an array
14
+ end
15
+ @hash = { type => value }
12
16
  end
13
17
 
14
18
  def to_json
@@ -16,12 +20,7 @@ module Slingshot
16
20
  end
17
21
 
18
22
  def to_hash
19
- initial = @options.size > 1 ? { @type => [] } : { @type => {} }
20
- method = initial[@type].is_a?(Hash) ? :update : :push
21
- @options.inject(initial) do |hash, option|
22
- hash[@type].send(method, option)
23
- hash
24
- end
23
+ @hash
25
24
  end
26
25
  end
27
26