activekit 0.5.0.dev2 → 0.5.0.dev4

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: 2e2eea75f42af6dee1472a45419f36d2ac503c1aaa30745e0a513728bca2a7de
4
- data.tar.gz: 4a377406bcfab283361ce08307989ee06a4753af4470524a2d0cc3ef3e0a0f50
3
+ metadata.gz: eff7a049cd21979e76253d095e0d0ba722b289370a15426cf4b87c922d2bab66
4
+ data.tar.gz: 27b310f65547d08961b9044fe43bc606cfb78f0766487052c9488a2fa3e78ed8
5
5
  SHA512:
6
- metadata.gz: 03727406b7acdd0257e723fe771c0cb5f85b2c0a72e57b6d63735e9ad92ef4634b3e500233ec4cc585f450ae952d7f0802d4e86b9a9dbadb8bb51ea8e894baf2
7
- data.tar.gz: 5a518d157ac540f007be8c7b9c57010f68934ed9ad74ab8ba2fbce38a51517d3d2a0c90c74b616299f66e3e256dc1b7a467ee5d8ef258689e8c92e4e76fa66bd
6
+ metadata.gz: ce4750f37977c8e2a63a6ec85ad4def621877f3789eb64e598d214066f840463c3cbe7a60b5e4853d9a639a2f83cd480a953c0436535547acf8a9b55b5113de9
7
+ data.tar.gz: c3813bb2b5df232389c25c8bc0ecf35f5875d6fdc34060d67993643f728bca9c0b6d0d81c85abed349dfb52bef8b0ac2a392ce88ce72a37adc722c3d533e1f64
@@ -0,0 +1,39 @@
1
+ require 'active_support/concern'
2
+
3
+ module ActiveKit
4
+ module Base
5
+ module Baseable
6
+ extend ActiveSupport::Concern
7
+
8
+ included do
9
+ end
10
+
11
+ class_methods do
12
+ def define_activekit_describer(baser, name, options)
13
+ name = name.to_sym
14
+ options.deep_symbolize_keys!
15
+
16
+ unless baser.find_describer_by(describer_name: name)
17
+ baser.new_describer(name: name, options: options)
18
+
19
+ define_singleton_method name do
20
+ describer = baser.find_describer_by(describer_name: name)
21
+ raise "could not find describer for the describer name '#{name}'" unless describer.present?
22
+
23
+ yield(describer) if block_given?
24
+ end
25
+ end
26
+ end
27
+
28
+ def define_activekit_attribute(baser, name, options)
29
+ define_activekit_describer(baser, :to_csv, kind: :csv, database: -> { ActiveRecord::Base.connection_db_config.database.to_sym }) unless baser.describers?
30
+
31
+ options.deep_symbolize_keys!
32
+ baser.new_attribute(name: name.to_sym, options: options)
33
+
34
+ yield if block_given?
35
+ end
36
+ end
37
+ end
38
+ end
39
+ end
@@ -0,0 +1,27 @@
1
+ module ActiveKit
2
+ module Base
3
+ module Baser
4
+ attr_reader :describers
5
+
6
+ def new_describer(name:, options:)
7
+ options.store(:attributes, {})
8
+ @describers.store(name, options)
9
+ end
10
+
11
+ def describers?
12
+ @describers.present?
13
+ end
14
+
15
+ def new_attribute(name:, options:)
16
+ describer_names = Array(options.delete(:describers))
17
+ describer_names = @describers.keys if describer_names.blank?
18
+
19
+ describer_names.each do |describer_name|
20
+ if describer_options = @describers.dig(describer_name)
21
+ describer_options[:attributes].store(name, options)
22
+ end
23
+ end
24
+ end
25
+ end
26
+ end
27
+ end
@@ -12,12 +12,10 @@ module ActiveKit
12
12
  initializer "active_kit.activekitable" do
13
13
  require "active_kit/export/exportable"
14
14
  require "active_kit/position/positionable"
15
- require "active_kit/search/searchable"
16
15
 
17
16
  ActiveSupport.on_load(:active_record) do
18
17
  include ActiveKit::Export::Exportable
