model_set 0.10.6

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