activekit 0.5.0.dev7 → 0.5.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|