dpickett-thinking-sphinx 1.1.12 → 1.1.23

Sign up to get free protection for your applications and to get access to all the features.
Files changed (44) hide show
  1. data/README.textile +19 -0
  2. data/lib/thinking_sphinx.rb +36 -2
  3. data/lib/thinking_sphinx/active_record.rb +18 -3
  4. data/lib/thinking_sphinx/adapters/postgresql_adapter.rb +9 -3
  5. data/lib/thinking_sphinx/association.rb +4 -1
  6. data/lib/thinking_sphinx/attribute.rb +85 -43
  7. data/lib/thinking_sphinx/configuration.rb +33 -12
  8. data/lib/thinking_sphinx/deltas.rb +9 -6
  9. data/lib/thinking_sphinx/deltas/datetime_delta.rb +3 -3
  10. data/lib/thinking_sphinx/deltas/default_delta.rb +4 -4
  11. data/lib/thinking_sphinx/deltas/delayed_delta/delta_job.rb +1 -1
  12. data/lib/thinking_sphinx/deploy/capistrano.rb +82 -64
  13. data/lib/thinking_sphinx/facet.rb +58 -21
  14. data/lib/thinking_sphinx/facet_collection.rb +12 -13
  15. data/lib/thinking_sphinx/field.rb +3 -1
  16. data/lib/thinking_sphinx/index.rb +28 -353
  17. data/lib/thinking_sphinx/index/builder.rb +255 -232
  18. data/lib/thinking_sphinx/property.rb +29 -2
  19. data/lib/thinking_sphinx/search.rb +32 -96
  20. data/lib/thinking_sphinx/search/facets.rb +104 -0
  21. data/lib/thinking_sphinx/source.rb +150 -0
  22. data/lib/thinking_sphinx/source/internal_properties.rb +46 -0
  23. data/lib/thinking_sphinx/source/sql.rb +128 -0
  24. data/lib/thinking_sphinx/tasks.rb +42 -8
  25. data/rails/init.rb +14 -0
  26. data/spec/unit/thinking_sphinx/active_record/delta_spec.rb +5 -5
  27. data/spec/unit/thinking_sphinx/active_record/search_spec.rb +4 -4
  28. data/spec/unit/thinking_sphinx/active_record_spec.rb +52 -39
  29. data/spec/unit/thinking_sphinx/association_spec.rb +4 -5
  30. data/spec/unit/thinking_sphinx/attribute_spec.rb +209 -19
  31. data/spec/unit/thinking_sphinx/collection_spec.rb +7 -6
  32. data/spec/unit/thinking_sphinx/configuration_spec.rb +93 -7
  33. data/spec/unit/thinking_sphinx/facet_spec.rb +256 -0
  34. data/spec/unit/thinking_sphinx/field_spec.rb +26 -17
  35. data/spec/unit/thinking_sphinx/index/builder_spec.rb +351 -1
  36. data/spec/unit/thinking_sphinx/index_spec.rb +3 -102
  37. data/spec/unit/thinking_sphinx/rails_additions_spec.rb +13 -5
  38. data/spec/unit/thinking_sphinx/search_spec.rb +154 -29
  39. data/spec/unit/thinking_sphinx/source_spec.rb +217 -0
  40. data/spec/unit/thinking_sphinx_spec.rb +22 -4
  41. data/tasks/distribution.rb +19 -0
  42. data/vendor/riddle/lib/riddle.rb +1 -1
  43. data/vendor/riddle/lib/riddle/configuration/section.rb +7 -1
  44. metadata +26 -3
@@ -2,15 +2,23 @@ module ThinkingSphinx
2
2
  class Property
3
3
  attr_accessor :alias, :columns, :associations, :model, :faceted, :admin
4
4
 
5
- def initialize(columns, options = {})
5
+ def initialize(source, columns, options = {})
6
+ @source = source
7
+ @model = source.model
6
8
  @columns = Array(columns)
7
9
  @associations = {}
8
10
 
9
- 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) }
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) }
10
12
 
11
13
  @alias = options[:as]
12
14
  @faceted = options[:facet]
13
15
  @admin = options[:admin]
16
+
17
+ @columns.each { |col|
18
+ @associations[col] = association_stack(col.__stack.clone).each { |assoc|
19
+ assoc.join_to(source.base)
20
+ }
21
+ }
14
22
  end
15
23
 
16
24
  # Returns the unique name of the attribute - which is either the alias of
