ripple_searchable 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/.gitignore ADDED
@@ -0,0 +1,18 @@
1
+ .DS_Store
2
+ *.gem
3
+ *.rbc
4
+ .bundle
5
+ .config
6
+ .yardoc
7
+ Gemfile.lock
8
+ InstalledFiles
9
+ _yardoc
10
+ coverage
11
+ doc/
12
+ lib/bundler/man
13
+ pkg
14
+ rdoc
15
+ spec/reports
16
+ test/tmp
17
+ test/version_tmp
18
+ tmp
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in ripple_searchable.gemspec
4
+ gemspec
data/LICENSE ADDED
@@ -0,0 +1,22 @@
1
+ Copyright (c) 2012 Mark Ronai
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.
data/README.md ADDED
@@ -0,0 +1,67 @@
1
+ # RippleSearchable
2
+
3
+ Mongoid / Active Record style query criteria DSL and Scoping for Ripple
4
+ using RIAK's solr search interface.
5
+
6
+ RippleSearchable adds chainable Criteria methods such as :where, :lt, :lte, :gt, :gte, :between
7
+ along with :sort, :skip, :limit options to your Ripple::Document models.
8
+
9
+ ## Installation
10
+
11
+ Add this line to your application's Gemfile:
12
+
13
+ gem 'ripple_searchable'
14
+
15
+ And then execute:
16
+
17
+ $ bundle
18
+
19
+ Or install it yourself as:
20
+
21
+ $ gem install ripple_searchable
22
+
23
+ ## Usage
24
+
25
+ ### Criteria:
26
+
27
+ Any of the following criteria can be chained:
28
+
29
+ :where, :lt, :lte, :gt, :gte, :between, with sort, :skip, :limit
30
+
31
+ === Example:
32
+
33
+ ```ruby
34
+ Product.where(tags: "nerd", name: "joe", something: 2).or({can_sell:
35
+ 1}, {can_sell: 3}).between(availibility: 1..3, price: [3,
36
+ 12]).gte(quantity: 0, ratings: 5).sort(created_at, :desc).limit(5)
37
+ ```
38
+
39
+ ### Scoping
40
+
41
+ Mongoid / Active Record style named scopes:
42
+
43
+ === Example:
44
+
45
+ ```ruby
46
+ class Product
47
+ include Ripple::Document
48
+
49
+ scope :active, where(active: true)
50
+ scope :avail, ->(count){ where(quantity: count)}
51
+
52
+ end
53
+ ```
54
+
55
+ See docs for method details.
56
+
57
+ TODO: Write better docs.
58
+
59
+ ## Contributing
60
+
61
+ This gem is still under heavy development. Feel free to contribute.
62
+
63
+ 1. Fork it
64
+ 2. Create your feature branch (`git checkout -b my-new-feature`)
65
+ 3. Commit your changes (`git commit -am 'Added some feature'`)
66
+ 4. Push to the branch (`git push origin my-new-feature`)
67
+ 5. Create new Pull Request
data/Rakefile ADDED
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env rake
2
+ require "bundler/gem_tasks"
@@ -0,0 +1,305 @@
1
+ require 'active_support/concern'
2
+
3
+ module Ripple
4
+
5
+ class CriteriaError < StandardError; end
6
+
7
+ # chainable Criteria methods such as :where, :lt, :lte, :gt, :gte, :between,
8
+ # with sort, :skip, :limit options
9
+ class Criteria
10
+
11
+ include Translation
12
+ include Enumerable
13
+
14
+ attr_accessor :selector, :klass, :options, :response, :cached, :total, :docs, :document_ids
15
+
16
+ def initialize(klass)
17
+ @selector, @klass, @options, @documents, @cached = "", klass, {}
18
+ clear_cache
19
+ end
20
+
21
+ # Main criteria selector to search records
22
+ #
23
+ # === Example
24
+ #
25
+ # Model.where(tags: "nerd", name: "Joe", something: 2)
26
+ #
27
+ # will append this selector:
28
+ # "(tags:nerd AND name:Joe AND something:2)"
29
+ def where(selector = nil)
30
+ case selector
31
+ when String
32
+ add_restriction selector
33
+ when Hash
34
+ add_restriction to_lucene_pair(selector)
35
+ end
36
+ self
37
+ end
38
+
39
+ # Add an OR selector
40
+ #
41
+ # === Example
42
+ #
43
+ # Product.or({name: "Pants"}, {name: "Shirt"})
44
+ #
45
+ # will append this selector:
46
+ # "((name:Pants) OR (name:Shirt))"
47
+ def or(*criterion)
48
+ add_restriction do
49
+ criterion.each do |crit|
50
+ add_restriction(to_lucene_pair(crit, operator: "OR"), operator: "OR" )
51
+ end
52
+ end
53
+ self
54
+ end
55
+
56
+ alias :any_of :or
57
+
58
+ # Add an Range selector. Values in the passed hash can be either a Range or an Array.
59
+ # of the passed hash has multiple elements, the condition will be AND.
60
+ # The range is inclusive.
61
+ #
62
+ # === Example
63
+ #
64
+ # Product.between(availibility: 1..3, price: [12, 20])
65
+ #
66
+ # will append this selector:
67
+ # "((availibility:[1 TO 3] AND price:[12 TO 20]))"
68
+ def between(*criterion)
69
+ add_restriction do
70
+ criterion.each do |crit|
71
+ add_restriction(to_lucene_pair(crit, operator: "BETWEEN"))
72
+ end
73
+ end
74
+ self
75
+ end
76
+
77
+ # Add a 'less or equal than' selector
78
+ #
79
+ # === Example
80
+ #
81
+ # Product.lte(quantity: 10, ratings: 5)
82
+ #
83
+ # will append this selector:
84
+ # "((quantity:[* TO 10] AND ratings:[* TO 5]))"
85
+ def lte(*criterion)
86
+ add_restriction do
87
+ criterion.each do |crit|
88
+ crit.each {|k,v| crit[k]=Array.wrap(v).unshift(10**20)}
89
+ add_restriction(to_lucene_pair(crit, operator: "BETWEEN"))
90
+ end
91
+ end
92
+ self
93
+ end
94
+
95
+ # Add a 'greater or equal than' selector
96
+ #
97
+ # === Example
98
+ #
99
+ # Product.gte(quantity: 0, ratings: 5)
100
+ #
101
+ # will append this selector:
102
+ # "((quantity:[0 TO *] AND ratings:[5 TO *]))"
103
+ def gte(*criterion)
104
+ add_restriction do
105
+ criterion.each do |crit|
106
+ crit.each {|k,v| crit[k]=Array.wrap(v).push(10**20)}
107
+ add_restriction(to_lucene_pair(crit, operator: "BETWEEN"))
108
+ end
109
+ end
110
+ self
111
+ end
112
+
113
+ # Add a 'less than' selector
114
+ #
115
+ # === Example
116
+ #
117
+ # Product.lt(quantity: 10, ratings: 5)
118
+ #
119
+ # will append this selector:
120
+ # "((quantity:{* TO 10} AND ratings:{* TO 5}))"
121
+ def lt(*criterion)
122
+ add_restriction do
123
+ criterion.each do |crit|
124
+ crit.each {|k,v| crit[k]=Array.wrap(v).unshift("*")}
125
+ add_restriction(to_lucene_pair(crit, operator: "BETWEEN", exclusive: true))
126
+ end
127
+ end
128
+ self
129
+ end
130
+
131
+ # Add a 'greater than' selector
132
+ #
133
+ # === Example
134
+ #
135
+ # Product.gt(quantity: 0, ratings: 5)
136
+ #
137
+ # will append this selector:
138
+ # "((quantity:{0 TO *} AND ratings:{5 TO *}))"
139
+ def gt(*criterion)
140
+ add_restriction do
141
+ criterion.each do |crit|
142
+ crit.each {|k,v| crit[k]=Array.wrap(v).push("*")}
143
+ add_restriction(to_lucene_pair(crit, operator: "BETWEEN", exclusive: true))
144
+ end
145
+ end
146
+ self
147
+ end
148
+
149
+ # Add sort options to criteria
150
+ #
151
+ # === Example
152
+ #
153
+ # Product.between(availibility:[1,3]).sort(availibility: :asc, created_at: :desc)
154
+ #
155
+ # will append this sort option:
156
+ # "availibility asc, created_at desc"
157
+ def sort(sort_options)
158
+ case sort_options
159
+ when String
160
+ add_sort_option sort_options
161
+ when Hash
162
+ sort_options.each {|k,v| add_sort_option "#{k} #{v.downcase}"}
163
+ end
164
+ self
165
+ end
166
+
167
+ alias :order_by :sort
168
+ alias :order :sort
169
+
170
+ # Add limit option to criteria. Useful for pagination. Default is 10.
171
+ #
172
+ # === Example
173
+ #
174
+ # Product.between(availibility:[1,3]).limit(10)
175
+ #
176
+ # will limit the number of returned documetns to 10
177
+ def limit(limit)
178
+ clear_cache
179
+ self.options[:rows] = limit
180
+ self
181
+ end
182
+
183
+ alias :rows :limit
184
+
185
+ # Add skip option to criteria. Useful for pagination. Default is 0.
186
+ #
187
+ # === Example
188
+ #
189
+ # Product.between(availibility:[1,3]).skip(10)
190
+ #
191
+ def skip(skip)
192
+ clear_cache
193
+ self.options[:start] = skip
194
+ self
195
+ end
196
+
197
+ alias :start :skip
198
+
199
+
200
+ # Executes the search query
201
+ def execute
202
+ raise CriteriaError, t('empty_selector_error') if self.selector.blank?
203
+ @response = @klass.search self.selector, self.options
204
+ end
205
+
206
+ # Returns the matched documents
207
+ def documents
208
+ if @cached
209
+ @documents
210
+ else
211
+ parse_response
212
+ @cached = true
213
+ @documents = self.klass.find self.document_ids
214
+ end
215
+ end
216
+
217
+ def each(&block)
218
+ documents.each(&block)
219
+ end
220
+
221
+ # Total number of matching documents
222
+ def total
223
+ parse_response
224
+ @total
225
+ end
226
+
227
+ # Array of matching document id's
228
+ def document_ids
229
+ parse_response
230
+ @document_ids
231
+ end
232
+
233
+ def merge(criteria)
234
+ add_restriction criteria.selector
235
+ self.options.merge!(criteria.options)
236
+ self
237
+ end
238
+
239
+ def method_missing(name, *args, &block)
240
+ if klass.respond_to?(name)
241
+ klass.send(:with_scope, self) do
242
+ klass.send(name, *args, &block)
243
+ end
244
+ end
245
+ end
246
+
247
+ private
248
+
249
+ def clear_cache
250
+ @documents, @cached, @response, @total, @docs, @document_ids = [], false
251
+ end
252
+
253
+ def parse_response
254
+ execute if @response.blank?
255
+ self.total = @response["response"]["numFound"]
256
+ self.docs = @response["response"]["docs"]
257
+ self.document_ids = self.docs.map {|e| e["id"]}
258
+ rescue
259
+ clear_cache
260
+ raise CriteriaError, t('failed_query')
261
+ end
262
+
263
+ def add_restriction(*args, &block)
264
+ clear_cache
265
+ options = args.extract_options!
266
+ operator = options[:operator] || "AND"
267
+ restriction = args.first
268
+ separator = @selector.present? ? " #{operator} " : ""
269
+ if block_given?
270
+ @selector << "#{separator}("
271
+ yield
272
+ @selector << ")"
273
+ else
274
+ @selector << "#{separator unless @selector[-1] == '('}(#{restriction})"
275
+ end
276
+ end
277
+
278
+ def add_sort_option(*args)
279
+ clear_cache
280
+ args.each do |s|
281
+ if options[:sort].present?
282
+ options[:sort] << ", #{s}"
283
+ else
284
+ options[:sort] = s
285
+ end
286
+ end
287
+ end
288
+
289
+ def to_lucene_pair(conditions, options = {})
290
+ operator = options[:operator] || "AND"
291
+ if operator == "BETWEEN"
292
+ conditions.map do |k,v|
293
+ case v
294
+ when Range, Array
295
+ "#{k}:#{options[:exclusive] ? '{' : '['}#{v.first} TO #{v.last}#{options[:exclusive] ? '}' : ']'}"
296
+ when String
297
+ "#{k}: #{v}"
298
+ end
299
+ end.join(" AND ")
300
+ else
301
+ conditions.map {|k,v| "#{k}:#{v}"}.join(" #{operator} ")
302
+ end
303
+ end
304
+ end
305
+ end
@@ -0,0 +1,73 @@
1
+ require 'active_support/concern'
2
+
3
+ module Ripple
4
+ module Scoping
5
+ extend ActiveSupport::Concern
6
+
7
+ included do
8
+ class_attribute :default_scoping
9
+ class_attribute :scopes
10
+ self.scopes = {}
11
+ end
12
+
13
+ module ClassMethods
14
+
15
+ def scope(name, value, &block)
16
+ name = name.to_sym
17
+ #valid_scope_name?(name)
18
+ scopes[name] = {
19
+ scope: strip_default_scope(value),
20
+ extension: Module.new(&block)
21
+ }
22
+ define_scope_method(name)
23
+ end
24
+
25
+ def scope_stack
26
+ Thread.current[:"#{self.bucket_name}_scope_stack"] ||= []
27
+ end
28
+
29
+ def with_default_scope
30
+ default_scoping || scope_stack.last || Criteria.new(self)
31
+ end
32
+
33
+ def with_scope(criteria)
34
+ scope_stack.push(criteria)
35
+ begin
36
+ yield criteria
37
+ ensure
38
+ scope_stack.pop
39
+ end
40
+ end
41
+
42
+ protected
43
+
44
+ def valid_scope_name?(name)
45
+ if logger && respond_to?(name, true)
46
+ logger.warn "Creating scope :#{name}. " \
47
+ "Overwriting existing method #{self.name}.#{name}."
48
+ end
49
+ end
50
+
51
+ def define_scope_method(name)
52
+ (class << self; self; end).class_eval <<-SCOPE
53
+ def #{name}(*args)
54
+ scoping = scopes[:#{name}]
55
+ scope, extension = scoping[:scope].(*args), scoping[:extension]
56
+ criteria = with_default_scope.merge(scope)
57
+ criteria.extend(extension)
58
+ criteria
59
+ end
60
+ SCOPE
61
+ end
62
+
63
+ def strip_default_scope(value)
64
+ if value.is_a?(Criteria)
65
+ ->{ value}
66
+ else
67
+ value
68
+ end
69
+ end
70
+
71
+ end
72
+ end
73
+ end
@@ -0,0 +1,35 @@
1
+ require 'active_support/concern'
2
+
3
+ module Ripple
4
+ module Searchable
5
+
6
+ extend ActiveSupport::Concern
7
+
8
+ included do
9
+ extend ClassMethods
10
+ end
11
+
12
+ unless method_defined? :id
13
+ define_method :id do
14
+ self.key
15
+ end
16
+ end
17
+
18
+ module ClassMethods
19
+
20
+ attr_accessor :criteria
21
+
22
+ delegate :where, :or, :any_of, :gte, :lte, :gt, :lt, :between, :sort, to: :criteria
23
+
24
+ # Performs a search via the Solr interface.
25
+ def search(*args)
26
+ Ripple.client.search(self.bucket_name, *args)
27
+ end
28
+
29
+ def criteria
30
+ @criteria = Criteria.new(self)
31
+ end
32
+ end
33
+
34
+ end
35
+ end
@@ -0,0 +1,3 @@
1
+ module RippleSearchable
2
+ VERSION = "0.0.1"
3
+ end
@@ -0,0 +1,12 @@
1
+ require 'ripple'
2
+ require 'ripple/translation'
3
+ require 'active_support/concern'
4
+ require 'ripple_searchable/version'
5
+ require 'ripple_searchable/searchable'
6
+ require 'ripple_searchable/criteria'
7
+ require 'ripple_searchable/scoping'
8
+
9
+ Ripple::Document.class_eval do
10
+ include Ripple::Searchable
11
+ include Ripple::Scoping
12
+ end
@@ -0,0 +1,23 @@
1
+ # -*- encoding: utf-8 -*-
2
+ require File.expand_path('../lib/ripple_searchable/version', __FILE__)
3
+
4
+ Gem::Specification.new do |gem|
5
+ gem.authors = ["Mark Ronai"]
6
+ gem.email = ["computadude@me.com"]
7
+ gem.description = %q{Mongoid / Active Record style query criteria and scoping for Ripple}
8
+ gem.summary = %q{Mongoid / Active Record style query criteria and scoping for Ripple}
9
+ gem.homepage = ""
10
+
11
+ gem.files = `git ls-files`.split($\)
12
+ gem.executables = gem.files.grep(%r{^bin/}).map{ |f| File.basename(f) }
13
+ gem.test_files = gem.files.grep(%r{^(test|spec|features)/})
14
+ gem.name = "ripple_searchable"
15
+ gem.require_paths = ["lib"]
16
+ gem.version = RippleSearchable::VERSION
17
+
18
+ gem.add_dependency "activesupport", [">= 3.0.0", "< 3.3.0"]
19
+ gem.add_dependency "activemodel", [">= 3.0.0", "< 3.3.0"]
20
+ gem.add_dependency "ripple", ">=1.0.0.beta2"
21
+
22
+ gem.add_development_dependency "rails", '3.2.8'
23
+ end
metadata ADDED
@@ -0,0 +1,133 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: ripple_searchable
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.1
5
+ prerelease:
6
+ platform: ruby
7
+ authors:
8
+ - Mark Ronai
9
+ autorequire:
10
+ bindir: bin
11
+ cert_chain: []
12
+ date: 2012-11-17 00:00:00.000000000 Z
13
+ dependencies:
14
+ - !ruby/object:Gem::Dependency
15
+ name: activesupport
16
+ requirement: !ruby/object:Gem::Requirement
17
+ none: false
18
+ requirements:
19
+ - - ! '>='
20
+ - !ruby/object:Gem::Version
21
+ version: 3.0.0
22
+ - - <
23
+ - !ruby/object:Gem::Version
24
+ version: 3.3.0
25
+ type: :runtime
26
+ prerelease: false
27
+ version_requirements: !ruby/object:Gem::Requirement
28
+ none: false
29
+ requirements:
30
+ - - ! '>='
31
+ - !ruby/object:Gem::Version
32
+ version: 3.0.0
33
+ - - <
34
+ - !ruby/object:Gem::Version
35
+ version: 3.3.0
36
+ - !ruby/object:Gem::Dependency
37
+ name: activemodel
38
+ requirement: !ruby/object:Gem::Requirement
39
+ none: false
40
+ requirements:
41
+ - - ! '>='
42
+ - !ruby/object:Gem::Version
43
+ version: 3.0.0
44
+ - - <
45
+ - !ruby/object:Gem::Version
46
+ version: 3.3.0
47
+ type: :runtime
48
+ prerelease: false
49
+ version_requirements: !ruby/object:Gem::Requirement
50
+ none: false
51
+ requirements:
52
+ - - ! '>='
53
+ - !ruby/object:Gem::Version
54
+ version: 3.0.0
55
+ - - <
56
+ - !ruby/object:Gem::Version
57
+ version: 3.3.0
58
+ - !ruby/object:Gem::Dependency
59
+ name: ripple
60
+ requirement: !ruby/object:Gem::Requirement
61
+ none: false
62
+ requirements:
63
+ - - ! '>='
64
+ - !ruby/object:Gem::Version
65
+ version: 1.0.0.beta2
66
+ type: :runtime
67
+ prerelease: false
68
+ version_requirements: !ruby/object:Gem::Requirement
69
+ none: false
70
+ requirements:
71
+ - - ! '>='
72
+ - !ruby/object:Gem::Version
73
+ version: 1.0.0.beta2
74
+ - !ruby/object:Gem::Dependency
75
+ name: rails
76
+ requirement: !ruby/object:Gem::Requirement
77
+ none: false
78
+ requirements:
79
+ - - '='
80
+ - !ruby/object:Gem::Version
81
+ version: 3.2.8
82
+ type: :development
83
+ prerelease: false
84
+ version_requirements: !ruby/object:Gem::Requirement
85
+ none: false
86
+ requirements:
87
+ - - '='
88
+ - !ruby/object:Gem::Version
89
+ version: 3.2.8
90
+ description: Mongoid / Active Record style query criteria and scoping for Ripple
91
+ email:
92
+ - computadude@me.com
93
+ executables: []
94
+ extensions: []
95
+ extra_rdoc_files: []
96
+ files:
97
+ - .gitignore
98
+ - Gemfile
99
+ - LICENSE
100
+ - README.md
101
+ - Rakefile
102
+ - lib/ripple_searchable.rb
103
+ - lib/ripple_searchable/criteria.rb
104
+ - lib/ripple_searchable/scoping.rb
105
+ - lib/ripple_searchable/searchable.rb
106
+ - lib/ripple_searchable/version.rb
107
+ - ripple_searchable.gemspec
108
+ homepage: ''
109
+ licenses: []
110
+ post_install_message:
111
+ rdoc_options: []
112
+ require_paths:
113
+ - lib
114
+ required_ruby_version: !ruby/object:Gem::Requirement
115
+ none: false
116
+ requirements:
117
+ - - ! '>='
118
+ - !ruby/object:Gem::Version
119
+ version: '0'
120
+ required_rubygems_version: !ruby/object:Gem::Requirement
121
+ none: false
122
+ requirements:
123
+ - - ! '>='
124
+ - !ruby/object:Gem::Version
125
+ version: '0'
126
+ requirements: []
127
+ rubyforge_project:
128
+ rubygems_version: 1.8.23
129
+ signing_key:
130
+ specification_version: 3
131
+ summary: Mongoid / Active Record style query criteria and scoping for Ripple
132
+ test_files: []
133
+ has_rdoc: