freelancing-god-thinking-sphinx 0.9.7 → 0.9.8

Sign up to get free protection for your applications and to get access to all the features.
@@ -37,6 +37,15 @@ module ThinkingSphinx
37
37
  initialize_from_builder(&block) if block_given?
38
38
  end
39
39
 
40
+ def name
41
+ model.name.underscore.tr(':/\\', '_')
42
+ end
43
+
44
+ def empty?(part = :core)
45
+ config = ThinkingSphinx::Configuration.new
46
+ File.size?("#{config.searchd_file_path}/#{self.name}_#{part}.spa").nil?
47
+ end
48
+
40
49
  def to_config(index, database_conf, charset_type)
41
50
  # Set up associations and joins
42
51
  link!
@@ -56,7 +65,7 @@ module ThinkingSphinx
56
65
 
57
66
  config = <<-SOURCE
58
67
 
59
- source #{model.name.downcase}_#{index}_core
68
+ source #{model.indexes.first.name}_#{index}_core
60
69
  {
61
70
  type = #{db_adapter}
62
71
  sql_host = #{database_conf[:host] || "localhost"}
@@ -65,6 +74,7 @@ sql_pass = #{database_conf[:password]}
65
74
  sql_db = #{database_conf[:database]}
66
75
 
67
76
  sql_query_pre = #{charset_type == "utf-8" && adapter == :mysql ? "SET NAMES utf8" : ""}
77
+ #{"sql_query_pre = SET SESSION group_concat_max_len = #{@options[:group_concat_max_len]}" if @options[:group_concat_max_len]}
68
78
  sql_query_pre = #{to_sql_query_pre}
69
79
  sql_query = #{to_sql.gsub(/\n/, ' ')}
70
80
  sql_query_range = #{to_sql_query_range}
@@ -76,9 +86,11 @@ sql_query_info = #{to_sql_query_info}
76
86
  if delta?
77
87
  config += <<-SOURCE
78
88
 
79
- source #{model.name.downcase}_#{index}_delta : #{model.name.downcase}_#{index}_core
89
+ source #{model.indexes.first.name}_#{index}_delta : #{model.indexes.first.name}_#{index}_core
80
90
  {
81
91
  sql_query_pre =
92
+ sql_query_pre = #{charset_type == "utf-8" && adapter == :mysql ? "SET NAMES utf8" : ""}
93
+ #{"sql_query_pre = SET SESSION group_concat_max_len = #{@options[:group_concat_max_len]}" if @options[:group_concat_max_len]}
82
94
  sql_query = #{to_sql(:delta => true).gsub(/\n/, ' ')}
83
95
  sql_query_range = #{to_sql_query_range :delta => true}
84
96
  }
@@ -131,7 +143,7 @@ sql_query_range = #{to_sql_query_range :delta => true}
131
143
 
132
144
  where_clause = ""
133
145
  if self.delta?
134
- where_clause << " AND #{@model.quoted_table_name}.#{quote_column('delta')}" +" = #{options[:delta] ? 1 : 0}"
146
+ where_clause << " AND #{@model.quoted_table_name}.#{quote_column('delta')}" +" = #{options[:delta] ? db_boolean(true) : db_boolean(false)}"
135
147
  end
136
148
  unless @conditions.empty?
137
149
  where_clause << " AND " << @conditions.join(" AND ")
@@ -175,11 +187,19 @@ GROUP BY #{ (
175
187
  # so pass in :delta => true to get the delta version of the SQL.
176
188
  #
177
189
  def to_sql_query_range(options={})
178
- sql = "SELECT MIN(#{quote_column(@model.primary_key)}), " +
179
- "MAX(#{quote_column(@model.primary_key)}) " +
190
+ min_statement = "MIN(#{quote_column(@model.primary_key)})"
191
+ max_statement = "MAX(#{quote_column(@model.primary_key)})"
192
+
193
+ # Fix to handle Sphinx PostgreSQL bug (it doesn't like NULLs or 0's)
194
+ if adapter == :postgres
195
+ min_statement = "COALESCE(#{min_statement}, 1)"
196
+ max_statement = "COALESCE(#{max_statement}, 1)"
197
+ end
198
+
199
+ sql = "SELECT #{min_statement}, #{max_statement} " +
180
200
  "FROM #{@model.quoted_table_name} "
181
201
  sql << "WHERE #{@model.quoted_table_name}.#{quote_column('delta')} " +
182
- "= #{options[:delta] ? 1 : 0}" if self.delta?
202
+ "= #{options[:delta] ? db_boolean(true) : db_boolean(false)}" if self.delta?
183
203
  sql
184
204
  end
185
205
 
@@ -188,7 +208,7 @@ GROUP BY #{ (
188
208
  # back to 0.
189
209
  #
190
210
  def to_sql_query_pre
191
- self.delta? ? "UPDATE #{@model.quoted_table_name} SET #{quote_column('delta')} = 0" : ""
211
+ self.delta? ? "UPDATE #{@model.quoted_table_name} SET #{quote_column('delta')} = #{db_boolean(false)}" : ""
192
212
  end
193
213
 
194
214
  # Flag to indicate whether this index has a corresponding delta index.
@@ -235,6 +255,11 @@ GROUP BY #{ (
235
255
 
236
256
  builder.instance_eval &block
237
257
 
258
+ unless @model.descends_from_active_record?
259
+ stored_class = @model.store_full_sti_class ? @model.name : @model.name.demodulize
260
+ builder.where("#{@model.quoted_table_name}.#{quote_column(@model.inheritance_column)} = '#{stored_class}'")
261
+ end
262
+
238
263
  @fields = builder.fields
239
264
  @attributes = builder.attributes
240
265
  @conditions = builder.conditions
@@ -299,5 +324,16 @@ GROUP BY #{ (
299
324
  def association(key)
300
325
  @associations[key] ||= Association.children(@model, key)
301
326
  end
327
+
328
+ # Returns the proper boolean value string literal for the
329
+ # current database adapter.
330
+ #
331
+ def db_boolean(val)
332
+ if adapter == :postgres
333
+ val ? 'TRUE' : 'FALSE'
334
+ else
335
+ val ? '1' : '0'
336
+ end
337
+ end
302
338
  end
303
- end
339
+ end
@@ -71,6 +71,13 @@ module ThinkingSphinx
71
71
  # limitations on whether they're symbols or methods or what level of
72
72
  # associations they come from.
73
73
  #
74
+ # Adding SQL Fragment Fields
75
+ #
76
+ # You can also define a field using an SQL fragment, useful for when
77
+ # you would like to index a calculated value.
78
+ #
79
+ # indexes "age < 18", :as => :minor
80
+ #
74
81
  def indexes(*args)
75
82
  options = args.extract_options!
76
83
  args.each do |columns|
@@ -116,6 +123,14 @@ module ThinkingSphinx
116
123
  # record. Might be best to read through the Sphinx documentation to get
117
124
  # a better idea of that though.
118
125
  #
126
+ # Adding SQL Fragment Attributes
127
+ #
128
+ # You can also define an attribute using an SQL fragment, useful for
129
+ # when you would like to index a calculated value. Don't forget to set
130
+ # the type of the attribute though:
131
+ #
132
+ # indexes "age < 18", :as => :minor, :type => :boolean
133
+ #
119
134
  # If you're creating attributes for latitude and longitude, don't
120
135
  # forget that Sphinx expects these values to be in radians.
121
136
  #
@@ -19,9 +19,11 @@ module ThinkingSphinx
19
19
  page = options[:page] ? options[:page].to_i : 1
20
20
 
21
21
  begin
22
- pager = WillPaginate::Collection.new(page,
23
- client.limit, results[:total] || 0)
24
- pager.replace results[:matches].collect { |match| match[:doc] }
22
+ pager = WillPaginate::Collection.create(page,
23
+ client.limit, results[:total_found] || 0) do |collection|
24
+ collection.replace results[:matches].collect { |match| match[:doc] }
25
+ collection.instance_variable_set :@total_entries, results[:total_found]
26
+ end
25
27
  rescue
26
28
  results[:matches].collect { |match| match[:doc] }
27
29
  end
@@ -50,6 +52,24 @@ module ThinkingSphinx
50
52
  #
51
53
  # User.search "pat", :include => :posts
52
54
  #
55
+ # == Advanced Searching
56
+ #
57
+ # Sphinx supports 5 different matching modes. By default Thinking Sphinx
58
+ # uses :all, which unsurprisingly requires all the supplied search terms
59
+ # to match a result.
60
+ #
61
+ # Alternative modes include:
62
+ #
63
+ # User.search "pat allan", :match_mode => :any
64
+ # User.search "pat allan", :match_mode => :phrase
65
+ # User.search "pat | allan", :match_mode => :boolean
66
+ # User.search "@name pat | @username pat", :match_mode => :extended
67
+ #
68
+ # Any will find results with any of the search terms. Phrase treats the search
69
+ # terms a single phrase instead of individual words. Boolean and extended allow
70
+ # for more complex query syntax, refer to the sphinx documentation for further
71
+ # details.
72
+ #
53
73
  # == Searching by Fields
54
74
  #
55
75
  # If you want to step it up a level, you can limit your search terms to
@@ -139,7 +159,8 @@ module ThinkingSphinx
139
159
  # you can do so in your model:
140
160
  #
141
161
  # define_index do
142
- # # ...
162
+ # has :latit # Float column, stored in radians
163
+ # has :longit # Float column, stored in radians
143
164
  #
144
165
  # set_property :latitude_attr => "latit"
145
166
  # set_property :longitude_attr => "longit"
@@ -148,12 +169,18 @@ module ThinkingSphinx
148
169
  # Now, geo-location searching really only has an affect if you have a
149
170
  # filter, sort or grouping clause related to it - otherwise it's just a
150
171
  # normal search. To make use of the positioning difference, use the
151
- # special attribute "@geo" in any of your filters or sorting or grouping
172
+ # special attribute "@geodist" in any of your filters or sorting or grouping
152
173
  # clauses.
153
174
  #
154
175
  # And don't forget - both the latitude and longitude you use in your
155
- # search, and the values in your indexes, need to be stored in radians,
156
- # _not_ degrees.
176
+ # search, and the values in your indexes, need to be stored as a float in radians,
177
+ # _not_ degrees. Keep in mind that if you do this conversion in SQL
178
+ # you will need to explicitly declare a column type of :float.
179
+ #
180
+ # define_index do
181
+ # has 'RADIANS(lat)', :as => :lat, :type => :float
182
+ # # ...
183
+ # end
157
184
  #
158
185
  def search(*args)
159
186
  results, client = search_results(*args.clone)
@@ -167,14 +194,45 @@ module ThinkingSphinx
167
194
  page = options[:page] ? options[:page].to_i : 1
168
195
 
169
196
  begin
170
- pager = WillPaginate::Collection.new(page,
171
- client.limit, results[:total] || 0)
172
- pager.replace instances_from_results(results[:matches], options, klass)
197
+ pager = WillPaginate::Collection.create(page,
198
+ client.limit, results[:total] || 0) do |collection|
199
+ collection.replace instances_from_results(results[:matches], options, klass)
200
+ collection.instance_variable_set :@total_entries, results[:total_found]
201
+ end
173
202
  rescue StandardError => err
174
203
  instances_from_results(results[:matches], options, klass)
175
204
  end
176
205
  end
177
206
 
207
+ # Checks if a document with the given id exists within a specific index.
208
+ # Expected parameters:
209
+ #
210
+ # - ID of the document
211
+ # - Index to check within
212
+ # - Options hash (defaults to {})
213
+ #
214
+ # Example:
215
+ #
216
+ # ThinkingSphinx::Search.search_for_id(10, "user_core", :class => User)
217
+ #
218
+ def search_for_id(*args)
219
+ options = args.extract_options!
220
+ client = client_from_options options
221
+
222
+ query, filters = search_conditions(
223
+ options[:class], options[:conditions] || {}
224
+ )
225
+ client.filters += filters
226
+ client.match_mode = :extended unless query.empty?
227
+ client.id_range = args.first..args.first
228
+
229
+ begin
230
+ return client.query(query, args[1])[:matches].length > 0
231
+ rescue Errno::ECONNREFUSED => err
232
+ raise ThinkingSphinx::ConnectionError, "Connection to Sphinx Daemon (searchd) failed."
233
+ end
234
+ end
235
+
178
236
  private
179
237
 
180
238
  # This method handles the common search functionality, and returns both
@@ -251,7 +309,7 @@ module ThinkingSphinx
251
309
  # Set all the appropriate settings for the client, using the provided
252
310
  # options hash.
253
311
  #
254
- def client_from_options(options)
312
+ def client_from_options(options = {})
255
313
  config = ThinkingSphinx::Configuration.new
256
314
  client = Riddle::Client.new config.address, config.port
257
315
  klass = options[:class]
@@ -385,7 +443,7 @@ module ThinkingSphinx
385
443
 
386
444
  case order = options[:order]
387
445
  when Symbol
388
- client.sort_mode ||= :attr_asc
446
+ client.sort_mode = :attr_asc if client.sort_mode == :relevance || client.sort_mode.nil?
389
447
  if fields.include?(order)
390
448
  client.sort_by = order.to_s.concat("_sort")
391
449
  else
@@ -5,18 +5,15 @@ describe "ThinkingSphinx::ActiveRecord::Delta" do
5
5
  before :each do
6
6
  Person.stub_method(:write_inheritable_array => true)
7
7
  end
8
-
9
- after :each do
10
- Person.unstub_method(:write_inheritable_array)
11
- end
12
-
13
- it "should add callbacks" do
14
- Person.after_commit :toggle_delta
15
-
16
- Person.should have_received(:write_inheritable_array).with(
17
- :after_commit, [:toggle_delta]
18
- )
19
- end
8
+
9
+ # This spec only passes with ActiveRecord 2.0.2 or earlier.
10
+ # it "should add callbacks" do
11
+ # Person.after_commit :toggle_delta
12
+ #
13
+ # Person.should have_received(:write_inheritable_array).with(
14
+ # :after_commit, [:toggle_delta]
15
+ # )
16
+ # end
20
17
 
21
18
  it "should have an after_commit method by default" do
22
19
  Person.instance_methods.should include("after_commit")
@@ -170,6 +167,7 @@ describe "ThinkingSphinx::ActiveRecord::Delta" do
170
167
  end
171
168
 
172
169
  it "shouldn't index if the environment is 'test'" do
170
+ ThinkingSphinx.unstub_method(:deltas_enabled?)
173
171
  ThinkingSphinx::Configuration.stub_method(:environment => "test")
174
172
 
175
173
  @person.send(:index_delta)
@@ -185,4 +183,4 @@ describe "ThinkingSphinx::ActiveRecord::Delta" do
185
183
  )
