cloudsearchable 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
data/.rspec ADDED
@@ -0,0 +1,2 @@
1
+ --color
2
+ --format documentation
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # gem's dependencies in cloudsearchable.gemspec
4
+ gemspec
@@ -0,0 +1,22 @@
1
+ Copyright (c) 2013 Lane LaRue
2
+
3
+ MIT License
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining
6
+ a copy of this software and associated documentation files (the
7
+ "Software"), to deal in the Software without restriction, including
8
+ without limitation the rights to use, copy, modify, merge, publish,
9
+ distribute, sublicense, and/or sell copies of the Software, and to
10
+ permit persons to whom the Software is furnished to do so, subject to
11
+ the following conditions:
12
+
13
+ The above copyright notice and this permission notice shall be
14
+ included in all copies or substantial portions of the Software.
15
+
16
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
19
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
20
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
21
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
22
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
@@ -0,0 +1,119 @@
1
+ # Cloudsearchable
2
+ An ActiveRecord-style ORM query interface for AWS Cloud Search.
3
+
4
+ ## Installation
5
+ Add to your Gemfile: gem 'cloudsearchable'. Run bundle or: gem install cloudsearchable.
6
+
7
+ ## Usage
8
+ ### 1. Mix Cloudsearchable into your class
9
+ ```ruby
10
+ class Customer
11
+ include Cloudsearchable
12
+
13
+ attr_accessor :id, :customer, :name, :lock_version
14
+
15
+ # This is the default index. You probably only need one.
16
+ index_in_cloudsearch do |idx|
17
+ literal :id, :searchable => true
18
+ end
19
+
20
+ # A named index.
21
+ index_in_cloudsearch :test_index do |idx|
22
+
23
+ # Fetch the customer_id field from customer
24
+ literal :customer_id, :returnable => true, :searchable => true, :source => Proc.new { customer }
25
+
26
+ # Map the 'name' Ruby attribute to a field called 'test_name'
27
+ text :test_name, :returnable => false, :searchable => true, :source => :name
28
+
29
+ # uint fields can be used in result ranking functions
30
+ uint :helpfulness, :returnable => true, :searchable => false do; 1234 end
31
+ end
32
+ end
33
+ ```
34
+ ### 2. Index some objects
35
+ ```ruby
36
+ c = Customer.new
37
+ c.add_to_indexes
38
+ c.update_indexes
39
+ c.remove_from_indexes
40
+ ```
41
+ ### 3. Start querying
42
+ ```ruby
43
+ Customer.search.where(customer_id: 12345)
44
+ Customer.search.where(customer_id: 12345).order('-helpfulness') # ordering
45
+ Customer.search.where(customer_id: 12345).limit(10) # limit, default 100000
46
+ Customer.search.where(customer_id: 12345).offset(100) # offset
47
+ Customer.search.where(customer_id: 12345).found_count # count
48
+
49
+ Customer.search.where(customer_id: 12345).where(helpfulness: 42) # query chain
50
+ Customer.search.where(customer_id: 12345, helpfulness: 42) # query chain from hash
51
+ Customer.search.where(:category, :any, ["big", "small"]) # multiple values
52
+ Customer.search.where(:customer_id, :!=, 1234) # "not equal to" operator
53
+ Customer.search.text('test') # text search
54
+ Customer.search.text('test').where(:featured, :==, 'f') # text search with other fields
55
+
56
+ Customer.search.where(:helpfulness, :within_range, 0..123) # uint range query, string range works too
57
+ Customer.search.where(:helpfulness, :>, 123) # uint greather than
58
+ Customer.search.where(:helpfulness, :>=, 123) # uint greather than or equal to
59
+ Customer.search.where(:helpfulness, :<, 123) # uint less than
60
+ Customer.search.where(:helpfulness, :<=, 123) # uint less than or equal to
61
+ ```
62
+ These queries return a Cloudsearchable::Query, calling .to_a or .found_count will fetch the results
63
+ ```ruby
64
+ Customer.search.where(customer_id: 12345).each |customer|
65
+ p "#{customer.class}: #{customer.name}"
66
+ end
67
+ # Customer: foo
68
+ # Customer: bar
69
+ ```
70
+ ### Configuration
71
+ ```ruby
72
+ # config\initializers\cloudsearchable_config.rb
73
+
74
+ require 'cloudsearchable'
75
+
76
+ Cloudsearchable.configure do |config|
77
+ config.domain_prefix = "dev-lane-"
78
+ end
79
+ ```
80
+ Supported Options
81
+ * domain_prefix - A name prefix string for your domains in CloudSearch. Defaults to Rails.env, or "" if Rails is undefined.
82
+ * config.fatal_warnings - raises WarningInQueryResult exception on warning. Defaults to false.
83
+ * config.logger - a custom logger, defaults to Rails if defined.
84
+
85
+ ### Other Features
86
+
87
+ Cloudsearchable provides access the underlying AWS client objects, such as '''CloudSearch.client''' and '''class.cloudsearch_domains'''. For example here is how to drop domains associated with Customer class:
88
+
89
+ ```ruby
90
+ client = CloudSearch.client
91
+ Customer.cloudsearch_domains.each do |key, domain|
92
+ domain_name = domain.name
93
+ puts "...dropping #{domain_name}"
94
+ client.delete_domain(:domain_name => domain_name)
95
+ end
96
+ ```
97
+
98
+ See spec tests and source code for more information.
99
+
100
+ ## TODO:
101
+ - use ActiveSupport instrumentation: http://edgeguides.rubyonrails.org/active_support_instrumentation.html#creating-custom-events
102
+
103
+ ## Credits
104
+
105
+ * [Logan Bowers](https://github.com/loganb)
106
+ * [Peter Abrahamsen](https://github.com/rainhead)
107
+ * [Lane LaRue](https://github.com/luxx)
108
+ * [Philip White](https://github.com/philipmw)
109
+
110
+ MIT License
111
+
112
+ ## Contributing
113
+
114
+ 1. Fork it
115
+ 2. Create your feature branch (`git checkout -b my-new-feature`)
116
+ 3. Run the tests (`rake spec`)
117
+ 4. Commit your changes (`git commit -am 'Add some feature'`)
118
+ 5. Push to the branch (`git push origin my-new-feature`)
119
+ 6. Create new Pull Request
@@ -0,0 +1,11 @@
1
+ require 'rspec/core/rake_task'
2
+ RSpec::Core::RakeTask.new('spec')
3
+
4
+ # make spec test the default task
5
+ task :default => :spec
6
+
7
+ require 'yard'
8
+ YARD::Rake::YardocTask.new do |t|
9
+ t.files = ['lib/**/*.rb', "README", "LICENSE"] # optional
10
+ t.options = ['-m', 'markdown'] # optional
11
+ end
@@ -0,0 +1,50 @@
1
+ # coding: utf-8
2
+ lib = File.expand_path('../lib', __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require 'cloudsearchable/version'
5
+
6
+ Gem::Specification.new do |spec|
7
+ spec.name = "cloudsearchable"
8
+ spec.version = Cloudsearchable::VERSION
9
+ spec.authors = ["Lane LaRue"]
10
+ spec.email = ["llarue@amazon.com"]
11
+ spec.description = %q{ActiveRecord-like query interface for AWS Cloud Search}
12
+ spec.summary = %q{ActiveRecord-like query interface for AWS Cloud Search}
13
+ spec.homepage = ""
14
+ spec.license = "MIT"
15
+
16
+ # generated with `git ls-files`.split($/)
17
+ spec.files = [
18
+ ".rspec",
19
+ "Gemfile",
20
+ "LICENSE.txt",
21
+ "README.md",
22
+ "Rakefile",
23
+ "cloudsearchable.gemspec",
24
+ "lib/cloudsearchable.rb",
25
+ "lib/cloudsearchable/cloud_search.rb",
26
+ "lib/cloudsearchable/domain.rb",
27
+ "lib/cloudsearchable/field.rb",
28
+ "lib/cloudsearchable/query_chain.rb",
29
+ "lib/cloudsearchable/version.rb",
30
+ "spec/cloudsearchable/cloud_search_spec.rb",
31
+ "spec/cloudsearchable/cloudsearchable_spec.rb",
32
+ "spec/cloudsearchable/domain_spec.rb",
33
+ "spec/cloudsearchable/field_spec.rb",
34
+ "spec/cloudsearchable/query_chain_spec.rb",
35
+ "spec/spec_helper.rb",
36
+ "spec/test_classes/cloud_searchable_test_class.rb"
37
+ ]
38
+
39
+ spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
40
+ spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
41
+ spec.require_paths = ["lib"]
42
+
43
+ spec.add_development_dependency "bundler", "~> 1.3"
44
+ spec.add_development_dependency "rake"
45
+ spec.add_dependency 'aws-sdk'
46
+
47
+ # testing dependencies
48
+ spec.add_development_dependency "rspec"
49
+ spec.add_development_dependency "activemodel"
50
+ end
@@ -0,0 +1,206 @@
1
+ require 'cloudsearchable/version'
2
+
3
+ require 'cloudsearchable/domain'
4
+ require 'cloudsearchable/field'
5
+ require 'cloudsearchable/query_chain'
6
+ require 'cloudsearchable/cloud_search'
7
+ require 'cloudsearchable/config'
8
+
9
+ require 'active_support/inflector'
10
+ require 'active_support/core_ext/string'
11
+
12
+ module Cloudsearchable
13
+ def self.configure
14
+ block_given? ? yield(Cloudsearchable::Config) : Cloudsearchable::Config
15
+ end
16
+
17
+ def self.config
18
+ configure
19
+ end
20
+
21
+ def self.logger
22
+ Cloudsearchable::Config.logger
23
+ end
24
+
25
+ def self.included(base)
26
+ base.extend ClassMethods
27
+ end
28
+
29
+ def cloudsearch_domains= *args
30
+ self.class.cloudsearch_domains = args
31
+ end
32
+
33
+ def cloudsearch_domains
34
+ self.class.cloudsearch_domains
35
+ end
36
+
37
+ def update_indexes
38
+ if destroyed?
39
+ remove_from_indexes
40
+ else
41
+ add_to_indexes
42
+ end
43
+ end
44
+
45
+ def add_to_indexes
46
+ cloudsearch_domains.map do |name, domain|
47
+ domain.post_record(self, id, lock_version)
48
+ end
49
+ end
50
+
51
+ def remove_from_indexes
52
+ cloudsearch_domains.map do |name, domain|
53
+ domain.delete_record(id, lock_version)
54
+ end
55
+ end
56
+
57
+ protected
58
+
59
+ class DSL
60
+ attr_reader :domain, :base
61
+
62
+ def initialize domain, base
63
+ @domain = domain
64
+ @base = base
65
+ end
66
+
67
+ def uint name, options = {}, &block
68
+ field name, :uint, options, &block
69
+ end
70
+
71
+ def text name, options = {}, &block
72
+ field name, :text, options, &block
73
+ end
74
+
75
+ def literal name, options = {}, &block
76
+ field name, :literal, options, &block
77
+ end
78
+
79
+ def field name, type, options = {}, &block
80
+ # This block is executed in the context of the record
81
+ if block_given?
82
+ options[:source] = block.to_proc
83
+ end
84
+ domain.add_field name, type, options
85
+ end
86
+ end
87
+
88
+ module ClassMethods
89
+ def cloudsearch_domains= domains
90
+ @cloudsearch_domains = domains
91
+ end
92
+
93
+ def cloudsearch_domains
94
+ @cloudsearch_domains || {}
95
+ end
96
+
97
+ #
98
+ # Declares a Cloudsearchable index that returns a list of object of this class.
99
+ #
100
+ # @param name (optional) optional name for the index. If not specified, a default (unnamed) index for the class will be created
101
+ # @param options (optional) Hash defining an index
102
+ #
103
+ # @option options [String] :name Name of the index
104
+ #
105
+ #
106
+ def index_in_cloudsearch(name = nil, &block)
107
+ locator_field = :"#{cloudsearch_prefix.singularize}_id"
108
+ # Fetches the existing search domain, or generates a new one
109
+ unless domain = cloudsearch_domains[name]
110
+ domain = new_cloudsearch_index(name).tap do |d|
111
+ # This id field is used to reify search results
112
+ d.add_field(locator_field, :literal,
113
+ :result_enabled => true, :search_enabled => true,
114
+ :source => :id)
115
+ end
116
+ self.cloudsearch_domains = self.cloudsearch_domains.merge({name => domain})
117
+ end
118
+
119
+ if block_given?
120
+ dsl = DSL.new(domain, self)
121
+ dsl.instance_exec &block
122
+ end
123
+
124
+ # Define the search method
125
+ search_method_name = "search#{name && ('_' + name.to_s)}".to_sym
126
+ define_singleton_method search_method_name do
127
+ Query.new(self, cloudsearch_index(name), locator_field)
128
+ end
129
+ end
130
+
131
+ def cloudsearch_index name = nil
132
+ cloudsearch_domains[name]
133
+ end
134
+
135
+ #
136
+ # Prefix name used for indexes, defaults to class name underscored
137
+ #
138
+ def cloudsearch_prefix
139
+ name.pluralize.underscore.gsub('/', '_')
140
+ end
141
+
142
+ def new_cloudsearch_index name
143
+ name = [cloudsearch_prefix, name].compact.join('-').gsub('_','-')
144
+ Cloudsearchable::Domain.new name
145
+ end
146
+
147
+ # By default use 'find' to materialize items
148
+ def materialize_method method_name = nil
149
+ @materialize_method = method_name unless method_name.nil?
150
+ @materialize_method.nil? ? :find : @materialize_method
151
+ end
152
+ end
153
+
154
+ #
155
+ # Wraps a Cloudsearchable::QueryChain, provides methods to execute and reify
156
+ # a query into search result objects
157
+ #
158
+ class Query
159
+ include Enumerable
160
+
161
+ attr_reader :query, :class
162
+
163
+ #
164
+ # @param clazz [ActiveRecord::Model] The class of the Model object that
165
+ # is being searched. The result set will be objects of this type.
166
+ # @param domain [Domain] Cloudsearchable Domain to search
167
+ # @param identity_field [Symbol] name of the field that contains the id of
168
+ # the clazz (e.g. :collection_id)
169
+ #
170
+ def initialize(clazz, domain, identity_field)
171
+ @query = Cloudsearchable::QueryChain.new(domain, fatal_warnings: Cloudsearchable.config.fatal_warnings)
172
+ @class = clazz
173
+ @query.returning(identity_field)
174
+ @identity_field = identity_field
175
+ end
176
+
177
+ [:where, :text, :order, :limit, :offset, :returning].each do |method_name|
178
+ # Passthrough methods, see CloudSearch::Domain for docs
179
+ define_method method_name do |*args|
180
+ @query.send(method_name, *args)
181
+ self
182
+ end
183
+ end
184
+
185
+ # Pass through to Cloudsearchable::Domain#materialize!, then retrieve objects from database
186
+ # TODO: this does NOT preserve order!
187
+ def materialize!(*args)
188
+ @results ||= begin
189
+ record_ids = @query.map{|result_hit| result_hit['data'][@identity_field.to_s].first}.reject{|r| r.nil?}
190
+ @class.send(@class.materialize_method, record_ids)
191
+ end
192
+ end
193
+
194
+ def each &block
195
+ # Returns an enumerator
196
+ return enum_for(__method__) unless block_given?
197
+ materialize!
198
+ @results.respond_to?(:each) ? @results.each { |o| yield o } : [@results].send(:each, &block)
199
+ end
200
+
201
+ def found_count
202
+ query.found_count
203
+ end
204
+
205
+ end
206
+ end
@@ -0,0 +1,41 @@
1
+ require 'aws-sdk'
2
+ require 'json'
3
+
4
+ module CloudSearch
5
+ API_VERSION = "2011-02-01"
6
+
7
+ def self.client
8
+ @client ||= AWS::CloudSearch::Client.new
9
+ end
10
+
11
+ def self.client=(client)
12
+ @client = client
13
+ end
14
+
15
+ #
16
+ # Send an SDF document to CloudSearch via http post request.
17
+ # Returns parsed JSON response, or raises an exception
18
+ #
19
+ def self.post_sdf endpoint, sdf
20
+ self.post_sdf_list endpoint, [sdf]
21
+ end
22
+
23
+ def self.post_sdf_list endpoint, sdf_list
24
+ uri = URI.parse("http://#{endpoint}/#{API_VERSION}/documents/batch")
25
+
26
+ req = Net::HTTP::Post.new(uri.path)
27
+ req.body = JSON.generate sdf_list
28
+ req["Content-Type"] = "application/json"
29
+
30
+ http = Net::HTTP.new uri.host,uri.port
31
+ response = http.start{|http| http.request(req)}
32
+
33
+ if response.is_a? Net::HTTPSuccess
34
+ JSON.parse response.body
35
+ else
36
+ # Raise an exception based on the response see http://ruby-doc.org/stdlib-1.9.2/libdoc/net/http/rdoc/Net/HTTP.html
37
+ response.error!
38
+ end
39
+
40
+ end
41
+ end