skalee-thinking-sphinx 1.3.14.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.
- data/LICENCE +20 -0
- data/README.textile +201 -0
- data/Rakefile +3 -0
- data/VERSION +1 -0
- data/contribute.rb +385 -0
- data/cucumber.yml +1 -0
- data/features/abstract_inheritance.feature +10 -0
- data/features/alternate_primary_key.feature +27 -0
- data/features/attribute_transformation.feature +22 -0
- data/features/attribute_updates.feature +51 -0
- data/features/deleting_instances.feature +67 -0
- data/features/direct_attributes.feature +11 -0
- data/features/excerpts.feature +13 -0
- data/features/extensible_delta_indexing.feature +9 -0
- data/features/facets.feature +82 -0
- data/features/facets_across_model.feature +29 -0
- data/features/handling_edits.feature +92 -0
- data/features/retry_stale_indexes.feature +24 -0
- data/features/searching_across_models.feature +20 -0
- data/features/searching_by_index.feature +40 -0
- data/features/searching_by_model.feature +175 -0
- data/features/searching_with_find_arguments.feature +56 -0
- data/features/sphinx_detection.feature +25 -0
- data/features/sphinx_scopes.feature +42 -0
- data/features/step_definitions/alpha_steps.rb +16 -0
- data/features/step_definitions/beta_steps.rb +7 -0
- data/features/step_definitions/common_steps.rb +188 -0
- data/features/step_definitions/extensible_delta_indexing_steps.rb +7 -0
- data/features/step_definitions/facet_steps.rb +96 -0
- data/features/step_definitions/find_arguments_steps.rb +36 -0
- data/features/step_definitions/gamma_steps.rb +15 -0
- data/features/step_definitions/scope_steps.rb +15 -0
- data/features/step_definitions/search_steps.rb +89 -0
- data/features/step_definitions/sphinx_steps.rb +35 -0
- data/features/sti_searching.feature +19 -0
- data/features/support/database.example.yml +3 -0
- data/features/support/db/.gitignore +1 -0
- data/features/support/db/fixtures/alphas.rb +10 -0
- data/features/support/db/fixtures/authors.rb +1 -0
- data/features/support/db/fixtures/betas.rb +10 -0
- data/features/support/db/fixtures/boxes.rb +9 -0
- data/features/support/db/fixtures/categories.rb +1 -0
- data/features/support/db/fixtures/cats.rb +3 -0
- data/features/support/db/fixtures/comments.rb +24 -0
- data/features/support/db/fixtures/developers.rb +29 -0
- data/features/support/db/fixtures/dogs.rb +3 -0
- data/features/support/db/fixtures/extensible_betas.rb +10 -0
- data/features/support/db/fixtures/foxes.rb +3 -0
- data/features/support/db/fixtures/gammas.rb +10 -0
- data/features/support/db/fixtures/music.rb +4 -0
- data/features/support/db/fixtures/people.rb +1001 -0
- data/features/support/db/fixtures/posts.rb +6 -0
- data/features/support/db/fixtures/robots.rb +14 -0
- data/features/support/db/fixtures/tags.rb +27 -0
- data/features/support/db/migrations/create_alphas.rb +8 -0
- data/features/support/db/migrations/create_animals.rb +5 -0
- data/features/support/db/migrations/create_authors.rb +3 -0
- data/features/support/db/migrations/create_authors_posts.rb +6 -0
- data/features/support/db/migrations/create_betas.rb +5 -0
- data/features/support/db/migrations/create_boxes.rb +5 -0
- data/features/support/db/migrations/create_categories.rb +3 -0
- data/features/support/db/migrations/create_comments.rb +10 -0
- data/features/support/db/migrations/create_developers.rb +9 -0
- data/features/support/db/migrations/create_extensible_betas.rb +5 -0
- data/features/support/db/migrations/create_gammas.rb +3 -0
- data/features/support/db/migrations/create_genres.rb +3 -0
- data/features/support/db/migrations/create_music.rb +6 -0
- data/features/support/db/migrations/create_people.rb +13 -0
- data/features/support/db/migrations/create_posts.rb +5 -0
- data/features/support/db/migrations/create_robots.rb +4 -0
- data/features/support/db/migrations/create_taggings.rb +5 -0
- data/features/support/db/migrations/create_tags.rb +4 -0
- data/features/support/env.rb +21 -0
- data/features/support/lib/generic_delta_handler.rb +8 -0
- data/features/support/models/alpha.rb +22 -0
- data/features/support/models/animal.rb +5 -0
- data/features/support/models/author.rb +3 -0
- data/features/support/models/beta.rb +8 -0
- data/features/support/models/box.rb +8 -0
- data/features/support/models/cat.rb +3 -0
- data/features/support/models/category.rb +4 -0
- data/features/support/models/comment.rb +10 -0
- data/features/support/models/developer.rb +16 -0
- data/features/support/models/dog.rb +3 -0
- data/features/support/models/extensible_beta.rb +9 -0
- data/features/support/models/fox.rb +5 -0
- data/features/support/models/gamma.rb +5 -0
- data/features/support/models/genre.rb +3 -0
- data/features/support/models/medium.rb +5 -0
- data/features/support/models/music.rb +8 -0
- data/features/support/models/person.rb +23 -0
- data/features/support/models/post.rb +21 -0
- data/features/support/models/robot.rb +12 -0
- data/features/support/models/tag.rb +3 -0
- data/features/support/models/tagging.rb +4 -0
- data/ginger_scenarios.rb +28 -0
- data/init.rb +5 -0
- data/install.rb +5 -0
- data/lib/cucumber/thinking_sphinx/external_world.rb +8 -0
- data/lib/cucumber/thinking_sphinx/internal_world.rb +126 -0
- data/lib/cucumber/thinking_sphinx/sql_logger.rb +20 -0
- data/lib/thinking_sphinx/active_record/attribute_updates.rb +19 -0
- data/lib/thinking_sphinx/active_record/delta.rb +47 -0
- data/lib/thinking_sphinx/active_record/has_many_association.rb +29 -0
- data/lib/thinking_sphinx/active_record/scopes.rb +75 -0
- data/lib/thinking_sphinx/active_record.rb +348 -0
- data/lib/thinking_sphinx/adapters/abstract_adapter.rb +42 -0
- data/lib/thinking_sphinx/adapters/mysql_adapter.rb +54 -0
- data/lib/thinking_sphinx/adapters/postgresql_adapter.rb +143 -0
- data/lib/thinking_sphinx/association.rb +164 -0
- data/lib/thinking_sphinx/attribute.rb +362 -0
- data/lib/thinking_sphinx/auto_version.rb +22 -0
- data/lib/thinking_sphinx/class_facet.rb +15 -0
- data/lib/thinking_sphinx/configuration.rb +300 -0
- data/lib/thinking_sphinx/context.rb +68 -0
- data/lib/thinking_sphinx/core/array.rb +7 -0
- data/lib/thinking_sphinx/core/string.rb +15 -0
- data/lib/thinking_sphinx/deltas/default_delta.rb +62 -0
- data/lib/thinking_sphinx/deltas.rb +28 -0
- data/lib/thinking_sphinx/deploy/capistrano.rb +100 -0
- data/lib/thinking_sphinx/excerpter.rb +22 -0
- data/lib/thinking_sphinx/facet.rb +125 -0
- data/lib/thinking_sphinx/facet_search.rb +136 -0
- data/lib/thinking_sphinx/field.rb +82 -0
- data/lib/thinking_sphinx/index/builder.rb +296 -0
- data/lib/thinking_sphinx/index/faux_column.rb +110 -0
- data/lib/thinking_sphinx/index.rb +157 -0
- data/lib/thinking_sphinx/property.rb +162 -0
- data/lib/thinking_sphinx/rails_additions.rb +150 -0
- data/lib/thinking_sphinx/search.rb +769 -0
- data/lib/thinking_sphinx/search_methods.rb +439 -0
- data/lib/thinking_sphinx/source/internal_properties.rb +46 -0
- data/lib/thinking_sphinx/source/sql.rb +130 -0
- data/lib/thinking_sphinx/source.rb +153 -0
- data/lib/thinking_sphinx/tasks.rb +131 -0
- data/lib/thinking_sphinx/test.rb +52 -0
- data/lib/thinking_sphinx.rb +225 -0
- data/rails/init.rb +16 -0
- data/recipes/thinking_sphinx.rb +3 -0
- data/spec/fixtures/data.sql +32 -0
- data/spec/fixtures/database.yml.default +3 -0
- data/spec/fixtures/models.rb +145 -0
- data/spec/fixtures/structure.sql +125 -0
- data/spec/spec_helper.rb +60 -0
- data/spec/sphinx_helper.rb +81 -0
- data/spec/thinking_sphinx/active_record/delta_spec.rb +128 -0
- data/spec/thinking_sphinx/active_record/has_many_association_spec.rb +55 -0
- data/spec/thinking_sphinx/active_record/scopes_spec.rb +177 -0
- data/spec/thinking_sphinx/active_record_spec.rb +622 -0
- data/spec/thinking_sphinx/association_spec.rb +239 -0
- data/spec/thinking_sphinx/attribute_spec.rb +570 -0
- data/spec/thinking_sphinx/auto_version_spec.rb +39 -0
- data/spec/thinking_sphinx/configuration_spec.rb +234 -0
- data/spec/thinking_sphinx/context_spec.rb +119 -0
- data/spec/thinking_sphinx/core/array_spec.rb +9 -0
- data/spec/thinking_sphinx/core/string_spec.rb +9 -0
- data/spec/thinking_sphinx/excerpter_spec.rb +57 -0
- data/spec/thinking_sphinx/facet_search_spec.rb +176 -0
- data/spec/thinking_sphinx/facet_spec.rb +333 -0
- data/spec/thinking_sphinx/field_spec.rb +154 -0
- data/spec/thinking_sphinx/index/builder_spec.rb +479 -0
- data/spec/thinking_sphinx/index/faux_column_spec.rb +30 -0
- data/spec/thinking_sphinx/index_spec.rb +183 -0
- data/spec/thinking_sphinx/rails_additions_spec.rb +203 -0
- data/spec/thinking_sphinx/search_methods_spec.rb +152 -0
- data/spec/thinking_sphinx/search_spec.rb +1181 -0
- data/spec/thinking_sphinx/source_spec.rb +235 -0
- data/spec/thinking_sphinx_spec.rb +204 -0
- data/tasks/distribution.rb +41 -0
- data/tasks/rails.rake +1 -0
- data/tasks/testing.rb +72 -0
- data/vendor/after_commit/.gitignore +1 -0
- data/vendor/after_commit/lib/after_commit/active_record.rb +122 -0
- data/vendor/after_commit/lib/after_commit/connection_adapters.rb +168 -0
- data/vendor/after_commit/lib/after_commit/test_bypass.rb +30 -0
- data/vendor/after_commit/lib/after_commit.rb +70 -0
- data/vendor/riddle/lib/riddle/0.9.8.rb +1 -0
- data/vendor/riddle/lib/riddle/0.9.9/client/filter.rb +22 -0
- data/vendor/riddle/lib/riddle/0.9.9/client.rb +49 -0
- data/vendor/riddle/lib/riddle/0.9.9/configuration/searchd.rb +28 -0
- data/vendor/riddle/lib/riddle/0.9.9.rb +7 -0
- data/vendor/riddle/lib/riddle/auto_version.rb +11 -0
- data/vendor/riddle/lib/riddle/client/filter.rb +62 -0
- data/vendor/riddle/lib/riddle/client/message.rb +70 -0
- data/vendor/riddle/lib/riddle/client/response.rb +94 -0
- data/vendor/riddle/lib/riddle/client.rb +745 -0
- data/vendor/riddle/lib/riddle/configuration/distributed_index.rb +49 -0
- data/vendor/riddle/lib/riddle/configuration/index.rb +149 -0
- data/vendor/riddle/lib/riddle/configuration/indexer.rb +20 -0
- data/vendor/riddle/lib/riddle/configuration/remote_index.rb +17 -0
- data/vendor/riddle/lib/riddle/configuration/searchd.rb +28 -0
- data/vendor/riddle/lib/riddle/configuration/section.rb +43 -0
- data/vendor/riddle/lib/riddle/configuration/source.rb +23 -0
- data/vendor/riddle/lib/riddle/configuration/sql_source.rb +53 -0
- data/vendor/riddle/lib/riddle/configuration/xml_source.rb +29 -0
- data/vendor/riddle/lib/riddle/configuration.rb +33 -0
- data/vendor/riddle/lib/riddle/controller.rb +78 -0
- data/vendor/riddle/lib/riddle.rb +51 -0
- metadata +312 -0
@@ -0,0 +1,19 @@
|
|
1
|
+
module ThinkingSphinx
|
2
|
+
module ActiveRecord
|
3
|
+
module AttributeUpdates
|
4
|
+
|
5
|
+
private
|
6
|
+
|
7
|
+
def updatable_attributes(index)
|
8
|
+
index.attributes.select { |attrib| attrib.updatable? }
|
9
|
+
end
|
10
|
+
|
11
|
+
def attribute_values_for_index(index)
|
12
|
+
updatable_attributes(index).inject({}) { |hash, attrib|
|
13
|
+
hash[attrib.unique_name.to_s] = attrib.live_value self
|
14
|
+
hash
|
15
|
+
}
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
@@ -0,0 +1,47 @@
|
|
1
|
+
module ThinkingSphinx
|
2
|
+
module ActiveRecord
|
3
|
+
# This module contains all the delta-related code for models. There isn't
|
4
|
+
# really anything you need to call manually in here - except perhaps
|
5
|
+
# index_delta, but not sure what reason why.
|
6
|
+
#
|
7
|
+
module Delta
|
8
|
+
# Code for after_commit callback is written by Eli Miller:
|
9
|
+
# http://elimiller.blogspot.com/2007/06/proper-cache-expiry-with-aftercommit.html
|
10
|
+
# with slight modification from Joost Hietbrink.
|
11
|
+
#
|
12
|
+
def self.included(base)
|
13
|
+
base.class_eval do
|
14
|
+
class << self
|
15
|
+
def delta_object
|
16
|
+
self.sphinx_indexes.first.delta_object
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
def toggled_delta?
|
21
|
+
self.class.delta_object.toggled(self)
|
22
|
+
end
|
23
|
+
|
24
|
+
private
|
25
|
+
|
26
|
+
# Set the delta value for the model to be true.
|
27
|
+
def toggle_delta
|
28
|
+
self.class.delta_object.toggle(self) if should_toggle_delta?
|
29
|
+
end
|
30
|
+
|
31
|
+
def should_toggle_delta?
|
32
|
+
self.new_record? || indexed_data_changed?
|
33
|
+
end
|
34
|
+
|
35
|
+
def indexed_data_changed?
|
36
|
+
sphinx_indexes.any? { |index|
|
37
|
+
index.fields.any? { |field| field.changed?(self) } ||
|
38
|
+
index.attributes.any? { |attrib|
|
39
|
+
attrib.public? && attrib.changed?(self) && !attrib.updatable?
|
40
|
+
}
|
41
|
+
}
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
@@ -0,0 +1,29 @@
|
|
1
|
+
module ThinkingSphinx
|
2
|
+
module ActiveRecord
|
3
|
+
module HasManyAssociation
|
4
|
+
def search(*args)
|
5
|
+
foreign_key = @reflection.primary_key_name
|
6
|
+
stack = [@reflection.options[:through]].compact
|
7
|
+
|
8
|
+
attribute = nil
|
9
|
+
@reflection.klass.define_indexes
|
10
|
+
(@reflection.klass.sphinx_indexes || []).each do |index|
|
11
|
+
attribute = index.attributes.detect { |attrib|
|
12
|
+
attrib.columns.length == 1 &&
|
13
|
+
attrib.columns.first.__name == foreign_key.to_sym
|
14
|
+
}
|
15
|
+
break if attribute
|
16
|
+
end
|
17
|
+
|
18
|
+
raise "Missing Attribute for Foreign Key #{foreign_key}" unless attribute
|
19
|
+
|
20
|
+
options = args.extract_options!
|
21
|
+
options[:with] ||= {}
|
22
|
+
options[:with][attribute.unique_name] = @owner.id
|
23
|
+
|
24
|
+
args << options
|
25
|
+
@reflection.klass.search(*args)
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
@@ -0,0 +1,75 @@
|
|
1
|
+
module ThinkingSphinx
|
2
|
+
module ActiveRecord
|
3
|
+
module Scopes
|
4
|
+
def self.included(base)
|
5
|
+
base.class_eval do
|
6
|
+
extend ThinkingSphinx::ActiveRecord::Scopes::ClassMethods
|
7
|
+
end
|
8
|
+
end
|
9
|
+
|
10
|
+
module ClassMethods
|
11
|
+
|
12
|
+
# Similar to ActiveRecord's default_scope method Thinking Sphinx supports
|
13
|
+
# a default_sphinx_scope. For example:
|
14
|
+
#
|
15
|
+
# default_sphinx_scope :some_sphinx_named_scope
|
16
|
+
#
|
17
|
+
# The scope is automatically applied when the search method is called. It
|
18
|
+
# will only be applied if it is an existing sphinx_scope.
|
19
|
+
def default_sphinx_scope(sphinx_scope_name)
|
20
|
+
@default_sphinx_scope = sphinx_scope_name
|
21
|
+
end
|
22
|
+
|
23
|
+
# Returns the default_sphinx_scope or nil if none is set.
|
24
|
+
def get_default_sphinx_scope
|
25
|
+
@default_sphinx_scope
|
26
|
+
end
|
27
|
+
|
28
|
+
# Returns true if the current Model has a default_sphinx_scope. Also checks if
|
29
|
+
# the default_sphinx_scope actually is a scope.
|
30
|
+
def has_default_sphinx_scope?
|
31
|
+
!@default_sphinx_scope.nil? && sphinx_scopes.include?(@default_sphinx_scope)
|
32
|
+
end
|
33
|
+
|
34
|
+
# Similar to ActiveRecord's named_scope method Thinking Sphinx supports
|
35
|
+
# scopes. For example:
|
36
|
+
#
|
37
|
+
# sphinx_scope(:latest_first) {
|
38
|
+
# {:order => 'created_at DESC, @relevance DESC'}
|
39
|
+
# }
|
40
|
+
#
|
41
|
+
# Usage:
|
42
|
+
#
|
43
|
+
# @articles = Article.latest_first.search 'pancakes'
|
44
|
+
#
|
45
|
+
def sphinx_scope(method, &block)
|
46
|
+
@sphinx_scopes ||= []
|
47
|
+
@sphinx_scopes << method
|
48
|
+
|
49
|
+
singleton_class.instance_eval do
|
50
|
+
define_method(method) do |*args|
|
51
|
+
options = {:classes => classes_option}
|
52
|
+
options.merge! block.call(*args)
|
53
|
+
|
54
|
+
ThinkingSphinx::Search.new(options)
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
# This returns an Array of all defined scopes. The default
|
60
|
+
# scope shows as :default.
|
61
|
+
def sphinx_scopes
|
62
|
+
@sphinx_scopes || []
|
63
|
+
end
|
64
|
+
|
65
|
+
def remove_sphinx_scopes
|
66
|
+
sphinx_scopes.each do |scope|
|
67
|
+
singleton_class.send(:undef_method, scope)
|
68
|
+
end
|
69
|
+
|
70
|
+
sphinx_scopes.clear
|
71
|
+
end
|
72
|
+
end
|
73
|
+
end
|
74
|
+
end
|
75
|
+
end
|
@@ -0,0 +1,348 @@
|
|
1
|
+
require 'thinking_sphinx/active_record/attribute_updates'
|
2
|
+
require 'thinking_sphinx/active_record/delta'
|
3
|
+
require 'thinking_sphinx/active_record/has_many_association'
|
4
|
+
require 'thinking_sphinx/active_record/scopes'
|
5
|
+
|
6
|
+
module ThinkingSphinx
|
7
|
+
# Core additions to ActiveRecord models - define_index for creating indexes
|
8
|
+
# for models. If you want to interrogate the index objects created for the
|
9
|
+
# model, you can use the class-level accessor :sphinx_indexes.
|
10
|
+
#
|
11
|
+
module ActiveRecord
|
12
|
+
def self.included(base)
|
13
|
+
base.class_eval do
|
14
|
+
class_inheritable_array :sphinx_indexes, :sphinx_facets
|
15
|
+
|
16
|
+
extend ThinkingSphinx::ActiveRecord::ClassMethods
|
17
|
+
|
18
|
+
class << self
|
19
|
+
attr_accessor :sphinx_index_blocks
|
20
|
+
|
21
|
+
def set_sphinx_primary_key(attribute)
|
22
|
+
@sphinx_primary_key_attribute = attribute
|
23
|
+
end
|
24
|
+
|
25
|
+
def primary_key_for_sphinx
|
26
|
+
@sphinx_primary_key_attribute || primary_key
|
27
|
+
end
|
28
|
+
|
29
|
+
def sphinx_index_options
|
30
|
+
sphinx_indexes.last.options
|
31
|
+
end
|
32
|
+
|
33
|
+
# Generate a unique CRC value for the model's name, to use to
|
34
|
+
# determine which Sphinx documents belong to which AR records.
|
35
|
+
#
|
36
|
+
# Really only written for internal use - but hey, if it's useful to
|
37
|
+
# you in some other way, awesome.
|
38
|
+
#
|
39
|
+
def to_crc32
|
40
|
+
self.name.to_crc32
|
41
|
+
end
|
42
|
+
|
43
|
+
def to_crc32s
|
44
|
+
(subclasses << self).collect { |klass| klass.to_crc32 }
|
45
|
+
end
|
46
|
+
|
47
|
+
def sphinx_database_adapter
|
48
|
+
@sphinx_database_adapter ||=
|
49
|
+
ThinkingSphinx::AbstractAdapter.detect(self)
|
50
|
+
end
|
51
|
+
|
52
|
+
def sphinx_name
|
53
|
+
self.name.underscore.tr(':/\\', '_')
|
54
|
+
end
|
55
|
+
|
56
|
+
#
|
57
|
+
# The above method to_crc32s is dependant on the subclasses being loaded consistently
|
58
|
+
# After a reset_subclasses is called (during a Dispatcher.cleanup_application in development)
|
59
|
+
# Our subclasses will be lost but our context will not reload them for us.
|
60
|
+
#
|
61
|
+
# We reset the context which causes the subclasses to be reloaded next time the context is called.
|
62
|
+
#
|
63
|
+
def reset_subclasses_with_thinking_sphinx
|
64
|
+
reset_subclasses_without_thinking_sphinx
|
65
|
+
ThinkingSphinx.reset_context!
|
66
|
+
end
|
67
|
+
|
68
|
+
alias_method_chain :reset_subclasses, :thinking_sphinx
|
69
|
+
|
70
|
+
private
|
71
|
+
|
72
|
+
def defined_indexes?
|
73
|
+
@defined_indexes
|
74
|
+
end
|
75
|
+
|
76
|
+
def defined_indexes=(value)
|
77
|
+
@defined_indexes = value
|
78
|
+
end
|
79
|
+
|
80
|
+
def sphinx_delta?
|
81
|
+
self.sphinx_indexes.any? { |index| index.delta? }
|
82
|
+
end
|
83
|
+
end
|
84
|
+
end
|
85
|
+
|
86
|
+
::ActiveRecord::Associations::HasManyAssociation.send(
|
87
|
+
:include, ThinkingSphinx::ActiveRecord::HasManyAssociation
|
88
|
+
)
|
89
|
+
::ActiveRecord::Associations::HasManyThroughAssociation.send(
|
90
|
+
:include, ThinkingSphinx::ActiveRecord::HasManyAssociation
|
91
|
+
)
|
92
|
+
end
|
93
|
+
|
94
|
+
module ClassMethods
|
95
|
+
# Allows creation of indexes for Sphinx. If you don't do this, there
|
96
|
+
# isn't much point trying to search (or using this plugin at all,
|
97
|
+
# really).
|
98
|
+
#
|
99
|
+
# An example or two:
|
100
|
+
#
|
101
|
+
# define_index
|
102
|
+
# indexes :id, :as => :model_id
|
103
|
+
# indexes name
|
104
|
+
# end
|
105
|
+
#
|
106
|
+
# You can also grab fields from associations - multiple levels deep
|
107
|
+
# if necessary.
|
108
|
+
#
|
109
|
+
# define_index do
|
110
|
+
# indexes tags.name, :as => :tag
|
111
|
+
# indexes articles.content
|
112
|
+
# indexes orders.line_items.product.name, :as => :product
|
113
|
+
# end
|
114
|
+
#
|
115
|
+
# And it will automatically concatenate multiple fields:
|
116
|
+
#
|
117
|
+
# define_index do
|
118
|
+
# indexes [author.first_name, author.last_name], :as => :author
|
119
|
+
# end
|
120
|
+
#
|
121
|
+
# The #indexes method is for fields - if you want attributes, use
|
122
|
+
# #has instead. All the same rules apply - but keep in mind that
|
123
|
+
# attributes are for sorting, grouping and filtering, not searching.
|
124
|
+
#
|
125
|
+
# define_index do
|
126
|
+
# # fields ...
|
127
|
+
#
|
128
|
+
# has created_at, updated_at
|
129
|
+
# end
|
130
|
+
#
|
131
|
+
# One last feature is the delta index. This requires the model to
|
132
|
+
# have a boolean field named 'delta', and is enabled as follows:
|
133
|
+
#
|
134
|
+
# define_index do
|
135
|
+
# # fields ...
|
136
|
+
# # attributes ...
|
137
|
+
#
|
138
|
+
# set_property :delta => true
|
139
|
+
# end
|
140
|
+
#
|
141
|
+
# Check out the more detailed documentation for each of these methods
|
142
|
+
# at ThinkingSphinx::Index::Builder.
|
143
|
+
#
|
144
|
+
def define_index(name = nil, &block)
|
145
|
+
self.sphinx_index_blocks ||= []
|
146
|
+
self.sphinx_indexes ||= []
|
147
|
+
self.sphinx_facets ||= []
|
148
|
+
|
149
|
+
ThinkingSphinx.context.add_indexed_model self
|
150
|
+
|
151
|
+
if sphinx_index_blocks.empty?
|
152
|
+
before_validation :define_indexes
|
153
|
+
before_destroy :define_indexes
|
154
|
+
end
|
155
|
+
|
156
|
+
self.sphinx_index_blocks << lambda {
|
157
|
+
index = ThinkingSphinx::Index::Builder.generate self, name, &block
|
158
|
+
add_sphinx_callbacks_and_extend(index.delta?)
|
159
|
+
add_sphinx_index index
|
160
|
+
}
|
161
|
+
|
162
|
+
end
|
163
|
+
|
164
|
+
def define_indexes
|
165
|
+
superclass.define_indexes unless superclass == ::ActiveRecord::Base
|
166
|
+
|
167
|
+
return if sphinx_index_blocks.nil? ||
|
168
|
+
defined_indexes? ||
|
169
|
+
!ThinkingSphinx.define_indexes?
|
170
|
+
|
171
|
+
sphinx_index_blocks.each do |block|
|
172
|
+
block.call
|
173
|
+
end
|
174
|
+
|
175
|
+
self.defined_indexes = true
|
176
|
+
|
177
|
+
# We want to make sure that if the database doesn't exist, then Thinking
|
178
|
+
# Sphinx doesn't mind when running non-TS tasks (like db:create, db:drop
|
179
|
+
# and db:migrate). It's a bit hacky, but I can't think of a better way.
|
180
|
+
rescue StandardError => err
|
181
|
+
case err.class.name
|
182
|
+
when "Mysql::Error", "Java::JavaSql::SQLException", "ActiveRecord::StatementInvalid"
|
183
|
+
return
|
184
|
+
else
|
185
|
+
raise err
|
186
|
+
end
|
187
|
+
end
|
188
|
+
|
189
|
+
def add_sphinx_index(index)
|
190
|
+
self.sphinx_indexes << index
|
191
|
+
subclasses.each { |klass| klass.add_sphinx_index(index) }
|
192
|
+
end
|
193
|
+
|
194
|
+
def has_sphinx_indexes?
|
195
|
+
sphinx_indexes &&
|
196
|
+
sphinx_index_blocks &&
|
197
|
+
(sphinx_indexes.length > 0 || sphinx_index_blocks.length > 0)
|
198
|
+
end
|
199
|
+
|
200
|
+
def indexed_by_sphinx?
|
201
|
+
sphinx_indexes && sphinx_indexes.length > 0
|
202
|
+
end
|
203
|
+
|
204
|
+
def delta_indexed_by_sphinx?
|
205
|
+
sphinx_indexes && sphinx_indexes.any? { |index| index.delta? }
|
206
|
+
end
|
207
|
+
|
208
|
+
def sphinx_index_names
|
209
|
+
define_indexes
|
210
|
+
sphinx_indexes.collect(&:all_names).flatten
|
211
|
+
end
|
212
|
+
|
213
|
+
def core_index_names
|
214
|
+
define_indexes
|
215
|
+
sphinx_indexes.collect(&:core_name)
|
216
|
+
end
|
217
|
+
|
218
|
+
def delta_index_names
|
219
|
+
define_indexes
|
220
|
+
sphinx_indexes.select(&:delta?).collect(&:delta_name)
|
221
|
+
end
|
222
|
+
|
223
|
+
def to_riddle
|
224
|
+
define_indexes
|
225
|
+
sphinx_database_adapter.setup
|
226
|
+
|
227
|
+
local_sphinx_indexes.collect { |index|
|
228
|
+
index.to_riddle(sphinx_offset)
|
229
|
+
}.flatten
|
230
|
+
end
|
231
|
+
|
232
|
+
def source_of_sphinx_index
|
233
|
+
define_indexes
|
234
|
+
possible_models = self.sphinx_indexes.collect { |index| index.model }
|
235
|
+
return self if possible_models.include?(self)
|
236
|
+
|
237
|
+
parent = self.superclass
|
238
|
+
while !possible_models.include?(parent) && parent != ::ActiveRecord::Base
|
239
|
+
parent = parent.superclass
|
240
|
+
end
|
241
|
+
|
242
|
+
return parent
|
243
|
+
end
|
244
|
+
|
245
|
+
def delete_in_index(index, document_id)
|
246
|
+
return unless ThinkingSphinx.sphinx_running? &&
|
247
|
+
search_for_id(document_id, index)
|
248
|
+
|
249
|
+
ThinkingSphinx::Configuration.instance.client.update(
|
250
|
+
index, ['sphinx_deleted'], {document_id => [1]}
|
251
|
+
)
|
252
|
+
end
|
253
|
+
|
254
|
+
def sphinx_offset
|
255
|
+
ThinkingSphinx.context.superclass_indexed_models.
|
256
|
+
index eldest_indexed_ancestor
|
257
|
+
end
|
258
|
+
|
259
|
+
# Temporarily disable delta indexing inside a block, then perform a single
|
260
|
+
# rebuild of index at the end.
|
261
|
+
#
|
262
|
+
# Useful when performing updates to batches of models to prevent
|
263
|
+
# the delta index being rebuilt after each individual update.
|
264
|
+
#
|
265
|
+
# In the following example, the delta index will only be rebuilt once,
|
266
|
+
# not 10 times.
|
267
|
+
#
|
268
|
+
# SomeModel.suspended_delta do
|
269
|
+
# 10.times do
|
270
|
+
# SomeModel.create( ... )
|
271
|
+
# end
|
272
|
+
# end
|
273
|
+
#
|
274
|
+
def suspended_delta(reindex_after = true, &block)
|
275
|
+
define_indexes
|
276
|
+
original_setting = ThinkingSphinx.deltas_enabled?
|
277
|
+
ThinkingSphinx.deltas_enabled = false
|
278
|
+
begin
|
279
|
+
yield
|
280
|
+
ensure
|
281
|
+
ThinkingSphinx.deltas_enabled = original_setting
|
282
|
+
end
|
283
|
+
end
|
284
|
+
|
285
|
+
private
|
286
|
+
|
287
|
+
def local_sphinx_indexes
|
288
|
+
sphinx_indexes.select { |index|
|
289
|
+
index.model == self
|
290
|
+
}
|
291
|
+
end
|
292
|
+
|
293
|
+
def add_sphinx_callbacks_and_extend(delta = false)
|
294
|
+
|
295
|
+
if delta && !delta_indexed_by_sphinx?
|
296
|
+
before_save :toggle_delta
|
297
|
+
end
|
298
|
+
end
|
299
|
+
|
300
|
+
def eldest_indexed_ancestor
|
301
|
+
ancestors.reverse.detect { |ancestor|
|
302
|
+
ThinkingSphinx.context.indexed_models.include?(ancestor.name)
|
303
|
+
}.name
|
304
|
+
end
|
305
|
+
end
|
306
|
+
|
307
|
+
def in_index?(suffix)
|
308
|
+
self.class.search_for_id self.sphinx_document_id, sphinx_index_name(suffix)
|
309
|
+
end
|
310
|
+
|
311
|
+
def in_core_index?
|
312
|
+
in_index? "core"
|
313
|
+
end
|
314
|
+
|
315
|
+
def in_delta_index?
|
316
|
+
in_index? "delta"
|
317
|
+
end
|
318
|
+
|
319
|
+
def in_both_indexes?
|
320
|
+
in_core_index? && in_delta_index?
|
321
|
+
end
|
322
|
+
|
323
|
+
# Returns the unique integer id for the object. This method uses the
|
324
|
+
# attribute hash to get around ActiveRecord always mapping the #id method
|
325
|
+
# to whatever the real primary key is (which may be a unique string hash).
|
326
|
+
#
|
327
|
+
# @return [Integer] Unique record id for the purposes of Sphinx.
|
328
|
+
#
|
329
|
+
def primary_key_for_sphinx
|
330
|
+
@primary_key_for_sphinx ||= read_attribute(self.class.primary_key_for_sphinx)
|
331
|
+
end
|
332
|
+
|
333
|
+
def sphinx_document_id
|
334
|
+
primary_key_for_sphinx * ThinkingSphinx.context.indexed_models.size +
|
335
|
+
self.class.sphinx_offset
|
336
|
+
end
|
337
|
+
|
338
|
+
private
|
339
|
+
|
340
|
+
def sphinx_index_name(suffix)
|
341
|
+
"#{self.class.source_of_sphinx_index.name.underscore.tr(':/\\', '_')}_#{suffix}"
|
342
|
+
end
|
343
|
+
|
344
|
+
def define_indexes
|
345
|
+
self.class.define_indexes
|
346
|
+
end
|
347
|
+
end
|
348
|
+
end
|
@@ -0,0 +1,42 @@
|
|
1
|
+
module ThinkingSphinx
|
2
|
+
class AbstractAdapter
|
3
|
+
def initialize(model)
|
4
|
+
@model = model
|
5
|
+
end
|
6
|
+
|
7
|
+
def setup
|
8
|
+
# Deliberately blank - subclasses should do something though. Well, if
|
9
|
+
# they need to.
|
10
|
+
end
|
11
|
+
|
12
|
+
def self.detect(model)
|
13
|
+
case model.connection.class.name
|
14
|
+
when "ActiveRecord::ConnectionAdapters::MysqlAdapter",
|
15
|
+
"ActiveRecord::ConnectionAdapters::MysqlplusAdapter"
|
16
|
+
ThinkingSphinx::MysqlAdapter.new model
|
17
|
+
when "ActiveRecord::ConnectionAdapters::PostgreSQLAdapter"
|
18
|
+
ThinkingSphinx::PostgreSQLAdapter.new model
|
19
|
+
when "ActiveRecord::ConnectionAdapters::JdbcAdapter"
|
20
|
+
if model.connection.config[:adapter] == "jdbcmysql"
|
21
|
+
ThinkingSphinx::MysqlAdapter.new model
|
22
|
+
elsif model.connection.config[:adapter] == "jdbcpostgresql"
|
23
|
+
ThinkingSphinx::PostgreSQLAdapter.new model
|
24
|
+
else
|
25
|
+
raise "Invalid Database Adapter: Sphinx only supports MySQL and PostgreSQL"
|
26
|
+
end
|
27
|
+
else
|
28
|
+
raise "Invalid Database Adapter: Sphinx only supports MySQL and PostgreSQL, not #{model.connection.class.name}"
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
def quote_with_table(column)
|
33
|
+
"#{@model.quoted_table_name}.#{@model.connection.quote_column_name(column)}"
|
34
|
+
end
|
35
|
+
|
36
|
+
protected
|
37
|
+
|
38
|
+
def connection
|
39
|
+
@connection ||= @model.connection
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
@@ -0,0 +1,54 @@
|
|
1
|
+
module ThinkingSphinx
|
2
|
+
class MysqlAdapter < AbstractAdapter
|
3
|
+
def setup
|
4
|
+
# Does MySQL actually need to do anything?
|
5
|
+
end
|
6
|
+
|
7
|
+
def sphinx_identifier
|
8
|
+
"mysql"
|
9
|
+
end
|
10
|
+
|
11
|
+
def concatenate(clause, separator = ' ')
|
12
|
+
"CONCAT_WS('#{separator}', #{clause})"
|
13
|
+
end
|
14
|
+
|
15
|
+
def group_concatenate(clause, separator = ' ')
|
16
|
+
"GROUP_CONCAT(DISTINCT IFNULL(#{clause}, '0') SEPARATOR '#{separator}')"
|
17
|
+
end
|
18
|
+
|
19
|
+
def cast_to_string(clause)
|
20
|
+
"CAST(#{clause} AS CHAR)"
|
21
|
+
end
|
22
|
+
|
23
|
+
def cast_to_datetime(clause)
|
24
|
+
"UNIX_TIMESTAMP(#{clause})"
|
25
|
+
end
|
26
|
+
|
27
|
+
def cast_to_unsigned(clause)
|
28
|
+
"CAST(#{clause} AS UNSIGNED)"
|
29
|
+
end
|
30
|
+
|
31
|
+
def convert_nulls(clause, default = '')
|
32
|
+
default = "'#{default}'" if default.is_a?(String)
|
33
|
+
|
34
|
+
"IFNULL(#{clause}, #{default})"
|
35
|
+
end
|
36
|
+
|
37
|
+
def boolean(value)
|
38
|
+
value ? 1 : 0
|
39
|
+
end
|
40
|
+
|
41
|
+
def crc(clause, blank_to_null = false)
|
42
|
+
clause = "NULLIF(#{clause},'')" if blank_to_null
|
43
|
+
"CRC32(#{clause})"
|
44
|
+
end
|
45
|
+
|
46
|
+
def utf8_query_pre
|
47
|
+
"SET NAMES utf8"
|
48
|
+
end
|
49
|
+
|
50
|
+
def time_difference(diff)
|
51
|
+
"DATE_SUB(NOW(), INTERVAL #{diff} SECOND)"
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|