sherpa99-thinking-sphinx 1.1.4
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 +107 -0
- data/README.textile +107 -0
- data/Rakefile +4 -0
- data/contribute.rb +328 -0
- data/cucumber.yml +1 -0
- data/features/a.rb +17 -0
- data/features/attribute_transformation.feature +22 -0
- data/features/datetime_deltas.feature +55 -0
- data/features/delayed_delta_indexing.feature +37 -0
- data/features/deleting_instances.feature +52 -0
- data/features/facets.feature +26 -0
- data/features/handling_edits.feature +67 -0
- data/features/retry_stale_indexes.feature +24 -0
- data/features/searching_across_models.feature +20 -0
- data/features/searching_by_model.feature +118 -0
- data/features/searching_with_find_arguments.feature +56 -0
- data/features/sphinx_detection.feature +16 -0
- data/features/step_definitions/alpha_steps.rb +3 -0
- data/features/step_definitions/beta_steps.rb +11 -0
- data/features/step_definitions/cat_steps.rb +3 -0
- data/features/step_definitions/common_steps.rb +154 -0
- data/features/step_definitions/datetime_delta_steps.rb +11 -0
- data/features/step_definitions/delayed_delta_indexing_steps.rb +7 -0
- data/features/step_definitions/facet_steps.rb +30 -0
- data/features/step_definitions/find_arguments_steps.rb +36 -0
- data/features/step_definitions/gamma_steps.rb +15 -0
- data/features/step_definitions/search_steps.rb +66 -0
- data/features/step_definitions/sphinx_steps.rb +23 -0
- data/features/support/db/active_record.rb +40 -0
- data/features/support/db/database.example.yml +4 -0
- data/features/support/db/migrations/create_alphas.rb +18 -0
- data/features/support/db/migrations/create_animals.rb +9 -0
- data/features/support/db/migrations/create_betas.rb +15 -0
- data/features/support/db/migrations/create_boxes.rb +13 -0
- data/features/support/db/migrations/create_comments.rb +13 -0
- data/features/support/db/migrations/create_delayed_betas.rb +28 -0
- data/features/support/db/migrations/create_developers.rb +39 -0
- data/features/support/db/migrations/create_gammas.rb +14 -0
- data/features/support/db/migrations/create_people.rb +1014 -0
- data/features/support/db/migrations/create_posts.rb +6 -0
- data/features/support/db/migrations/create_thetas.rb +16 -0
- data/features/support/db/mysql.rb +4 -0
- data/features/support/db/postgresql.rb +4 -0
- data/features/support/env.rb +6 -0
- data/features/support/models/alpha.rb +9 -0
- data/features/support/models/animal.rb +5 -0
- data/features/support/models/beta.rb +7 -0
- data/features/support/models/box.rb +8 -0
- data/features/support/models/cat.rb +3 -0
- data/features/support/models/comment.rb +3 -0
- data/features/support/models/delayed_beta.rb +7 -0
- data/features/support/models/developer.rb +8 -0
- data/features/support/models/gamma.rb +5 -0
- data/features/support/models/person.rb +8 -0
- data/features/support/models/post.rb +8 -0
- data/features/support/models/theta.rb +7 -0
- data/features/support/post_database.rb +37 -0
- data/features/support/z.rb +19 -0
- data/ginger_scenarios.rb +24 -0
- data/init.rb +12 -0
- data/lib/thinking_sphinx.rb +144 -0
- data/lib/thinking_sphinx/active_record.rb +245 -0
- data/lib/thinking_sphinx/active_record/delta.rb +74 -0
- data/lib/thinking_sphinx/active_record/has_many_association.rb +29 -0
- data/lib/thinking_sphinx/active_record/search.rb +57 -0
- data/lib/thinking_sphinx/adapters/abstract_adapter.rb +34 -0
- data/lib/thinking_sphinx/adapters/mysql_adapter.rb +53 -0
- data/lib/thinking_sphinx/adapters/postgresql_adapter.rb +129 -0
- data/lib/thinking_sphinx/association.rb +144 -0
- data/lib/thinking_sphinx/attribute.rb +258 -0
- data/lib/thinking_sphinx/collection.rb +142 -0
- data/lib/thinking_sphinx/configuration.rb +236 -0
- data/lib/thinking_sphinx/core/string.rb +22 -0
- data/lib/thinking_sphinx/deltas.rb +22 -0
- data/lib/thinking_sphinx/deltas/datetime_delta.rb +50 -0
- data/lib/thinking_sphinx/deltas/default_delta.rb +65 -0
- data/lib/thinking_sphinx/deltas/delayed_delta.rb +25 -0
- data/lib/thinking_sphinx/deltas/delayed_delta/delta_job.rb +24 -0
- data/lib/thinking_sphinx/deltas/delayed_delta/flag_as_deleted_job.rb +27 -0
- data/lib/thinking_sphinx/deltas/delayed_delta/job.rb +26 -0
- data/lib/thinking_sphinx/facet.rb +58 -0
- data/lib/thinking_sphinx/facet_collection.rb +44 -0
- data/lib/thinking_sphinx/field.rb +172 -0
- data/lib/thinking_sphinx/index.rb +414 -0
- data/lib/thinking_sphinx/index/builder.rb +233 -0
- data/lib/thinking_sphinx/index/faux_column.rb +110 -0
- data/lib/thinking_sphinx/rails_additions.rb +133 -0
- data/lib/thinking_sphinx/search.rb +638 -0
- data/lib/thinking_sphinx/tasks.rb +128 -0
- data/rails/init.rb +6 -0
- data/spec/fixtures/data.sql +32 -0
- data/spec/fixtures/database.yml.default +3 -0
- data/spec/fixtures/models.rb +81 -0
- data/spec/fixtures/structure.sql +84 -0
- data/spec/spec_helper.rb +54 -0
- data/spec/sphinx_helper.rb +109 -0
- data/spec/unit/thinking_sphinx/active_record/delta_spec.rb +136 -0
- data/spec/unit/thinking_sphinx/active_record/has_many_association_spec.rb +53 -0
- data/spec/unit/thinking_sphinx/active_record/search_spec.rb +107 -0
- data/spec/unit/thinking_sphinx/active_record_spec.rb +256 -0
- data/spec/unit/thinking_sphinx/association_spec.rb +247 -0
- data/spec/unit/thinking_sphinx/attribute_spec.rb +212 -0
- data/spec/unit/thinking_sphinx/collection_spec.rb +14 -0
- data/spec/unit/thinking_sphinx/configuration_spec.rb +136 -0
- data/spec/unit/thinking_sphinx/core/string_spec.rb +9 -0
- data/spec/unit/thinking_sphinx/field_spec.rb +145 -0
- data/spec/unit/thinking_sphinx/index/builder_spec.rb +5 -0
- data/spec/unit/thinking_sphinx/index/faux_column_spec.rb +30 -0
- data/spec/unit/thinking_sphinx/index_spec.rb +54 -0
- data/spec/unit/thinking_sphinx/search_spec.rb +59 -0
- data/spec/unit/thinking_sphinx_spec.rb +129 -0
- data/tasks/distribution.rb +48 -0
- data/tasks/rails.rake +1 -0
- data/tasks/testing.rb +86 -0
- data/thinking-sphinx.gemspec +232 -0
- data/vendor/after_commit/LICENSE +20 -0
- data/vendor/after_commit/README +16 -0
- data/vendor/after_commit/Rakefile +22 -0
- data/vendor/after_commit/init.rb +5 -0
- data/vendor/after_commit/lib/after_commit.rb +42 -0
- data/vendor/after_commit/lib/after_commit/active_record.rb +91 -0
- data/vendor/after_commit/lib/after_commit/connection_adapters.rb +103 -0
- data/vendor/after_commit/test/after_commit_test.rb +53 -0
- data/vendor/delayed_job/lib/delayed/job.rb +251 -0
- data/vendor/delayed_job/lib/delayed/message_sending.rb +7 -0
- data/vendor/delayed_job/lib/delayed/performable_method.rb +55 -0
- data/vendor/delayed_job/lib/delayed/worker.rb +54 -0
- data/vendor/riddle/lib/riddle.rb +30 -0
- data/vendor/riddle/lib/riddle/client.rb +619 -0
- data/vendor/riddle/lib/riddle/client/filter.rb +53 -0
- data/vendor/riddle/lib/riddle/client/message.rb +65 -0
- data/vendor/riddle/lib/riddle/client/response.rb +84 -0
- data/vendor/riddle/lib/riddle/configuration.rb +33 -0
- data/vendor/riddle/lib/riddle/configuration/distributed_index.rb +48 -0
- data/vendor/riddle/lib/riddle/configuration/index.rb +142 -0
- data/vendor/riddle/lib/riddle/configuration/indexer.rb +19 -0
- data/vendor/riddle/lib/riddle/configuration/remote_index.rb +17 -0
- data/vendor/riddle/lib/riddle/configuration/searchd.rb +25 -0
- data/vendor/riddle/lib/riddle/configuration/section.rb +37 -0
- data/vendor/riddle/lib/riddle/configuration/source.rb +23 -0
- data/vendor/riddle/lib/riddle/configuration/sql_source.rb +34 -0
- data/vendor/riddle/lib/riddle/configuration/xml_source.rb +28 -0
- data/vendor/riddle/lib/riddle/controller.rb +44 -0
- metadata +248 -0
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
module ThinkingSphinx
|
|
2
|
+
module Core
|
|
3
|
+
module String
|
|
4
|
+
|
|
5
|
+
def to_crc32
|
|
6
|
+
result = 0xFFFFFFFF
|
|
7
|
+
self.each_byte do |byte|
|
|
8
|
+
result ^= byte
|
|
9
|
+
8.times do
|
|
10
|
+
result = (result >> 1) ^ (0xEDB88320 * (result & 1))
|
|
11
|
+
end
|
|
12
|
+
end
|
|
13
|
+
result ^ 0xFFFFFFFF
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
class String
|
|
21
|
+
include ThinkingSphinx::Core::String
|
|
22
|
+
end
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
require 'thinking_sphinx/deltas/default_delta'
|
|
2
|
+
require 'thinking_sphinx/deltas/delayed_delta'
|
|
3
|
+
require 'thinking_sphinx/deltas/datetime_delta'
|
|
4
|
+
|
|
5
|
+
module ThinkingSphinx
|
|
6
|
+
module Deltas
|
|
7
|
+
def self.parse(index, options)
|
|
8
|
+
case options.delete(:delta)
|
|
9
|
+
when TrueClass, :default
|
|
10
|
+
DefaultDelta.new index, options
|
|
11
|
+
when :delayed
|
|
12
|
+
DelayedDelta.new index, options
|
|
13
|
+
when :datetime
|
|
14
|
+
DatetimeDelta.new index, options
|
|
15
|
+
when FalseClass, nil
|
|
16
|
+
nil
|
|
17
|
+
else
|
|
18
|
+
raise "Unknown delta type"
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
end
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
module ThinkingSphinx
|
|
2
|
+
module Deltas
|
|
3
|
+
class DatetimeDelta < ThinkingSphinx::Deltas::DefaultDelta
|
|
4
|
+
attr_accessor :column, :threshold
|
|
5
|
+
|
|
6
|
+
def initialize(index, options)
|
|
7
|
+
@index = index
|
|
8
|
+
@column = options.delete(:delta_column) || :updated_at
|
|
9
|
+
@threshold = options.delete(:threshold) || 1.day
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def index(model, instance = nil)
|
|
13
|
+
# do nothing
|
|
14
|
+
true
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def delayed_index(model)
|
|
18
|
+
config = ThinkingSphinx::Configuration.instance
|
|
19
|
+
rotate = ThinkingSphinx.sphinx_running? ? "--rotate" : ""
|
|
20
|
+
|
|
21
|
+
output = `#{config.bin_path}indexer --config #{config.config_file} #{rotate} #{delta_index_name model}`
|
|
22
|
+
output += `#{config.bin_path}indexer --config #{config.config_file} #{rotate} --merge #{core_index_name model} #{delta_index_name model} --merge-dst-range sphinx_deleted 0 0`
|
|
23
|
+
puts output unless ThinkingSphinx.suppress_delta_output?
|
|
24
|
+
|
|
25
|
+
true
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def toggle(instance)
|
|
29
|
+
# do nothing
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def toggled(instance)
|
|
33
|
+
instance.send(@column) > @threshold.ago
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def reset_query(model)
|
|
37
|
+
nil
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def clause(model, toggled)
|
|
41
|
+
if toggled
|
|
42
|
+
"#{model.quoted_table_name}.#{@index.quote_column(@column.to_s)}" +
|
|
43
|
+
" > #{adapter.time_difference(@threshold)}"
|
|
44
|
+
else
|
|
45
|
+
nil
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
end
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
module ThinkingSphinx
|
|
2
|
+
module Deltas
|
|
3
|
+
class DefaultDelta
|
|
4
|
+
attr_accessor :column
|
|
5
|
+
|
|
6
|
+
def initialize(index, options)
|
|
7
|
+
@index = index
|
|
8
|
+
@column = options.delete(:delta_column) || :delta
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def index(model, instance = nil)
|
|
12
|
+
return true unless ThinkingSphinx.updates_enabled? &&
|
|
13
|
+
ThinkingSphinx.deltas_enabled?
|
|
14
|
+
|
|
15
|
+
config = ThinkingSphinx::Configuration.instance
|
|
16
|
+
client = Riddle::Client.new config.address, config.port
|
|
17
|
+
|
|
18
|
+
client.update(
|
|
19
|
+
core_index_name(model),
|
|
20
|
+
['sphinx_deleted'],
|
|
21
|
+
{instance.sphinx_document_id => [1]}
|
|
22
|
+
) if instance && ThinkingSphinx.sphinx_running? && instance.in_core_index?
|
|
23
|
+
|
|
24
|
+
output = `#{config.bin_path}indexer --config #{config.config_file} --rotate #{delta_index_name model}`
|
|
25
|
+
puts output unless ThinkingSphinx.suppress_delta_output?
|
|
26
|
+
|
|
27
|
+
true
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def toggle(instance)
|
|
31
|
+
instance.delta = true
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def toggled(instance)
|
|
35
|
+
instance.delta
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def reset_query(model)
|
|
39
|
+
"UPDATE #{model.quoted_table_name} SET " +
|
|
40
|
+
"#{@index.quote_column(@column.to_s)} = #{adapter.boolean(false)}"
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def clause(model, toggled)
|
|
44
|
+
"#{model.quoted_table_name}.#{@index.quote_column(@column.to_s)}" +
|
|
45
|
+
" = #{adapter.boolean(toggled)}"
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
protected
|
|
49
|
+
|
|
50
|
+
def core_index_name(model)
|
|
51
|
+
"#{model.source_of_sphinx_index.name.underscore.tr(':/\\', '_')}_core"
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def delta_index_name(model)
|
|
55
|
+
"#{model.source_of_sphinx_index.name.underscore.tr(':/\\', '_')}_delta"
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
private
|
|
59
|
+
|
|
60
|
+
def adapter
|
|
61
|
+
@adapter = @index.model.sphinx_database_adapter
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
end
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
require 'delayed/job'
|
|
2
|
+
|
|
3
|
+
require 'thinking_sphinx/deltas/delayed_delta/delta_job'
|
|
4
|
+
require 'thinking_sphinx/deltas/delayed_delta/flag_as_deleted_job'
|
|
5
|
+
require 'thinking_sphinx/deltas/delayed_delta/job'
|
|
6
|
+
|
|
7
|
+
module ThinkingSphinx
|
|
8
|
+
module Deltas
|
|
9
|
+
class DelayedDelta < ThinkingSphinx::Deltas::DefaultDelta
|
|
10
|
+
def index(model, instance = nil)
|
|
11
|
+
ThinkingSphinx::Deltas::Job.enqueue(
|
|
12
|
+
ThinkingSphinx::Deltas::DeltaJob.new(delta_index_name(model))
|
|
13
|
+
)
|
|
14
|
+
|
|
15
|
+
Delayed::Job.enqueue(
|
|
16
|
+
ThinkingSphinx::Deltas::FlagAsDeletedJob.new(
|
|
17
|
+
core_index_name(model), instance.sphinx_document_id
|
|
18
|
+
)
|
|
19
|
+
) if instance
|
|
20
|
+
|
|
21
|
+
true
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
end
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
module ThinkingSphinx
|
|
2
|
+
module Deltas
|
|
3
|
+
class DeltaJob
|
|
4
|
+
attr_accessor :index
|
|
5
|
+
|
|
6
|
+
def initialize(index)
|
|
7
|
+
@index = index
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
def perform
|
|
11
|
+
return true unless ThinkingSphinx.updates_enabled? &&
|
|
12
|
+
ThinkingSphinx.deltas_enabled?
|
|
13
|
+
|
|
14
|
+
config = ThinkingSphinx::Configuration.instance
|
|
15
|
+
client = Riddle::Client.new config.address, config.port
|
|
16
|
+
|
|
17
|
+
output = `#{config.bin_path}indexer --config #{config.config_file} --rotate #{index}`
|
|
18
|
+
puts output unless ThinkingSphinx.suppress_delta_output?
|
|
19
|
+
|
|
20
|
+
true
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
end
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
module ThinkingSphinx
|
|
2
|
+
module Deltas
|
|
3
|
+
class FlagAsDeletedJob
|
|
4
|
+
attr_accessor :index, :document_id
|
|
5
|
+
|
|
6
|
+
def initialize(index, document_id)
|
|
7
|
+
@index, @document_id = index, document_id
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
def perform
|
|
11
|
+
return true unless ThinkingSphinx.updates_enabled?
|
|
12
|
+
|
|
13
|
+
config = ThinkingSphinx::Configuration.instance
|
|
14
|
+
client = Riddle::Client.new config.address, config.port
|
|
15
|
+
|
|
16
|
+
client.update(
|
|
17
|
+
@index,
|
|
18
|
+
['sphinx_deleted'],
|
|
19
|
+
{@document_id => [1]}
|
|
20
|
+
) if ThinkingSphinx.sphinx_running? &&
|
|
21
|
+
ThinkingSphinx::Search.search_for_id(@document_id, @index)
|
|
22
|
+
|
|
23
|
+
true
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
module ThinkingSphinx
|
|
2
|
+
module Deltas
|
|
3
|
+
class Job < Delayed::Job
|
|
4
|
+
def self.enqueue(object, priority = 0)
|
|
5
|
+
super unless duplicates_exist(object)
|
|
6
|
+
end
|
|
7
|
+
|
|
8
|
+
def self.cancel_thinking_sphinx_jobs
|
|
9
|
+
if connection.tables.include?("delayed_jobs")
|
|
10
|
+
delete_all("handler LIKE '--- !ruby/object:ThinkingSphinx::Deltas::%'")
|
|
11
|
+
end
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
private
|
|
15
|
+
|
|
16
|
+
def self.duplicates_exist(object)
|
|
17
|
+
count(
|
|
18
|
+
:conditions => {
|
|
19
|
+
:handler => object.to_yaml,
|
|
20
|
+
:locked_at => nil
|
|
21
|
+
}
|
|
22
|
+
) > 0
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
end
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
module ThinkingSphinx
|
|
2
|
+
class Facet
|
|
3
|
+
attr_reader :reference
|
|
4
|
+
|
|
5
|
+
def initialize(reference)
|
|
6
|
+
@reference = reference
|
|
7
|
+
|
|
8
|
+
if reference.columns.length != 1
|
|
9
|
+
raise "Can't translate Facets on multiple-column field or attribute"
|
|
10
|
+
end
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def name
|
|
14
|
+
reference.unique_name
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
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"
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def value(object, attribute_value)
|
|
27
|
+
return translate(object, attribute_value) if @reference.is_a?(Field)
|
|
28
|
+
|
|
29
|
+
case @reference.type
|
|
30
|
+
when :string, :multi
|
|
31
|
+
translate(object, attribute_value)
|
|
32
|
+
when :datetime
|
|
33
|
+
Time.at(attribute_value)
|
|
34
|
+
when :boolean
|
|
35
|
+
attribute_value > 0
|
|
36
|
+
else
|
|
37
|
+
attribute_value
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def to_s
|
|
42
|
+
name
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
private
|
|
46
|
+
|
|
47
|
+
def translate(object, attribute_value)
|
|
48
|
+
column.__stack.each { |method|
|
|
49
|
+
object = object.send(method)
|
|
50
|
+
}
|
|
51
|
+
object.send(column.__name)
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def column
|
|
55
|
+
@reference.columns.first
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
end
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
module ThinkingSphinx
|
|
2
|
+
class FacetCollection < Hash
|
|
3
|
+
attr_accessor :arguments
|
|
4
|
+
|
|
5
|
+
def initialize(arguments)
|
|
6
|
+
@arguments = arguments.clone
|
|
7
|
+
@attribute_values = {}
|
|
8
|
+
@facets = []
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def add_from_results(facet, results)
|
|
12
|
+
self[facet.name] = {}
|
|
13
|
+
@attribute_values[facet.name] = {}
|
|
14
|
+
@facets << facet
|
|
15
|
+
|
|
16
|
+
results.each_with_groupby_and_count { |result, group, count|
|
|
17
|
+
facet_value = facet.value(result, group)
|
|
18
|
+
|
|
19
|
+
self[facet.name][facet_value] = count
|
|
20
|
+
@attribute_values[facet.name][facet_value] = group
|
|
21
|
+
}
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def for(hash = {})
|
|
25
|
+
arguments = @arguments.clone
|
|
26
|
+
options = arguments.extract_options!
|
|
27
|
+
options[:with] ||= {}
|
|
28
|
+
|
|
29
|
+
hash.each do |key, value|
|
|
30
|
+
attrib = facet_for_key(key).attribute_name
|
|
31
|
+
options[:with][attrib] = @attribute_values[key][value]
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
arguments << options
|
|
35
|
+
ThinkingSphinx::Search.search *arguments
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
private
|
|
39
|
+
|
|
40
|
+
def facet_for_key(key)
|
|
41
|
+
@facets.detect { |facet| facet.name == key }
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
end
|
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
module ThinkingSphinx
|
|
2
|
+
# Fields - holding the string data which Sphinx indexes for your searches.
|
|
3
|
+
# This class isn't really useful to you unless you're hacking around with the
|
|
4
|
+
# internals of Thinking Sphinx - but hey, don't let that stop you.
|
|
5
|
+
#
|
|
6
|
+
# One key thing to remember - if you're using the field manually to
|
|
7
|
+
# generate SQL statements, you'll need to set the base model, and all the
|
|
8
|
+
# associations. Which can get messy. Use Index.link!, it really helps.
|
|
9
|
+
#
|
|
10
|
+
class Field
|
|
11
|
+
attr_accessor :alias, :columns, :sortable, :associations, :model, :infixes,
|
|
12
|
+
:prefixes, :faceted
|
|
13
|
+
|
|
14
|
+
# To create a new field, you'll need to pass in either a single Column
|
|
15
|
+
# or an array of them, and some (optional) options. The columns are
|
|
16
|
+
# references to the data that will make up the field.
|
|
17
|
+
#
|
|
18
|
+
# Valid options are:
|
|
19
|
+
# - :as => :alias_name
|
|
20
|
+
# - :sortable => true
|
|
21
|
+
# - :infixes => true
|
|
22
|
+
# - :prefixes => true
|
|
23
|
+
#
|
|
24
|
+
# Alias is only required in three circumstances: when there's
|
|
25
|
+
# another attribute or field with the same name, when the column name is
|
|
26
|
+
# 'id', or when there's more than one column.
|
|
27
|
+
#
|
|
28
|
+
# Sortable defaults to false - but is quite useful when set to true, as
|
|
29
|
+
# it creates an attribute with the same string value (which Sphinx converts
|
|
30
|
+
# to an integer value), which can be sorted by. Thinking Sphinx is smart
|
|
31
|
+
# enough to realise that when you specify fields in sort statements, you
|
|
32
|
+
# mean their respective attributes.
|
|
33
|
+
#
|
|
34
|
+
# If you have partial matching enabled (ie: enable_star), then you can
|
|
35
|
+
# specify certain fields to have their prefixes and infixes indexed. Keep
|
|
36
|
+
# in mind, though, that Sphinx's default is _all_ fields - so once you
|
|
37
|
+
# highlight a particular field, no other fields in the index will have
|
|
38
|
+
# these partial indexes.
|
|
39
|
+
#
|
|
40
|
+
# Here's some examples:
|
|
41
|
+
#
|
|
42
|
+
# Field.new(
|
|
43
|
+
# Column.new(:name)
|
|
44
|
+
# )
|
|
45
|
+
#
|
|
46
|
+
# Field.new(
|
|
47
|
+
# [Column.new(:first_name), Column.new(:last_name)],
|
|
48
|
+
# :as => :name, :sortable => true
|
|
49
|
+
# )
|
|
50
|
+
#
|
|
51
|
+
# Field.new(
|
|
52
|
+
# [Column.new(:posts, :subject), Column.new(:posts, :content)],
|
|
53
|
+
# :as => :posts, :prefixes => true
|
|
54
|
+
# )
|
|
55
|
+
#
|
|
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) }
|
|
61
|
+
|
|
62
|
+
@alias = options[:as]
|
|
63
|
+
@sortable = options[:sortable] || false
|
|
64
|
+
@infixes = options[:infixes] || false
|
|
65
|
+
@prefixes = options[:prefixes] || false
|
|
66
|
+
@faceted = options[:facet] || false
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
# Get the part of the SELECT clause related to this field. Don't forget
|
|
70
|
+
# to set your model and associations first though.
|
|
71
|
+
#
|
|
72
|
+
# This will concatenate strings if there's more than one data source or
|
|
73
|
+
# multiple data values (has_many or has_and_belongs_to_many associations).
|
|
74
|
+
#
|
|
75
|
+
def to_select_sql
|
|
76
|
+
clause = @columns.collect { |column|
|
|
77
|
+
column_with_prefix(column)
|
|
78
|
+
}.join(', ')
|
|
79
|
+
|
|
80
|
+
clause = adapter.concatenate(clause) if concat_ws?
|
|
81
|
+
clause = adapter.group_concatenate(clause) if is_many?
|
|
82
|
+
|
|
83
|
+
"#{adapter.cast_to_string clause } AS #{quote_column(unique_name)}"
|
|
84
|
+
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
|
+
end
|
|
172
|
+
end
|