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 +7 -0
- data/lib/search_enjoy.rb +24 -0
- data/lib/search_enjoy/aggregation.rb +44 -0
- data/lib/search_enjoy/configuration.rb +26 -0
- data/lib/search_enjoy/dumping.rb +51 -0
- data/lib/search_enjoy/indexing.rb +81 -0
- data/lib/search_enjoy/query.rb +148 -0
- data/lib/search_enjoy/schema.rb +138 -0
- data/lib/search_enjoy/search_index.rb +39 -0
- data/lib/search_enjoy/searching.rb +127 -0
- data/lib/search_enjoy/version.rb +3 -0
- data/lib/temp.rb +64 -0
- metadata +53 -0
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
|
data/lib/search_enjoy.rb
ADDED
|
@@ -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
|
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: []
|