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
data/lib/slingshot.rb CHANGED
@@ -1,7 +1,9 @@
1
1
  require 'rest_client'
2
2
  require 'yajl/json_gem'
3
+ require 'active_model'
3
4
 
4
5
  require 'slingshot/rubyext/hash'
6
+ require 'slingshot/rubyext/symbol'
5
7
  require 'slingshot/logger'
6
8
  require 'slingshot/configuration'
7
9
  require 'slingshot/client'
@@ -12,10 +14,21 @@ require 'slingshot/search/sort'
12
14
  require 'slingshot/search/facet'
13
15
  require 'slingshot/search/filter'
14
16
  require 'slingshot/search/highlight'
17
+ require 'slingshot/results/pagination'
15
18
  require 'slingshot/results/collection'
16
19
  require 'slingshot/results/item'
17
20
  require 'slingshot/index'
18
21
  require 'slingshot/dsl'
22
+ require 'slingshot/model/naming'
23
+ require 'slingshot/model/callbacks'
24
+ require 'slingshot/model/search'
25
+ require 'slingshot/model/indexing'
26
+ require 'slingshot/model/import'
27
+ require 'slingshot/model/persistence/finders'
28
+ require 'slingshot/model/persistence/attributes'
29
+ require 'slingshot/model/persistence/storage'
30
+ require 'slingshot/model/persistence'
31
+ require 'slingshot/tasks'
19
32
 
20
33
  module Slingshot
21
34
  extend DSL
@@ -4,12 +4,18 @@ module Slingshot
4
4
 
5
5
  class Base
6
6
  def get(url)
7
- raise NoMethodError, "Implement this method in your client class"
7
+ raise_no_method_error
8
8
  end
9
9
  def post(url, data)
10
+ raise_no_method_error
11
+ end
12
+ def put(url, data)
10
13
  raise NoMethodError, "Implement this method in your client class"
11
14
  end
12
15
  def delete(url)
16
+ raise_no_method_error
17
+ end
18
+ def raise_no_method_error
13
19
  raise NoMethodError, "Implement this method in your client class"
14
20
  end
15
21
  end
@@ -21,6 +27,9 @@ module Slingshot
21
27
  def self.post(url, data)
22
28
  ::RestClient.post url, data
23
29
  end
30
+ def self.put(url, data)
31
+ ::RestClient.put url, data
32
+ end
24
33
  def self.delete(url)
25
34
  ::RestClient.delete url rescue nil
26
35
  end
data/lib/slingshot/dsl.rb CHANGED
@@ -6,7 +6,23 @@ module Slingshot
6
6
  end
7
7
 
8
8
  def search(indices, options={}, &block)
9
- Search::Search.new(indices, options, &block).perform
9
+ if block_given?
10
+ Search::Search.new(indices, options, &block).perform
11
+ else
12
+ payload = case options
13
+ when Hash then options.to_json
14
+ when String then options
15
+ else raise ArgumentError, "Please pass a Ruby Hash or String with JSON"
16
+ end
17
+
18
+ response = Configuration.client.post( "#{Configuration.url}/#{indices}/_search", payload)
19
+ json = Yajl::Parser.parse(response.body)
20
+ results = Results::Collection.new(json, options)
21
+ end
22
+ rescue Exception => error
23
+ STDERR.puts "[REQUEST FAILED] #{error.class} #{error.http_body rescue nil}\n"
24
+ raise
25
+ ensure
10
26
  end
11
27
 
12
28
  def index(name, &block)
@@ -1,15 +1,23 @@
1
1
  module Slingshot
2
2
  class Index
3
3
 
4
+ attr_reader :name
5
+
4
6
  def initialize(name, &block)
5
7
  @name = name
6
8
  instance_eval(&block) if block_given?
7
9
  end
8
10
 
11
+ def exists?
12
+ !!Configuration.client.get("#{Configuration.url}/#{@name}/_status")
13
+ rescue Exception => error
14
+ false
15
+ end
16
+
9
17
  def delete
10
18
  # FIXME: RestClient does not return response for DELETE requests?
11
19
  @response = Configuration.client.delete "#{Configuration.url}/#{@name}"
12
- return @response =~ /error/ ? false : true
20
+ return @response.body =~ /error/ ? false : true
13
21
  rescue Exception => error
14
22
  false
15
23
  ensure
@@ -28,7 +36,8 @@ module Slingshot
28
36
  end
29
37
 
30
38
  def mapping
31
- JSON.parse( Configuration.client.get("#{Configuration.url}/#{@name}/_mapping") )[@name]
39
+ @response = Configuration.client.get("#{Configuration.url}/#{@name}/_mapping")
40
+ JSON.parse(@response.body)[@name]
32
41
  end
33
42
 
34
43
  def store(*args)
@@ -56,7 +65,7 @@ module Slingshot
56
65
  url = id ? "#{Configuration.url}/#{@name}/#{type}/#{id}" : "#{Configuration.url}/#{@name}/#{type}/"
57
66
 
58
67
  @response = Configuration.client.post url, document
59
- JSON.parse(@response)
68
+ JSON.parse(@response.body)
60
69
 
