pg_search 0.2.2 → 0.3
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/.gitignore +1 -2
- data/.rspec +1 -0
- data/.travis.yml +2 -0
- data/CHANGELOG +8 -5
- data/Gemfile +10 -5
- data/README.rdoc +151 -44
- data/Rakefile +9 -24
- data/lib/pg_search.rb +45 -21
- data/lib/pg_search/configuration.rb +13 -1
- data/lib/pg_search/configuration/association.rb +5 -6
- data/lib/pg_search/configuration/column.rb +1 -2
- data/lib/pg_search/document.rb +21 -0
- data/lib/pg_search/features/tsearch.rb +19 -10
- data/lib/pg_search/multisearch.rb +46 -0
- data/lib/pg_search/multisearchable.rb +28 -0
- data/lib/pg_search/railtie.rb +0 -4
- data/lib/pg_search/scope.rb +1 -1
- data/lib/pg_search/scope_options.rb +13 -17
- data/lib/pg_search/tasks.rb +37 -0
- data/lib/pg_search/version.rb +1 -1
- data/pg_search.gemspec +3 -0
- data/spec/associations_spec.rb +209 -230
- data/spec/pg_search/document_spec.rb +49 -0
- data/spec/pg_search/multisearch_spec.rb +66 -0
- data/spec/pg_search/multisearchable_spec.rb +108 -0
- data/spec/pg_search_spec.rb +148 -42
- data/spec/spec_helper.rb +6 -2
- metadata +83 -31
- checksums.yaml +0 -7
- data/TODO +0 -10
- data/gemfiles/Gemfile.common +0 -9
- data/gemfiles/rails2/Gemfile +0 -4
- data/gemfiles/rails3/Gemfile +0 -4
@@ -4,7 +4,7 @@ module PgSearch
|
|
4
4
|
class Configuration
|
5
5
|
class Association
|
6
6
|
attr_reader :columns
|
7
|
-
|
7
|
+
|
8
8
|
def initialize(model, name, column_names)
|
9
9
|
@model = model
|
10
10
|
@name = name
|
@@ -12,11 +12,11 @@ module PgSearch
|
|
12
12
|
Column.new(column_name, weight, @model, self)
|
13
13
|
end
|
14
14
|
end
|
15
|
-
|
15
|
+
|
16
16
|
def table_name
|
17
17
|
@model.reflect_on_association(@name).table_name
|
18
18
|
end
|
19
|
-
|
19
|
+
|
20
20
|
def join(primary_key)
|
21
21
|
selects = columns.map do |column|
|
22
22
|
"string_agg(#{column.full_name}, ' ') AS #{column.alias}"
|
@@ -24,10 +24,9 @@ module PgSearch
|
|
24
24
|
relation = @model.joins(@name).select("#{primary_key} AS id, #{selects}").group(primary_key)
|
25
25
|
"LEFT OUTER JOIN (#{relation.to_sql}) #{subselect_alias} ON #{subselect_alias}.id = #{primary_key}"
|
26
26
|
end
|
27
|
-
|
27
|
+
|
28
28
|
def subselect_alias
|
29
|
-
|
30
|
-
"pg_search_#{Digest::SHA2.hexdigest(subselect_name)}"
|
29
|
+
Configuration.alias(table_name, @name, "subselect")
|
31
30
|
end
|
32
31
|
end
|
33
32
|
end
|
@@ -0,0 +1,21 @@
|
|
1
|
+
require "pg_search/scope"
|
2
|
+
|
3
|
+
module PgSearch
|
4
|
+
class Document < ActiveRecord::Base
|
5
|
+
include PgSearch
|
6
|
+
set_table_name :pg_search_documents
|
7
|
+
belongs_to :searchable, :polymorphic => true
|
8
|
+
|
9
|
+
before_validation :update_content
|
10
|
+
|
11
|
+
pg_search_scope :search, :against => :content
|
12
|
+
|
13
|
+
private
|
14
|
+
|
15
|
+
def update_content
|
16
|
+
methods = Array.wrap(searchable.pg_search_multisearchable_options[:against])
|
17
|
+
searchable_text = methods.map { |symbol| searchable.send(symbol) }.join(" ")
|
18
|
+
self.content = searchable_text
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
@@ -46,22 +46,31 @@ module PgSearch
|
|
46
46
|
tsquery_sql = "#{tsquery_sql} || #{connection.quote(':*')}" if @options[:prefix]
|
47
47
|
|
48
48
|
"to_tsquery(:dictionary, #{tsquery_sql})"
|
49
|
-
end.join(
|
49
|
+
end.join(@options[:any_word] ? ' || ' : ' && ')
|
50
50
|
end
|
51
51
|
|
52
52
|
def tsdocument
|
53
|
-
|
54
|
-
@
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
53
|
+
@columns.map do |search_column|
|
54
|
+
tsvector = "to_tsvector(:dictionary, #{@normalizer.add_normalization(search_column.to_sql)})"
|
55
|
+
search_column.weight.nil? ? tsvector : "setweight(#{tsvector}, #{connection.quote(search_column.weight)})"
|
56
|
+
end.join(" || ")
|
57
|
+
end
|
58
|
+
|
59
|
+
# From http://www.postgresql.org/docs/8.3/static/textsearch-controls.html
|
60
|
+
# 0 (the default) ignores the document length
|
61
|
+
# 1 divides the rank by 1 + the logarithm of the document length
|
62
|
+
# 2 divides the rank by the document length
|
63
|
+
# 4 divides the rank by the mean harmonic distance between extents (this is implemented only by ts_rank_cd)
|
64
|
+
# 8 divides the rank by the number of unique words in document
|
65
|
+
# 16 divides the rank by 1 + the logarithm of the number of unique words in document
|
66
|
+
# 32 divides the rank by itself + 1
|
67
|
+
# The integer option controls several behaviors, so it is a bit mask: you can specify one or more behaviors
|
68
|
+
def normalization
|
69
|
+
@options[:normalization] || 0
|
61
70
|
end
|
62
71
|
|
63
72
|
def tsearch_rank
|
64
|
-
["ts_rank((#{tsdocument}), (#{tsquery}))", interpolations]
|
73
|
+
["ts_rank((#{tsdocument}), (#{tsquery}), #{normalization})", interpolations]
|
65
74
|
end
|
66
75
|
|
67
76
|
def dictionary
|
@@ -0,0 +1,46 @@
|
|
1
|
+
module PgSearch
|
2
|
+
module Multisearch
|
3
|
+
REBUILD_SQL_TEMPLATE = <<-SQL
|
4
|
+
INSERT INTO :documents_table (searchable_type, searchable_id, content)
|
5
|
+
SELECT :model_name AS searchable_type,
|
6
|
+
:model_table.id AS searchable_id,
|
7
|
+
(
|
8
|
+
:content_expressions
|
9
|
+
) AS content
|
10
|
+
FROM :model_table
|
11
|
+
SQL
|
12
|
+
|
13
|
+
class << self
|
14
|
+
def rebuild(model)
|
15
|
+
model.transaction do
|
16
|
+
PgSearch::Document.where(:searchable_type => model.name).delete_all
|
17
|
+
model.connection.execute(rebuild_sql(model))
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
def rebuild_sql(model)
|
22
|
+
connection = model.connection
|
23
|
+
|
24
|
+
columns = Array.wrap(
|
25
|
+
model.pg_search_multisearchable_options[:against]
|
26
|
+
)
|
27
|
+
|
28
|
+
content_expressions = columns.map do |column|
|
29
|
+
%Q{coalesce(:model_table.#{column}, '')}
|
30
|
+
end.join(" || ' ' || ")
|
31
|
+
|
32
|
+
REBUILD_SQL_TEMPLATE.gsub(
|
33
|
+
":content_expressions", content_expressions
|
34
|
+
).gsub(
|
35
|
+
":model_name", connection.quote(model.name)
|
36
|
+
).gsub(
|
37
|
+
":model_table", model.quoted_table_name
|
38
|
+
).gsub(
|
39
|
+
":documents_table", PgSearch::Document.quoted_table_name
|
40
|
+
)
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
|
@@ -0,0 +1,28 @@
|
|
1
|
+
require "active_support/concern"
|
2
|
+
require "active_support/core_ext/class/attribute"
|
3
|
+
|
4
|
+
module PgSearch
|
5
|
+
module Multisearchable
|
6
|
+
extend ActiveSupport::Concern
|
7
|
+
|
8
|
+
included do
|
9
|
+
has_one :pg_search_document,
|
10
|
+
:as => :searchable,
|
11
|
+
:class_name => "PgSearch::Document",
|
12
|
+
:dependent => :delete
|
13
|
+
|
14
|
+
after_create :create_pg_search_document,
|
15
|
+
:if => lambda { PgSearch.multisearch_enabled? }
|
16
|
+
|
17
|
+
after_update :update_pg_search_document,
|
18
|
+
:if => lambda { PgSearch.multisearch_enabled? }
|
19
|
+
end
|
20
|
+
|
21
|
+
module InstanceMethods
|
22
|
+
def update_pg_search_document
|
23
|
+
create_pg_search_document unless self.pg_search_document
|
24
|
+
self.pg_search_document.save
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
data/lib/pg_search/railtie.rb
CHANGED
data/lib/pg_search/scope.rb
CHANGED
@@ -19,13 +19,8 @@ module PgSearch
|
|
19
19
|
@feature_names = @config.features.map { |feature_name, feature_options| feature_name }
|
20
20
|
end
|
21
21
|
|
22
|
-
def
|
23
|
-
{
|
24
|
-
:select => "#{quoted_table_name}.*, (#{rank}) AS pg_search_rank",
|
25
|
-
:conditions => conditions,
|
26
|
-
:order => "pg_search_rank DESC, #{primary_key} ASC",
|
27
|
-
:joins => joins
|
28
|
-
}
|
22
|
+
def to_relation
|
23
|
+
@model.select("#{quoted_table_name}.*, (#{rank}) AS pg_search_rank").where(conditions).order("pg_search_rank DESC, #{order_within_rank}").joins(joins)
|
29
24
|
end
|
30
25
|
|
31
26
|
private
|
@@ -34,6 +29,10 @@ module PgSearch
|
|
34
29
|
@feature_names.map { |feature_name| "(#{sanitize_sql_array(feature_for(feature_name).conditions)})" }.join(" OR ")
|
35
30
|
end
|
36
31
|
|
32
|
+
def order_within_rank
|
33
|
+
@config.order_within_rank || "#{primary_key} ASC"
|
34
|
+
end
|
35
|
+
|
37
36
|
def primary_key
|
38
37
|
"#{quoted_table_name}.#{connection.quote_column_name(model.primary_key)}"
|
39
38
|
end
|
@@ -44,14 +43,15 @@ module PgSearch
|
|
44
43
|
end.join(' ')
|
45
44
|
end
|
46
45
|
|
46
|
+
FEATURE_CLASSES = {
|
47
|
+
:dmetaphone => Features::DMetaphone,
|
48
|
+
:tsearch => Features::TSearch,
|
49
|
+
:trigram => Features::Trigram
|
50
|
+
}
|
51
|
+
|
47
52
|
def feature_for(feature_name)
|
48
53
|
feature_name = feature_name.to_sym
|
49
|
-
|
50
|
-
feature_class = {
|
51
|
-
:dmetaphone => Features::DMetaphone,
|
52
|
-
:tsearch => Features::TSearch,
|
53
|
-
:trigram => Features::Trigram
|
54
|
-
}[feature_name]
|
54
|
+
feature_class = FEATURE_CLASSES[feature_name]
|
55
55
|
|
56
56
|
raise ArgumentError.new("Unknown feature: #{feature_name}") unless feature_class
|
57
57
|
|
@@ -60,10 +60,6 @@ module PgSearch
|
|
60
60
|
feature_class.new(@config.query, @feature_options[feature_name], @config.columns, @model, normalizer)
|
61
61
|
end
|
62
62
|
|
63
|
-
def tsearch_rank
|
64
|
-
sanitize_sql_array(@feature_names[Features::TSearch].rank)
|
65
|
-
end
|
66
|
-
|
67
63
|
def rank
|
68
64
|
(@config.ranking_sql || ":tsearch").gsub(/:(\w*)/) do
|
69
65
|
sanitize_sql_array(feature_for($1).rank)
|
data/lib/pg_search/tasks.rb
CHANGED
@@ -2,7 +2,44 @@ require 'rake'
|
|
2
2
|
require 'pg_search'
|
3
3
|
|
4
4
|
namespace :pg_search do
|
5
|
+
namespace :multisearch do
|
6
|
+
desc "Rebuild PgSearch multisearch records for MODEL"
|
7
|
+
task rebuild: :environment do
|
8
|
+
raise "must set MODEL=<model name>" unless ENV["MODEL"]
|
9
|
+
model_class = ENV["MODEL"].classify.constantize
|
10
|
+
PgSearch::Multisearch.rebuild(model_class)
|
11
|
+
end
|
12
|
+
end
|
13
|
+
|
5
14
|
namespace :migration do
|
15
|
+
desc "Generate migration to add table for multisearch"
|
16
|
+
task :multisearch do
|
17
|
+
now = Time.now.utc
|
18
|
+
filename = "#{now.strftime('%Y%m%d%H%M%S')}_create_pg_search_documents.rb"
|
19
|
+
|
20
|
+
File.open(Rails.root + 'db' + 'migrate' + filename, 'wb') do |migration_file|
|
21
|
+
migration_file.puts <<-RUBY
|
22
|
+
class CreatePgSearchDocuments < ActiveRecord::Migration
|
23
|
+
def self.up
|
24
|
+
say_with_time("Creating table for pg_search multisearch") do
|
25
|
+
create_table :pg_search_documents do |t|
|
26
|
+
t.text :content
|
27
|
+
t.belongs_to :searchable, :polymorphic => true
|
28
|
+
t.timestamps
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
def self.down
|
34
|
+
say_with_time("Dropping table for pg_search multisearch") do
|
35
|
+
drop_table :pg_search_documents
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
39
|
+
RUBY
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
6
43
|
desc "Generate migration to add support functions for :dmetaphone"
|
7
44
|
task :dmetaphone do
|
8
45
|
now = Time.now.utc
|
data/lib/pg_search/version.rb
CHANGED
data/pg_search.gemspec
CHANGED
@@ -16,4 +16,7 @@ Gem::Specification.new do |s|
|
|
16
16
|
s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
|
17
17
|
s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
|
18
18
|
s.require_paths = ["lib"]
|
19
|
+
|
20
|
+
s.add_dependency 'activerecord', '>=3'
|
21
|
+
s.add_dependency 'activesupport', '>=3'
|
19
22
|
end
|
data/spec/associations_spec.rb
CHANGED
@@ -2,314 +2,293 @@ require File.expand_path(File.dirname(__FILE__) + '/spec_helper')
|
|
2
2
|
|
3
3
|
describe PgSearch do
|
4
4
|
context "joining to another table" do
|
5
|
-
|
6
|
-
context "
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
t.string "title"
|
11
|
-
end
|
5
|
+
context "with Arel support" do
|
6
|
+
context "without an :against" do
|
7
|
+
with_model :AssociatedModel do
|
8
|
+
table do |t|
|
9
|
+
t.string "title"
|
12
10
|
end
|
11
|
+
end
|
13
12
|
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
13
|
+
with_model :ModelWithoutAgainst do
|
14
|
+
table do |t|
|
15
|
+
t.string "title"
|
16
|
+
t.belongs_to :another_model
|
17
|
+
end
|
19
18
|
|
20
|
-
|
21
|
-
|
22
|
-
|
19
|
+
model do
|
20
|
+
include PgSearch
|
21
|
+
belongs_to :another_model, :class_name => 'AssociatedModel'
|
23
22
|
|
24
|
-
|
25
|
-
end
|
23
|
+
pg_search_scope :with_another, :associated_against => {:another_model => :title}
|
26
24
|
end
|
25
|
+
end
|
27
26
|
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
27
|
+
it "returns rows that match the query in the columns of the associated model only" do
|
28
|
+
associated = AssociatedModel.create!(:title => 'abcdef')
|
29
|
+
included = [
|
30
|
+
ModelWithoutAgainst.create!(:title => 'abcdef', :another_model => associated),
|
31
|
+
ModelWithoutAgainst.create!(:title => 'ghijkl', :another_model => associated)
|
32
|
+
]
|
33
|
+
excluded = [
|
34
|
+
ModelWithoutAgainst.create!(:title => 'abcdef')
|
35
|
+
]
|
36
|
+
|
37
|
+
results = ModelWithoutAgainst.with_another('abcdef')
|
38
|
+
results.map(&:title).should =~ included.map(&:title)
|
39
|
+
results.should_not include(excluded)
|
40
|
+
end
|
41
|
+
end
|
37
42
|
|
38
|
-
|
39
|
-
|
40
|
-
|
43
|
+
context "through a belongs_to association" do
|
44
|
+
with_model :AssociatedModel do
|
45
|
+
table do |t|
|
46
|
+
t.string 'title'
|
41
47
|
end
|
42
48
|
end
|
43
49
|
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
end
|
50
|
+
with_model :ModelWithBelongsTo do
|
51
|
+
table do |t|
|
52
|
+
t.string 'title'
|
53
|
+
t.belongs_to 'another_model'
|
49
54
|
end
|
50
55
|
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
t.belongs_to 'another_model'
|
55
|
-
end
|
56
|
+
model do
|
57
|
+
include PgSearch
|
58
|
+
belongs_to :another_model, :class_name => 'AssociatedModel'
|
56
59
|
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
+
pg_search_scope :with_associated, :against => :title, :associated_against => {:another_model => :title}
|
61
|
+
end
|
62
|
+
end
|
60
63
|
|
61
|
-
|
62
|
-
|
64
|
+
it "returns rows that match the query in either its own columns or the columns of the associated model" do
|
65
|
+
associated = AssociatedModel.create!(:title => 'abcdef')
|
66
|
+
included = [
|
67
|
+
ModelWithBelongsTo.create!(:title => 'ghijkl', :another_model => associated),
|
68
|
+
ModelWithBelongsTo.create!(:title => 'abcdef')
|
69
|
+
]
|
70
|
+
excluded = ModelWithBelongsTo.create!(:title => 'mnopqr',
|
71
|
+
:another_model => AssociatedModel.create!(:title => 'stuvwx'))
|
72
|
+
|
73
|
+
results = ModelWithBelongsTo.with_associated('abcdef')
|
74
|
+
results.map(&:title).should =~ included.map(&:title)
|
75
|
+
results.should_not include(excluded)
|
76
|
+
end
|
77
|
+
end
|
78
|
+
|
79
|
+
context "through a has_many association" do
|
80
|
+
with_model :AssociatedModelWithHasMany do
|
81
|
+
table do |t|
|
82
|
+
t.string 'title'
|
83
|
+
t.belongs_to 'ModelWithHasMany'
|
63
84
|
end
|
85
|
+
end
|
64
86
|
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
ModelWithBelongsTo.create!(:title => 'abcdef')
|
70
|
-
]
|
71
|
-
excluded = ModelWithBelongsTo.create!(:title => 'mnopqr',
|
72
|
-
:another_model => AssociatedModel.create!(:title => 'stuvwx'))
|
87
|
+
with_model :ModelWithHasMany do
|
88
|
+
table do |t|
|
89
|
+
t.string 'title'
|
90
|
+
end
|
73
91
|
|
74
|
-
|
75
|
-
|
76
|
-
|
92
|
+
model do
|
93
|
+
include PgSearch
|
94
|
+
has_many :other_models, :class_name => 'AssociatedModelWithHasMany', :foreign_key => 'ModelWithHasMany_id'
|
95
|
+
|
96
|
+
pg_search_scope :with_associated, :against => [:title], :associated_against => {:other_models => :title}
|
77
97
|
end
|
78
98
|
end
|
79
99
|
|
80
|
-
|
81
|
-
|
100
|
+
it "returns rows that match the query in either its own columns or the columns of the associated model" do
|
101
|
+
included = [
|
102
|
+
ModelWithHasMany.create!(:title => 'abcdef', :other_models => [
|
103
|
+
AssociatedModelWithHasMany.create!(:title => 'foo'),
|
104
|
+
AssociatedModelWithHasMany.create!(:title => 'bar')
|
105
|
+
]),
|
106
|
+
ModelWithHasMany.create!(:title => 'ghijkl', :other_models => [
|
107
|
+
AssociatedModelWithHasMany.create!(:title => 'foo bar'),
|
108
|
+
AssociatedModelWithHasMany.create!(:title => 'mnopqr')
|
109
|
+
]),
|
110
|
+
ModelWithHasMany.create!(:title => 'foo bar')
|
111
|
+
]
|
112
|
+
excluded = ModelWithHasMany.create!(:title => 'stuvwx', :other_models => [
|
113
|
+
AssociatedModelWithHasMany.create!(:title => 'abcdef')
|
114
|
+
])
|
115
|
+
|
116
|
+
results = ModelWithHasMany.with_associated('foo bar')
|
117
|
+
results.map(&:title).should =~ included.map(&:title)
|
118
|
+
results.should_not include(excluded)
|
119
|
+
end
|
120
|
+
end
|
121
|
+
|
122
|
+
context "across multiple associations" do
|
123
|
+
context "on different tables" do
|
124
|
+
with_model :FirstAssociatedModel do
|
82
125
|
table do |t|
|
83
126
|
t.string 'title'
|
84
|
-
t.belongs_to '
|
127
|
+
t.belongs_to 'ModelWithManyAssociations'
|
85
128
|
end
|
129
|
+
model {}
|
86
130
|
end
|
87
131
|
|
88
|
-
with_model :
|
132
|
+
with_model :SecondAssociatedModel do
|
89
133
|
table do |t|
|
90
134
|
t.string 'title'
|
91
135
|
end
|
136
|
+
model {}
|
137
|
+
end
|
138
|
+
|
139
|
+
with_model :ModelWithManyAssociations do
|
140
|
+
table do |t|
|
141
|
+
t.string 'title'
|
142
|
+
t.belongs_to 'model_of_second_type'
|
143
|
+
end
|
92
144
|
|
93
145
|
model do
|
94
146
|
include PgSearch
|
95
|
-
has_many :
|
147
|
+
has_many :models_of_first_type, :class_name => 'FirstAssociatedModel', :foreign_key => 'ModelWithManyAssociations_id'
|
148
|
+
belongs_to :model_of_second_type, :class_name => 'SecondAssociatedModel'
|
96
149
|
|
97
|
-
pg_search_scope :with_associated, :against =>
|
150
|
+
pg_search_scope :with_associated, :against => :title,
|
151
|
+
:associated_against => {:models_of_first_type => :title, :model_of_second_type => :title}
|
98
152
|
end
|
99
153
|
end
|
100
154
|
|
101
155
|
it "returns rows that match the query in either its own columns or the columns of the associated model" do
|
156
|
+
matching_second = SecondAssociatedModel.create!(:title => "foo bar")
|
157
|
+
unmatching_second = SecondAssociatedModel.create!(:title => "uiop")
|
158
|
+
|
102
159
|
included = [
|
103
|
-
|
104
|
-
|
105
|
-
|
160
|
+
ModelWithManyAssociations.create!(:title => 'abcdef', :models_of_first_type => [
|
161
|
+
FirstAssociatedModel.create!(:title => 'foo'),
|
162
|
+
FirstAssociatedModel.create!(:title => 'bar')
|
106
163
|
]),
|
107
|
-
|
108
|
-
|
109
|
-
|
164
|
+
ModelWithManyAssociations.create!(:title => 'ghijkl', :models_of_first_type => [
|
165
|
+
FirstAssociatedModel.create!(:title => 'foo bar'),
|
166
|
+
FirstAssociatedModel.create!(:title => 'mnopqr')
|
110
167
|
]),
|
111
|
-
|
168
|
+
ModelWithManyAssociations.create!(:title => 'foo bar'),
|
169
|
+
ModelWithManyAssociations.create!(:title => 'qwerty', :model_of_second_type => matching_second)
|
170
|
+
]
|
171
|
+
excluded = [
|
172
|
+
ModelWithManyAssociations.create!(:title => 'stuvwx', :models_of_first_type => [
|
173
|
+
FirstAssociatedModel.create!(:title => 'abcdef')
|
174
|
+
]),
|
175
|
+
ModelWithManyAssociations.create!(:title => 'qwerty', :model_of_second_type => unmatching_second)
|
112
176
|
]
|
113
|
-
excluded = ModelWithHasMany.create!(:title => 'stuvwx', :other_models => [
|
114
|
-
AssociatedModelWithHasMany.create!(:title => 'abcdef')
|
115
|
-
])
|
116
177
|
|
117
|
-
results =
|
178
|
+
results = ModelWithManyAssociations.with_associated('foo bar')
|
118
179
|
results.map(&:title).should =~ included.map(&:title)
|
119
|
-
results.should_not include(
|
120
|
-
end
|
121
|
-
end
|
122
|
-
|
123
|
-
context "across multiple associations" do
|
124
|
-
context "on different tables" do
|
125
|
-
with_model :FirstAssociatedModel do
|
126
|
-
table do |t|
|
127
|
-
t.string 'title'
|
128
|
-
t.belongs_to 'model_with_many_associations'
|
129
|
-
end
|
130
|
-
model {}
|
131
|
-
end
|
132
|
-
|
133
|
-
with_model :SecondAssociatedModel do
|
134
|
-
table do |t|
|
135
|
-
t.string 'title'
|
136
|
-
end
|
137
|
-
model {}
|
138
|
-
end
|
139
|
-
|
140
|
-
with_model :ModelWithManyAssociations do
|
141
|
-
table do |t|
|
142
|
-
t.string 'title'
|
143
|
-
t.belongs_to 'model_of_second_type'
|
144
|
-
end
|
145
|
-
|
146
|
-
model do
|
147
|
-
include PgSearch
|
148
|
-
has_many :models_of_first_type, :class_name => 'FirstAssociatedModel', :foreign_key => 'model_with_many_associations_id'
|
149
|
-
belongs_to :model_of_second_type, :class_name => 'SecondAssociatedModel'
|
150
|
-
|
151
|
-
pg_search_scope :with_associated, :against => :title,
|
152
|
-
:associated_against => {:models_of_first_type => :title, :model_of_second_type => :title}
|
153
|
-
end
|
154
|
-
end
|
155
|
-
|
156
|
-
it "returns rows that match the query in either its own columns or the columns of the associated model" do
|
157
|
-
matching_second = SecondAssociatedModel.create!(:title => "foo bar")
|
158
|
-
unmatching_second = SecondAssociatedModel.create!(:title => "uiop")
|
159
|
-
|
160
|
-
included = [
|
161
|
-
ModelWithManyAssociations.create!(:title => 'abcdef', :models_of_first_type => [
|
162
|
-
FirstAssociatedModel.create!(:title => 'foo'),
|
163
|
-
FirstAssociatedModel.create!(:title => 'bar')
|
164
|
-
]),
|
165
|
-
ModelWithManyAssociations.create!(:title => 'ghijkl', :models_of_first_type => [
|
166
|
-
FirstAssociatedModel.create!(:title => 'foo bar'),
|
167
|
-
FirstAssociatedModel.create!(:title => 'mnopqr')
|
168
|
-
]),
|
169
|
-
ModelWithManyAssociations.create!(:title => 'foo bar'),
|
170
|
-
ModelWithManyAssociations.create!(:title => 'qwerty', :model_of_second_type => matching_second)
|
171
|
-
]
|
172
|
-
excluded = [
|
173
|
-
ModelWithManyAssociations.create!(:title => 'stuvwx', :models_of_first_type => [
|
174
|
-
FirstAssociatedModel.create!(:title => 'abcdef')
|
175
|
-
]),
|
176
|
-
ModelWithManyAssociations.create!(:title => 'qwerty', :model_of_second_type => unmatching_second)
|
177
|
-
]
|
178
|
-
|
179
|
-
results = ModelWithManyAssociations.with_associated('foo bar')
|
180
|
-
results.map(&:title).should =~ included.map(&:title)
|
181
|
-
excluded.each { |object| results.should_not include(object) }
|
182
|
-
end
|
183
|
-
end
|
184
|
-
|
185
|
-
context "on the same table" do
|
186
|
-
with_model :DoublyAssociatedModel do
|
187
|
-
table do |t|
|
188
|
-
t.string 'title'
|
189
|
-
t.belongs_to 'model_with_double_association'
|
190
|
-
t.belongs_to 'model_with_double_association_again'
|
191
|
-
end
|
192
|
-
model {}
|
193
|
-
end
|
194
|
-
|
195
|
-
with_model :model_with_double_association do
|
196
|
-
table do |t|
|
197
|
-
t.string 'title'
|
198
|
-
end
|
199
|
-
|
200
|
-
model do
|
201
|
-
include PgSearch
|
202
|
-
has_many :things, :class_name => 'DoublyAssociatedModel', :foreign_key => 'model_with_double_association_id'
|
203
|
-
has_many :thingamabobs, :class_name => 'DoublyAssociatedModel', :foreign_key => 'model_with_double_association_again_id'
|
204
|
-
|
205
|
-
pg_search_scope :with_associated, :against => :title,
|
206
|
-
:associated_against => {:things => :title, :thingamabobs => :title}
|
207
|
-
end
|
208
|
-
end
|
209
|
-
|
210
|
-
it "returns rows that match the query in either its own columns or the columns of the associated model" do
|
211
|
-
included = [
|
212
|
-
ModelWithDoubleAssociation.create!(:title => 'abcdef', :things => [
|
213
|
-
DoublyAssociatedModel.create!(:title => 'foo'),
|
214
|
-
DoublyAssociatedModel.create!(:title => 'bar')
|
215
|
-
]),
|
216
|
-
ModelWithDoubleAssociation.create!(:title => 'ghijkl', :things => [
|
217
|
-
DoublyAssociatedModel.create!(:title => 'foo bar'),
|
218
|
-
DoublyAssociatedModel.create!(:title => 'mnopqr')
|
219
|
-
]),
|
220
|
-
ModelWithDoubleAssociation.create!(:title => 'foo bar'),
|
221
|
-
ModelWithDoubleAssociation.create!(:title => 'qwerty', :thingamabobs => [
|
222
|
-
DoublyAssociatedModel.create!(:title => "foo bar")
|
223
|
-
])
|
224
|
-
]
|
225
|
-
excluded = [
|
226
|
-
ModelWithDoubleAssociation.create!(:title => 'stuvwx', :things => [
|
227
|
-
DoublyAssociatedModel.create!(:title => 'abcdef')
|
228
|
-
]),
|
229
|
-
ModelWithDoubleAssociation.create!(:title => 'qwerty', :thingamabobs => [
|
230
|
-
DoublyAssociatedModel.create!(:title => "uiop")
|
231
|
-
])
|
232
|
-
]
|
233
|
-
|
234
|
-
results = ModelWithDoubleAssociation.with_associated('foo bar')
|
235
|
-
results.map(&:title).should =~ included.map(&:title)
|
236
|
-
excluded.each { |object| results.should_not include(object) }
|
237
|
-
end
|
180
|
+
excluded.each { |object| results.should_not include(object) }
|
238
181
|
end
|
239
182
|
end
|
240
183
|
|
241
|
-
context "
|
242
|
-
with_model :
|
184
|
+
context "on the same table" do
|
185
|
+
with_model :DoublyAssociatedModel do
|
243
186
|
table do |t|
|
244
187
|
t.string 'title'
|
245
|
-
t.
|
188
|
+
t.belongs_to 'ModelWithDoubleAssociation'
|
189
|
+
t.belongs_to 'ModelWithDoubleAssociation_again'
|
246
190
|
end
|
191
|
+
model {}
|
247
192
|
end
|
248
193
|
|
249
|
-
with_model :
|
194
|
+
with_model :ModelWithDoubleAssociation do
|
250
195
|
table do |t|
|
251
|
-
t.
|
196
|
+
t.string 'title'
|
252
197
|
end
|
253
198
|
|
254
199
|
model do
|
255
200
|
include PgSearch
|
256
|
-
|
201
|
+
has_many :things, :class_name => 'DoublyAssociatedModel', :foreign_key => 'ModelWithDoubleAssociation_id'
|
202
|
+
has_many :thingamabobs, :class_name => 'DoublyAssociatedModel', :foreign_key => 'ModelWithDoubleAssociation_again_id'
|
257
203
|
|
258
|
-
pg_search_scope :with_associated, :
|
204
|
+
pg_search_scope :with_associated, :against => :title,
|
205
|
+
:associated_against => {:things => :title, :thingamabobs => :title}
|
259
206
|
end
|
260
207
|
end
|
261
208
|
|
262
|
-
it "
|
209
|
+
it "returns rows that match the query in either its own columns or the columns of the associated model" do
|
263
210
|
included = [
|
264
|
-
|
265
|
-
|
266
|
-
|
267
|
-
|
268
|
-
|
269
|
-
|
270
|
-
|
271
|
-
|
272
|
-
|
273
|
-
|
274
|
-
|
275
|
-
|
211
|
+
ModelWithDoubleAssociation.create!(:title => 'abcdef', :things => [
|
212
|
+
DoublyAssociatedModel.create!(:title => 'foo'),
|
213
|
+
DoublyAssociatedModel.create!(:title => 'bar')
|
214
|
+
]),
|
215
|
+
ModelWithDoubleAssociation.create!(:title => 'ghijkl', :things => [
|
216
|
+
DoublyAssociatedModel.create!(:title => 'foo bar'),
|
217
|
+
DoublyAssociatedModel.create!(:title => 'mnopqr')
|
218
|
+
]),
|
219
|
+
ModelWithDoubleAssociation.create!(:title => 'foo bar'),
|
220
|
+
ModelWithDoubleAssociation.create!(:title => 'qwerty', :thingamabobs => [
|
221
|
+
DoublyAssociatedModel.create!(:title => "foo bar")
|
222
|
+
])
|
276
223
|
]
|
277
224
|
excluded = [
|
278
|
-
|
279
|
-
|
280
|
-
|
281
|
-
|
282
|
-
|
283
|
-
|
225
|
+
ModelWithDoubleAssociation.create!(:title => 'stuvwx', :things => [
|
226
|
+
DoublyAssociatedModel.create!(:title => 'abcdef')
|
227
|
+
]),
|
228
|
+
ModelWithDoubleAssociation.create!(:title => 'qwerty', :thingamabobs => [
|
229
|
+
DoublyAssociatedModel.create!(:title => "uiop")
|
230
|
+
])
|
284
231
|
]
|
285
232
|
|
286
|
-
results =
|
287
|
-
|
288
|
-
results.to_sql.scan("INNER JOIN").length.should == 1
|
289
|
-
included.each { |object| results.should include(object) }
|
233
|
+
results = ModelWithDoubleAssociation.with_associated('foo bar')
|
234
|
+
results.map(&:title).should =~ included.map(&:title)
|
290
235
|
excluded.each { |object| results.should_not include(object) }
|
291
236
|
end
|
292
|
-
|
293
237
|
end
|
294
238
|
end
|
295
|
-
|
296
|
-
context "
|
297
|
-
with_model :
|
239
|
+
|
240
|
+
context "against multiple attributes on one association" do
|
241
|
+
with_model :AssociatedModel do
|
298
242
|
table do |t|
|
299
243
|
t.string 'title'
|
244
|
+
t.text 'author'
|
245
|
+
end
|
246
|
+
end
|
247
|
+
|
248
|
+
with_model :ModelWithAssociation do
|
249
|
+
table do |t|
|
250
|
+
t.belongs_to 'another_model'
|
300
251
|
end
|
301
252
|
|
302
253
|
model do
|
303
254
|
include PgSearch
|
304
|
-
|
255
|
+
belongs_to :another_model, :class_name => 'AssociatedModel'
|
256
|
+
|
257
|
+
pg_search_scope :with_associated, :associated_against => {:another_model => [:title, :author]}
|
305
258
|
end
|
306
259
|
end
|
307
260
|
|
308
|
-
it "should
|
309
|
-
|
310
|
-
|
311
|
-
|
261
|
+
it "should only do one join" do
|
262
|
+
included = [
|
263
|
+
ModelWithAssociation.create!(
|
264
|
+
:another_model => AssociatedModel.create!(
|
265
|
+
:title => "foo",
|
266
|
+
:author => "bar"
|
267
|
+
)
|
268
|
+
),
|
269
|
+
ModelWithAssociation.create!(
|
270
|
+
:another_model => AssociatedModel.create!(
|
271
|
+
:title => "foo bar",
|
272
|
+
:author => "baz"
|
273
|
+
)
|
274
|
+
)
|
275
|
+
]
|
276
|
+
excluded = [
|
277
|
+
ModelWithAssociation.create!(
|
278
|
+
:another_model => AssociatedModel.create!(
|
279
|
+
:title => "foo",
|
280
|
+
:author => "baz"
|
281
|
+
)
|
282
|
+
)
|
283
|
+
]
|
284
|
+
|
285
|
+
results = ModelWithAssociation.with_associated('foo bar')
|
286
|
+
|
287
|
+
results.to_sql.scan("INNER JOIN").length.should == 1
|
288
|
+
included.each { |object| results.should include(object) }
|
289
|
+
excluded.each { |object| results.should_not include(object) }
|
312
290
|
end
|
291
|
+
|
313
292
|
end
|
314
293
|
end
|
315
294
|
end
|