alchemy-pg_search 3.0.0 → 4.1.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (31) hide show
  1. checksums.yaml +4 -4
  2. data/.gitignore +5 -0
  3. data/CHANGELOG.md +26 -0
  4. data/Gemfile +3 -1
  5. data/README.md +80 -11
  6. data/Rakefile +29 -18
  7. data/alchemy-pg_search.gemspec +1 -1
  8. data/{lib → app/controller}/alchemy/pg_search/controller_methods.rb +11 -18
  9. data/{lib → app/extensions}/alchemy/pg_search/content_extension.rb +2 -11
  10. data/app/extensions/alchemy/pg_search/element_extension.rb +8 -0
  11. data/app/extensions/alchemy/pg_search/essence_picture_extension.rb +19 -0
  12. data/app/extensions/alchemy/pg_search/essence_richtext_extension.rb +20 -0
  13. data/app/extensions/alchemy/pg_search/essence_text_extension.rb +19 -0
  14. data/app/extensions/alchemy/pg_search/ingredient_extension.rb +20 -0
  15. data/app/extensions/alchemy/pg_search/page_extension.rb +30 -0
  16. data/app/extensions/alchemy/pg_search/pg_search_document_extension.rb +21 -0
  17. data/app/jobs/alchemy/pg_search/index_page_job.rb +10 -0
  18. data/app/views/alchemy/search/_result.html.erb +4 -5
  19. data/app/views/alchemy/search/_results.html.erb +2 -8
  20. data/db/migrate/20220826125413_add_page_id_column_to_pg_search_documents.rb +5 -0
  21. data/lib/alchemy/pg_search/engine.rb +14 -4
  22. data/lib/alchemy/pg_search/search.rb +59 -0
  23. data/lib/alchemy/pg_search/version.rb +1 -1
  24. data/lib/alchemy-pg_search.rb +28 -9
  25. data/lib/generators/alchemy/pg_search/install/install_generator.rb +25 -0
  26. metadata +19 -13
  27. data/lib/alchemy/pg_search/element_extension.rb +0 -53
  28. data/lib/alchemy/pg_search/page_extension.rb +0 -54
  29. data/lib/alchemy/pg_search/page_search_scope.rb +0 -9
  30. data/lib/generators/alchemy/pg_search/upgrade/templates/migration.rb.tt +0 -31
  31. data/lib/generators/alchemy/pg_search/upgrade/upgrade_generator.rb +0 -20
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: '041410896e0f98a81e3ead2437c7a5b02cb434e37d72ab5736db7ef3dbbaa5c0'
4
- data.tar.gz: 8b93ed647898f4b0c5540d4674233565beb2dcb9fa41370f0d228de48a1fb852
3
+ metadata.gz: 366fce3505557ca67b0a956441bc78e27c2bac492a8a008359571b2e768f2b18
4
+ data.tar.gz: c30b0e00f6c8e6dbd32326fc569ee94aba2b2b9bf7acbfeb71ce0084c930f3f3
5
5
  SHA512:
