model_set 0.10.6
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/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
|