cloud_search 0.0.1 → 0.0.2

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.
data/.gitignore CHANGED
@@ -15,3 +15,4 @@ spec/reports
15
15
  test/tmp
16
16
  test/version_tmp
17
17
  tmp
18
+ bin
data/.travis.yml ADDED
@@ -0,0 +1,3 @@
1
+ language: ruby
2
+ rvm:
3
+ - 1.9.3
data/Guardfile ADDED
@@ -0,0 +1,10 @@
1
+ # A sample Guardfile
2
+ # More info at https://github.com/guard/guard#readme
3
+
4
+ guard 'rspec', :version => 2 do
5
+ watch(%r{^spec/.+_spec\.rb$})
6
+ watch(%r{^lib/(.+)\.rb$}) { |m| "spec/lib/#{m[1]}_spec.rb" }
7
+ watch('spec/spec_helper.rb') { "spec" }
8
+ watch(%r{^spec/support/(.+)\.rb$}) { "spec" }
9
+ end
10
+
data/README.md CHANGED
@@ -1,3 +1,5 @@
1
+ [![Build Status](https://secure.travis-ci.org/willian/cloud_search.png)](http://travis-ci.org/willian/cloud_search)
2
+
1
3
  # CloudSearch
2
4
 
3
5
  TODO: Write a gem description
@@ -28,24 +30,22 @@ CloudSearch.configure do |config|
28
30
  end
29
31
 
30
32
  # Search for 'star wars' on 'imdb-movies'
31
- resp, msg = CloudSearch::Search.request("star wars",
32
- :actor,
33
- :director,
34
- :title,
35
- :year,
36
- :text_relevance)
33
+ search = CloudSearch::Search.new
34
+ resp = search.with_fields(:actor, :director, :title, :year, :text_relevance)
35
+ .query("star wars")
36
+ .request
37
37
 
38
38
  # Or you can search using part of the name
39
- resp, msg = CloudSearch::Search.request("matri*",
40
- :actor,
41
- :title,
42
- :year,
43
- :text_relevance)
39
+ search = CloudSearch::Search.new
40
+ resp = search.with_fields(:actor, :director, :title, :year, :text_relevance)
41
+ .query("matri*")
42
+ .request
44
43
 
45
44
  # Number of results
46
- resp["found"]
45
+ resp.hits
47
46
 
48
- resp["hit"].each do |result|
47
+ # Results
48
+ res.results.each do |result|
49
49
  movie = result["data"]
50
50
 
51
51
  # List of actors on the movie
@@ -60,6 +60,20 @@ resp["hit"].each do |result|
60
60
  end
61
61
  ```
62
62
 
63
+ ## Indexing documents
64
+
65
+ ``` ruby
66
+ document = CloudSearch::Document.new :type => "add", # or "delete"
67
+ :version => 123,
68
+ :id => 680, :lang => :en,
69
+ :fields => {:title => "Lord of the Rings"}
70
+
71
+ indexer = CloudSearch::Indexer.new
72
+ indexer << document # add as many documents as you want
73
+ indexer.index
74
+ ```
75
+
76
+
63
77
  ## Contributing
64
78
 
65
79
  1. Fork it
data/cloud_search.gemspec CHANGED
@@ -24,6 +24,9 @@ Gem::Specification.new do |gem|
24
24
  gem.add_development_dependency "simplecov" , "~> 0.6"
25
25
  gem.add_development_dependency "vcr" , "~> 2.2"
26
26
  gem.add_development_dependency "webmock"
27
+ gem.add_development_dependency "guard"
28
+ gem.add_development_dependency "guard-rspec"
29
+ gem.add_development_dependency "rb-fsevent"
27
30
 
28
31
  gem.add_dependency "em-http-request" , "~> 1.0"
29
32
  end
data/lib/cloud_search.rb CHANGED
@@ -1,9 +1,14 @@
1
1
  require "em-http"
2
+ require "json"
2
3
  require "cloud_search/version"
3
4
 
4
5
  module CloudSearch
5
- autoload :Config, "cloud_search/config"
6
- autoload :Search, "cloud_search/search"
6
+ autoload :Config, "cloud_search/config"
7
+ autoload :Searcher, "cloud_search/searcher"
8
+ autoload :SearchResponse, "cloud_search/search_response"
9
+ autoload :Indexer, "cloud_search/indexer"
10
+ autoload :Document, "cloud_search/document"
11
+ autoload :InvalidDocument, "cloud_search/invalid_document"
7
12
 
8
13
  def self.config
9
14
  Config.instance
@@ -21,7 +21,7 @@ module CloudSearch
21
21
  end
22
22
 
23
23
  def document_url
24
- @document_url ||= "http://doc-#{self.domain_name}-#{self.domain_id}.#{self.region}.cloudsearch.amazonaws.com"
24
+ @document_url ||= "http://doc-#{self.domain_name}-#{self.domain_id}.#{self.region}.cloudsearch.amazonaws.com/#{self.api_version}"
25
25
  end
26
26
 
27
27
  def region
@@ -29,7 +29,7 @@ module CloudSearch
29
29
  end
30
30
 
31
31
  def search_url
32
- @search_url ||= "http://search-#{self.domain_name}-#{self.domain_id}.#{self.region}.cloudsearch.amazonaws.com"
32
+ @search_url ||= "http://search-#{self.domain_name}-#{self.domain_id}.#{self.region}.cloudsearch.amazonaws.com/#{self.api_version}"
33
33
  end
34
34
  end
35
35
  end
@@ -0,0 +1,94 @@
1
+ module CloudSearch
2
+ class Document
3
+ MAX_VERSION = 4294967295
4
+
5
+ attr_accessor :type, :lang, :fields
6
+ attr_reader :errors, :id, :version
7
+
8
+ def initialize(attributes = {})
9
+ attributes.each_pair { |key, value| self.__send__("#{key}=", value) }
10
+ @errors = {}
11
+ end
12
+
13
+ def id=(_id)
14
+ @id = _id.to_s
15
+ end
16
+
17
+ def version=(_version)
18
+ begin
19
+ @version = Integer(_version)
20
+ rescue ArgumentError, TypeError
21
+ @version = _version
22
+ end
23
+ end
24
+
25
+ def valid?
26
+ @errors = {}
27
+ run_id_validations
28
+ run_version_validations
29
+ run_type_validations
30
+ run_lang_validations
31
+ run_fields_validations
32
+ errors.empty?
33
+ end
34
+
35
+ def as_json
36
+ {:type => type, :id => id, :version => version}.tap do |hash|
37
+ hash.merge!(:lang => lang, :fields => fields) if type == "add"
38
+ end
39
+ end
40
+
41
+ def to_json
42
+ JSON.unparse as_json
43
+ end
44
+
45
+ private
46
+
47
+ def run_id_validations
48
+ validate :id do |messages|
49
+ messages << "can't be blank" if blank?(:id)
50
+ messages << "is invalid" unless blank?(:id) or id =~ /\A[^_][a-z0-9_]+\z/
51
+ end
52
+ end
53
+
54
+ def run_version_validations
55
+ validate :version do |messages|
56
+ messages << "can't be blank" if blank?(:version)
57
+ messages << "is invalid" unless blank?(:version) or version.to_s =~ /\A[0-9]+\z/
58
+ messages << "must be less than #{MAX_VERSION + 1}" if messages.empty? and version > MAX_VERSION
59
+ end
60
+ end
61
+
62
+ def run_type_validations
63
+ validate :type do |messages|
64
+ messages << "can't be blank" if blank?(:type)
65
+ messages << "is invalid" if !blank?(:type) and !%w(add delete).include?(type)
66
+ end
67
+ end
68
+
69
+ def run_lang_validations
70
+ validate :lang do |messages|
71
+ messages << "can't be blank" if blank?(:lang)
72
+ messages << "is invalid" unless blank?(:lang) or lang =~ /\A[a-z]{2}\z/
73
+ end
74
+ end
75
+
76
+ def run_fields_validations
77
+ validate :fields do |messages|
78
+ messages << "can't be empty" if fields.nil?
79
+ messages << "must be an instance of Hash" if !fields.nil? and !fields.instance_of?(Hash)
80
+ end
81
+ end
82
+
83
+ def blank?(attr)
84
+ self.__send__(attr).to_s.strip.length.zero?
85
+ end
86
+
87
+ def validate(attr, &block)
88
+ messages = []
89
+ yield messages
90
+ errors[attr] = messages unless messages.empty?
91
+ end
92
+ end
93
+ end
94
+
@@ -0,0 +1,54 @@
1
+ module CloudSearch
2
+ class Indexer
3
+ def initialize
4
+ @documents = []
5
+ end
6
+
7
+ def <<(document)
8
+ raise InvalidDocument.new(document) unless document.valid?
9
+ @documents << document
10
+ end
11
+
12
+ alias :add :<<
13
+
14
+ def documents
15
+ @documents.freeze
16
+ end
17
+
18
+ def index
19
+ response, message = nil
20
+ EM.run do
21
+ http = EM::HttpRequest.new(url).post :body => documents_json, :head => headers
22
+
23
+ http.callback {
24
+ message = "#{http.response_header.status} - #{http.response.length} bytes\n#{url}\n"
25
+ response = JSON.parse(http.response)
26
+
27
+ EM.stop
28
+ }
29
+
30
+ http.errback {
31
+ message = "#{url}\n#{http.error}"
32
+
33
+ EM.stop
34
+ }
35
+ end
36
+
37
+ [response, message]
38
+ end
39
+
40
+ private
41
+
42
+ def headers
43
+ {"Content-Type" => "application/json"}
44
+ end
45
+
46
+ def documents_json
47
+ JSON.unparse(@documents.map(&:as_json))
48
+ end
49
+
50
+ def url
51
+ "#{CloudSearch.config.document_url}/documents/batch"
52
+ end
53
+ end
54
+ end
@@ -0,0 +1,11 @@
1
+ module CloudSearch
2
+ class InvalidDocument < StandardError
3
+ def initialize(document)
4
+ document.valid?
5
+ error_message = document.errors.map do
6
+ |attribute, errors| errors.empty? ? nil : "#{attribute}: #{errors.join(", ")}"
7
+ end.join("; ")
8
+ super error_message
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,23 @@
1
+ module CloudSearch
2
+ class SearchResponse
3
+ attr_accessor :body
4
+ attr_accessor :http_code
5
+
6
+ def results
7
+ (_hits and _hits['hit']) or []
8
+ end
9
+
10
+ def hits
11
+ (_hits and _hits['found']) or 0
12
+ end
13
+
14
+ def found?
15
+ hits >= 1
16
+ end
17
+
18
+ private
19
+ def _hits
20
+ body and body['hits']
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,48 @@
1
+ module CloudSearch
2
+ class Searcher
3
+
4
+ def search
5
+ response = SearchResponse.new
6
+
7
+ EM.run do
8
+ http = EM::HttpRequest.new(url).get
9
+
10
+ http.callback do
11
+ response.http_code = http.response_header.status
12
+ response.body = JSON.parse(http.response)
13
+
14
+ EM.stop
15
+ end
16
+
17
+ http.errback do
18
+ response.http_code = http.error
19
+ response.body = http.response
20
+
21
+ EM.stop
22
+ end
23
+ end
24
+
25
+ response
26
+ end
27
+
28
+ def query(q)
29
+ @query = q
30
+ self
31
+ end
32
+
33
+ def with_fields(*fields)
34
+ @fields = fields
35
+ self
36
+ end
37
+
38
+ private
39
+
40
+ def url
41
+ url = CloudSearch.config.search_url
42
+ url+= "/search"
43
+ url+= "?q=#{CGI.escape(@query)}"
44
+ url+= "&return-fields=#{CGI.escape(@fields.join(","))}" if @fields.any?
45
+ end
46
+ end
47
+ end
48
+
@@ -1,3 +1,3 @@
1
1
  module CloudSearch
2
- VERSION = "0.0.1"
2
+ VERSION = "0.0.2"
3
3
  end
@@ -15,9 +15,9 @@ describe CloudSearch::Config do
15
15
  it { expect(subject.configuration_url).to eql("https://cloudsearch.us-east-1.amazonaws.com") }
16
16
  it { expect(subject.domain_id).to eql("pl6u4t3elu7dhsbwaqbsy3y6be") }
17
17
  it { expect(subject.domain_name).to eql("imdb-movies") }
18
- it { expect(subject.document_url).to eql("http://doc-imdb-movies-pl6u4t3elu7dhsbwaqbsy3y6be.us-east-1.cloudsearch.amazonaws.com") }
18
+ it { expect(subject.document_url).to eql("http://doc-imdb-movies-pl6u4t3elu7dhsbwaqbsy3y6be.us-east-1.cloudsearch.amazonaws.com/2011-02-01") }
19
19
  it { expect(subject.region).to eql("us-east-1") }
20
- it { expect(subject.search_url).to eql("http://search-imdb-movies-pl6u4t3elu7dhsbwaqbsy3y6be.us-east-1.cloudsearch.amazonaws.com") }
20
+ it { expect(subject.search_url).to eql("http://search-imdb-movies-pl6u4t3elu7dhsbwaqbsy3y6be.us-east-1.cloudsearch.amazonaws.com/2011-02-01") }
21
21
  end
22
22
  end
23
23
 
@@ -0,0 +1,315 @@
1
+ # encoding: utf-8
2
+
3
+ require "spec_helper"
4
+
5
+ describe CloudSearch::Document do
6
+ it "has a 'id' attribute" do
7
+ expect(described_class.new(:id => 123).id).to eq("123")
8
+ end
9
+
10
+ it "has a 'type' attribute" do
11
+ expect(described_class.new(:type => "add").type).to eq("add")
12
+ end
13
+
14
+ it "has a 'version' attribute" do
15
+ expect(described_class.new(:version => 1234).version).to eq(1234)
16
+ end
17
+
18
+ it "has a 'lang' attribute" do
19
+ expect(described_class.new(:lang => "en").lang).to eq("en")
20
+ end
21
+
22
+ it "has a 'fields' attribute" do
23
+ expect(described_class.new(:fields => {:foo => "bar"}).fields).to eq(:foo => "bar")
24
+ end
25
+
26
+ it "clears errors between validations" do
27
+ document = described_class.new :id => nil
28
+ expect(document).to_not be_valid
29
+ document.id = "123"
30
+ document.valid?
31
+ expect(document.errors[:id]).to be_nil
32
+ end
33
+
34
+ context "id validation" do
35
+ it "is invalid without an id" do
36
+ document = described_class.new
37
+ document.valid?
38
+ expect(document.errors[:id]).to eq(["can't be blank"])
39
+ end
40
+
41
+ %w(- ? A & * ç à @ % $ ! = +).each do |char|
42
+ it "is invalid containing #{char}" do
43
+ document = described_class.new :id => "1#{char}2"
44
+ document.valid?
45
+ expect(document.errors[:id]).to eq(["is invalid"])
46
+ end
47
+ end
48
+
49
+ it "is invalid starting with an '_'" do
50
+ document = described_class.new :id => "_abc"
51
+ document.valid?
52
+ expect(document.errors[:id]).to eq(["is invalid"])
53
+ end
54
+
55
+ it "is invalid with a string containing only spaces" do
56
+ document = described_class.new :id => " "
57
+ document.valid?
58
+ expect(document.errors[:id]).to eq(["can't be blank"])
59
+ end
60
+
61
+ it "is valid with a valid id" do
62
+ document = described_class.new :id => "507c54a44a42c408f4000001"
63
+ document.valid?
64
+ expect(document.errors[:id]).to be_nil
65
+ end
66
+
67
+ it "is valid with integers" do
68
+ document = described_class.new :id => 123
69
+ document.valid?
70
+ expect(document.errors[:id]).to be_nil
71
+ end
72
+
73
+ it "converts integers to strings" do
74
+ expect(described_class.new(:id => 123).id).to eq("123")
75
+ end
76
+ end
77
+
78
+ context "version validation" do
79
+ it "is invalid with a non numeric value" do
80
+ document = described_class.new :version => "123a3545656"
81
+ document.valid?
82
+ expect(document.errors[:version]).to eq(["is invalid"])
83
+ end
84
+
85
+ it "is invalid with a nil value" do
86
+ document = described_class.new :version => nil
87
+ document.valid?
88
+ expect(document.errors[:version]).to eq(["can't be blank"])
89
+ end
90
+
91
+ it "converts strings to integers" do
92
+ expect(described_class.new(:version => "123").version).to eq(123)
93
+ end
94
+
95
+ it "does not convert strings to integers if they contain non numerical characters" do
96
+ expect(described_class.new(:version => "123abc567").version).to eq("123abc567")
97
+ end
98
+
99
+ it "is invalid if value is greater than CloudSearch::Document::MAX_VERSION" do
100
+ document = described_class.new :version => 4294967296
101
+ document.valid?
102
+ expect(document.errors[:version]).to eq(["must be less than 4294967296"])
103
+ end
104
+
105
+ it "is valid with integers greater than zero and less or equal to CloudSearch::Document::MAX_VERSION" do
106
+ document = described_class.new :version => 4294967295
107
+ document.valid?
108
+ expect(document.errors[:version]).to be_nil
109
+ end
110
+ end
111
+
112
+ context "type validation" do
113
+ it "is valid if type is 'add'" do
114
+ document = described_class.new :type => "add"
115
+ document.valid?
116
+ expect(document.errors[:type]).to be_nil
117
+ end
118
+
119
+ it "is valid if type is 'delete'" do
120
+ document = described_class.new :type => "delete"
121
+ document.valid?
122
+ expect(document.errors[:type]).to be_nil
123
+ end
124
+
125
+ it "is invalid if type is anything else" do
126
+ document = described_class.new :type => "wrong"
127
+ document.valid?
128
+ expect(document.errors[:type]).to eq(["is invalid"])
129
+ end
130
+
131
+ it "is invalid if type is nil" do
132
+ document = described_class.new :type => nil
133
+ document.valid?
134
+ expect(document.errors[:type]).to eq(["can't be blank"])
135
+ end
136
+
137
+ it "is invalid if type is a blank string" do
138
+ document = described_class.new :type => " "
139
+ document.valid?
140
+ expect(document.errors[:type]).to eq(["can't be blank"])
141
+ end
142
+ end
143
+
144
+ context "lang validation" do
145
+ it "is invalid if lang is nil" do
146
+ document = described_class.new :lang => nil
147
+ document.valid?
148
+ expect(document.errors[:lang]).to eql(["can't be blank"])
149
+ end
150
+
151
+ it "is invalid if lang contains digits" do
152
+ document = described_class.new :lang => "a1"
153
+ document.valid?
154
+ expect(document.errors[:lang]).to eql(["is invalid"])
155
+ end
156
+
157
+ it "is invalid if lang contains more than 2 characters" do
158
+ document = described_class.new :lang => "abc"
159
+ document.valid?
160
+ expect(document.errors[:lang]).to eql(["is invalid"])
161
+ end
162
+
163
+ it "is invalid if lang contains upper case characters" do
164
+ document = described_class.new :lang => "Ab"
165
+ document.valid?
166
+ expect(document.errors[:lang]).to eql(["is invalid"])
167
+ end
168
+
169
+ it "is valid if lang contains 2 lower case characters" do
170
+ document = described_class.new :lang => "en"
171
+ document.valid?
172
+ expect(document.errors[:lang]).to be_nil
173
+ end
174
+ end
175
+
176
+ context "fields validation" do
177
+ it "is invalid if fields is nil" do
178
+ document = described_class.new :fields => nil
179
+ document.valid?
180
+ expect(document.errors[:fields]).to eql(["can't be empty"])
181
+ end
182
+
183
+ it "is invalid if fields is not a hash" do
184
+ document = described_class.new :fields => []
185
+ document.valid?
186
+ expect(document.errors[:fields]).to eql(["must be an instance of Hash"])
187
+ end
188
+
189
+ it "is valid with a Hash" do
190
+ document = described_class.new :fields => {}
191
+ document.valid?
192
+ expect(document.errors[:fields]).to be_nil
193
+ end
194
+ end
195
+
196
+ context "#as_json" do
197
+ let(:attributes) { {
198
+ :type => type,
199
+ :id => "123abc",
200
+ :version => 123456,
201
+ :lang => "pt",
202
+ :fields => {:foo => "bar"}
203
+ } }
204
+ let(:document) { described_class.new attributes }
205
+ let(:as_json) { document.as_json }
206
+
207
+ context "when 'type' is 'add'" do
208
+ let(:type) { "add" }
209
+
210
+ it "includes the 'type' attribute" do
211
+ expect(as_json[:type]).to eq("add")
212
+ end
213
+
214
+ it "includes the 'id' attribute" do
215
+ expect(as_json[:id]).to eq("123abc")
216
+ end
217
+
218
+ it "includes the 'version' attribute" do
219
+ expect(as_json[:version]).to eq(123456)
220
+ end
221
+
222
+ it "includes the 'lang' attribute" do
223
+ expect(as_json[:lang]).to eq("pt")
224
+ end
225
+
226
+ it "includes the 'fields' attribute" do
227
+ expect(as_json[:fields]).to eq(:foo => "bar")
228
+ end
229
+ end
230
+
231
+ context "when 'type' is 'delete'" do
232
+ let(:type) { "delete" }
233
+
234
+ it "includes the 'type' attribute" do
235
+ expect(as_json[:type]).to eq("delete")
236
+ end
237
+
238
+ it "includes the 'id' attribute" do
239
+ expect(as_json[:id]).to eq("123abc")
240
+ end
241
+
242
+ it "includes the 'version' attribute" do
243
+ expect(as_json[:version]).to eq(123456)
244
+ end
245
+
246
+ it "does not include the 'lang' attribute" do
247
+ expect(as_json[:lang]).to be_nil
248
+ end
249
+
250
+ it "does not include the 'fields' attribute" do
251
+ expect(as_json[:fields]).to be_nil
252
+ end
253
+ end
254
+ end
255
+
256
+ context "#to_json" do
257
+ let(:attributes) { {
258
+ :type => type,
259
+ :id => "123abc",
260
+ :version => 123456,
261
+ :lang => "pt",
262
+ :fields => {:foo => "bar"}
263
+ } }
264
+ let(:parsed_json) { JSON.parse(described_class.new(attributes).to_json) }
265
+
266
+ context "when 'type' is 'add'" do
267
+ let(:type) { "add" }
268
+
269
+ it "includes the 'type' attribute" do
270
+ expect(parsed_json["type"]).to eq("add")
271
+ end
272
+
273
+ it "includes the 'id' attribute" do
274
+ expect(parsed_json["id"]).to eq("123abc")
275
+ end
276
+
277
+ it "includes the 'version' attribute" do
278
+ expect(parsed_json["version"]).to eq(123456)
279
+ end
280
+
281
+ it "includes the 'lang' attribute" do
282
+ expect(parsed_json["lang"]).to eq("pt")
283
+ end
284
+
285
+ it "includes the 'fields' attribute" do
286
+ expect(parsed_json["fields"]).to eq("foo" => "bar")
287
+ end
288
+ end
289
+
290
+ context "when 'type' is 'delete'" do
291
+ let(:type) { "delete" }
292
+
293
+ it "includes the 'type' attribute" do
294
+ expect(parsed_json["type"]).to eq("delete")
295
+ end
296
+
297
+ it "includes the 'id' attribute" do
298
+ expect(parsed_json["id"]).to eq("123abc")
299
+ end
300
+
301
+ it "includes the 'version' attribute" do
302
+ expect(parsed_json["version"]).to eq(123456)
303
+ end
304
+
305
+ it "does not include the 'lang' attribute" do
306
+ expect(parsed_json["lang"]).to be_nil
307
+ end
308
+
309
+ it "does not include the 'fields' attribute" do
310
+ expect(parsed_json["fields"]).to be_nil
311
+ end
312
+ end
313
+ end
314
+ end
315
+