model_set 1.0.0 → 1.1.0

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,3 @@
1
+ Gemfile.lock
2
+ /pkg/
3
+ *~
data/.rbenv-gemsets ADDED
@@ -0,0 +1 @@
1
+ model_set
data/.rbenv-version ADDED
@@ -0,0 +1 @@
1
+ ree-1.8.7-2012.02
data/Gemfile ADDED
@@ -0,0 +1,3 @@
1
+ source "http://www.rubygems.org"
2
+
3
+ gemspec
data/Rakefile CHANGED
@@ -1,47 +1,10 @@
1
- require 'rake'
2
1
  require 'rake/testtask'
3
- require 'rake/rdoctask'
4
-
5
- begin
6
- require 'jeweler'
7
- Jeweler::Tasks.new do |s|
8
- s.name = "model_set"
9
- s.summary = %Q{Easy manipulation of sets of ActiveRecord models}
10
- s.email = "code@justinbalthrop.com"
11
- s.homepage = "http://github.com/ninjudd/model_set"
12
- s.description = "Easy manipulation of sets of ActiveRecord models"
13
- s.authors = ["Justin Balthrop"]
14
- s.add_dependency('ordered_set', '>= 1.0.1')
15
- s.add_dependency('deep_clonable', '>= 1.1.0')
16
- s.add_dependency('activerecord', '>= 2.0.0')
17
- end
18
- Jeweler::GemcutterTasks.new
19
- rescue LoadError
20
- puts "Jeweler not available. Install it with: sudo gem install technicalpickles-jeweler -s http://gems.github.com"
21
- end
2
+ require 'bundler/gem_tasks'
22
3
 
23
4
  Rake::TestTask.new do |t|
24
- t.libs << 'lib'
5
+ t.libs = ['lib']
25
6
  t.pattern = 'test/**/*_test.rb'
26
7
  t.verbose = false
27
8
  end
28
9
 
29
- Rake::RDocTask.new do |rdoc|
30
- rdoc.rdoc_dir = 'rdoc'
31
- rdoc.title = 'model_set'
32
- rdoc.options << '--line-numbers' << '--inline-source'
33
- rdoc.rdoc_files.include('README*')
34
- rdoc.rdoc_files.include('lib/**/*.rb')
35
- end
36
-
37
- begin
38
- require 'rcov/rcovtask'
39
- Rcov::RcovTask.new do |t|
40
- t.libs << 'test'
41
- t.test_files = FileList['test/**/*_test.rb']
42
- t.verbose = true
43
- end
44
- rescue LoadError
45
- end
46
-
47
10
  task :default => :test
data/VERSION CHANGED
@@ -1 +1 @@
1
- 1.0.0
1
+ 1.1.0
@@ -2,28 +2,27 @@ class ModelSet
2
2
  module Conditioned
3
3
  # Shared methods for dealing with conditions.
4
4
  attr_accessor :conditions
5
-
6
- def add_conditions!(*conditions)
7
- operator = conditions.shift if conditions.first.kind_of?(Symbol)
8
- operator ||= :and
9
5
 
10
- # Sanitize conditions.
11
- conditions.collect! do |condition|
12
- condition.kind_of?(Conditions) ? condition : Conditions.new( sanitize_condition(condition) )
13
- end
6
+ def add_conditions!(*conditions)
7
+ new_conditions = conditions.first.kind_of?(Symbol) ? [conditions.shift] : []
14
8
 
15
- if operator == :not
16
- # In this case, :not actually means :and :not.
17
- conditions = ~Conditions.new(:and, *conditions)
18
- operator = :and
9
+ conditions.each do |condition|
10
+ if condition.kind_of?(Conditions)
11
+ new_conditions << condition
12
+ else
13
+ new_conditions.concat([*transform_condition(condition)])
14
+ end
19
15
  end
16
+ return self if new_conditions.empty?
20
17
 
21
- conditions << @conditions if @conditions
22
- @conditions = Conditions.new(operator, *conditions)
23
-
18
+ @conditions = to_conditions(*new_conditions) << @conditions
24
19
  clear_cache!
25
20
  end
26
21
 
22
+ def to_conditions(*conditions)
23
+ Conditions.new(conditions, condition_ops)
24
+ end
25
+
27
26
  def invert!
28
27
  raise 'cannot invert without conditions' if @conditions.nil?
29
28
  @conditions = ~@conditions
@@ -4,40 +4,27 @@ class ModelSet
4
4
 
