thinking-sphinx 1.4.11 → 1.4.12

Sign up to get free protection for your applications and to get access to all the features.
data/README.textile CHANGED
@@ -204,3 +204,8 @@ Since I first released this library, there's been quite a few people who have su
204
204
  * Tony Pitale
205
205
  * Kenn Ejima
206
206
  * Matthew Barnett
207
+ * Ilia Lobsanov
208
+ * Andrew Hunter
209
+ * Simon Hürlimann
210
+ * Nathan Smith
211
+ * Cedric Maion
@@ -12,11 +12,11 @@ Feature: Handle not-quite-supported column types as attributes
12
12
  Scenario: Dates as Datetimes
13
13
  Given Sphinx is running
14
14
  And I am searching on alphas
15
- When I filter between 1 and 3 days ago on created_on by date
16
- Then I should get 2 results
15
+ When I filter between 2 and 4 days ago on created_on by date
16
+ Then I should get 3 results
17
17
 
18
18
  Scenario: Timestamps as Datetimes
19
19
  Given Sphinx is running
20
20
  And I am searching on alphas
21
- When I filter between 1 and 3 days ago on created_at
22
- Then I should get 2 results
21
+ When I filter between 2 and 4 days ago on created_at
22
+ Then I should get 2 results
@@ -1,10 +1,8 @@
1
- Alpha.create :name => "one", :value => 1, :cost => 1.51, :created_on => 1.day.ago.to_date, :created_at => 1.day.ago
2
- Alpha.create :name => "two", :value => 2, :cost => 2.52, :created_on => 2.day.ago.to_date, :created_at => 2.day.ago
3
- Alpha.create :name => "three", :value => 3, :cost => 3.53, :created_on => 3.day.ago.to_date, :created_at => 3.day.ago
4
- Alpha.create :name => "four", :value => 4, :cost => 4.54, :created_on => 4.day.ago.to_date, :created_at => 4.day.ago
5
- Alpha.create :name => "five", :value => 5, :cost => 5.55, :created_on => 5.day.ago.to_date, :created_at => 5.day.ago
6
- Alpha.create :name => "six", :value => 6, :cost => 6.56, :created_on => 6.day.ago.to_date, :created_at => 6.day.ago
7
- Alpha.create :name => "seven", :value => 7, :cost => 7.57, :created_on => 7.day.ago.to_date, :created_at => 7.day.ago
8
- Alpha.create :name => "eight", :value => 8, :cost => 8.58, :created_on => 8.day.ago.to_date, :created_at => 8.day.ago
9
- Alpha.create :name => "nine", :value => 9, :cost => 9.59, :created_on => 9.day.ago.to_date, :created_at => 9.day.ago
10
- Alpha.create :name => "ten", :value => 10, :cost => 10.50, :created_on => 10.day.ago.to_date, :created_at => 10.day.ago
1
+ %w(
2
+ one two three four five six seven eight nine ten
3
+ ).each_with_index do |number, index|
4
+ value = index + 1
5
+ cost = value.to_f + 0.5 + (value * 0.01)
6
+ Alpha.create :name => number, :value => value, :cost => cost,
7
+ :created_on => value.days.ago.to_date, :created_at => value.days.ago
8
+ end
@@ -16,11 +16,20 @@ module Cucumber
16
16
  @fixtures_directory = "#{pwd}/features/thinking_sphinx/db/fixtures"
17
17
  @database_file = "#{pwd}/features/thinking_sphinx/database.yml"
18
18
 
19
- @adapter = (ENV['DATABASE'] || 'mysql').gsub /^mysql$/, 'mysql2'
19
+ @adapter = (ENV['DATABASE'] || 'mysql')
20
20
  @database = 'thinking_sphinx'
21
- @username = @adapter[/mysql/] ? 'root' : 'postgres'
22
- # @password = 'thinking_sphinx'
21
+ @username = ENV['USER']
23
22
  @host = 'localhost'
23
+
24
+ if ActiveRecord.constants.include?('VERSION')
25
+ @adapter = @adapter.gsub /^mysql$/, 'mysql2'
26
+ end
27
+
28
+ if @adapter[/mysql/]
29
+ @username = 'root'
30
+ elsif ENV['TRAVIS']
31
+ @username = 'postgres'
32
+ end
24
33
  end
25
34
 
26
35
  def setup