19
18
  include ActiveKit::Position::Positionable
20
- include ActiveKit::Search::Searchable
21
19
  end
22
20
  end
23
21
  end
@@ -14,50 +14,11 @@ module ActiveKit
14
14
  end
15
15
 
16
16
  def export_describer(name, **options)
17
- name = name.to_sym
18
- options.deep_symbolize_keys!
19
-
20
- unless exporter.find_describer_by(describer_name: name)
21
- exporter.new_describer(name: name, options: options)
22
- define_export_describer_method(kind: options[:kind], name: name)
23
- end
17
+ exporter.create_export_describer(name, options)
24
18
  end
25
19
 
26
20
  def export_attribute(name, **options)
27
- export_describer(:to_csv, kind: :csv, database: -> { ActiveRecord::Base.connection_db_config.database.to_sym }) unless exporter.describers?
28
-
29
- options.deep_symbolize_keys!
30
- exporter.new_attribute(name: name.to_sym, options: options)
31
- end
32
-
33
- def define_export_describer_method(kind:, name:)
34
- case kind
35
- when :csv
36
- define_singleton_method name do
37
- describer = exporter.find_describer_by(describer_name: name)
38
- raise "could not find describer for the describer name '#{name}'" unless describer.present?
39
-
40
- # The 'all' relation must be captured outside the Enumerator,
41
- # else it will get reset to all the records of the class.
42
- all_activerecord_relation = all.includes(describer.includes)
43
-
44
- Enumerator.new do |yielder|
45
- ActiveRecord::Base.connected_to(role: :writing, shard: describer.database.call) do
46
- exporting = exporter.new_exporting(describer: describer)
47
-
48
- # Add the headings.
49
- yielder << CSV.generate_line(exporting.headings) if exporting.headings?
50
-
51
- # Add the values.
52
- # find_each will ignore any order if set earlier.
53
- all_activerecord_relation.find_each do |record|
54
- lines = exporting.lines_for(record: record)
55
- lines.each { |line| yielder << CSV.generate_line(line) }
56
- end
57
- end
58
- end
59
- end
60
- end
21
+ exporter.create_export_attribute(name, options)
61
22
  end
62
23
  end
63
24
  end
@@ -1,41 +1,27 @@
1
1
  module ActiveKit
2
2
  module Export
3
3
  class Exporter
4
- attr_reader :describers
5
-
6
4
  def initialize(current_class:)
7
5
  @current_class = current_class
8
6
  @describers = {}
9
7
  end
10
8
 
11
- def find_describer_by(describer_name:)
12
- describer_options = @describers.dig(describer_name)
13
- return nil unless describer_options.present?
9
+ def create_export_describer(name, options)
10
+ name = name.to_sym
11
+ options.deep_symbolize_keys!
14
12
 
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)
13
+ unless find_describer_by(describer_name: name)
14
+ options.store(:attributes, {})
15
+ @describers.store(name, options)
16
+ define_describer_method(kind: options[:kind], name: name)
17
+ end
27
18
  end
28
19
 
29
- def new_describer(name:, options:)
30
- options.store(:attributes, {})
31
- @describers.store(name, options)
32
- end
20
+ def create_export_attribute(name, options)
21
+ create_export_describer(:to_csv, kind: :csv, database: -> { ActiveRecord::Base.connection_db_config.database.to_sym }) unless @describers.present?
33
22
 
34
- def describers?
35
- @describers.present?
36
- end
23
+ options.deep_symbolize_keys!
37
24
 
38
- def new_attribute(name:, options:)
39
25
  describer_names = Array(options.delete(:describers))
40
26
  describer_names = @describers.keys if describer_names.blank?
41
27
 
@@ -46,11 +32,61 @@ module ActiveKit
46
32
  end
47
33
  end
48
34
 
