thinking-sphinx 1.4.1 → 1.4.2

Sign up to get free protection for your applications and to get access to all the features.
data/README.textile CHANGED
@@ -177,3 +177,6 @@ Since I first released this library, there's been quite a few people who have su
177
177
  * Sam Goldstein
178
178
  * Matt Todd
179
179
  * Paco Guzmán
180
+ * Marcin Stecki
181
+ * Robert Glaser
182
+ * Paul Schyska
data/VERSION CHANGED
@@ -1 +1 @@
1
- 1.4.1
1
+ 1.4.2
@@ -1,8 +1,10 @@
1
1
  Feature: Search and browse models by their defined facets
2
2
 
3
- Scenario: Requesting facets
3
+ Background:
4
4
  Given Sphinx is running
5
- And I am searching on developers
5
+
6
+ Scenario: Requesting facets
7
+ Given I am searching on developers
6
8
  When I am requesting facet results
7
9
  Then I should have valid facet results
8
10
  And I should have 6 facets
@@ -14,8 +16,7 @@ Feature: Search and browse models by their defined facets
14
16
  And I should have the facet Tags
15
17
 
16
18
  Scenario: Requesting specific facets
17
- Given Sphinx is running
18
- And I am searching on developers
19
+ Given I am searching on developers
19
20
  When I am requesting facet results
20
21
  And I am requesting just the facet State
21
22
  Then I should have valid facet results
@@ -28,29 +29,25 @@ Feature: Search and browse models by their defined facets
28
29
  And I should have the facet Age
29
30
 
30
31
  Scenario: Requesting float facets
31
- Given Sphinx is running
32
- And I am searching on alphas
32
+ Given I am searching on alphas
33
33
  When I am requesting facet results
34
34
  Then I should have 1 facet
35
35
  And the Cost facet should have a 5.55 key
36
36
 
37
37
  Scenario: Requesting facet results
38
- Given Sphinx is running
39
- And I am searching on developers
38
+ Given I am searching on developers
40
39
  When I am requesting facet results
41
40
  And I drill down where Country is Australia
42
41
  Then I should get 11 results
43
42
 
44
43
  Scenario: Requesting facet results by multiple facets
45
- Given Sphinx is running
46
- And I am searching on developers
44
+ Given I am searching on developers
47
45
  When I am requesting facet results
48
46
  And I drill down where Country is Australia and Age is 30
49
47
  Then I should get 4 results
50
48
 
51
49
  Scenario: Requesting facets with classes included
52
- Given Sphinx is running
53
- And I am searching on developers
50
+ Given I am searching on developers
54
51
  When I am requesting facet results
55
52
  And I want classes included
56
53
  Then I should have valid facet results
@@ -58,8 +55,7 @@ Feature: Search and browse models by their defined facets
58
55
  And I should have the facet Class
59
56
 
60
57
  Scenario: Requesting MVA facets
61
- Given Sphinx is running
62
- And I am searching on developers
58
+ Given I am searching on developers
63
59
  When I am requesting facet results
64
60
  And I drill down where tag_ids includes the id of tag Australia
65
61
  Then I should get 11 results
@@ -68,23 +64,25 @@ Feature: Search and browse models by their defined facets
68
64
  Then I should get 5 results
69
65
 
70
66
  Scenario: Requesting MVA string facets
71
- Given Sphinx is running
72
- And I am searching on developers
67
+ Given I am searching on developers
73
68
  When I am requesting facet results
74
69
  Then the Tags facet should have an "Australia" key
75
70
  Then the Tags facet should have an "Melbourne" key
76
71
  Then the Tags facet should have an "Victoria" key
77
72
 
78
73
  Scenario: Requesting MVA facets from source queries
79
- Given Sphinx is running
80
- And I am searching on posts
74
+ Given I am searching on posts
81
75
  When I am requesting facet results
82
76
  Then the Comment Ids facet should have 9 keys
83
77
 
84
78
  Scenario: Requesting facets from a subclass
