activerecord-mysql-search 0.1.3 → 0.2.1

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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 0c41e02668032b01baac019990ad3ecd0c71ffb344c7f0a91fac8fce768228fb
4
- data.tar.gz: e489ceecc065d10b4ce3091d52d9a5d3df34900e9e8ee26aff73632ba131bfff
3
+ metadata.gz: bf223abbfa9e47d49642b4f8f52d05eb60256749f6f1ca22b40f9a17db7aad57
4
+ data.tar.gz: 197c6c7c9c15de41a0ed7bd3578a988be3beb366acac94f249e1aa0315747728
5
5
  SHA512:
6
- metadata.gz: 8d9e87974524b8389599b691b7180a7c0d674e76960e6f28478abc7d6d3d28ab577a303294436f48aff8cb6521dd1ea3536fbc99a04ca54912d574b2b7754b87
7
- data.tar.gz: 0b58f3cbe89df20425d5181469f12b00ca07e5642217c355ddf805b9586272c49957c6fc5a472d0f552fdff24c2a47996c83d6549d8c7b8ec87fec102167a0e4
6
+ metadata.gz: 225730631d733499a6fe947fc8c90ed27f61441939206516f54b5bc951c520130692ffdab155ca072a2986fc9c6d6e2f4bd6e1d10f302be15aca133402dd2d5e
7
+ data.tar.gz: 5475a8bd4aaf08b71c694814abcee48ca74bbf28514e20085ab34cf12ae4f1418e0dee2b57578a97134de1d687d7efd6d79f004bc429b2c2fe7dabc01b6f30de
data/Appraisals CHANGED
@@ -16,3 +16,11 @@ end
16
16
  appraise 'rails_8.0' do
17
17
  gem 'rails', '~> 8.0.0'
18
18
  end
19
+
20
+ appraise 'rails_8.0' do
21
+ gem 'rails', '~> 8.0.0'
22
+ end
23
+
24
+ appraise 'rails_8.1' do
25
+ gem 'rails', '~> 8.1.0'
26
+ end
data/CHANGELOG.md CHANGED
@@ -1,5 +1,31 @@
1
1
  ## [Unreleased]
2
2
 
3
+ ## [0.2.1] - 2026-07-15
4
+
5
+ - Adds automatic loading of source classes on Rails startup via new `autoload_sources` configuration option (default: true)
6
+ - Adds `load_source_classes!` method for manually loading source classes
7
+ - Fixes source actualization query composition to preserve base STI scope while appending OR conditions for associated updates
8
+ - Adds regression coverage for STI subclass source relations with associated updates
9
+ - Extracts Arel `AGAINST` integration into a dedicated extension file
10
+
11
+ ## [0.2.0] - 2025-09-25
12
+
13
+ - Removes text normalization from default `:text` formatter
14
+ - Search term passed as argument to the scope `.full_text_search` is not pre-normalized anymore
15
+ - Adds `register_format` method to register custom formatters
16
+
17
+ ## [0.1.3] - 2025-08-06
18
+
19
+ - Move rake tasks to the proper place + explicit tasks load
20
+
21
+ ## [0.1.2] - 2025-08-12
22
+
23
+ - Removes code of conduct
24
+
25
+ ## [0.1.1] - 2025-08-05
26
+
27
+ - Fixes load paths to rake tasks
28
+
3
29
  ## [0.1.0] - 2025-07-22
4
30
 
5
31
  - Initial release
data/Gemfile CHANGED
@@ -16,6 +16,9 @@ gem 'database_cleaner-active_record'
16
16
  gem 'rspec'
17
17
 
18
18
  gem 'rubocop'
19
+ gem 'rubocop-performance'
19
20
  gem 'rubocop-rails'
20
21
  gem 'rubocop-rake'
21
22
  gem 'rubocop-rspec'
23
+
24
+ gem 'ruby-lsp'
data/README.md CHANGED
@@ -89,14 +89,23 @@ class ArticleSource < MySQL::Search::Source
89
89
  end
