activekit 0.5.0.dev7 → 0.5.0
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/README.md +42 -2
- data/lib/active_kit/bedrock/bedrockable.rb +29 -0
- data/lib/active_kit/bedrock/bedrocker.rb +148 -0
- data/lib/active_kit/bedrock/bedrocking.rb +10 -0
- data/lib/active_kit/bedrock.rb +8 -0
- data/lib/active_kit/engine.rb +3 -0
- data/lib/active_kit/export/exportable.rb +1 -36
- data/lib/active_kit/export/exporter.rb +20 -103
- data/lib/active_kit/export/exporting.rb +1 -6
- data/lib/active_kit/search/index.rb +183 -0
- data/lib/active_kit/search/key.rb +45 -0
- data/lib/active_kit/search/manager.rb +44 -0
- data/lib/active_kit/search/search_result.rb +33 -0
- data/lib/active_kit/search/searchable.rb +16 -0
- data/lib/active_kit/search/searcher.rb +51 -0
- data/lib/active_kit/search/searching.rb +68 -0
- data/lib/active_kit/search/suggestion.rb +41 -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 +21 -0
- metadata +19 -6
- 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: 71b9eb7bd5fc27d2794cd15b8ec08a68e9baae06e37e072188260ddbaaa25473
|
4
|
+
data.tar.gz: 33f906a96b417c86b8048a2f663dbac5d4a6b0595f9cea23b6a0db92f98982cb
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 5172f9cb6b0aa0623a9e6f9a0263de7a5fadc016a25c6b92514f9916fca13653f8326479f4bc7fa8c54314c77c8d9d75435034b3c0e1e40054f7f002bf969d90
|
7
|
+
data.tar.gz: 65cb7a16747fb2281042dced7e1781bf0709437c470e8621b7ee232b6b205ab1ad021d8b1287a147abba85ddb96fe4f020e1ebb455b00a532d45488fbdf6d47f
|
data/README.md
CHANGED
@@ -3,6 +3,46 @@ Add the essential kit for rails ActiveRecord models and be happy.
|
|
3
3
|
|
4
4
|
## Usage
|
5
5
|
|
6
|
+
### Search Attribute
|
7
|
+
|
8
|
+
Add searching to your ActiveRecord models.
|
9
|
+
Search Attribute provides full searching functionality for your model database records using redis search including search suggestions.
|
10
|
+
|
11
|
+
You can define any number of model attributes in one model to search together.
|
12
|
+
|
13
|
+
Define the search attributes in accordance with the column name in your model like below.
|
14
|
+
```ruby
|
15
|
+
class Product < ApplicationRecord
|
16
|
+
search_attribute :name, type: :text
|
17
|
+
search_attribute :permalink, type: :tag
|
18
|
+
search_attribute :short_description, type: :text
|
19
|
+
search_attribute :published, type: :tag, sortable: true
|
20
|
+
end
|
21
|
+
```
|
22
|
+
|
23
|
+
You can also define a search_describer to describe the details of the search instead of using the defaults.
|
24
|
+
```ruby
|
25
|
+
class Product < ApplicationRecord
|
26
|
+
# search_describer method_name, database: -> { ActiveRecord::Base.connection_db_config.database }
|
27
|
+
search_describer :limit_by_search, database: -> { System::Current.tenant.database }
|
28
|
+
search_attribute :name, type: :text
|
29
|
+
search_attribute :permalink, type: :tag
|
30
|
+
search_attribute :short_description, type: :text
|
31
|
+
search_attribute :published, type: :tag, sortable: true
|
32
|
+
end
|
33
|
+
```
|
34
|
+
|
35
|
+
The following class methods will be added to your model class to use in accordance with details provided for search_describer:
|
36
|
+
```ruby
|
37
|
+
Product.limit_by_search(term: "term", tags: { published: true }, order: "name asc", page: 1)
|
38
|
+
Product.searcher.for(:limit_by_search).current_page
|
39
|
+
Product.searcher.for(:limit_by_search).previous_page?
|
40
|
+
Product.searcher.for(:limit_by_search).previous_page
|
41
|
+
Product.searcher.for(:limit_by_search).next_page?
|
42
|
+
Product.searcher.for(:limit_by_search).next_page
|
43
|
+
Product.searcher.for(:limit_by_search).suggestions(prefix: "prefix_term").keys
|
44
|
+
```
|
45
|
+
|
6
46
|
### Export Attribute
|
7
47
|
|
8
48
|
Add exporting to your ActiveRecord models.
|
@@ -23,8 +63,8 @@ end
|
|
23
63
|
You can also define an export_describer to describe the details of the export instead of using the defaults.
|
24
64
|
```ruby
|
25
65
|
class Product < ApplicationRecord
|
26
|
-
# export_describer method_name, kind: :csv, database: -> { ActiveRecord::Base.connection_db_config.database
|
27
|
-
export_describer :to_csv, kind: :csv, database: -> { System::Current.tenant.database
|
66
|
+
# export_describer method_name, kind: :csv, database: -> { ActiveRecord::Base.connection_db_config.database }
|
67
|
+
export_describer :to_csv, kind: :csv, database: -> { System::Current.tenant.database }
|
28
68
|
export_attribute :name
|
29
69
|
export_attribute :sku, heading: "SKU No."
|
30
70
|
export_attribute :image_name, value: lambda { |record| record.image&.name }, includes: :image
|
@@ -0,0 +1,29 @@
|
|
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
|
+
def #{current_component}er
|
12
|
+
@#{current_component}er ||= ActiveKit::#{current_component.to_s.titleize}::#{current_component.to_s.titleize}er.new(current_component: :#{current_component}, current_class: self)
|
13
|
+
end
|
14
|
+
|
15
|
+
private
|
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
|
+
end
|
25
|
+
CODE
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
@@ -0,0 +1,148 @@
|
|
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}(**params)
|
19
|
+
#{@current_component}er.run_describer_method("#{name}", params)
|
20
|
+
end
|
21
|
+
CODE
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
def create_attribute(name, options)
|
26
|
+
options.deep_symbolize_keys!
|
27
|
+
|
28
|
+
create_default_describer unless @describers.present?
|
29
|
+
|
30
|
+
describer_names = Array(options.delete(:describers))
|
31
|
+
describer_names = @describers.keys if describer_names.blank?
|
32
|
+
|
33
|
+
describer_names.each do |describer_name|
|
34
|
+
if describer_options = @describers.dig(describer_name)
|
35
|
+
describer_options[:attributes].store(name, options)
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
describer_names
|
40
|
+
end
|
41
|
+
|
42
|
+
def run_describer_method(describer_name, params)
|
43
|
+
raise "Could not find describer while creating describer method." unless describer = find_describer_by(name: describer_name.to_sym)
|
44
|
+
describer_method(describer, params)
|
45
|
+
end
|
46
|
+
|
47
|
+
def describer_method(describer, params)
|
48
|
+
raise NotImplementedError
|
49
|
+
end
|
50
|
+
|
51
|
+
def for(describer_name)
|
52
|
+
describer_name = @describers.keys[0] if describer_name.nil?
|
53
|
+
raise "Could not find any describer name in #{@current_class.name}." if describer_name.blank?
|
54
|
+
|
55
|
+
describer_name = describer_name.to_sym
|
56
|
+
raise "Could not find describer '#{describer_name}' in #{@current_class.name}." unless @describers.dig(describer_name)
|
57
|
+
componenting = @describers.dig(describer_name, :componenting)
|
58
|
+
return componenting if componenting
|
59
|
+
|
60
|
+
@describers[describer_name][:componenting] = "ActiveKit::#{@current_component.to_s.titleize}::#{@current_component.to_s.titleize}ing".constantize.new(describer: find_describer_by(name: describer_name), current_class: @current_class)
|
61
|
+
@describers[describer_name][:componenting]
|
62
|
+
end
|
63
|
+
|
64
|
+
def get_describer_names
|
65
|
+
@describers.keys.map(&:to_s)
|
66
|
+
end
|
67
|
+
|
68
|
+
private
|
69
|
+
|
70
|
+
def create_default_describer
|
71
|
+
case @current_component
|
72
|
+
when :export
|
73
|
+
create_describer(:to_csv, kind: :csv, database: -> { ActiveRecord::Base.connection_db_config.database })
|
74
|
+
when :search
|
75
|
+
create_describer(:limit_by_search, database: -> { ActiveRecord::Base.connection_db_config.database })
|
76
|
+
end
|
77
|
+
end
|
78
|
+
|
79
|
+
def find_describer_by(name:)
|
80
|
+
options = @describers.dig(name)
|
81
|
+
return nil unless options.present?
|
82
|
+
|
83
|
+
hash = {
|
84
|
+
name: name,
|
85
|
+
database: options[:database],
|
86
|
+
attributes: options[:attributes]
|
87
|
+
}
|
88
|
+
|
89
|
+
if @current_component == :export
|
90
|
+
hash.merge!(kind: options[:kind])
|
91
|
+
hash.merge!(includes: options[:attributes].values.map { |options| options.dig(:includes) }.compact.flatten(1).uniq)
|
92
|
+
hash.merge!(fields: build_describer_fields(options[:attributes]))
|
93
|
+
end
|
94
|
+
OpenStruct.new(hash)
|
95
|
+
end
|
96
|
+
|
97
|
+
def build_describer_fields(describer_attributes)
|
98
|
+
describer_attributes.inject({}) do |fields_hash, (name, options)|
|
99
|
+
enclosed_attributes = Array(options.dig(:attributes))
|
100
|
+
|
101
|
+
if enclosed_attributes.blank?
|
102
|
+
field_key, field_value = (get_heading(options.dig(:heading))&.to_s || name.to_s.titleize), (options.dig(:value) || name)
|
103
|
+
else
|
104
|
+
field_key, field_value = get_nested_field(name, options, enclosed_attributes)
|
105
|
+
end
|
106
|
+
fields_hash.store(field_key, field_value)
|
107
|
+
|
108
|
+
fields_hash
|
109
|
+
end
|
110
|
+
end
|
111
|
+
|
112
|
+
def get_nested_field(name, options, enclosed_attributes, ancestor_heading = nil)
|
113
|
+
parent_heading = ancestor_heading.present? ? ancestor_heading : ""
|
114
|
+
parent_heading += (get_heading(options.dig(:heading))&.to_s || name.to_s.singularize.titleize) + " "
|
115
|
+
parent_value = options.dig(:value) || name
|
116
|
+
|
117
|
+
enclosed_attributes.inject([[], [parent_value]]) do |nested_field, enclosed_attribute|
|
118
|
+
unless enclosed_attribute.is_a? Hash
|
119
|
+
nested_field_key = parent_heading + enclosed_attribute.to_s.titleize
|
120
|
+
nested_field_val = enclosed_attribute
|
121
|
+
|
122
|
+
nested_field[0].push(nested_field_key)
|
123
|
+
nested_field[1].push(nested_field_val)
|
124
|
+
else
|
125
|
+
enclosed_attribute.each do |enclosed_attribute_key, enclosed_attribute_value|
|
126
|
+
wrapped_attributes = Array(enclosed_attribute_value.dig(:attributes))
|
127
|
+
if wrapped_attributes.blank?
|
128
|
+
nested_field_key = parent_heading + (get_heading(enclosed_attribute_value.dig(:heading))&.to_s || enclosed_attribute_key.to_s.titleize)
|
129
|
+
nested_field_val = enclosed_attribute_value.dig(:value) || enclosed_attribute_key
|
130
|
+
else
|
131
|
+
nested_field_key, nested_field_val = get_nested_field(enclosed_attribute_key, enclosed_attribute_value, wrapped_attributes, parent_heading)
|
132
|
+
end
|
133
|
+
|
134
|
+
nested_field[0].push(nested_field_key)
|
135
|
+
nested_field[1].push(nested_field_val)
|
136
|
+
end
|
137
|
+
end
|
138
|
+
|
139
|
+
nested_field
|
140
|
+
end
|
141
|
+
end
|
142
|
+
|
143
|
+
def get_heading(options_heading)
|
144
|
+
options_heading.is_a?(Proc) ? options_heading.call(@current_class) : options_heading
|
145
|
+
end
|
146
|
+
end
|
147
|
+
end
|
148
|
+
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,48 +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
|
-
def export_describer_method(describer)
|
25
|
-
case describer.kind
|
26
|
-
when :csv
|
27
|
-
# The 'all' relation must be captured outside the Enumerator,
|
28
|
-
# else it will get reset to all the records of the class.
|
29
|
-
all_activerecord_relation = all.includes(describer.includes)
|
30
|
-
|
31
|
-
Enumerator.new do |yielder|
|
32
|
-
ActiveRecord::Base.connected_to(role: :writing, shard: describer.database.call) do
|
33
|
-
exporting = exporter.new_exporting(describer: describer)
|
34
|
-
|
35
|
-
# Add the headings.
|
36
|
-
yielder << CSV.generate_line(exporting.headings) if exporting.headings?
|
37
|
-
|
38
|
-
# Add the values.
|
39
|
-
# find_each will ignore any order if set earlier.
|
40
|
-
all_activerecord_relation.find_each do |record|
|
41
|
-
lines = exporting.lines_for(record: record)
|
42
|
-
lines.each { |line| yielder << CSV.generate_line(line) }
|
43
|
-
end
|
44
|
-
end
|
45
|
-
end
|
46
|
-
end
|
47
|
-
end
|
48
13
|
end
|
49
14
|
end
|
50
15
|
end
|
@@ -1,113 +1,30 @@
|
|
1
1
|
module ActiveKit
|
2
2
|
module Export
|
3
|
-
class Exporter
|
4
|
-
def
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
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
|
-
|
56
|
-
def new_exporting(describer:)
|
57
|
-
Exporting.new(describer: describer)
|
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)
|
3
|
+
class Exporter < Bedrock::Bedrocker
|
4
|
+
def describer_method(describer, params)
|
5
|
+
case describer.kind
|
6
|
+
when :csv
|
7
|
+
# The 'all' relation must be captured outside the Enumerator,
|
8
|
+
# else it will get reset to all the records of the class.
|
9
|
+
all_activerecord_relation = @current_class.all.includes(describer.includes)
|
10
|
+
|
11
|
+
Enumerator.new do |yielder|
|
12
|
+
ActiveRecord::Base.connected_to(role: :writing, shard: describer.database.call.to_sym) do
|
13
|
+
exporting = self.for(describer.name)
|
14
|
+
|
15
|
+
# Add the headings.
|
16
|
+
yielder << CSV.generate_line(exporting.headings) if exporting.headings?
|
17
|
+
|
18
|
+
# Add the values.
|
19
|
+
# find_each will ignore any order if set earlier.
|
20
|
+
all_activerecord_relation.find_each do |record|
|
21
|
+
lines = exporting.lines_for(record: record)
|
22
|
+
lines.each { |line| yielder << CSV.generate_line(line) }
|
97
23
|
end
|
98
|
-
|
99
|
-
nested_field[0].push(nested_field_key)
|
100
|
-
nested_field[1].push(nested_field_val)
|
101
24
|
end
|
102
25
|
end
|
103
|
-
|
104
|
-
nested_field
|
105
26
|
end
|
106
27
|
end
|
107
|
-
|
108
|
-
def get_heading(options_heading)
|
109
|
-
options_heading.is_a?(Proc) ? options_heading.call(@current_class) : options_heading
|
110
|
-
end
|
111
28
|
end
|
112
29
|
end
|
113
30
|
end
|
@@ -0,0 +1,183 @@
|
|
1
|
+
module ActiveKit
|
2
|
+
module Search
|
3
|
+
class Index
|
4
|
+
attr_reader :prefix, :schema, :attribute_value_parser
|
5
|
+
|
6
|
+
def initialize(current_class:, describer:)
|
7
|
+
@redis = ActiveKit::Search.redis
|
8
|
+
@current_class = current_class
|
9
|
+
@describer = describer
|
10
|
+
|
11
|
+
current_class_name = current_class.to_s.parameterize.pluralize
|
12
|
+
describer_name = describer.name.to_s
|
13
|
+
@name = "activekit:search:index:#{current_class_name}:#{describer_name}"
|
14
|
+
@prefix = "activekit:search:#{current_class_name}:#{describer_name}"
|
15
|
+
@schema = {}
|
16
|
+
@attribute_value_parser = {}
|
17
|
+
end
|
18
|
+
|
19
|
+
def add_attribute_to_schema(name:, options:)
|
20
|
+
raise "Error: No type specified for the search attribute '#{name}'." unless options[:type].present?
|
21
|
+
|
22
|
+
attribute_schema = []
|
23
|
+
|
24
|
+
as = options.delete(:as)
|
25
|
+
attribute_schema.push("AS #{as}") unless as.nil?
|
26
|
+
|
27
|
+
type = options.delete(:type)
|
28
|
+
attribute_schema.push(type.to_s.upcase) unless type.nil?
|
29
|
+
|
30
|
+
options.each do |key, value|
|
31
|
+
if key == :value
|
32
|
+
@attribute_value_parser.store(name.to_s, value)
|
33
|
+
elsif key.is_a?(Symbol)
|
34
|
+
if value == true
|
35
|
+
attribute_schema.push(key.to_s.upcase)
|
36
|
+
elsif value != false
|
37
|
+
attribute_schema.push("#{key.to_s.upcase} #{value.to_s}")
|
38
|
+
end
|
39
|
+
else
|
40
|
+
raise "Invalid option provided to search attribute."
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
@schema.store(name.to_s, attribute_schema.join(" "))
|
45
|
+
end
|
46
|
+
|
47
|
+
def reload
|
48
|
+
current_command = @redis.get("#{@name}:command")
|
49
|
+
schema = { "database" => "TAG SORTABLE", "id" => "NUMERIC SORTABLE" }.merge(@schema)
|
50
|
+
command = "FT.CREATE #{@name} ON HASH PREFIX 1 #{@prefix}: SCHEMA #{schema.to_a.flatten.join(' ')}"
|
51
|
+
unless current_command == command
|
52
|
+
drop
|
53
|
+
@redis.call(command.split(' '))
|
54
|
+
@redis.set("#{@name}:command", command)
|
55
|
+
Rails.logger.info "ActiveKit::Search | Index Reloaded: " + "#{@name}:command"
|
56
|
+
Rails.logger.debug "=> " + @redis.get("#{@name}:command").to_s
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
def drop
|
61
|
+
if exists?
|
62
|
+
command = "FT.DROPINDEX #{@name}"
|
63
|
+
@redis.call(command.split(' '))
|
64
|
+
@redis.del("#{@name}:command")
|
65
|
+
Rails.logger.info "ActiveKit::Search | Index Dropped: " + @name
|
66
|
+
end
|
67
|
+
end
|
68
|
+
|
69
|
+
# Redis returns the results in the following form. Where first value is count of results, then every 2 elements are document_id, attributes respectively.
|
70
|
+
# [2, "doc:3", ["name", "Grape Juice", "stock_quantity", "4", "minimum_stock", "2"], "doc:4", ["name", "Apple Juice", "stock_quantity", "4", "minimum_stock", "2"]]
|
71
|
+
def fetch(term: nil, matching: "*", tags: {}, modifiers: {}, offset: nil, limit: nil, order: nil, page: nil, **options)
|
72
|
+
original_term = term
|
73
|
+
|
74
|
+
if term == ""
|
75
|
+
results = nil
|
76
|
+
elsif self.exists?
|
77
|
+
if term.present?
|
78
|
+
term.strip!
|
79
|
+
term = escape_separators(term)
|
80
|
+
|
81
|
+
case matching
|
82
|
+
when "*"
|
83
|
+
term = "#{term}*"
|
84
|
+
when "%"
|
85
|
+
term = "%#{term}%"
|
86
|
+
when "%%"
|
87
|
+
term = "%%#{term}%%"
|
88
|
+
when "%%%"
|
89
|
+
term = "%%%#{term}%%%"
|
90
|
+
end
|
91
|
+
|
92
|
+
term = " #{term}"
|
93
|
+
else
|
94
|
+
term = ""
|
95
|
+
end
|
96
|
+
|
97
|
+
if tags.present?
|
98
|
+
tags = tags.map do |key, value|
|
99
|
+
value = value.join("|") if value.is_a?(Array)
|
100
|
+
"@#{escape_separators(key)}:{#{escape_separators(value, include_space: true).presence || 'nil'}}"
|
101
|
+
end
|
102
|
+
tags = tags.join(" ")
|
103
|
+
tags = " #{tags}"
|
104
|
+
else
|
105
|
+
tags = ""
|
106
|
+
end
|
107
|
+
|
108
|
+
if modifiers.present?
|
109
|
+
modifiers = modifiers.map { |key, value| "@#{escape_separators(key)}:#{escape_separators(value)}" }.join(" ")
|
110
|
+
modifiers = " #{modifiers}"
|
111
|
+
else
|
112
|
+
modifiers = ""
|
113
|
+
end
|
114
|
+
|
115
|
+
if (offset.present? || limit.present?) && page.present?
|
116
|
+
raise "Error: Cannot specify page and offset/limit at the same time. Please specify one of either page or offset/limit."
|
117
|
+
end
|
118
|
+
|
119
|
+
if page.present?
|
120
|
+
page = page.to_i.abs
|
121
|
+
|
122
|
+
case page
|
123
|
+
when 0
|
124
|
+
page = 1
|
125
|
+
offset = 0
|
126
|
+
limit = 15
|
127
|
+
when 1
|
128
|
+
offset = 0
|
129
|
+
limit = 15
|
130
|
+
when 2
|
131
|
+
offset = 15
|
132
|
+
limit = 30
|
133
|
+
when 3
|
134
|
+
offset = 45
|
135
|
+
limit = 50
|
136
|
+
else
|
137
|
+
limit = 100
|
138
|
+
offset = 15 + 30 + 50 + (page - 4) * limit
|
139
|
+
end
|
140
|
+
end
|
141
|
+
|
142
|
+
query = "@database:{#{escape_separators(@describer.database.call, include_space: true)}}#{term}#{tags}#{modifiers}"
|
143
|
+
command = [
|
144
|
+
"FT.SEARCH",
|
145
|
+
@name,
|
146
|
+
query,
|
147
|
+
"LIMIT",
|
148
|
+
offset ? offset.to_i : 0, # 0 is the default offset of redisearch in LIMIT 0 10. https://redis.io/commands/ft.search
|
149
|
+
limit ? limit.to_i : 10 # 10 is the default limit of redisearch in LIMIT 0 10. https://redis.io/commands/ft.search
|
150
|
+
]
|
151
|
+
command.push("SORTBY", *order.split(' ')) if order.present?
|
152
|
+
results = @redis.call(command)
|
153
|
+
Rails.logger.info "ActiveKit::Search | Index Searched: " + command.to_s
|
154
|
+
Rails.logger.debug "=> " + results.to_s
|
155
|
+
else
|
156
|
+
results = nil
|
157
|
+
end
|
158
|
+
|
159
|
+
SearchResult.new(term: original_term, results: results, offset: offset, limit: limit, page: page, current_class: @current_class)
|
160
|
+
end
|
161
|
+
|
162
|
+
# List of characters from https://oss.redislabs.com/redisearch/Escaping/
|
163
|
+
# ,.<>{}[]"':;!@#$%^&*()-+=~
|
164
|
+
def escape_separators(value, include_space: false)
|
165
|
+
value = value.to_s
|
166
|
+
|
167
|
+
unless include_space
|
168
|
+
pattern = %r{(\'|\"|\.|\,|\;|\<|\>|\{|\}|\[|\]|\"|\'|\=|\~|\*|\:|\#|\+|\^|\$|\@|\%|\!|\&|\)|\(|/|\-|\\)}
|
169
|
+
else
|
170
|
+
pattern = %r{(\'|\"|\.|\,|\;|\<|\>|\{|\}|\[|\]|\"|\'|\=|\~|\*|\:|\#|\+|\^|\$|\@|\%|\!|\&|\)|\(|/|\-|\\|\s)}
|
171
|
+
end
|
172
|
+
|
173
|
+
value.gsub(pattern) { |match| '\\' + match }
|
174
|
+
end
|
175
|
+
|
176
|
+
private
|
177
|
+
|
178
|
+
def exists?
|
179
|
+
@redis.call("FT._LIST").include?(@name)
|
180
|
+
end
|
181
|
+
end
|
182
|
+
end
|
183
|
+
end
|
@@ -0,0 +1,45 @@
|
|
1
|
+
module ActiveKit
|
2
|
+
module Search
|
3
|
+
class Key
|
4
|
+
def initialize(index:, describer:)
|
5
|
+
@redis = ActiveKit::Search.redis
|
6
|
+
@index = index
|
7
|
+
@describer = describer
|
8
|
+
end
|
9
|
+
|
10
|
+
def reload(record:)
|
11
|
+
clear(record: record)
|
12
|
+
|
13
|
+
hash_key = key(record: record)
|
14
|
+
hash_value = { "database" => @describer.database.call, "id" => record.id }
|
15
|
+
@index.schema.each do |field_name, field_value|
|
16
|
+
attribute_name = field_name
|
17
|
+
attribute_value = @index.attribute_value_parser[field_name]&.call(record) || record.public_send(field_name)
|
18
|
+
attribute_value = field_value.downcase.include?("tag") ? attribute_value : @index.escape_separators(attribute_value)
|
19
|
+
hash_value.store(attribute_name, attribute_value)
|
20
|
+
end
|
21
|
+
@redis.hset(hash_key, hash_value)
|
22
|
+
Rails.logger.info "ActiveKit::Search | Key Reloaded: " + hash_key
|
23
|
+
Rails.logger.debug "=> " + @redis.hgetall("#{hash_key}").to_s
|
24
|
+
end
|
25
|
+
|
26
|
+
def clear(record:)
|
27
|
+
hash_key = key(record: record)
|
28
|
+
drop(keys: hash_key)
|
29
|
+
end
|
30
|
+
|
31
|
+
def drop(keys:)
|
32
|
+
return unless keys.present?
|
33
|
+
if @redis.del(keys) > 0
|
34
|
+
Rails.logger.info "ActiveKit::Search | Keys Removed: " + keys.to_s
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
private
|
39
|
+
|
40
|
+
def key(record:)
|
41
|
+
"#{@index.prefix}:#{@describer.database.call}:#{record.id}"
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
@@ -0,0 +1,44 @@
|
|
1
|
+
module ActiveKit
|
2
|
+
module Search
|
3
|
+
class Manager
|
4
|
+
def initialize(given_class:, given_describer:)
|
5
|
+
@given_class = given_class
|
6
|
+
@given_describer = given_describer
|
7
|
+
|
8
|
+
Rails.application.eager_load!
|
9
|
+
end
|
10
|
+
|
11
|
+
def reload
|
12
|
+
task(name: :reload, log_name: "Reloading")
|
13
|
+
end
|
14
|
+
|
15
|
+
def clear
|
16
|
+
task(name: :clear, log_name: "Clearing")
|
17
|
+
end
|
18
|
+
|
19
|
+
def drop
|
20
|
+
task(name: :drop, log_name: "Dropping")
|
21
|
+
end
|
22
|
+
|
23
|
+
private
|
24
|
+
|
25
|
+
def task(name:, log_name: "Reloading")
|
26
|
+
models = @given_class.present? ? [@given_class] : ActiveRecord::Base.descendants.collect(&:name)
|
27
|
+
# Removing these models for efficiency as they will never contain searcher.
|
28
|
+
models -= ["ApplicationRecord", "ActionText::Record", "ActionText::RichText", "ActiveKit::ApplicationRecord", "ActionMailbox::Record", "ActionMailbox::InboundEmail", "ActiveStorage::Record", "ActiveStorage::Blob", "ActiveStorage::VariantRecord", "ActiveStorage::Attachment"]
|
29
|
+
models.each do |model|
|
30
|
+
model_const = model.constantize
|
31
|
+
if model_const.try(:searcher)
|
32
|
+
describer_names = @given_describer.present? ? [@given_describer] : model_const.searcher.get_describer_names
|
33
|
+
describer_names.each do |describer_name|
|
34
|
+
if model_const.searcher.for(describer_name).attributes_present?
|
35
|
+
puts "ActiveKit::Search | #{log_name}: #{model}"
|
36
|
+
model_const.searcher.for(describer_name).public_send(name)
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
44
|
+
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,51 @@
|
|
1
|
+
module ActiveKit
|
2
|
+
module Search
|
3
|
+
class Searcher < Bedrock::Bedrocker
|
4
|
+
def create_attribute(name, options)
|
5
|
+
describer_names = super
|
6
|
+
|
7
|
+
depends_on = options.delete(:depends_on) || {}
|
8
|
+
describer_names.each do |describer_name|
|
9
|
+
set_reload_callbacks(depends_on, describer_name)
|
10
|
+
self.for(describer_name).add_attribute(name: name, options: options.deep_dup)
|
11
|
+
end
|
12
|
+
end
|
13
|
+
|
14
|
+
def describer_method(describer, params)
|
15
|
+
params[:page] = 1 if params.key?(:page) && params[:page].blank?
|
16
|
+
self.for(describer.name).fetch(term: params.delete(:term), **params).records
|
17
|
+
end
|
18
|
+
|
19
|
+
private
|
20
|
+
|
21
|
+
# Set callbacks for current class and depending classes.
|
22
|
+
def set_reload_callbacks(depends_on, describer_name)
|
23
|
+
@current_class.class_eval do
|
24
|
+
unless searcher.for(describer_name).attributes_present?
|
25
|
+
after_commit do
|
26
|
+
self.class.searcher.for(describer_name).reload(record: self)
|
27
|
+
logger.info "ActiveKit::Search | Indexing from #{self.class.name}: Done."
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
unless depends_on.empty?
|
32
|
+
depends_on.each do |depends_on_association, depends_on_inverse|
|
33
|
+
klass = self.reflect_on_all_associations.map { |assoc| [assoc.name, assoc.klass.name] }.to_h[depends_on_association]
|
34
|
+
klass.constantize.class_eval do
|
35
|
+
after_commit do
|
36
|
+
inverse_assoc = self.public_send(depends_on_inverse)
|
37
|
+
if inverse_assoc.respond_to?(:each)
|
38
|
+
inverse_assoc.each { |instance| instance.class.searcher.for(describer_name).reload(record: instance) }
|
39
|
+
else
|
40
|
+
inverse_assoc.class.searcher.for(describer_name).reload(record: inverse_assoc)
|
41
|
+
end
|
42
|
+
logger.info "ActiveKit::Search | Indexing from #{self.class.name}: Done."
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
@@ -0,0 +1,68 @@
|
|
1
|
+
module ActiveKit
|
2
|
+
module Search
|
3
|
+
class Searching < Bedrock::Bedrocking
|
4
|
+
attr_reader :current_page, :previous_page, :next_page
|
5
|
+
|
6
|
+
def initialize(describer:, current_class:)
|
7
|
+
super
|
8
|
+
|
9
|
+
@index = Index.new(current_class: @current_class, describer: describer)
|
10
|
+
@key = Key.new(index: @index, describer: describer)
|
11
|
+
@suggestion = Suggestion.new(current_class: @current_class, describer: describer)
|
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,41 @@
|
|
1
|
+
module ActiveKit
|
2
|
+
module Search
|
3
|
+
class Suggestion
|
4
|
+
def initialize(current_class:, describer:)
|
5
|
+
@redis = ActiveKit::Search.redis
|
6
|
+
@current_class = current_class
|
7
|
+
@describer = describer
|
8
|
+
@current_class_name = current_class.to_s.parameterize.pluralize
|
9
|
+
@describer_name = describer.name.to_s
|
10
|
+
end
|
11
|
+
|
12
|
+
def add(term:, score: 1, increment: true)
|
13
|
+
command = ["FT.SUGADD", key, term, score, (increment ? 'INCR' : '')]
|
14
|
+
@redis.call(command)
|
15
|
+
end
|
16
|
+
|
17
|
+
def fetch(prefix:)
|
18
|
+
command = ["FT.SUGGET", key, prefix, "FUZZY", "MAX", "10", "WITHSCORES"]
|
19
|
+
results = @redis.call(command)
|
20
|
+
|
21
|
+
SuggestionResult.new(prefix: prefix, results: results)
|
22
|
+
end
|
23
|
+
|
24
|
+
def del(term:)
|
25
|
+
command = ["FT.SUGDEL", key, term]
|
26
|
+
@redis.call(command)
|
27
|
+
end
|
28
|
+
|
29
|
+
def len
|
30
|
+
command = ["FT.SUGLEN", key]
|
31
|
+
@redis.call(command)
|
32
|
+
end
|
33
|
+
|
34
|
+
private
|
35
|
+
|
36
|
+
def key
|
37
|
+
"activekit:search:suggestions:#{@current_class_name}:#{@describer_name}:#{@describer.database.call}"
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
41
|
+
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 :Manager
|
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,21 @@
|
|
1
|
+
namespace :active_kit do
|
2
|
+
namespace :search do
|
3
|
+
desc "bundle exec rails active_kit:search:reload CLASS='Article' DESCRIBER='limit_by_search'"
|
4
|
+
task :reload do
|
5
|
+
manager = ActiveKit::Search::Manager.new(given_class: ENV['CLASS'], given_describer: ENV['DESCRIBER'])
|
6
|
+
manager.reload
|
7
|
+
end
|
8
|
+
|
9
|
+
desc "bundle exec rails active_kit:search:clear CLASS='Article' DESCRIBER='limit_by_search'"
|
10
|
+
task :clear do
|
11
|
+
manager = ActiveKit::Search::Manager.new(given_class: ENV['CLASS'], given_describer: ENV['DESCRIBER'])
|
12
|
+
manager.clear
|
13
|
+
end
|
14
|
+
|
15
|
+
desc "bundle exec rails active_kit:search:drop CLASS='Article' DESCRIBER='limit_by_search'"
|
16
|
+
task :drop do
|
17
|
+
manager = ActiveKit::Search::Manager.new(given_class: ENV['CLASS'], given_describer: ENV['DESCRIBER'])
|
18
|
+
manager.drop
|
19
|
+
end
|
20
|
+
end
|
21
|
+
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
|
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-24 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: rails
|
@@ -50,8 +50,10 @@ 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
|
56
|
+
- lib/active_kit/bedrock/bedrocking.rb
|
55
57
|
- lib/active_kit/engine.rb
|
56
58
|
- lib/active_kit/export.rb
|
57
59
|
- lib/active_kit/export/exportable.rb
|
@@ -63,9 +65,20 @@ files:
|
|
63
65
|
- lib/active_kit/position/positionable.rb
|
64
66
|
- lib/active_kit/position/positioner.rb
|
65
67
|
- lib/active_kit/position/positioning.rb
|
68
|
+
- lib/active_kit/search.rb
|
69
|
+
- lib/active_kit/search/index.rb
|
70
|
+
- lib/active_kit/search/key.rb
|
71
|
+
- lib/active_kit/search/manager.rb
|
72
|
+
- lib/active_kit/search/search_result.rb
|
73
|
+
- lib/active_kit/search/searchable.rb
|
74
|
+
- lib/active_kit/search/searcher.rb
|
75
|
+
- lib/active_kit/search/searching.rb
|
76
|
+
- lib/active_kit/search/suggestion.rb
|
77
|
+
- lib/active_kit/search/suggestion_result.rb
|
66
78
|
- lib/active_kit/version.rb
|
67
79
|
- lib/activekit.rb
|
68
80
|
- lib/tasks/active_kit_tasks.rake
|
81
|
+
- lib/tasks/search_tasks.rake
|
69
82
|
homepage: https://github.com/plainsource/activekit
|
70
83
|
licenses:
|
71
84
|
- MIT
|
@@ -82,9 +95,9 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
82
95
|
version: '0'
|
83
96
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
84
97
|
requirements:
|
85
|
-
- - "
|
98
|
+
- - ">="
|
86
99
|
- !ruby/object:Gem::Version
|
87
|
-
version:
|
100
|
+
version: '0'
|
88
101
|
requirements: []
|
89
102
|
rubygems_version: 3.1.2
|
90
103
|
signing_key:
|
@@ -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
|