pg_search 0.5.1 → 0.5.2
Sign up to get free protection for your applications and to get access to all the features.
- data/.travis.yml +2 -0
- data/CHANGELOG.rdoc +4 -0
- data/LICENSE +1 -1
- data/lib/pg_search.rb +24 -15
- data/lib/pg_search/configuration.rb +24 -16
- data/lib/pg_search/configuration/association.rb +18 -8
- data/lib/pg_search/configuration/column.rb +17 -16
- data/lib/pg_search/configuration/foreign_column.rb +28 -0
- data/lib/pg_search/document.rb +3 -3
- data/lib/pg_search/features.rb +5 -3
- data/lib/pg_search/features/dmetaphone.rb +10 -7
- data/lib/pg_search/features/feature.rb +28 -0
- data/lib/pg_search/features/trigram.rb +9 -19
- data/lib/pg_search/features/tsearch.rb +16 -18
- data/lib/pg_search/multisearch.rb +2 -44
- data/lib/pg_search/multisearch/rebuilder.rb +75 -0
- data/lib/pg_search/multisearchable.rb +5 -2
- data/lib/pg_search/normalizer.rb +6 -6
- data/lib/pg_search/scope.rb +11 -15
- data/lib/pg_search/scope_options.rb +17 -17
- data/lib/pg_search/version.rb +1 -1
- data/spec/pg_search/multisearch_spec.rb +31 -21
- data/spec/spec_helper.rb +12 -8
- metadata +7 -4
data/.travis.yml
CHANGED
data/CHANGELOG.rdoc
CHANGED
data/LICENSE
CHANGED
data/lib/pg_search.rb
CHANGED
@@ -3,6 +3,16 @@ require "active_support/concern"
|
|
3
3
|
require "active_support/core_ext/module/attribute_accessors"
|
4
4
|
|
5
5
|
module PgSearch
|
6
|
+
autoload :Configuration, "pg_search/configuration"
|
7
|
+
autoload :Document, "pg_search/document"
|
8
|
+
autoload :Features, "pg_search/features"
|
9
|
+
autoload :Multisearch, "pg_search/multisearch"
|
10
|
+
autoload :Multisearchable, "pg_search/multisearchable"
|
11
|
+
autoload :Normalizer, "pg_search/normalizer"
|
12
|
+
autoload :Scope, "pg_search/scope"
|
13
|
+
autoload :ScopeOptions, "pg_search/scope_options"
|
14
|
+
autoload :Version, "pg_search/version"
|
15
|
+
|
6
16
|
extend ActiveSupport::Concern
|
7
17
|
|
8
18
|
mattr_accessor :multisearch_options
|
@@ -10,10 +20,15 @@ module PgSearch
|
|
10
20
|
|
11
21
|
module ClassMethods
|
12
22
|
def pg_search_scope(name, options)
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
23
|
+
scope = PgSearch::Scope.new(name, self, options)
|
24
|
+
|
25
|
+
method_proc = scope.method(:build_relation)
|
26
|
+
|
27
|
+
if respond_to?(:define_singleton_method)
|
28
|
+
define_singleton_method name, &method_proc
|
29
|
+
else
|
30
|
+
(class << self; self; end).send :define_method, name, &method_proc
|
31
|
+
end
|
17
32
|
end
|
18
33
|
|
19
34
|
def multisearchable(options = {})
|
@@ -40,21 +55,15 @@ module PgSearch
|
|
40
55
|
end
|
41
56
|
|
42
57
|
def multisearch_enabled?
|
43
|
-
Thread.current.key?("PgSearch.enable_multisearch")
|
58
|
+
if Thread.current.key?("PgSearch.enable_multisearch")
|
59
|
+
Thread.current["PgSearch.enable_multisearch"]
|
60
|
+
else
|
61
|
+
true
|
62
|
+
end
|
44
63
|
end
|
45
64
|
end
|
46
65
|
|
47
66
|
class NotSupportedForPostgresqlVersion < StandardError; end
|
48
67
|
end
|
49
68
|
|
50
|
-
require "pg_search/configuration"
|
51
|
-
require "pg_search/document"
|
52
|
-
require "pg_search/features"
|
53
|
-
require "pg_search/multisearch"
|
54
|
-
require "pg_search/multisearchable"
|
55
|
-
require "pg_search/normalizer"
|
56
|
-
require "pg_search/scope"
|
57
|
-
require "pg_search/scope_options"
|
58
|
-
require "pg_search/version"
|
59
|
-
|
60
69
|
require "pg_search/railtie" if defined?(Rails)
|
@@ -1,13 +1,16 @@
|
|
1
|
-
require "pg_search/configuration/association"
|
2
|
-
require "pg_search/configuration/column"
|
3
|
-
|
4
1
|
module PgSearch
|
5
2
|
class Configuration
|
3
|
+
autoload :Association, "pg_search/configuration/association"
|
4
|
+
autoload :Column, "pg_search/configuration/column"
|
5
|
+
autoload :ForeignColumn, "pg_search/configuration/foreign_column"
|
6
|
+
|
7
|
+
attr_reader :model
|
8
|
+
|
6
9
|
def initialize(options, model)
|
7
|
-
options =
|
8
|
-
assert_valid_options(options)
|
9
|
-
@options = options
|
10
|
+
@options = default_options.merge(options)
|
10
11
|
@model = model
|
12
|
+
|
13
|
+
assert_valid_options(@options)
|
11
14
|
end
|
12
15
|
|
13
16
|
class << self
|
@@ -32,8 +35,7 @@ module PgSearch
|
|
32
35
|
def associations
|
33
36
|
return [] unless @options[:associated_against]
|
34
37
|
@options[:associated_against].map do |association, column_names|
|
35
|
-
|
36
|
-
association
|
38
|
+
Association.new(@model, association, column_names)
|
37
39
|
end.flatten
|
38
40
|
end
|
39
41
|
|
@@ -75,20 +77,26 @@ module PgSearch
|
|
75
77
|
{:using => :tsearch}
|
76
78
|
end
|
77
79
|
|
78
|
-
|
79
|
-
|
80
|
-
|
81
|
-
:ignoring => [:accents]
|
82
|
-
}
|
80
|
+
VALID_KEYS = %w[
|
81
|
+
against ranked_by ignoring using query associated_against order_within_rank
|
82
|
+
].map(&:to_sym)
|
83
83
|
|
84
|
+
VALID_VALUES = {
|
85
|
+
:ignoring => [:accents]
|
86
|
+
}
|
87
|
+
|
88
|
+
def assert_valid_options(options)
|
84
89
|
unless options[:against] || options[:associated_against]
|
85
90
|
raise ArgumentError, "the search scope #{@name} must have :against#{" or :associated_against" if defined?(ActiveRecord::Relation)} in its options"
|
86
91
|
end
|
87
|
-
raise ArgumentError, ":associated_against requires ActiveRecord 3 or later" if options[:associated_against] && !defined?(ActiveRecord::Relation)
|
88
92
|
|
89
|
-
options
|
93
|
+
if options[:associated_against] && !defined?(ActiveRecord::Relation)
|
94
|
+
raise ArgumentError, ":associated_against requires Active Record 3 or later"
|
95
|
+
end
|
96
|
+
|
97
|
+
options.assert_valid_keys(VALID_KEYS)
|
90
98
|
|
91
|
-
|
99
|
+
VALID_VALUES.each do |key, values_for_key|
|
92
100
|
Array.wrap(options[key]).each do |value|
|
93
101
|
unless values_for_key.include?(value)
|
94
102
|
raise ArgumentError, ":#{key} cannot accept #{value}"
|
@@ -9,7 +9,7 @@ module PgSearch
|
|
9
9
|
@model = model
|
10
10
|
@name = name
|
11
11
|
@columns = Array(column_names).map do |column_name, weight|
|
12
|
-
|
12
|
+
ForeignColumn.new(column_name, weight, @model, self)
|
13
13
|
end
|
14
14
|
end
|
15
15
|
|
@@ -18,20 +18,30 @@ module PgSearch
|
|
18
18
|
end
|
19
19
|
|
20
20
|
def join(primary_key)
|
21
|
-
|
22
|
-
|
21
|
+
"LEFT OUTER JOIN (#{relation(primary_key).to_sql}) #{subselect_alias} ON #{subselect_alias}.id = #{primary_key}"
|
22
|
+
end
|
23
|
+
|
24
|
+
def subselect_alias
|
25
|
+
Configuration.alias(table_name, @name, "subselect")
|
26
|
+
end
|
27
|
+
|
28
|
+
private
|
29
|
+
|
30
|
+
def selects
|
31
|
+
postgresql_version = @model.connection.send(:postgresql_version)
|
32
|
+
|
33
|
+
columns.map do |column|
|
34
|
+
case postgresql_version
|
23
35
|
when 0..90000
|
24
|
-
"array_to_string(array_agg(#{column.full_name}), ' ') AS #{column.alias}"
|
36
|
+
"array_to_string(array_agg(#{column.full_name}::text), ' ') AS #{column.alias}"
|
25
37
|
else
|
26
38
|
"string_agg(#{column.full_name}::text, ' ') AS #{column.alias}"
|
27
39
|
end
|
28
40
|
end.join(", ")
|
29
|
-
relation = @model.joins(@name).select("#{primary_key} AS id, #{selects}").group(primary_key)
|
30
|
-
"LEFT OUTER JOIN (#{relation.to_sql}) #{subselect_alias} ON #{subselect_alias}.id = #{primary_key}"
|
31
41
|
end
|
32
42
|
|
33
|
-
def
|
34
|
-
|
43
|
+
def relation(primary_key)
|
44
|
+
@model.joins(@name).select("#{primary_key} AS id, #{selects}").group(primary_key)
|
35
45
|
end
|
36
46
|
end
|
37
47
|
end
|
@@ -3,34 +3,35 @@ require 'digest'
|
|
3
3
|
module PgSearch
|
4
4
|
class Configuration
|
5
5
|
class Column
|
6
|
-
attr_reader :weight
|
6
|
+
attr_reader :weight
|
7
7
|
|
8
|
-
def initialize(column_name, weight, model
|
8
|
+
def initialize(column_name, weight, model)
|
9
9
|
@column_name = column_name.to_s
|
10
10
|
@weight = weight
|
11
11
|
@model = model
|
12
|
-
@
|
13
|
-
end
|
14
|
-
|
15
|
-
def table
|
16
|
-
foreign? ? @association.table_name : @model.table_name
|
12
|
+
@connection = model.connection
|
17
13
|
end
|
18
14
|
|
19
15
|
def full_name
|
20
|
-
"#{
|
16
|
+
"#{table_name}.#{column_name}"
|
21
17
|
end
|
22
18
|
|
23
19
|
def to_sql
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
20
|
+
"coalesce(#{expression}::text, '')"
|
21
|
+
end
|
22
|
+
|
23
|
+
private
|
24
|
+
|
25
|
+
def table_name
|
26
|
+
@connection.quote_table_name(@model.table_name)
|
27
|
+
end
|
28
|
+
|
29
|
+
def column_name
|
30
|
+
@connection.quote_column_name(@column_name)
|
30
31
|
end
|
31
32
|
|
32
|
-
def
|
33
|
-
|
33
|
+
def expression
|
34
|
+
full_name
|
34
35
|
end
|
35
36
|
|
36
37
|
def alias
|
@@ -0,0 +1,28 @@
|
|
1
|
+
require 'digest'
|
2
|
+
|
3
|
+
module PgSearch
|
4
|
+
class Configuration
|
5
|
+
class ForeignColumn < Column
|
6
|
+
attr_reader :weight
|
7
|
+
|
8
|
+
def initialize(column_name, weight, model, association)
|
9
|
+
super(column_name, weight, model)
|
10
|
+
@association = association
|
11
|
+
end
|
12
|
+
|
13
|
+
def alias
|
14
|
+
Configuration.alias(@association.subselect_alias, @column_name)
|
15
|
+
end
|
16
|
+
|
17
|
+
private
|
18
|
+
|
19
|
+
def expression
|
20
|
+
"#{@association.subselect_alias}.#{self.alias}"
|
21
|
+
end
|
22
|
+
|
23
|
+
def table_name
|
24
|
+
@connection.quote_table_name(@association.table_name)
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
data/lib/pg_search/document.rb
CHANGED
@@ -1,5 +1,4 @@
|
|
1
1
|
require "logger"
|
2
|
-
require "pg_search/scope"
|
3
2
|
|
4
3
|
module PgSearch
|
5
4
|
class Document < ActiveRecord::Base
|
@@ -19,9 +18,10 @@ module PgSearch
|
|
19
18
|
options = if PgSearch.multisearch_options.respond_to?(:call)
|
20
19
|
PgSearch.multisearch_options.call(*args)
|
21
20
|
else
|
22
|
-
|
21
|
+
{:query => args.first}.merge(PgSearch.multisearch_options)
|
23
22
|
end
|
24
|
-
|
23
|
+
|
24
|
+
{:against => :content}.merge(options)
|
25
25
|
}
|
26
26
|
|
27
27
|
private
|
data/lib/pg_search/features.rb
CHANGED
@@ -1,7 +1,9 @@
|
|
1
1
|
module PgSearch
|
2
2
|
module Features
|
3
|
+
autoload :Feature, "pg_search/features/feature"
|
4
|
+
|
5
|
+
autoload :DMetaphone, "pg_search/features/dmetaphone"
|
6
|
+
autoload :Trigram, "pg_search/features/trigram"
|
7
|
+
autoload :TSearch, "pg_search/features/tsearch"
|
3
8
|
end
|
4
9
|
end
|
5
|
-
require 'pg_search/features/dmetaphone'
|
6
|
-
require 'pg_search/features/trigram'
|
7
|
-
require 'pg_search/features/tsearch'
|
@@ -1,15 +1,18 @@
|
|
1
|
-
require "active_support/core_ext/module/delegation"
|
2
|
-
|
3
1
|
module PgSearch
|
4
2
|
module Features
|
5
3
|
class DMetaphone
|
6
|
-
|
7
|
-
|
8
|
-
# config is temporary as we refactor
|
9
|
-
def initialize(query, options, config, model, normalizer)
|
4
|
+
def initialize(query, options, columns, model, normalizer)
|
10
5
|
dmetaphone_normalizer = Normalizer.new(normalizer)
|
11
6
|
options = (options || {}).merge(:dictionary => 'simple')
|
12
|
-
@tsearch = TSearch.new(query, options,
|
7
|
+
@tsearch = TSearch.new(query, options, columns, model, dmetaphone_normalizer)
|
8
|
+
end
|
9
|
+
|
10
|
+
def conditions
|
11
|
+
@tsearch.conditions
|
12
|
+
end
|
13
|
+
|
14
|
+
def rank
|
15
|
+
@tsearch.rank
|
13
16
|
end
|
14
17
|
|
15
18
|
# Decorates a normalizer with dmetaphone processing.
|
@@ -0,0 +1,28 @@
|
|
1
|
+
module PgSearch
|
2
|
+
module Features
|
3
|
+
class Feature
|
4
|
+
def initialize(query, options, columns, model, normalizer)
|
5
|
+
@query = query
|
6
|
+
@options = options || {}
|
7
|
+
@columns = columns
|
8
|
+
@model = model
|
9
|
+
@normalizer = normalizer
|
10
|
+
end
|
11
|
+
|
12
|
+
private
|
13
|
+
|
14
|
+
def document
|
15
|
+
if @columns.length == 1
|
16
|
+
@columns.first.to_sql
|
17
|
+
else
|
18
|
+
expressions = @columns.map { |column| column.to_sql }.join(", ")
|
19
|
+
"array_to_string(ARRAY[#{expressions}], ' ')"
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
def normalize(expression)
|
24
|
+
@normalizer.add_normalization(expression)
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
@@ -1,28 +1,18 @@
|
|
1
|
-
require "active_support/core_ext/module/delegation"
|
2
|
-
|
3
1
|
module PgSearch
|
4
2
|
module Features
|
5
|
-
class Trigram
|
6
|
-
def initialize(query, options, columns, model, normalizer)
|
7
|
-
@query = query
|
8
|
-
@options = options
|
9
|
-
@columns = columns
|
10
|
-
@model = model
|
11
|
-
@normalizer = normalizer
|
12
|
-
end
|
13
|
-
|
3
|
+
class Trigram < Feature
|
14
4
|
def conditions
|
15
|
-
[
|
5
|
+
[
|
6
|
+
"(#{normalize(document)}) % #{normalize(":query")}",
|
7
|
+
{:query => @query}
|
8
|
+
]
|
16
9
|
end
|
17
10
|
|
18
11
|
def rank
|
19
|
-
[
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
def document
|
25
|
-
@columns.map { |column| column.to_sql }.join(" || ' ' || ")
|
12
|
+
[
|
13
|
+
"similarity((#{normalize(document)}), #{normalize(":query")})",
|
14
|
+
{:query => @query}
|
15
|
+
]
|
26
16
|
end
|
27
17
|
end
|
28
18
|
end
|
@@ -2,15 +2,11 @@ require "active_support/core_ext/module/delegation"
|
|
2
2
|
|
3
3
|
module PgSearch
|
4
4
|
module Features
|
5
|
-
class TSearch
|
5
|
+
class TSearch < Feature
|
6
6
|
delegate :connection, :quoted_table_name, :to => :'@model'
|
7
7
|
|
8
8
|
def initialize(query, options, columns, model, normalizer)
|
9
|
-
|
10
|
-
@options = options || {}
|
11
|
-
@model = model
|
12
|
-
@columns = columns
|
13
|
-
@normalizer = normalizer
|
9
|
+
super
|
14
10
|
|
15
11
|
if @options[:prefix] && @model.connection.send(:postgresql_version) < 80400
|
16
12
|
raise PgSearch::NotSupportedForPostgresqlVersion.new(<<-MESSAGE.gsub /^\s*/, '')
|
@@ -33,24 +29,20 @@ module PgSearch
|
|
33
29
|
{:query => @query.to_s, :dictionary => dictionary.to_s}
|
34
30
|
end
|
35
31
|
|
36
|
-
def document
|
37
|
-
@columns.map { |column| column.to_sql }.join(" || ' ' || ")
|
38
|
-
end
|
39
|
-
|
40
32
|
DISALLOWED_TSQUERY_CHARACTERS = /['?\\:]/
|
41
33
|
|
42
34
|
def tsquery_for_term(term)
|
43
35
|
sanitized_term = term.gsub(DISALLOWED_TSQUERY_CHARACTERS, " ")
|
44
36
|
|
45
|
-
term_sql =
|
37
|
+
term_sql = normalize(connection.quote(sanitized_term))
|
46
38
|
|
47
39
|
# After this, the SQL expression evaluates to a string containing the term surrounded by single-quotes.
|
48
40
|
# If :prefix is true, then the term will also have :* appended to the end.
|
49
41
|
tsquery_sql = [
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
42
|
+
connection.quote("' "),
|
43
|
+
term_sql,
|
44
|
+
connection.quote(" '"),
|
45
|
+
(connection.quote(':*') if @options[:prefix])
|
54
46
|
].compact.join(" || ")
|
55
47
|
|
56
48
|
"to_tsquery(:dictionary, #{tsquery_sql})"
|
@@ -58,7 +50,9 @@ module PgSearch
|
|
58
50
|
|
59
51
|
def tsquery
|
60
52
|
return "''" if @query.blank?
|
61
|
-
@query.split(" ").compact
|
53
|
+
query_terms = @query.split(" ").compact
|
54
|
+
tsquery_terms = query_terms.map { |term| tsquery_for_term(term) }
|
55
|
+
tsquery_terms.join(@options[:any_word] ? ' || ' : ' && ')
|
62
56
|
end
|
63
57
|
|
64
58
|
def tsdocument
|
@@ -67,8 +61,12 @@ module PgSearch
|
|
67
61
|
"#{quoted_table_name}.#{column_name}"
|
68
62
|
else
|
69
63
|
@columns.map do |search_column|
|
70
|
-
tsvector = "to_tsvector(:dictionary, #{
|
71
|
-
search_column.weight.nil?
|
64
|
+
tsvector = "to_tsvector(:dictionary, #{normalize(search_column.to_sql)})"
|
65
|
+
if search_column.weight.nil?
|
66
|
+
tsvector
|
67
|
+
else
|
68
|
+
"setweight(#{tsvector}, #{connection.quote(search_column.weight)})"
|
69
|
+
end
|
72
70
|
end.join(" || ")
|
73
71
|
end
|
74
72
|
end
|
@@ -1,56 +1,14 @@
|
|
1
1
|
module PgSearch
|
2
2
|
module Multisearch
|
3
|
-
|
4
|
-
INSERT INTO :documents_table (searchable_type, searchable_id, content, created_at, updated_at)
|
5
|
-
SELECT :model_name AS searchable_type,
|
6
|
-
:model_table.id AS searchable_id,
|
7
|
-
(
|
8
|
-
:content_expressions
|
9
|
-
) AS content,
|
10
|
-
:current_time AS created_at,
|
11
|
-
:current_time AS updated_at
|
12
|
-
FROM :model_table
|
13
|
-
SQL
|
3
|
+
autoload :Rebuilder, "pg_search/multisearch/rebuilder"
|
14
4
|
|
15
5
|
class << self
|
16
6
|
def rebuild(model, clean_up=true)
|
17
7
|
model.transaction do
|
18
8
|
PgSearch::Document.where(:searchable_type => model.name).delete_all if clean_up
|
19
|
-
|
20
|
-
model.rebuild_pg_search_documents
|
21
|
-
else
|
22
|
-
model.connection.execute(rebuild_sql(model))
|
23
|
-
end
|
9
|
+
Rebuilder.new(model).rebuild
|
24
10
|
end
|
25
11
|
end
|
26
|
-
|
27
|
-
def rebuild_sql(model)
|
28
|
-
connection = model.connection
|
29
|
-
|
30
|
-
unless model.respond_to?(:pg_search_multisearchable_options)
|
31
|
-
raise ModelNotMultisearchable.new(model)
|
32
|
-
end
|
33
|
-
|
34
|
-
columns = Array.wrap(
|
35
|
-
model.pg_search_multisearchable_options[:against]
|
36
|
-
)
|
37
|
-
|
38
|
-
content_expressions = columns.map { |column|
|
39
|
-
%Q{coalesce(:model_table.#{column}::text, '')}
|
40
|
-
}.join(" || ' ' || ")
|
41
|
-
|
42
|
-
REBUILD_SQL_TEMPLATE.gsub(
|
43
|
-
":content_expressions", content_expressions
|
44
|
-
).gsub(
|
45
|
-
":model_name", connection.quote(model.name)
|
46
|
-
).gsub(
|
47
|
-
":model_table", model.quoted_table_name
|
48
|
-
).gsub(
|
49
|
-
":documents_table", PgSearch::Document.quoted_table_name
|
50
|
-
).gsub(
|
51
|
-
":current_time", connection.quote(connection.quoted_date(Time.now))
|
52
|
-
)
|
53
|
-
end
|
54
12
|
end
|
55
13
|
|
56
14
|
class ModelNotMultisearchable < StandardError
|
@@ -0,0 +1,75 @@
|
|
1
|
+
module PgSearch
|
2
|
+
module Multisearch
|
3
|
+
class Rebuilder
|
4
|
+
REBUILD_SQL_TEMPLATE = <<-SQL
|
5
|
+
INSERT INTO :documents_table (searchable_type, searchable_id, content, created_at, updated_at)
|
6
|
+
SELECT :model_name AS searchable_type,
|
7
|
+
:model_table.id AS searchable_id,
|
8
|
+
(
|
9
|
+
:content_expressions
|
10
|
+
) AS content,
|
11
|
+
:current_time AS created_at,
|
12
|
+
:current_time AS updated_at
|
13
|
+
FROM :model_table
|
14
|
+
SQL
|
15
|
+
|
16
|
+
def initialize(model)
|
17
|
+
unless model.respond_to?(:pg_search_multisearchable_options)
|
18
|
+
raise ModelNotMultisearchable.new(model)
|
19
|
+
end
|
20
|
+
|
21
|
+
@model = model
|
22
|
+
end
|
23
|
+
|
24
|
+
def rebuild
|
25
|
+
if @model.respond_to?(:rebuild_pg_search_documents)
|
26
|
+
@model.rebuild_pg_search_documents
|
27
|
+
else
|
28
|
+
@model.connection.execute(rebuild_sql)
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
private
|
33
|
+
|
34
|
+
def connection
|
35
|
+
@model.connection
|
36
|
+
end
|
37
|
+
|
38
|
+
def rebuild_sql
|
39
|
+
replacements.inject(REBUILD_SQL_TEMPLATE) do |sql, key|
|
40
|
+
sql.gsub ":#{key}", send(key)
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
def replacements
|
45
|
+
%w[content_expressions model_name model_table documents_table current_time]
|
46
|
+
end
|
47
|
+
|
48
|
+
def content_expressions
|
49
|
+
columns.map { |column|
|
50
|
+
%Q{coalesce(:model_table.#{column}::text, '')}
|
51
|
+
}.join(" || ' ' || ")
|
52
|
+
end
|
53
|
+
|
54
|
+
def columns
|
55
|
+
Array.wrap(@model.pg_search_multisearchable_options[:against])
|
56
|
+
end
|
57
|
+
|
58
|
+
def model_name
|
59
|
+
connection.quote(@model.name)
|
60
|
+
end
|
61
|
+
|
62
|
+
def model_table
|
63
|
+
@model.quoted_table_name
|
64
|
+
end
|
65
|
+
|
66
|
+
def documents_table
|
67
|
+
PgSearch::Document.quoted_table_name
|
68
|
+
end
|
69
|
+
|
70
|
+
def current_time
|
71
|
+
connection.quote(connection.quoted_date(Time.now))
|
72
|
+
end
|
73
|
+
end
|
74
|
+
end
|
75
|
+
end
|
@@ -19,8 +19,11 @@ module PgSearch
|
|
19
19
|
end
|
20
20
|
|
21
21
|
def update_pg_search_document
|
22
|
-
|
23
|
-
|
22
|
+
if self.pg_search_document
|
23
|
+
self.pg_search_document.save
|
24
|
+
else
|
25
|
+
create_pg_search_document
|
26
|
+
end
|
24
27
|
end
|
25
28
|
end
|
26
29
|
end
|
data/lib/pg_search/normalizer.rb
CHANGED
@@ -4,19 +4,19 @@ module PgSearch
|
|
4
4
|
@config = config
|
5
5
|
end
|
6
6
|
|
7
|
-
def add_normalization(
|
8
|
-
normalized_sql = original_sql
|
7
|
+
def add_normalization(sql_expression)
|
9
8
|
if @config.ignore.include?(:accents)
|
10
9
|
if @config.postgresql_version < 90000
|
11
10
|
raise PgSearch::NotSupportedForPostgresqlVersion.new(<<-MESSAGE.gsub /^\s*/, '')
|
12
|
-
|
13
|
-
|
11
|
+
Sorry, {:ignoring => :accents} only works in PostgreSQL 9.0 and above.
|
12
|
+
#{@config.inspect}
|
14
13
|
MESSAGE
|
15
14
|
else
|
16
|
-
|
15
|
+
"unaccent(#{sql_expression})"
|
17
16
|
end
|
17
|
+
else
|
18
|
+
sql_expression
|
18
19
|
end
|
19
|
-
normalized_sql
|
20
20
|
end
|
21
21
|
end
|
22
22
|
end
|
data/lib/pg_search/scope.rb
CHANGED
@@ -6,26 +6,22 @@ module PgSearch
|
|
6
6
|
@options_proc = build_options_proc(scope_options_or_proc)
|
7
7
|
end
|
8
8
|
|
9
|
-
def
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
}
|
9
|
+
def build_relation(*args)
|
10
|
+
config = Configuration.new(@options_proc.call(*args), @model)
|
11
|
+
scope_options = ScopeOptions.new(@name, config)
|
12
|
+
scope_options.apply(@model)
|
14
13
|
end
|
15
14
|
|
16
15
|
private
|
17
16
|
|
18
|
-
def build_options_proc(
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
lambda { |query|
|
24
|
-
scope_options_or_proc.reverse_merge(:query => query)
|
25
|
-
}
|
26
|
-
else
|
27
|
-
raise ArgumentError, "A PgSearch scope expects a Proc or Hash for its options"
|
17
|
+
def build_options_proc(scope_options)
|
18
|
+
return scope_options if scope_options.respond_to?(:call)
|
19
|
+
|
20
|
+
unless scope_options.respond_to?(:merge)
|
21
|
+
raise ArgumentError, "pg_search_scope expects a Hash or Proc"
|
28
22
|
end
|
23
|
+
|
24
|
+
lambda { |query| {:query => query}.merge(scope_options) }
|
29
25
|
end
|
30
26
|
end
|
31
27
|
end
|
@@ -2,31 +2,25 @@ require "active_support/core_ext/module/delegation"
|
|
2
2
|
|
3
3
|
module PgSearch
|
4
4
|
class ScopeOptions
|
5
|
-
|
5
|
+
delegate :connection, :quoted_table_name, :sanitize_sql_array, :to => :@model
|
6
6
|
|
7
|
-
|
8
|
-
|
9
|
-
def initialize(name, model, config)
|
7
|
+
def initialize(name, config)
|
10
8
|
@name = name
|
11
|
-
@model = model
|
12
9
|
@config = config
|
13
|
-
|
14
|
-
@feature_options =
|
15
|
-
features_hash.merge(
|
16
|
-
feature_name => feature_options
|
17
|
-
)
|
18
|
-
end
|
19
|
-
@feature_names = @config.features.map { |feature_name, feature_options| feature_name }
|
10
|
+
@model = config.model
|
11
|
+
@feature_options = Hash[config.features]
|
20
12
|
end
|
21
13
|
|
22
|
-
def
|
23
|
-
|
14
|
+
def apply(scope)
|
15
|
+
scope.select("#{quoted_table_name}.*, (#{rank}) AS pg_search_rank").where(conditions).order("pg_search_rank DESC, #{order_within_rank}").joins(joins)
|
24
16
|
end
|
25
17
|
|
26
18
|
private
|
27
19
|
|
28
20
|
def conditions
|
29
|
-
@
|
21
|
+
@config.features.map do |feature_name, feature_options|
|
22
|
+
"(#{sanitize_sql_array(feature_for(feature_name).conditions)})"
|
23
|
+
end.join(" OR ")
|
30
24
|
end
|
31
25
|
|
32
26
|
def order_within_rank
|
@@ -34,7 +28,7 @@ module PgSearch
|
|
34
28
|
end
|
35
29
|
|
36
30
|
def primary_key
|
37
|
-
"#{quoted_table_name}.#{connection.quote_column_name(model.primary_key)}"
|
31
|
+
"#{quoted_table_name}.#{connection.quote_column_name(@model.primary_key)}"
|
38
32
|
end
|
39
33
|
|
40
34
|
def joins
|
@@ -57,7 +51,13 @@ module PgSearch
|
|
57
51
|
|
58
52
|
normalizer = Normalizer.new(@config)
|
59
53
|
|
60
|
-
feature_class.new(
|
54
|
+
feature_class.new(
|
55
|
+
@config.query,
|
56
|
+
@feature_options[feature_name],
|
57
|
+
@config.columns,
|
58
|
+
@config.model,
|
59
|
+
normalizer
|
60
|
+
)
|
61
61
|
end
|
62
62
|
|
63
63
|
def rank
|
data/lib/pg_search/version.rb
CHANGED
@@ -105,22 +105,21 @@ describe PgSearch::Multisearch do
|
|
105
105
|
PgSearch::Document.last(2).map(&:searchable).map(&:title).should =~ new_models.map(&:title)
|
106
106
|
end
|
107
107
|
end
|
108
|
-
end
|
109
|
-
|
110
|
-
describe ".rebuild_sql" do
|
111
|
-
let(:now) { Time.now }
|
112
108
|
|
113
|
-
|
114
|
-
|
115
|
-
end
|
109
|
+
describe "the generated SQL" do
|
110
|
+
let(:now) { Time.now }
|
116
111
|
|
117
|
-
context "with one attribute" do
|
118
112
|
before do
|
119
|
-
|
113
|
+
Time.stub(:now => now)
|
120
114
|
end
|
121
115
|
|
122
|
-
|
123
|
-
|
116
|
+
context "with one attribute" do
|
117
|
+
before do
|
118
|
+
model.multisearchable :against => [:title]
|
119
|
+
end
|
120
|
+
|
121
|
+
it "should generate the proper SQL code" do
|
122
|
+
expected_sql = <<-SQL
|
124
123
|
INSERT INTO #{PgSearch::Document.quoted_table_name} (searchable_type, searchable_id, content, created_at, updated_at)
|
125
124
|
SELECT #{connection.quote(model.name)} AS searchable_type,
|
126
125
|
#{model.quoted_table_name}.id AS searchable_id,
|
@@ -132,17 +131,22 @@ INSERT INTO #{PgSearch::Document.quoted_table_name} (searchable_type, searchable
|
|
132
131
|
FROM #{model.quoted_table_name}
|
133
132
|
SQL
|
134
133
|
|
135
|
-
|
136
|
-
|
137
|
-
end
|
134
|
+
statements = []
|
135
|
+
connection.stub(:execute) { |sql| statements << sql }
|
138
136
|
|
139
|
-
|
140
|
-
|
141
|
-
|
137
|
+
PgSearch::Multisearch.rebuild(model)
|
138
|
+
|
139
|
+
statements.should include(expected_sql)
|
140
|
+
end
|
142
141
|
end
|
143
142
|
|
144
|
-
|
145
|
-
|
143
|
+
context "with multiple attributes" do
|
144
|
+
before do
|
145
|
+
model.multisearchable :against => [:title, :content]
|
146
|
+
end
|
147
|
+
|
148
|
+
it "should generate the proper SQL code" do
|
149
|
+
expected_sql = <<-SQL
|
146
150
|
INSERT INTO #{PgSearch::Document.quoted_table_name} (searchable_type, searchable_id, content, created_at, updated_at)
|
147
151
|
SELECT #{connection.quote(model.name)} AS searchable_type,
|
148
152
|
#{model.quoted_table_name}.id AS searchable_id,
|
@@ -152,9 +156,15 @@ INSERT INTO #{PgSearch::Document.quoted_table_name} (searchable_type, searchable
|
|
152
156
|
#{connection.quote(connection.quoted_date(now))} AS created_at,
|
153
157
|
#{connection.quote(connection.quoted_date(now))} AS updated_at
|
154
158
|
FROM #{model.quoted_table_name}
|
155
|
-
|
159
|
+
SQL
|
160
|
+
|
161
|
+
statements = []
|
162
|
+
connection.stub(:execute) { |sql| statements << sql }
|
156
163
|
|
157
|
-
|
164
|
+
PgSearch::Multisearch.rebuild(model)
|
165
|
+
|
166
|
+
statements.should include(expected_sql)
|
167
|
+
end
|
158
168
|
end
|
159
169
|
end
|
160
170
|
end
|
data/spec/spec_helper.rb
CHANGED
@@ -28,11 +28,13 @@ begin
|
|
28
28
|
postgresql_version = connection.send(:postgresql_version)
|
29
29
|
connection.execute("SELECT 1")
|
30
30
|
rescue error_class => e
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
31
|
+
at_exit do
|
32
|
+
puts "-" * 80
|
33
|
+
puts "Unable to connect to database. Please run:"
|
34
|
+
puts
|
35
|
+
puts " createdb pg_search_test"
|
36
|
+
puts "-" * 80
|
37
|
+
end
|
36
38
|
raise e
|
37
39
|
end
|
38
40
|
|
@@ -56,9 +58,11 @@ rescue => e
|
|
56
58
|
puts $!.message
|
57
59
|
end
|
58
60
|
rescue => e2
|
59
|
-
|
60
|
-
|
61
|
-
|
61
|
+
at_exit do
|
62
|
+
puts "-" * 80
|
63
|
+
puts "Please install the #{name} contrib module"
|
64
|
+
puts "-" * 80
|
65
|
+
end
|
62
66
|
raise e2
|
63
67
|
end
|
64
68
|
end
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: pg_search
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.5.
|
4
|
+
version: 0.5.2
|
5
5
|
prerelease:
|
6
6
|
platform: ruby
|
7
7
|
authors:
|
@@ -9,7 +9,7 @@ authors:
|
|
9
9
|
autorequire:
|
10
10
|
bindir: bin
|
11
11
|
cert_chain: []
|
12
|
-
date: 2012-
|
12
|
+
date: 2012-09-06 00:00:00.000000000 Z
|
13
13
|
dependencies:
|
14
14
|
- !ruby/object:Gem::Dependency
|
15
15
|
name: activerecord
|
@@ -65,9 +65,11 @@ files:
|
|
65
65
|
- lib/pg_search/configuration.rb
|
66
66
|
- lib/pg_search/configuration/association.rb
|
67
67
|
- lib/pg_search/configuration/column.rb
|
68
|
+
- lib/pg_search/configuration/foreign_column.rb
|
68
69
|
- lib/pg_search/document.rb
|
69
70
|
- lib/pg_search/features.rb
|
70
71
|
- lib/pg_search/features/dmetaphone.rb
|
72
|
+
- lib/pg_search/features/feature.rb
|
71
73
|
- lib/pg_search/features/trigram.rb
|
72
74
|
- lib/pg_search/features/tsearch.rb
|
73
75
|
- lib/pg_search/migration/associated_against_generator.rb
|
@@ -77,6 +79,7 @@ files:
|
|
77
79
|
- lib/pg_search/migration/templates/add_pg_search_dmetaphone_support_functions.rb.erb
|
78
80
|
- lib/pg_search/migration/templates/create_pg_search_documents.rb
|
79
81
|
- lib/pg_search/multisearch.rb
|
82
|
+
- lib/pg_search/multisearch/rebuilder.rb
|
80
83
|
- lib/pg_search/multisearchable.rb
|
81
84
|
- lib/pg_search/normalizer.rb
|
82
85
|
- lib/pg_search/railtie.rb
|
@@ -111,7 +114,7 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
111
114
|
version: '0'
|
112
115
|
segments:
|
113
116
|
- 0
|
114
|
-
hash: -
|
117
|
+
hash: -1173780423204059667
|
115
118
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
116
119
|
none: false
|
117
120
|
requirements:
|
@@ -120,7 +123,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
120
123
|
version: '0'
|
121
124
|
segments:
|
122
125
|
- 0
|
123
|
-
hash: -
|
126
|
+
hash: -1173780423204059667
|
124
127
|
requirements: []
|
125
128
|
rubyforge_project:
|
126
129
|
rubygems_version: 1.8.24
|