pg_search 2.1.7 → 2.3.3

Sign up to get free protection for your applications and to get access to all the features.
Files changed (45) hide show
  1. checksums.yaml +4 -4
  2. data/.codeclimate.yml +1 -0
  3. data/.editorconfig +10 -0
  4. data/.github/dependabot.yml +11 -0
  5. data/.rubocop.yml +89 -7
  6. data/.travis.yml +11 -19
  7. data/CHANGELOG.md +38 -14
  8. data/Gemfile +1 -1
  9. data/LICENSE +1 -1
  10. data/README.md +74 -42
  11. data/lib/pg_search.rb +15 -58
  12. data/lib/pg_search/document.rb +2 -2
  13. data/lib/pg_search/features/dmetaphone.rb +4 -6
  14. data/lib/pg_search/features/trigram.rb +29 -5
  15. data/lib/pg_search/features/tsearch.rb +13 -12
  16. data/lib/pg_search/migration/templates/add_pg_search_dmetaphone_support_functions.rb.erb +6 -6
  17. data/lib/pg_search/migration/templates/create_pg_search_documents.rb.erb +2 -2
  18. data/lib/pg_search/model.rb +59 -0
  19. data/lib/pg_search/multisearch.rb +10 -1
  20. data/lib/pg_search/multisearch/rebuilder.rb +7 -3
  21. data/lib/pg_search/multisearchable.rb +4 -4
  22. data/lib/pg_search/scope_options.rb +2 -10
  23. data/lib/pg_search/tasks.rb +2 -1
  24. data/lib/pg_search/version.rb +1 -1
  25. data/pg_search.gemspec +10 -5
  26. data/spec/.rubocop.yml +2 -2
  27. data/spec/integration/.rubocop.yml +11 -0
  28. data/spec/integration/associations_spec.rb +36 -75
  29. data/spec/integration/deprecation_spec.rb +33 -0
  30. data/spec/integration/pagination_spec.rb +1 -1
  31. data/spec/integration/pg_search_spec.rb +199 -188
  32. data/spec/integration/single_table_inheritance_spec.rb +2 -2
  33. data/spec/lib/pg_search/configuration/association_spec.rb +10 -8
  34. data/spec/lib/pg_search/configuration/foreign_column_spec.rb +3 -3
  35. data/spec/lib/pg_search/features/dmetaphone_spec.rb +2 -2
  36. data/spec/lib/pg_search/features/trigram_spec.rb +48 -19
  37. data/spec/lib/pg_search/features/tsearch_spec.rb +16 -10
  38. data/spec/lib/pg_search/multisearch/rebuilder_spec.rb +124 -76
  39. data/spec/lib/pg_search/multisearch_spec.rb +49 -30
  40. data/spec/lib/pg_search/multisearchable_spec.rb +155 -101
  41. data/spec/lib/pg_search/normalizer_spec.rb +12 -10
  42. data/spec/lib/pg_search_spec.rb +62 -46
  43. data/spec/spec_helper.rb +13 -4
  44. data/spec/support/database.rb +1 -1
  45. metadata +90 -13
@@ -2,11 +2,12 @@
2
2
 
3
3
  require "active_record"
4
4
  require "active_support/concern"
5
- require "active_support/core_ext/module/attribute_accessors"
5
+ require "active_support/core_ext/module/attribute_accessors_per_thread"
6
6
  require "active_support/core_ext/string/strip"
7
7
 
8
8
  require "pg_search/configuration"
9
9
  require "pg_search/features"
10
+ require "pg_search/model"
10
11
  require "pg_search/multisearch"
11
12
  require "pg_search/multisearchable"
12
13
  require "pg_search/normalizer"
@@ -14,37 +15,23 @@ require "pg_search/scope_options"
14
15
  require "pg_search/version"
15
16
 
16
17
  module PgSearch
17
- extend ActiveSupport::Concern
18
+ autoload :Document, "pg_search/document"
18
19
 
19
- mattr_accessor :multisearch_options
20
- self.multisearch_options = {}
20
+ def self.included(base)
21
+ ActiveSupport::Deprecation.warn <<~MESSAGE
22
+ Directly including `PgSearch` into an Active Record model is deprecated and will be removed in pg_search 3.0.
21
23
 
