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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 7fe66b1b57a945fa39f8d3daad8daa1dd3d564d190fb302453e1e1d9b4bc64e8
4
- data.tar.gz: '0026541966d64d4fb509ae32dbd69715cf86d8dd79abd51537aee335bb0da393'
3
+ metadata.gz: 71b9eb7bd5fc27d2794cd15b8ec08a68e9baae06e37e072188260ddbaaa25473
4
+ data.tar.gz: 33f906a96b417c86b8048a2f663dbac5d4a6b0595f9cea23b6a0db92f98982cb
5
5
  SHA512:
6
- metadata.gz: ebc4d83438c610266f4de8c72395199f71282f5e8b00cfdebcf9e532950094997f520b0dd3e65a85e05f5e62fae365e5eaebc770691c4fcb2131a0ce3225bbc1
7
- data.tar.gz: bbb51f533e437c2de942dcc5a8d9120b3f1cf0317b138c0b924cd433a48bbe6c145b4c77e3eaf0160c2a37d47d0d35df07916a090fbd237bc4929c7de8b0d790
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.to_sym }
27
- export_describer :to_csv, kind: :csv, database: -> { System::Current.tenant.database.to_sym }
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
- if describer = #{@current_component}er.find_describer_by(name: "#{name}".to_sym)
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
@@ -0,0 +1,10 @@
1
+ module ActiveKit
2
+ module Bedrock
3
+ class Bedrocking
4
+ def initialize(describer:, current_class:)
5
+ @describer = describer
6
+ @current_class = current_class
7
+ end
8
+ end
9
+ end
10
+ end
@@ -3,5 +3,6 @@ module ActiveKit
3
3
  extend ActiveSupport::Autoload
4
4
 
5
5
  autoload :Bedrocker
6
+ autoload :Bedrocking
6
7
  end
7
8
  end
@@ -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 new_exporting(describer:)
5
- Exporting.new(describer: describer)
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
@@ -1,11 +1,6 @@
1
1
  module ActiveKit
2
2
  module Export
3
- class Exporting
4
-
5
- def initialize(describer:)
6
- @describer = describer
7
- end
8
-
3
+ class Exporting < Bedrock::Bedrocking
9
4
  def headings
10
5
  @headings ||= @describer.fields.keys.flatten
11
6
  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
