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 +4 -4
- data/lib/active_kit/base/baseable.rb +39 -0
- data/lib/active_kit/base/baser.rb +27 -0
- data/lib/active_kit/engine.rb +0 -2
- data/lib/active_kit/export/exportable.rb +2 -41
- data/lib/active_kit/export/exporter.rb +62 -26
- data/lib/active_kit/version.rb +1 -1
- data/lib/active_kit.rb +0 -1
- metadata +4 -12
- data/lib/active_kit/search/index.rb +0 -181
- data/lib/active_kit/search/key.rb +0 -44
- data/lib/active_kit/search/search.rb +0 -68
- data/lib/active_kit/search/search_result.rb +0 -33
- data/lib/active_kit/search/searchable.rb +0 -126
- data/lib/active_kit/search/searcher.rb +0 -106
- data/lib/active_kit/search/searching.rb +0 -104
- data/lib/active_kit/search/suggestion.rb +0 -39
- data/lib/active_kit/search/suggestion_result.rb +0 -21
- data/lib/active_kit/search.rb +0 -16
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: eff7a049cd21979e76253d095e0d0ba722b289370a15426cf4b87c922d2bab66
|
4
|
+
data.tar.gz: 27b310f65547d08961b9044fe43bc606cfb78f0766487052c9488a2fa3e78ed8
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
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
|
data/lib/active_kit/engine.rb
CHANGED
@@ -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
|
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
|
-
|
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
|
12
|
-
|
13
|
-
|
9
|
+
def create_export_describer(name, options)
|
10
|
+
name = name.to_sym
|
11
|
+
options.deep_symbolize_keys!
|
14
12
|
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
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
|
30
|
-
|
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
|
-
|
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
|
-
|
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)|
|
data/lib/active_kit/version.rb
CHANGED
data/lib/active_kit.rb
CHANGED
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: activekit
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.5.0.
|
4
|
+
version: 0.5.0.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-
|
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
|
data/lib/active_kit/search.rb
DELETED
@@ -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
|