6
- metadata.gz: 2ede6256f9123b9bee9d13656c243a1ee3f370f40e04210674dd7ca2450ac83a66b2c3edda878ea6a0dca3b2aa64d042e6bdc4e0d2b3a676708455db4050db08
7
- data.tar.gz: cecc52649b848c514ebd07fff4b4822d19233f8fb50def4ffcb99acf14d1ad2ba45a07fc0ec53d3385fa49f566d8250b80862f530ec40acfd9edc841b10f3c8f
6
+ metadata.gz: c43a3f9bc07488d00811cc028f0be916a92afe87836dfb6e5017d454fcd5da9cf361e220be56b063a9132ade82947491e909ea924c2b6762441ddfd065257c19
7
+ data.tar.gz: 2c991f3476b1b82d960255d2f6d4d4adfd4610d6e93caa59bf8235326e485f2d4d36f0bcd16b0517472038e47e76835791cf7699c125aa5c3fc4c0eed8d91a5a
data/.gitignore CHANGED
@@ -7,3 +7,8 @@ spec/dummy/log/*.log
7
7
  spec/dummy/db/*.sqlite3*
8
8
  spec/dummy/tmp/*
9
9
  .ruby-*
10
+ node_modules
11
+ yarn.lock
12
+
13
+ .idea
14
+ .vscode
data/CHANGELOG.md CHANGED
@@ -1,3 +1,26 @@
1
+ # Changelog
2
+
3
+ ## [v4.1.0](https://github.com/AlchemyCMS/alchemy-pg_search/tree/v4.1.0) (2022-09-19)
4
+
5
+ [Full Changelog](https://github.com/AlchemyCMS/alchemy-pg_search/compare/v4.0.0...v4.1.0)
6
+
7
+ **Closed issues:**
8
+
9
+ - V4 issue [\#32](https://github.com/AlchemyCMS/alchemy-pg_search/issues/32)
10
+
11
+ **Merged pull requests:**
12
+
13
+ - Allow to exclude pages, elements and ingredients from search index [\#35](https://github.com/AlchemyCMS/alchemy-pg_search/pull/35) ([tvdeyen](https://github.com/tvdeyen))
14
+ - Add multisearch configuration [\#34](https://github.com/AlchemyCMS/alchemy-pg_search/pull/34) ([kulturbande](https://github.com/kulturbande))
15
+ - Remove old ferret upgrader [\#31](https://github.com/AlchemyCMS/alchemy-pg_search/pull/31) ([tvdeyen](https://github.com/tvdeyen))
16
+
17
+ ## 4.0.0 (2022-08-30)
18
+
19
+ - Introduced new search module [#30](https://github.com/AlchemyCMS/alchemy-pg_search/pull/30) by [kulturbande](https://github.com/kulturbande)
20
+ - Moved all extensions to a separate folder [#30](https://github.com/AlchemyCMS/alchemy-pg_search/pull/30) by [kulturbande](https://github.com/kulturbande)
21
+ - Enable pg_search multisearch [#30](https://github.com/AlchemyCMS/alchemy-pg_search/pull/30) by [kulturbande](https://github.com/kulturbande)
22
+ - Update Dummy App to Alchemy 6 [#30](https://github.com/AlchemyCMS/alchemy-pg_search/pull/30) by [kulturbande](https://github.com/kulturbande)
23
+
1
24
  ## 3.0.0 (2022-07-01)
2
25
 
3
26
  - Changed searchable option initialization [#29](https://github.com/AlchemyCMS/alchemy-pg_search/pull/29) by [kulturbande](https://github.com/kulturbande)
@@ -29,3 +52,6 @@
29
52
 
30
53
  - Rename the #search method into #full_text_search [#14](https://github.com/AlchemyCMS/alchemy-pg_search/pull/14) by [tvdeyen](https://github.com/tvdeyen)
31
54
  - Add a page search scope class [#13](https://github.com/AlchemyCMS/alchemy-pg_search/pull/13) by [tvdeyen](https://github.com/tvdeyen)
55
+
56
+
57
+ \* *This Changelog was automatically generated by [github_changelog_generator](https://github.com/github-changelog-generator/github-changelog-generator)*
data/Gemfile CHANGED
@@ -3,7 +3,7 @@ source "https://rubygems.org"
3
3
  gemspec
4
4
 
5
5
  gem "rails", "~> 6.0.0"
6
- ENV.fetch("ALCHEMY_BRANCH", "5.3-stable").tap do |branch|
6
+ ENV.fetch("ALCHEMY_BRANCH", "6.0-stable").tap do |branch|
7
7
  gem "alchemy_cms", github: "AlchemyCMS/alchemy_cms", branch: branch
8
8
  end
9
9
  gem "sassc-rails"
@@ -18,3 +18,5 @@ group :test do
18
18
  gem "pry-byebug"
19
19
  gem "launchy"
20
20
  end
21
+
22
+ gem "github_changelog_generator", "~> 1.16"
data/README.md CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  # Alchemy CMS Postgresql Fulltext Search
4
4
 
5
- This gem provides full text search for projects using postgresql databases to Alchemy CMS 4.0 and above.
5
+ This gem provides full text search for projects using postgresql databases to Alchemy CMS 6.0 and above.
6
6
 
7
7
  ## Installation
8
8
 
@@ -18,11 +18,10 @@ And then execute:
18
18
  $ bundle install
19
19
  ```
20
20
 
21
- Add migrations:
21
+ Run install script:
22
22
 
23
23
  ```shell
24
- $ bin/rake alchemy_pg_search:install:migrations
25
- $ bin/rake db:migrate
24
+ $ bin/rails g alchemy:pg_search:install
26
25
  ```
27
26
 
28
27
  ## Usage
@@ -31,16 +30,79 @@ Every `EssenceText`, `EssenceRichtext` and `EssencePicture` will be indexed unle
31
30
 
32
31
  ### Disable Indexing
33
32
 
34
- Simply pass `searchable: false` to your content definitions and Alchemy will not index results from that particular content.
33
+ #### Exclude whole pages from the search index
34
+
35
+ Pass `searchable: false` to your page layout definitions and Alchemy will not index that particular page.
36
+
37
+ ```yaml
38
+ # page_layouts.yml
39
+ - name: secret_page
40
+ searchable: false
41
+ elements:
42
+ - secret_sauce
43
+ ```
44
+
45
+ #### Exclude whole elements from the search index
46
+
47
+ Pass `searchable: false` to your element definitions and Alchemy will not index that particular element.
48
+
49
+ ```yaml
50
+ # elements.yml
51
+ - name: secret_sauce
52
+ searchable: false
53
+ ingredients:
54
+ - name: sauce
55
+ type: Text
56
+ default: 'This is my secret sauce.'
57
+ ```
58
+
59
+ #### Exclude single contents from being indexed
60
+
61
+ Pass `searchable: false` to your content definitions and Alchemy will not index that particular content.
35
62
 
36
63
  ```yaml
37
64
  # elements.yml
38
65
  - name: secrets
39
66
  contents:
40
- - name: passwords
41
- type: EssenceText
42
- searchable: false
43
- default: 'This is my secret password.'
67
+ - name: passwords
68
+ type: EssenceText
69
+ searchable: false
70
+ default: 'This is my secret password.'
71
+ ```
72
+
73
+ The same works for `ingredients` as well
74
+
75
+ ```yaml
76
+ # elements.yml
77
+ - name: secrets
78
+ ingredients:
79
+ - name: passwords
80
+ type: Text
81
+ searchable: false
82
+ default: 'This is my secret password.'
83
+ ```
84
+
85
+ ### Configure Behavior
86
+
87
+ Configure the gem in an initializer. The default configurations are:
88
+
89
+ ```ruby
90
+ Alchemy::PgSearch.config = {
91
+ paginate_per: 10, # amount of results per page
92
+ }
93
+ ```
94
+
95
+ You can also overwrite the default multisearch configuration to use other search strategies. For more information take
96
+ a look into the [PgSearch Readme](https://github.com/Casecommons/pg_search#configuring-multi-search).
97
+
98
+ ```ruby
99
+ Rails.application.config.after_initialize do
100
+ ::PgSearch.multisearch_options = {
101
+ using: {
102
+ tsearch: { prefix: true }
103
+ }
104
+ }
105
+ end
44
106
  ```
45
107
 
46
108
  ### Rendering search results.
@@ -120,13 +182,20 @@ en:
120
182
 
121
183
  ## Upgrading
122
184
 
123
- If you are upgrading from an old Alchemy < 4.0 based project that uses the ferret based full text search, please run this handy generator:
185
+ If you are upgrading from v3.0.0 please run the install generator:
124
186
 
125
187
  ```shell
126
- $ bin/rails g alchemy:pg_search:upgrade
188
+ $ bin/rails g alchemy:pg_search:install
127
189
  $ bin/rake db:migrate
128
190
  ```
129
191
 
192
+ and reindex your database in your Rails console
193
+
194
+ ```rb
195
+ # rails console
196
+ $ Alchemy::PgSearch::Search.rebuild
197
+ ```
198
+
130
199
  ## Contributing
131
200
 
132
201
  1. Fork it
data/Rakefile CHANGED
@@ -1,46 +1,57 @@
1
1
  #!/usr/bin/env rake
2
2
  begin
3
- require 'bundler/setup'
3
+ require "bundler/setup"
4
4
  rescue LoadError
5
- puts 'You must `gem install bundler` and `bundle install` to run rake tasks'
5
+ puts "You must `gem install bundler` and `bundle install` to run rake tasks"
6
6
  end
7
7
 
8
8
  begin
9
- require 'rdoc/task'
9
+ require "rdoc/task"
10
10
  rescue LoadError
11
- require 'rdoc/rdoc'
12
- require 'rake/rdoctask'
11
+ require "rdoc/rdoc"
12
+ require "rake/rdoctask"
13
13
  RDoc::Task = Rake::RDocTask
14
14
  end
15
15
 
16
- desc 'Generate documentation for Alchemy CMS.'
16
+ desc "Generate documentation for Alchemy CMS."
17
17
  RDoc::Task.new(:rdoc) do |rdoc|
18
- rdoc.rdoc_dir = 'rdoc'
19
- rdoc.title = 'Alchemy PgSearch'
20
- rdoc.options << '--line-numbers' << '--inline-source'
21
- rdoc.rdoc_files.include('README.md')
22
- rdoc.rdoc_files.include('lib/**/*.rb')
23
- rdoc.rdoc_files.include('app/**/*.rb')
18
+ rdoc.rdoc_dir = "rdoc"
19
+ rdoc.title = "Alchemy PgSearch"
20
+ rdoc.options << "--line-numbers" << "--inline-source"
21
+ rdoc.rdoc_files.include("README.md")
22
+ rdoc.rdoc_files.include("lib/**/*.rb")
23
+ rdoc.rdoc_files.include("app/**/*.rb")
24
24
  end