22
- mattr_accessor :unaccent_function
23
- self.unaccent_function = "unaccent"
24
+ Please replace `include PgSearch` with `include PgSearch::Model`.
25
+ MESSAGE
24
26
 
25
- module ClassMethods
26
- def pg_search_scope(name, options)
27
- options_proc = if options.respond_to?(:call)
28
- options
29
- elsif options.respond_to?(:merge)
30
- ->(query) { { query: query }.merge(options) }
31
- else
32
- raise ArgumentError, 'pg_search_scope expects a Hash or Proc'
33
- end
27
+ base.include PgSearch::Model
28
+ end
34
29
 
35
- define_singleton_method(name) do |*args|
36
- config = Configuration.new(options_proc.call(*args), self)
37
- scope_options = ScopeOptions.new(config)
38
- scope_options.apply(self)
39
- end
40
- end
30
+ thread_mattr_accessor :multisearch_options
31
+ self.multisearch_options = {}
41
32
 
42
- def multisearchable(options = {})
43
- include PgSearch::Multisearchable
44
- class_attribute :pg_search_multisearchable_options
45
- self.pg_search_multisearchable_options = options
46
- end
47
- end
33
+ thread_mattr_accessor :unaccent_function
34
+ self.unaccent_function = "unaccent"
48
35
 
49
36
  class << self
50
37
  def multisearch(*args)
@@ -67,32 +54,6 @@ module PgSearch
67
54
  end
68
55
  end
69
56
 
70
- def method_missing(symbol, *args)
71
- case symbol
72
- when :pg_search_rank
73
- raise PgSearchRankNotSelected unless respond_to?(:pg_search_rank)
74
-
75
- read_attribute(:pg_search_rank).to_f
76
- when :pg_search_highlight
77
- raise PgSearchHighlightNotSelected unless respond_to?(:pg_search_highlight)
78
-
79
- read_attribute(:pg_search_highlight)
80
- else
81
- super
82
- end
83
- end
84
-
85
- def respond_to_missing?(symbol, *args)
86
- case symbol
87
- when :pg_search_rank
88
- attributes.key?(:pg_search_rank)
89
- when :pg_search_highlight
90
- attributes.key?(:pg_search_highlight)
91
- else
92
- super
93
- end
94
- end
95
-
96
57
  class PgSearchRankNotSelected < StandardError
97
58
  def message
98
59
  "You must chain .with_pg_search_rank after the pg_search_scope " \
@@ -108,8 +69,4 @@ module PgSearch
108
69
  end
109
70
  end
110
71
 
111
- ActiveSupport.on_load(:active_record) do
112
- require "pg_search/document"
113
- end
114
-
115
- require "pg_search/railtie" if defined?(Rails)
72
+ require "pg_search/railtie" if defined?(Rails::Railtie)
@@ -4,7 +4,7 @@ require 'logger'
4
4
 
5
5
  module PgSearch
6
6
  class Document < ActiveRecord::Base
7
- include PgSearch
7
+ include PgSearch::Model
8
8
 
9
9
  self.table_name = 'pg_search_documents'
10
10
  belongs_to :searchable, polymorphic: true
@@ -12,7 +12,7 @@ module PgSearch
12
12
  # The logger might not have loaded yet.
13
13
  # https://github.com/Casecommons/pg_search/issues/26
14
14
  def self.logger
15
- super || Logger.new(STDERR)
15
+ super || Logger.new($stderr)
16
16
  end
17
17
 
18
18
  pg_search_scope :search, lambda { |*args|
@@ -1,5 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require "active_support/core_ext/module/delegation"
4
+
3
5
  module PgSearch
4
6
  module Features
5
7
  class DMetaphone
@@ -9,13 +11,9 @@ module PgSearch
9
11
  @tsearch = TSearch.new(query, options, columns, model, dmetaphone_normalizer)
10
12
  end
11
13
 
12
- def conditions
13
- tsearch.conditions
14
- end
14
+ delegate :conditions, to: :tsearch
15
15
 
16
- def rank
17
- tsearch.rank
18
- end
16
+ delegate :rank, to: :tsearch
19
17
 
20
18
  private
21
19
 
@@ -4,7 +4,7 @@ module PgSearch
4
4
  module Features
5
5
  class Trigram < Feature
6
6
  def self.valid_options
7
- super + [:threshold]
7
+ super + %i[threshold word_similarity]
8
8
  end
9
9
 
10
10
  def conditions
@@ -14,7 +14,11 @@ module PgSearch
14
14
  )