- @name = "activekit:search:index:#{current_class_name}"
12
- @prefix = "activekit:search:#{current_class_name}"
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(System::Current.tenant.database, include_space: true)}}#{term}#{tags}#{modifiers}"
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" => System::Current.tenant.database, "id" => record.id }
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}:#{System::Current.tenant.database}:#{record.id}"
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
- # module ActiveKit
2
- # module Search
3
- # class Searcher
4
- # attr_reader :describers
5
-
6
- # def initialize(current_class:)
7
- # @current_class = current_class
8
- # @describers = {}
9
- # end
10
-
11
- # def find_describer_by(describer_name:)
12
- # describer_options = @describers.dig(describer_name)
13
- # return nil unless describer_options.present?
14
-
15
- # describer_attributes = describer_options[:attributes]
16
- # includes = describer_attributes.values.map { |options| options.dig(:includes) }.compact.flatten(1).uniq
17
- # fields = build_describer_fields(describer_attributes)
18
- # hash = {
19
- # name: describer_name,
20
- # kind: describer_options[:kind],
21
- # database: describer_options[:database],
22
- # attributes: describer_attributes,
23
- # includes: includes,
24
- # fields: fields
25
- # }
26
- # OpenStruct.new(hash)
27
- # end
28
-
29
- # def new_describer(name:, options:)
30
- # options.store(:attributes, {})
31
- # @describers.store(name, options)
32
- # end
33
-
34
- # def describers?
35
- # @describers.present?
36
- # end
37
-
38
- # def new_attribute(name:, options:)
39
- # describer_names = Array(options.delete(:describers))
40
- # describer_names = @describers.keys if describer_names.blank?
41
-
42
- # describer_names.each do |describer_name|
43
- # if describer_options = @describers.dig(describer_name)
44
- # describer_options[:attributes].store(name, options)
45
- # end
46
- # end
47
- # end
48
-
49
- # def new_exporting(describer:)
50
- # Exporting.new(describer: describer)
51
- # end
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
- # module ActiveKit
2
- # module Search
3
- # class Searching
4
-
5
- # def initialize(describer:)
6
- # @describer = describer
7
- # end
8
-
9
- # def headings
10
- # @headings ||= @describer.fields.keys.flatten
11
- # end
12
-
13
- # def headings?
14
- # headings.present?
15
- # end
16
-
17
- # def lines_for(record:)
18
- # row_counter, column_counter = 1, 0
19
-
20
- # @describer.fields.inject([[]]) do |rows, (heading, value)|
21
- # if value.is_a? Proc
22
- # rows[0].push(value.call(record))
23
- # column_counter += 1
24
- # elsif value.is_a?(Symbol) || value.is_a?(String)
25
- # rows[0].push(record.public_send(value))
26
- # column_counter += 1
27
- # elsif value.is_a? Array
28
- # deeprows = get_deeprows(record, heading, value, column_counter)
29
- # deeprows.each do |deeprow|
30
- # rows[row_counter] = deeprow
31
- # row_counter += 1
32
- # end
33
-
34
- # column_count = get_column_count_for(value)
35
- # column_count.times { |i| rows[0].push(nil) }
36
- # column_counter += column_count
37
- # else
38
- # raise "Could not identify '#{value}' for '#{heading}'."
39
- # end
40
-
41
- # rows
42
- # end
43
- # end
44
-
45
- # private
46
-
47
- # def get_deeprows(record, heading, value, column_counter)
48
- # value_clone = value.clone
49
- # assoc_value = value_clone.shift
50
-
51
- # if assoc_value.is_a? Proc
52
- # assoc_records = assoc_value.call(record)
53
- # elsif assoc_value.is_a?(Symbol) || assoc_value.is_a?(String)
54
- # assoc_records = record.public_send(assoc_value)
55
- # else
56
- # raise "Count not identity '#{assoc_value}' for '#{heading}'."
57
- # end
58
-
59
- # subrows = []
60
- # assoc_records.each do |assoc_record|
61
- # subrow, subrow_column_counter, deeprows = [], 0, []
62
- # column_counter.times { |i| subrow.push(nil) }
63
-
64
- # subrow = value_clone.inject(subrow) do |subrow, v|
65
- # if v.is_a? Proc
66
- # subrow.push(v.call(assoc_record))
67
- # subrow_column_counter += 1
68
- # elsif v.is_a?(Symbol) || v.is_a?(String)
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}:#{System::Current.tenant.database}"
37
+ "activekit:search:suggestions:#{@current_class_name}:#{@describer_name}:#{@describer.database.call}"
36
38
  end
37
39
  end
38
40
  end
@@ -4,7 +4,7 @@ module ActiveKit
4
4
 
5
5
  autoload :Index
6
6
  autoload :Key
7
- autoload :Search
7
+ autoload :Manager
8
8
  autoload :SearchResult
9
9
  autoload :Searcher
10
10
  autoload :Searching
@@ -1,3 +1,3 @@
1
1
  module ActiveKit
2
- VERSION = '0.5.0.dev8'
2
+ VERSION = '0.5.0'
3
3
  end
@@ -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' DOMAIN='www.yourdomain.com'"
28
- task reload: [:boot] do
29
- if ENV['CLASS']
30
- if ENV['CLASS'].constantize.searcher.attributes_present?
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' DOMAIN='www.yourdomain.com'"
47
- task clear: [:boot] do
48
- if ENV['CLASS']
49
- if ENV['CLASS'].constantize.searcher.attributes_present?
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' DOMAIN='www.yourdomain.com'"
66
- task drop: [:boot] do
67
- if ENV['CLASS']
68
- if ENV['CLASS'].constantize.searcher.attributes_present?
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.dev8
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-23 00:00:00.000000000 Z
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/search.rb
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: 1.3.1
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