85
- Given Sphinx is running
86
- And I am searching on animals
79
+ Given I am searching on animals
87
80
  When I am requesting facet results
88
81
  And I want classes included
89
82
  Then I should have the facet Class
90
83
 
84
+ Scenario: Requesting facets with explicit value sources
85
+ Given I am searching on developers
86
+ When I am requesting facet results
87
+ Then the City facet should have a "Melbourne" key
88
+
@@ -1,3 +1,3 @@
1
1
  %w( rogue nat molly jasper moggy ).each do |name|
2
- Cat.create :name => name
2
+ Cat.new(:name => name).save(false)
3
3
  end
@@ -1,3 +1,3 @@
1
1
  %w( rover lassie gaspode ).each do |name|
2
- Dog.create :name => name
2
+ Dog.new(:name => name).save(false)
3
3
  end
@@ -1,3 +1,3 @@
1
1
  %w( fantastic ).each do |name|
2
- Fox.create :name => name
2
+ Fox.new(:name => name).save(false)
3
3
  end
@@ -9,8 +9,12 @@ class Developer < ActiveRecord::Base
9
9
  indexes country, :facet => true
10
10
  indexes state, :facet => true
11
11
  indexes tags.text, :as => :tags, :facet => true
12
+
12
13
  has age, :facet => true
13
14
  has tags(:id), :as => :tag_ids, :facet => true
14
- facet city
15
+
16
+ facet "LOWER(city)", :as => :city, :type => :string, :value => :city
17
+
18
+ group_by 'city'
15
19
  end
16
20
  end
@@ -258,6 +258,8 @@ module ThinkingSphinx
258
258
  ThinkingSphinx::Configuration.instance.client.update(
259
259
  index, ['sphinx_deleted'], {document_id => [1]}
260
260
  )
261
+ rescue Riddle::ConnectionError, ThinkingSphinx::SphinxError
262
+ # Not the end of the world if Sphinx isn't running.
261
263
  end
262
264
 
263
265
  def sphinx_offset
@@ -44,6 +44,8 @@ module ThinkingSphinx
44
44
  config.client.update index_name, attribute_names, {
45
45
  sphinx_document_id => attribute_values
46
46
  } if self.class.search_for_id(sphinx_document_id, index_name)
47
+ rescue Riddle::ConnectionError, ThinkingSphinx::SphinxError
48
+ # Not the end of the world if Sphinx isn't running.
47
49
  end
48
50
  end
49
51
  end
@@ -16,6 +16,8 @@ module ThinkingSphinx
16
16
  ThinkingSphinx::MysqlAdapter.new model
17
17
  when :postgresql
18
18
  ThinkingSphinx::PostgreSQLAdapter.new model
19
+ when Class
20
+ adapter.new model
19
21
  else
20
22
  raise "Invalid Database Adapter: Sphinx only supports MySQL and PostgreSQL, not #{adapter}"
21
23
  end
@@ -69,6 +71,13 @@ module ThinkingSphinx
69
71
  "LOWER(#{clause})"
70
72
  end
71
73
 
74
+ def case(expression, pairs, default)
75
+ "CASE #{expression} " +
76
+ pairs.keys.inject('') { |string, key|
77
+ string + "WHEN '#{key}' THEN #{pairs[key]} "
78
+ } + "ELSE #{default} END"
79
+ end
80
+
72
81
  protected
73
82
 
74
83
  def connection
@@ -89,7 +89,7 @@ module ThinkingSphinx
89
89
  # datetimes to timestamps, as needed.
90
90
  #
91
91
  def to_select_sql
92
- return nil unless include_as_association?
92
+ return nil unless include_as_association? && available?
93
93
 
94
94
  separator = all_ints? || all_datetimes? || @crc ? ',' : ' '
95
95
 
@@ -147,6 +147,8 @@ module ThinkingSphinx
147
147
  model = model.constantize
148
148
  model.define_indexes
149
149
  @configuration.indexes.concat model.to_riddle
150
+
151
+ enforce_common_attribute_types
150
152
  end
151
153
  end
152
154
 
