activekit 0.5.0.dev2 → 0.5.0.dev4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml 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