cloud_search 0.0.1 → 0.0.2

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