@@ -129,5 +137,24 @@ module ThinkingSphinx
129
137
  }.compact.join(', ')
130
138
  end
131
139
  end
140
+
141
+ # Gets a stack of associations for a specific path.
142
+ #
143
+ def association_stack(path, parent = nil)
144
+ assocs = []
145
+
146
+ if parent.nil?
147
+ assocs = @source.association(path.shift)
148
+ else
149
+ assocs = parent.children(path.shift)
150
+ end
151
+
152
+ until path.empty?
153
+ point = path.shift
154
+ assocs = assocs.collect { |assoc| assoc.children(point) }.flatten
155
+ end
156
+
157
+ assocs
158
+ end
132
159
  end
133
160
  end
@@ -1,3 +1,5 @@
1
+ require 'thinking_sphinx/search/facets'
2
+
1
3
  module ThinkingSphinx
2
4
  # Once you've got those indexes in and built, this is the stuff that
3
5
  # matters - how to search! This class provides a generic search
@@ -13,6 +15,8 @@ module ThinkingSphinx
13
15
  }
14
16
 
15
17
  class << self
18
+ include ThinkingSphinx::Search::Facets
19
+
16
20
  # Searches for results that match the parameters provided. Will only
17
21
  # return the ids for the matching objects. See #search for syntax
18
22
  # examples.
@@ -218,6 +222,10 @@ module ThinkingSphinx
218
222
  # * <tt>:group_by</tt> determines the field which is used for grouping
219
223
  # * <tt>:group_clause</tt> determines the sorting order
220
224
  #
225
+ # As a convenience, you can also use
226
+ # * <tt>:group</tt>
227
+ # which sets :group_by and defaults to :group_function of :attr
228
+ #
221
229
  # === group_function
222
230
  #
223
231
  # Valid values for :group_function are
@@ -354,10 +362,8 @@ module ThinkingSphinx
354
362
 
355
363
  retry_search_on_stale_index(query, options) do
356
364
  results, client = search_results(*(query + [options]))
357
-
358
- ::ActiveRecord::Base.logger.error(
359
- "Sphinx Error: #{results[:error]}"
360
- ) if results[:error]
365
+
366
+ log "Sphinx Error: #{results[:error]}", :error if results[:error]
361
367
 
362
368
  klass = options[:class]
363
369
  page = options[:page] ? options[:page].to_i : 1
@@ -390,9 +396,9 @@ module ThinkingSphinx
390
396
  options[:without_ids] = Array(options[:without_ids]) | e.ids # Actual exclusion
391
397
 
392
398
  tries = stale_retries_left
393
- ::ActiveRecord::Base.logger.debug("Sphinx Stale Ids (%s %s left): %s" % [
394
- tries, (tries==1 ? 'try' : 'tries'), stale_ids.join(', ')
395
- ])
399
+ log "Sphinx Stale Ids (%s %s left): %s" % [
400
+ tries, (tries==1 ? 'try' : 'tries'), stale_ids.join(', ')
401
+ ]
396
402
 
397
403
  retry
398
404
  end
@@ -432,21 +438,6 @@ module ThinkingSphinx
432
438
  end
433
439
  end
434
440
 
435
- # Model.facets *args
436
- # ThinkingSphinx::Search.facets *args
437
- # ThinkingSphinx::Search.facets *args, :all_attributes => true
438
- # ThinkingSphinx::Search.facets *args, :class_facet => false
439
- #
440
- def facets(*args)
441
- options = args.extract_options!
442
-
443
- if options[:class]
444
- facets_for_model options[:class], args, options
445
- else
446
- facets_for_all_models args, options
447
- end
448
- end
449
-
450
441
  private
451
442
 
452
443
  # This method handles the common search functionality, and returns both
@@ -474,11 +465,14 @@ module ThinkingSphinx
474
465
  page = options[:page] ? options[:page].to_i : 1
475
466
  page = 1 if page <= 0
476
467
  client.offset = (page - 1) * client.limit
477
-
468
+
478
469
  begin
479
- ::ActiveRecord::Base.logger.debug "Sphinx: #{query}"
480
- results = client.query query
481
- ::ActiveRecord::Base.logger.debug "Sphinx Result: #{results[:matches].collect{|m| m[:attributes]["sphinx_internal_id"]}.inspect}"
470
+ log "Sphinx: #{query}"
471
+ results = client.query(query, '*', options[:comment] || '')
472
+ log "Sphinx Result:"
473
+ log results[:matches].collect { |m|
474
+ m[:attributes]["sphinx_internal_id"]
475
+ }.inspect
482
476
  rescue Errno::ECONNREFUSED => err