15
15
  else
16
16
  Arel::Nodes::Grouping.new(
17
- Arel::Nodes::InfixOperation.new("%", normalized_document, normalized_query)
17
+ Arel::Nodes::InfixOperation.new(
18
+ infix_operator,
19
+ normalized_query,
20
+ normalized_document
21
+ )
18
22
  )
19
23
  end
20
24
  end
@@ -25,12 +29,32 @@ module PgSearch
25
29
 
26
30
  private
27
31
 
32
+ def word_similarity?
33
+ options[:word_similarity]
34
+ end
35
+
36
+ def similarity_function
37
+ if word_similarity?
38
+ 'word_similarity'
39
+ else
40
+ 'similarity'
41
+ end
42
+ end
43
+
44
+ def infix_operator
45
+ if word_similarity?
46
+ '<%'
47
+ else
48
+ '%'
49
+ end
50
+ end
51
+
28
52
  def similarity
29
53
  Arel::Nodes::NamedFunction.new(
30
- "similarity",
54
+ similarity_function,
31
55
  [
32
- normalized_document,
33
- normalized_query
56
+ normalized_query,
57
+ normalized_document
34
58
  ]
35
59
  )
36
60
  end
@@ -59,7 +59,7 @@ module PgSearch
59
59
  end
60
60
  end
61
61
 
62
- def deprecated_headline_options
62
+ def deprecated_headline_options # rubocop:disable Metrics/MethodLength
63
63
  indifferent_options = options.with_indifferent_access
64
64
 
