slingshot-rb 0.0.8 → 0.0.9

Sign up to get free protection for your applications and to get access to all the features.
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