search_enjoy 0.1.1

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 ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: ddd4395bd6c040811e98b5b0a5870074e37278996c998060b5ddc3180c58f0d0
4
+ data.tar.gz: fb15c8b3a45ff5b06979ed74ae7b4cbcbabd0edba92774bb1ed71eb03c1fd967
5
+ SHA512:
6
+ metadata.gz: 38fd91eaf7198951e487308c490c87d207253e34fd34e3d2837885c82de61a070146f5820fa7f13da71bd277a803bb28e236fc7fcebe1bf0bdb01a23149798b6
7
+ data.tar.gz: c41f44e7ef552e65f24174ad9cd17f00ea168551ce8cd98d815571f3ec6675318c94008cb8c65467ef544b3e273c075155e4154d6cbf990f605932d08c51d473
@@ -0,0 +1,24 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'search_enjoy/schema'
4
+ require_relative 'search_enjoy/indexing'
5
+ require_relative 'search_enjoy/searching'
6
+ require_relative 'search_enjoy/aggregation'
7
+ require_relative 'search_enjoy/query'
8
+ require_relative 'search_enjoy/configuration'
9
+ require_relative 'search_enjoy/dumping'
10
+ require_relative 'search_enjoy/search_index'
11
+ require 'dry-schema'
12
+
13
+ module SearchEnjoy
14
+ def self.included(base)
15
+ base.class_eval do
16
+ include SearchEnjoy::Schema
17
+ include SearchEnjoy::Indexing
18
+ include SearchEnjoy::Searching
19
+ include SearchEnjoy::Aggregation
20
+ include SearchEnjoy::Configuration
21
+ include SearchEnjoy::Dumping
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,44 @@
1
+ module SearchEnjoy
2
+ module Aggregation
3
+ def self.included(base)
4
+ base.class_eval do
5
+ include InstanceMethods
6
+ extend ClassMethods
7
+ end
8
+ end
9
+
10
+ module InstanceMethods
11
+
12
+ end
13
+
14
+ module ClassMethods
15
+ def aggregate(*args)
16
+ values = @search_index.values.map { |object| args.map { |key| object[key] } }
17
+ values_per_keys = values.transpose.map(&:uniq).zip(args)
18
+
19
+ aggregations = []
20
+
21
+ values_per_keys.each do |key_values|
22
+ key = key_values.last
23
+
24
+ hash = { field: key, data: [] }
25
+
26
+ key_values.first.each do |value|
27
+ count = search(key => value).size
28
+
29
+ data_hash = {
30
+ value: value,
31
+ count: count
32
+ }
33
+
34
+ hash[:data] << data_hash
35
+ end
36
+
37
+ aggregations << hash
38
+ end
39
+
40
+ aggregations
41
+ end
42
+ end
43
+ end
44
+ end
@@ -0,0 +1,26 @@
1
+ module SearchEnjoy
2
+ module Configuration
3
+ def self.included(base)
4
+ base.class_eval do
5
+ @@index_configuration = Configuration.new(base)
6
+
7
+ def self.index_configuration(&block)
8
+ return @@index_configuration unless block_given?
9
+
10
+ yield @@index_configuration
11
+ end
12
+ end
13
+ end
14
+
15
+ class Configuration
16
+ attr_accessor :dump_dir_path, :dump_frequency, :dump_enable, :dump_filename
17
+
18
+ def initialize(indexing_class)
19
+ @dump_dir_path = "./data/search_enjoy"
20
+ @dump_filename = "#{indexing_class}_#{Time.now.strftime('%Y%m%d%H%M%S')}"
21
+ @dump_frequency = 100
22
+ @dump_enable = true
23
+ end
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,51 @@
1
+ module SearchEnjoy
2
+ module Dumping
3
+ def self.included(base)
4
+ base.class_eval do
5
+ extend ClassMethods
6
+ end
7
+ end
8
+
9
+ class DumpException < RuntimeError; end
10
+
11
+ module ClassMethods
12
+ attr_reader :search_index_state
13
+
14
+ def dump_index_to_file
15
+ path = index_configuration.dump_dir_path
16
+ filename = index_configuration.dump_filename
17
+
18
+ FileUtils.mkdir_p(path) unless File.exist? path
19
+
20
+ File.open("#{path}/#{filename}", 'w') do |file|
21
+ file.write(@search_index.to_json)
22
+ end
23
+ end
24
+
25
+ def load_index_from_file
26
+ path = index_configuration.dump_dir_path
27
+ filename = index_configuration.dump_filename
28
+
29
+ File.open("#{path}/#{filename}", 'r') do |file|
30
+ search_index.load_json(file.read)
31
+ end
32
+ end
33
+
34
+ def initialize_dump_counter
35
+ @search_index_state[:dump_counter] = 0
36
+ end
37
+
38
+ def increase_dump_counter
39
+ @search_index_state[:dump_counter] += 1
40
+ end
41
+
42
+ def need_dump?
43
+ raise DumpException, "Index doesn't exist" if @search_index.nil?
44
+
45
+ @search_index_state[:dump_counter] >= index_configuration.dump_frequency && index_configuration.dump_enable
46
+ rescue StandardError => e
47
+ e.message
48
+ end
49
+ end
50
+ end
51
+ end
@@ -0,0 +1,81 @@
1
+ # frozen_string_literal: true
2
+ require 'json'
3
+
4
+ module SearchEnjoy
5
+ # Module responsible for indexing elements in collection
6
+ module Indexing
7
+ def self.included(base)
8
+ base.class_eval do
9
+ include InstanceMethods
10
+ extend ClassMethods
11
+ end
12
+ end
13
+
14
+ class IndexException < RuntimeError; end
15
+
16
+ # Class methods and variables for indexing
17
+ module ClassMethods
18
+ attr_accessor :search_index
19
+
20
+ def create_index!
21
+ raise IndexException, 'Index already exist' unless @search_index.nil?
22
+
23
+ @search_index = SearchIndex.new(index_schema: @index_schema)
24
+ @search_index_state = {}
25
+
26
+ initialize_dump_counter
27
+ rescue StandardError => e
28
+ e.message
29
+ end
30
+
31
+ def delete_index!
32
+ raise IndexException, "Index doesn't exist" if @search_index.nil?
33
+
34
+ @search_index = nil
35
+ rescue StandardError => e
36
+ e.message
37
+ end
38
+
39
+ def recreate_index!
40
+ delete_index!
41
+ create_index!
42
+ end
43
+
44
+ end
45
+
46
+ module InstanceMethods
47
+ # For default execute methods with attributes name
48
+ def as_indexed_json
49
+ schema = self.class.index_schema
50
+
51
+ hash = {}
52
+
53
+ schema.key_map.each do |key|
54
+ hash[key.name.to_sym] = send(key.name)
55
+ end
56
+
57
+ hash
58
+ end
59
+
60
+ def index_object
61
+ raise IndexException, 'Index doesnt exist' if self.class.search_index.nil?
62
+
63
+ indexed_object = self.class.index_schema.call(as_indexed_json)
64
+
65
+ return if indexed_object.errors.messages.size > 0
66
+
67
+ self.class.search_index[id.to_s.to_sym] = indexed_object
68
+
69
+ Thread.new do
70
+ self.class.increase_dump_counter
71
+
72
+ self.class.dump_index_to_file if self.class.need_dump?
73
+ end.join
74
+
75
+ indexed_object
76
+ rescue StandardError => e
77
+ e.message
78
+ end
79
+ end
80
+ end
81
+ end
@@ -0,0 +1,148 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SearchEnjoy
4
+ class QueryResult < Array
5
+ attr_writer :parent_class
6
+
7
+ def initialize(parent_class, *args)
8
+ super(*args)
9
+
10
+ @parent_class = parent_class
11
+ end
12
+
13
+ private def method_missing(symbol, *args)
14
+ @parent_class.send(symbol, *(args << self))
15
+ end
16
+
17
+ def respond_to_missing?(symbol, *_args)
18
+ @parent_class.respond_to? symbol
19
+ end
20
+ end
21
+
22
+ class Query
23
+ attr_reader :default_options, :query_hash
24
+
25
+ def initialize(hash, **opts)
26
+ @default_options = {}
27
+
28
+ @default_options = opts
29
+
30
+ @parent_query = nil
31
+
32
+ @default_options[:must] ||= false
33
+ @default_options[:inverse] ||= false
34
+
35
+ @query_hash = to_query_hash(hash)
36
+ end
37
+
38
+ def to_query_hash(hash, **opts)
39
+ result = {}
40
+
41
+ hash.each_pair do |key, value|
42
+ value = to_query_hash(value, opts) if value.instance_of? Hash
43
+
44
+ result[key] = { value: value }.merge(@default_options).merge(opts)
45
+ end
46
+
47
+ result
48
+ end
49
+
50
+ def inverse!(hash = nil)
51
+ result = {}
52
+
53
+ hash ||= @query_hash
54
+
55
+ hash.each_pair do |key, value|
56
+ value = inverse(value) if value.instance_of? Hash
57
+
58
+ result[key] = { value: value, must: !hash[:must], inverse: !hash[:inverse] }
59
+ end
60
+
61
+ @query_hash = result if hash == @query_hash
62
+
63
+ result
64
+ end
65
+
66
+ # build_query do
67
+ # must(:attr1).be_in []
68
+ # must(:attr2).eq_to value
69
+ #
70
+ # describe :attr3 do
71
+ # must(:key_1).be_in
72
+ # must.not(:key_2).eq_to value
73
+ # end
74
+ # end
75
+ def self.build_query(&block)
76
+ query = new({})
77
+
78
+ query.instance_eval(&block)
79
+
80
+ query
81
+ end
82
+
83
+ def add_statements(&block)
84
+ instance_eval(&block)
85
+ end
86
+
87
+ def must(attribute = nil)
88
+ query = if attribute.nil?
89
+ Query.new({}, must: true)
90
+ else
91
+ Query.new({attribute => nil}, must: true)
92
+ end
93
+
94
+ query.send('parent_query=', self)
95
+
96
+
97
+ query
98
+ end
99
+
100
+ def should(attribute = nil)
101
+ query = if attribute.nil?
102
+ Query.new({}, must: false)
103
+ else
104
+ Query.new({attribute => nil}, must: false)
105
+ end
106
+
107
+ query.send('parent_query=', self)
108
+
109
+ query
110
+ end
111
+
112
+ def not(attribute)
113
+ @default_options[:inverse] = true
114
+
115
+ @query_hash = to_query_hash({attribute => nil})
116
+
117
+ self
118
+ end
119
+
120
+ def be(value)
121
+ key = @query_hash.keys.first
122
+
123
+ @query_hash[key][:value] = value
124
+
125
+ merge_to_parent!
126
+ end
127
+
128
+ def describe(attribute, &block)
129
+ query = Query.new({})
130
+
131
+ query.instance_eval(&block)
132
+
133
+ puts query.inspect
134
+
135
+ @query_hash.merge!({attribute => query.query_hash})
136
+ end
137
+
138
+ private
139
+
140
+ def parent_query=(query)
141
+ @parent_query = query
142
+ end
143
+
144
+ def merge_to_parent!
145
+ @parent_query.query_hash.merge!(@query_hash)
146
+ end
147
+ end
148
+ end
@@ -0,0 +1,138 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SearchEnjoy
4
+ # Module responsible for defining index schema
5
+ module Schema
6
+ def self.included(base)
7
+ base.class_eval do
8
+ extend ClassMethods
9
+ end
10
+ end
11
+
12
+ # class responsible for creating hash schema due to schema DSL
13
+ class Mapping
14
+ attr_reader :mapping
15
+
16
+ FILLED_FIELDS = %i[
17
+ integer
18
+ string
19
+ float
20
+ date
21
+ ].freeze
22
+
23
+ NESTED_FIELDS = %i[array hash].freeze
24
+ def initialize
25
+ @mapping = {}
26
+ end
27
+
28
+ def respond_to_missing?; end
29
+
30
+ private
31
+
32
+ # @todo
33
+ # Think about other DSL
34
+ #
35
+ # attr1(:array).of :integer
36
+ # attr2(:hash).with do
37
+ # key(:key_1).of :integer
38
+ # end
39
+ def hash_key(key, value_type, &block)
40
+ @mapping[key] = if block_given?
41
+ nested_mapping = Mapping.new
42
+ nested_mapping.instance_eval(&block)
43
+
44
+ nested_mapping.mapping
45
+ else
46
+ value_type
47
+ end
48
+ end
49
+
50
+ def array_type(type, &block)
51
+ # @mapping[:array] = if block_given?
52
+ # nested_mapping = Mapping.new
53
+ # nested_mapping.instance_eval(&block)
54
+ #
55
+ # nested_mapping.mapping
56
+ # else
57
+ # type
58
+ # end
59
+
60
+ hash_key(:array, type, &block)
61
+ end
62
+
63
+ def method_missing(method, *args, &block)
64
+ type = args.first
65
+
66
+ @mapping[method] = type if FILLED_FIELDS.include? type
67
+
68
+ if NESTED_FIELDS.include? type
69
+ nested_mapping = Mapping.new
70
+ nested_mapping.instance_eval(&block)
71
+
72
+ @mapping[method] = nested_mapping.mapping
73
+ end
74
+
75
+ @mapping[method]
76
+ end
77
+ end
78
+
79
+ # Class methods and variables
80
+ module ClassMethods
81
+ attr_reader :index_schema
82
+
83
+ # define_json_schema do
84
+ # attr1 :value_type
85
+ #
86
+ # attr2 :array do
87
+ # type :value_type
88
+ # end
89
+ #
90
+ # attr3 :hash do
91
+ # key :key, :value_type
92
+ # end
93
+ # end
94
+ def define_json_schema(&block)
95
+ mapping = Mapping.new
96
+
97
+ mapping.instance_eval(&block)
98
+
99
+ @index_schema = create_schema(mapping.mapping)
100
+ end
101
+
102
+ private
103
+
104
+ # define a method for required(key) in Dry::Schema.Params
105
+ def dry_schema_method(value)
106
+ if value.instance_of?(Hash)
107
+ value.key?(:array) ? :array : :hash
108
+ elsif value.instance_of?(Symbol)
109
+ :filled
110
+ end
111
+ end
112
+
113
+ # define a arguments for required(key) in Dry::Schema.Params
114
+ def dry_schema_args(method, value)
115
+ if method == :filled
116
+ value
117
+ elsif method == :array
118
+ value[:array].instance_of?(Hash) ? create_schema(value[:array]) : value[:array]
119
+ elsif method == :hash
120
+ create_schema(value)
121
+ end
122
+ end
123
+
124
+ def create_schema(mapping)
125
+ schema = self
126
+ Dry::Schema.Params do
127
+ mapping.each_pair do |key, value|
128
+ method = schema.send('dry_schema_method', value)
129
+
130
+ args = schema.send('dry_schema_args', method, value)
131
+
132
+ required(key).send(method, args)
133
+ end
134
+ end
135
+ end
136
+ end
137
+ end
138
+ end
@@ -0,0 +1,39 @@
1
+ module SearchEnjoy
2
+ class SearchIndex < Hash
3
+ def initialize(*several_variants, index_schema: nil)
4
+ super
5
+ @index_schema = index_schema
6
+ end
7
+
8
+ class SearchIndexException < RuntimeError; end
9
+
10
+ def to_json
11
+ hash = {}
12
+
13
+ each_pair do |id, object|
14
+ object_hash = {}
15
+
16
+ @index_schema.key_map.each do |key|
17
+ object_hash[key.name.to_sym] = object[key.name.to_sym]
18
+ end
19
+
20
+ hash[id] = object_hash
21
+ end
22
+
23
+ JSON.generate(hash)
24
+ rescue StandardError => e
25
+ e.message
26
+ end
27
+
28
+ def load_json(json)
29
+ hash = JSON.parse(json)
30
+ hash.each_pair do |id, object|
31
+ indexed_object = @index_schema.call(object)
32
+ self[id] = indexed_object
33
+ end
34
+ rescue StandardError => e
35
+ e.message
36
+ end
37
+
38
+ end
39
+ end
@@ -0,0 +1,127 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative './query'
4
+
5
+ module SearchEnjoy
6
+ module Searching
7
+ def self.included(base)
8
+ base.class_eval do
9
+ # include InstanceMethods
10
+ extend ClassMethods
11
+ end
12
+ end
13
+
14
+ class Comparator
15
+ def initialize(conditions)
16
+ if conditions.instance_of? Query
17
+ conditions = conditions.to_hash
18
+ end
19
+
20
+ @conditions = conditions
21
+ end
22
+
23
+ def compare(subject, conditions = @conditions)
24
+ return false unless check_must_conditions(subject, conditions)
25
+ return false unless check_should_conditions(subject, conditions)
26
+
27
+ true
28
+ end
29
+
30
+ def check_must_conditions(subject, conditions)
31
+ conditions.each_pair do |attr, condition|
32
+ next unless condition[:must]
33
+
34
+ result = check_condition(subject[attr], condition[:value])
35
+
36
+ result = !result if condition[:inverse]
37
+
38
+ return false unless result
39
+ end
40
+
41
+ true
42
+ end
43
+
44
+ def check_should_conditions(subject, conditions)
45
+ conditions.each_pair do |attr, condition|
46
+ next if condition[:must]
47
+
48
+ result = check_condition(subject[attr], condition[:value])
49
+
50
+ result = !result if condition[:inverse]
51
+
52
+ return true if result
53
+ end
54
+
55
+ false
56
+ end
57
+
58
+ def check_condition(condition_subject, condition_body)
59
+ if condition_body.instance_of? Hash
60
+ compare(condition_subject, condition_body)
61
+ elsif condition_body.instance_of? Array
62
+ condition_body.include? condition_subject
63
+ else
64
+ condition_subject == condition_body
65
+ end
66
+ end
67
+ end
68
+
69
+ class SearchException < RuntimeError; end
70
+
71
+ module ClassMethods
72
+ def search(*args)
73
+ conditions = if args.first.instance_of? Query
74
+ args.first.query_hash
75
+ else
76
+ Query.new({**args.first}).query_hash
77
+ end
78
+
79
+ previous_result = args.last if args.last.instance_of? QueryResult
80
+
81
+ comparator = Comparator.new(conditions)
82
+
83
+ source = previous_result.nil? ? search_index.values : previous_result
84
+
85
+ result = source.select { |object| comparator.compare(object) }
86
+
87
+ QueryResult.new(self, result)
88
+ end
89
+
90
+ def search_not(*args)
91
+ query = if args.first.instance_of? Query
92
+ args.first.inverse!
93
+ else
94
+ Query.new(args.first, must: true, inverse: true)
95
+ end
96
+
97
+ result = search(query, args[1..])
98
+
99
+ QueryResult.new(self, result)
100
+ end
101
+
102
+ def search_must(*args)
103
+ raise SearchException, 'Forbidden use Query in search_must' if args.first.instance_of? Query
104
+
105
+ query = Query.new(args.first, must: true)
106
+
107
+ result = search(query, args[1..])
108
+
109
+ QueryResult.new(self, result)
110
+ rescue StandardError => e
111
+ e.message
112
+ end
113
+
114
+ def search_must_not(*args)
115
+ raise SearchException, 'Forbidden use Query in search_must_not' if args.first.instance_of? Query
116
+
117
+ query = Query.new(args.first, inverse: true)
118
+
119
+ result = search(query, args[1..])
120
+
121
+ QueryResult.new(self, result)
122
+ rescue StandardError => e
123
+ e.message
124
+ end
125
+ end
126
+ end
127
+ end
@@ -0,0 +1,3 @@
1
+ module SearchEnjoy
2
+ VERSION = '0.1.1'
3
+ end
data/lib/temp.rb ADDED
@@ -0,0 +1,64 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'search_enjoy'
4
+
5
+ class DummyIndexingModel
6
+ include SearchEnjoy
7
+
8
+ attr_accessor :id, :attr1, :attr2, :attr3,
9
+ :attr4, :attr5, :attr6, :attr7, :attr8
10
+
11
+ define_json_schema do
12
+ attr1 :integer
13
+ attr4 :float
14
+ attr5 :integer
15
+ attr6 :array do
16
+ array_type :integer
17
+ end
18
+
19
+ attr7 :string
20
+
21
+ attr8 :hash do
22
+ key_1 :integer
23
+ key_2 :float
24
+ end
25
+
26
+ attr2 :array do
27
+ array_type :string
28
+ end
29
+
30
+ attr3 :hash do
31
+ hash_key :key_1, :string
32
+ hash_key :key_2, :array do
33
+ array_type :string
34
+ end
35
+ end
36
+ end
37
+
38
+ def initialize(id, attr1, attr2, attr3)
39
+ @id = id
40
+ @attr1 = attr1
41
+ @attr2 = attr2
42
+ @attr3 = attr3
43
+ @attr4 = (attr1 + 2).to_f / 3
44
+ @attr5 = attr1 * 4
45
+ @attr6 = [attr1, @attr5, @attr5 - attr1]
46
+ @attr7 = "test_#{@attr5}"
47
+ @attr8 = { key_1: attr1 - 4, key_2: @attr5 * 0.22 }
48
+ end
49
+ end
50
+
51
+ DummyIndexingModel.create_index!
52
+
53
+ test_collection = []
54
+
55
+ 1.upto 1000 do |i|
56
+ arr = [i.to_s, (i + 1).to_s, (i * 2).to_s]
57
+ hash = { key_1: (i % 3).to_s, key_2: arr }
58
+ test_collection << DummyIndexingModel.new(i, i % 2, arr, hash)
59
+ end
60
+
61
+ test_collection.each(&:index_object)
62
+ def test_search
63
+ DummyIndexingModel.search
64
+ end
metadata ADDED
@@ -0,0 +1,53 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: search_enjoy
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.1
5
+ platform: ruby
6
+ authors:
7
+ - Shmorgun Egor
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2020-12-02 00:00:00.000000000 Z
12
+ dependencies: []
13
+ description: Search with Enjoy
14
+ email: egor@shmorgun.ru
15
+ executables: []
16
+ extensions: []
17
+ extra_rdoc_files: []
18
+ files:
19
+ - lib/search_enjoy.rb
20
+ - lib/search_enjoy/aggregation.rb
21
+ - lib/search_enjoy/configuration.rb
22
+ - lib/search_enjoy/dumping.rb
23
+ - lib/search_enjoy/indexing.rb
24
+ - lib/search_enjoy/query.rb
25
+ - lib/search_enjoy/schema.rb
26
+ - lib/search_enjoy/search_index.rb
27
+ - lib/search_enjoy/searching.rb
28
+ - lib/search_enjoy/version.rb
29
+ - lib/temp.rb
30
+ homepage: https://github.com/LarsWl/SearchEnjoy
31
+ licenses:
32
+ - MIT
33
+ metadata: {}
34
+ post_install_message:
35
+ rdoc_options: []
36
+ require_paths:
37
+ - lib
38
+ required_ruby_version: !ruby/object:Gem::Requirement
39
+ requirements:
40
+ - - ">="
41
+ - !ruby/object:Gem::Version
42
+ version: '0'
43
+ required_rubygems_version: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - ">="
46
+ - !ruby/object:Gem::Version
47
+ version: '0'
48
+ requirements: []
49
+ rubygems_version: 3.1.4
50
+ signing_key:
51
+ specification_version: 4
52
+ summary: Search with Enjoy!
53
+ test_files: []