activekit 0.5.0.dev7 → 0.5.0.dev8
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/bedrock/bedrockable.rb +33 -0
- data/lib/active_kit/bedrock/bedrocker.rb +120 -0
- data/lib/active_kit/bedrock.rb +7 -0
- data/lib/active_kit/engine.rb +3 -0
- data/lib/active_kit/export/exportable.rb +1 -12
- data/lib/active_kit/export/exporter.rb +1 -105
- data/lib/active_kit/search/index.rb +181 -0
- data/lib/active_kit/search/key.rb +44 -0
- data/lib/active_kit/search/search.rb +68 -0
- data/lib/active_kit/search/search_result.rb +33 -0
- data/lib/active_kit/search/searchable.rb +126 -0
- data/lib/active_kit/search/searcher.rb +106 -0
- data/lib/active_kit/search/searching.rb +104 -0
- data/lib/active_kit/search/suggestion.rb +39 -0
- data/lib/active_kit/search/suggestion_result.rb +21 -0
- data/lib/active_kit/search.rb +16 -0
- data/lib/active_kit/version.rb +1 -1
- data/lib/active_kit.rb +2 -0
- data/lib/tasks/search_tasks.rake +84 -0
- metadata +16 -4
- data/lib/active_kit/base/baseable.rb +0 -39
- data/lib/active_kit/base/baser.rb +0 -27
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 7fe66b1b57a945fa39f8d3daad8daa1dd3d564d190fb302453e1e1d9b4bc64e8
|
4
|
+
data.tar.gz: '0026541966d64d4fb509ae32dbd69715cf86d8dd79abd51537aee335bb0da393'
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: ebc4d83438c610266f4de8c72395199f71282f5e8b00cfdebcf9e532950094997f520b0dd3e65a85e05f5e62fae365e5eaebc770691c4fcb2131a0ce3225bbc1
|
7
|
+
data.tar.gz: bbb51f533e437c2de942dcc5a8d9120b3f1cf0317b138c0b924cd433a48bbe6c145b4c77e3eaf0160c2a37d47d0d35df07916a090fbd237bc4929c7de8b0d790
|
@@ -0,0 +1,33 @@
|
|
1
|
+
require 'active_support/concern'
|
2
|
+
|
3
|
+
module ActiveKit
|
4
|
+
module Bedrock
|
5
|
+
module Bedrockable
|
6
|
+
def self.extended(base)
|
7
|
+
current_component = base.name.deconstantize.demodulize.downcase
|
8
|
+
|
9
|
+
base.module_eval <<-CODE, __FILE__, __LINE__ + 1
|
10
|
+
module ClassMethods
|
11
|
+
private
|
12
|
+
|
13
|
+
def #{current_component}er
|
14
|
+
@#{current_component}er ||= ActiveKit::#{current_component.to_s.titleize}::#{current_component.to_s.titleize}er.new(current_component: :#{current_component}, current_class: self)
|
15
|
+
end
|
16
|
+
|
17
|
+
def #{current_component}_describer(name, **options)
|
18
|
+
#{current_component}er.create_describer(name, options)
|
19
|
+
end
|
20
|
+
|
21
|
+
def #{current_component}_attribute(name, **options)
|
22
|
+
#{current_component}er.create_attribute(name, options)
|
23
|
+
end
|
24
|
+
|
25
|
+
def #{current_component}_describer_method(describer)
|
26
|
+
raise NotImplementedError
|
27
|
+
end
|
28
|
+
end
|
29
|
+
CODE
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
@@ -0,0 +1,120 @@
|
|
1
|
+
module ActiveKit
|
2
|
+
module Bedrock
|
3
|
+
class Bedrocker
|
4
|
+
def initialize(current_component:, current_class:)
|
5
|
+
@current_component = current_component
|
6
|
+
@current_class = current_class
|
7
|
+
@describers = {}
|
8
|
+
end
|
9
|
+
|
10
|
+
def create_describer(name, options)
|
11
|
+
name = name.to_sym
|
12
|
+
options.deep_symbolize_keys!
|
13
|
+
|
14
|
+
unless find_describer_by(name: name)
|
15
|
+
options.store(:attributes, {})
|
16
|
+
@describers.store(name, options)
|
17
|
+
@current_class.class_eval <<-CODE, __FILE__, __LINE__ + 1
|
18
|
+
def self.#{name}
|
19
|
+
if describer = #{@current_component}er.find_describer_by(name: "#{name}".to_sym)
|
20
|
+
#{@current_component}_describer_method(describer)
|
21
|
+
end
|
22
|
+
end
|
23
|
+
CODE
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
def create_attribute(name, options)
|
28
|
+
options.deep_symbolize_keys!
|
29
|
+
|
30
|
+
create_default_describer unless @describers.present?
|
31
|
+
|
32
|
+
describer_names = Array(options.delete(:describers))
|
33
|
+
describer_names = @describers.keys if describer_names.blank?
|
34
|
+
|
35
|
+
describer_names.each do |describer_name|
|
36
|
+
if describer_options = @describers.dig(describer_name)
|
37
|
+
describer_options[:attributes].store(name, options)
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
def find_describer_by(name:)
|
43
|
+
options = @describers.dig(name)
|
44
|
+
return nil unless options.present?
|
45
|
+
|
46
|
+
hash = {
|
47
|
+
name: name,
|
48
|
+
database: options[:database],
|
49
|
+
attributes: options[:attributes]
|
50
|
+
}
|
51
|
+
|
52
|
+
if @current_component == :export
|
53
|
+
hash.merge!(kind: options[:kind])
|
54
|
+
hash.merge!(includes: options[:attributes].values.map { |options| options.dig(:includes) }.compact.flatten(1).uniq)
|
55
|
+
hash.merge!(fields: build_describer_fields(options[:attributes]))
|
56
|
+
end
|
57
|
+
OpenStruct.new(hash)
|
58
|
+
end
|
59
|
+
|
60
|
+
private
|
61
|
+
|
62
|
+
def build_describer_fields(describer_attributes)
|
63
|
+
describer_attributes.inject({}) do |fields_hash, (name, options)|
|
64
|
+
enclosed_attributes = Array(options.dig(:attributes))
|
65
|
+
|
66
|
+
if enclosed_attributes.blank?
|
67
|
+
field_key, field_value = (get_heading(options.dig(:heading))&.to_s || name.to_s.titleize), (options.dig(:value) || name)
|
68
|
+
else
|
69
|
+
field_key, field_value = get_nested_field(name, options, enclosed_attributes)
|
70
|
+
end
|
71
|
+
fields_hash.store(field_key, field_value)
|
72
|
+
|
73
|
+
fields_hash
|
74
|
+
end
|
75
|
+
end
|
76
|
+
|
77
|
+
def get_nested_field(name, options, enclosed_attributes, ancestor_heading = nil)
|
78
|
+
parent_heading = ancestor_heading.present? ? ancestor_heading : ""
|
79
|
+
parent_heading += (get_heading(options.dig(:heading))&.to_s || name.to_s.singularize.titleize) + " "
|
80
|
+
parent_value = options.dig(:value) || name
|
81
|
+
|
82
|
+
enclosed_attributes.inject([[], [parent_value]]) do |nested_field, enclosed_attribute|
|
83
|
+
unless enclosed_attribute.is_a? Hash
|
84
|
+
nested_field_key = parent_heading + enclosed_attribute.to_s.titleize
|
85
|
+
nested_field_val = enclosed_attribute
|
86
|
+
|
87
|
+
nested_field[0].push(nested_field_key)
|
88
|
+
nested_field[1].push(nested_field_val)
|
89
|
+
else
|
90
|
+
enclosed_attribute.each do |enclosed_attribute_key, enclosed_attribute_value|
|
91
|
+
wrapped_attributes = Array(enclosed_attribute_value.dig(:attributes))
|
92
|
+
if wrapped_attributes.blank?
|
93
|
+
nested_field_key = parent_heading + (get_heading(enclosed_attribute_value.dig(:heading))&.to_s || enclosed_attribute_key.to_s.titleize)
|
94
|
+
nested_field_val = enclosed_attribute_value.dig(:value) || enclosed_attribute_key
|
95
|
+
else
|
96
|
+
nested_field_key, nested_field_val = get_nested_field(enclosed_attribute_key, enclosed_attribute_value, wrapped_attributes, parent_heading)
|
97
|
+
end
|
98
|
+
|
99
|
+
nested_field[0].push(nested_field_key)
|
100
|
+
nested_field[1].push(nested_field_val)
|
101
|
+
end
|
102
|
+
end
|
103
|
+
|
104
|
+
nested_field
|
105
|
+
end
|
106
|
+
end
|
107
|
+
|
108
|
+
def get_heading(options_heading)
|
109
|
+
options_heading.is_a?(Proc) ? options_heading.call(@current_class) : options_heading
|
110
|
+
end
|
111
|
+
|
112
|
+
def create_default_describer
|
113
|
+
case @current_component
|
114
|
+
when :export
|
115
|
+
create_describer(:to_csv, kind: :csv, database: -> { ActiveRecord::Base.connection_db_config.database.to_sym })
|
116
|
+
end
|
117
|
+
end
|
118
|
+
end
|
119
|
+
end
|
120
|
+
end
|
data/lib/active_kit/engine.rb
CHANGED
@@ -10,12 +10,15 @@ module ActiveKit
|
|
10
10
|
end
|
11
11
|
|
12
12
|
initializer "active_kit.activekitable" do
|
13
|
+
require "active_kit/bedrock/bedrockable"
|
13
14
|
require "active_kit/export/exportable"
|
14
15
|
require "active_kit/position/positionable"
|
16
|
+
require "active_kit/search/searchable"
|
15
17
|
|
16
18
|
ActiveSupport.on_load(:active_record) do
|
17
19
|
include ActiveKit::Export::Exportable
|
18
20
|
include ActiveKit::Position::Positionable
|
21
|
+
include ActiveKit::Search::Searchable
|
19
22
|
end
|
20
23
|
end
|
21
24
|
end
|
@@ -3,24 +3,13 @@ require 'active_support/concern'
|
|
3
3
|
module ActiveKit
|
4
4
|
module Export
|
5
5
|
module Exportable
|
6
|
+
extend Bedrock::Bedrockable
|
6
7
|
extend ActiveSupport::Concern
|
7
8
|
|
8
9
|
included do
|
9
10
|
end
|
10
11
|
|
11
12
|
class_methods do
|
12
|
-
def exporter
|
13
|
-
@exporter ||= ActiveKit::Export::Exporter.new(current_class: self)
|
14
|
-
end
|
15
|
-
|
16
|
-
def export_describer(name, **options)
|
17
|
-
exporter.create_describer(name, options)
|
18
|
-
end
|
19
|
-
|
20
|
-
def export_attribute(name, **options)
|
21
|
-
exporter.create_attribute(name, options)
|
22
|
-
end
|
23
|
-
|
24
13
|
def export_describer_method(describer)
|
25
14
|
case describer.kind
|
26
15
|
when :csv
|
@@ -1,113 +1,9 @@
|
|
1
1
|
module ActiveKit
|
2
2
|
module Export
|
3
|
-
class Exporter
|
4
|
-
def initialize(current_class:)
|
5
|
-
@current_class = current_class
|
6
|
-
@describers = {}
|
7
|
-
end
|
8
|
-
|
9
|
-
def create_describer(name, options)
|
10
|
-
name = name.to_sym
|
11
|
-
options.deep_symbolize_keys!
|
12
|
-
|
13
|
-
unless find_describer_by(describer_name: name)
|
14
|
-
options.store(:attributes, {})
|
15
|
-
@describers.store(name, options)
|
16
|
-
@current_class.class_eval do
|
17
|
-
define_singleton_method name do
|
18
|
-
if describer = exporter.find_describer_by(describer_name: name)
|
19
|
-
export_describer_method(describer)
|
20
|
-
end
|
21
|
-
end
|
22
|
-
end
|
23
|
-
end
|
24
|
-
end
|
25
|
-
|
26
|
-
def create_attribute(name, options)
|
27
|
-
options.deep_symbolize_keys!
|
28
|
-
|
29
|
-
create_describer(:to_csv, kind: :csv, database: -> { ActiveRecord::Base.connection_db_config.database.to_sym }) unless @describers.present?
|
30
|
-
|
31
|
-
describer_names = Array(options.delete(:describers))
|
32
|
-
describer_names = @describers.keys if describer_names.blank?
|
33
|
-
|
34
|
-
describer_names.each do |describer_name|
|
35
|
-
if describer_options = @describers.dig(describer_name)
|
36
|
-
describer_options[:attributes].store(name, options)
|
37
|
-
end
|
38
|
-
end
|
39
|
-
end
|
40
|
-
|
41
|
-
def find_describer_by(name:)
|
42
|
-
options = @describers.dig(name)
|
43
|
-
return nil unless options.present?
|
44
|
-
|
45
|
-
hash = {
|
46
|
-
name: name,
|
47
|
-
kind: options[:kind],
|
48
|
-
database: options[:database],
|
49
|
-
attributes: options[:attributes],
|
50
|
-
includes: options[:attributes].values.map { |options| options.dig(:includes) }.compact.flatten(1).uniq,
|
51
|
-
fields: build_describer_fields(options[:attributes])
|
52
|
-
}
|
53
|
-
OpenStruct.new(hash)
|
54
|
-
end
|
55
|
-
|
3
|
+
class Exporter < Bedrock::Bedrocker
|
56
4
|
def new_exporting(describer:)
|
57
5
|
Exporting.new(describer: describer)
|
58
6
|
end
|
59
|
-
|
60
|
-
private
|
61
|
-
|
62
|
-
def build_describer_fields(describer_attributes)
|
63
|
-
describer_attributes.inject({}) do |fields_hash, (name, options)|
|
64
|
-
enclosed_attributes = Array(options.dig(:attributes))
|
65
|
-
|
66
|
-
if enclosed_attributes.blank?
|
67
|
-
field_key, field_value = (get_heading(options.dig(:heading))&.to_s || name.to_s.titleize), (options.dig(:value) || name)
|
68
|
-
else
|
69
|
-
field_key, field_value = get_nested_field(name, options, enclosed_attributes)
|
70
|
-
end
|
71
|
-
fields_hash.store(field_key, field_value)
|
72
|
-
|
73
|
-
fields_hash
|
74
|
-
end
|
75
|
-
end
|
76
|
-
|
77
|
-
def get_nested_field(name, options, enclosed_attributes, ancestor_heading = nil)
|
78
|
-
parent_heading = ancestor_heading.present? ? ancestor_heading : ""
|
79
|
-
parent_heading += (get_heading(options.dig(:heading))&.to_s || name.to_s.singularize.titleize) + " "
|
80
|
-
parent_value = options.dig(:value) || name
|
81
|
-
|
82
|
-
enclosed_attributes.inject([[], [parent_value]]) do |nested_field, enclosed_attribute|
|
83
|
-
unless enclosed_attribute.is_a? Hash
|
84
|
-
nested_field_key = parent_heading + enclosed_attribute.to_s.titleize
|
85
|
-
nested_field_val = enclosed_attribute
|
86
|
-
|
87
|
-
nested_field[0].push(nested_field_key)
|
88
|
-
nested_field[1].push(nested_field_val)
|
89
|
-
else
|
90
|
-
enclosed_attribute.each do |enclosed_attribute_key, enclosed_attribute_value|
|
91
|
-
wrapped_attributes = Array(enclosed_attribute_value.dig(:attributes))
|
92
|
-
if wrapped_attributes.blank?
|
93
|
-
nested_field_key = parent_heading + (get_heading(enclosed_attribute_value.dig(:heading))&.to_s || enclosed_attribute_key.to_s.titleize)
|
94
|
-
nested_field_val = enclosed_attribute_value.dig(:value) || enclosed_attribute_key
|
95
|
-
else
|
96
|
-
nested_field_key, nested_field_val = get_nested_field(enclosed_attribute_key, enclosed_attribute_value, wrapped_attributes, parent_heading)
|
97
|
-
end
|
98
|
-
|
99
|
-
nested_field[0].push(nested_field_key)
|
100
|
-
nested_field[1].push(nested_field_val)
|
101
|
-
end
|
102
|
-
end
|
103
|
-
|
104
|
-
nested_field
|
105
|
-
end
|
106
|
-
end
|
107
|
-
|
108
|
-
def get_heading(options_heading)
|
109
|
-
options_heading.is_a?(Proc) ? options_heading.call(@current_class) : options_heading
|
110
|
-
end
|
111
7
|
end
|
112
8
|
end
|
113
9
|
end
|
@@ -0,0 +1,181 @@
|
|
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
|
@@ -0,0 +1,44 @@
|
|
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
|
@@ -0,0 +1,68 @@
|
|
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
|
@@ -0,0 +1,33 @@
|
|
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
|
@@ -0,0 +1,126 @@
|
|
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
|
@@ -0,0 +1,106 @@
|
|
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
|
@@ -0,0 +1,104 @@
|
|
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
|
@@ -0,0 +1,39 @@
|
|
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
|
@@ -0,0 +1,21 @@
|
|
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
|
@@ -0,0 +1,16 @@
|
|
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
|
data/lib/active_kit/version.rb
CHANGED
data/lib/active_kit.rb
CHANGED
@@ -0,0 +1,84 @@
|
|
1
|
+
namespace :active_kit do
|
2
|
+
desc "bundle exec rails active_kit:boot DOMAIN='www.yourdomain.com'"
|
3
|
+
task boot: [:environment] do
|
4
|
+
# Database & Preferences
|
5
|
+
domain = ENV['DOMAIN'] || raise("DOMAIN not specified")
|
6
|
+
|
7
|
+
tenant = nil
|
8
|
+
shard_name = nil
|
9
|
+
|
10
|
+
# Returns the first db config for a specific environment where it is assumed that all tenants data is stored.
|
11
|
+
default_shard_name = ActiveRecord::Base.configurations.find_db_config(Rails.env).name
|
12
|
+
|
13
|
+
ActiveRecord::Base.connected_to(role: :writing, shard: default_shard_name.to_sym) do
|
14
|
+
tenant = System::Tenant.where(domain: domain).or(System::Tenant.where(custom_domain: domain)).select(:database, :storage, :domain, :custom_domain).first
|
15
|
+
shard_name = tenant.database
|
16
|
+
raise RuntimeError, 'Could not set shard name.' unless shard_name.present? # TODO: In future, redirect this to "Nothing Here Yet" page
|
17
|
+
end
|
18
|
+
|
19
|
+
ApplicationRecord.connects_to database: { writing: "#{shard_name}".to_sym }
|
20
|
+
|
21
|
+
System::Current.tenant = OpenStruct.new(tenant.serializable_hash)
|
22
|
+
System::Current.preferences = System::Preference.revealed
|
23
|
+
System::Current.integrations = System::Integration.revealed
|
24
|
+
end
|
25
|
+
|
26
|
+
namespace :search do
|
27
|
+
desc "bundle exec rails active_kit:search:reload CLASS='Article' DOMAIN='www.yourdomain.com'"
|
28
|
+
task reload: [:boot] do
|
29
|
+
if ENV['CLASS']
|
30
|
+
if ENV['CLASS'].constantize.searcher.attributes_present?
|
31
|
+
puts "ActiveKit::Search | Reloading: #{ENV['CLASS']}"
|
32
|
+
ENV['CLASS'].constantize.searcher.reload
|
33
|
+
end
|
34
|
+
else
|
35
|
+
Rails.application.eager_load!
|
36
|
+
models = ApplicationRecord.descendants.collect(&:name)
|
37
|
+
models.each do |model|
|
38
|
+
if model.constantize.searcher.attributes_present?
|
39
|
+
puts "ActiveKit::Search | Reloading: #{model}"
|
40
|
+
model.constantize.searcher.reload
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
desc "bundle exec rails active_kit:search:clear CLASS='Article' DOMAIN='www.yourdomain.com'"
|
47
|
+
task clear: [:boot] do
|
48
|
+
if ENV['CLASS']
|
49
|
+
if ENV['CLASS'].constantize.searcher.attributes_present?
|
50
|
+
puts "ActiveKit::Search | Clearing: #{ENV['CLASS']}"
|
51
|
+
ENV['CLASS'].constantize.searcher.clear
|
52
|
+
end
|
53
|
+
else
|
54
|
+
Rails.application.eager_load!
|
55
|
+
models = ApplicationRecord.descendants.collect(&:name)
|
56
|
+
models.each do |model|
|
57
|
+
if model.constantize.searcher.attributes_present?
|
58
|
+
puts "ActiveKit::Search | Clearing: #{model}"
|
59
|
+
model.constantize.searcher.clear
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|
63
|
+
end
|
64
|
+
|
65
|
+
desc "bundle exec rails active_kit:search:drop CLASS='Article' DOMAIN='www.yourdomain.com'"
|
66
|
+
task drop: [:boot] do
|
67
|
+
if ENV['CLASS']
|
68
|
+
if ENV['CLASS'].constantize.searcher.attributes_present?
|
69
|
+
puts "ActiveKit::Search | Dropping: #{ENV['CLASS']}"
|
70
|
+
ENV['CLASS'].constantize.searcher.drop
|
71
|
+
end
|
72
|
+
else
|
73
|
+
Rails.application.eager_load!
|
74
|
+
models = ApplicationRecord.descendants.collect(&:name)
|
75
|
+
models.each do |model|
|
76
|
+
if model.constantize.searcher.attributes_present?
|
77
|
+
puts "ActiveKit::Search | Dropping: #{model}"
|
78
|
+
model.constantize.searcher.drop
|
79
|
+
end
|
80
|
+
end
|
81
|
+
end
|
82
|
+
end
|
83
|
+
end
|
84
|
+
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.
|
4
|
+
version: 0.5.0.dev8
|
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-23 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: rails
|
@@ -50,8 +50,9 @@ 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/
|
54
|
-
- lib/active_kit/
|
53
|
+
- lib/active_kit/bedrock.rb
|
54
|
+
- lib/active_kit/bedrock/bedrockable.rb
|
55
|
+
- lib/active_kit/bedrock/bedrocker.rb
|
55
56
|
- lib/active_kit/engine.rb
|
56
57
|
- lib/active_kit/export.rb
|
57
58
|
- lib/active_kit/export/exportable.rb
|
@@ -63,9 +64,20 @@ files:
|
|
63
64
|
- lib/active_kit/position/positionable.rb
|
64
65
|
- lib/active_kit/position/positioner.rb
|
65
66
|
- lib/active_kit/position/positioning.rb
|
67
|
+
- lib/active_kit/search.rb
|
68
|
+
- lib/active_kit/search/index.rb
|
69
|
+
- lib/active_kit/search/key.rb
|
70
|
+
- lib/active_kit/search/search.rb
|
71
|
+
- lib/active_kit/search/search_result.rb
|
72
|
+
- lib/active_kit/search/searchable.rb
|
73
|
+
- lib/active_kit/search/searcher.rb
|
74
|
+
- lib/active_kit/search/searching.rb
|
75
|
+
- lib/active_kit/search/suggestion.rb
|
76
|
+
- lib/active_kit/search/suggestion_result.rb
|
66
77
|
- lib/active_kit/version.rb
|
67
78
|
- lib/activekit.rb
|
68
79
|
- lib/tasks/active_kit_tasks.rake
|
80
|
+
- lib/tasks/search_tasks.rake
|
69
81
|
homepage: https://github.com/plainsource/activekit
|
70
82
|
licenses:
|
71
83
|
- MIT
|
@@ -1,39 +0,0 @@
|
|
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
|
@@ -1,27 +0,0 @@
|
|
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
|