pg_search 0.5.1 → 0.5.2
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/.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
|