61
70
  rescue Exception => error
62
71
  raise
@@ -65,14 +74,107 @@ module Slingshot
65
74
  logged(error, "/#{@name}/#{type}/", curl)
66
75
  end
67
76
 
77
+ def bulk_store documents
78
+ create unless exists?
79
+
80
+ payload = documents.map do |document|
81
+ old_verbose, $VERBOSE = $VERBOSE, nil # Silence Object#id deprecation warnings
82
+ id = case
83
+ when document.is_a?(Hash) then document[:id] || document['id']
84
+ when document.respond_to?(:id) && document.id != document.object_id then document.id
85
+ # TODO: Raise error when no id present
86
+ end
87
+ $VERBOSE = old_verbose
88
+
89
+ type = case
90
+ when document.is_a?(Hash) then document[:type] || document['type']
91
+ when document.respond_to?(:document_type) then document.document_type
92
+ end || 'document'
93
+
94
+ output = []
95
+ output << %Q|{"index":{"_index":"#{@name}","_type":"#{type}","_id":"#{id}"}}|
96
+ output << document.to_indexed_json
97
+ output.join("\n")
98
+ end
99
+ payload << ""
100
+
101
+ tries = 5
102
+ count = 0
103
+
104
+ begin
105
+ # STDERR.puts "Posting payload..."
106
+ # STDERR.puts payload.join("\n")
107
+ Configuration.client.post("#{Configuration.url}/_bulk", payload.join("\n"))
108
+ rescue Exception => error
109
+ if count < tries
110
+ count += 1
111
+ STDERR.puts "[ERROR] #{error.message}:#{error.http_body rescue nil}, retrying (#{count})..."
112
+ retry
113
+ else
114
+ STDERR.puts "[ERROR] Too many exceptions occured, giving up..."
115
+ STDERR.puts "Response: #{error.http_body rescue nil}"
116
+ raise
117
+ end
118
+ ensure
119
+ curl = %Q|curl -X POST "#{Configuration.url}/_bulk" -d '{... data omitted ...}'|
120
+ logged(error, 'BULK', curl)
121
+ end
122
+ end
123
+
124
+ def import(klass_or_collection, method=nil, options={})
125
+ # p [klass_or_collection, method, options]
126
+
127
+ case
128
+
129
+ when method
130
+ options = {:page => 1, :per_page => 1000}.merge options
131
+ while documents = klass_or_collection.send(method.to_sym, options.merge(:page => options[:page])) \
132
+ and not documents.empty?
133
+ documents = yield documents if block_given?
134
+
135
+ bulk_store documents
136
+ options[:page] += 1
137
+ end
138
+
139
+ when klass_or_collection.respond_to?(:map)
140
+ documents = block_given? ? yield(klass_or_collection) : klass_or_collection
141
+ bulk_store documents
142
+ else
143
+ raise ArgumentError, "Please pass either a collection of objects, "+
144
+ "or method for fetching records, or Enumerable compatible class"
145
+ end
146
+ end
147
+
148
+ def remove(*args)
149
+ # TODO: Infer type from the document (hash property, method)
150
+
151
+ if args.size > 1
152
+ (type, document = args)
153
+ else
154
+ (document = args.pop; type = :document)
155
+ end
156
+
157
+ old_verbose, $VERBOSE = $VERBOSE, nil # Silence Object#id deprecation warnings
158
+ id = case true
159
+ when document.is_a?(Hash) then document[:id] || document['id']
160
+ when document.respond_to?(:id) && document.id != document.object_id then document.id
161
+ else document
162
+ end
163
+ $VERBOSE = old_verbose
164
+
165
+ result = Configuration.client.delete "#{Configuration.url}/#{@name}/#{type}/#{id}"
166
+ JSON.parse(result) if result
167
+ end
168
+
68
169
  def retrieve(type, id)
69
170
  @response = Configuration.client.get "#{Configuration.url}/#{@name}/#{type}/#{id}"
70
- h = JSON.parse(@response)
171
+ h = JSON.parse(@response.body)
71
172
  if Configuration.wrapper == Hash then h
72
173
  else
73
- document = h['_source'] ? h['_source'] : h['fields']
74
- h.update document if document
75
- Configuration.wrapper.new(h)
174
+ document = {}
175
+ document = h['_source'] ? document.update( h['_source'] ) : document.update( h['fields'] )
176
+ document.update('id' => h['_id'], '_version' => h['_version'])
177
+ Configuration.wrapper.new(document)
76
178
  end
77
179
  end
78
180
 