90
90
  ```
91
91
 
92
- **Available Formatters **
92
+ #### Available Formatters
93
93
 
94
94
  - **`:text`** - Extracts text content from the field
95
95
  - **`:date`** - Formats dates using the configured date format (e.g., "12.01.2025", format is configurable)
96
96
  - **`:calendar_week`** - Extracts calendar week information (e.g., "week 42", format is configurable)
97
97
  - **`Proc`** - Custom extraction logic with access to the attribute value
98
98
  - **Nested Associations** - Supports nested associations
99
- - **Your formatters**. Check the [source code](lib/mysql/search/utils/formatter.rb) for more details.
99
+
100
+ #### Registering new formatters
101
+
102
+ You can register new formatters using the `register_format` method:
103
+
104
+ ```ruby
105
+ MySQL::Search.configure do |config|
106
+ config.register_format(:upcase) { |value| value.to_s.upcase }
107
+ end
108
+ ```
100
109
 
101
110
  ### Step 6: Index Existing Data
102
111
 
@@ -204,6 +213,13 @@ MySQL::Search.configure do |config|
204
213
  # Path to search source classes (default: 'app/search_sources')
205
214
  config.sources_path = 'app/search_sources'
206
215
 
216
+ # Automatically load source classes on Rails startup (default: true)
217
+ # Loading of source classes assigns callbacks to the target models
218
+ # to keep the search index updated. In case of loading issue you can load
219
+ # the source classes manually with `MySQL::Search.load_source_classes!`
220
+ # and disable automatic initialization.
221
+ config.autoload_sources = true
222
+
207
223
  # Automatically update search index when models change (default: true)
208
224
  config.automatic_update = true
209
225
 
@@ -215,6 +231,9 @@ MySQL::Search.configure do |config|
215
231
 
216
232
  # Calendar week format (default: 'week %V')
217
233
  config.calendar_week_format = 'week %V'
234
+
235
+ # Register custom formatters
236
+ config.register_format(:upcase) { |value| value.to_s.upcase }
218
237
  end
219
238
  ```
220
239
 
@@ -2,7 +2,7 @@
2
2
 
3
3
  Gem::Specification.new do |spec|
4
4
  spec.name = 'activerecord-mysql-search'
5
- spec.version = '0.1.3'
5
+ spec.version = '0.2.1'
6
6
  spec.authors = ['Daydream Unicorn GmbH & Co. KG']
7
7
  spec.email = ['hello@daydreamunicorn.com']
8
8
 
@@ -13,8 +13,10 @@ gem 'rails', '~> 7.0.0'
13
13
  gem 'rake', '~> 13.0'
14
14
  gem 'rspec'
15
15
  gem 'rubocop'
16
+ gem 'rubocop-performance'
16
17
  gem 'rubocop-rails'
17
18
  gem 'rubocop-rake'
18
19
  gem 'rubocop-rspec'
20
+ gem 'ruby-lsp'
19
21
 
20
22
  gemspec path: '../'
@@ -12,8 +12,10 @@ gem 'rails', '~> 7.1.0'
12
12
  gem 'rake', '~> 13.0'
13
13
  gem 'rspec'
14
14
  gem 'rubocop'
15
+ gem 'rubocop-performance'
15
16
  gem 'rubocop-rails'
16
17
  gem 'rubocop-rake'
17
18
  gem 'rubocop-rspec'
19
+ gem 'ruby-lsp'
18
20
 
19
21
  gemspec path: '../'
@@ -12,8 +12,10 @@ gem 'rails', '~> 7.2.0'
12
12
  gem 'rake', '~> 13.0'
13
13
  gem 'rspec'
14
14
  gem 'rubocop'
15
+ gem 'rubocop-performance'
15
16
  gem 'rubocop-rails'
16
17
  gem 'rubocop-rake'
17
18
  gem 'rubocop-rspec'
19
+ gem 'ruby-lsp'
18
20
 
19
21
  gemspec path: '../'
@@ -12,8 +12,10 @@ gem 'rails', '~> 8.0.0'
12
12
  gem 'rake', '~> 13.0'
13
13
  gem 'rspec'
14
14
  gem 'rubocop'
15
+ gem 'rubocop-performance'
15
16
  gem 'rubocop-rails'
16
17
  gem 'rubocop-rake'
17
18
  gem 'rubocop-rspec'
19
+ gem 'ruby-lsp'
18
20
 
19
21
  gemspec path: '../'
@@ -0,0 +1,21 @@
1
+ # frozen_string_literal: true
2
+
3
+ # This file was generated by Appraisal
4
+
5
+ source 'https://rubygems.org'
6
+
7
+ gem 'appraisal'
8
+ gem 'database_cleaner-active_record'
9
+ gem 'mysql2'
10
+ gem 'pry'
11
+ gem 'rails', '~> 8.1.0'
12
+ gem 'rake', '~> 13.0'
13
+ gem 'rspec'
14
+ gem 'rubocop'
15
+ gem 'rubocop-performance'
16
+ gem 'rubocop-rails'
17
+ gem 'rubocop-rake'
18
+ gem 'rubocop-rspec'
19
+ gem 'ruby-lsp'
20
+
21
+ gemspec path: '../'
@@ -15,11 +15,7 @@ module ActiveRecord
15
15
  add_column table_name, :updated_at, 'DATETIME ON UPDATE CURRENT_TIMESTAMP', **options