@@ -296,5 +298,26 @@ module ThinkingSphinx
296
298
  send("#{key}=", value) if self.respond_to?("#{key}")
297
299
  end
298
300
  end
301
+
302
+ def enforce_common_attribute_types
303
+ sql_indexes = configuration.indexes.reject { |index|
304
+ index.is_a? Riddle::Configuration::DistributedIndex
305
+ }
306
+
307
+ return unless sql_indexes.any? { |index|
308
+ index.sources.any? { |source|
309
+ source.sql_attr_bigint.include? :sphinx_internal_id
310
+ }
311
+ }
312
+
313
+ sql_indexes.each { |index|
314
+ index.sources.each { |source|
315
+ next if source.sql_attr_bigint.include? :sphinx_internal_id
316
+
317
+ source.sql_attr_bigint << :sphinx_internal_id
318
+ source.sql_attr_uint.delete :sphinx_internal_id
319
+ }
320
+ }
321
+ end
299
322
  end
300
323
  end
@@ -20,13 +20,13 @@ module ThinkingSphinx
20
20
  end
21
21
 
22
22
  def toggle(instance)
23
- instance.delta = true
23
+ instance.send "#{@column}=", true
24
24
  end
25
-
25
+
26
26
  def toggled(instance)
27
- instance.delta
27
+ instance.send "#{@column}"
28
28
  end
29
-
29
+
30
30
  def reset_query(model)
31
31
  "UPDATE #{model.quoted_table_name} SET " +
32
32
  "#{model.connection.quote_column_name(@column.to_s)} = #{adapter.boolean(false)} " +
@@ -84,9 +84,10 @@ DESC
84
84
  start
85
85
  end
86
86
 
87
- desc "Add the shared folder for sphinx files for the production environment"
87
+ desc "Add the shared folder for sphinx files"
88
88
  task :shared_sphinx_folder, :roles => :web do
89
- run "mkdir -p #{shared_path}/db/sphinx/production"
89
+ rails_env = fetch(:rails_env, "production")
90
+ run "mkdir -p #{shared_path}/sphinx/#{rails_env}"
90
91
  end
91
92
 
92
93
  def rake(*tasks)
@@ -1,9 +1,10 @@
1
1
  module ThinkingSphinx
2
2
  class Facet
3
- attr_reader :property
3
+ attr_reader :property, :value_source
4
4
 
5
- def initialize(property)
6
- @property = property
5
+ def initialize(property, value_source = nil)
6
+ @property = property
7
+ @value_source = value_source
7
8
 
8
9
  if property.columns.length != 1
9
10
  raise "Can't translate Facets on multiple-column field or attribute"
@@ -104,7 +105,8 @@ module ThinkingSphinx
104
105
  item.to_crc32 == attribute_value
105
106
  }
106
107
  else
107
- objects.first.send(column.__name)
108
+ method = value_source || column.__name
109
+ objects.first.send(method)
108
110
  end
109
111
  end
110
112
 
@@ -44,6 +44,8 @@ module ThinkingSphinx
44
44
  end
45
45
 
46
46
  def populate
