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.
Files changed (95) hide show
  1. data/LICENSE +20 -0
  2. data/README.rdoc +39 -0
  3. data/VERSION.yml +5 -0
  4. data/lib/model_set/conditioned.rb +33 -0
  5. data/lib/model_set/conditions.rb +103 -0
  6. data/lib/model_set/query.rb +132 -0
  7. data/lib/model_set/raw_query.rb +41 -0
  8. data/lib/model_set/raw_sql_query.rb +19 -0
  9. data/lib/model_set/set_query.rb +34 -0
  10. data/lib/model_set/solr_query.rb +70 -0
  11. data/lib/model_set/sphinx_query.rb +206 -0
  12. data/lib/model_set/sql_base_query.rb +52 -0
  13. data/lib/model_set/sql_query.rb +109 -0
  14. data/lib/model_set.rb +743 -0
  15. data/lib/multi_set.rb +67 -0
  16. data/test/model_set_test.rb +329 -0
  17. data/test/multi_set_test.rb +65 -0
  18. data/test/test_helper.rb +23 -0
  19. data/vendor/sphinx_client/README.rdoc +41 -0
  20. data/vendor/sphinx_client/Rakefile +21 -0
  21. data/vendor/sphinx_client/init.rb +1 -0
  22. data/vendor/sphinx_client/install.rb +5 -0
  23. data/vendor/sphinx_client/lib/sphinx/client.rb +1093 -0
  24. data/vendor/sphinx_client/lib/sphinx/request.rb +50 -0
  25. data/vendor/sphinx_client/lib/sphinx/response.rb +69 -0
  26. data/vendor/sphinx_client/lib/sphinx.rb +6 -0
  27. data/vendor/sphinx_client/spec/client_response_spec.rb +112 -0
  28. data/vendor/sphinx_client/spec/client_spec.rb +469 -0
  29. data/vendor/sphinx_client/spec/fixtures/default_search.php +8 -0
  30. data/vendor/sphinx_client/spec/fixtures/default_search_index.php +8 -0
  31. data/vendor/sphinx_client/spec/fixtures/excerpt_custom.php +11 -0
  32. data/vendor/sphinx_client/spec/fixtures/excerpt_default.php +8 -0
  33. data/vendor/sphinx_client/spec/fixtures/excerpt_flags.php +11 -0
  34. data/vendor/sphinx_client/spec/fixtures/field_weights.php +9 -0
  35. data/vendor/sphinx_client/spec/fixtures/filter.php +9 -0
  36. data/vendor/sphinx_client/spec/fixtures/filter_exclude.php +9 -0
  37. data/vendor/sphinx_client/spec/fixtures/filter_float_range.php +9 -0
  38. data/vendor/sphinx_client/spec/fixtures/filter_float_range_exclude.php +9 -0
  39. data/vendor/sphinx_client/spec/fixtures/filter_range.php +9 -0
  40. data/vendor/sphinx_client/spec/fixtures/filter_range_exclude.php +9 -0
  41. data/vendor/sphinx_client/spec/fixtures/filter_range_int64.php +10 -0
  42. data/vendor/sphinx_client/spec/fixtures/filter_ranges.php +10 -0
  43. data/vendor/sphinx_client/spec/fixtures/filters.php +10 -0
  44. data/vendor/sphinx_client/spec/fixtures/filters_different.php +13 -0
  45. data/vendor/sphinx_client/spec/fixtures/geo_anchor.php +9 -0
  46. data/vendor/sphinx_client/spec/fixtures/group_by_attr.php +9 -0
  47. data/vendor/sphinx_client/spec/fixtures/group_by_attrpair.php +9 -0
  48. data/vendor/sphinx_client/spec/fixtures/group_by_day.php +9 -0
  49. data/vendor/sphinx_client/spec/fixtures/group_by_day_sort.php +9 -0
  50. data/vendor/sphinx_client/spec/fixtures/group_by_month.php +9 -0
  51. data/vendor/sphinx_client/spec/fixtures/group_by_week.php +9 -0
  52. data/vendor/sphinx_client/spec/fixtures/group_by_year.php +9 -0
  53. data/vendor/sphinx_client/spec/fixtures/group_distinct.php +10 -0
  54. data/vendor/sphinx_client/spec/fixtures/id_range.php +9 -0
  55. data/vendor/sphinx_client/spec/fixtures/id_range64.php +9 -0
  56. data/vendor/sphinx_client/spec/fixtures/index_weights.php +9 -0
  57. data/vendor/sphinx_client/spec/fixtures/keywords.php +8 -0
  58. data/vendor/sphinx_client/spec/fixtures/limits.php +9 -0
  59. data/vendor/sphinx_client/spec/fixtures/limits_cutoff.php +9 -0
  60. data/vendor/sphinx_client/spec/fixtures/limits_max.php +9 -0
  61. data/vendor/sphinx_client/spec/fixtures/limits_max_cutoff.php +9 -0
  62. data/vendor/sphinx_client/spec/fixtures/match_all.php +9 -0
  63. data/vendor/sphinx_client/spec/fixtures/match_any.php +9 -0
  64. data/vendor/sphinx_client/spec/fixtures/match_boolean.php +9 -0
  65. data/vendor/sphinx_client/spec/fixtures/match_extended.php +9 -0
  66. data/vendor/sphinx_client/spec/fixtures/match_extended2.php +9 -0
  67. data/vendor/sphinx_client/spec/fixtures/match_fullscan.php +9 -0
  68. data/vendor/sphinx_client/spec/fixtures/match_phrase.php +9 -0
  69. data/vendor/sphinx_client/spec/fixtures/max_query_time.php +9 -0
  70. data/vendor/sphinx_client/spec/fixtures/miltiple_queries.php +12 -0
  71. data/vendor/sphinx_client/spec/fixtures/ranking_bm25.php +9 -0
  72. data/vendor/sphinx_client/spec/fixtures/ranking_none.php +9 -0
  73. data/vendor/sphinx_client/spec/fixtures/ranking_proximity.php +9 -0
  74. data/vendor/sphinx_client/spec/fixtures/ranking_proximity_bm25.php +9 -0
  75. data/vendor/sphinx_client/spec/fixtures/ranking_wordcount.php +9 -0
  76. data/vendor/sphinx_client/spec/fixtures/retries.php +9 -0
  77. data/vendor/sphinx_client/spec/fixtures/retries_delay.php +9 -0
  78. data/vendor/sphinx_client/spec/fixtures/select.php +9 -0
  79. data/vendor/sphinx_client/spec/fixtures/set_override.php +11 -0
  80. data/vendor/sphinx_client/spec/fixtures/sort_attr_asc.php +9 -0
  81. data/vendor/sphinx_client/spec/fixtures/sort_attr_desc.php +9 -0
  82. data/vendor/sphinx_client/spec/fixtures/sort_expr.php +9 -0
  83. data/vendor/sphinx_client/spec/fixtures/sort_extended.php +9 -0
  84. data/vendor/sphinx_client/spec/fixtures/sort_relevance.php +9 -0
  85. data/vendor/sphinx_client/spec/fixtures/sort_time_segments.php +9 -0
  86. data/vendor/sphinx_client/spec/fixtures/sphinxapi.php +1269 -0
  87. data/vendor/sphinx_client/spec/fixtures/update_attributes.php +8 -0
  88. data/vendor/sphinx_client/spec/fixtures/update_attributes_mva.php +8 -0
  89. data/vendor/sphinx_client/spec/fixtures/weights.php +9 -0
  90. data/vendor/sphinx_client/spec/sphinx/sphinx-id64.conf +67 -0
  91. data/vendor/sphinx_client/spec/sphinx/sphinx.conf +67 -0
  92. data/vendor/sphinx_client/spec/sphinx/sphinx_test.sql +86 -0
  93. data/vendor/sphinx_client/sphinx.yml.tpl +3 -0
  94. data/vendor/sphinx_client/tasks/sphinx.rake +75 -0
  95. 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,5 @@
1
+ ---
2
+ :build:
3
+ :patch: 6
4
+ :major: 0
5
+ :minor: 10
@@ -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