@@ -17,7 +17,7 @@ module ThinkingSphinx
17
17
  extend ThinkingSphinx::ActiveRecord::ClassMethods
18
18
 
19
19
  class << self
20
- attr_accessor :sphinx_index_blocks
20
+ attr_accessor :sphinx_index_blocks, :sphinx_types
21
21
 
22
22
  def set_sphinx_primary_key(attribute)
23
23
  @sphinx_primary_key_attribute = attribute
@@ -49,6 +49,10 @@ module ThinkingSphinx
49
49
  sphinx_indexes.last.options
50
50
  end
51
51
 
52
+ def set_sphinx_types(types)
53
+ @sphinx_types = types
54
+ end
55
+
52
56
  # Generate a unique CRC value for the model's name, to use to
53
57
  # determine which Sphinx documents belong to which AR records.
54
58
  #
@@ -20,7 +20,7 @@ module ThinkingSphinx
20
20
  end
21
21
 
22
22
  def group_concatenate(clause, separator = ' ')
23
- if connection.raw_connection.server_version >= 80400
23
+ if server_version >= 80400
24
24
  "array_to_string(array_agg(COALESCE(#{clause}, '0')), '#{separator}')"
25
25
  else
26
26
  "array_to_string(array_accum(COALESCE(#{clause}, '0')), '#{separator}')"
@@ -109,11 +109,9 @@ module ThinkingSphinx
109
109
  end
110
110
 
111
111
  def create_array_accum_function
112
- if connection.raw_connection.respond_to?(:server_version) &&
113
- connection.raw_connection.server_version >= 80400
112
+ if server_version >= 80400
114
113
  return
115
- elsif connection.raw_connection.respond_to?(:server_version) &&
116
- connection.raw_connection.server_version > 80200
114
+ elsif server_version > 80200
117
115
  execute <<-SQL
118
116
  CREATE AGGREGATE array_accum (anyelement)
