cloudsearchable 0.0.1

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