model_set 1.0.0 → 1.1.0

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