ripple_searchable 0.0.1

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