483
477
  raise ThinkingSphinx::ConnectionError, "Connection to Sphinx Daemon (searchd) failed."
484
478
  end
@@ -518,6 +512,12 @@ module ThinkingSphinx
518
512
  end
519
513
  end
520
514
 
515
+ # Group by defaults using :group
516
+ if options[:group]
517
+ options[:group_by] = options[:group].to_s
518
+ options[:group_function] ||= :attr
519
+ end
520
+
521
521
  [
522
522
  :max_matches, :match_mode, :sort_mode, :sort_by, :id_range,
523
523
  :group_by, :group_function, :group_clause, :group_distinct, :cut_off,
@@ -642,11 +642,11 @@ module ThinkingSphinx
642
642
  }.flatten : []
643
643
 
644
644
  lat_attr = klass ? klass.sphinx_indexes.collect { |index|
645
- index.options[:latitude_attr]
645
+ index.local_options[:latitude_attr]
646
646
  }.compact.first : nil
647
647
 
648
648
  lon_attr = klass ? klass.sphinx_indexes.collect { |index|
649
- index.options[:longitude_attr]
649
+ index.local_options[:longitude_attr]
650
650
  }.compact.first : nil
651
651
 
652
652
  lat_attr = options[:latitude_attr] if options[:latitude_attr]
@@ -695,7 +695,7 @@ module ThinkingSphinx
695
695
  client.sort_by = order.to_s
696
696
  end
697
697
  when String
698
- client.sort_mode = :extended
698
+ client.sort_mode = :extended unless options[:sort_mode]
699
699
  client.sort_by = sorted_fields_to_attributes(order, fields)
700
700
  else
701
701
  # do nothing
@@ -718,73 +718,9 @@ module ThinkingSphinx
718
718
  string
719
719
  end
720
720
 
721
- def facets_for_model(klass, args, options)
722
- hash = ThinkingSphinx::FacetCollection.new args + [options]
723
- options = options.clone.merge! facet_query_options
724
-
725
- klass.sphinx_facets.inject(hash) do |hash, facet|
726
- unless facet.name == :class && !options[:class_facet]
727
- options[:group_by] = facet.attribute_name
728
- hash.add_from_results facet, search(*(args + [options]))
729
- end
730
-
731
- hash
732
- end
733
- end
734
-
735
- def facets_for_all_models(args, options)
736
- options = GlobalFacetOptions.merge(options)
737
- hash = ThinkingSphinx::FacetCollection.new args + [options]
738
- options = options.merge! facet_query_options
739
-
740
- facet_names(options).inject(hash) do |hash, name|
741
- options[:group_by] = name
742
- hash.add_from_results name, search(*(args + [options]))
743
- hash
744
- end
745
- end
746
-
747
- def facet_query_options
748
- config = ThinkingSphinx::Configuration.instance
749
- max = config.configuration.searchd.max_matches || 1000
750
-
751
- {
752
- :group_function => :attr,
753
- :limit => max,
754
- :max_matches => max
755
- }
756
- end
757
-
758
- def facet_classes(options)
759
- options[:classes] || ThinkingSphinx.indexed_models.collect { |model|
760
- model.constantize
761
- }
762
- end
763
-
764
- def facet_names(options)
765
- classes = facet_classes(options)
766
- names = options[:all_attributes] ?
767
- facet_names_for_all_classes(classes) :
768
- facet_names_common_to_all_classes(classes)
769
-
770
- names.delete "class_crc" unless options[:class_facet]
771
- names
772
- end
773
-
774
- def facet_names_for_all_classes(classes)
775
- classes.collect { |klass|
776
- klass.sphinx_facets.collect { |facet| facet.attribute_name }
777
- }.flatten.uniq
778
- end
779
-
780
- def facet_names_common_to_all_classes(classes)
781
- facet_names_for_all_classes(classes).select { |name|
782
- classes.all? { |klass|
783
- klass.sphinx_facets.detect { |facet|
784
- facet.attribute_name == name
785
- }
786
- }
787
- }
721
+ def log(message, method = :debug)
722
+ return if ::ActiveRecord::Base.logger.nil?
723
+ ::ActiveRecord::Base.logger.send method, message
788
724
  end
789
725
  end
790
726
  end