25
25
 
26
26
  APP_RAKEFILE = File.expand_path("../spec/dummy/Rakefile", __FILE__)
27
- load 'rails/tasks/engine.rake'
27
+ load "rails/tasks/engine.rake"
28
28
 
29
- require 'rspec/core'
30
- require 'rspec/core/rake_task'
29
+ require "rspec/core"
30
+ require "rspec/core/rake_task"
31
31
 
32
32
  RSpec::Core::RakeTask.new(:spec)
33
33
 
34
- task :default => ['alchemy:spec:prepare', :spec]
34
+ task :default => ["alchemy:spec:prepare", :spec]
35
35
 
36
36
  Bundler::GemHelper.install_tasks
37
37
 
38
38
  namespace :alchemy do
39
39
  namespace :spec do
40
-
41
40
  desc "Prepares database for testing"
42
41
  task :prepare do
43
- system 'cd spec/dummy; RAILS_ENV=test bin/rake db:setup db:seed; cd -'
42
+ system "cd spec/dummy; RAILS_ENV=test bin/rake db:setup db:seed; cd -"
44
43
  end
45
44
  end
46
45
  end
46
+
47
+ require "github_changelog_generator/task"
48
+ require "alchemy/pg_search/version"
49
+
50
+ namespace :changelog do
51
+ GitHubChangelogGenerator::RakeTask.new :update do |config|
52
+ config.user = "AlchemyCMS"
53
+ config.project = "alchemy-pg_search"
54
+ config.since_tag = "v4.0.0"
55
+ config.future_release = "v#{Alchemy::PgSearch::VERSION}"
56
+ end
57
+ end
@@ -16,7 +16,7 @@ Gem::Specification.new do |spec|
16
16
  spec.files = `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^spec/}) }
17
17
  spec.require_paths = ["lib"]
18
18
 
19
- spec.add_runtime_dependency "alchemy_cms", [">= 5.0", "< 6"]
19
+ spec.add_runtime_dependency "alchemy_cms", [">= 6.0", "< 7"]
20
20
  spec.add_runtime_dependency "pg_search", ["~> 2.1"]
21
21
  spec.add_runtime_dependency "pg"
22
22
 
@@ -35,7 +35,7 @@ module Alchemy
35
35
  #
36
36
  def perform_search
37
37
  if self.class == Alchemy::Admin::PagesController && params[:query].blank?
38
- params[:query] = 'lorem'
38
+ params[:query] = "lorem"
39
39
  end
40
40
  return if params[:query].blank?
41
41
  @search_results = search_results
@@ -49,31 +49,24 @@ module Alchemy
49
49
  # @return [Array]
50
50
  #
51
51
  def search_results
52
- pages = Alchemy::PgSearch.config[:page_search_scope].pages
53
- # Since CanCan cannot (oh the irony) merge +accessible_by+ scope with pg_search scopes,
54
- # we need to fake a page object here
55
- if can? :show, Alchemy::Page.new(restricted: true, public_on: Date.current)
56
- pages.full_text_search(params[:query])
57
- else
58
- pages.not_restricted.full_text_search(params[:query])
59
- end
52
+ Alchemy::PgSearch.search params[:query], ability: current_ability
60
53
  end
61
54
 
62
55
  # A view helper that loads the search result page.
63
56
  #
64
- # @return [Alchemy::Page]
57
+ # @return [Alchemy::Page,nil]
65
58
  #
66
59
  def search_result_page
67
60
  @search_result_page ||= begin
68
- page = Page.published.find_by(
69
- page_layout: search_result_page_layout['name'],
70
- language_id: Language.current.id
71
- )
72
- if page.nil?
73
- logger.warn "\n++++++\nNo published search result page found. Please create one or publish your search result page.\n++++++\n"
61
+ page = Page.published.find_by(
62
+ page_layout: search_result_page_layout["name"],
63
+ language_id: Language.current.id,
64
+ )
65
+ if page.nil?
66
+ logger.warn "\n++++++\nNo published search result page found. Please create one or publish your search result page.\n++++++\n"
67
+ end
68
+ page
74
69
  end
75
- page
76
- end
77
70
  end
78
71
 
79
72
  def search_result_page_layout
@@ -10,17 +10,8 @@ module Alchemy::PgSearch::ContentExtension
10
10
  end
11
11
 
12
12
  module InstanceMethods
13
- def searchable_ingredient
14
- case essence_type
15
- when "Alchemy::EssencePicture"
16
- essence.caption
17
- when "Alchemy::EssenceRichtext"
18
- essence.stripped_body
19
- when "Alchemy::EssenceText"
20
- essence.body
21
- else
22
- ingredient
23
- end
13
+ def searchable?
14
+ searchable && element.searchable?
24
15
  end
25
16
 
26
17
  Alchemy::Content.prepend self
@@ -0,0 +1,8 @@
1
+ module Alchemy::PgSearch::ElementExtension
2
+ def searchable?
3
+ (definition.key?(:searchable) ? definition[:searchable] : true) &&
4
+ public? && page.searchable? && page_version.public?
5
+ end
6
+ end
7
+
8
+ Alchemy::Element.prepend(Alchemy::PgSearch::ElementExtension)
@@ -0,0 +1,19 @@
1
+ module Alchemy::PgSearch::EssencePictureExtension
2
+
3
+ def self.prepended(base)
4
+ base.include PgSearch::Model
5
+ base.multisearchable(
6
+ against: [
7
+ :caption
8
+ ],
9
+ additional_attributes: -> (essence_picture) { { page_id: essence_picture.page.id } },
10
+ if: :searchable?
11
+ )
12
+ end
13
+
14
+ def searchable?
15
+ caption.present? && !!content&.searchable?
16
+ end
17
+ end
18
+
19
+ Alchemy::EssencePicture.prepend(Alchemy::PgSearch::EssencePictureExtension)
@@ -0,0 +1,20 @@
1
+ module Alchemy::PgSearch::EssenceRichtextExtension
2
+
3
+ def self.prepended(base)
4
+ base.include PgSearch::Model
5
+ base.multisearchable(
6
+ against: [
7
+ :stripped_body
8
+ ],
9
+ additional_attributes: -> (essence_richtext) { { page_id: essence_richtext.page.id } },
10
+ if: :searchable?
11
+ )
12
+ end
13
+
14
+ def searchable?
15
+ stripped_body.present? && !!content&.searchable?
16
+ end
17
+ end
18
+
19
+ Alchemy::EssenceRichtext.prepend(Alchemy::PgSearch::EssenceRichtextExtension)
20
+
@@ -0,0 +1,19 @@
1
+ module Alchemy::PgSearch::EssenceTextExtension
2
+ def self.prepended(base)
3
+ base.include PgSearch::Model
4
+ base.multisearchable(
5
+ against: [
6
+ :body
7
+ ],
8
+ additional_attributes: -> (essence_text) { { page_id: essence_text.page.id } },
9
+ if: :searchable?
10
+ )
11
+ end
12
+
13
+ def searchable?
14
+ body.present? && !!content&.searchable?
15
+ end
16
+ end
17
+
18
+ Alchemy::EssenceText.prepend(Alchemy::PgSearch::EssenceTextExtension)
19
+
@@ -0,0 +1,20 @@
1
+ module Alchemy::PgSearch::IngredientExtension
2
+ def self.prepended(base)
3
+ base.include PgSearch::Model
4
+ base.multisearchable(
5
+ against: [
6
+ :value,
7
+ ],
8
+ additional_attributes: ->(ingredient) { { page_id: ingredient.element.page.id } },
9
+ if: :searchable?,
10
+ )
11
+ end
12
+
13
+ def searchable?
14
+ Alchemy::PgSearch.is_searchable?(type) &&
15
+ (definition.key?(:searchable) ? definition[:searchable] : true) &&
16
+ value.present? && !!element&.searchable?
17
+ end
18
+ end
19
+
20
+ Alchemy::Ingredient.prepend(Alchemy::PgSearch::IngredientExtension)
@@ -0,0 +1,30 @@
1
+ # Enable Postgresql full text indexing.
2
+ #
3
+ module Alchemy::PgSearch::PageExtension
4
+ def self.prepended(base)
5
+ base.include PgSearch::Model
6
+ base.after_save :remove_unpublished_page
7
+ base.multisearchable(
8
+ against: [
9
+ :meta_description,
10
+ :meta_keywords,
11
+ :name,
12
+ ],
13
+ additional_attributes: ->(page) { { page_id: page.id } },
14
+ if: :searchable?,
15
+ )
16
+ end
17
+
18
+ def searchable?
19
+ (definition.key?(:searchable) ? definition[:searchable] : true) &&
20
+ public? && !layoutpage?
21
+ end
22
+
23
+ private
24
+
25
+ def remove_unpublished_page
26
+ Alchemy::PgSearch::Search.remove_page(self) unless searchable?
27
+ end
28
+ end
29
+
30
+ Alchemy::Page.prepend(Alchemy::PgSearch::PageExtension)
@@ -0,0 +1,21 @@
1
+ module Alchemy::PgSearch::PgSearchDocumentExtension
2
+ def self.prepended(base)
3
+ base.belongs_to :page, class_name: "::Alchemy::Page", foreign_key: "page_id"
4
+ end
5
+
6
+ ##
7
+ # get a list of excerpts of the searched phrase
8
+ # The JSON_AGG - method will transform the grouped content entries into json which have to be "unpacked".
9
+ # @return [array<string>]
10
+ def excerpts
11
+ return [] if content.blank?
12
+ begin
13
+ parsed_content = JSON.parse content
14
+ parsed_content.kind_of?(Array) ? parsed_content : []
15
+ rescue JSON::ParserError
16
+ []
17
+ end
18
+ end
19
+ end
20
+
21
+ PgSearch::Document.prepend(Alchemy::PgSearch::PgSearchDocumentExtension)
@@ -0,0 +1,10 @@
1
+ module Alchemy
2
+ module PgSearch
3
+ class IndexPageJob < BaseJob
4
+ def perform(page)
5
+ Search.index_page(page)
6
+ end
7
+ end
8
+ end
9
+ end
10
+
@@ -1,10 +1,9 @@
1
1
  <li class="search_result">
2
+ <% page = result.page %>
2
3
  <h3><%= link_to page.name, show_alchemy_page_path(page) %></h3>
3
- <% if elements.any? %>
4
- <% elements.each do |element| %>
5
- <% element.searchable_contents.each do |content| %>
6
- <p><%= highlighted_excerpt(content.searchable_ingredient.to_s, params[:query]) %></p>
7
- <% end %>
4
+ <% if result.excerpts.any? %>
5
+ <% result.excerpts.each do |excerpt| %>
6
+ <p><%= highlighted_excerpt(excerpt, params[:query]) %></p>
8
7
  <% end %>
9
8
  <% else %>
10
9
  <p><%= page.meta_description %></p>
@@ -7,16 +7,10 @@
7
7
  <h2 class="search_results_heading">
8
8
  <%= raw Alchemy.t("search_result_page.result_heading", query: h(params[:query])) %>
9
9
  <%= Alchemy.t("search_result_page.result_count",
10
- count: @search_results.try(:total_count) || @search_results.size) %>
10
+ count: @search_results.try(:total_count) || @search_results.size) %>
11
11
  </h2>
12
12
  <ul class="search_result_list">
13
- <% @search_results.each do |page| %>
14
- <%= render('alchemy/search/result',
15
- elements: page.element_search_results(params[:query]),
16
- options: options,
17
- page: page
18
- ) %>
19
- <% end %>
13
+ <%= render(partial: 'alchemy/search/result', collection: @search_results) %>
20
14
  </ul>
21
15
  <% end %>
22
16
  </div>
@@ -0,0 +1,5 @@
1
+ class AddPageIdColumnToPgSearchDocuments < ActiveRecord::Migration[6.0]
2
+ def change
3
+ add_reference :pg_search_documents, :page, index: true
4
+ end
5
+ end
@@ -7,13 +7,23 @@ module Alchemy
7
7
  engine_name "alchemy_pg_search"
8
8
 
9
9
  config.to_prepare do
10
- require_relative "./content_extension"
11
- require_relative "./controller_methods"
12
- require_relative "./element_extension"
13
- require_relative "./page_extension"
10
+ Dir.glob(Alchemy::PgSearch::Engine.root.join("app", "extensions", "**", "*_extension.rb")) do |c|
11
+ require_dependency(c)
12
+ end
14
13
 
15
14
  # We need to have the search methods present in all Alchemy controllers
16
15
  Alchemy::BaseController.send(:include, Alchemy::PgSearch::ControllerMethods)
16
+
17
+ # reindex the page after it was published
18
+ Alchemy.publish_targets << Alchemy::PgSearch::IndexPageJob
19
+
20
+ # configure multiselect to find also partial words
21
+ # @link https://github.com/Casecommons/pg_search#searching-using-different-search-features
22
+ ::PgSearch.multisearch_options = {
23
+ using: {
24
+ tsearch: { prefix: true }
25
+ }
26
+ }
17
27
  end
18
28
  end
19
29
  end
@@ -0,0 +1,59 @@
1
+ module Alchemy
2
+ module PgSearch
3
+ module Search
4
+
5
+ ##
6
+ # index all supported Alchemy models
7
+ def self.rebuild
8
+ ([Alchemy::Page, Alchemy::Ingredient] + Alchemy::PgSearch.searchable_essence_classes).each do |model|
9
+ ::PgSearch::Multisearch.rebuild(model)
10
+ end
11
+ end
12
+
13
+ ##
14
+ # remove the whole index for the page
15
+ #
16
+ # @param page [Alchemy::Page]
17
+ def self.remove_page(page)
18
+ ::PgSearch::Document.delete_by(page_id: page.id)
19
+ end
20
+
21
+ ##
22
+ # index a single page and indexable essences
23
+ #
24
+ # @param page [Alchemy::Page]
25
+ def self.index_page(page)
26
+ remove_page page
27
+
28
+ page.update_pg_search_document
29
+ page.all_elements.includes(:ingredients, contents: :essence).find_each do |element|
30
+ element.contents.each do |content|
31
+ content.essence.update_pg_search_document if Alchemy::PgSearch.is_searchable?(content.essence_type)
32
+ end
33
+
34
+ element.ingredients.each do |ingredient|
35
+ ingredient.update_pg_search_document if Alchemy::PgSearch.is_searchable?(ingredient.type)
36
+ end
37
+ end
38
+ end
39
+
40
+ ##
41
+ # search for page results
42
+ #
43
+ # @param query [string]
44
+ # @param ability [nil|CanCan::Ability]
45
+ # @return [ActiveRecord::Relation]
46
+ def self.search(query, ability: nil)
47
+ query = ::PgSearch.multisearch(query)
48
+ .select("JSON_AGG(content) as content", :page_id)
49
+ .reorder("")
50
+ .group(:page_id)
51
+ .joins(:page)
52
+
53
+ query = query.merge(Alchemy::Page.accessible_by(ability, :read)) if ability
54
+
55
+ query
56
+ end
57
+ end
58
+ end
59
+ end
@@ -1,5 +1,5 @@
1
1
  module Alchemy
2
2
  module PgSearch
3
- VERSION = "3.0.0"
3
+ VERSION = "4.1.0"
4
4
  end
5
5
  end
@@ -1,23 +1,42 @@
1
1
  require "alchemy/pg_search/engine"
2
2
  require "alchemy/pg_search/config"
3
- require "alchemy/pg_search/page_search_scope"
3
+ require "alchemy/pg_search/search"
4
4
 
5
5
  module Alchemy
6
6
  module PgSearch
7
- SEARCHABLE_ESSENCES = %w[EssenceText EssenceRichtext EssencePicture]
8
- DEFAULT_CONFIG = {
9
- page_search_scope: PageSearchScope.new,
10
- }
7
+ SEARCHABLE_INGREDIENTS = %w[Text Richtext Picture]
11
8
 
12
9
  extend Config
13
- self.config = DEFAULT_CONFIG
14
10
 
15
- def self.is_searchable_essence?(essence_type)
16
- SEARCHABLE_ESSENCES.include?(essence_type)
11
+ ##
12
+ # is essence or ingredient searchable?
13
+ # @param essence_type [string]
14
+ # @return [boolean]
15
+ def self.is_searchable?(essence_type)
16
+ SEARCHABLE_INGREDIENTS.include?(essence_type.gsub(/Alchemy::(Essence|Ingredients::)/, ""))
17
17
  end
18
18
 
19
+ ##
20
+ # generate an array of all supported essences classes
21
+ # @return [array]
19
22
  def self.searchable_essence_classes
20
- SEARCHABLE_ESSENCES.map { |k| "Alchemy::#{k.classify}".constantize }
23
+ SEARCHABLE_INGREDIENTS.map { |k| "Alchemy::Essence#{k.classify}".constantize }
24
+ end
25
+
26
+ ##
27
+ # search for page results
28
+ #
29
+ # @param query [string]
30
+ # @param ability [nil|CanCan::Ability]
31
+ # @return [ActiveRecord::Relation]
32
+ def self.search(query, ability: nil)
33
+ Search.search(query, ability: ability)
34
+ end
35
+
36
+ ##
37
+ # index all supported Alchemy models
38
+ def self.rebuild
39
+ Search.rebuild
21
40
  end
22
41
  end
23
42
  end
@@ -0,0 +1,25 @@
1
+ require "rails/generators"
2
+ require "rails/generators/active_record/migration"
3
+
4
+ module Alchemy
5
+ module PgSearch
6
+ class InstallGenerator < ::Rails::Generators::Base
7
+ include ActiveRecord::Generators::Migration
8
+
9
+ desc "Install Alchemy PgSearch - Gem into Rails App."
10
+
11
+ source_root(File.expand_path("../../../..", __dir__))
12
+
13
+ def install_migrations
14
+ # Install pg_search multisearch - migration - the pg_search is not testing if the migration already exists
15
+ generate("pg_search:migration:multisearch", abort_on_failure: true) unless self.class.migration_exists?("db/migrate", 'create_pg_search_documents')
16
+
17
+ # Copy the migrations of the gem
18
+ rake("alchemy_pg_search:install:migrations", abort_on_failure: true)
19
+
20
+ # run migrations
21
+ rake("db:migrate", abort_on_failure: true)
22
+ end
23
+ end
24
+ end
25
+ end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: alchemy-pg_search
3
3
  version: !ruby/object:Gem::Version
4
- version: 3.0.0
4
+ version: 4.1.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Thomas von Deyen
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2022-07-01 00:00:00.000000000 Z
11
+ date: 2022-09-19 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: alchemy_cms
@@ -16,20 +16,20 @@ dependencies:
16
16
  requirements:
17
17
  - - ">="
18
18
  - !ruby/object:Gem::Version
19
- version: '5.0'
19
+ version: '6.0'
20
20
  - - "<"
21
21
  - !ruby/object:Gem::Version
22
- version: '6'
22
+ version: '7'
23
23
  type: :runtime
24
24
  prerelease: false
25
25
  version_requirements: !ruby/object:Gem::Requirement
26
26
  requirements:
27
27
  - - ">="
28
28
  - !ruby/object:Gem::Version
29
- version: '5.0'
29
+ version: '6.0'
30
30
  - - "<"
31
31
  - !ruby/object:Gem::Version
32
- version: '6'
32
+ version: '7'
33
33
  - !ruby/object:Gem::Dependency
34
34
  name: pg_search
35
35
  requirement: !ruby/object:Gem::Requirement
@@ -115,7 +115,17 @@ files:
115
115
  - README.md
116
116
  - Rakefile
117
117
  - alchemy-pg_search.gemspec
118
+ - app/controller/alchemy/pg_search/controller_methods.rb
119
+ - app/extensions/alchemy/pg_search/content_extension.rb
120
+ - app/extensions/alchemy/pg_search/element_extension.rb
121
+ - app/extensions/alchemy/pg_search/essence_picture_extension.rb
122
+ - app/extensions/alchemy/pg_search/essence_richtext_extension.rb
123
+ - app/extensions/alchemy/pg_search/essence_text_extension.rb
124
+ - app/extensions/alchemy/pg_search/ingredient_extension.rb
125
+ - app/extensions/alchemy/pg_search/page_extension.rb
126
+ - app/extensions/alchemy/pg_search/pg_search_document_extension.rb
118
127
  - app/helpers/alchemy/pg_search/search_helper.rb
128
+ - app/jobs/alchemy/pg_search/index_page_job.rb
119
129
  - app/views/alchemy/search/_form.html.erb
120
130
  - app/views/alchemy/search/_result.html.erb
121
131
  - app/views/alchemy/search/_results.html.erb
@@ -127,17 +137,13 @@ files:
127
137
  - db/migrate/20141211105942_add_searchable_to_alchemy_essence_richtexts.rb
128
138
  - db/migrate/20141211110126_add_searchable_to_alchemy_essence_pictures.rb
129
139
  - db/migrate/20210923081905_move_searchable_to_contents.rb
140
+ - db/migrate/20220826125413_add_page_id_column_to_pg_search_documents.rb
130
141
  - lib/alchemy-pg_search.rb
131
142
  - lib/alchemy/pg_search/config.rb
132
- - lib/alchemy/pg_search/content_extension.rb
133
- - lib/alchemy/pg_search/controller_methods.rb
134
- - lib/alchemy/pg_search/element_extension.rb
135
143
  - lib/alchemy/pg_search/engine.rb
136
- - lib/alchemy/pg_search/page_extension.rb
137
- - lib/alchemy/pg_search/page_search_scope.rb
144
+ - lib/alchemy/pg_search/search.rb
138
145
  - lib/alchemy/pg_search/version.rb
139
- - lib/generators/alchemy/pg_search/upgrade/templates/migration.rb.tt
140
- - lib/generators/alchemy/pg_search/upgrade/upgrade_generator.rb
146
+ - lib/generators/alchemy/pg_search/install/install_generator.rb
141
147
  - lib/generators/alchemy/pg_search/views/views_generator.rb
142
148
  - lib/tasks/alchemy/pg_search_tasks.rake
143
149
  homepage: https://alchemy-cms.com
@@ -1,53 +0,0 @@
1
- Alchemy::Element.class_eval do
2
- include PgSearch::Model
3
-
4
- pg_search_scope :full_text_search,
5
- associated_against: {
6
- searchable_essence_texts: :body,
7
- searchable_essence_richtexts: :stripped_body,
8
- searchable_essence_pictures: :caption,
9
- },
10
- using: {
11
- tsearch: { prefix: true },
12
- }
13
-
14
- has_many :searchable_essence_texts,
15
- -> {
16
- includes(:element)
17
- .where(alchemy_contents: { searchable: true })
18
- .where(alchemy_elements: { public: true })
19
- },
20
- class_name: "Alchemy::EssenceText",
21
- source_type: "Alchemy::EssenceText",
22
- through: :contents,
23
- source: :essence
24
-
25
- has_many :searchable_essence_richtexts,
26
- -> {
27
- includes(:element)
28
- .where(alchemy_contents: { searchable: true })
29
- .where(alchemy_elements: { public: true })
30
- },
31
- class_name: "Alchemy::EssenceRichtext",
32
- source_type: "Alchemy::EssenceRichtext",
33
- through: :contents,
34
- source: :essence
35
-
36
- has_many :searchable_essence_pictures,
37
- -> {
38
- includes(:element)
39
- .where(alchemy_contents: { searchable: true })
40
- .where(alchemy_elements: { public: true })
41
- },
42
- class_name: "Alchemy::EssencePicture",
43
- source_type: "Alchemy::EssencePicture",
44
- through: :contents,
45
- source: :essence
46
-
47
- has_many :searchable_contents,
48
- -> {
49
- where(essence_type: Alchemy::PgSearch::SEARCHABLE_ESSENCES.map { |k| "Alchemy::#{k}" })
50
- },
51
- class_name: "Alchemy::Content",
52
- source: :contents
53
- end
@@ -1,54 +0,0 @@
1
- # Enable Postgresql full text indexing.
2
- #
3
- module Alchemy::PgSearch::PageExtension
4
- def self.extended(base)
5
- base.include InstanceMethods
6
- base.include PgSearch::Model
7
-
8
- base.pg_search_scope(
9
- :full_text_search,
10
- against: {
11
- meta_description: "B",
12
- meta_keywords: "B",
13
- title: "B",
14
- name: "A",
15
- },
16
- associated_against: {
17
- searchable_essence_texts: :body,
18
- searchable_essence_richtexts: :stripped_body,
19
- searchable_essence_pictures: :caption,
20
- },
21
- using: {
22
- tsearch: { prefix: true },
23
- },
24
- )
25
-
26
- base.has_many(
27
- :searchable_contents,
28
- -> {
29
- where(alchemy_elements: { public: true })
30
- .where(searchable: true)
31
- },
32
- class_name: "Alchemy::Content",
33
- through: :all_elements,
34
- )
35
-
36
- Alchemy::PgSearch::SEARCHABLE_ESSENCES.each do |klass|
37
- base.has_many(
38
- :"searchable_#{klass.underscore.pluralize}",
39
- class_name: "Alchemy::#{klass}",
40
- source_type: "Alchemy::#{klass}",
41
- through: :searchable_contents,
42
- source: :essence,
43
- )
44
- end
45
- end
46
-
47
- module InstanceMethods
48
- def element_search_results(query)
49
- all_elements.full_text_search(query)
50
- end
51
- end
52
-
53
- Alchemy::Page.extend self
54
- end
@@ -1,9 +0,0 @@
1
- module Alchemy
2
- module PgSearch
3
- class PageSearchScope
4
- def pages
5
- Alchemy::Page.published.contentpages.with_language(Alchemy::Language.current.id)
6
- end
7
- end
8
- end
9
- end
@@ -1,31 +0,0 @@
1
- require 'yaml'
2
-
3
- class UpgradeFromAlchemyFerret < ActiveRecord::Migration
4
- def up
5
- not_searchable_elements.each do |element|
6
- element['contents'].each do |content|
7
- next unless Alchemy::PgSearch.is_searchable_essence?(content)
8
- next if content['searchable'].nil?
9
- "Alchemy::#{content['type'].classify}".constantize
10
- .joins(content: :element)
11
- .where(alchemy_contents: {name: content['name']})
12
- .where(alchemy_elements: {name: element['name']})
13
- .update_all(searchable: false)
14
- say "Sets searchable to false for `#{content['name']}` contents of `#{element['name']}` elements."
15
- end
16
- end
17
- end
18
-
19
- private
20
-
21
- def elements_yml
22
- @elements_yml ||= YAML.load_file Rails.root.join('config/alchemy/elements.yml')
23
- end
24
-
25
- def not_searchable_elements
26
- elements_yml.select do |element|
27
- next if !element['contents']
28
- element['contents'].any? { |content| content['searchable'] == false }
29
- end
30
- end
31
- end
@@ -1,20 +0,0 @@
1
- require 'rails'
2
-
3
- module Alchemy
4
- module PgSearch
5
- class UpgradeGenerator < ::Rails::Generators::Base
6
- desc "This generator upgrades your project from alchemy-ferret based projects."
7
- source_root File.expand_path('templates', File.dirname(__FILE__))
8
-
9
- def replace_element_config
10
- gsub_file Rails.root.join('config/alchemy/elements.yml'),
11
- 'do_not_index: true', 'searchable: false'
12
- end
13
-
14
- def copy_migration_file
15
- timestamp = Time.now.strftime('%Y%m%d%H%M%S')
16
- copy_file "migration.rb.tt", Rails.root.join("db/migrate/#{timestamp}_upgrade_from_alchemy_ferret.alchemy_pg_search.rb")
17
- end
18
- end
19
- end
20
- end