47
+ return if facet_names.empty?
48
+
47
49
  ThinkingSphinx::Search.bundle_searches(facet_names) { |sphinx, name|
48
50
  sphinx.search *(args + [facet_search_options(name)])
49
51
  }.each_with_index { |search, index|
@@ -69,6 +69,8 @@ module ThinkingSphinx
69
69
  # multiple data values (has_many or has_and_belongs_to_many associations).
70
70
  #
71
71
  def to_select_sql
72
+ return nil unless available?
73
+
72
74
  clause = columns_with_prefixes.join(', ')
73
75
 
74
76
  clause = adapter.concatenate(clause) if concat_ws?
@@ -10,10 +10,11 @@ module ThinkingSphinx
10
10
 
11
11
  raise "Cannot define a field or attribute in #{source.model.name} with no columns. Maybe you are trying to index a field with a reserved name (id, name). You can fix this error by using a symbol rather than a bare name (:id instead of id)." if @columns.empty? || @columns.any? { |column| !column.respond_to?(:__stack) }
12
12
 
13
- @alias = options[:as]
14
- @faceted = options[:facet]
15
- @admin = options[:admin]
16
- @sortable = options[:sortable] || false
13
+ @alias = options[:as]
14
+ @faceted = options[:facet]
15
+ @admin = options[:admin]
16
+ @sortable = options[:sortable] || false
17
+ @value_source = options[:value]
17
18
 
18
19
  @alias = @alias.to_sym unless @alias.blank?
19
20
 
@@ -40,7 +41,7 @@ module ThinkingSphinx
40
41
  def to_facet
41
42
  return nil unless @faceted
42
43
 
43
- ThinkingSphinx::Facet.new(self)
44
+ ThinkingSphinx::Facet.new(self, @value_source)
44
45
  end
45
46
 
46
47
  # Get the part of the GROUP BY clause related to this attribute - if one is
@@ -77,6 +78,10 @@ module ThinkingSphinx
77
78
  !admin
78
79
  end
79
80
 
81
+ def available?
82
+ columns.any? { |column| column_available?(column) }
83
+ end
84
+
80
85
  private
81
86
 
82
87
  # Could there be more than one value related to the parent record? If so,
@@ -128,9 +133,11 @@ module ThinkingSphinx
128
133
  # figure out how to correctly reference a column in SQL.
129
134
  #
130
135
  def column_with_prefix(column)
136
+ return nil unless column_available?(column)
137
+
131
138
  if column.is_string?
132
139
  column.__name
133
- elsif associations[column].empty?
140
+ elsif column.__stack.empty?
134
141
  "#{@model.quoted_table_name}.#{quote_column(column.__name)}"
135
142
  else
136
143
  associations[column].collect { |assoc|
@@ -144,7 +151,17 @@ module ThinkingSphinx
144
151
  def columns_with_prefixes
145
152
  @columns.collect { |column|
146
153
  column_with_prefix column
147
- }.flatten
154
+ }.flatten.compact
155
+ end
156
+
157
+ def column_available?(column)
158
+ if column.is_string?
159
+ true
160
+ elsif column.__stack.empty?
161
+ @model.column_names.include?(column.__name.to_s)
162
+ else
163
+ associations[column].any? { |assoc| assoc.has_column?(column.__name) }
164
+ end
148
165
  end
149
166
 
150
167
  # Gets a stack of associations for a specific path.
@@ -468,8 +468,10 @@ module ThinkingSphinx
468
468
  end
469
469
 
470
470
  def prepare(client)
471
- index_options = one_class ?
472
- one_class.sphinx_indexes.first.local_options : {}
471
+ index_options = {}
472
+ if one_class && one_class.sphinx_indexes && one_class.sphinx_indexes.first
473
+ index_options = one_class.sphinx_indexes.first.local_options
474
+ end
473
475
 
474
476
  [
475
477
  :max_matches, :group_by, :group_function, :group_clause,
@@ -82,6 +82,10 @@ module ThinkingSphinx
82
82
  @adapter ||= @model.sphinx_database_adapter
83
83
  end
84
84
 
85
+ def available_attributes
86
+ attributes.select { |attrib| attrib.available? }
87
+ end
88
+
85
89
  def set_source_database_settings(source)
86
90
  config = @database_configuration
87
91
 
@@ -94,7 +98,7 @@ module ThinkingSphinx
94
98
  end
95
99
 
96
100
  def set_source_attributes(source, offset, delta = false)
97
- attributes.each do |attrib|
101
+ available_attributes.each do |attrib|
98
102
  source.send(attrib.type_to_config) << attrib.config_value(offset, delta)
99
103
  end
100
104
  end
@@ -117,14 +117,30 @@ GROUP BY #{ sql_group_clause }
117
117
  if @model.table_exists? &&
118
118
  @model.column_names.include?(@model.inheritance_column)
119
119
 
120
- adapter.cast_to_unsigned(adapter.convert_nulls(
121
- adapter.crc(adapter.quote_with_table(@model.inheritance_column), true),
122
- @model.to_crc32
123
- ))
120
+ types = types_to_crcs
121
+ return @model.to_crc32.to_s if types.empty?
122
+
123
+ adapter.case(adapter.convert_nulls(
124
+ adapter.quote_with_table(@model.inheritance_column)),
125
+ types_to_crcs, @model.to_crc32)
124
126
  else
125
127
  @model.to_crc32.to_s
126
128
  end
127
129
  end
130
+
131
+ def type_values
132
+ @model.connection.select_values <<-SQL
133
+ SELECT DISTINCT #{@model.inheritance_column}
134
+ FROM #{@model.table_name}
135
+ SQL
136
+ end
137
+
138
+ def types_to_crcs
139
+ type_values.compact.inject({}) { |hash, type|
140
+ hash[type] = type.to_crc32
141
+ hash
142
+ }
143
+ end
128
144
  end
129
145
  end
130
146
  end
@@ -1,5 +1,9 @@
1
1
  require 'spec_helper'
2
2
 
3
+ class CustomAdapter < ThinkingSphinx::AbstractAdapter
4
+ #
5
+ end
6
+
3
7
  describe ThinkingSphinx::AbstractAdapter do
4
8
  describe '.detect' do
5
9
  let(:model) { stub('model') }
@@ -18,6 +22,13 @@ describe ThinkingSphinx::AbstractAdapter do
18
22
  adapter.should be_a(ThinkingSphinx::PostgreSQLAdapter)
19
23
  end
20
24
 
25
+ it "instantiates the provided class if one is provided" do
26
+ ThinkingSphinx::AbstractAdapter.stub(:adapter_for_model => CustomAdapter)
27
+ CustomAdapter.should_receive(:new).and_return(stub('adapter'))
28
+
29
+ ThinkingSphinx::AbstractAdapter.detect(model)
30
+ end
31
+
21
32
  it "raises an exception for other responses" do
22
33
  ThinkingSphinx::AbstractAdapter.stub(:adapter_for_model => :sqlite)
23
34
 
@@ -68,6 +68,15 @@ describe ThinkingSphinx::Attribute do
68
68
 
69
69
  attribute.to_select_sql.should == "CONCAT_WS(' ', IFNULL(`cricket_teams`.`name`, ''), IFNULL(`football_teams`.`name`, ''), IFNULL(`football_teams`.`league`, '')) AS `team`"
70
70
  end
71
+
72
+ it "should return nil if polymorphic association data does not exist" do
73
+ attribute = ThinkingSphinx::Attribute.new(@source,
74
+ [ThinkingSphinx::Index::FauxColumn.new(:source, :id)],
75
+ :as => :source_id, :type => :integer
76
+ )
77
+
78
+ attribute.to_select_sql.should be_nil
79
+ end
71
80
  end
72
81
 
73
82
  describe '#is_many?' do
@@ -231,6 +231,23 @@ describe ThinkingSphinx::Configuration do
231
231
  file.should_not match(/index alpha_core\s+\{\s+[^\}]*prefix_fields\s+=[^\}]*\}/m)
