activekit 0.5.0.dev8 → 0.5.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.
- checksums.yaml +4 -4
- data/README.md +42 -2
- data/lib/active_kit/bedrock/bedrockable.rb +2 -6
- data/lib/active_kit/bedrock/bedrocker.rb +41 -13
- data/lib/active_kit/bedrock/bedrocking.rb +10 -0
- data/lib/active_kit/bedrock.rb +1 -0
- data/lib/active_kit/export/exportable.rb +0 -24
- data/lib/active_kit/export/exporter.rb +23 -2
- data/lib/active_kit/export/exporting.rb +1 -6
- data/lib/active_kit/search/index.rb +7 -5
- data/lib/active_kit/search/key.rb +4 -3
- data/lib/active_kit/search/manager.rb +44 -0
- data/lib/active_kit/search/searchable.rb +1 -111
- data/lib/active_kit/search/searcher.rb +51 -106
- data/lib/active_kit/search/searching.rb +68 -104
- data/lib/active_kit/search/suggestion.rb +4 -2
- data/lib/active_kit/search.rb +1 -1
- data/lib/active_kit/version.rb +1 -1
- data/lib/tasks/search_tasks.rake +12 -75
- metadata +6 -5
- data/lib/active_kit/search/search.rb +0 -68
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 71b9eb7bd5fc27d2794cd15b8ec08a68e9baae06e37e072188260ddbaaa25473
|
4
|
+
data.tar.gz: 33f906a96b417c86b8048a2f663dbac5d4a6b0595f9cea23b6a0db92f98982cb
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 5172f9cb6b0aa0623a9e6f9a0263de7a5fadc016a25c6b92514f9916fca13653f8326479f4bc7fa8c54314c77c8d9d75435034b3c0e1e40054f7f002bf969d90
|
7
|
+
data.tar.gz: 65cb7a16747fb2281042dced7e1781bf0709437c470e8621b7ee232b6b205ab1ad021d8b1287a147abba85ddb96fe4f020e1ebb455b00a532d45488fbdf6d47f
|
data/README.md
CHANGED
@@ -3,6 +3,46 @@ Add the essential kit for rails ActiveRecord models and be happy.
|
|
3
3
|
|
4
4
|
## Usage
|
5
5
|
|
6
|
+
### Search Attribute
|
7
|
+
|
8
|
+
Add searching to your ActiveRecord models.
|
9
|
+
Search Attribute provides full searching functionality for your model database records using redis search including search suggestions.
|
10
|
+
|
11
|
+
You can define any number of model attributes in one model to search together.
|
12
|
+
|
13
|
+
Define the search attributes in accordance with the column name in your model like below.
|
14
|
+
```ruby
|
15
|
+
class Product < ApplicationRecord
|
16
|
+
search_attribute :name, type: :text
|
17
|
+
search_attribute :permalink, type: :tag
|
18
|
+
search_attribute :short_description, type: :text
|
19
|
+
search_attribute :published, type: :tag, sortable: true
|
20
|
+
end
|
21
|
+
```
|
22
|
+
|
23
|
+
You can also define a search_describer to describe the details of the search instead of using the defaults.
|
24
|
+
```ruby
|
25
|
+
class Product < ApplicationRecord
|
26
|
+
# search_describer method_name, database: -> { ActiveRecord::Base.connection_db_config.database }
|
27
|
+
search_describer :limit_by_search, database: -> { System::Current.tenant.database }
|
28
|
+
search_attribute :name, type: :text
|
29
|
+
search_attribute :permalink, type: :tag
|
30
|
+
search_attribute :short_description, type: :text
|
31
|
+
search_attribute :published, type: :tag, sortable: true
|
32
|
+
end
|
33
|
+
```
|
34
|
+
|
35
|
+
The following class methods will be added to your model class to use in accordance with details provided for search_describer:
|
36
|
+
```ruby
|
37
|
+
Product.limit_by_search(term: "term", tags: { published: true }, order: "name asc", page: 1)
|
38
|
+
Product.searcher.for(:limit_by_search).current_page
|
39
|
+
Product.searcher.for(:limit_by_search).previous_page?
|
40
|
+
Product.searcher.for(:limit_by_search).previous_page
|
41
|
+
Product.searcher.for(:limit_by_search).next_page?
|
42
|
+
Product.searcher.for(:limit_by_search).next_page
|
43
|
+
Product.searcher.for(:limit_by_search).suggestions(prefix: "prefix_term").keys
|
44
|
+
```
|
45
|
+
|
6
46
|
### Export Attribute
|
7
47
|
|
8
48
|
Add exporting to your ActiveRecord models.
|
@@ -23,8 +63,8 @@ end
|
|
23
63
|
You can also define an export_describer to describe the details of the export instead of using the defaults.
|
24
64
|
```ruby
|
25
65
|
class Product < ApplicationRecord
|
26
|
-
# export_describer method_name, kind: :csv, database: -> { ActiveRecord::Base.connection_db_config.database
|
27
|
-
export_describer :to_csv, kind: :csv, database: -> { System::Current.tenant.database
|
66
|
+
# export_describer method_name, kind: :csv, database: -> { ActiveRecord::Base.connection_db_config.database }
|
67
|
+
export_describer :to_csv, kind: :csv, database: -> { System::Current.tenant.database }
|
28
68
|
export_attribute :name
|
29
69
|
export_attribute :sku, heading: "SKU No."
|
30
70
|
export_attribute :image_name, value: lambda { |record| record.image&.name }, includes: :image
|
@@ -8,12 +8,12 @@ module ActiveKit
|
|
8
8
|
|
9
9
|
base.module_eval <<-CODE, __FILE__, __LINE__ + 1
|
10
10
|
module ClassMethods
|
11
|
-
private
|
12
|
-
|
13
11
|
def #{current_component}er
|
14
12
|
@#{current_component}er ||= ActiveKit::#{current_component.to_s.titleize}::#{current_component.to_s.titleize}er.new(current_component: :#{current_component}, current_class: self)
|
15
13
|
end
|
16
14
|
|
15
|
+
private
|
16
|
+
|
17
17
|
def #{current_component}_describer(name, **options)
|
18
18
|
#{current_component}er.create_describer(name, options)
|
19
19
|
end
|
@@ -21,10 +21,6 @@ module ActiveKit
|
|
21
21
|
def #{current_component}_attribute(name, **options)
|
22
22
|
#{current_component}er.create_attribute(name, options)
|
23
23
|
end
|
24
|
-
|
25
|
-
def #{current_component}_describer_method(describer)
|
26
|
-
raise NotImplementedError
|
27
|
-
end
|
28
24
|
end
|
29
25
|
CODE
|
30
26
|
end
|
@@ -15,10 +15,8 @@ module ActiveKit
|
|
15
15
|
options.store(:attributes, {})
|
16
16
|
@describers.store(name, options)
|
17
17
|
@current_class.class_eval <<-CODE, __FILE__, __LINE__ + 1
|
18
|
-
def self.#{name}
|
19
|
-
|
20
|
-
#{@current_component}_describer_method(describer)
|
21
|
-
end
|
18
|
+
def self.#{name}(**params)
|
19
|
+
#{@current_component}er.run_describer_method("#{name}", params)
|
22
20
|
end
|
23
21
|
CODE
|
24
22
|
end
|
@@ -37,6 +35,45 @@ module ActiveKit
|
|
37
35
|
describer_options[:attributes].store(name, options)
|
38
36
|
end
|
39
37
|
end
|
38
|
+
|
39
|
+
describer_names
|
40
|
+
end
|
41
|
+
|
42
|
+
def run_describer_method(describer_name, params)
|
43
|
+
raise "Could not find describer while creating describer method." unless describer = find_describer_by(name: describer_name.to_sym)
|
44
|
+
describer_method(describer, params)
|
45
|
+
end
|
46
|
+
|
47
|
+
def describer_method(describer, params)
|
48
|
+
raise NotImplementedError
|
49
|
+
end
|
50
|
+
|
51
|
+
def for(describer_name)
|
52
|
+
describer_name = @describers.keys[0] if describer_name.nil?
|
53
|
+
raise "Could not find any describer name in #{@current_class.name}." if describer_name.blank?
|
54
|
+
|
55
|
+
describer_name = describer_name.to_sym
|
56
|
+
raise "Could not find describer '#{describer_name}' in #{@current_class.name}." unless @describers.dig(describer_name)
|
57
|
+
componenting = @describers.dig(describer_name, :componenting)
|
58
|
+
return componenting if componenting
|
59
|
+
|
60
|
+
@describers[describer_name][:componenting] = "ActiveKit::#{@current_component.to_s.titleize}::#{@current_component.to_s.titleize}ing".constantize.new(describer: find_describer_by(name: describer_name), current_class: @current_class)
|
61
|
+
@describers[describer_name][:componenting]
|
62
|
+
end
|
63
|
+
|
64
|
+
def get_describer_names
|
65
|
+
@describers.keys.map(&:to_s)
|
66
|
+
end
|
67
|
+
|
68
|
+
private
|
69
|
+
|
70
|
+
def create_default_describer
|
71
|
+
case @current_component
|
72
|
+
when :export
|
73
|
+
create_describer(:to_csv, kind: :csv, database: -> { ActiveRecord::Base.connection_db_config.database })
|
74
|
+
when :search
|
75
|
+
create_describer(:limit_by_search, database: -> { ActiveRecord::Base.connection_db_config.database })
|
76
|
+
end
|
40
77
|
end
|
41
78
|
|
42
79
|
def find_describer_by(name:)
|
@@ -57,8 +94,6 @@ module ActiveKit
|
|
57
94
|
OpenStruct.new(hash)
|
58
95
|
end
|
59
96
|
|
60
|
-
private
|
61
|
-
|
62
97
|
def build_describer_fields(describer_attributes)
|
63
98
|
describer_attributes.inject({}) do |fields_hash, (name, options)|
|
64
99
|
enclosed_attributes = Array(options.dig(:attributes))
|
@@ -108,13 +143,6 @@ module ActiveKit
|
|
108
143
|
def get_heading(options_heading)
|
109
144
|
options_heading.is_a?(Proc) ? options_heading.call(@current_class) : options_heading
|
110
145
|
end
|
111
|
-
|
112
|
-
def create_default_describer
|
113
|
-
case @current_component
|
114
|
-
when :export
|
115
|
-
create_describer(:to_csv, kind: :csv, database: -> { ActiveRecord::Base.connection_db_config.database.to_sym })
|
116
|
-
end
|
117
|
-
end
|
118
146
|
end
|
119
147
|
end
|
120
148
|
end
|
data/lib/active_kit/bedrock.rb
CHANGED
@@ -10,30 +10,6 @@ module ActiveKit
|
|
10
10
|
end
|
11
11
|
|
12
12
|
class_methods do
|
13
|
-
def export_describer_method(describer)
|
14
|
-
case describer.kind
|
15
|
-
when :csv
|
16
|
-
# The 'all' relation must be captured outside the Enumerator,
|
17
|
-
# else it will get reset to all the records of the class.
|
18
|
-
all_activerecord_relation = all.includes(describer.includes)
|
19
|
-
|
20
|
-
Enumerator.new do |yielder|
|
21
|
-
ActiveRecord::Base.connected_to(role: :writing, shard: describer.database.call) do
|
22
|
-
exporting = exporter.new_exporting(describer: describer)
|
23
|
-
|
24
|
-
# Add the headings.
|
25
|
-
yielder << CSV.generate_line(exporting.headings) if exporting.headings?
|
26
|
-
|
27
|
-
# Add the values.
|
28
|
-
# find_each will ignore any order if set earlier.
|
29
|
-
all_activerecord_relation.find_each do |record|
|
30
|
-
lines = exporting.lines_for(record: record)
|
31
|
-
lines.each { |line| yielder << CSV.generate_line(line) }
|
32
|
-
end
|
33
|
-
end
|
34
|
-
end
|
35
|
-
end
|
36
|
-
end
|
37
13
|
end
|
38
14
|
end
|
39
15
|
end
|
@@ -1,8 +1,29 @@
|
|
1
1
|
module ActiveKit
|
2
2
|
module Export
|
3
3
|
class Exporter < Bedrock::Bedrocker
|
4
|
-
def
|
5
|
-
|
4
|
+
def describer_method(describer, params)
|
5
|
+
case describer.kind
|
6
|
+
when :csv
|
7
|
+
# The 'all' relation must be captured outside the Enumerator,
|
8
|
+
# else it will get reset to all the records of the class.
|
9
|
+
all_activerecord_relation = @current_class.all.includes(describer.includes)
|
10
|
+
|
11
|
+
Enumerator.new do |yielder|
|
12
|
+
ActiveRecord::Base.connected_to(role: :writing, shard: describer.database.call.to_sym) do
|
13
|
+
exporting = self.for(describer.name)
|
14
|
+
|
15
|
+
# Add the headings.
|
16
|
+
yielder << CSV.generate_line(exporting.headings) if exporting.headings?
|
17
|
+
|
18
|
+
# Add the values.
|
19
|
+
# find_each will ignore any order if set earlier.
|
20
|
+
all_activerecord_relation.find_each do |record|
|
21
|
+
lines = exporting.lines_for(record: record)
|
22
|
+
lines.each { |line| yielder << CSV.generate_line(line) }
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
6
27
|
end
|
7
28
|
end
|
8
29
|
end
|
@@ -3,19 +3,21 @@ module ActiveKit
|
|
3
3
|
class Index
|
4
4
|
attr_reader :prefix, :schema, :attribute_value_parser
|
5
5
|
|
6
|
-
def initialize(current_class:)
|
6
|
+
def initialize(current_class:, describer:)
|
7
7
|
@redis = ActiveKit::Search.redis
|
8
8
|
@current_class = current_class
|
9
|
+
@describer = describer
|
9
10
|
|
10
11
|
current_class_name = current_class.to_s.parameterize.pluralize
|
11
|
-
|
12
|
-
@
|
12
|
+
describer_name = describer.name.to_s
|
13
|
+
@name = "activekit:search:index:#{current_class_name}:#{describer_name}"
|
14
|
+
@prefix = "activekit:search:#{current_class_name}:#{describer_name}"
|
13
15
|
@schema = {}
|
14
16
|
@attribute_value_parser = {}
|
15
17
|
end
|
16
18
|
|
17
19
|
def add_attribute_to_schema(name:, options:)
|
18
|
-
raise "Error: No type specified for the search attribute #{name}." unless options[:type].present?
|
20
|
+
raise "Error: No type specified for the search attribute '#{name}'." unless options[:type].present?
|
19
21
|
|
20
22
|
attribute_schema = []
|
21
23
|
|
@@ -137,7 +139,7 @@ module ActiveKit
|
|
137
139
|
end
|
138
140
|
end
|
139
141
|
|
140
|
-
query = "@database:{#{escape_separators(
|
142
|
+
query = "@database:{#{escape_separators(@describer.database.call, include_space: true)}}#{term}#{tags}#{modifiers}"
|
141
143
|
command = [
|
142
144
|
"FT.SEARCH",
|
143
145
|
@name,
|
@@ -1,16 +1,17 @@
|
|
1
1
|
module ActiveKit
|
2
2
|
module Search
|
3
3
|
class Key
|
4
|
-
def initialize(index:)
|
4
|
+
def initialize(index:, describer:)
|
5
5
|
@redis = ActiveKit::Search.redis
|
6
6
|
@index = index
|
7
|
+
@describer = describer
|
7
8
|
end
|
8
9
|
|
9
10
|
def reload(record:)
|
10
11
|
clear(record: record)
|
11
12
|
|
12
13
|
hash_key = key(record: record)
|
13
|
-
hash_value = { "database" =>
|
14
|
+
hash_value = { "database" => @describer.database.call, "id" => record.id }
|
14
15
|
@index.schema.each do |field_name, field_value|
|
15
16
|
attribute_name = field_name
|
16
17
|
attribute_value = @index.attribute_value_parser[field_name]&.call(record) || record.public_send(field_name)
|
@@ -37,7 +38,7 @@ module ActiveKit
|
|
37
38
|
private
|
38
39
|
|
39
40
|
def key(record:)
|
40
|
-
"#{@index.prefix}:#{
|
41
|
+
"#{@index.prefix}:#{@describer.database.call}:#{record.id}"
|
41
42
|
end
|
42
43
|
end
|
43
44
|
end
|
@@ -0,0 +1,44 @@
|
|
1
|
+
module ActiveKit
|
2
|
+
module Search
|
3
|
+
class Manager
|
4
|
+
def initialize(given_class:, given_describer:)
|
5
|
+
@given_class = given_class
|
6
|
+
@given_describer = given_describer
|
7
|
+
|
8
|
+
Rails.application.eager_load!
|
9
|
+
end
|
10
|
+
|
11
|
+
def reload
|
12
|
+
task(name: :reload, log_name: "Reloading")
|
13
|
+
end
|
14
|
+
|
15
|
+
def clear
|
16
|
+
task(name: :clear, log_name: "Clearing")
|
17
|
+
end
|
18
|
+
|
19
|
+
def drop
|
20
|
+
task(name: :drop, log_name: "Dropping")
|
21
|
+
end
|
22
|
+
|
23
|
+
private
|
24
|
+
|
25
|
+
def task(name:, log_name: "Reloading")
|
26
|
+
models = @given_class.present? ? [@given_class] : ActiveRecord::Base.descendants.collect(&:name)
|
27
|
+
# Removing these models for efficiency as they will never contain searcher.
|
28
|
+
models -= ["ApplicationRecord", "ActionText::Record", "ActionText::RichText", "ActiveKit::ApplicationRecord", "ActionMailbox::Record", "ActionMailbox::InboundEmail", "ActiveStorage::Record", "ActiveStorage::Blob", "ActiveStorage::VariantRecord", "ActiveStorage::Attachment"]
|
29
|
+
models.each do |model|
|
30
|
+
model_const = model.constantize
|
31
|
+
if model_const.try(:searcher)
|
32
|
+
describer_names = @given_describer.present? ? [@given_describer] : model_const.searcher.get_describer_names
|
33
|
+
describer_names.each do |describer_name|
|
34
|
+
if model_const.searcher.for(describer_name).attributes_present?
|
35
|
+
puts "ActiveKit::Search | #{log_name}: #{model}"
|
36
|
+
model_const.searcher.for(describer_name).public_send(name)
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
@@ -3,124 +3,14 @@ require 'active_support/concern'
|
|
3
3
|
module ActiveKit
|
4
4
|
module Search
|
5
5
|
module Searchable
|
6
|
+
extend Bedrock::Bedrockable
|
6
7
|
extend ActiveSupport::Concern
|
7
8
|
|
8
9
|
included do
|
9
10
|
end
|
10
11
|
|
11
12
|
class_methods do
|
12
|
-
def searcher
|
13
|
-
@searcher ||= ActiveKit::Search::Search.new(current_class: self)
|
14
|
-
end
|
15
|
-
|
16
|
-
def searching(term: nil, **options)
|
17
|
-
options[:page] = 1 if options.key?(:page) && options[:page].blank?
|
18
|
-
searcher.fetch(term: term, **options).records
|
19
|
-
end
|
20
|
-
|
21
|
-
def search_attribute(name, **options)
|
22
|
-
options.deep_symbolize_keys!
|
23
|
-
|
24
|
-
set_activekit_search_callbacks unless searcher.attributes_present?
|
25
|
-
depends_on = options.delete(:depends_on) || {}
|
26
|
-
set_activekit_search_depends_on_callbacks(depends_on: depends_on) unless depends_on.empty?
|
27
|
-
|
28
|
-
searcher.add_attribute(name: name, options: options)
|
29
|
-
end
|
30
|
-
|
31
|
-
def set_activekit_search_callbacks
|
32
|
-
after_commit do
|
33
|
-
self.class.searcher.reload(record: self)
|
34
|
-
logger.info "ActiveKit::Search | Indexing from #{self.class.name}: Done."
|
35
|
-
end
|
36
|
-
end
|
37
|
-
|
38
|
-
def set_activekit_search_depends_on_callbacks(depends_on:)
|
39
|
-
depends_on.each do |depends_on_association, depends_on_inverse|
|
40
|
-
klass = self.reflect_on_all_associations.map { |assoc| [assoc.name, assoc.klass.name] }.to_h[depends_on_association]
|
41
|
-
klass.constantize.class_eval do
|
42
|
-
after_commit do
|
43
|
-
inverse_assoc = self.public_send(depends_on_inverse)
|
44
|
-
if inverse_assoc.respond_to?(:each)
|
45
|
-
inverse_assoc.each { |instance| instance.class.searcher.reload(record: instance) }
|
46
|
-
else
|
47
|
-
inverse_assoc.class.searcher.reload(record: inverse_assoc)
|
48
|
-
end
|
49
|
-
logger.info "ActiveKit::Search | Indexing from #{self.class.name}: Done."
|
50
|
-
end
|
51
|
-
end
|
52
|
-
end
|
53
|
-
end
|
54
13
|
end
|
55
14
|
end
|
56
15
|
end
|
57
16
|
end
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
# require 'active_support/concern'
|
63
|
-
|
64
|
-
# module ActiveKit
|
65
|
-
# module Search
|
66
|
-
# module Searchable
|
67
|
-
# extend ActiveSupport::Concern
|
68
|
-
|
69
|
-
# included do
|
70
|
-
# end
|
71
|
-
|
72
|
-
# class_methods do
|
73
|
-
# def searcher
|
74
|
-
# @searcher ||= ActiveKit::Search::Searcher.new(current_class: self)
|
75
|
-
# end
|
76
|
-
|
77
|
-
# def search_describer(name, **options)
|
78
|
-
# name = name.to_sym
|
79
|
-
# options.deep_symbolize_keys!
|
80
|
-
|
81
|
-
# unless searcher.find_describer_by(describer_name: name)
|
82
|
-
# searcher.new_describer(name: name, options: options)
|
83
|
-
# define_search_describer_method(kind: options[:kind], name: name)
|
84
|
-
# end
|
85
|
-
# end
|
86
|
-
|
87
|
-
# def search_attribute(name, **options)
|
88
|
-
# search_describer(:to_csv, kind: :csv, database: -> { ActiveRecord::Base.connection_db_config.database.to_sym }) unless searcher.describers?
|
89
|
-
|
90
|
-
# options.deep_symbolize_keys!
|
91
|
-
# searcher.new_attribute(name: name.to_sym, options: options)
|
92
|
-
# end
|
93
|
-
|
94
|
-
# def define_search_describer_method(kind:, name:)
|
95
|
-
# case kind
|
96
|
-
# when :csv
|
97
|
-
# define_singleton_method name do
|
98
|
-
# describer = exporter.find_describer_by(describer_name: name)
|
99
|
-
# raise "could not find describer for the describer name '#{name}'" unless describer.present?
|
100
|
-
|
101
|
-
# # The 'all' relation must be captured outside the Enumerator,
|
102
|
-
# # else it will get reset to all the records of the class.
|
103
|
-
# all_activerecord_relation = all.includes(describer.includes)
|
104
|
-
|
105
|
-
# Enumerator.new do |yielder|
|
106
|
-
# ActiveRecord::Base.connected_to(role: :writing, shard: describer.database.call) do
|
107
|
-
# exporting = exporter.new_exporting(describer: describer)
|
108
|
-
|
109
|
-
# # Add the headings.
|
110
|
-
# yielder << CSV.generate_line(exporting.headings) if exporting.headings?
|
111
|
-
|
112
|
-
# # Add the values.
|
113
|
-
# # find_each will ignore any order if set earlier.
|
114
|
-
# all_activerecord_relation.find_each do |record|
|
115
|
-
# lines = exporting.lines_for(record: record)
|
116
|
-
# lines.each { |line| yielder << CSV.generate_line(line) }
|
117
|
-
# end
|
118
|
-
# end
|
119
|
-
# end
|
120
|
-
# end
|
121
|
-
# end
|
122
|
-
# end
|
123
|
-
# end
|
124
|
-
# end
|
125
|
-
# end
|
126
|
-
# end
|
@@ -1,106 +1,51 @@
|
|
1
|
-
|
2
|
-
|
3
|
-
|
4
|
-
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
#
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
#
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
# private
|
54
|
-
|
55
|
-
# def build_describer_fields(describer_attributes)
|
56
|
-
# describer_attributes.inject({}) do |fields_hash, (name, options)|
|
57
|
-
# enclosed_attributes = Array(options.dig(:attributes))
|
58
|
-
|
59
|
-
# if enclosed_attributes.blank?
|
60
|
-
# field_key, field_value = (get_heading(options.dig(:heading))&.to_s || name.to_s.titleize), (options.dig(:value) || name)
|
61
|
-
# else
|
62
|
-
# field_key, field_value = get_nested_field(name, options, enclosed_attributes)
|
63
|
-
# end
|
64
|
-
# fields_hash.store(field_key, field_value)
|
65
|
-
|
66
|
-
# fields_hash
|
67
|
-
# end
|
68
|
-
# end
|
69
|
-
|
70
|
-
# def get_nested_field(name, options, enclosed_attributes, ancestor_heading = nil)
|
71
|
-
# parent_heading = ancestor_heading.present? ? ancestor_heading : ""
|
72
|
-
# parent_heading += (get_heading(options.dig(:heading))&.to_s || name.to_s.singularize.titleize) + " "
|
73
|
-
# parent_value = options.dig(:value) || name
|
74
|
-
|
75
|
-
# enclosed_attributes.inject([[], [parent_value]]) do |nested_field, enclosed_attribute|
|
76
|
-
# unless enclosed_attribute.is_a? Hash
|
77
|
-
# nested_field_key = parent_heading + enclosed_attribute.to_s.titleize
|
78
|
-
# nested_field_val = enclosed_attribute
|
79
|
-
|
80
|
-
# nested_field[0].push(nested_field_key)
|
81
|
-
# nested_field[1].push(nested_field_val)
|
82
|
-
# else
|
83
|
-
# enclosed_attribute.each do |enclosed_attribute_key, enclosed_attribute_value|
|
84
|
-
# wrapped_attributes = Array(enclosed_attribute_value.dig(:attributes))
|
85
|
-
# if wrapped_attributes.blank?
|
86
|
-
# nested_field_key = parent_heading + (get_heading(enclosed_attribute_value.dig(:heading))&.to_s || enclosed_attribute_key.to_s.titleize)
|
87
|
-
# nested_field_val = enclosed_attribute_value.dig(:value) || enclosed_attribute_key
|
88
|
-
# else
|
89
|
-
# nested_field_key, nested_field_val = get_nested_field(enclosed_attribute_key, enclosed_attribute_value, wrapped_attributes, parent_heading)
|
90
|
-
# end
|
91
|
-
|
92
|
-
# nested_field[0].push(nested_field_key)
|
93
|
-
# nested_field[1].push(nested_field_val)
|
94
|
-
# end
|
95
|
-
# end
|
96
|
-
|
97
|
-
# nested_field
|
98
|
-
# end
|
99
|
-
# end
|
100
|
-
|
101
|
-
# def get_heading(options_heading)
|
102
|
-
# options_heading.is_a?(Proc) ? options_heading.call(@current_class) : options_heading
|
103
|
-
# end
|
104
|
-
# end
|
105
|
-
# end
|
106
|
-
# end
|
1
|
+
module ActiveKit
|
2
|
+
module Search
|
3
|
+
class Searcher < Bedrock::Bedrocker
|
4
|
+
def create_attribute(name, options)
|
5
|
+
describer_names = super
|
6
|
+
|
7
|
+
depends_on = options.delete(:depends_on) || {}
|
8
|
+
describer_names.each do |describer_name|
|
9
|
+
set_reload_callbacks(depends_on, describer_name)
|
10
|
+
self.for(describer_name).add_attribute(name: name, options: options.deep_dup)
|
11
|
+
end
|
12
|
+
end
|
13
|
+
|
14
|
+
def describer_method(describer, params)
|
15
|
+
params[:page] = 1 if params.key?(:page) && params[:page].blank?
|
16
|
+
self.for(describer.name).fetch(term: params.delete(:term), **params).records
|
17
|
+
end
|
18
|
+
|
19
|
+
private
|
20
|
+
|
21
|
+
# Set callbacks for current class and depending classes.
|
22
|
+
def set_reload_callbacks(depends_on, describer_name)
|
23
|
+
@current_class.class_eval do
|
24
|
+
unless searcher.for(describer_name).attributes_present?
|
25
|
+
after_commit do
|
26
|
+
self.class.searcher.for(describer_name).reload(record: self)
|
27
|
+
logger.info "ActiveKit::Search | Indexing from #{self.class.name}: Done."
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
unless depends_on.empty?
|
32
|
+
depends_on.each do |depends_on_association, depends_on_inverse|
|
33
|
+
klass = self.reflect_on_all_associations.map { |assoc| [assoc.name, assoc.klass.name] }.to_h[depends_on_association]
|
34
|
+
klass.constantize.class_eval do
|
35
|
+
after_commit do
|
36
|
+
inverse_assoc = self.public_send(depends_on_inverse)
|
37
|
+
if inverse_assoc.respond_to?(:each)
|
38
|
+
inverse_assoc.each { |instance| instance.class.searcher.for(describer_name).reload(record: instance) }
|
39
|
+
else
|
40
|
+
inverse_assoc.class.searcher.for(describer_name).reload(record: inverse_assoc)
|
41
|
+
end
|
42
|
+
logger.info "ActiveKit::Search | Indexing from #{self.class.name}: Done."
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
@@ -1,104 +1,68 @@
|
|
1
|
-
|
2
|
-
|
3
|
-
|
4
|
-
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
# subrow.push(assoc_record.public_send(v))
|
70
|
-
# subrow_column_counter += 1
|
71
|
-
# elsif v.is_a? Array
|
72
|
-
# deeprows = get_deeprows(assoc_record, heading, v, (column_counter + subrow_column_counter))
|
73
|
-
|
74
|
-
# column_count = get_column_count_for(v)
|
75
|
-
# column_count.times { |i| subrow.push(nil) }
|
76
|
-
# subrow_column_counter += column_count
|
77
|
-
# end
|
78
|
-
|
79
|
-
# subrow
|
80
|
-
# end
|
81
|
-
|
82
|
-
# subrows.push(subrow)
|
83
|
-
# deeprows.each { |deeprow| subrows.push(deeprow) }
|
84
|
-
# end
|
85
|
-
|
86
|
-
# subrows
|
87
|
-
# end
|
88
|
-
|
89
|
-
# def get_column_count_for(value)
|
90
|
-
# count = 0
|
91
|
-
|
92
|
-
# value.each do |v|
|
93
|
-
# unless v.is_a? Array
|
94
|
-
# count += 1
|
95
|
-
# else
|
96
|
-
# count += get_column_count_for(v)
|
97
|
-
# end
|
98
|
-
# end
|
99
|
-
|
100
|
-
# count - 1
|
101
|
-
# end
|
102
|
-
# end
|
103
|
-
# end
|
104
|
-
# end
|
1
|
+
module ActiveKit
|
2
|
+
module Search
|
3
|
+
class Searching < Bedrock::Bedrocking
|
4
|
+
attr_reader :current_page, :previous_page, :next_page
|
5
|
+
|
6
|
+
def initialize(describer:, current_class:)
|
7
|
+
super
|
8
|
+
|
9
|
+
@index = Index.new(current_class: @current_class, describer: describer)
|
10
|
+
@key = Key.new(index: @index, describer: describer)
|
11
|
+
@suggestion = Suggestion.new(current_class: @current_class, describer: describer)
|
12
|
+
end
|
13
|
+
|
14
|
+
def reload(record: nil)
|
15
|
+
record ? @key.reload(record: record) : @current_class.all.each { |rec| @key.reload(record: rec) }
|
16
|
+
@index.reload
|
17
|
+
end
|
18
|
+
|
19
|
+
def clear(record: nil)
|
20
|
+
record ? @key.clear(record: record) : @current_class.all.each { |rec| @key.clear(record: rec) }
|
21
|
+
@index.reload
|
22
|
+
end
|
23
|
+
|
24
|
+
def fetch(**options)
|
25
|
+
search_result = @index.fetch(**options)
|
26
|
+
|
27
|
+
if search_result.keys.any?
|
28
|
+
@suggestion.add(term: search_result.term)
|
29
|
+
else
|
30
|
+
@suggestion.del(term: search_result.term)
|
31
|
+
end
|
32
|
+
|
33
|
+
@current_page = search_result.current_page
|
34
|
+
@previous_page = search_result.previous_page
|
35
|
+
@next_page = search_result.next_page
|
36
|
+
|
37
|
+
search_result
|
38
|
+
end
|
39
|
+
|
40
|
+
def suggestions(prefix:)
|
41
|
+
@suggestion.fetch(prefix: prefix)
|
42
|
+
end
|
43
|
+
|
44
|
+
def drop
|
45
|
+
total_count = @index.fetch(offset: 0, limit: 0).count
|
46
|
+
keys = @index.fetch(offset: 0, limit: total_count).keys
|
47
|
+
@key.drop(keys: keys)
|
48
|
+
@index.drop
|
49
|
+
end
|
50
|
+
|
51
|
+
def previous_page?
|
52
|
+
!!@previous_page
|
53
|
+
end
|
54
|
+
|
55
|
+
def next_page?
|
56
|
+
!!@next_page
|
57
|
+
end
|
58
|
+
|
59
|
+
def add_attribute(name:, options:)
|
60
|
+
@index.add_attribute_to_schema(name: name, options: options)
|
61
|
+
end
|
62
|
+
|
63
|
+
def attributes_present?
|
64
|
+
@index.schema.present?
|
65
|
+
end
|
66
|
+
end
|
67
|
+
end
|
68
|
+
end
|
@@ -1,10 +1,12 @@
|
|
1
1
|
module ActiveKit
|
2
2
|
module Search
|
3
3
|
class Suggestion
|
4
|
-
def initialize(current_class:)
|
4
|
+
def initialize(current_class:, describer:)
|
5
5
|
@redis = ActiveKit::Search.redis
|
6
6
|
@current_class = current_class
|
7
|
+
@describer = describer
|
7
8
|
@current_class_name = current_class.to_s.parameterize.pluralize
|
9
|
+
@describer_name = describer.name.to_s
|
8
10
|
end
|
9
11
|
|
10
12
|
def add(term:, score: 1, increment: true)
|
@@ -32,7 +34,7 @@ module ActiveKit
|
|
32
34
|
private
|
33
35
|
|
34
36
|
def key
|
35
|
-
"activekit:search:suggestions:#{@current_class_name}:#{
|
37
|
+
"activekit:search:suggestions:#{@current_class_name}:#{@describer_name}:#{@describer.database.call}"
|
36
38
|
end
|
37
39
|
end
|
38
40
|
end
|
data/lib/active_kit/search.rb
CHANGED
data/lib/active_kit/version.rb
CHANGED
data/lib/tasks/search_tasks.rake
CHANGED
@@ -1,84 +1,21 @@
|
|
1
1
|
namespace :active_kit do
|
2
|
-
desc "bundle exec rails active_kit:boot DOMAIN='www.yourdomain.com'"
|
3
|
-
task boot: [:environment] do
|
4
|
-
# Database & Preferences
|
5
|
-
domain = ENV['DOMAIN'] || raise("DOMAIN not specified")
|
6
|
-
|
7
|
-
tenant = nil
|
8
|
-
shard_name = nil
|
9
|
-
|
10
|
-
# Returns the first db config for a specific environment where it is assumed that all tenants data is stored.
|
11
|
-
default_shard_name = ActiveRecord::Base.configurations.find_db_config(Rails.env).name
|
12
|
-
|
13
|
-
ActiveRecord::Base.connected_to(role: :writing, shard: default_shard_name.to_sym) do
|
14
|
-
tenant = System::Tenant.where(domain: domain).or(System::Tenant.where(custom_domain: domain)).select(:database, :storage, :domain, :custom_domain).first
|
15
|
-
shard_name = tenant.database
|
16
|
-
raise RuntimeError, 'Could not set shard name.' unless shard_name.present? # TODO: In future, redirect this to "Nothing Here Yet" page
|
17
|
-
end
|
18
|
-
|
19
|
-
ApplicationRecord.connects_to database: { writing: "#{shard_name}".to_sym }
|
20
|
-
|
21
|
-
System::Current.tenant = OpenStruct.new(tenant.serializable_hash)
|
22
|
-
System::Current.preferences = System::Preference.revealed
|
23
|
-
System::Current.integrations = System::Integration.revealed
|
24
|
-
end
|
25
|
-
|
26
2
|
namespace :search do
|
27
|
-
desc "bundle exec rails active_kit:search:reload CLASS='Article'
|
28
|
-
task reload
|
29
|
-
|
30
|
-
|
31
|
-
puts "ActiveKit::Search | Reloading: #{ENV['CLASS']}"
|
32
|
-
ENV['CLASS'].constantize.searcher.reload
|
33
|
-
end
|
34
|
-
else
|
35
|
-
Rails.application.eager_load!
|
36
|
-
models = ApplicationRecord.descendants.collect(&:name)
|
37
|
-
models.each do |model|
|
38
|
-
if model.constantize.searcher.attributes_present?
|
39
|
-
puts "ActiveKit::Search | Reloading: #{model}"
|
40
|
-
model.constantize.searcher.reload
|
41
|
-
end
|
42
|
-
end
|
43
|
-
end
|
3
|
+
desc "bundle exec rails active_kit:search:reload CLASS='Article' DESCRIBER='limit_by_search'"
|
4
|
+
task :reload do
|
5
|
+
manager = ActiveKit::Search::Manager.new(given_class: ENV['CLASS'], given_describer: ENV['DESCRIBER'])
|
6
|
+
manager.reload
|
44
7
|
end
|
45
8
|
|
46
|
-
desc "bundle exec rails active_kit:search:clear CLASS='Article'
|
47
|
-
task clear
|
48
|
-
|
49
|
-
|
50
|
-
puts "ActiveKit::Search | Clearing: #{ENV['CLASS']}"
|
51
|
-
ENV['CLASS'].constantize.searcher.clear
|
52
|
-
end
|
53
|
-
else
|
54
|
-
Rails.application.eager_load!
|
55
|
-
models = ApplicationRecord.descendants.collect(&:name)
|
56
|
-
models.each do |model|
|
57
|
-
if model.constantize.searcher.attributes_present?
|
58
|
-
puts "ActiveKit::Search | Clearing: #{model}"
|
59
|
-
model.constantize.searcher.clear
|
60
|
-
end
|
61
|
-
end
|
62
|
-
end
|
9
|
+
desc "bundle exec rails active_kit:search:clear CLASS='Article' DESCRIBER='limit_by_search'"
|
10
|
+
task :clear do
|
11
|
+
manager = ActiveKit::Search::Manager.new(given_class: ENV['CLASS'], given_describer: ENV['DESCRIBER'])
|
12
|
+
manager.clear
|
63
13
|
end
|
64
14
|
|
65
|
-
desc "bundle exec rails active_kit:search:drop CLASS='Article'
|
66
|
-
task drop
|
67
|
-
|
68
|
-
|
69
|
-
puts "ActiveKit::Search | Dropping: #{ENV['CLASS']}"
|
70
|
-
ENV['CLASS'].constantize.searcher.drop
|
71
|
-
end
|
72
|
-
else
|
73
|
-
Rails.application.eager_load!
|
74
|
-
models = ApplicationRecord.descendants.collect(&:name)
|
75
|
-
models.each do |model|
|
76
|
-
if model.constantize.searcher.attributes_present?
|
77
|
-
puts "ActiveKit::Search | Dropping: #{model}"
|
78
|
-
model.constantize.searcher.drop
|
79
|
-
end
|
80
|
-
end
|
81
|
-
end
|
15
|
+
desc "bundle exec rails active_kit:search:drop CLASS='Article' DESCRIBER='limit_by_search'"
|
16
|
+
task :drop do
|
17
|
+
manager = ActiveKit::Search::Manager.new(given_class: ENV['CLASS'], given_describer: ENV['DESCRIBER'])
|
18
|
+
manager.drop
|
82
19
|
end
|
83
20
|
end
|
84
21
|
end
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: activekit
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.5.0
|
4
|
+
version: 0.5.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- plainsource
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2024-04-
|
11
|
+
date: 2024-04-24 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: rails
|
@@ -53,6 +53,7 @@ files:
|
|
53
53
|
- lib/active_kit/bedrock.rb
|
54
54
|
- lib/active_kit/bedrock/bedrockable.rb
|
55
55
|
- lib/active_kit/bedrock/bedrocker.rb
|
56
|
+
- lib/active_kit/bedrock/bedrocking.rb
|
56
57
|
- lib/active_kit/engine.rb
|
57
58
|
- lib/active_kit/export.rb
|
58
59
|
- lib/active_kit/export/exportable.rb
|
@@ -67,7 +68,7 @@ files:
|
|
67
68
|
- lib/active_kit/search.rb
|
68
69
|
- lib/active_kit/search/index.rb
|
69
70
|
- lib/active_kit/search/key.rb
|
70
|
-
- lib/active_kit/search/
|
71
|
+
- lib/active_kit/search/manager.rb
|
71
72
|
- lib/active_kit/search/search_result.rb
|
72
73
|
- lib/active_kit/search/searchable.rb
|
73
74
|
- lib/active_kit/search/searcher.rb
|
@@ -94,9 +95,9 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
94
95
|
version: '0'
|
95
96
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
96
97
|
requirements:
|
97
|
-
- - "
|
98
|
+
- - ">="
|
98
99
|
- !ruby/object:Gem::Version
|
99
|
-
version:
|
100
|
+
version: '0'
|
100
101
|
requirements: []
|
101
102
|
rubygems_version: 3.1.2
|
102
103
|
signing_key:
|
@@ -1,68 +0,0 @@
|
|
1
|
-
module ActiveKit
|
2
|
-
module Search
|
3
|
-
class Search
|
4
|
-
attr_reader :current_page, :previous_page, :next_page
|
5
|
-
|
6
|
-
def initialize(current_class:)
|
7
|
-
@current_class = current_class
|
8
|
-
|
9
|
-
@index = Index.new(current_class: @current_class)
|
10
|
-
@key = Key.new(index: @index)
|
11
|
-
@suggestion = Suggestion.new(current_class: @current_class)
|
12
|
-
end
|
13
|
-
|
14
|
-
def reload(record: nil)
|
15
|
-
record ? @key.reload(record: record) : @current_class.all.each { |rec| @key.reload(record: rec) }
|
16
|
-
@index.reload
|
17
|
-
end
|
18
|
-
|
19
|
-
def clear(record: nil)
|
20
|
-
record ? @key.clear(record: record) : @current_class.all.each { |rec| @key.clear(record: rec) }
|
21
|
-
@index.reload
|
22
|
-
end
|
23
|
-
|
24
|
-
def fetch(**options)
|
25
|
-
search_result = @index.fetch(**options)
|
26
|
-
|
27
|
-
if search_result.keys.any?
|
28
|
-
@suggestion.add(term: search_result.term)
|
29
|
-
else
|
30
|
-
@suggestion.del(term: search_result.term)
|
31
|
-
end
|
32
|
-
|
33
|
-
@current_page = search_result.current_page
|
34
|
-
@previous_page = search_result.previous_page
|
35
|
-
@next_page = search_result.next_page
|
36
|
-
|
37
|
-
search_result
|
38
|
-
end
|
39
|
-
|
40
|
-
def suggestions(prefix:)
|
41
|
-
@suggestion.fetch(prefix: prefix)
|
42
|
-
end
|
43
|
-
|
44
|
-
def drop
|
45
|
-
total_count = @index.fetch(offset: 0, limit: 0).count
|
46
|
-
keys = @index.fetch(offset: 0, limit: total_count).keys
|
47
|
-
@key.drop(keys: keys)
|
48
|
-
@index.drop
|
49
|
-
end
|
50
|
-
|
51
|
-
def previous_page?
|
52
|
-
!!@previous_page
|
53
|
-
end
|
54
|
-
|
55
|
-
def next_page?
|
56
|
-
!!@next_page
|
57
|
-
end
|
58
|
-
|
59
|
-
def add_attribute(name:, options:)
|
60
|
-
@index.add_attribute_to_schema(name: name, options: options)
|
61
|
-
end
|
62
|
-
|
63
|
-
def attributes_present?
|
64
|
-
@index.schema.present?
|
65
|
-
end
|
66
|
-
end
|
67
|
-
end
|
68
|
-
end
|