35
+ private
36
+
37
+ def define_describer_method(kind:, name:)
38
+ case kind
39
+ when :csv
40
+ @current_class.class_eval do
41
+ define_singleton_method name do
42
+ describer = exporter.find_describer_by(describer_name: name)
43
+ raise "could not find describer for the describer name '#{name}'" unless describer.present?
44
+
45
+ # The 'all' relation must be captured outside the Enumerator,
46
+ # else it will get reset to all the records of the class.
47
+ all_activerecord_relation = all.includes(describer.includes)
48
+
49
+ Enumerator.new do |yielder|
50
+ ActiveRecord::Base.connected_to(role: :writing, shard: describer.database.call) do
51
+ exporting = exporter.new_exporting(describer: describer)
52
+
53
+ # Add the headings.
54
+ yielder << CSV.generate_line(exporting.headings) if exporting.headings?
55
+
56
+ # Add the values.
57
+ # find_each will ignore any order if set earlier.
58
+ all_activerecord_relation.find_each do |record|
59
+ lines = exporting.lines_for(record: record)
60
+ lines.each { |line| yielder << CSV.generate_line(line) }
61
+ end
62
+ end
63
+ end
64
+ end
65
+ end
66
+ end
67
+ end
68
+
49
69
  def new_exporting(describer:)
50
70
  Exporting.new(describer: describer)
51
71
  end
52
72
 
53
- private
73
+ def find_describer_by(describer_name:)
74
+ describer_options = @describers.dig(describer_name)
75
+ return nil unless describer_options.present?
76
+
77
+ describer_attributes = describer_options[:attributes]
78
+ includes = describer_attributes.values.map { |options| options.dig(:includes) }.compact.flatten(1).uniq
79
+ fields = build_describer_fields(describer_attributes)
80
+ hash = {
81
+ name: describer_name,
82
+ kind: describer_options[:kind],
83
+ database: describer_options[:database],
84
+ attributes: describer_attributes,
85
+ includes: includes,
86
+ fields: fields
87
+ }
88
+ OpenStruct.new(hash)
89
+ end
54
90
 
55
91
  def build_describer_fields(describer_attributes)
56
92
  describer_attributes.inject({}) do |fields_hash, (name, options)|
@@ -1,3 +1,3 @@
1
1
  module ActiveKit
2
- VERSION = '0.5.0.dev2'
2
+ VERSION = '0.5.0.dev4'
3
3
  end
data/lib/active_kit.rb CHANGED
@@ -6,5 +6,4 @@ module ActiveKit
6
6
 
7
7
  autoload :Export
8
8
  autoload :Position
9
- autoload :Search
10
9
  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.dev2
4
+ version: 0.5.0.dev4
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-19 00:00:00.000000000 Z
11
+ date: 2024-04-22 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: rails
@@ -50,6 +50,8 @@ files:
50
50
  - app/views/layouts/active_kit/application.html.erb
51
51
  - config/routes.rb
52
52
  - lib/active_kit.rb
53
+ - lib/active_kit/base/baseable.rb
54
+ - lib/active_kit/base/baser.rb
53
55
  - lib/active_kit/engine.rb
54
56
  - lib/active_kit/export.rb
55
57
  - lib/active_kit/export/exportable.rb
@@ -61,16 +63,6 @@ files:
61
63
  - lib/active_kit/position/positionable.rb
62
64
  - lib/active_kit/position/positioner.rb
63
65
  - lib/active_kit/position/positioning.rb
64
- - lib/active_kit/search.rb
65
- - lib/active_kit/search/index.rb
66
- - lib/active_kit/search/key.rb
67
- - lib/active_kit/search/search.rb
68
- - lib/active_kit/search/search_result.rb
69
- - lib/active_kit/search/searchable.rb
70
- - lib/active_kit/search/searcher.rb
71
- - lib/active_kit/search/searching.rb
72
- - lib/active_kit/search/suggestion.rb
73
- - lib/active_kit/search/suggestion_result.rb
74
66
  - lib/active_kit/version.rb
75
67
  - lib/activekit.rb
76
68
  - lib/tasks/active_kit_tasks.rake