119
117
  (
@@ -175,5 +173,16 @@ module ThinkingSphinx
175
173
  SQL
176
174
  execute function, true
177
175
  end
176
+
177
+ def server_version
178
+ if RUBY_PLATFORM == 'java'
179
+ (connection.raw_connection.connection.server_major_version * 10000) +
180
+ (connection.raw_connection.connection.server_minor_version * 100)
181
+ elsif connection.raw_connection.respond_to?(:server_version)
182
+ connection.raw_connection.server_version
183
+ else
184
+ 0
185
+ end
186
+ end
178
187
  end
179
188
  end
@@ -269,7 +269,7 @@ WHERE #{@source.index.delta_object.clause(model, true)})
269
269
  end
270
270
 
271
271
  def end_association_for_mva
272
- @association_for_mva ||= associations[columns.first].detect { |assoc|
272
+ @association_for_mva ||= associations[columns.first.__stack].detect { |assoc|
273
273
  assoc.has_column?(columns.first.__name)
274
274
  }
275
275
  end
@@ -358,8 +358,8 @@ block:
358
358
 
359
359
  def all_of_type?(*column_types)
360
360
  @columns.all? { |col|
361
- klasses = @associations[col].empty? ? [@model] :
362
- @associations[col].collect { |assoc| assoc.reflection.klass }
361
+ klasses = @associations[col.__stack].empty? ? [@model] :
362
+ @associations[col.__stack].collect { |assoc| assoc.reflection.klass }
363
363
  klasses.all? { |klass|
364
364
  column = klass.columns.detect { |column| column.name == col.__name.to_s }
365
365
  !column.nil? && column_types.include?(column.type)
@@ -7,9 +7,9 @@ module ThinkingSphinx
7
7
  require "riddle/#{version}"
8
8
  when /1.10/
9
9
  require 'riddle/1.10'
10
- when /2.0.\d/
10
+ when /2.0.[12]/
11
11
  require 'riddle/2.0.1'
12
- when /2.1.\d/
12
+ when /2.0.3/, /2.0.4/, /2.1.\d/
13
13
  require 'riddle/2.1.0'
14
14
  else
15
15
  documentation_link = %Q{
@@ -195,6 +195,14 @@ module ThinkingSphinx
195
195
  @configuration.searchd.port = port
196
196
  end
197
197
 
198
+ def use_socket=(use_socket)
199
+ if use_socket
200
+ socket = "#{app_root}/tmp/sockets/searchd.#{self.environment}.sock"
201
+ @configuration.searchd.listen = socket
202
+ self.address = socket
203
+ end
204
+ end
205
+
198
206
  def pid_file
199
207
  @configuration.searchd.pid_file
200
208
  end
@@ -92,7 +92,7 @@ DESC
92
92
  rails_env = fetch(:rails_env, "production")
93
93
  rake = fetch(:rake, "rake")
94
94
  tasks.each do |t|
95
- run "if [ -d #{release_path} ]; then cd #{release_path}; else cd #{current_path}; fi; #{rake} RAILS_ENV=#{rails_env} #{t}"
95
+ run "if [ -d #{release_path} ]; then cd #{release_path}; else cd #{current_path}; fi; if [ -f Rakefile ]; then #{rake} RAILS_ENV=#{rails_env} #{t}; fi;"
96
96
  end
97
97
  end
98
98
  end
@@ -1,11 +1,11 @@
1
1
  module ThinkingSphinx
2
2
  class Facet
3
3
  attr_reader :property, :value_source
4
-
4
+
5
5
  def initialize(property, value_source = nil)
6
6
  @property = property
7
7
  @value_source = value_source
8
-
8
+
9
9
  if property.columns.length != 1
10
10
  raise "Can't translate Facets on multiple-column field or attribute"
11
11
  end
@@ -20,11 +20,11 @@ module ThinkingSphinx
20
20
  facet.to_s.gsub(/(_facet|_crc)$/,'').to_sym
21
21
  end
22
22
  end
23
-
23
+
24
24
  def self.attribute_name_for(name)
25
25
  name.to_s == 'class' ? 'class_crc' : "#{name}_facet"
26
26
  end
27
-
27
+
28
28
  def self.attribute_name_from_value(name, value)
29
29
  case value
30
30
  when String
@@ -39,10 +39,10 @@ module ThinkingSphinx
39
39
  name
40
40
  end
41
41
  end
42
-
42
+
43
43
  def self.translate?(property)
44
44
  return true if property.is_a?(Field)
45
-
45
+
46
46
  case property.type
47
47
  when :string
48
48
  true
@@ -52,11 +52,11 @@ module ThinkingSphinx
52
52
  !property.all_ints?
53
53
  end
54
54
  end
55
-
55
+
56
56
  def name
57
57
  property.unique_name
58
58
  end
59
-
59
+
60
60
  def attribute_name
61
61
  if translate?
62
62
  Facet.attribute_name_for(@property.unique_name)
@@ -64,23 +64,23 @@ module ThinkingSphinx
64
64
  @property.unique_name.to_s
65
65
  end
66
66
  end
67
-
67
+
68
68
  def translate?
69
69
  Facet.translate?(@property)
70
70
  end
71
-
71
+
72
72
  def type
73
73
  @property.is_a?(Field) ? :string : @property.type
74
74
  end
75
-
75
+
76
76
  def float?
77
77
  @property.type == :float
78
78
  end
79
-
79
+
80
80
  def value(object, attribute_hash)
81
81
  attribute_value = attribute_hash['@groupby']
82
82
  return translate(object, attribute_value) if translate? || float?
83
-
83
+
84
84
  case @property.type
85
85
  when :datetime
86
86
  Time.at(attribute_value)
@@ -90,13 +90,13 @@ module ThinkingSphinx
90
90
  attribute_value
91
91
  end
92
92
  end
93
-
93
+
94
94
  def to_s
95
95
  name
96
96
  end
97
-
97
+
98
98
  private
99
-
99
+
100
100
  def translate(object, attribute_value)
101
101
  objects = source_objects(object)
102
102
  return if objects.blank?
@@ -107,20 +107,20 @@ module ThinkingSphinx
107
107
  result && result.to_crc32 == attribute_value
108
108
  }
109
109
 
110
- object.try(method)
110
+ object ? object.send(method) : nil
111
111
  end
112
-
112
+
113
113
  def source_objects(object)
114
114
  column.__stack.each { |method|
115
115
  object = Array(object).collect { |item|
116
116
  item.send(method)
117
117
  }.flatten.compact
118
-
118
+
119
119
  return nil if object.empty?
120
120
  }
121
121
  Array(object)
122
122
  end
123
-
123
+
124
124
  def column
125
125
  @property.columns.first
126
126
  end
@@ -3,7 +3,7 @@ require 'thinking_sphinx/index/faux_column'
3
3
 
4
4
  module ThinkingSphinx
5
5
  class Index
6
- attr_accessor :name, :model, :sources, :delta_object
6
+ attr_accessor :name, :model, :sources, :delta_object, :additional_indices
7
7
 
8
8
  # Create a new index instance by passing in the model it is tied to, and
9
9
  # a block to build it with (optional but recommended). For documentation
@@ -25,6 +25,7 @@ module ThinkingSphinx
25
25
  @sources = []
26
26
  @options = {}
27
27
  @delta_object = nil
28
+ @additional_indices = []
28
29
  end
29
30
 
30
31
  def fields
@@ -132,6 +133,7 @@ module ThinkingSphinx
132
133
  def to_riddle_for_distributed
133
134
  index = Riddle::Configuration::DistributedIndex.new name
134
135
  index.local_indices << core_name
136
+ index.local_indices += additional_indices
135
137
  index.local_indices.unshift delta_name if delta?
136
138
  index
137
139
  end
@@ -52,6 +52,11 @@ module ThinkingSphinx
52
52
  self.instance_eval &block
53
53
  end
54
54
 
55
+ def use_local_indices(*indexes)
56
+ @index.additional_indices += indexes.map {|index_name| "#{index_name.to_s}_core"}
57
+ end
58
+ alias_method :use_local_index, :use_local_indices
59
+
55
60
  # This is how you add fields - the strings Sphinx looks at - to your
56
61
  # index. Technically, to use this method, you need to pass in some
57
62
  # columns and options - but there's some neat method_missing stuff
@@ -1,7 +1,7 @@
1
1
  module ThinkingSphinx
2
2
  class Property
3
3
  attr_accessor :alias, :columns, :associations, :model, :faceted, :admin
4
-
4
+
5
5
  def initialize(source, columns, options = {})
6
6
  @source = source
7
7
  @model = source.model
@@ -9,27 +9,27 @@ module ThinkingSphinx
9
9
  @associations = {}
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
13
  @alias = options[:as]
14
14
  @faceted = options[:facet]
15
15
  @admin = options[:admin]
16
16
  @sortable = options[:sortable] || false
17
17
  @value_source = options[:value]
18
-
18
+
19
19
  @alias = @alias.to_sym unless @alias.blank?
20
-
20
+
21
21
  @columns.each { |col|
22
- @associations[col] = association_stack(col.__stack.clone).each { |assoc|
22
+ @associations[col.__stack] = association_stack(col.__stack.clone).each { |assoc|
23
23
  assoc.join_to(source.base)
24
24
  }
25
25
  }
26
26
  end
27
-
27
+
28
28
  # Returns the unique name of the attribute - which is either the alias of
29
29
  # the attribute, or the name of the only column - if there is only one. If
30
30
  # there isn't, there should be an alias. Else things probably won't work.
31
31
  # Consider yourself warned.
32
- #
32
+ #
33
33
  def unique_name
34
34
  if @columns.length == 1
35
35
  @alias || @columns.first.__name
@@ -37,19 +37,19 @@ module ThinkingSphinx
37
37
  @alias
38
38
  end
39
39
  end
40
-
40
+
41
41
  def to_facet
42
42
  return nil unless @faceted
43
-
43
+
44
44
  ThinkingSphinx::Facet.new(self, @value_source)
45
45
  end
46
-
46
+
47
47
  # Get the part of the GROUP BY clause related to this attribute - if one is
48
48
  # needed. If not, all you'll get back is nil. The latter will happen if
49
49
  # there isn't actually a real column to get data from, or if there's
50
50
  # multiple data values (read: a has_many or has_and_belongs_to_many
51
51
  # association).
52
- #
52
+ #
53
53
  def to_group_sql
54
54
  case
55
55
  when is_many?, is_string?, ThinkingSphinx.use_group_by_shortcut?
@@ -60,126 +60,128 @@ module ThinkingSphinx
60
60
  }
61
61
  end
62
62
  end
63
-
63
+
64
64
  def changed?(instance)
65
65
  return true if is_string? || @columns.any? { |col| !col.__stack.empty? }
66
-
66
+
67
67
  !@columns.all? { |col|
68
68
  instance.respond_to?("#{col.__name.to_s}_changed?") &&
69
69
  !instance.send("#{col.__name.to_s}_changed?")
70
70
  }
71
71
  end
72
-
72
+
73
73
  def admin?
74
74
  admin
75
75
  end
76
-
76
+
77
77
  def public?
78
78
  !admin
79
79
  end
80
-
80
+
81
81
  def available?
82
82
  columns.any? { |column| column_available?(column) }
83
83
  end
84
-
84
+
85
85
  private
86
-
86
+
87
87
  # Could there be more than one value related to the parent record? If so,
88
88
  # then this will return true. If not, false. It's that simple.
89
- #
89
+ #
90
90
  def is_many?
91
91
  associations.values.flatten.any? { |assoc| assoc.is_many? }
92
92
  end
93
-
93
+
94
94
  # Returns true if any of the columns are string values, instead of database
95
95
  # column references.
96
96
  def is_string?
97
97
  columns.all? { |col| col.is_string? }
98
98
  end
99
-
99
+
100
100
  def adapter
101
101
  @adapter ||= @model.sphinx_database_adapter
102
102
  end
103
-
103
+
104
104
  def quote_with_table(table, column)
105
105
  "#{quote_table_name(table)}.#{quote_column(column)}"
106
106
  end
107
-
107
+
108
108
  def quote_column(column)
109
109
  @model.connection.quote_column_name(column)
110
110
  end
111
-
111
+
112
112
  def quote_table_name(table_name)
113
113
  @model.connection.quote_table_name(table_name)
114
114
  end
115
-
115
+
116
116
  # Indication of whether the columns should be concatenated with a space
117
117
  # between each value. True if there's either multiple sources or multiple
118
118
  # associations.
119
- #
119
+ #
120
120
  def concat_ws?
121
121
  multiple_associations? || @columns.length > 1
122
122
  end
123
-
123
+
124
124
  # Checks whether any column requires multiple associations (which only
125
125
  # happens for polymorphic situations).
126
- #
126
+ #
127
127
  def multiple_associations?
128
- associations.any? { |col,assocs| assocs.length > 1 }
128
+ associations.values.any? { |assocs| assocs.length > 1 }
129
129
  end
130
-
130
+
131
131
  # Builds a column reference tied to the appropriate associations. This
132
132
  # dives into the associations hash and their corresponding joins to
133
133
  # figure out how to correctly reference a column in SQL.
134
- #
134
+ #
135
135
  def column_with_prefix(column)
136
136
  return nil unless column_available?(column)
137
-
137
+
138
138
  if column.is_string?
139
139
  column.__name
140
140
  elsif column.__stack.empty?
141
141
  "#{@model.quoted_table_name}.#{quote_column(column.__name)}"
142
142
  else
143
- associations[column].collect { |assoc|
143
+ associations[column.__stack].collect { |assoc|
144
144
  assoc.has_column?(column.__name) ?
145
145
  "#{quote_with_table(assoc.join.aliased_table_name, column.__name)}" :
146
146
  nil
147
147
  }.compact
148
148
  end
149
149
  end
150
-
150
+
151
151
  def columns_with_prefixes
152
152
  @columns.collect { |column|
153
153
  column_with_prefix column
154
154
  }.flatten.compact
155
155
  end
156
-
156
+
157
157
  def column_available?(column)
158
158
  if column.is_string?
159
159
  true
160
160
  elsif column.__stack.empty?
161
161
  @model.column_names.include?(column.__name.to_s)
162
162
  else
163
- associations[column].any? { |assoc| assoc.has_column?(column.__name) }
163
+ associations[column.__stack].any? { |assoc|
164
+ assoc.has_column?(column.__name)
165
+ }
164
166
  end
165
167
  end
166
-
168
+
167
169
  # Gets a stack of associations for a specific path.
168
- #
170
+ #
169
171
  def association_stack(path, parent = nil)
170
172
  assocs = []
171
-
173
+
172
174
  if parent.nil?
173
175
  assocs = @source.association(path.shift)
174
176
  else
175
177
  assocs = parent.children(path.shift)
176
178
  end
177
-
179
+
178
180
  until path.empty?
179
181
  point = path.shift
180
182
  assocs = assocs.collect { |assoc| assoc.children(point) }.flatten
181
183
  end
182
-
184
+
183
185
  assocs
184
186
  end
185
187
  end