232
232
  end
233
233
 
234
+ describe '#generate' do
235
+ let(:config) { ThinkingSphinx::Configuration.instance }
236
+
237
+ it "should set all sphinx_internal_id attributes to bigints if one is" do
238
+ config.reset
239
+ config.generate
240
+
241
+ config.configuration.indexes.each do |index|
242
+ next if index.is_a? Riddle::Configuration::DistributedIndex
243
+
244
+ index.sources.each do |source|
245
+ source.sql_attr_bigint.should include(:sphinx_internal_id)
246
+ end
247
+ end
248
+ end
249
+ end
250
+
234
251
  describe '#client' do
235
252
  before :each do
236
253
  @config = ThinkingSphinx::Configuration.instance
@@ -329,5 +329,19 @@ describe ThinkingSphinx::Facet do
329
329
  @facet.value(alpha, {'cost' => 1093140480}).should == 10.5
330
330
  end
331
331
  end
332
+
333
+ context 'manual value source' do
334
+ let(:index) { ThinkingSphinx::Index.new(Alpha) }
335
+ let(:source) { ThinkingSphinx::Source.new(index) }
336
+ let(:column) { ThinkingSphinx::Index::FauxColumn.new('LOWER(name)') }
337
+ let(:field) { ThinkingSphinx::Field.new(source, column) }
338
+ let(:facet) { ThinkingSphinx::Facet.new(field, :name) }
339
+
340
+ it "should use the given value source to figure out the value" do
341
+ alpha = Alpha.new(:name => 'Foo')
342
+
343
+ facet.value(alpha, {'foo_facet' => 'foo'.to_crc32}).should == 'Foo'
344
+ end
345
+ end
332
346
  end
