DrMark-thinking-sphinx 1.1.6 → 1.1.14

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 (40) hide show
  1. data/{README → README.textile} +84 -84
  2. data/lib/thinking_sphinx/active_record/attribute_updates.rb +48 -0
  3. data/lib/thinking_sphinx/active_record/delta.rb +10 -1
  4. data/lib/thinking_sphinx/active_record.rb +10 -3
  5. data/lib/thinking_sphinx/adapters/postgresql_adapter.rb +1 -1
  6. data/lib/thinking_sphinx/attribute.rb +44 -134
  7. data/lib/thinking_sphinx/class_facet.rb +15 -0
  8. data/lib/thinking_sphinx/collection.rb +1 -0
  9. data/lib/thinking_sphinx/configuration.rb +7 -3
  10. data/lib/thinking_sphinx/deltas/datetime_delta.rb +1 -1
  11. data/lib/thinking_sphinx/deltas/default_delta.rb +3 -2
  12. data/lib/thinking_sphinx/deltas/delayed_delta.rb +4 -2
  13. data/lib/thinking_sphinx/deltas.rb +9 -6
  14. data/lib/thinking_sphinx/deploy/capistrano.rb +82 -0
  15. data/lib/thinking_sphinx/facet.rb +68 -18
  16. data/lib/thinking_sphinx/facet_collection.rb +16 -17
  17. data/lib/thinking_sphinx/field.rb +7 -97
  18. data/lib/thinking_sphinx/index/builder.rb +255 -232
  19. data/lib/thinking_sphinx/index.rb +37 -349
  20. data/lib/thinking_sphinx/property.rb +160 -0
  21. data/lib/thinking_sphinx/search/facets.rb +98 -0
  22. data/lib/thinking_sphinx/search.rb +4 -73
  23. data/lib/thinking_sphinx/source/internal_properties.rb +46 -0
  24. data/lib/thinking_sphinx/source/sql.rb +124 -0
  25. data/lib/thinking_sphinx/source.rb +150 -0
  26. data/lib/thinking_sphinx/tasks.rb +1 -1
  27. data/lib/thinking_sphinx.rb +3 -1
  28. data/spec/unit/thinking_sphinx/active_record_spec.rb +14 -12
  29. data/spec/unit/thinking_sphinx/attribute_spec.rb +16 -11
  30. data/spec/unit/thinking_sphinx/facet_collection_spec.rb +64 -0
  31. data/spec/unit/thinking_sphinx/facet_spec.rb +278 -0
  32. data/spec/unit/thinking_sphinx/field_spec.rb +18 -9
  33. data/spec/unit/thinking_sphinx/index/builder_spec.rb +347 -1
  34. data/spec/unit/thinking_sphinx/index_spec.rb +22 -27
  35. data/spec/unit/thinking_sphinx/rails_additions_spec.rb +183 -0
  36. data/spec/unit/thinking_sphinx/search_spec.rb +71 -0
  37. data/spec/unit/thinking_sphinx/source_spec.rb +156 -0
  38. data/tasks/distribution.rb +1 -1
  39. data/tasks/testing.rb +7 -15
  40. metadata +19 -3
@@ -4,20 +4,23 @@ require 'thinking_sphinx/deltas/datetime_delta'
4
4
 
5
5
  module ThinkingSphinx
6
6
  module Deltas
7
- def self.parse(index, options)
8
- delta_option = options.delete(:delta)
7
+ def self.parse(index)
8
+ delta_option = index.local_options.delete(:delta)
9
9
  case delta_option
10
10
  when TrueClass, :default
11
- DefaultDelta.new index, options
11
+ DefaultDelta.new index, index.local_options
12
12
  when :delayed
13
- DelayedDelta.new index, options
13
+ DelayedDelta.new index, index.local_options
14
14
  when :datetime
15
- DatetimeDelta.new index, options
15
+ DatetimeDelta.new index, index.local_options
16
16
  when FalseClass, nil
17
17
  nil
18
18
  else
19
+ if delta_option.is_a?(String)
20
+ delta_option = Kernel.const_get(delta_option)
21
+ end
19
22
  if delta_option.ancestors.include?(ThinkingSphinx::Deltas::DefaultDelta)
20
- delta_option.new index, options
23
+ delta_option.new index, index.local_options
21
24
  else
22
25
  raise "Unknown delta type"
23
26
  end