16
16
  end
17
17
  end
18
- end
19
- end
20
18
 
21
- module ActiveRecord
22
- module ConnectionAdapters
23
19
  # Overrides the `timestamps` method in `TableDefinition` to use MySQL's `DATETIME ON UPDATE CURRENT_TIMESTAMP`
24
20
  # for the `updated_at` column.
25
21
  # This allows the `updated_at` column to automatically update its value whenever the row is updated.
@@ -2,21 +2,31 @@
2
2
 
3
3
  # Configure MySQL::Search settings
4
4
  MySQL::Search.configure do |config|
5
- # Defines the name of the search index activerecord model.
6
- config.search_index_class_name = 'SearchIndex'
5
+ # Model class name for search indices (default: 'SearchIndex')
6
+ # config.search_index_class_name = 'SearchIndex'
7
7
 
8
- # Location of the search sources folder
9
- config.sources_path = 'app/search_sources'
8
+ # Path to search source classes (default: 'app/search_sources')
9
+ # config.sources_path = 'app/search_sources'
10
10
 
11
- # Enables the search index to be automatically updated via source and nested models callbacks "on save"
12
- config.automatic_update = true
11
+ # Automatically load source classes on Rails startup (default: true)
12
+ # Loading of source classes assigns callbacks to the target models
13
+ # to keep the search index updated. In case of loading issue you can load
14
+ # the source classes manually with `MySQL::Search.load_source_classes!`
15
+ # and disable automatic initialization.
16
+ # config.autoload_sources = true
13
17
 
14
- # Use ActiveJob to update the search index in the background
15
- config.update_asyncronously = true
18
+ # Automatically update search index when models change (default: true)
19
+ # config.automatic_update = true
16
20
 
17
- # Defines the format for `calendar_week` formatter.
18
- config.calendar_week_format = 'week %W'
21
+ # Process index updates asynchronously (default: false)
22
+ # config.update_asyncronously = false
19
23
 
20
- # Defines the format for `date` formater.
21
- config.date_format = '%d.%m.%Y'
24
+ # Date format for date fields (default: '%d.%m.%Y')
25
+ # config.date_format = '%d.%m.%Y'
26
+
27
+ # Calendar week format (default: 'week %V')
28
+ # config.calendar_week_format = 'week %V'
29
+
30
+ # Register custom formatters
31
+ # config.register_format(:upcase) { |value| value.to_s.upcase }
22
32
  end
@@ -0,0 +1,28 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'arel'
4
+
5
+ module Arel
6
+ module Visitors
7
+ # Custom visitor for MySQL to handle the `AGAINST` clause in full-text search.
8
+ class MySQL
9
+ def visit_Arel_Nodes_Against(node, collector) # rubocop:disable Naming/MethodName
10
+ visit(node.left, collector) << ' AGAINST ('
11
+ visit(node.right, collector) << ')'
12
+ end
13
+ end
14
+ end
15
+
16
+ module Nodes
17
+ # Represents the `AGAINST` clause used in MySQL full-text search queries.
18
+ class Against < Arel::Nodes::Matches
19
+ end
20
+ end
21
+
22
+ # Adds a method to the `Arel::Nodes::Node` class to allow for full-text search queries.
23
+ module Predications
24
+ def against(other)
25
+ Arel::Nodes::Against.new(self, quoted_node(other))
26
+ end
27
+ end
28
+ end
@@ -1,35 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'arel'
4
-
5
- module Arel
6
- module Visitors
7
- # Custom visitor for MySQL to handle the `AGAINST` clause in full-text search.
8
- class MySQL
9
- def visit_Arel_Nodes_Against(node, collector) # rubocop:disable Naming/MethodName
10
- visit(node.left, collector) << ' AGAINST ('
11
- visit(node.right, collector) << ')'
12
- end
13
- end
14
- end
15
- end
16
-
17
- module Arel
18
- module Nodes
19
- # Represents the `AGAINST` clause used in MySQL full-text search queries.
20
- class Against < Arel::Nodes::Matches
21
- end
22
- end
23
- end
24
-
25
- module Arel
26
- # Adds a method to the `Arel::Nodes::Node` class to allow for full-text search queries.
27
- module Predications
28
- def against(other)
29
- Arel::Nodes::Against.new(self, quoted_node(other))
30
- end
31
- end
32
- end
3
+ require_relative '../extensions/arel_against'
33
4
 
