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
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