pixeltrix-thinking-sphinx 1.1.5 → 1.2.1

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 (76) hide show
  1. data/README.textile +147 -0
  2. data/lib/thinking_sphinx/active_record/attribute_updates.rb +48 -0
  3. data/lib/thinking_sphinx/active_record/delta.rb +14 -1
  4. data/lib/thinking_sphinx/active_record/scopes.rb +37 -0
  5. data/lib/thinking_sphinx/active_record.rb +46 -12
  6. data/lib/thinking_sphinx/adapters/abstract_adapter.rb +9 -1
  7. data/lib/thinking_sphinx/adapters/mysql_adapter.rb +3 -2
  8. data/lib/thinking_sphinx/adapters/postgresql_adapter.rb +12 -5
  9. data/lib/thinking_sphinx/association.rb +20 -0
  10. data/lib/thinking_sphinx/attribute.rb +187 -116
  11. data/lib/thinking_sphinx/class_facet.rb +15 -0
  12. data/lib/thinking_sphinx/configuration.rb +46 -14
  13. data/lib/thinking_sphinx/core/string.rb +3 -10
  14. data/lib/thinking_sphinx/deltas/datetime_delta.rb +3 -3
  15. data/lib/thinking_sphinx/deltas/default_delta.rb +9 -6
  16. data/lib/thinking_sphinx/deltas/delayed_delta/delta_job.rb +1 -1
  17. data/lib/thinking_sphinx/deltas/delayed_delta.rb +4 -2
  18. data/lib/thinking_sphinx/deltas.rb +14 -6
  19. data/lib/thinking_sphinx/deploy/capistrano.rb +98 -0
  20. data/lib/thinking_sphinx/excerpter.rb +22 -0
  21. data/lib/thinking_sphinx/facet.rb +68 -18
  22. data/lib/thinking_sphinx/facet_search.rb +134 -0
  23. data/lib/thinking_sphinx/field.rb +7 -97
  24. data/lib/thinking_sphinx/index/builder.rb +255 -201
  25. data/lib/thinking_sphinx/index.rb +28 -343
  26. data/lib/thinking_sphinx/property.rb +160 -0
  27. data/lib/thinking_sphinx/rails_additions.rb +7 -4
  28. data/lib/thinking_sphinx/search.rb +593 -587
  29. data/lib/thinking_sphinx/search_methods.rb +421 -0
  30. data/lib/thinking_sphinx/source/internal_properties.rb +46 -0
  31. data/lib/thinking_sphinx/source/sql.rb +128 -0
  32. data/lib/thinking_sphinx/source.rb +150 -0
  33. data/lib/thinking_sphinx/tasks.rb +45 -11
  34. data/lib/thinking_sphinx.rb +88 -14
  35. data/rails/init.rb +14 -0
  36. data/spec/{unit → lib}/thinking_sphinx/active_record/delta_spec.rb +7 -7
  37. data/spec/{unit → lib}/thinking_sphinx/active_record/has_many_association_spec.rb +0 -0
  38. data/spec/lib/thinking_sphinx/active_record/scopes_spec.rb +92 -0
  39. data/spec/{unit → lib}/thinking_sphinx/active_record_spec.rb +115 -42
  40. data/spec/{unit → lib}/thinking_sphinx/association_spec.rb +4 -5
  41. data/spec/lib/thinking_sphinx/attribute_spec.rb +465 -0
  42. data/spec/{unit → lib}/thinking_sphinx/configuration_spec.rb +118 -7
  43. data/spec/{unit → lib}/thinking_sphinx/core/string_spec.rb +0 -0
  44. data/spec/lib/thinking_sphinx/excerpter_spec.rb +49 -0
  45. data/spec/lib/thinking_sphinx/facet_search_spec.rb +176 -0
  46. data/spec/lib/thinking_sphinx/facet_spec.rb +302 -0
  47. data/spec/{unit → lib}/thinking_sphinx/field_spec.rb +26 -17
  48. data/spec/lib/thinking_sphinx/index/builder_spec.rb +355 -0
  49. data/spec/{unit → lib}/thinking_sphinx/index/faux_column_spec.rb +0 -0
  50. data/spec/{unit → lib}/thinking_sphinx/index_spec.rb +3 -12
  51. data/spec/lib/thinking_sphinx/rails_additions_spec.rb +191 -0
  52. data/spec/lib/thinking_sphinx/search_methods_spec.rb +152 -0
  53. data/spec/lib/thinking_sphinx/search_spec.rb +887 -0
  54. data/spec/lib/thinking_sphinx/source_spec.rb +217 -0
  55. data/spec/{unit → lib}/thinking_sphinx_spec.rb +30 -8
  56. data/tasks/distribution.rb +20 -1
  57. data/tasks/testing.rb +7 -15
  58. data/vendor/after_commit/init.rb +3 -0
  59. data/vendor/after_commit/lib/after_commit/active_record.rb +27 -4
  60. data/vendor/after_commit/lib/after_commit/connection_adapters.rb +1 -1
  61. data/vendor/after_commit/lib/after_commit.rb +4 -1
  62. data/vendor/riddle/lib/riddle/client/message.rb +4 -3
  63. data/vendor/riddle/lib/riddle/client.rb +3 -0
  64. data/vendor/riddle/lib/riddle/configuration/section.rb +8 -2
  65. data/vendor/riddle/lib/riddle/controller.rb +1 -1
  66. data/vendor/riddle/lib/riddle.rb +1 -1
  67. metadata +75 -39
  68. data/README +0 -107
  69. data/lib/thinking_sphinx/active_record/search.rb +0 -57
  70. data/lib/thinking_sphinx/collection.rb +0 -142
  71. data/lib/thinking_sphinx/facet_collection.rb +0 -44
  72. data/spec/unit/thinking_sphinx/active_record/search_spec.rb +0 -107
  73. data/spec/unit/thinking_sphinx/attribute_spec.rb +0 -212
  74. data/spec/unit/thinking_sphinx/collection_spec.rb +0 -14
  75. data/spec/unit/thinking_sphinx/index/builder_spec.rb +0 -5
  76. data/spec/unit/thinking_sphinx/search_spec.rb +0 -59