34
5
  module MySQL
35
6
  module Search
@@ -59,7 +30,6 @@ module MySQL
59
30
  private
60
31
 
61
32
  def search_expression(search_term, search_column)
62
- search_term = ::MySQL::Search::Utils::TextNormalizer.normalize(search_term)
63
33
  search_indices = ::MySQL::Search.search_index_class_name.constantize.arel_table
64
34
  search_columns = Array.wrap(search_column).map { |col| search_indices[col] }
65
35
 
@@ -14,24 +14,37 @@ module MySQL
14
14
 
15
15
  def call(time_ago)
16
16
  joins_args = source_class_name.constantize.joins_args
17
- relation = source_relation.left_joins(joins_args).where(updated_at: time_ago..)
17
+ base_relation = source_relation.left_joins(joins_args)
18
+ relation = base_relation.where(source_relation.klass.arel_table[:updated_at].gteq(time_ago))
18
19
 
19
- append_conditions(relation, source_relation, joins_args, time_ago)
20
+ append_conditions(relation, source_relation.klass, joins_args, time_ago, base_relation)
20
21
  end
21
22
 
22
23
  private
23
24
 
24
- def append_conditions(relation, root_class, config, time_ago)
25
+ def append_conditions(relation, root_class, config, time_ago, base_relation)
25
26
  case config
26
- when Array then config.reduce(relation) { |rel, item| append_conditions(rel, root_class, item, time_ago) }
27
+ when Array
28
+ append_array_conditions(relation, root_class, config, time_ago, base_relation)
27
29
  when Hash
28
- relation = append_conditions(relation, root_class, config.keys, time_ago)
29
-
30
- config.reduce(relation) do |rel, (root_association, nested_config)|
31
- append_conditions(rel, association_class(root_class, root_association), nested_config, time_ago)
32
- end
30
+ append_hash_conditions(relation, root_class, config, time_ago, base_relation)
33
31
  else
34
- append_or(relation, association_class(root_class, config), time_ago)
32
+ append_or(relation, association_class(root_class, config), time_ago, base_relation)
33
+ end
34
+ end
35
+
36
+ def append_array_conditions(relation, root_class, config, time_ago, base_relation)
37
+ config.reduce(relation) do |rel, item|
38
+ append_conditions(rel, root_class, item, time_ago, base_relation)
39
+ end
40
+ end
41
+
42
+ def append_hash_conditions(relation, root_class, config, time_ago, base_relation)
43
+ relation = append_conditions(relation, root_class, config.keys, time_ago, base_relation)
44
+
45
+ config.reduce(relation) do |rel, (root_association, nested_config)|
46
+ append_conditions(rel, association_class(root_class, root_association), nested_config, time_ago,
47
+ base_relation)
35
48
  end
36
49
  end
37
50
 
@@ -39,10 +52,10 @@ module MySQL
39
52
  root_class.reflect_on_association(association).klass
40
53
  end
41
54
 
42
- def append_or(relation, model, time_ago)
55
+ def append_or(relation, model, time_ago, base_relation)
43
56
  return relation unless model.column_names.include?('updated_at')
44
57
 
45
- relation.or(model.where(updated_at: time_ago..))
58
+ relation.or(base_relation.where(model.arel_table[:updated_at].gteq(time_ago)))
46
59
  end
47
60
  end
48
61
  end
@@ -6,6 +6,10 @@ module MySQL
6
6
  class Railtie < Rails::Railtie
7
7
  railtie_name :mysql_search
8
8
 
9
+ config.to_prepare do
10
+ MySQL::Search.load_source_classes! if MySQL::Search.autoload_sources
11
+ end
12
+
9
13
  rake_tasks do
10
14
  load 'tasks/mysql/search/actualize.rake'
11
15
  load 'tasks/mysql/search/reindex.rake'
@@ -7,6 +7,12 @@ module MySQL
7
7
  class Formatter
8
8
  attr_reader :value, :formatter
9
9
 
10
+ def self.register(name, &)
11
+ define_method(name) do
12
+ yield(value)
13
+ end
14
+ end
15
+
10
16
  def initialize(value, formatter)
11
17
  if formatter.instance_of?(Proc)
12
18
  @value = formatter.call(value)
@@ -20,13 +26,13 @@ module MySQL
20
26
  end
21
27
 
22
28
  def format
