model_set 0.10.6
Sign up to get free protection for your applications and to get access to all the features.
- data/LICENSE +20 -0
- data/README.rdoc +39 -0
- data/VERSION.yml +5 -0
- data/lib/model_set/conditioned.rb +33 -0
- data/lib/model_set/conditions.rb +103 -0
- data/lib/model_set/query.rb +132 -0
- data/lib/model_set/raw_query.rb +41 -0
- data/lib/model_set/raw_sql_query.rb +19 -0
- data/lib/model_set/set_query.rb +34 -0
- data/lib/model_set/solr_query.rb +70 -0
- data/lib/model_set/sphinx_query.rb +206 -0
- data/lib/model_set/sql_base_query.rb +52 -0
- data/lib/model_set/sql_query.rb +109 -0
- data/lib/model_set.rb +743 -0
- data/lib/multi_set.rb +67 -0
- data/test/model_set_test.rb +329 -0
- data/test/multi_set_test.rb +65 -0
- data/test/test_helper.rb +23 -0
- data/vendor/sphinx_client/README.rdoc +41 -0
- data/vendor/sphinx_client/Rakefile +21 -0
- data/vendor/sphinx_client/init.rb +1 -0
- data/vendor/sphinx_client/install.rb +5 -0
- data/vendor/sphinx_client/lib/sphinx/client.rb +1093 -0
- data/vendor/sphinx_client/lib/sphinx/request.rb +50 -0
- data/vendor/sphinx_client/lib/sphinx/response.rb +69 -0
- data/vendor/sphinx_client/lib/sphinx.rb +6 -0
- data/vendor/sphinx_client/spec/client_response_spec.rb +112 -0
- data/vendor/sphinx_client/spec/client_spec.rb +469 -0
- data/vendor/sphinx_client/spec/fixtures/default_search.php +8 -0
- data/vendor/sphinx_client/spec/fixtures/default_search_index.php +8 -0
- data/vendor/sphinx_client/spec/fixtures/excerpt_custom.php +11 -0
- data/vendor/sphinx_client/spec/fixtures/excerpt_default.php +8 -0
- data/vendor/sphinx_client/spec/fixtures/excerpt_flags.php +11 -0
- data/vendor/sphinx_client/spec/fixtures/field_weights.php +9 -0
- data/vendor/sphinx_client/spec/fixtures/filter.php +9 -0
- data/vendor/sphinx_client/spec/fixtures/filter_exclude.php +9 -0
- data/vendor/sphinx_client/spec/fixtures/filter_float_range.php +9 -0
- data/vendor/sphinx_client/spec/fixtures/filter_float_range_exclude.php +9 -0
- data/vendor/sphinx_client/spec/fixtures/filter_range.php +9 -0
- data/vendor/sphinx_client/spec/fixtures/filter_range_exclude.php +9 -0
- data/vendor/sphinx_client/spec/fixtures/filter_range_int64.php +10 -0
- data/vendor/sphinx_client/spec/fixtures/filter_ranges.php +10 -0
- data/vendor/sphinx_client/spec/fixtures/filters.php +10 -0
- data/vendor/sphinx_client/spec/fixtures/filters_different.php +13 -0
- data/vendor/sphinx_client/spec/fixtures/geo_anchor.php +9 -0
- data/vendor/sphinx_client/spec/fixtures/group_by_attr.php +9 -0
- data/vendor/sphinx_client/spec/fixtures/group_by_attrpair.php +9 -0
- data/vendor/sphinx_client/spec/fixtures/group_by_day.php +9 -0
- data/vendor/sphinx_client/spec/fixtures/group_by_day_sort.php +9 -0
- data/vendor/sphinx_client/spec/fixtures/group_by_month.php +9 -0
- data/vendor/sphinx_client/spec/fixtures/group_by_week.php +9 -0
- data/vendor/sphinx_client/spec/fixtures/group_by_year.php +9 -0
- data/vendor/sphinx_client/spec/fixtures/group_distinct.php +10 -0
- data/vendor/sphinx_client/spec/fixtures/id_range.php +9 -0
- data/vendor/sphinx_client/spec/fixtures/id_range64.php +9 -0
- data/vendor/sphinx_client/spec/fixtures/index_weights.php +9 -0
- data/vendor/sphinx_client/spec/fixtures/keywords.php +8 -0
- data/vendor/sphinx_client/spec/fixtures/limits.php +9 -0
- data/vendor/sphinx_client/spec/fixtures/limits_cutoff.php +9 -0
- data/vendor/sphinx_client/spec/fixtures/limits_max.php +9 -0
- data/vendor/sphinx_client/spec/fixtures/limits_max_cutoff.php +9 -0
- data/vendor/sphinx_client/spec/fixtures/match_all.php +9 -0
- data/vendor/sphinx_client/spec/fixtures/match_any.php +9 -0
- data/vendor/sphinx_client/spec/fixtures/match_boolean.php +9 -0
- data/vendor/sphinx_client/spec/fixtures/match_extended.php +9 -0
- data/vendor/sphinx_client/spec/fixtures/match_extended2.php +9 -0
- data/vendor/sphinx_client/spec/fixtures/match_fullscan.php +9 -0
- data/vendor/sphinx_client/spec/fixtures/match_phrase.php +9 -0
- data/vendor/sphinx_client/spec/fixtures/max_query_time.php +9 -0
- data/vendor/sphinx_client/spec/fixtures/miltiple_queries.php +12 -0
- data/vendor/sphinx_client/spec/fixtures/ranking_bm25.php +9 -0
- data/vendor/sphinx_client/spec/fixtures/ranking_none.php +9 -0
- data/vendor/sphinx_client/spec/fixtures/ranking_proximity.php +9 -0
- data/vendor/sphinx_client/spec/fixtures/ranking_proximity_bm25.php +9 -0
- data/vendor/sphinx_client/spec/fixtures/ranking_wordcount.php +9 -0
- data/vendor/sphinx_client/spec/fixtures/retries.php +9 -0
- data/vendor/sphinx_client/spec/fixtures/retries_delay.php +9 -0
- data/vendor/sphinx_client/spec/fixtures/select.php +9 -0
- data/vendor/sphinx_client/spec/fixtures/set_override.php +11 -0
- data/vendor/sphinx_client/spec/fixtures/sort_attr_asc.php +9 -0
- data/vendor/sphinx_client/spec/fixtures/sort_attr_desc.php +9 -0
- data/vendor/sphinx_client/spec/fixtures/sort_expr.php +9 -0
- data/vendor/sphinx_client/spec/fixtures/sort_extended.php +9 -0
- data/vendor/sphinx_client/spec/fixtures/sort_relevance.php +9 -0
- data/vendor/sphinx_client/spec/fixtures/sort_time_segments.php +9 -0
- data/vendor/sphinx_client/spec/fixtures/sphinxapi.php +1269 -0
- data/vendor/sphinx_client/spec/fixtures/update_attributes.php +8 -0
- data/vendor/sphinx_client/spec/fixtures/update_attributes_mva.php +8 -0
- data/vendor/sphinx_client/spec/fixtures/weights.php +9 -0
- data/vendor/sphinx_client/spec/sphinx/sphinx-id64.conf +67 -0
- data/vendor/sphinx_client/spec/sphinx/sphinx.conf +67 -0
- data/vendor/sphinx_client/spec/sphinx/sphinx_test.sql +86 -0
- data/vendor/sphinx_client/sphinx.yml.tpl +3 -0
- data/vendor/sphinx_client/tasks/sphinx.rake +75 -0
- metadata +151 -0
data/LICENSE
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
Copyright (c) 2008 Justin Balthrop
|
2
|
+
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
4
|
+
a copy of this software and associated documentation files (the
|
5
|
+
"Software"), to deal in the Software without restriction, including
|
6
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
7
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
8
|
+
permit persons to whom the Software is furnished to do so, subject to
|
9
|
+
the following conditions:
|
10
|
+
|
11
|
+
The above copyright notice and this permission notice shall be
|
12
|
+
included in all copies or substantial portions of the Software.
|
13
|
+
|
14
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
15
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
16
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
17
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
18
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
19
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
20
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.rdoc
ADDED
@@ -0,0 +1,39 @@
|
|
1
|
+
= ModelSet
|
2
|
+
|
3
|
+
ModelSet is a array-like class for dealing with sets of ActiveRecord models. ModelSet
|
4
|
+
stores a list of ids and fetches the models lazily only when necessary. You can also add
|
5
|
+
conditions in SQL to further limit the set. Currently I support alternate queries using
|
6
|
+
the Solr search engine through a subclass, but I plan to abstract this out into a "query
|
7
|
+
engine" class that will support SQL, Solr, Sphinx, and eventually, other query methods
|
8
|
+
(possibly raw RecordCache hashes and other search engines).
|
9
|
+
|
10
|
+
== Usage:
|
11
|
+
|
12
|
+
class RobotSet < ModelSet
|
13
|
+
end
|
14
|
+
|
15
|
+
set1 = RobotSet.new([1,2,3,4]) # doesn't fetch the models
|
16
|
+
|
17
|
+
set1.each do |model| # fetches all
|
18
|
+
# do something
|
19
|
+
end
|
20
|
+
|
21
|
+
set2 = RobotSet.new([1,2])
|
22
|
+
|
23
|
+
set3 = set1 - set2
|
24
|
+
set3.ids
|
25
|
+
# => [3,4]
|
26
|
+
|
27
|
+
set3 << Robot.find(5)
|
28
|
+
set3.ids
|
29
|
+
# => [3,4,5]
|
30
|
+
|
31
|
+
== Install:
|
32
|
+
|
33
|
+
sudo gem install ninjudd-deep_clonable -s http://gems.github.com
|
34
|
+
sudo gem install ninjudd-ordered_set -s http://gems.github.com
|
35
|
+
sudo gem install ninjudd-model_set -s http://gems.github.com
|
36
|
+
|
37
|
+
== License:
|
38
|
+
|
39
|
+
Copyright (c) 2009 Justin Balthrop, Geni.com; Published under The MIT License, see LICENSE
|
data/VERSION.yml
ADDED
@@ -0,0 +1,33 @@
|
|
1
|
+
class ModelSet
|
2
|
+
module Conditioned
|
3
|
+
# Shared methods for dealing with conditions.
|
4
|
+
attr_reader :conditions
|
5
|
+
|
6
|
+
def add_conditions!(*conditions)
|
7
|
+
operator = conditions.shift if conditions.first.kind_of?(Symbol)
|
8
|
+
operator ||= :and
|
9
|
+
|
10
|
+
# Sanitize conditions.
|
11
|
+
conditions.collect! do |condition|
|
12
|
+
condition.kind_of?(Conditions) ? condition : Conditions.new( sanitize_condition(condition) )
|
13
|
+
end
|
14
|
+
|
15
|
+
if operator == :not
|
16
|
+
# In this case, :not actually means :and :not.
|
17
|
+
conditions = ~Conditions.new(:and, *conditions)
|
18
|
+
operator = :and
|
19
|
+
end
|
20
|
+
|
21
|
+
conditions << @conditions if @conditions
|
22
|
+
@conditions = Conditions.new(operator, *conditions)
|
23
|
+
|
24
|
+
clear_cache!
|
25
|
+
end
|
26
|
+
|
27
|
+
def invert!
|
28
|
+
raise 'cannot invert without conditions' if @conditions.nil?
|
29
|
+
@conditions = ~@conditions
|
30
|
+
clear_cache!
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
@@ -0,0 +1,103 @@
|
|
1
|
+
class ModelSet
|
2
|
+
class Conditions
|
3
|
+
deep_clonable
|
4
|
+
|
5
|
+
attr_reader :operator, :conditions
|
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
|
+
def new(*args)
|
20
|
+
self.class.new(*args)
|
21
|
+
end
|
22
|
+
|
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)]
|
34
|
+
else
|
35
|
+
# Compact the conditions if possible.
|
36
|
+
@conditions = []
|
37
|
+
args.each do |clause|
|
38
|
+
self << clause
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
def terminal?
|
45
|
+
operator.nil?
|
46
|
+
end
|
47
|
+
|
48
|
+
def <<(clause)
|
49
|
+
raise 'cannot append conditions to a terminal' if terminal?
|
50
|
+
|
51
|
+
clause = self.class.new(clause)
|
52
|
+
if clause.operator == operator
|
53
|
+
@conditions.concat(clause.conditions)
|
54
|
+
else
|
55
|
+
@conditions << clause
|
56
|
+
end
|
57
|
+
@conditions.uniq!
|
58
|
+
end
|
59
|
+
|
60
|
+
def ~
|
61
|
+
if operator == :not
|
62
|
+
conditions.first.clone
|
63
|
+
else
|
64
|
+
new(:not, self)
|
65
|
+
end
|
66
|
+
end
|
67
|
+
|
68
|
+
def |(other)
|
69
|
+
new(:or, self, other)
|
70
|
+
end
|
71
|
+
|
72
|
+
def &(other)
|
73
|
+
new(:and, self, other)
|
74
|
+
end
|
75
|
+
|
76
|
+
def to_s
|
77
|
+
return conditions.first if terminal?
|
78
|
+
|
79
|
+
condition_strings = conditions.collect do |condition|
|
80
|
+
condition.operator == :not ? condition.to_s : "(#{condition.to_s})"
|
81
|
+
end.sort_by {|s| s.size}
|
82
|
+
|
83
|
+
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 ')}"
|
90
|
+
end
|
91
|
+
end
|
92
|
+
|
93
|
+
def hash
|
94
|
+
# for uniq
|
95
|
+
[operator, conditions].hash
|
96
|
+
end
|
97
|
+
|
98
|
+
def eql?(other)
|
99
|
+
# for uniq
|
100
|
+
self.hash == other.hash
|
101
|
+
end
|
102
|
+
end
|
103
|
+
end
|
@@ -0,0 +1,132 @@
|
|
1
|
+
class ModelSet
|
2
|
+
class Query
|
3
|
+
deep_clonable
|
4
|
+
|
5
|
+
def initialize(model_set = ModelSet)
|
6
|
+
if model_set.kind_of?(Class)
|
7
|
+
@set_class = model_set
|
8
|
+
else
|
9
|
+
@set_class = model_set.class
|
10
|
+
anchor!(model_set.query) if model_set.query
|
11
|
+
end
|
12
|
+
end
|
13
|
+
|
14
|
+
def order_by!(order)
|
15
|
+
@sort_order = order
|
16
|
+
clear_cache!
|
17
|
+
end
|
18
|
+
|
19
|
+
def unsorted!
|
20
|
+
@sort_order = nil
|
21
|
+
clear_cache!
|
22
|
+
end
|
23
|
+
|
24
|
+
def page!(page)
|
25
|
+
@page = page ? page.to_i : nil
|
26
|
+
@offset = nil
|
27
|
+
clear_limited_cache!
|
28
|
+
end
|
29
|
+
|
30
|
+
def limit_enabled?
|
31
|
+
true # Override if limit is not possible for subclass.
|
32
|
+
end
|
33
|
+
|
34
|
+
def limit!(limit, offset = nil)
|
35
|
+
@limit = limit ? limit.to_i : nil
|
36
|
+
@offset = offset ? offset.to_i : nil
|
37
|
+
@page = nil if offset
|
38
|
+
clear_limited_cache!
|
39
|
+
end
|
40
|
+
|
41
|
+
def unlimited!
|
42
|
+
@limit = nil
|
43
|
+
@offset = nil
|
44
|
+
@page = nil
|
45
|
+
clear_limited_cache!
|
46
|
+
end
|
47
|
+
|
48
|
+
def clear_limited_cache!
|
49
|
+
@ids = nil
|
50
|
+
@size = nil
|
51
|
+
self
|
52
|
+
end
|
53
|
+
|
54
|
+
def clear_cache!
|
55
|
+
@count = nil
|
56
|
+
clear_limited_cache!
|
57
|
+
end
|
58
|
+
|
59
|
+
attr_reader :set_class
|
60
|
+
delegate :id_field, :id_field_with_prefix, :to => :set_class
|
61
|
+
|
62
|
+
def model_class
|
63
|
+
set_class.query_model_class
|
64
|
+
end
|
65
|
+
|
66
|
+
def model_name
|
67
|
+
model_class.name
|
68
|
+
end
|
69
|
+
|
70
|
+
def table_name
|
71
|
+
model_class.table_name
|
72
|
+
end
|
73
|
+
|
74
|
+
attr_reader :limit, :sort_order
|
75
|
+
|
76
|
+
def offset
|
77
|
+
if limit
|
78
|
+
@offset ||= @page ? (@page - 1) * limit : 0
|
79
|
+
end
|
80
|
+
end
|
81
|
+
|
82
|
+
def page
|
83
|
+
if limit
|
84
|
+
@page ||= @offset ? (@offset / limit) : 1
|
85
|
+
end
|
86
|
+
end
|
87
|
+
|
88
|
+
def pages
|
89
|
+
limit ? (1.0 * count / limit).ceil : 1
|
90
|
+
end
|
91
|
+
|
92
|
+
def before_query(*args)
|
93
|
+
proc = self.class.before_query
|
94
|
+
proc.bind(self).call(*args) if proc
|
95
|
+
end
|
96
|
+
|
97
|
+
def self.before_query(&block)
|
98
|
+
if block
|
99
|
+
@before_query = block
|
100
|
+
else
|
101
|
+
@before_query
|
102
|
+
end
|
103
|
+
end
|
104
|
+
|
105
|
+
def on_exception(*args)
|
106
|
+
proc = self.class.on_exception
|
107
|
+
proc ? proc.bind(self).call(*args) : raise(args.first)
|
108
|
+
end
|
109
|
+
|
110
|
+
def self.on_exception(&block)
|
111
|
+
if block
|
112
|
+
@on_exception = block
|
113
|
+
else
|
114
|
+
@on_exception
|
115
|
+
end
|
116
|
+
end
|
117
|
+
|
118
|
+
def after_query(*args)
|
119
|
+
proc = self.class.after_query
|
120
|
+
proc.bind(self).call(*args) if proc
|
121
|
+
end
|
122
|
+
|
123
|
+
def self.after_query(*args, &block)
|
124
|
+
if block
|
125
|
+
@after_query = block
|
126
|
+
else
|
127
|
+
@after_query
|
128
|
+
end
|
129
|
+
end
|
130
|
+
|
131
|
+
end
|
132
|
+
end
|
@@ -0,0 +1,41 @@
|
|
1
|
+
class ModelSet
|
2
|
+
class RawQuery < Query
|
3
|
+
attr_reader :records
|
4
|
+
|
5
|
+
def anchor!(query, raw_method = 'find_raw_by_id')
|
6
|
+
@records = model_class.send(raw_method, query.ids.to_a)
|
7
|
+
end
|
8
|
+
|
9
|
+
def select!(&block)
|
10
|
+
records.select!(&block)
|
11
|
+
end
|
12
|
+
|
13
|
+
def reject!(&block)
|
14
|
+
records.reject!(&block)
|
15
|
+
end
|
16
|
+
|
17
|
+
def sort_by!(&block)
|
18
|
+
@records = records.sort_by(&block)
|
19
|
+
end
|
20
|
+
|
21
|
+
def ids
|
22
|
+
if limit
|
23
|
+
(records[offset, limit] || []).collect {|r| r['id'].to_i}
|
24
|
+
else
|
25
|
+
records.collect {|r| r['id'].to_i}
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
def size
|
30
|
+
if limit
|
31
|
+
[count - offset, limit].min
|
32
|
+
else
|
33
|
+
count
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
def count
|
38
|
+
records.size
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
@@ -0,0 +1,19 @@
|
|
1
|
+
class ModelSet
|
2
|
+
class RawSQLQuery < SQLBaseQuery
|
3
|
+
def sql=(sql)
|
4
|
+
@sql = sanitize_condition(sql)
|
5
|
+
['LIMIT', 'OFFSET'].each do |term|
|
6
|
+
raise "#{term} not permitted in raw sql" if @sql.match(/ #{term} \d+/i)
|
7
|
+
end
|
8
|
+
end
|
9
|
+
|
10
|
+
def sql
|
11
|
+
"#{@sql} #{limit_clause}"
|
12
|
+
end
|
13
|
+
|
14
|
+
def count
|
15
|
+
# The only way to get the count if there is a limit is to fetch all ids without the limit.
|
16
|
+
@count ||= limit ? fetch_id_set(@sql).size : size
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
@@ -0,0 +1,34 @@
|
|
1
|
+
class ModelSet
|
2
|
+
class SetQuery < Query
|
3
|
+
delegate :add!, :unshift!, :subtract!, :intersect!, :reorder!, :reverse!, :reverse_reorder!, :shuffle!, :to => :set
|
4
|
+
|
5
|
+
def anchor!(query)
|
6
|
+
@set = query.ids.to_ordered_set
|
7
|
+
end
|
8
|
+
|
9
|
+
def set
|
10
|
+
@set ||= [].to_ordered_set
|
11
|
+
end
|
12
|
+
|
13
|
+
def ids
|
14
|
+
if limit
|
15
|
+
set.limit(limit, offset)
|
16
|
+
else
|
17
|
+
set.clone
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
def size
|
22
|
+
if limit
|
23
|
+
[count - offset, limit].min
|
24
|
+
else
|
25
|
+
count
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
def count
|
30
|
+
set.size
|
31
|
+
end
|
32
|
+
|
33
|
+
end
|
34
|
+
end
|
@@ -0,0 +1,70 @@
|
|
1
|
+
class ModelSet
|
2
|
+
class SolrQuery < Query
|
3
|
+
include Conditioned
|
4
|
+
|
5
|
+
MAX_SOLR_RESULTS = 1000
|
6
|
+
|
7
|
+
def anchor!(query)
|
8
|
+
add_conditions!( ids_clause(query.ids) )
|
9
|
+
end
|
10
|
+
|
11
|
+
def size
|
12
|
+
fetch_results if @size.nil?
|
13
|
+
@size
|
14
|
+
end
|
15
|
+
|
16
|
+
def count
|
17
|
+
fetch_results if @count.nil?
|
18
|
+
@count
|
19
|
+
end
|
20
|
+
|
21
|
+
def ids
|
22
|
+
fetch_results if @ids.nil?
|
23
|
+
@ids
|
24
|
+
end
|
25
|
+
|
26
|
+
private
|
27
|
+
|
28
|
+
def fetch_results
|
29
|
+
query = "#{conditions.to_s};#{@sort_order.to_s}"
|
30
|
+
|
31
|
+
solr_params = []
|
32
|
+
solr_params << "q=#{ ERB::Util::url_encode(query) }"
|
33
|
+
solr_params << "wt=ruby"
|
34
|
+
solr_params << "fl=pk_i"
|
35
|
+
|
36
|
+
if limit
|
37
|
+
solr_params << "rows=#{limit}"
|
38
|
+
solr_params << "start=#{offset}"
|
39
|
+
else
|
40
|
+
solr_params << "rows=#{MAX_SOLR_RESULTS}"
|
41
|
+
end
|
42
|
+
|
43
|
+
solr_params = solr_params.join('&')
|
44
|
+
before_query(solr_params)
|
45
|
+
|
46
|
+
# Catch any errors when calling solr so we can log the params.
|
47
|
+
begin
|
48
|
+
resp = eval ActsAsSolr::Post.execute(solr_params)
|
49
|
+
rescue Exception => e
|
50
|
+
on_exception(e, solr_params)
|
51
|
+
end
|
52
|
+
|
53
|
+
after_query(solr_params)
|
54
|
+
|
55
|
+
@count = resp['response']['numFound']
|
56
|
+
@ids = resp['response']['docs'].collect {|doc| doc['pk_i'].to_i}.to_ordered_set
|
57
|
+
@size = @ids.size
|
58
|
+
end
|
59
|
+
|
60
|
+
def ids_clause(ids, field = nil)
|
61
|
+
return 'pk_i:(false)' if ids.empty?
|
62
|
+
field ||= 'pk_i'
|
63
|
+
"#{field}:(#{ids.join(' OR ')})"
|
64
|
+
end
|
65
|
+
|
66
|
+
def sanitize_condition(condition)
|
67
|
+
condition
|
68
|
+
end
|
69
|
+
end
|
70
|
+
end
|
@@ -0,0 +1,206 @@
|
|
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
|
11
|
+
|
12
|
+
class ModelSet
|
13
|
+
class SphinxQuery < Query
|
14
|
+
MAX_SPHINX_RESULTS = 1000
|
15
|
+
MAX_QUERY_TIME = 5
|
16
|
+
|
17
|
+
attr_reader :conditions, :filters
|
18
|
+
|
19
|
+
def max_query_time
|
20
|
+
@max_query_time || MAX_QUERY_TIME
|
21
|
+
end
|
22
|
+
|
23
|
+
def max_query_time!(seconds)
|
24
|
+
@max_query_time = seconds
|
25
|
+
end
|
26
|
+
|
27
|
+
def anchor!(query)
|
28
|
+
add_filters!( id_field => query.ids.to_a )
|
29
|
+
end
|
30
|
+
|
31
|
+
def add_filters!(filters)
|
32
|
+
@filters ||= []
|
33
|
+
|
34
|
+
filters.each do |key, value|
|
35
|
+
next if value.nil?
|
36
|
+
@empty = true if value.kind_of?(Array) and value.empty?
|
37
|
+
@filters << [key, value]
|
38
|
+
end
|
39
|
+
clear_cache!
|
40
|
+
end
|
41
|
+
|
42
|
+
def geo_anchor!(opts)
|
43
|
+
@geo = opts
|
44
|
+
end
|
45
|
+
|
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.collect {|v| '"' + v + '"'}.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
|
+
def index
|
63
|
+
@index ||= '*'
|
64
|
+
end
|
65
|
+
|
66
|
+
def use_index!(index)
|
67
|
+
@index = index
|
68
|
+
end
|
69
|
+
|
70
|
+
SORT_MODES = {
|
71
|
+
:relevance => Sphinx::Client::SPH_SORT_RELEVANCE,
|
72
|
+
:descending => Sphinx::Client::SPH_SORT_ATTR_DESC,
|
73
|
+
:ascending => Sphinx::Client::SPH_SORT_ATTR_ASC,
|
74
|
+
:time => Sphinx::Client::SPH_SORT_TIME_SEGMENTS,
|
75
|
+
:extending => Sphinx::Client::SPH_SORT_EXTENDED,
|
76
|
+
:expression => Sphinx::Client::SPH_SORT_EXPR,
|
77
|
+
}
|
78
|
+
|
79
|
+
def order_by!(field, mode = :ascending)
|
80
|
+
if field == :relevance
|
81
|
+
@sort_order = [SORT_MODES[:relevance]]
|
82
|
+
else
|
83
|
+
raise "invalid mode: :#{mode}" unless SORT_MODES[mode]
|
84
|
+
@sort_order = [SORT_MODES[mode], field.to_s]
|
85
|
+
end
|
86
|
+
clear_cache!
|
87
|
+
end
|
88
|
+
|
89
|
+
def size
|
90
|
+
fetch_results if @size.nil?
|
91
|
+
@size
|
92
|
+
end
|
93
|
+
|
94
|
+
def count
|
95
|
+
fetch_results if @count.nil?
|
96
|
+
@count
|
97
|
+
end
|
98
|
+
|
99
|
+
def ids
|
100
|
+
fetch_results if @ids.nil?
|
101
|
+
@ids
|
102
|
+
end
|
103
|
+
|
104
|
+
class SphinxError < StandardError
|
105
|
+
attr_accessor :opts
|
106
|
+
def message
|
107
|
+
"#{super}: #{opts.inspect}"
|
108
|
+
end
|
109
|
+
end
|
110
|
+
|
111
|
+
private
|
112
|
+
|
113
|
+
def fetch_results
|
114
|
+
if @conditions.nil? or @empty
|
115
|
+
@count = 0
|
116
|
+
@size = 0
|
117
|
+
@ids = []
|
118
|
+
else
|
119
|
+
opts = {
|
120
|
+
:filters => @filters,
|
121
|
+
:query => conditions_clause,
|
122
|
+
}
|
123
|
+
before_query(opts)
|
124
|
+
|
125
|
+
search = Sphinx::Client.new
|
126
|
+
search.SetMaxQueryTime(max_query_time * 1000)
|
127
|
+
search.SetServer(self.class.server_host, self.class.server_port)
|
128
|
+
search.SetMatchMode(Sphinx::Client::SPH_MATCH_EXTENDED2)
|
129
|
+
if limit
|
130
|
+
search.SetLimits(offset, limit, offset + limit)
|
131
|
+
else
|
132
|
+
search.SetLimits(0, MAX_SPHINX_RESULTS, MAX_SPHINX_RESULTS)
|
133
|
+
end
|
134
|
+
|
135
|
+
search.SetSortMode(*@sort_order) if @sort_order
|
136
|
+
search.SetFilter('class_id', model_class.class_id) if model_class.respond_to?(:class_id)
|
137
|
+
|
138
|
+
if @geo
|
139
|
+
# Latitude and longitude in radians, radius in meters.
|
140
|
+
lat_field = @geo[:latitude_field] || "#{@geo[:prefix]}_latitude"
|
141
|
+
long_field = @geo[:longitude_field] || "#{@geo[:prefix]}_longitude"
|
142
|
+
|
143
|
+
search.SetGeoAnchor(lat_field, long_field, @geo[:latitude].to_f, @geo[:longitude].to_f)
|
144
|
+
search.SetFloatRange('@geodist', 0.0, @geo[:radius].to_f)
|
145
|
+
end
|
146
|
+
|
147
|
+
@filters and @filters.each do |field, value|
|
148
|
+
exclude = defined?(AntiObject) && value.kind_of?(AntiObject)
|
149
|
+
value = ~value if exclude
|
150
|
+
|
151
|
+
if value.kind_of?(Range)
|
152
|
+
min, max = filter_values([value.begin, value.end])
|
153
|
+
if min.kind_of?(Float) or max.kind_of?(Float)
|
154
|
+
search.SetFilterFloatRange(field.to_s, min.to_f, max.to_f, exclude)
|
155
|
+
else
|
156
|
+
search.SetFilterRange(field.to_s, min, max, exclude)
|
157
|
+
end
|
158
|
+
else
|
159
|
+
search.SetFilter(field.to_s, filter_values(value), exclude)
|
160
|
+
end
|
161
|
+
end
|
162
|
+
|
163
|
+
begin
|
164
|
+
response = SystemTimer.timeout(max_query_time) do
|
165
|
+
search.Query(opts[:query], index)
|
166
|
+
end
|
167
|
+
unless response
|
168
|
+
e = SphinxError.new(search.GetLastError)
|
169
|
+
e.opts = opts
|
170
|
+
raise e
|
171
|
+
end
|
172
|
+
rescue Exception => e
|
173
|
+
e = SphinxError.new(e) unless e.kind_of?(SphinxError)
|
174
|
+
e.opts = opts
|
175
|
+
on_exception(e)
|
176
|
+
end
|
177
|
+
|
178
|
+
@count = response['total_found']
|
179
|
+
@ids = response['matches'].collect {|r| r['id']}.to_ordered_set
|
180
|
+
@size = @ids.size
|
181
|
+
|
182
|
+
after_query(opts)
|
183
|
+
end
|
184
|
+
end
|
185
|
+
|
186
|
+
def filter_values(values)
|
187
|
+
Array(values).collect do |value|
|
188
|
+
case value
|
189
|
+
when Date : value.to_time.to_i
|
190
|
+
when TrueClass : 1
|
191
|
+
when FalseClass : 0
|
192
|
+
else
|
193
|
+
value.to_i
|
194
|
+
end
|
195
|
+
end
|
196
|
+
end
|
197
|
+
|
198
|
+
class << self
|
199
|
+
attr_accessor :server_host, :server_port
|
200
|
+
end
|
201
|
+
|
202
|
+
def conditions_clause
|
203
|
+
@conditions ? @conditions.join(' ') : ''
|
204
|
+
end
|
205
|
+
end
|
206
|
+
end
|