186
184
  end
187
185
  end
188
- end
186
+ end
@@ -9,15 +9,15 @@ describe "ThinkingSphinx::ActiveRecord::Search" do
9
9
  ActiveRecord::Base.methods.should include("search")
10
10
  end
11
11
 
12
+ it "should add search_for_id to ActiveRecord::Base" do
13
+ ActiveRecord::Base.methods.should include("search_for_id")
14
+ end
15
+
12
16
  describe "search_for_ids method" do
13
17
  before :each do
14
18
  ThinkingSphinx::Search.stub_method(:search_for_ids => true)
15
19
  end
16
20
 
17
- after :each do
18
- ThinkingSphinx::Search.unstub_method(:search_for_ids)
19
- end
20
-
21
21
  it "should call ThinkingSphinx::Search#search_for_ids with the class option set" do
22
22
  Person.search_for_ids("search")
23
23
 
@@ -40,10 +40,6 @@ describe "ThinkingSphinx::ActiveRecord::Search" do
40
40
  ThinkingSphinx::Search.stub_method(:search => true)
41
41
  end
42
42
 
43
- after :each do
44
- ThinkingSphinx::Search.unstub_method(:search)
45
- end
46
-
47
43
  it "should call ThinkingSphinx::Search#search with the class option set" do
48
44
  Person.search("search")