@@ -0,0 +1,82 @@
1
+ Capistrano::Configuration.instance(:must_exist).load do
2
+ namespace :thinking_sphinx do
3
+ namespace :install do
4
+ desc "Install Sphinx by source"
5
+ task :sphinx do
6
+ with_postgres = false
7
+ run "which pg_config" do |channel, stream, data|
8
+ with_postgres = !(data.nil? || data == "")
9
+ end
10
+
11
+ args = []
12
+ if with_postgres
13
+ run "pg_config --pkgincludedir" do |channel, stream, data|
14
+ args << "--with-pgsql=#{data}"
15
+ end
16
+ end
17
+
18
+ commands = <<-CMD
19
+ wget -q http://www.sphinxsearch.com/downloads/sphinx-0.9.8.1.tar.gz >> sphinx.log
20
+ tar xzvf sphinx-0.9.8.1.tar.gz
21
+ cd sphinx-0.9.8.1
22
+ ./configure #{args.join(" ")}
23
+ make
24
+ sudo make install
25
+ rm -rf sphinx-0.9.8.1 sphinx-0.9.8.1.tar.gz
26
+ CMD
27
+ run commands.split(/\n\s+/).join(" && ")
28
+ end
29
+
30
+ desc "Install Thinking Sphinx as a gem from GitHub"
31
+ task :ts do
32
+ sudo "gem install freelancing-god-thinking-sphinx --source http://gems.github.com"
33
+ end
34
+ end
35
+
36
+ desc "Generate the Sphinx configuration file"
37
+ task :configure do
38
+ rake "thinking_sphinx:configure"
39
+ end
40
+
41
+ desc "Index data"
42
+ task :index do
43
+ rake "thinking_sphinx:index"
44
+ end
45
+
46
+ desc "Start the Sphinx daemon"
47
+ task :start do
48
+ configure
49
+ rake "thinking_sphinx:start"
50
+ end
51
+
52
+ desc "Stop the Sphinx daemon"
53
+ task :stop do
54
+ configure
55
+ rake "thinking_sphinx:stop"
56
+ end
57
+
58
+ desc "Stop and then start the Sphinx daemon"
59
+ task :restart do
60
+ stop
61
+ start
62
+ end
63
+
64
+ desc "Stop, re-index and then start the Sphinx daemon"
65
+ task :rebuild do
66
+ stop
67
+ index
68
+ start
69
+ end
70
+
71
+ desc "Add the shared folder for sphinx files for the production environment"
72
+ task :shared_sphinx_folder, :roles => :web do
73
+ sudo "mkdir -p #{shared_path}/db/sphinx/production"
74
+ end
75
+
76
+ def rake(*tasks)
77
+ tasks.each do |t|
78
+ run "cd #{current_path} && rake #{t} RAILS_ENV=production"
79
+ end
80
+ end
81
+ end
82
+ end
@@ -1,34 +1,80 @@
1
1
  module ThinkingSphinx
2
2
  class Facet
3
- attr_reader :reference
3
+ attr_reader :property
4
4
 
5
- def initialize(reference)
6
- @reference = reference
5
+ def initialize(property)
6
+ @property = property
7
7
 
8
- if reference.columns.length != 1
8
+ if property.columns.length != 1
9
9
  raise "Can't translate Facets on multiple-column field or attribute"
10
10
  end
11
11
  end
12
+
13
+ def self.name_for(facet)
14
+ case facet
15
+ when Facet
16
+ facet.name
17
+ when String, Symbol
18
+ facet.to_s.gsub(/(_facet|_crc)$/,'').to_sym
19
+ end
20
+ end
21
+
22
+ def self.attribute_name_for(name)
23
+ name.to_s == 'class' ? 'class_crc' : "#{name}_facet"
24
+ end
25
+
26
+ def self.attribute_name_from_value(name, value)
27
+ case value
28
+ when String
29
+ attribute_name_for(name)
30
+ when Array
31
+ if value.all? { |val| val.is_a?(Integer) }
32
+ name
33
+ else
34
+ attribute_name_for(name)
35
+ end
36
+ else
37
+ name
38
+ end
39
+ end
40
+
41
+ def self.translate?(property)
42
+ return true if property.is_a?(Field)
43
+
44
+ case property.type
45
+ when :string
46
+ true
47
+ when :integer, :boolean, :datetime, :float
48
+ false
49
+ when :multi
50
+ !property.all_ints?
51
+ end
52
+ end
12
53
 