@@ -4,18 +4,26 @@ require 'thinking_sphinx/deltas/datetime_delta'
4
4
 
5
5
  module ThinkingSphinx
6
6
  module Deltas
7
- def self.parse(index, options)
8
- case options.delete(:delta)
7
+ def self.parse(index)
8
+ delta_option = index.local_options.delete(:delta)
9
+ case delta_option
9
10
  when TrueClass, :default
10
- DefaultDelta.new index, options
11
+ DefaultDelta.new index, index.local_options
11
12
  when :delayed
12
- DelayedDelta.new index, options
13
+ DelayedDelta.new index, index.local_options
13
14
  when :datetime
14
- DatetimeDelta.new index, options
15
+ DatetimeDelta.new index, index.local_options
15
16
  when FalseClass, nil
16
17
  nil
17
18
  else
18
- raise "Unknown delta type"
19
+ if delta_option.is_a?(String)
20
+ delta_option = Kernel.const_get(delta_option)
21
+ end
22
+ if delta_option.ancestors.include?(ThinkingSphinx::Deltas::DefaultDelta)
23
+ delta_option.new index, index.local_options
24
+ else
25
+ raise "Unknown delta type"
26
+ end
19
27
  end
20
28
  end
21
29
  end
@@ -0,0 +1,98 @@
1
+ Capistrano::Configuration.instance(:must_exist).load do
2
+ namespace :thinking_sphinx do
3
+ namespace :install do
4
+ desc <<-DESC
5
+ Install Sphinx by source
6
+
7
+ If Postgres is available, Sphinx will use it.
8
+
9
+ If the variable :thinking_sphinx_configure_args is set, it will
10
+ be passed to the Sphinx configure script. You can use this to
11
+ install Sphinx in a non-standard location:
12
+
13
+ set :thinking_sphinx_configure_args, "--prefix=$HOME/software"
14
+ DESC
15
+
16
+ task :sphinx do
17
+ with_postgres = false
18
+ begin
19
+ run "which pg_config" do |channel, stream, data|
20
+ with_postgres = !(data.nil? || data == "")
21
+ end
22
+ rescue Capistrano::CommandError => e
23
+ puts "Continuing despite error: #{e.message}"
24
+ end
25
+
26
+ args = []
27
+ if with_postgres
28
+ run "pg_config --pkgincludedir" do |channel, stream, data|
29
+ args << "--with-pgsql=#{data}"
30
+ end
31
+ end
32
+ args << fetch(:thinking_sphinx_configure_args, '')
33
+
34
+ commands = <<-CMD
35
+ wget -q http://www.sphinxsearch.com/downloads/sphinx-0.9.8.1.tar.gz >> sphinx.log
36
+ tar xzvf sphinx-0.9.8.1.tar.gz
37
+ cd sphinx-0.9.8.1
38
+ ./configure #{args.join(" ")}
39
+ make
40
+ #{try_sudo} make install
41
+ rm -rf sphinx-0.9.8.1 sphinx-0.9.8.1.tar.gz
42
+ CMD
43
+ run commands.split(/\n\s+/).join(" && ")
44
+ end
45
+
46
+ desc "Install Thinking Sphinx as a gem from GitHub"
47
+ task :ts do
48
+ run "#{try_sudo} gem install freelancing-god-thinking-sphinx --source http://gems.github.com"
49
+ end
50
+ end
51
+
52
+ desc "Generate the Sphinx configuration file"
53
+ task :configure do
54
+ rake "thinking_sphinx:configure"
55
+ end
56
+
57
+ desc "Index data"
58
+ task :index do
59
+ rake "thinking_sphinx:index"
60
+ end
61
+
62
+ desc "Start the Sphinx daemon"
63
+ task :start do
64
+ configure
65
+ rake "thinking_sphinx:start"
66
+ end
67
+
68
+ desc "Stop the Sphinx daemon"
69
+ task :stop do
70
+ configure
71
+ rake "thinking_sphinx:stop"
72
+ end
73
+
74
+ desc "Stop and then start the Sphinx daemon"
75
+ task :restart do
76
+ stop
77
+ start
78
+ end
79
+
80
+ desc "Stop, re-index and then start the Sphinx daemon"
81
+ task :rebuild do
82
+ stop
83
+ index
84
+ start
85
+ end
86
+
87
+ desc "Add the shared folder for sphinx files for the production environment"
88
+ task :shared_sphinx_folder, :roles => :web do
89
+ run "mkdir -p #{shared_path}/db/sphinx/production"
90
+ end
91
+
92
+ def rake(*tasks)
93
+ tasks.each do |t|
94
+ run "cd #{current_path} && rake #{t} RAILS_ENV=production"
95
+ end
96
+ end
97
+ end
98
+ end
@@ -0,0 +1,22 @@
1
+ module ThinkingSphinx
2
+ class Excerpter
3
+ CoreMethods = %w( kind_of? object_id respond_to? should should_not stub! )
4
+ # Hide most methods, to allow them to be passed through to the instance.
5
+ instance_methods.select { |method|
6
+ method.to_s[/^__/].nil? && !CoreMethods.include?(method.to_s)
7
+ }.each { |method|
8
+ undef_method method
9
+ }
10
+
11
+ def initialize(search, instance)
12
+ @search = search
13
+ @instance = instance
14
+ end
15
+
16
+ def method_missing(method, *args, &block)
17
+ string = @instance.send(method, *args, &block).to_s
18
+
19
+ @search.excerpt_for(string, @instance.class)
20
+ end
21
+ end
22
+ 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
- @reference.unique_name.to_s + "_sort"
59
+ if translate?
60
+ Facet.attribute_name_for(@property.unique_name)
61
+ else
62
+ @property.unique_name.to_s
23
63
  end
