Module: ThinkingSphinx::ActiveRecord

Defined in:
lib/thinking_sphinx/active_record.rb,
lib/thinking_sphinx/active_record/delta.rb,
lib/thinking_sphinx/active_record/scopes.rb,
lib/thinking_sphinx/active_record/attribute_updates.rb,
lib/thinking_sphinx/active_record/has_many_association.rb

Overview

Core additions to ActiveRecord models - define_index for creating indexes for models. If you want to interrogate the index objects created for the model, you can use the class-level accessor :sphinx_indexes.

Defined Under Namespace

Modules: AttributeUpdates, Delta, HasManyAssociation, Scopes

Class Method Summary collapse

Instance Method Summary collapse

Class Method Details

.included(base) ⇒ Object



12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
# File 'lib/thinking_sphinx/active_record.rb', line 12

def self.included(base)
  base.class_eval do
    class_inheritable_array :sphinx_indexes, :sphinx_facets
    class << self
      
      def set_sphinx_primary_key(attribute)
        @sphinx_primary_key_attribute = attribute
      end
      
      def primary_key_for_sphinx
        @sphinx_primary_key_attribute || primary_key
      end
      
      # Allows creation of indexes for Sphinx. If you don't do this, there
      # isn't much point trying to search (or using this plugin at all,
      # really).
      #
      # An example or two:
      #
      #   define_index
      #     indexes :id, :as => :model_id
      #     indexes name
      #   end
      #
      # You can also grab fields from associations - multiple levels deep
      # if necessary.
      #
      #   define_index do
      #     indexes tags.name, :as => :tag
      #     indexes articles.content
      #     indexes orders.line_items.product.name, :as => :product
      #   end
      #
      # And it will automatically concatenate multiple fields:
      #
      #   define_index do
      #     indexes [author.first_name, author.last_name], :as => :author
      #   end
      #
      # The #indexes method is for fields - if you want attributes, use
      # #has instead. All the same rules apply - but keep in mind that
      # attributes are for sorting, grouping and filtering, not searching.
      #
      #   define_index do
      #     # fields ...
      #     
      #     has created_at, updated_at
      #   end
      #
      # One last feature is the delta index. This requires the model to
      # have a boolean field named 'delta', and is enabled as follows:
      #
      #   define_index do
      #     # fields ...
      #     # attributes ...
      #     
      #     set_property :delta => true
      #   end
      #
      # Check out the more detailed documentation for each of these methods
      # at ThinkingSphinx::Index::Builder.
      # 
      def define_index(&block)
        return unless ThinkingSphinx.define_indexes?
        
        self.sphinx_indexes ||= []
        index = ThinkingSphinx::Index::Builder.generate(self, &block)
        
        self.sphinx_indexes << index
        unless ThinkingSphinx.indexed_models.include?(self.name)
          ThinkingSphinx.indexed_models << self.name
        end
        
        if index.delta?
          before_save   :toggle_delta
          after_commit  :index_delta
        end
        
        after_destroy :toggle_deleted
        
        include ThinkingSphinx::SearchMethods
        include ThinkingSphinx::ActiveRecord::AttributeUpdates
        include ThinkingSphinx::ActiveRecord::Scopes
        
        index
        
        # We want to make sure that if the database doesn't exist, then Thinking
        # Sphinx doesn't mind when running non-TS tasks (like db:create, db:drop
        # and db:migrate). It's a bit hacky, but I can't think of a better way.
      rescue StandardError => err
        case err.class.name
        when "Mysql::Error", "Java::JavaSql::SQLException", "ActiveRecord::StatementInvalid"
          return
        else
          raise err
        end
      end
      alias_method :sphinx_index, :define_index
      
      def sphinx_index_options
        sphinx_indexes.last.options
      end
      
      # Generate a unique CRC value for the model's name, to use to
      # determine which Sphinx documents belong to which AR records.
      # 
      # Really only written for internal use - but hey, if it's useful to
      # you in some other way, awesome.
      # 
      def to_crc32
        self.name.to_crc32
      end
      
      def to_crc32s
        (subclasses << self).collect { |klass| klass.to_crc32 }
      end
      
      def source_of_sphinx_index
        possible_models = self.sphinx_indexes.collect { |index| index.model }
        return self if possible_models.include?(self)

        parent = self.superclass
        while !possible_models.include?(parent) && parent != ::ActiveRecord::Base
          parent = parent.superclass
        end

        return parent
      end
      
      def to_riddle(offset)
        sphinx_database_adapter.setup
        
        indexes = [to_riddle_for_core(offset)]
        indexes << to_riddle_for_delta(offset) if sphinx_delta?
        indexes << to_riddle_for_distributed
      end
      
      def sphinx_database_adapter
        @sphinx_database_adapter ||=
          ThinkingSphinx::AbstractAdapter.detect(self)
      end
      
      def sphinx_name
        self.name.underscore.tr(':/\\', '_')
      end
      
      def sphinx_index_names
        klass = source_of_sphinx_index
        names = ["#{klass.sphinx_name}_core"]
        names << "#{klass.sphinx_name}_delta" if sphinx_delta?
        
        names
      end
      
      private
      
      def sphinx_delta?
        self.sphinx_indexes.any? { |index| index.delta? }
      end
      
      def to_riddle_for_core(offset)
        index = Riddle::Configuration::Index.new("#{sphinx_name}_core")
        index.path = File.join(
          ThinkingSphinx::Configuration.instance.searchd_file_path, index.name
        )
        
        set_configuration_options_for_indexes index
        set_field_settings_for_indexes        index
        
        self.sphinx_indexes.select { |ts_index|
          ts_index.model == self
        }.each_with_index do |ts_index, i|
          index.sources += ts_index.sources.collect { |source|
            source.to_riddle_for_core(offset, i)
          }
        end
        
        index
      end
      
      def to_riddle_for_delta(offset)
        index = Riddle::Configuration::Index.new("#{sphinx_name}_delta")
        index.parent = "#{sphinx_name}_core"
        index.path = File.join(ThinkingSphinx::Configuration.instance.searchd_file_path, index.name)
        
        self.sphinx_indexes.each_with_index do |ts_index, i|
          index.sources += ts_index.sources.collect { |source|
            source.to_riddle_for_delta(offset, i)
          } if ts_index.delta?
        end
        
        index
      end
      
      def to_riddle_for_distributed
        index = Riddle::Configuration::DistributedIndex.new(sphinx_name)
        index.local_indexes << "#{sphinx_name}_core"
        index.local_indexes.unshift "#{sphinx_name}_delta" if sphinx_delta?
        index
      end
      
      def set_configuration_options_for_indexes(index)
        ThinkingSphinx::Configuration.instance.index_options.each do |key, value|
          index.send("#{key}=".to_sym, value)
        end
        
        self.sphinx_indexes.each do |ts_index|
          ts_index.options.each do |key, value|
            index.send("#{key}=".to_sym, value) if ThinkingSphinx::Configuration::IndexOptions.include?(key.to_s) && !value.nil?
          end
        end
      end
      
      def set_field_settings_for_indexes(index)
        field_names = lambda { |field| field.unique_name.to_s }
        
        self.sphinx_indexes.each do |ts_index|
          index.prefix_field_names += ts_index.prefix_fields.collect(&field_names)
          index.infix_field_names  += ts_index.infix_fields.collect(&field_names)
        end
      end
    end
  end
  
  base.send(:include, ThinkingSphinx::ActiveRecord::Delta)
  
  ::ActiveRecord::Associations::HasManyAssociation.send(
    :include, ThinkingSphinx::ActiveRecord::HasManyAssociation
  )
  ::ActiveRecord::Associations::HasManyThroughAssociation.send(
    :include, ThinkingSphinx::ActiveRecord::HasManyAssociation
  )