@@ -0,0 +1,104 @@
1
+ module ThinkingSphinx
2
+ class Search
3
+ module Facets
4
+ # Model.facets *args
5
+ # ThinkingSphinx::Search.facets *args
6
+ # ThinkingSphinx::Search.facets *args, :all_attributes => true
7
+ # ThinkingSphinx::Search.facets *args, :class_facet => false
8
+ #
9
+ def facets(*args)
10
+ options = args.extract_options!
11
+
12
+ if options[:class]
13
+ facets_for_model options[:class], args, options
14
+ else
15
+ facets_for_all_models args, options
16
+ end
17
+ end
18
+
19
+ private
20
+
21
+ def facets_for_model(klass, args, options)
22
+ hash = ThinkingSphinx::FacetCollection.new args + [options]
23
+ options = options.clone.merge! facet_query_options
24
+
25
+ facets = klass.sphinx_facets
26
+ facets = Array(options.delete(:facets)).collect { |name|
27
+ klass.sphinx_facets.detect { |facet| facet.name.to_s == name.to_s }
28
+ }.compact if options[:facets]
29
+
30
+ facets.inject(hash) do |hash, facet|
31
+ unless facet.name == :class && !options[:class_facet]
32
+ options[:group_by] = facet.attribute_name
33
+ hash.add_from_results facet, search(*(args + [options]))
34
+ end
35
+
36
+ hash
37
+ end
38
+ end
39
+
40
+ def facets_for_all_models(args, options)
41
+ options = GlobalFacetOptions.merge(options)
42
+ hash = ThinkingSphinx::FacetCollection.new args + [options]
43
+ options = options.merge! facet_query_options
44
+
45
+ facet_names(options).inject(hash) do |hash, name|
46
+ options[:group_by] = name
47
+ hash.add_from_results name, search(*(args + [options]))
48
+ hash
49
+ end
50
+ end
51
+
52
+ def facet_query_options
53
+ config = ThinkingSphinx::Configuration.instance
54
+ max = config.configuration.searchd.max_matches || 1000
55
+
56
+ {
57
+ :group_function => :attr,
58
+ :limit => max,
59
+ :max_matches => max,
60
+ :page => 1
61
+ }
62
+ end
63
+
64
+ def facet_classes(options)
65
+ options[:classes] || ThinkingSphinx.indexed_models.collect { |model|
66
+ model.constantize
67
+ }
68
+ end
69
+
70
+ def facet_names(options)
71
+ classes = facet_classes(options)
72
+ names = options[:all_attributes] ?
73
+ facet_names_for_all_classes(classes) :
74
+ facet_names_common_to_all_classes(classes)
75
+
76
+ names.delete "class_crc" unless options[:class_facet]
77
+ names
78
+ end
79
+
80
+ def facet_names_for_all_classes(classes)
81
+ all_facets = classes.collect { |klass| klass.sphinx_facets }.flatten
82
+
83
+ all_facets.group_by { |facet|
84
+ facet.name
85
+ }.collect { |name, facets|
86
+ if facets.collect { |facet| facet.type }.uniq.length > 1
87
+ raise "Facet #{name} exists in more than one model with different types"
88
+ end
89
+ facets.first.attribute_name
90
+ }
91
+ end
92
+
93
+ def facet_names_common_to_all_classes(classes)
94
+ facet_names_for_all_classes(classes).select { |name|
95
+ classes.all? { |klass|
96
+ klass.sphinx_facets.detect { |facet|
97
+ facet.attribute_name == name
98
+ }
99
+ }
100
+ }
101
+ end
102
+ end
103
+ end
104
+ end
@@ -0,0 +1,150 @@
1
+ require 'thinking_sphinx/source/internal_properties'
2
+ require 'thinking_sphinx/source/sql'
3
+
4
+ module ThinkingSphinx
5
+ class Source
6
+ include ThinkingSphinx::Source::InternalProperties
7
+ include ThinkingSphinx::Source::SQL
8
+
9
+ attr_accessor :model, :fields, :attributes, :conditions, :groupings,
10
+ :options
11
+ attr_reader :base, :index
12
+
13
+ def initialize(index, options = {})
14
+ @index = index
15
+ @model = index.model
16
+ @fields = []
17
+ @attributes = []
18
+ @conditions = []
19
+ @groupings = []
20
+ @options = options
21
+ @associations = {}
22
+
23
+ @base = ::ActiveRecord::Associations::ClassMethods::JoinDependency.new(
24
+ @model, [], nil
25
+ )
26
+
27
+ unless @model.descends_from_active_record?
28
+ stored_class = @model.store_full_sti_class ? @model.name : @model.name.demodulize
29
+ @conditions << "#{@model.quoted_table_name}.#{quote_column(@model.inheritance_column)} = '#{stored_class}'"
30
+ end
31
+
32
+ add_internal_attributes_and_facets
33
+ end
34
+
35
+ def name
36
+ @model.name.underscore.tr(':/\\', '_')
37
+ end
38
+
39
+ def to_riddle_for_core(offset, index)
40
+ source = Riddle::Configuration::SQLSource.new(
41
+ "#{name}_core_#{index}", adapter.sphinx_identifier
42
+ )
43
+
44
+ set_source_database_settings source
45
+ set_source_attributes source, offset
46
+ set_source_sql source, offset
47
+ set_source_settings source
48
+
49
+ source
50
+ end
51
+
52
+ def to_riddle_for_delta(offset, index)
53
+ source = Riddle::Configuration::SQLSource.new(
54
+ "#{name}_delta_#{index}", adapter.sphinx_identifier
55
+ )
56
+ source.parent = "#{name}_core_#{index}"
57
+
58
+ set_source_database_settings source
59
+ set_source_attributes source, offset, true
60
+ set_source_sql source, offset, true
61
+
62
+ source
63
+ end
64
+
65
+ def delta?
66
+ !@index.delta_object.nil?
67
+ end
68
+
69
+ # Gets the association stack for a specific key.
70
+ #
71
+ def association(key)
72
+ @associations[key] ||= Association.children(@model, key)
73
+ end
74
+
75
+ private
76
+
77
+ def adapter
78
+ @adapter ||= @model.sphinx_database_adapter
79
+ end
80
+
81
+ def set_source_database_settings(source)
82
+ config = @model.connection.instance_variable_get(:@config)
83
+
84
+ source.sql_host = config[:host] || "localhost"
85
+ source.sql_user = config[:username] || config[:user] || ""
86
+ source.sql_pass = (config[:password].to_s || "").gsub('#', '\#')
87
+ source.sql_db = config[:database]
88
+ source.sql_port = config[:port]
89
+ source.sql_sock = config[:socket]
90
+ end
91
+
92
+ def set_source_attributes(source, offset, delta = false)
93
+ attributes.each do |attrib|
94
+ source.send(attrib.type_to_config) << attrib.config_value(offset, delta)
95
+ end
96
+ end
97
+
98
+ def set_source_sql(source, offset, delta = false)
99
+ source.sql_query = to_sql(:offset => offset, :delta => delta).gsub(/\n/, ' ')
100
+ source.sql_query_range = to_sql_query_range(:delta => delta)
101
+ source.sql_query_info = to_sql_query_info(offset)
102
+
103
+ source.sql_query_pre += send(!delta ? :sql_query_pre_for_core : :sql_query_pre_for_delta)
104
+
105
+ if @index.local_options[:group_concat_max_len]
106
+ source.sql_query_pre << "SET SESSION group_concat_max_len = #{@index.local_options[:group_concat_max_len]}"
107
+ end
108
+
109
+ source.sql_query_pre += [adapter.utf8_query_pre].compact if utf8?
110
+ end
111
+
112
+ def set_source_settings(source)
113
+ config = ThinkingSphinx::Configuration.instance
114
+ config.source_options.each do |key, value|
115
+ source.send("#{key}=".to_sym, value)
116
+ end
117
+
118
+ source_options = ThinkingSphinx::Configuration::SourceOptions
119
+ @options.each do |key, value|
120
+ if source_options.include?(key.to_s) && !value.nil?
121
+ source.send("#{key}=".to_sym, value)
122
+ end
123
+ end
124
+ end
125
+
126
+ # Returns all associations used amongst all the fields and attributes.
127
+ # This includes all associations between the model and what the actual
128
+ # columns are from.
129
+ #
130
+ def all_associations
131
+ @all_associations ||= (
132
+ # field associations
133
+ @fields.collect { |field|
134
+ field.associations.values
135
+ }.flatten +
136
+ # attribute associations
137
+ @attributes.collect { |attrib|
138
+ attrib.associations.values if attrib.include_as_association?
139
+ }.compact.flatten
140
+ ).uniq.collect { |assoc|
141
+ # get ancestors as well as column-level associations
142
+ assoc.ancestors
143
+ }.flatten.uniq
144
+ end
145
+
146
+ def utf8?
147
+ @index.options[:charset_type] == "utf-8"
148
+ end
149
+ end
150
+ end