13
54
  def name
14
- reference.unique_name
55
+ property.unique_name
15
56
  end
16
57
 
17
58
  def attribute_name
18
- # @attribute_name ||= case @reference
19
- # when Attribute
20
- # @reference.unique_name.to_s
21
- # when Field
22
- @attribute_name ||= @reference.unique_name.to_s + "_facet"
23
- # end
59
+ if translate?
60
+ Facet.attribute_name_for(@property.unique_name)
61
+ else
62
+ @property.unique_name.to_s
63
+ end
64
+ end
65
+
66
+ def translate?
67
+ Facet.translate?(@property)
68
+ end
69
+
70
+ def type
71
+ @property.is_a?(Field) ? :string : @property.type
24
72
  end
25
73
 
26
74
  def value(object, attribute_value)
27
- return translate(object, attribute_value) if @reference.is_a?(Field)
75
+ return translate(object, attribute_value) if translate?
28
76
 
29
- case @reference.type
30
- when :string
31
- translate(object, attribute_value)
77
+ case @property.type
32
78
  when :datetime
33
79
  Time.at(attribute_value)
34
80
  when :boolean
@@ -48,11 +94,15 @@ module ThinkingSphinx
48
94
  column.__stack.each { |method|
49
95
  object = object.send(method)
50
96
  }
51
- object.send(column.__name)
97
+ if object.is_a?(Array)
98
+ object.collect { |item| item.send(column.__name) }
99
+ else
100
+ object.send(column.__name)
101
+ end
52
102
  end
53
103
 
54
104
  def column
55
- @reference.columns.first
105
+ @property.columns.first
56
106
  end
57
107
  end
58
- end
108
+ end
@@ -3,24 +3,25 @@ module ThinkingSphinx
3
3
  attr_accessor :arguments
4
4
 
5
5
  def initialize(arguments)
6
- @arguments = arguments.clone
7
- @attribute_values = {}
8
- @facets = []
6
+ @arguments = arguments.clone
7
+ @facet_names = []
9
8
  end
10
9
 
11
10
  def add_from_results(facet, results)
12
- facet = facet_from_object(results.first, facet) if facet.is_a?(String)
11
+ name = ThinkingSphinx::Facet.name_for(facet)
12
+
13
+ self[name] ||= {}
14
+ @facet_names << name
15
+
16
+ return if results.empty?
13
17
 
14
- self[facet.name] ||= {}
15
- @attribute_values[facet.name] ||= {}
16
- @facets << facet
18
+ facet = facet_from_object(results.first, facet) if facet.is_a?(String)
17
19
 
18
20
  results.each_with_groupby_and_count { |result, group, count|
19
21
  facet_value = facet.value(result, group)
20
22
 
21
- self[facet.name][facet_value] ||= 0
22
- self[facet.name][facet_value] += count
23
- @attribute_values[facet.name][facet_value] = group
23
+ self[name][facet_value] ||= 0
24
+ self[name][facet_value] += count
24
25
  }
25
26
  end
26
27
 
@@ -30,7 +31,7 @@ module ThinkingSphinx
30
31
  options[:with] ||= {}
31
32
 
32
33
  hash.each do |key, value|
33
- attrib = facet_for_key(key).attribute_name
34
+ attrib = ThinkingSphinx::Facet.attribute_name_from_value(key, value)
34
35
  options[:with][attrib] = underlying_value key, value
35
36
  end
36
37
 
@@ -44,17 +45,15 @@ module ThinkingSphinx
44
45
  case value
45
46
  when Array
46
47
  value.collect { |item| underlying_value(key, item) }
48
+ when String
49
+ value.to_crc32
47
50
  else
48
- @attribute_values[key][value]
51
+ value
49
52
  end
50
53
  end
51
54
 
52
- def facet_for_key(key)
53
- @facets.detect { |facet| facet.name == key }
54
- end
55
-
56
55
  def facet_from_object(object, name)
57
56
  object.sphinx_facets.detect { |facet| facet.attribute_name == name }
58
57
  end
59
58
  end
60
- end
59
+ end
@@ -7,9 +7,8 @@ module ThinkingSphinx
7
7
  # generate SQL statements, you'll need to set the base model, and all the
8
8
  # associations. Which can get messy. Use Index.link!, it really helps.
9
9
  #