23
- formatter ? send(formatter) : value
29
+ (formatter ? send(formatter) : value).to_s.strip
24
30
  end
25
31
 
26
32
  private
27
33
 
28
34
  def text
29
- TextNormalizer.normalize(value.to_s)
35
+ value
30
36
  end
31
37
 
32
38
  def calendar_week
@@ -1,7 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require_relative 'utils/formatter'
4
- require_relative 'utils/text_normalizer'
5
4
  require_relative 'utils/duration_parser'
6
5
 
7
6
  module MySQL
data/lib/mysql/search.rb CHANGED
@@ -18,6 +18,7 @@ module MySQL
18
18
  # Runtime configuration
19
19
  mattr_accessor :automatic_update, default: true
20
20
  mattr_accessor :update_asyncronously, default: false
21
+ mattr_accessor :autoload_sources, default: true
21
22
 
22
23
  # Search Index & Sources
23
24
  mattr_accessor :search_index_class_name, default: 'SearchIndex'
@@ -27,16 +28,25 @@ module MySQL
27
28
  mattr_accessor :calendar_week_format, default: 'week %V'
28
29
  mattr_accessor :date_format, default: '%d.%m.%Y'
29
30
 
31
+ def register_format(name, &)
32
+ Utils::Formatter.register(name, &)
33
+ end
34
+
30
35
  def search_index_class
31
36
  @search_index_class ||= search_index_class_name.constantize
32
37
  end
33
38
 
34
39
  def source_classes
35
- @source_classes ||= Dir.glob("#{sources_path}/**/*.rb").filter_map do |file|
40
+ Dir.glob("#{sources_path}/**/*.rb").filter_map do |file|
36
41
  file.sub("#{sources_path}/", '').sub('.rb', '').camelize.safe_constantize
37
42
  end
38
43
  end
39
44
 
45
+ # Keep a separate API entrypoint used by the Railtie initialization hook.
46
+ def load_source_classes!
47
+ source_classes
48
+ end
49
+
40
50
  def configure
41
51
  yield self
42
52
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: activerecord-mysql-search
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.3
4
+ version: 0.2.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Daydream Unicorn GmbH & Co. KG
@@ -32,6 +32,7 @@ files:
32
32
  - gemfiles/rails_7.1.gemfile
33
33
  - gemfiles/rails_7.2.gemfile
34
34
  - gemfiles/rails_8.0.gemfile
35
+ - gemfiles/rails_8.1.gemfile
35
36
  - lib/activerecord-mysql-search.rb
36
37
  - lib/generators/mysql/search/create_trigger_generator.rb
37
38
  - lib/generators/mysql/search/install_generator.rb
@@ -42,6 +43,7 @@ files:
42
43
  - lib/generators/mysql/search/templates/db/migrate/enable_auto_update_of_updated_at.rb
43
44
  - lib/mysql/search.rb
44
45
  - lib/mysql/search/callbacks.rb
46
+ - lib/mysql/search/extensions/arel_against.rb
45
47
  - lib/mysql/search/grabber.rb
46
48
  - lib/mysql/search/jobs.rb
47
49
  - lib/mysql/search/jobs/scheduled_updater_job.rb
@@ -55,7 +57,6 @@ files:
55
57
  - lib/mysql/search/utils.rb
56
58
  - lib/mysql/search/utils/duration_parser.rb
57
59
  - lib/mysql/search/utils/formatter.rb
58
- - lib/mysql/search/utils/text_normalizer.rb
59
60
  - lib/tasks/mysql/search/actualize.rake
60
61
  - lib/tasks/mysql/search/reindex.rake
61
62
  homepage: https://github.com/ddunicorn/activerecord-mysql-search.rubygem/blob/main/README.md
@@ -80,7 +81,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
80
81
  - !ruby/object:Gem::Version
81
82
  version: '0'
82
83
  requirements: []
83
- rubygems_version: 3.6.9
84
+ rubygems_version: 4.0.16
84
85
  specification_version: 4
85
86
  summary: Full Text Search for ActiveRecord with MySQL
86
87
  test_files: []
@@ -1,16 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module MySQL
4
- module Search
5
- module Utils
6
- # Normalizes text by removing non-alphanumeric characters, except for spaces and hyphens.
7
- class TextNormalizer
8
- REGEXP = /[[:alnum:][:blank:]+\-*~<>()"@\.]/
9
-
10
- def self.normalize(value)
11
- value.to_s.scan(REGEXP).join.squish
12
- end
13
- end
14
- end
15
- end
16
- end