333
347
  end
@@ -44,6 +44,17 @@ describe ThinkingSphinx::Field do
44
44
  @field.unique_name.should == "col_name"
45
45
  end
46
46
  end
47
+
48
+ describe '#to_select_sql' do
49
+ it "should return nil if polymorphic association data does not exist" do
50
+ field = ThinkingSphinx::Field.new(@source,
51
+ [ThinkingSphinx::Index::FauxColumn.new(:source, :name)],
52
+ :as => :source_name
53
+ )
54
+
55
+ field.to_select_sql.should be_nil
56
+ end
57
+ end
47
58
 
48
59
  describe "prefixes method" do
49
60
  it "should default to false" do
@@ -48,6 +48,10 @@ describe ThinkingSphinx::Source do
48
48
  @source, ThinkingSphinx::Index::FauxColumn.new(:contacts, :id),
49
49
  :as => :contact_ids, :source => :query
50
50
  )
51
+ ThinkingSphinx::Attribute.new(
52
+ @source, ThinkingSphinx::Index::FauxColumn.new(:source, :id),
53
+ :as => :source_id, :type => :integer
54
+ )
51
55
 
52
56
  ThinkingSphinx::Join.new(
53
57
  @source, ThinkingSphinx::Index::FauxColumn.new(:links)
@@ -103,6 +107,12 @@ describe ThinkingSphinx::Source do
103
107
  @riddle.sql_attr_timestamp.first.should == :birthday
104
108
  end
105
109
 
110
+ it "should not include an attribute definition for polymorphic references without data" do
111
+ @riddle.sql_attr_uint.select { |uint|
112
+ uint == :source_id
113
+ }.should be_empty
114
+ end
115
+
106
116
  it "should set Sphinx Source options" do
107
117
  @riddle.sql_range_step.should == 1000
108
118
  @riddle.sql_ranged_throttle.should == 100
metadata CHANGED
@@ -1,13 +1,13 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: thinking-sphinx
3
3
  version: !ruby/object:Gem::Version
4
- hash: 5
4
+ hash: 3
5
5
  prerelease: false
6
6
  segments:
7
7
  - 1
8
8
  - 4
9
- - 1
10
- version: 1.4.1
9
+ - 2
10
+ version: 1.4.2
11
11
  platform: ruby
12
12
  authors:
13
13
  - Pat Allan
@@ -15,7 +15,7 @@ autorequire:
15
15
  bindir: bin
16
16
  cert_chain: []
17
17
 
18
- date: 2010-12-21 00:00:00 +11:00
18
+ date: 2011-01-13 00:00:00 +11:00
19
19
  default_executable:
20
20
  dependencies:
21
21
  - !ruby/object:Gem::Dependency
@@ -51,12 +51,12 @@ dependencies:
51
51
  requirements:
52
52
  - - ">="
53
53
  - !ruby/object:Gem::Version
54
- hash: 31
54
+ hash: 27
55
55
  segments:
56
56
  - 1
57
57
  - 2
58
- - 0
59
- version: 1.2.0
58
+ - 2
59
+ version: 1.2.2
60
60
  requirement: *id002
61
61
  - !ruby/object:Gem::Dependency
62
62
  type: :runtime