49
45
 
@@ -60,4 +56,26 @@ describe "ThinkingSphinx::ActiveRecord::Search" do
60
56
  )
61
57
  end
62
58
  end
63
- end
59
+
60
+ describe "search_for_id method" do
61
+ before :each do
62
+ ThinkingSphinx::Search.stub_method(:search_for_id => true)
63
+ end
64
+
65
+ it "should call ThinkingSphinx::Search#search with the class option set" do
66
+ Person.search_for_id(10)
67
+
68
+ ThinkingSphinx::Search.should have_received(:search_for_id).with(
69
+ 10, :class => Person
70
+ )
71
+ end
72
+
73
+ it "should override the class option" do
74
+ Person.search_for_id(10, :class => Friendship)
75
+
76
+ ThinkingSphinx::Search.should have_received(:search_for_id).with(
77
+ 10, :class => Person
78
+ )
79
+ end
80
+ end
81
+ end
@@ -100,6 +100,16 @@ describe "ThinkingSphinx::ActiveRecord" do
100
100
  end
101
101
  end
102
102
 
103
+ describe "in_core_index? method" do
104
+ it "should return the model's corresponding search_for_id value" do
105
+ Person.stub_method(:search_for_id => :searching_for_id)
106
+
107
+ person = Person.find(:first)
108
+ person.in_core_index?.should == :searching_for_id
109
+ Person.should have_received(:search_for_id).with(person.id, "person_core")
110
+ end
111
+ end
112
+
103
113
  describe "toggle_deleted method" do