10
- class Field
11
- attr_accessor :alias, :columns, :sortable, :associations, :model, :infixes,
12
- :prefixes, :faceted
10
+ class Field < ThinkingSphinx::Property
11
+ attr_accessor :sortable, :infixes, :prefixes
13
12
 
14
13
  # To create a new field, you'll need to pass in either a single Column
15
14
  # or an array of them, and some (optional) options. The columns are
@@ -53,17 +52,14 @@ module ThinkingSphinx
53
52
  # :as => :posts, :prefixes => true
54
53
  # )
55
54
  #
56
- def initialize(columns, options = {})
57
- @columns = Array(columns)
58
- @associations = {}
59
-
60
- raise "Cannot define a field 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) }
55
+ def initialize(source, columns, options = {})
56
+ super
61
57
 
62
- @alias = options[:as]
63
58
  @sortable = options[:sortable] || false
64
59
  @infixes = options[:infixes] || false
65
60
  @prefixes = options[:prefixes] || false
66
- @faceted = options[:facet] || false
61
+
62
+ source.fields << self
67
63
  end
68
64
 
69
65
  # Get the part of the SELECT clause related to this field. Don't forget
@@ -77,96 +73,10 @@ module ThinkingSphinx
77
73
  column_with_prefix(column)
78
74
  }.join(', ')
79
75
 
80
- clause = adapter.concatenate(clause) if concat_ws?
76
+ clause = adapter.concatenate(clause) if concat_ws?
81
77
  clause = adapter.group_concatenate(clause) if is_many?
82
78
 
83
79
  "#{adapter.cast_to_string clause } AS #{quote_column(unique_name)}"
84
80
  end
85
-
86
- # Get the part of the GROUP BY clause related to this field - if one is
87
- # needed. If not, all you'll get back is nil. The latter will happen if
88
- # there's multiple data values (read: a has_many or has_and_belongs_to_many
89
- # association).
90
- #
91
- def to_group_sql
92
- case
93
- when is_many?, ThinkingSphinx.use_group_by_shortcut?
94
- nil
95
- else
96
- @columns.collect { |column|
97
- column_with_prefix(column)
98
- }
99
- end
100
- end
101
-
102
- # Returns the unique name of the field - which is either the alias of
103
- # the field, or the name of the only column - if there is only one. If
104
- # there isn't, there should be an alias. Else things probably won't work.
105
- # Consider yourself warned.
106
- #
107
- def unique_name
108
- if @columns.length == 1
109
- @alias || @columns.first.__name
110
- else
111
- @alias
112
- end
113
- end
114
-
115
- def to_facet
116
- return nil unless @faceted
117
-
118
- ThinkingSphinx::Facet.new(self)
119
- end
120
-
121
- private
122
-
123
- def adapter
124
- @adapter ||= @model.sphinx_database_adapter
125
- end
126
-
127
- def quote_column(column)
128
- @model.connection.quote_column_name(column)
129
- end
130
-
131
- # Indication of whether the columns should be concatenated with a space
132
- # between each value. True if there's either multiple sources or multiple
133
- # associations.
134
- #
135
- def concat_ws?
136
- @columns.length > 1 || multiple_associations?
137
- end
138
-
139
- # Checks whether any column requires multiple associations (which only
140
- # happens for polymorphic situations).
141
- #
142
- def multiple_associations?
143
- associations.any? { |col,assocs| assocs.length > 1 }
144
- end
145
-
146
- # Builds a column reference tied to the appropriate associations. This
147
- # dives into the associations hash and their corresponding joins to
148
- # figure out how to correctly reference a column in SQL.
149
- #
150
- def column_with_prefix(column)
151
- if column.is_string?
152
- column.__name
153
- elsif associations[column].empty?
154
- "#{@model.quoted_table_name}.#{quote_column(column.__name)}"
155
- else
156
- associations[column].collect { |assoc|
157
- assoc.has_column?(column.__name) ?
158
- "#{@model.connection.quote_table_name(assoc.join.aliased_table_name)}" +
159
- ".#{quote_column(column.__name)}" :
160
- nil
161
- }.compact.join(', ')
162
- end
163
- end
164
-
165
- # Could there be more than one value related to the parent record? If so,
166
- # then this will return true. If not, false. It's that simple.
167
- #
168
- def is_many?
169
- associations.values.flatten.any? { |assoc| assoc.is_many? }
170
- end
171
81
  end
172
82
  end