5
5
  attr_reader :operator, :conditions
6
6
 
7
- def self.new(*args)
8
- if args.size == 1 and args.first.kind_of?(self)
9
- # Just clone if the only argument is a Conditions object.
10
- args.first.clone
11
- elsif args.size == 2 and [:and, :or].include?(args.first)
12
- # The operator is not necessary if there is only one subcondition.
13
- new(args.last)
14
- else
15
- super
16
- end
17
- end
18
-
19
7
  def new(*args)
20
8
  self.class.new(*args)
21
9
  end
22
10
 
23
- def initialize(*args)
24
- if args.size == 1 and not args.first.kind_of?(Symbol)
25
- # Terminal.
26
- @conditions = args
27
- else
28
- @operator = args.shift
29
- raise "invalid operator :#{operator}" unless [:and, :or, :not].include?(operator)
30
-
31
- if operator == :not
32
- raise "unary operator :not cannot have multiple conditions" if args.size > 1
33
- @conditions = [self.class.new(args.first)]
11
+ def initialize(conditions, ops)
12
+ @ops = ops
13
+ if conditions.kind_of?(Array)
14
+ @operator = conditions.first.kind_of?(Symbol) ? conditions.shift : :and
15
+ if @operator == :not
16
+ # In this case, :not actually means :and :not.
17
+ @conditions = ~Conditions.new([:and, conditions], @ops)
34
18
  else
19
+ raise "invalid operator :#{operator}" unless [:and, :or].include?(@operator)
35
20
  # Compact the conditions if possible.
36
21
  @conditions = []
37
- args.each do |clause|
22
+ conditions.each do |clause|
38
23
  self << clause
39
24
  end
40
25
  end
26
+ else
27
+ @conditions = [conditions]
41
28
  end
42
29
  end
43
30
 
@@ -46,15 +33,17 @@ class ModelSet
46
33
  end
47
34
 
48
35
  def <<(clause)
36
+ return self unless clause
49
37
  raise 'cannot append conditions to a terminal' if terminal?
50
-
51
- clause = self.class.new(clause)
38
+
39
+ clause = new(clause, @ops) unless clause.kind_of?(Conditions)
52
40
  if clause.operator == operator
53
41
  @conditions.concat(clause.conditions)
54
42
  else
55
43
  @conditions << clause
56
44
  end
57
45
  @conditions.uniq!
46
+ self
58
47
  end
59
48
 
60
49
  def ~
@@ -73,20 +62,24 @@ class ModelSet
73
62
  new(:and, self, other)
74
63
  end
75
64
 
65
+ def op(type)
66
+ @ops[type]
67
+ end
68
+
76
69
  def to_s
77
- return conditions.first if terminal?
70
+ return conditions.first.to_s if terminal? or conditions.empty?
78
71
 
79
72
  condition_strings = conditions.collect do |condition|
80
73
  condition.operator == :not ? condition.to_s : "(#{condition.to_s})"
81
74
  end.sort_by {|s| s.size}
82
75
 
83
76
  case operator
84
- when :not
85
- "NOT #{condition_strings.first}"
86
- when :and
87
- "#{condition_strings.join(' AND ')}"
88
- when :or
89
- "#{condition_strings.join(' OR ')}"
77
+ when :not then
78
+ "#{op(:not)} #{condition_strings.first}"
79
+ when :and then
80
+ "#{condition_strings.join(op(:and))}"
81
+ when :or then
82
+ "#{condition_strings.join(op(:or))}"
90
83
  end
91
84
  end
92
85
 
@@ -37,27 +37,28 @@ class ModelSet
37
37
  @page = nil if offset
38
38
  clear_limited_cache!
39
39
  end
40
-
40
+
41
41
  def unlimited!
42
42
  @limit = nil
43
43
  @offset = nil
44
44
  @page = nil
45
45
  clear_limited_cache!
46
46
  end
47
-
47
+
48
48
  def clear_limited_cache!
49
49
  @ids = nil
50
50
  @size = nil
51
51
  self
52
52
  end
53
-
53
+
54
54
  def clear_cache!
55
55
  @count = nil
56
56
  clear_limited_cache!
57
57
  end
58
58
 
59
- attr_reader :set_class
60
- delegate :id_field, :to => :set_class
59
+ attr_reader :set_class, :limit, :sort_order
60
+ delegate :id_field, :to => :set_class
61
+ delegate :id_field_with_prefix, :to => :set_class
61
62
 