104
114
  before :each do
105
115
  @configuration = ThinkingSphinx::Configuration.stub_instance(
@@ -112,12 +122,7 @@ describe "ThinkingSphinx::ActiveRecord" do
112
122
  ThinkingSphinx::Configuration.stub_method(:new => @configuration)
113
123
  Riddle::Client.stub_method(:new => @client)
114
124
  Person.indexes.each { |index| index.stub_method(:delta? => false) }
115
- end
116
-
117
- after :each do
118
- ThinkingSphinx::Configuration.unstub_method(:new)
119
- Riddle::Client.unstub_method(:new)
120
- Person.indexes.each { |index| index.unstub_method(:delta?) }
125
+ @person.stub_method(:in_core_index? => true)
121
126
  end
122
127
 
123
128
  it "should create a client using the Configuration's address and port" do
@@ -128,7 +133,7 @@ describe "ThinkingSphinx::ActiveRecord" do
128
133
  )
129
134
  end
130
135
 
131
- it "should update the core index's deleted flag" do
136
+ it "should update the core index's deleted flag if in core index" do
132
137
  @person.toggle_deleted
133
138
 
134
139
  @client.should have_received(:update).with(
@@ -136,8 +141,20 @@ describe "ThinkingSphinx::ActiveRecord" do
136
141
  )