@@ -1,181 +0,0 @@
1
- module ActiveKit
2
- module Search
3
- class Index
4
- attr_reader :prefix, :schema, :attribute_value_parser
5
-
6
- def initialize(current_class:)
7
- @redis = ActiveKit::Search.redis
8
- @current_class = current_class
9
-
10
- current_class_name = current_class.to_s.parameterize.pluralize
11
- @name = "activekit:search:index:#{current_class_name}"
12
- @prefix = "activekit:search:#{current_class_name}"
13
- @schema = {}
14
- @attribute_value_parser = {}
15
- end
16
-
17
- def add_attribute_to_schema(name:, options:)
18
- raise "Error: No type specified for the search attribute #{name}." unless options[:type].present?
19
-
20
- attribute_schema = []
21
-
22
- as = options.delete(:as)
23
- attribute_schema.push("AS #{as}") unless as.nil?
24
-
25
- type = options.delete(:type)
26
- attribute_schema.push(type.to_s.upcase) unless type.nil?
27
-
28
- options.each do |key, value|
29
- if key == :value
30
- @attribute_value_parser.store(name.to_s, value)
31
- elsif key.is_a?(Symbol)
32
- if value == true
33
- attribute_schema.push(key.to_s.upcase)
34
- elsif value != false
35
- attribute_schema.push("#{key.to_s.upcase} #{value.to_s}")
36
- end
37
- else
38
- raise "Invalid option provided to search attribute."
39
- end
40
- end
41
-
42
- @schema.store(name.to_s, attribute_schema.join(" "))
43
- end
44
-
45
- def reload
46
- current_command = @redis.get("#{@name}:command")
47
- schema = { "database" => "TAG SORTABLE", "id" => "NUMERIC SORTABLE" }.merge(@schema)
48
- command = "FT.CREATE #{@name} ON HASH PREFIX 1 #{@prefix}: SCHEMA #{schema.to_a.flatten.join(' ')}"
49
- unless current_command == command
50
- drop
51
- @redis.call(command.split(' '))
52
- @redis.set("#{@name}:command", command)
53
- Rails.logger.info "ActiveKit::Search | Index Reloaded: " + "#{@name}:command"
54
- Rails.logger.debug "=> " + @redis.get("#{@name}:command").to_s
55
- end
56
- end
57
-
58
- def drop
59
- if exists?
60
- command = "FT.DROPINDEX #{@name}"
61
- @redis.call(command.split(' '))
62
- @redis.del("#{@name}:command")
63
- Rails.logger.info "ActiveKit::Search | Index Dropped: " + @name
64
- end
65
- end
66
-
67
- # Redis returns the results in the following form. Where first value is count of results, then every 2 elements are document_id, attributes respectively.
68
- # [2, "doc:3", ["name", "Grape Juice", "stock_quantity", "4", "minimum_stock", "2"], "doc:4", ["name", "Apple Juice", "stock_quantity", "4", "minimum_stock", "2"]]
69
- def fetch(term: nil, matching: "*", tags: {}, modifiers: {}, offset: nil, limit: nil, order: nil, page: nil, **options)
70
- original_term = term
71
-
72
- if term == ""
73
- results = nil
74
- elsif self.exists?
75
- if term.present?
76
- term.strip!
77
- term = escape_separators(term)
78
-
79
- case matching
80
- when "*"
81
- term = "#{term}*"
82
- when "%"
83
- term = "%#{term}%"
84
- when "%%"
85
- term = "%%#{term}%%"
86
- when "%%%"
87
- term = "%%%#{term}%%%"
88
- end
89
-
90
- term = " #{term}"
91
- else
92
- term = ""
93
- end
94
-
95
- if tags.present?
96
- tags = tags.map do |key, value|
97
- value = value.join("|") if value.is_a?(Array)
98
- "@#{escape_separators(key)}:{#{escape_separators(value, include_space: true).presence || 'nil'}}"
99
- end
100
- tags = tags.join(" ")
101
- tags = " #{tags}"
102
- else
103
- tags = ""
104
- end
105
-
106
- if modifiers.present?
107
- modifiers = modifiers.map { |key, value| "@#{escape_separators(key)}:#{escape_separators(value)}" }.join(" ")
108
- modifiers = " #{modifiers}"
109
- else
110
- modifiers = ""
111
- end
112
-
113
- if (offset.present? || limit.present?) && page.present?
114
- raise "Error: Cannot specify page and offset/limit at the same time. Please specify one of either page or offset/limit."
115
- end
116
-
117
- if page.present?
118
- page = page.to_i.abs
119
-
120
- case page
121
- when 0
122
- page = 1
123
- offset = 0
124
- limit = 15
125
- when 1
126
- offset = 0
127
- limit = 15
128
- when 2
129
- offset = 15
130
- limit = 30
131
- when 3
132
- offset = 45
133
- limit = 50
134
- else
135
- limit = 100
136
- offset = 15 + 30 + 50 + (page - 4) * limit
137
- end
138
- end
139
-
140
- query = "@database:{#{escape_separators(System::Current.tenant.database, include_space: true)}}#{term}#{tags}#{modifiers}"
141
- command = [
142
- "FT.SEARCH",
143
- @name,
144
- query,
145
- "LIMIT",
146
- offset ? offset.to_i : 0, # 0 is the default offset of redisearch in LIMIT 0 10. https://redis.io/commands/ft.search
147
- limit ? limit.to_i : 10 # 10 is the default limit of redisearch in LIMIT 0 10. https://redis.io/commands/ft.search
148
- ]
149
- command.push("SORTBY", *order.split(' ')) if order.present?
150
- results = @redis.call(command)
151
- Rails.logger.info "ActiveKit::Search | Index Searched: " + command.to_s
152
- Rails.logger.debug "=> " + results.to_s
153
- else
154
- results = nil
155
- end
156
-
157
- SearchResult.new(term: original_term, results: results, offset: offset, limit: limit, page: page, current_class: @current_class)
158
- end
159
-
160
- # List of characters from https://oss.redislabs.com/redisearch/Escaping/
161
- # ,.<>{}[]"':;!@#$%^&*()-+=~
162
- def escape_separators(value, include_space: false)
163
- value = value.to_s
164
-
165
- unless include_space
166
- pattern = %r{(\'|\"|\.|\,|\;|\<|\>|\{|\}|\[|\]|\"|\'|\=|\~|\*|\:|\#|\+|\^|\$|\@|\%|\!|\&|\)|\(|/|\-|\\)}
167
- else
168
- pattern = %r{(\'|\"|\.|\,|\;|\<|\>|\{|\}|\[|\]|\"|\'|\=|\~|\*|\:|\#|\+|\^|\$|\@|\%|\!|\&|\)|\(|/|\-|\\|\s)}
169
- end
170
-
171
- value.gsub(pattern) { |match| '\\' + match }
172
- end
173
-
174
- private
175
-
176
- def exists?
177
- @redis.call("FT._LIST").include?(@name)
178
- end
179
- end
180
- end
181
- end
@@ -1,44 +0,0 @@
1
- module ActiveKit
2
- module Search
3
- class Key
4
- def initialize(index:)
5
- @redis = ActiveKit::Search.redis
6
- @index = index
7
- end
8
-
9
- def reload(record:)
10
- clear(record: record)
11
-
12
- hash_key = key(record: record)
13
- hash_value = { "database" => System::Current.tenant.database, "id" => record.id }
14
- @index.schema.each do |field_name, field_value|
15
- attribute_name = field_name
16
- attribute_value = @index.attribute_value_parser[field_name]&.call(record) || record.public_send(field_name)
17
- attribute_value = field_value.downcase.include?("tag") ? attribute_value : @index.escape_separators(attribute_value)
18
- hash_value.store(attribute_name, attribute_value)
19
- end
20
- @redis.hset(hash_key, hash_value)
21
- Rails.logger.info "ActiveKit::Search | Key Reloaded: " + hash_key
22
- Rails.logger.debug "=> " + @redis.hgetall("#{hash_key}").to_s
23
- end
24
-
25
- def clear(record:)
26
- hash_key = key(record: record)
27
- drop(keys: hash_key)
28
- end
29
-
30
- def drop(keys:)
31
- return unless keys.present?
32
- if @redis.del(keys) > 0
33
- Rails.logger.info "ActiveKit::Search | Keys Removed: " + keys.to_s
34
- end
35
- end
36
-
37
- private
38
-
39
- def key(record:)
40
- "#{@index.prefix}:#{System::Current.tenant.database}:#{record.id}"
41
- end
42
- end
43
- end
44
- end
@@ -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
@@ -1,33 +0,0 @@
1
- module ActiveKit
2
- module Search
3
- class SearchResult
4
- attr_reader :term, :count, :documents, :keys, :ids, :records, :current_page, :previous_page, :next_page
5
-
6
- def initialize(term:, results:, offset:, limit:, page:, current_class:)
7
- @term = term
8
-
9
- if results
10
- @count = results.shift
11
- @documents = results.each_slice(2).map { |key, attributes| [key, attributes.each_slice(2).to_h] }.to_h
12
-
13
- if page.present?
14
- @current_page = page
15
- @previous_page = @current_page > 1 ? (@current_page - 1) : nil
16
- @next_page = (offset + limit) < count ? (@current_page + 1) : nil
17
- end
18
- else
19
- @count = 0
20
- @documents = {}
21
- end
22
-
23
- @keys = @documents.keys
24
- @ids = @documents.map { |key, value| key.split(":").last }
25
-
26
- # Return records from database.
27
- # This also ensures that any left over document_ids in redis that have been deleted in the database are left out of the results.
28
- # This orders the records in the order of executed search.
29
- @records = current_class.where(id: ids).reorder(Arel.sql("FIELD(#{current_class.table_name}.id, #{ids.join(', ')})"))
30
- end
31
- end
32
- end
33
- end
@@ -1,126 +0,0 @@
1
- require 'active_support/concern'
2
-
3
- module ActiveKit
4
- module Search
5
- module Searchable
6
- extend ActiveSupport::Concern
7
-
8
- included do
9
- end
10
-
11
- 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
- end
55
- end
56
- end
57
- 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 +0,0 @@
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,104 +0,0 @@
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,39 +0,0 @@
1
- module ActiveKit
2
- module Search
3
- class Suggestion
4
- def initialize(current_class:)
5
- @redis = ActiveKit::Search.redis
6
- @current_class = current_class
7
- @current_class_name = current_class.to_s.parameterize.pluralize
8
- end
9
-
10
- def add(term:, score: 1, increment: true)
11
- command = ["FT.SUGADD", key, term, score, (increment ? 'INCR' : '')]
12
- @redis.call(command)
13
- end
14
-
15
- def fetch(prefix:)
16
- command = ["FT.SUGGET", key, prefix, "FUZZY", "MAX", "10", "WITHSCORES"]
17
- results = @redis.call(command)
18
-
19
- SuggestionResult.new(prefix: prefix, results: results)
20
- end
21
-
22
- def del(term:)
23
- command = ["FT.SUGDEL", key, term]
24
- @redis.call(command)
25
- end
26
-
27
- def len
28
- command = ["FT.SUGLEN", key]
29
- @redis.call(command)
30
- end
31
-
32
- private
33
-
34
- def key
35
- "activekit:search:suggestions:#{@current_class_name}:#{System::Current.tenant.database}"
36
- end
37
- end
38
- end
39
- end
@@ -1,21 +0,0 @@
1
- module ActiveKit
2
- module Search
3
- class SuggestionResult
4
- attr_reader :prefix, :documents, :keys, :scores
5
-
6
- def initialize(prefix:, results:)
7
- @prefix = prefix
8
-
9
- if results
10
- @documents = results.each_slice(2).map { |key, value| [key, value] }.to_h
11
- @keys = @documents.keys
12
- @scores = @documents.values
13
- else
14
- @documents = {}
15
- @keys = []
16
- @scores = []
17
- end
18
- end
19
- end
20
- end
21
- end
@@ -1,16 +0,0 @@
1
- module ActiveKit
2
- module Search
3
- extend ActiveSupport::Autoload
4
-
5
- autoload :Index
6
- autoload :Key
7
- autoload :Search
8
- autoload :SearchResult
9
- autoload :Searcher
10
- autoload :Searching
11
- autoload :Suggestion
12
- autoload :SuggestionResult
13
-
14
- mattr_accessor :redis, instance_accessor: false
15
- end
16
- end