24
64
  end
25
65
 
66
+ def translate?
67
+ Facet.translate?(@property)
68
+ end
69
+
70
+ def type
71
+ @property.is_a?(Field) ? :string : @property.type
72
+ end
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, :multi
31
- translate(object, attribute_value)
77
+ case @property.type
32
78
  when :datetime
33
79
  Time.at(attribute_value)
34
80
  when :boolean
@@ -46,13 +92,17 @@ module ThinkingSphinx
46
92
 
47
93
  def translate(object, attribute_value)
48
94
  column.__stack.each { |method|
49
- object = object.send(method)
95
+ return nil unless 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
@@ -0,0 +1,134 @@
1
+ module ThinkingSphinx
2
+ class FacetSearch < Hash
3
+ attr_accessor :args, :options
4
+
5
+ def initialize(*args)
6
+ @options = args.extract_options!
7
+ @args = args
8
+
9
+ set_default_options
10
+
11
+ populate
12
+ end
13
+
14
+ def for(hash = {})
15
+ for_options = {:with => {}}.merge(options)
16
+
17
+ hash.each do |key, value|
18
+ attrib = ThinkingSphinx::Facet.attribute_name_from_value(key, value)
19
+ for_options[:with][attrib] = underlying_value key, value
20
+ end
21
+
22
+ ThinkingSphinx.search *(args + [for_options])
23
+ end
24
+
25
+ def facet_names
26
+ @facet_names ||= begin
27
+ names = options[:all_facets] ?
28
+ facet_names_for_all_classes : facet_names_common_to_all_classes
29
+
30
+ names.delete "class_crc" unless options[:class_facet]
31
+ names
32
+ end
33
+ end
34
+
35
+ private
36
+
37
+ def set_default_options
38
+ options[:all_facets] ||= false
39
+ if options[:class_facet].nil?
40
+ options[:class_facet] = ((options[:classes] || []).length != 1)
41
+ end
42
+ end
43
+
44
+ def populate
45
+ facet_names.each do |name|
46
+ search_options = facet_search_options.merge(:group_by => name)
47
+ add_from_results name, ThinkingSphinx.search(
48
+ *(args + [search_options])
49
+ )
50
+ end
51
+ end
52
+
53
+ def facet_search_options
54
+ config = ThinkingSphinx::Configuration.instance
55
+ max = config.configuration.searchd.max_matches || 1000
56
+
57
+ options.merge(
58
+ :group_function => :attr,
59
+ :limit => max,
60
+ :max_matches => max,
61
+ :page => 1
62
+ )
63
+ end
64
+
65
+ def facet_classes
66
+ (
67
+ options[:classes] || ThinkingSphinx.indexed_models.collect { |model|
68
+ model.constantize
69
+ }
70
+ ).select { |klass| klass.sphinx_facets.any? }
71
+ end
72
+
73
+ def all_facets
74
+ facet_classes.collect { |klass|
75
+ klass.sphinx_facets
76
+ }.flatten.select { |facet|
77
+ options[:facets].blank? || Array(options[:facets]).include?(facet.name)
78
+ }
79
+ end
80
+
81
+ def facet_names_for_all_classes
82
+ all_facets.group_by { |facet|
83
+ facet.name
84
+ }.collect { |name, facets|
85
+ if facets.collect { |facet| facet.type }.uniq.length > 1
86
+ raise "Facet #{name} exists in more than one model with different types"
87
+ end
88
+ facets.first.attribute_name
89
+ }
90
+ end
91
+
92
+ def facet_names_common_to_all_classes
93
+ facet_names_for_all_classes.select { |name|
94
+ facet_classes.all? { |klass|
95
+ klass.sphinx_facets.detect { |facet|
96
+ facet.attribute_name == name
97
+ }
98
+ }
99
+ }
100
+ end
101
+
102
+ def add_from_results(facet, results)
103
+ name = ThinkingSphinx::Facet.name_for(facet)
104
+
105
+ self[name] ||= {}
106
+
107
+ return if results.empty?
108
+
109
+ facet = facet_from_object(results.first, facet) if facet.is_a?(String)
110
+
111
+ results.each_with_groupby_and_count { |result, group, count|
112
+ facet_value = facet.value(result, group)
113
+
114
+ self[name][facet_value] ||= 0
115
+ self[name][facet_value] += count
116
+ }
117
+ end
118
+
119
+ def underlying_value(key, value)
120
+ case value
121
+ when Array
122
+ value.collect { |item| underlying_value(key, item) }
123
+ when String
124
+ value.to_crc32
125
+ else
126
+ value
127
+ end
128
+ end
129
+
130
+ def facet_from_object(object, name)
131
+ object.sphinx_facets.detect { |facet| facet.attribute_name == name }
132
+ end
133
+ end
134
+ 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