activekit 0.5.0.dev8 → 0.5.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|