end

Instance Method Details

#in_both_indexes?Boolean

Returns:

  • (Boolean)


258
259
260
# File 'lib/thinking_sphinx/active_record.rb', line 258

def in_both_indexes?
  in_core_index? && in_delta_index?
end

#in_core_index?Boolean

Returns:

  • (Boolean)


250
251
252
# File 'lib/thinking_sphinx/active_record.rb', line 250

def in_core_index?
  in_index? "core"
end

#in_delta_index?Boolean

Returns:

  • (Boolean)


254
255
256
# File 'lib/thinking_sphinx/active_record.rb', line 254

def in_delta_index?
  in_index? "delta"
end

#in_index?(suffix) ⇒ Boolean

Returns:

  • (Boolean)


246
247
248
# File 'lib/thinking_sphinx/active_record.rb', line 246

def in_index?(suffix)
  self.class.search_for_id self.sphinx_document_id, sphinx_index_name(suffix)
end

#primary_key_for_sphinxInteger

Returns the unique integer id for the object. This method uses the attribute hash to get around ActiveRecord always mapping the #id method to whatever the real primary key is (which may be a unique string hash).

Returns:

  • (Integer)

    Unique record id for the purposes of Sphinx.



291
292
293
# File 'lib/thinking_sphinx/active_record.rb', line 291

def primary_key_for_sphinx
  @primary_key_for_sphinx ||= read_attribute(self.class.primary_key_for_sphinx)
end

#sphinx_document_idObject



295
296
297
298
# File 'lib/thinking_sphinx/active_record.rb', line 295

def sphinx_document_id
  primary_key_for_sphinx * ThinkingSphinx.indexed_models.size +
    ThinkingSphinx.indexed_models.index(self.class.source_of_sphinx_index.name)
end

#toggle_deletedObject



262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
# File 'lib/thinking_sphinx/active_record.rb', line 262

def toggle_deleted
  return unless ThinkingSphinx.updates_enabled? && ThinkingSphinx.sphinx_running?
  
  config = ThinkingSphinx::Configuration.instance
  client = Riddle::Client.new config.address, config.port
  
  client.update(
    "#{self.class.sphinx_indexes.first.name}_core",
    ['sphinx_deleted'],
    {self.sphinx_document_id => 1}
  ) if self.in_core_index?
  
  client.update(
    "#{self.class.sphinx_indexes.first.name}_delta",
    ['sphinx_deleted'],
    {self.sphinx_document_id => 1}
  ) if ThinkingSphinx.deltas_enabled? &&
    self.class.sphinx_indexes.any? { |index| index.delta? } &&
    self.toggled_delta?
rescue ::ThinkingSphinx::ConnectionError
  # nothing
end