62
63
  def model_class
63
64
  set_class.query_model_class
@@ -71,12 +72,6 @@ class ModelSet
71
72
  model_class.table_name
72
73
  end
73
74
 
74
- def id_field_with_prefix
75
- "#{table_name}.#{id_field}"
76
- end
77
-
78
- attr_reader :limit, :sort_order
79
-
80
75
  def offset
81
76
  if limit
82
77
  @offset ||= @page ? (@page - 1) * limit : 0
@@ -97,7 +92,7 @@ class ModelSet
97
92
  proc = self.class.before_query
98
93
  proc.bind(self).call(*args) if proc
99
94
  end
100
-
95
+
101
96
  def self.before_query(&block)
102
97
  if block
103
98
  @before_query = block
@@ -132,5 +127,14 @@ class ModelSet
132
127
  end
133
128
  end
134
129
 
130
+ def condition_ops
131
+ { :not => 'NOT ',
132
+ :and => ' AND ',
133
+ :or => ' OR ' }
134
+ end
135
+
136
+ def transform_condition(condition)
137
+ [condition]
138
+ end
135
139
  end
136
140
  end
@@ -1,15 +1,20 @@
1
- require 'solr'
1
+ require 'rsolr'
2
+ require 'json'
2
3
 
3
4
  class ModelSet
4
5
  class SolrQuery < Query
5
- attr_reader :response
6
6
  include Conditioned
7
7
 
8
8
  MAX_SOLR_RESULTS = 1000
9
9
 
10
+ class << self
11
+ attr_accessor :host
12
+ end
13
+ attr_reader :response
14
+
10
15
  def anchor!(query)
11
16
  add_conditions!( ids_clause(query.ids) )
12
- end
17
+ end
13
18
 
14
19
  def size
15
20
  fetch_results if @size.nil?
@@ -26,41 +31,50 @@ class ModelSet
26
31
  @ids
27
32
  end
28
33
 
29
- def config(params)
30
- @config = @config ? @config.merge(params) : params
34
+ def use_core!(core)
35
+ @core = core
31
36
  end
32
37
 
33
- private
34
-
35
- def fetch_results
36
- query = "#{conditions.to_s}"
37
- solr_params = {:highlighting => {}}
38
+ def solr_params!(opts)
39
+ @opts = opts
40
+ end
38
41
 
39
- if set_class.respond_to?(:solr_field_list)
40
- solr_params[:field_list] = set_class.solr_field_list
42
+ def id_field
43
+ if set_class.respond_to?(:solr_id_field)
44
+ set_class.solr_id_field
45
+ else
46
+ 'id'
41
47
  end
48
+ end
49
+
50
+ private
42
51
 
52
+ def fetch_results
53
+ params = @opts || {}
54
+ params[:q] = "#{conditions.to_s}"
55
+ params[:fl] ||= [id_field]
56
+ params[:fl] = params[:fl].join(",")
57
+ params[:wt] = :json
43
58
  if limit
44
- solr_params[:rows] = limit
45
- solr_params[:start] = offset
59
+ params[:rows] = limit
60
+ params[:start] = offset
46
61
  else
47
- solr_params[:rows] = MAX_SOLR_RESULTS
62
+ params[:rows] = MAX_SOLR_RESULTS
48
63
  end
49
64
 
50
- before_query(solr_params)
51
- begin
52
- solr_uri = "http://" + SOLR_HOST
53
- if @config[:core]
54
- solr_uri << "/" + @config[:core]
55
- end
56
- @response = Solr::Connection.new(solr_uri).search(query, solr_params)
65
+ before_query(params)
66
+ begin
67
+ url = "http://" + self.class.host
68
+ url += "/" + @core if @core
69
+ search = RSolr.connect(:url => url)
70
+ @response = JSON.parse(search.get('select', :params => params))
57
71
  rescue Exception => e
58
- on_exception(e, solr_params)
72
+ on_exception(e, params)
59
73
  end
60
- after_query(solr_params)
74
+ after_query(params)
61
75
 
62
- @count = @response.total_hits
63
- @ids = @response.hits.map{ |hit| hit[@config[:response_id_field]].to_i }
76
+ @count = response['response']['numFound']
77
+ @ids = response['response']['docs'].collect {|doc| set_class.as_id(doc[id_field])}.to_ordered_set
64
78
  @size = @ids.size
65
79
  end
66
80
 