137
142
  end
138
143
 
139
- it "should update the delta index's deleted flag if delta indexing is enabled" do
144
+ it "shouldn't update the core index's deleted flag if the record isn't in it" do
145
+ @person.stub_method(:in_core_index? => false)
146
+
147
+ @person.toggle_deleted
148
+
149
+ @client.should_not have_received(:update).with(
150
+ "person_core", ["sphinx_deleted"], {@person.id => 1}
151
+ )
152
+ end
153
+
154
+ it "should update the delta index's deleted flag if delta indexes are enabled and the instance's delta is true" do
155
+ ThinkingSphinx.stub_method(:deltas_enabled? => true)
140
156
  Person.indexes.each { |index| index.stub_method(:delta? => true) }
157
+ @person.delta = true
141
158
 
142
159
  @person.toggle_deleted
143
160
 
@@ -146,12 +163,61 @@ describe "ThinkingSphinx::ActiveRecord" do
146
163
  )
147
164
  end
148
165
 
149
- it "shouldn't update the delta index if delta indexing is disabled" do
166
+ it "should not update the delta index's deleted flag if delta indexes are enabled and the instance's delta is false" do
167
+ ThinkingSphinx.stub_method(:deltas_enabled? => true)
168
+ Person.indexes.each { |index| index.stub_method(:delta? => true) }
169
+ @person.delta = false
170
+
150
171
  @person.toggle_deleted
151
172
 
152
173
  @client.should_not have_received(:update).with(
153
174
  "person_delta", ["sphinx_deleted"], {@person.id => 1}
154
175
  )
155
176
  end
177
+
178
+ it "should not update the delta index's deleted flag if delta indexes are enabled and the instance's delta is equivalent to false" do
179
+ ThinkingSphinx.stub_method(:deltas_enabled? => true)
180
+ Person.indexes.each { |index| index.stub_method(:delta? => true) }
181
+ @person.delta = 0
182
+
183
+ @person.toggle_deleted
184
+
185
+ @client.should_not have_received(:update).with(
186
+ "person_delta", ["sphinx_deleted"], {@person.id => 1}
187
+ )
188
+ end
189
+
190
+ it "shouldn't update the delta index if delta indexes are disabled" do
191
+ ThinkingSphinx.stub_method(:deltas_enabled? => true)
192
+ @person.toggle_deleted
193
+
194
+ @client.should_not have_received(:update).with(
195
+ "person_delta", ["sphinx_deleted"], {@person.id => 1}
196
+ )
197
+ end
198
+
199
+ it "should not update the delta index if delta indexing is disabled" do
200
+ ThinkingSphinx.stub_method(:deltas_enabled? => false)
201
+ Person.indexes.each { |index| index.stub_method(:delta? => true) }
202
+ @person.delta = true
203
+
204
+ @person.toggle_deleted
205
+
206
+ @client.should_not have_received(:update).with(
207
+ "person_delta", ["sphinx_deleted"], {@person.id => 1}
208
+ )
209
+ end
210
+ end
211
+
212
+ describe "indexes in the inheritance chain (STI)" do
213
+ it "should hand defined indexes on a class down to its child classes" do
214
+ Child.indexes.should include(*Person.indexes)
215
+ end
216
+
217
+ it "should allow associations to other STI models" do
218
+ Child.indexes.last.link!
219
+ sql = Child.indexes.last.to_sql.gsub('$start', '0').gsub('$end', '100')
220
+ lambda { Child.connection.execute(sql) }.should_not raise_error(ActiveRecord::StatementInvalid)
221
+ end
156
222
  end
157
- end
223
+ end