activekit 0.5.0.dev2 → 0.5.0.dev3
Sign up to get free protection for your applications and to get access to all the features.
- 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 +61 -25
- 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: b32a04bec4de6085fcce1d004a96792b2fdfd9166d55cb9a2584a719be226a45
|
4
|
+
data.tar.gz: 4dc7aa2af40b73b744225a2509a8fb8447eaa94a3b02fafd893889fd02673780
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 971bc6562a3d04df66068fb6d498ccc0b67de160a2d0d02c7e6d62d4c0d6e60a0196f4c888f0edeb189e5c310ea4d485c74d095182d37d33c59bb080bd226746
|
7
|
+
data.tar.gz: 75394d757e7997b919c73e9ecfbde9531754fee398e985c6f5919f12dee63e795b658271e64db3301f1d415e391a2b3ddfd26edd7bd38eab400b83951ba39466
|
@@ -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
|
@@ -8,34 +8,22 @@ module ActiveKit
|
|
8
8
|
@describers = {}
|
9
9
|
end
|
10
10
|
|
11
|
-
def
|
12
|
-
|
13
|
-
|
14
|
-
|
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)
|
11
|
+
def create_export_describer(name, options)
|
12
|
+
name = name.to_sym
|
13
|
+
options.deep_symbolize_keys!
|
14
|
+
|
15
|
+
unless find_describer_by(describer_name: name)
|
16
|
+
options.store(:attributes, {})
|
17
|
+
@describers.store(name, options)
|
18
|
+
define_describer_method(kind: options[:kind], name: name)
|
19
|
+
end
|
27
20
|
end
|
28
21
|
|
29
|
-
def
|
30
|
-
|
31
|
-
@describers.store(name, options)
|
32
|
-
end
|
22
|
+
def create_export_attribute(name, options)
|
23
|
+
create_export_describer(:to_csv, kind: :csv, database: -> { ActiveRecord::Base.connection_db_config.database.to_sym }) unless @describers.present?
|
33
24
|
|
34
|
-
|
35
|
-
@describers.present?
|
36
|
-
end
|
25
|
+
options.deep_symbolize_keys!
|
37
26
|
|
38
|
-
def new_attribute(name:, options:)
|
39
27
|
describer_names = Array(options.delete(:describers))
|
40
28
|
describer_names = @describers.keys if describer_names.blank?
|
41
29
|
|
@@ -46,11 +34,59 @@ module ActiveKit
|
|
46
34
|
end
|
47
35
|
end
|
48
36
|
|
37
|
+
private
|
38
|
+
|
39
|
+
def define_describer_method(kind:, name:)
|
40
|
+
case kind
|
41
|
+
when :csv
|
42
|
+
define_singleton_method name do
|
43
|
+
describer = exporter.find_describer_by(describer_name: name)
|
44
|
+
raise "could not find describer for the describer name '#{name}'" unless describer.present?
|
45
|
+
|
46
|
+
# The 'all' relation must be captured outside the Enumerator,
|
47
|
+
# else it will get reset to all the records of the class.
|
48
|
+
all_activerecord_relation = all.includes(describer.includes)
|
49
|
+
|
50
|
+
Enumerator.new do |yielder|
|
51
|
+
ActiveRecord::Base.connected_to(role: :writing, shard: describer.database.call) do
|
52
|
+
exporting = exporter.new_exporting(describer: describer)
|
53
|
+
|
54
|
+
# Add the headings.
|
55
|
+
yielder << CSV.generate_line(exporting.headings) if exporting.headings?
|
56
|
+
|
57
|
+
# Add the values.
|
58
|
+
# find_each will ignore any order if set earlier.
|
59
|
+
all_activerecord_relation.find_each do |record|
|
60
|
+
lines = exporting.lines_for(record: record)
|
61
|
+
lines.each { |line| yielder << CSV.generate_line(line) }
|
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.dev3
|
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
|