@@ -69,9 +83,5 @@ class ModelSet
69
83
  field ||= 'pk_i'
70
84
  "#{field}:(#{ids.join(' OR ')})"
71
85
  end
72
-
73
- def sanitize_condition(condition)
74
- condition
75
- end
76
86
  end
77
87
  end
@@ -1,20 +1,17 @@
1
1
  require File.dirname(__FILE__) + '/../../vendor/sphinx_client/lib/sphinx'
2
- begin
3
- require 'system_timer'
4
- rescue LoadError => e
5
- module SystemTimer
6
- def self.timeout(time, &block)
7
- Timeout.timeout(time, &block)
8
- end
9
- end
10
- end
2
+ require 'system_timer'
11
3
 
12
4
  class ModelSet
13
5
  class SphinxQuery < Query
14
- MAX_SPHINX_RESULTS = 1000
15
- MAX_QUERY_TIME = 5
6
+ include Conditioned
16
7
 
17
- attr_reader :conditions, :filters
8
+ MAX_RESULTS = 1000
9
+ MAX_QUERY_TIME = 5
10
+
11
+ class << self
12
+ attr_accessor :host, :port
13
+ end
14
+ attr_reader :filters, :response
18
15
 
19
16
  def max_query_time
20
17
  @max_query_time || MAX_QUERY_TIME
@@ -24,6 +21,14 @@ class ModelSet
24
21
  @max_query_time = seconds
25
22
  end
26
23
 
24
+ def max_results
25
+ @max_results || MAX_RESULTS
26
+ end
27
+
28
+ def max_results!(max)
29
+ @max_results = max
30
+ end
31
+
27
32
  def anchor!(query)
28
33
  add_filters!( id_field => query.ids.to_a )
29
34
  end
@@ -43,28 +48,20 @@ class ModelSet
43
48
  @geo = opts
44
49
  end
45
50
 
46
- def add_conditions!(conditions)
47
- if conditions.kind_of?(Hash)
48
- conditions.each do |field, value|
49
- next if value.nil?
50
- field = field.join(',') if field.kind_of?(Array)
51
- value = "(#{value.join('|')})" if value.kind_of?(Array)
52
- add_conditions!("@(#{field}) #{value}")
53
- end
54
- else
55
- @conditions ||= []
56
- @conditions << conditions
57
- @conditions.uniq!
58
- clear_cache!
59
- end
60
- end
61
-
62
51
  def index
63
52
  @index ||= '*'
64
53
  end
65
54
 
66
- def use_index!(index)
67
- @index = index
55
+ def use_index!(index, opts = {})
56
+ if opts[:delta]
57
+ @index = "#{index} #{index}_delta"
58
+ else
59
+ @index = index
60
+ end
61
+ end
62
+
63
+ def select_fields!(*fields)
64
+ @select = fields.flatten
68
65
  end
69
66
 
70
67
  SORT_MODES = {
@@ -72,7 +69,7 @@ class ModelSet
72
69
  :descending => Sphinx::Client::SPH_SORT_ATTR_DESC,
73
70
  :ascending => Sphinx::Client::SPH_SORT_ATTR_ASC,
74
71
  :time => Sphinx::Client::SPH_SORT_TIME_SEGMENTS,
75
- :extending => Sphinx::Client::SPH_SORT_EXTENDED,
72
+ :extended => Sphinx::Client::SPH_SORT_EXTENDED,
76
73
  :expression => Sphinx::Client::SPH_SORT_EXPR,
77
74
  }
78
75
 
@@ -86,6 +83,27 @@ class ModelSet
86
83
  clear_cache!
87
84
  end
88
85
 
86
+ RANKING_MODES = {
87
+ :proximity_bm25 => Sphinx::Client::SPH_RANK_PROXIMITY_BM25,
88
+ :bm25 => Sphinx::Client::SPH_RANK_BM25,
89
+ :none => Sphinx::Client::SPH_RANK_NONE,
90
+ :word_count => Sphinx::Client::SPH_RANK_WORDCOUNT,
91
+ :proximity => Sphinx::Client::SPH_RANK_PROXIMITY,
92
+ :fieldmask => Sphinx::Client::SPH_RANK_FIELDMASK,
93
+ :sph04 => Sphinx::Client::SPH_RANK_SPH04,
94
+ :total => Sphinx::Client::SPH_RANK_TOTAL,
95
+ }
96
+
97
+ def rank_using!(mode_or_expr)
98
+ if mode_or_expr.nil?
99
+ @ranking = nil
100
+ elsif mode = RANKING_MODES[mode_or_expr]
101
+ @ranking = [mode]
102
+ else
103
+ @ranking = [Sphinx::Client::SPH_RANK_EXPR, mode_or_expr]
104
+ end
105
+ end
106
+
89
107
  def size