65
65
  %w[
@@ -97,7 +97,7 @@ module PgSearch
97
97
 
98
98
  DISALLOWED_TSQUERY_CHARACTERS = /['?\\:‘’]/.freeze
99
99
 
100
- def tsquery_for_term(unsanitized_term) # rubocop:disable Metrics/AbcSize
100
+ def tsquery_for_term(unsanitized_term)
101
101
  if options[:negation] && unsanitized_term.start_with?("!")
102
102
  unsanitized_term[0] = ''
103
103
  negated = true
@@ -107,25 +107,26 @@ module PgSearch
107
107
 
108
108
  term_sql = Arel.sql(normalize(connection.quote(sanitized_term)))
109
109
 
110
- # After this, the SQL expression evaluates to a string containing the term surrounded by single-quotes.
111
- # If :prefix is true, then the term will have :* appended to the end.
112
- # If :negated is true, then the term will have ! prepended to the front.
110
+ tsquery = tsquery_expression(term_sql, negated: negated, prefix: options[:prefix])
111
+
112
+ Arel::Nodes::NamedFunction.new("to_tsquery", [dictionary, tsquery]).to_sql
113
+ end
114
+
115
+ # After this, the SQL expression evaluates to a string containing the term surrounded by single-quotes.
116
+ # If :prefix is true, then the term will have :* appended to the end.
117
+ # If :negated is true, then the term will have ! prepended to the front.
118
+ def tsquery_expression(term_sql, negated:, prefix:)
113
119
  terms = [
114
120
  (Arel::Nodes.build_quoted('!') if negated),
115
121
  Arel::Nodes.build_quoted("' "),
116
122
  term_sql,
117
123
  Arel::Nodes.build_quoted(" '"),
118
- (Arel::Nodes.build_quoted(":*") if options[:prefix])
124
+ (Arel::Nodes.build_quoted(":*") if prefix)
119
125
  ].compact
120
126
 
121
- tsquery_sql = terms.inject do |memo, term|
127
+ terms.inject do |memo, term|
122
128
  Arel::Nodes::InfixOperation.new("||", memo, Arel::Nodes.build_quoted(term))
123
129
  end
124
-
125
- Arel::Nodes::NamedFunction.new(
126
- "to_tsquery",
127
- [dictionary, tsquery_sql]
128
- ).to_sql
129
130
  end
130
131
 
131
132
  def tsquery
@@ -1,16 +1,16 @@
1
1
  class AddPgSearchDmetaphoneSupportFunctions < ActiveRecord::Migration<%= migration_version %>
2
- def self.up
2
+ def up
3
3
  say_with_time("Adding support functions for pg_search :dmetaphone") do
4
- execute <<-'SQL'
5
- <%= read_sql_file "dmetaphone" %>
4
+ execute <<~'SQL'.squish
5
+ <%= indent(read_sql_file("dmetaphone"), 8) %>
6
6
  SQL
7
7
  end
8
8
  end
9
9
 
10
- def self.down
10
+ def down
11
11
  say_with_time("Dropping support functions for pg_search :dmetaphone") do
12
- execute <<-'SQL'
13
- <%= read_sql_file "uninstall_dmetaphone" %>
12
+ execute <<~'SQL'.squish
13
+ <%= indent(read_sql_file("uninstall_dmetaphone"), 8) %>
14
14
  SQL
15
15
  end
16
16
  end
@@ -1,5 +1,5 @@
1
1
  class CreatePgSearchDocuments < ActiveRecord::Migration<%= migration_version %>
2
- def self.up
2
+ def up
3
3
  say_with_time("Creating table for pg_search multisearch") do
4
4
  create_table :pg_search_documents do |t|
5
5
  t.text :content
@@ -9,7 +9,7 @@ class CreatePgSearchDocuments < ActiveRecord::Migration<%= migration_version %>
9
9
  end
10
10
  end
11
11
 
12
- def self.down
12
+ def down
13
13
  say_with_time("Dropping table for pg_search multisearch") do
14
14
  drop_table :pg_search_documents
15
15
  end
@@ -0,0 +1,59 @@
1
+ # frozen_string_literal: true
2
+
3
+ module PgSearch
4
+ module Model
5
+ extend ActiveSupport::Concern
6
+
7
+ module ClassMethods
8
+ def pg_search_scope(name, options)
9
+ options_proc = if options.respond_to?(:call)
10
+ options
11
+ elsif options.respond_to?(:merge)
12
+ ->(query) { { query: query }.merge(options) }
13
+ else
14
+ raise ArgumentError, 'pg_search_scope expects a Hash or Proc'
15
+ end
16
+
17
+ define_singleton_method(name) do |*args|
18
+ config = Configuration.new(options_proc.call(*args), self)
19
+ scope_options = ScopeOptions.new(config)
20
+ scope_options.apply(self)
21
+ end
22
+ end
23
+
24
+ # rubocop:disable ThreadSafety/ClassAndModuleAttributes
25
+ def multisearchable(options = {})
26
+ include PgSearch::Multisearchable
27
+ class_attribute :pg_search_multisearchable_options
28
+ self.pg_search_multisearchable_options = options
29
+ end
30
+ # rubocop:enable ThreadSafety/ClassAndModuleAttributes
31
+ end
32
+
33
+ def method_missing(symbol, *args)
34
+ case symbol
35
+ when :pg_search_rank
36
+ raise PgSearchRankNotSelected unless respond_to?(:pg_search_rank)
37
+
38
+ read_attribute(:pg_search_rank).to_f
39
+ when :pg_search_highlight
40
+ raise PgSearchHighlightNotSelected unless respond_to?(:pg_search_highlight)
41
+
42
+ read_attribute(:pg_search_highlight)
43
+ else
44
+ super
45
+ end
46
+ end
47
+
48
+ def respond_to_missing?(symbol, *args)
49
+ case symbol
50
+ when :pg_search_rank
51
+ attributes.key?(:pg_search_rank)
52
+ when :pg_search_highlight
53
+ attributes.key?(:pg_search_highlight)
54
+ else
55
+ super
56
+ end
57
+ end
58
+ end
59
+ end
@@ -5,7 +5,15 @@ require "pg_search/multisearch/rebuilder"
5
5
  module PgSearch
6
6
  module Multisearch
7
7
  class << self
8
- def rebuild(model, clean_up = true)
8
+ def rebuild(model, deprecated_clean_up = nil, clean_up: true)
9
+ unless deprecated_clean_up.nil?
10
+ ActiveSupport::Deprecation.warn(
11
+ "pg_search 3.0 will no longer accept a boolean second argument to PgSearchMultisearch.rebuild, " \
12
+ "use keyword argument `clean_up:` instead."
13
+ )
14
+ clean_up = deprecated_clean_up
15
+ end
16
+
9
17
  model.transaction do
10
18
  PgSearch::Document.where(searchable_type: model.base_class.name).delete_all if clean_up
11
19
  Rebuilder.new(model).rebuild
@@ -15,6 +23,7 @@ module PgSearch
15
23
 
16
24
  class ModelNotMultisearchable < StandardError
17
25
  def initialize(model_class)
26
+ super
18
27
  @model_class = model_class
19
28
  end
20
29
 
@@ -13,7 +13,7 @@ module PgSearch
13
13
  def rebuild
14
14
  if model.respond_to?(:rebuild_pg_search_documents)
15
15
  model.rebuild_pg_search_documents
16
- elsif conditional? || dynamic?
16
+ elsif conditional? || dynamic? || additional_attributes?
17
17
  model.find_each(&:update_pg_search_document)
18
18
  else
19
19
  model.connection.execute(rebuild_sql)
@@ -30,7 +30,11 @@ module PgSearch
30
30
 
31
31
  def dynamic?
32
32
  column_names = model.columns.map(&:name)
33
- columns.any? { |column| !column_names.include?(column.to_s) }
33
+ columns.any? { |column| column_names.exclude?(column.to_s) }
34
+ end
35
+
36
+ def additional_attributes?
37
+ model.pg_search_multisearchable_options.key?(:additional_attributes)
34
38
  end
35
39
 
36
40
  def connection
@@ -42,7 +46,7 @@ module PgSearch
42
46
  end
43
47
 
44
48
  def rebuild_sql_template
45
- <<-SQL.strip_heredoc
49
+ <<~SQL.squish
46
50
  INSERT INTO :documents_table (searchable_type, searchable_id, content, created_at, updated_at)
47
51
  SELECT :base_model_name AS searchable_type,
48
52
  :model_table.#{primary_key} AS searchable_id,
@@ -7,12 +7,12 @@ module PgSearch
7
7
  def self.included(mod)
8
8
  mod.class_eval do
9
9
  has_one :pg_search_document,
10
- as: :searchable,
11
- class_name: "PgSearch::Document",
12
- dependent: :delete
10
+ as: :searchable,
11
+ class_name: "PgSearch::Document",
12
+ dependent: :delete
13
13
 
14
14
  after_save :update_pg_search_document,
15
- if: -> { PgSearch.multisearch_enabled? }
15
+ if: -> { PgSearch.multisearch_enabled? }
16
16
  end
17
17
  end
18
18
 
@@ -14,23 +14,15 @@ module PgSearch
14
14
 
15
15
  def apply(scope)
16
16
  scope = include_table_aliasing_for_rank(scope)
17
- rank_table_alias = scope.pg_search_rank_table_alias(:include_counter)
17
+ rank_table_alias = scope.pg_search_rank_table_alias(include_counter: true)
18
18
 
19
19
  scope
20
20
  .joins(rank_join(rank_table_alias))
21
21
  .order(Arel.sql("#{rank_table_alias}.rank DESC, #{order_within_rank}"))
22
- .extend(DisableEagerLoading)
23
22
  .extend(WithPgSearchRank)
24
23
  .extend(WithPgSearchHighlight[feature_for(:tsearch)])
25
24
  end
26
25
 
27
- # workaround for https://github.com/Casecommons/pg_search/issues/14
28
- module DisableEagerLoading
29
- def eager_loading?
30
- false
31
- end
32
- end
33
-
34
26
  module WithPgSearchHighlight
35
27
  def self.[](tsearch)
36
28
  Module.new do
@@ -66,7 +58,7 @@ module PgSearch
66
58
  end
67
59
 
68
60
  module PgSearchRankTableAliasing
69
- def pg_search_rank_table_alias(include_counter = false)
61
+ def pg_search_rank_table_alias(include_counter: false)
70
62
  components = [arel_table.name]
71
63
  if include_counter
72
64
  count = increment_counter