@@ -0,0 +1,23 @@
1
+ module Slingshot
2
+ module Model
3
+
4
+ module Callbacks
5
+
6
+ def self.included(base)
7
+ if base.respond_to?(:after_save) && base.respond_to?(:after_destroy)
8
+ base.send :after_save, :update_elastic_search_index
9
+ base.send :after_destroy, :update_elastic_search_index
10
+ end
11
+
12
+ if base.respond_to?(:before_destroy) && !base.respond_to?(:destroyed?)
13
+ base.class_eval do
14
+ before_destroy { @destroyed = true }
15
+ def destroyed?; !!@destroyed; end
16
+ end
17
+ end
18
+ end
19
+
20
+ end
21
+
22
+ end
23
+ end
@@ -0,0 +1,18 @@
1
+ module Slingshot
2
+ module Model
3
+
4
+ module Import
5
+
6
+ module ClassMethods
7
+
8
+ def import options={}, &block
9
+ method = options.delete(:method) || 'paginate'
10
+ self.index.import self, method, options, &block
11
+ end
12
+
13
+ end
14
+
15
+ end
16
+
17
+ end
18
+ end
@@ -0,0 +1,50 @@
1
+ module Slingshot
2
+ module Model
3
+
4
+ module Indexing
5
+
6
+ module ClassMethods
7
+
8
+ def mapping
9
+ if block_given?
10
+ @store_mapping = true
11
+ yield
12
+ @store_mapping = false
13
+ create_index_or_update_mapping
14
+ else
15
+ @mapping ||= {}
16
+ end
17
+ end
18
+
19
+ def indexes(name, options = {})
20
+ # p "#{self}, SEARCH PROPERTY, #{name}"
21
+ mapping[name] = options
22
+ end
23
+
24
+ def store_mapping?
25
+ @store_mapping || false
26
+ end
27
+
28
+ def create_index_or_update_mapping
29
+ # STDERR.puts "Creating index with mapping", mapping_to_hash.inspect
30
+ # STDERR.puts "Index exists?, #{index.exists?}"
31
+ unless index.exists?
32
+ index.create :mappings => mapping_to_hash
33
+ else
34
+ # TODO: Update mapping
35
+ end
36
+ rescue Exception => e
37
+ # TODO: STDERR + logger
38
+ raise
39
+ end
40
+
41
+ def mapping_to_hash
42
+ { document_type.to_sym => { :properties => mapping } }
43
+ end
44
+
45
+ end
46
+
47
+ end
48
+
49
+ end
50
+ end
@@ -0,0 +1,30 @@
1
+ module Slingshot
2
+ module Model
3
+
4
+ module Naming
5
+
6
+ module ClassMethods
7
+ def index_name name=nil
8
+ @index_name = name if name
9
+ @index_name || model_name.plural
10
+ end
11
+
12
+ def document_type
13
+ model_name.singular
14
+ end
15
+ end
16
+
17
+ module InstanceMethods
18
+ def index_name
19
+ self.class.index_name
20
+ end
21
+
22
+ def document_type
23
+ self.class.document_type
24
+ end
25
+ end
26
+
27
+ end
28
+
29
+ end
30
+ end
@@ -0,0 +1,34 @@
1
+ module Slingshot
2
+ module Model
3
+
4
+ module Persistence
5
+
6
+ def self.included(base)
7
+
8
+ base.class_eval do
9
+ include ActiveModel::AttributeMethods
10
+ include ActiveModel::Validations
11
+ include ActiveModel::Serialization
12
+ include ActiveModel::Serializers::JSON
13
+ include ActiveModel::Naming
14
+ include ActiveModel::Conversion
15
+
16
+ extend ActiveModel::Callbacks
17
+ define_model_callbacks :save, :destroy
18
+
19
+ include Slingshot::Model::Search
20
+ include Slingshot::Model::Callbacks
21
+
22
+ extend Persistence::Finders::ClassMethods
23
+ extend Persistence::Attributes::ClassMethods
24
+ include Persistence::Attributes::InstanceMethods
25
+
26
+ include Persistence::Storage
27
+ end
28
+
29
+ end
30
+
31
+ end
32
+
33
+ end
34
+ end
@@ -0,0 +1,60 @@
1
+ module Slingshot
2
+ module Model
3
+
4
+ module Persistence
5
+
6
+ module Attributes
7
+
8
+ module ClassMethods
9
+
10
+ def property(name, options = {})
11
+ # p "#{self}, PERSISTENCE PROPERTY, #{name}"
12
+ attr_accessor name.to_sym
13
+ properties << name.to_s unless properties.include?(name.to_s)
14
+ define_query_method name.to_sym
15
+ define_attribute_methods [name.to_sym]
16
+ mapping[name] = options if store_mapping?
17
+ self
18
+ end
19
+
20
+ def properties
21
+ @properties ||= []
22
+ end
23
+
24
+ private
25
+
26
+ def define_query_method name
27
+ define_method("#{name}?") { !! send(name) }
28
+ end
29
+
30
+ end
31
+
32
+ module InstanceMethods
33
+
34
+ attr_accessor :id
35
+
36
+ def initialize(attributes={})
37
+ attributes.each { |name, value| send("#{name}=", value) }
38
+ end
39
+
40
+ def attributes
41
+ self.class.properties.
42
+ inject( self.id ? {'id' => self.id} : {} ) {|attributes, key| attributes[key] = send(key); attributes}
43
+ end
44
+
45
+ def attribute_names
46
+ self.class.properties.sort
47
+ end
48
+
49
+ def has_attribute?(name)
50
+ properties.include?(name.to_s)
51
+ end
52
+ alias :has_property? :has_attribute?
53
+
54
+ end
55
+
56
+ end
57
+
58
+ end
59
+ end
60
+ end