90
108
  fetch_results if @size.nil?
91
109
  @size
@@ -96,11 +114,23 @@ class ModelSet
96
114
  @count
97
115
  end
98
116
 
117
+ def total_count
118
+ response['total_found']
119
+ end
120
+
99
121
  def ids
100
122
  fetch_results if @ids.nil?
101
123
  @ids
102
124
  end
103
125
 
126
+ def id_field
127
+ if set_class.respond_to?(:sphinx_id_field)
128
+ set_class.sphinx_id_field
129
+ else
130
+ 'id'
131
+ end
132
+ end
133
+
104
134
  class SphinxError < StandardError
105
135
  attr_accessor :opts
106
136
  def message
@@ -111,28 +141,30 @@ class ModelSet
111
141
  private
112
142
 
113
143
  def fetch_results
114
- if @conditions.nil? or @empty
144
+ if conditions.nil? or @empty
115
145
  @count = 0
116
146
  @size = 0
117
147
  @ids = []
118
148
  else
119
149
  opts = {
120
150
  :filters => @filters,
121
- :query => conditions_clause,
151
+ :query => conditions.to_s,
122
152
  }
123
153
  before_query(opts)
124
154
 
125
155
  search = Sphinx::Client.new
126
156
  search.SetMaxQueryTime(max_query_time * 1000)
127
- search.SetServer(self.class.server_host, self.class.server_port)
157
+ search.SetServer(self.class.host, self.class.port)
158
+ search.SetSelect((@select || [id_field]).join(','))
128
159
  search.SetMatchMode(Sphinx::Client::SPH_MATCH_EXTENDED2)
129
160
  if limit
130
- search.SetLimits(offset, limit, offset + limit)
161
+ search.SetLimits(offset, limit, max_results)
131
162
  else
132
- search.SetLimits(0, MAX_SPHINX_RESULTS, MAX_SPHINX_RESULTS)
163
+ search.SetLimits(0, max_results, max_results)
133
164
  end
134
165
 
135
166
  search.SetSortMode(*@sort_order) if @sort_order
167
+ search.SetRankingMode(*@ranking) if @ranking
136
168
  search.SetFilter('class_id', model_class.class_id) if model_class.respond_to?(:class_id)
137
169
 
138
170
  if @geo
@@ -161,7 +193,7 @@ class ModelSet
161
193
  end
162
194
 
163
195
  begin
164
- response = SystemTimer.timeout(max_query_time) do
196
+ @response = SystemTimer.timeout(max_query_time) do
165
197
  search.Query(opts[:query], index)
166
198
  end
167
199
  unless response
@@ -175,8 +207,8 @@ class ModelSet
175
207
  on_exception(e)
176
208
  end
177
209
 
178
- @count = response['total_found']
179
- @ids = response['matches'].collect {|r| r['id']}.to_ordered_set
210
+ @count = [response['total_found'], max_results].min
211
+ @ids = response['matches'].collect {|match| set_class.as_id(match[id_field])}.to_ordered_set
180
212
  @size = @ids.size
181
213
 
182
214
  after_query(opts)
@@ -195,12 +227,26 @@ class ModelSet
195
227
  end
196
228
  end
197
229
 
198
- class << self
199
- attr_accessor :server_host, :server_port
230
+ def condition_ops
231
+ { :not => '-',
232
+ :and => ' ',
233
+ :or => '|' }
200
234
  end
201
235
 
202
- def conditions_clause
203
- @conditions ? @conditions.join(' ') : ''
236
+ def transform_condition(condition)
237
+ if condition.kind_of?(Hash)
238
+ condition.collect do |field, value|
239
+ next if value.nil?
240
+ field = field.join(',') if field.kind_of?(Array)
241
+ if value.kind_of?(Array)
242
+ value = [:or, *value] unless value.first.kind_of?(Symbol)
243
+ value = to_conditions(*value).to_s
244
+ end
245
+ "@(#{field}) #{value}"
246
+ end.compact
247
+ else
248
+ condition
249
+ end
204
250
  end
205
251
  end
206
252
  end