activerecord-mysql-search 0.1.0

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.
Files changed (42) hide show
  1. checksums.yaml +7 -0
  2. data/.rspec +3 -0
  3. data/.rubocop.yml +22 -0
  4. data/Appraisals +18 -0
  5. data/CHANGELOG.md +5 -0
  6. data/Gemfile +21 -0
  7. data/LICENSE.txt +21 -0
  8. data/README.md +235 -0
  9. data/Rakefile +16 -0
  10. data/activerecord-mysql-search.gemspec +39 -0
  11. data/gemfiles/.bundle/config +2 -0
  12. data/gemfiles/rails_7.0.gemfile +20 -0
  13. data/gemfiles/rails_7.1.gemfile +19 -0
  14. data/gemfiles/rails_7.2.gemfile +19 -0
  15. data/gemfiles/rails_8.0.gemfile +19 -0
  16. data/lib/activerecord-mysql-search.rb +3 -0
  17. data/lib/generators/mysql/search/create_trigger_generator.rb +28 -0
  18. data/lib/generators/mysql/search/install_generator.rb +25 -0
  19. data/lib/generators/mysql/search/templates/app/models/search_index.rb +6 -0
  20. data/lib/generators/mysql/search/templates/config/initializers/active_record_ext.rb +37 -0
  21. data/lib/generators/mysql/search/templates/config/initializers/mysql_search.rb +22 -0
  22. data/lib/generators/mysql/search/templates/db/migrate/create_search_indices.rb +11 -0
  23. data/lib/generators/mysql/search/templates/db/migrate/enable_auto_update_of_updated_at.rb +22 -0
  24. data/lib/mysql/search/callbacks.rb +75 -0
  25. data/lib/mysql/search/grabber.rb +51 -0
  26. data/lib/mysql/search/jobs/scheduled_updater_job.rb +43 -0
  27. data/lib/mysql/search/jobs/updater_job.rb +24 -0
  28. data/lib/mysql/search/jobs.rb +12 -0
  29. data/lib/mysql/search/queries/full_text_search_query.rb +71 -0
  30. data/lib/mysql/search/queries/updated_sources_query.rb +50 -0
  31. data/lib/mysql/search/railtie.rb +19 -0
  32. data/lib/mysql/search/searchable.rb +24 -0
  33. data/lib/mysql/search/source.rb +69 -0
  34. data/lib/mysql/search/updater.rb +46 -0
  35. data/lib/mysql/search/utils/duration_parser.rb +20 -0
  36. data/lib/mysql/search/utils/formatter.rb +42 -0
  37. data/lib/mysql/search/utils/text_normalizer.rb +16 -0
  38. data/lib/mysql/search/utils.rb +13 -0
  39. data/lib/mysql/search.rb +47 -0
  40. data/lib/tasks/actualize.rake +24 -0
  41. data/lib/tasks/reindex.rake +30 -0
  42. metadata +86 -0
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: ac5bdb13de210b726a4e3616bfd79905f559bdea9018994e1ad853aae3c3db25
4
+ data.tar.gz: 7f16cc54115105d57b7043557a7fba5c27ec7ea095c411ceee175cf44845ba3f
5
+ SHA512:
6
+ metadata.gz: 1f0437f9d721c3834712ce514c899d7d597446db22c2cfa768b62fc121929d1931473ee209e8933578b9c5d790f3873fd5b36e13428011ddb1b6c6a67831999e
7
+ data.tar.gz: 0bac11a15dbf2a6df82c6636833eec5826aa12673e8e02bdc91bf5ef2298410101a7833f7a59672e9ea469c1383d46e0ef585d77f5f340dcf26059841f7abcab
data/.rspec ADDED
@@ -0,0 +1,3 @@
1
+ --format documentation
2
+ --color
3
+ --require spec_helper
data/.rubocop.yml ADDED
@@ -0,0 +1,22 @@
1
+ plugins:
2
+ - rubocop-rspec
3
+ - rubocop-rake
4
+ - rubocop-rails
5
+
6
+ AllCops:
7
+ TargetRubyVersion: 3.1
8
+ NewCops: enable
9
+
10
+ Layout/LineLength:
11
+ Max: 120
12
+
13
+ RSpec/SpecFilePathFormat:
14
+ CustomTransform:
15
+ MySQL: mysql
16
+
17
+ Rails/ApplicationJob:
18
+ Enabled: false
19
+
20
+ Naming/FileName:
21
+ Exclude:
22
+ - 'lib/activerecord-mysql-search.rb'
data/Appraisals ADDED
@@ -0,0 +1,18 @@
1
+ # frozen_string_literal: true
2
+
3
+ appraise 'rails_7.0' do
4
+ gem 'concurrent-ruby', '1.3.4'
5
+ gem 'rails', '~> 7.0.0'
6
+ end
7
+
8
+ appraise 'rails_7.1' do
9
+ gem 'rails', '~> 7.1.0'
10
+ end
11
+
12
+ appraise 'rails_7.2' do
13
+ gem 'rails', '~> 7.2.0'
14
+ end
15
+
16
+ appraise 'rails_8.0' do
17
+ gem 'rails', '~> 8.0.0'
18
+ end
data/CHANGELOG.md ADDED
@@ -0,0 +1,5 @@
1
+ ## [Unreleased]
2
+
3
+ ## [0.1.0] - 2025-07-22
4
+
5
+ - Initial release
data/Gemfile ADDED
@@ -0,0 +1,21 @@
1
+ # frozen_string_literal: true
2
+
3
+ source 'https://rubygems.org'
4
+
5
+ # Specify your gem's dependencies in activerecord-mysql-search.gemspec
6
+ gemspec
7
+
8
+ gem 'appraisal'
9
+ gem 'pry'
10
+
11
+ gem 'mysql2'
12
+ gem 'rails', '~> 7.1.0'
13
+ gem 'rake', '~> 13.0'
14
+
15
+ gem 'database_cleaner-active_record'
16
+ gem 'rspec'
17
+
18
+ gem 'rubocop'
19
+ gem 'rubocop-rails'
20
+ gem 'rubocop-rake'
21
+ gem 'rubocop-rspec'
data/LICENSE.txt ADDED
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2025 Daydream Unicorn GmbH & Co. KG
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in
13
+ all copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21
+ THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,235 @@
1
+ # ActiveRecord MySQL Search
2
+
3
+ [![Checks](https://github.com/ddunicorn/activerecord-mysql-search.rubygem/actions/workflows/main.yml/badge.svg)](https://github.com/ddunicorn/activerecord-mysql-search.rubygem/actions/workflows/main.yml)
4
+
5
+ A Ruby gem that provides efficient full-text search capabilities for ActiveRecord models using MySQL's native full-text search features. This gem simplifies the process of making your models searchable by automatically indexing specified fields and providing intuitive search methods.
6
+
7
+ ## How It Works
8
+
9
+ The gem creates a dedicated `search_indices` table that stores denormalized, searchable content extracted from your ActiveRecord models. When you define a search schema, the gem automatically copies and transforms data from your source models (e.g., `articles`, `products`) into optimized text columns with MySQL FULLTEXT indexes. This design delivers **fast search performance** by avoiding complex JOINs across multiple tables during queries, while **automatic synchronization** keeps the search index current when your source data changes. The gem handles the complexity of data extraction, formatting, and index maintenance, so you get scalable full-text search without manual SQL or external search services.
10
+
11
+ ## Features
12
+
13
+ - 🚀 **Native MySQL Full-Text Search**: Fast, relevant searches using MySQL `FULLTEXT` indexes.
14
+ - 🔄 **Automatic & Background Indexing**: Keeps search data up-to-date with synchronous or ActiveJob-powered background updates.
15
+ - 📝 **Declarative Search Schema**: Easily specify indexed fields, including nested associations, dates, calendar weeks, or custom logic via `Proc` in dedicated source classes.
16
+ - 👥 **Multi-Role & Context-Aware Search**: Define separate search columns for different user roles (e.g., buyer, seller, admin) to support multi-tenant apps and data privacy.
17
+ - 🧹 **Separation of Concerns**: Move indexing logic and field formatting out of models for cleaner, more maintainable code.
18
+ - ⚡ **Easy Rails Integration**: Includes generators for setup, migrations, and rake tasks for bulk reindexing.
19
+ - 🧩 **Flexible Field Mapping**: Supports complex data structures and custom extraction for precise indexing.
20
+ - 🎯 **Customizable Search Scopes**: Search specific columns based on user context or application needs.
21
+
22
+ ## Requirements
23
+
24
+ - Ruby >= 3.1.0
25
+ - Rails (supports versions 7.0+)
26
+ - MySQL database with FULLTEXT index support
27
+
28
+ ## Implementing Full-Text Search in 5 Minutes
29
+
30
+ ### Step 1: Install the Gem
31
+
32
+ Add to your `Gemfile`:
33
+
34
+ ```ruby
35
+ gem 'activerecord-mysql-search'
36
+ ```
37
+
38
+ And:
39
+
40
+ ```bash
41
+ bundle install
42
+ ```
43
+
44
+ ### Step 2: Generate Configuration and Migrations
45
+
46
+ Run:
47
+
48
+ ```bash
49
+ rails generate mysql:search:install
50
+ ```
51
+
52
+ This creates:
53
+ - `config/initializers/mysql_search.rb`: Search configuration.
54
+ - `app/models/search_index.rb`: Search index model.
55
+ - A migration to create `search_indices` table.
56
+
57
+ ### Step 3: Run the Migration
58
+
59
+ ```bash
60
+ rails db:migrate
61
+ ```
62
+
63
+ ### Step 4: Enable Search in Your Model
64
+
65
+ Add to your model (e.g., `Article`):
66
+
67
+ ```ruby
68
+ class Article < ApplicationRecord
69
+ include MySQL::Search::Searchable
70
+ belongs_to :news_digest
71
+ end
72
+ ```
73
+
74
+ ### Step 5: Define the Indexing Schema (Source Class)
75
+
76
+ Create `app/search_sources/article_source.rb`:
77
+
78
+ ```ruby
79
+ class ArticleSource < MySQL::Search::Source
80
+ schema content: {
81
+ title: :text,
82
+ content: :text,
83
+ type: ->(value) { I18n.t("article.types.#{value}") },
84
+ news_digest: {
85
+ title: :text,
86
+ published_at: [:date, :calendar_week]
87
+ }
88
+ }
89
+ end
90
+ ```
91
+
92
+ **Available Formatters **
93
+
94
+ - **`:text`** - Extracts text content from the field
95
+ - **`:date`** - Formats dates using the configured date format (e.g., "12.01.2025", format is configurable)
96
+ - **`:calendar_week`** - Extracts calendar week information (e.g., "week 42", format is configurable)
97
+ - **`Proc`** - Custom extraction logic with access to the attribute value
98
+ - **Nested Associations** - Supports nested associations
99
+ - **Your formatters**. Check the [source code](lib/mysql/search/utils/formatter.rb) for more details.
100
+
101
+ ### Step 6: Index Existing Data
102
+
103
+ ```bash
104
+ rails mysql:search:reindex
105
+ ```
106
+
107
+ This command populates the `search_indices` table with existing data from your model(s), using the schema defined in your source class.
108
+
109
+ ### Step 7: Use Search in Controllers or Services
110
+
111
+ ```ruby
112
+ results = Article.full_text_search("Ruby on Rails")
113
+ ```
114
+
115
+ **That’s it!** Users now get fast, scalable, and relevant search—no complex SQL, external services, or maintenance headaches.
116
+
117
+ ## Advanced Scenarios: Multi-Column Search for Roles and Contexts
118
+
119
+ Real projects rarely need "single-column search." Business logic often requires showing different data to different users, supporting flexible filters, and ensuring privacy. `activerecord-mysql-search` supports this out of the box. For example, clients, sellers, and admins each need their own "view of the world." The gem lets you create separate indexes per role:
120
+
121
+ ```ruby
122
+ class ProductSource < MySQL::Search::Source
123
+ schema content: {
124
+ name: :text,
125
+ description: :text,
126
+ brand: :text,
127
+ reviews: { content: :text, rating: :text }
128
+ },
129
+ # Extra information for seller's search
130
+ seller_extra: {
131
+ sku: :text,
132
+ internal_notes: :text,
133
+ supplier: { name: :text, contact_info: :text },
134
+ },
135
+ # Even more detailed information for admin's search
136
+ admin_extra: {
137
+ created_by: { name: :text, email: :text }
138
+ }
139
+ end
140
+ ```
141
+
142
+ Add columns and indexes to `SearchIndex`:
143
+
144
+ ```ruby
145
+ class ExtraContentForSearchIndices < ActiveRecord::Migration[7.1]
146
+ def change
147
+ add_column :search_indices, :seller_extra, :text
148
+ add_column :search_indices, :admin_extra, :text
149
+
150
+ add_index :search_indices, [:content, :seller_extra], type: :fulltext
151
+ add_index :search_indices, [:content, :seller_extra, :admin_extra], type: :fulltext
152
+ end
153
+ end
154
+ ```
155
+
156
+ Now, sellers search with:
157
+
158
+ ```ruby
159
+ results = Product.full_text_search("Ruby on Rails", search_column: [:content, :seller_extra])
160
+ ```
161
+
162
+ Admins use:
163
+
164
+ ```ruby
165
+ results = Product.full_text_search("Ruby on Rails", search_column: [:content, :seller_extra, :admin_extra])
166
+ ```
167
+
168
+ You can completely separate search contexts for different roles. In this case, there is no need to create combined indexes, just use different columns and separate indexes for each role.
169
+
170
+ ### What if I use methods that don't trigger ActiveRecord callbacks?
171
+
172
+ Using `#update_column` and other methods that don't trigger ActiveRecord callbacks can lead to search index desynchronization. Solution: use `#update` or `#save` to update records to ensure indexes remain current. If you don't have this option, the gem provides the following tool to maintain index consistency.
173
+
174
+ In this case, the gem relies on the `updated_at` column. You can delegate keeping this column up-to-date to the database itself using a trigger. Create a migration using the generator:
175
+
176
+ ```bash
177
+ rails generate mysql:search:create_triggers
178
+ ```
179
+
180
+ This migration will create a trigger in each table that will update the `updated_at` column when records are modified, and will also add a monkey-patch to ActiveRecord's `#timestamps` method in migrations (to automatically add this trigger to future tables). This allows maintaining search index relevance using one or more of the following tools:
181
+
182
+ - Rake task `rails mysql:search:actualize[1.hour]` - periodically checks and updates indexes, syncing them with the current database state. You can configure it to run via cron.
183
+ - `MySQL::Search::Jobs::ScheduledUpdaterJob` - a background job that periodically checks and updates indexes. Example for Solid Queue:
184
+
185
+ ```ruby
186
+ # config/recurring.yml
187
+ actualize_search_indices:
188
+ class: MySQL::Search::Jobs::ScheduledUpdaterJob
189
+ args: [:daily]
190
+ schedule: every day at noon
191
+ ```
192
+
193
+ - Full reindexing via rake task `mysql:search:reindex` - if you want to completely refresh index content, for example after migrations or schema changes. In this case, adding SQL triggers isn't required. You can also use this task to reindex specific models by passing their names as arguments, e.g., `rails mysql:search:reindex[Article]`.
194
+
195
+ ## Configuration
196
+
197
+ Configure the gem in `config/initializers/mysql_search.rb`:
198
+
199
+ ```ruby
200
+ MySQL::Search.configure do |config|
201
+ # Model class name for search indices (default: 'SearchIndex')
202
+ config.search_index_class_name = 'SearchIndex'
203
+
204
+ # Path to search source classes (default: 'app/search_sources')
205
+ config.sources_path = 'app/search_sources'
206
+
207
+ # Automatically update search index when models change (default: true)
208
+ config.automatic_update = true
209
+
210
+ # Process index updates asynchronously (default: false)
211
+ config.update_asyncronously = false
212
+
213
+ # Date format for date fields (default: '%d.%m.%Y')
214
+ config.date_format = '%d.%m.%Y'
215
+
216
+ # Calendar week format (default: 'week %V')
217
+ config.calendar_week_format = 'week %V'
218
+ end
219
+ ```
220
+
221
+ ### MySQL FULLTEXT Limitations
222
+
223
+ - **Minimum word length**: MySQL ignores words shorter than 4 characters by default (`ft_min_word_len`)
224
+ - **Stop words**: Common words like "the", "and", "or" are ignored
225
+ - **Memory usage**: FULLTEXT indexes can be memory-intensive for large datasets
226
+
227
+ ## Development
228
+
229
+ After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake spec` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
230
+
231
+ To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and the created tag, and push the `.gem` file to [rubygems.org](https://rubygems.org).
232
+
233
+ ## Contributing
234
+
235
+ Bug reports and pull requests are welcome on GitHub at https://github.com/ddunicorn/activerecord-mysql-search.rubygem
data/Rakefile ADDED
@@ -0,0 +1,16 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'bundler/gem_tasks'
4
+
5
+ load 'tasks/actualize.rake'
6
+ load 'tasks/reindex.rake'
7
+
8
+ require 'rspec/core/rake_task'
9
+
10
+ RSpec::Core::RakeTask.new(:spec)
11
+
12
+ require 'rubocop/rake_task'
13
+
14
+ RuboCop::RakeTask.new
15
+
16
+ task default: %i[spec rubocop]
@@ -0,0 +1,39 @@
1
+ # frozen_string_literal: true
2
+
3
+ Gem::Specification.new do |spec|
4
+ spec.name = 'activerecord-mysql-search'
5
+ spec.version = '0.1.0'
6
+ spec.authors = ['Daydream Unicorn GmbH & Co. KG']
7
+ spec.email = ['hello@daydreamunicorn.com']
8
+
9
+ spec.summary = 'Full Text Search for ActiveRecord with MySQL'
10
+ spec.description = <<~DESC
11
+ This gem provides a simple way to perform full-text search in MySQL databases using ActiveRecord.
12
+ It allows you to define search scopes and perform searches on your models with ease.
13
+ DESC
14
+ spec.homepage = 'https://github.com/ddunicorn/activerecord-mysql-search.rubygem/blob/main/README.md'
15
+ spec.license = 'MIT'
16
+ spec.required_ruby_version = '>= 3.1.0'
17
+
18
+ spec.metadata['homepage_uri'] = spec.homepage
19
+ spec.metadata['source_code_uri'] = 'https://github.com/ddunicorn/activerecord-mysql-search.rubygem'
20
+ spec.metadata['changelog_uri'] = 'https://github.com/ddunicorn/activerecord-mysql-search.rubygem/blob/main/CHANGELOG.md'
21
+ spec.metadata['rubygems_mfa_required'] = 'true'
22
+
23
+ # Specify which files should be added to the gem when it is released.
24
+ # The `git ls-files -z` loads the files in the RubyGem that have been added into git.
25
+ spec.files = Dir.chdir(__dir__) do
26
+ `git ls-files -z`.split("\x0").reject do |f|
27
+ (f == __FILE__) || f.match(%r{\A(?:(?:bin|test|spec|features)/|\.(?:git|travis|circleci)|appveyor)})
28
+ end
29
+ end
30
+ spec.bindir = 'exe'
31
+ spec.executables = spec.files.grep(%r{\Aexe/}) { |f| File.basename(f) }
32
+ spec.require_paths = ['lib']
33
+
34
+ # Uncomment to register a new dependency of your gem
35
+ # spec.add_dependency "example-gem", "~> 1.0"
36
+
37
+ # For more information and examples about making a new gem, check out our
38
+ # guide at: https://bundler.io/guides/creating_gem.html
39
+ end
@@ -0,0 +1,2 @@
1
+ ---
2
+ BUNDLE_RETRY: "1"
@@ -0,0 +1,20 @@
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 'concurrent-ruby', '1.3.4'
9
+ gem 'database_cleaner-active_record'
10
+ gem 'mysql2'
11
+ gem 'pry'
12
+ gem 'rails', '~> 7.0.0'
13
+ gem 'rake', '~> 13.0'
14
+ gem 'rspec'
15
+ gem 'rubocop'
16
+ gem 'rubocop-rails'
17
+ gem 'rubocop-rake'
18
+ gem 'rubocop-rspec'
19
+
20
+ gemspec path: '../'
@@ -0,0 +1,19 @@
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', '~> 7.1.0'
12
+ gem 'rake', '~> 13.0'
13
+ gem 'rspec'
14
+ gem 'rubocop'
15
+ gem 'rubocop-rails'
16
+ gem 'rubocop-rake'
17
+ gem 'rubocop-rspec'
18
+
19
+ gemspec path: '../'
@@ -0,0 +1,19 @@
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', '~> 7.2.0'
12
+ gem 'rake', '~> 13.0'
13
+ gem 'rspec'
14
+ gem 'rubocop'
15
+ gem 'rubocop-rails'
16
+ gem 'rubocop-rake'
17
+ gem 'rubocop-rspec'
18
+
19
+ gemspec path: '../'
@@ -0,0 +1,19 @@
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.0.0'
12
+ gem 'rake', '~> 13.0'
13
+ gem 'rspec'
14
+ gem 'rubocop'
15
+ gem 'rubocop-rails'
16
+ gem 'rubocop-rake'
17
+ gem 'rubocop-rspec'
18
+
19
+ gemspec path: '../'
@@ -0,0 +1,3 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'mysql/search'
@@ -0,0 +1,28 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Generates an initializer file for the MySQL Search gem
4
+ module MySQL
5
+ module Search
6
+ # This generator creates a migration to add a trigger for automatically updating the `updated_at` column in MySQL.
7
+ # It also includes a monkey-patch for ActiveRecord's `#timestamps` method to use
8
+ # MySQL's `DATETIME ON UPDATE CURRENT_TIMESTAMP` for the `updated_at` column.
9
+ class CreateTriggerGenerator < Rails::Generators::Base
10
+ include Rails::Generators::Migration
11
+
12
+ namespace 'mysql:search:create_trigger'
13
+ source_root File.expand_path('templates', __dir__)
14
+
15
+ def self.next_migration_number(_path)
16
+ Time.now.utc.strftime('%Y%m%d%H%M%S')
17
+ end
18
+
19
+ desc 'Generates migration to add a trigger to automatically update the `updated_at` ' \
20
+ 'column in MySQL + ActiveRecord monkey-patch for `#timestamps`'
21
+ def create_config_and_migration_files
22
+ migration_template 'db/migrate/enable_auto_update_of_updated_at.rb',
23
+ 'db/migrate/enable_auto_update_of_updated_at.rb'
24
+ template 'config/initializers/active_record_ext.rb'
25
+ end
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,25 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Generates an initializer file for the MySQL Search gem
4
+ module MySQL
5
+ module Search
6
+ # This generator creates an initializer file for configuring MySQL Search.
7
+ class InstallGenerator < Rails::Generators::Base
8
+ include Rails::Generators::Migration
9
+
10
+ namespace 'mysql:search:install'
11
+ source_root File.expand_path('templates', __dir__)
12
+
13
+ def self.next_migration_number(_path)
14
+ Time.now.utc.strftime('%Y%m%d%H%M%S')
15
+ end
16
+
17
+ desc 'Creates an initializer file for MySQL Search configuration'
18
+ def create_config_and_migration_files
19
+ template 'config/initializers/mysql_search.rb'
20
+ template 'app/models/search_index.rb'
21
+ migration_template 'db/migrate/create_search_indices.rb', 'db/migrate/create_search_indices.rb'
22
+ end
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,6 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Model that stores search indices for various models.
4
+ class SearchIndex < ApplicationRecord
5
+ belongs_to :searchable, polymorphic: true
6
+ end
@@ -0,0 +1,37 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActiveRecord
4
+ module ConnectionAdapters
5
+ # Overrides the default `add_timestamps` method to use MySQL's `DATETIME ON UPDATE CURRENT_TIMESTAMP`
6
+ # for the `updated_at` column.
7
+ # This allows the `updated_at` column to automatically update its value whenever the row is updated.
8
+ module SchemaStatements
9
+ def add_timestamps(table_name, **options)
10
+ options[:null] = false if options[:null].nil?
11
+
12
+ options[:precision] = 6 if !options.key?(:precision) && supports_datetime_with_precision?
13
+
14
+ add_column table_name, :created_at, :datetime, **options
15
+ add_column table_name, :updated_at, 'DATETIME ON UPDATE CURRENT_TIMESTAMP', **options
16
+ end
17
+ end
18
+ end
19
+ end
20
+
21
+ module ActiveRecord
22
+ module ConnectionAdapters
23
+ # Overrides the `timestamps` method in `TableDefinition` to use MySQL's `DATETIME ON UPDATE CURRENT_TIMESTAMP`
24
+ # for the `updated_at` column.
25
+ # This allows the `updated_at` column to automatically update its value whenever the row is updated.
26
+ class TableDefinition
27
+ def timestamps(**options)
28
+ options[:null] = false if options[:null].nil?
29
+
30
+ options[:precision] = 6 if !options.key?(:precision) && @conn.supports_datetime_with_precision?
31
+
32
+ column(:created_at, :datetime, **options)
33
+ column(:updated_at, 'DATETIME ON UPDATE CURRENT_TIMESTAMP', **options)
34
+ end
35
+ end
36
+ end
37
+ end
@@ -0,0 +1,22 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Configure MySQL::Search settings
4
+ MySQL::Search.configure do |config|
5
+ # Defines the name of the search index activerecord model.
6
+ config.search_index_class_name = 'SearchIndex'
7
+
8
+ # Location of the search sources folder
9
+ config.sources_path = 'app/search_sources'
10
+
11
+ # Enables the search index to be automatically updated via source and nested models callbacks "on save"
12
+ config.automatic_update = true
13
+
14
+ # Use ActiveJob to update the search index in the background
15
+ config.update_asyncronously = true
16
+
17
+ # Defines the format for `calendar_week` formatter.
18
+ config.calendar_week_format = 'week %W'
19
+
20
+ # Defines the format for `date` formater.
21
+ config.date_format = '%d.%m.%Y'
22
+ end
@@ -0,0 +1,11 @@
1
+ # frozen_string_literal: true
2
+
3
+ # This migration creates a search index table for MySQL full-text search.
4
+ class CreateSearchIndices < ActiveRecord::Migration[7.0]
5
+ create_table :search_indices do |t|
6
+ t.text :content, null: false
7
+ t.references :searchable, polymorphic: true, null: false
8
+
9
+ t.index :content, type: :fulltext
10
+ end
11
+ end
@@ -0,0 +1,22 @@
1
+ # frozen_string_literal: true
2
+
3
+ # This migration enables automatic updates for `updated_at` columns in all tables that have this column.
4
+ # It changes the column type to `DATETIME ON UPDATE CURRENT_TIMESTAMP`, which allows MySQL to automatically update
5
+ # the `updated_at` timestamp whenever the row is updated.
6
+ class EnableAutoUpdateForUpdatedAtColumns < ActiveRecord::Migration[7.0]
7
+ def up
8
+ ActiveRecord::Base.connection.tables.each do |table_name|
9
+ next unless column_exists?(table_name, :updated_at)
10
+
11
+ change_column table_name, :updated_at, 'DATETIME ON UPDATE CURRENT_TIMESTAMP'
12
+ end
13
+ end
14
+
15
+ def down
16
+ ActiveRecord::Base.connection.tables.each do |table_name|
17
+ next unless column_exists?(table_name, :updated_at)
18
+
19
+ change_column table_name, :updated_at, :datetime
20
+ end
21
+ end
22
+ end