gummi 0.1.2 → 0.2.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.
- data/.gitignore +1 -0
- data/gummi.gemspec +7 -6
- data/lib/gummi.rb +32 -26
- data/lib/gummi/api.rb +3 -1
- data/lib/gummi/db_layer/default_index.rb +15 -0
- data/lib/gummi/db_layer/document.rb +206 -0
- data/lib/gummi/db_layer/document/attributes.rb +40 -0
- data/lib/gummi/db_layer/document/object.rb +15 -0
- data/lib/gummi/db_layer/document/search/filtered.rb +42 -0
- data/lib/gummi/db_layer/document/search/raw.rb +12 -0
- data/lib/gummi/db_layer/document/search/result.rb +34 -0
- data/lib/gummi/db_layer/document/search/searching.rb +51 -0
- data/lib/gummi/db_layer/fields/boolean.rb +13 -0
- data/lib/gummi/db_layer/fields/integer.rb +16 -0
- data/lib/gummi/db_layer/fields/keyword.rb +15 -0
- data/lib/gummi/db_layer/fields/ngram_and_plain.rb +20 -0
- data/lib/gummi/db_layer/fields/path_hierarchy.rb +15 -0
- data/lib/gummi/db_layer/fields/positive_integer.rb +21 -0
- data/lib/gummi/db_layer/fields/sanitized_string.rb +30 -0
- data/lib/gummi/db_layer/fields/string.rb +17 -0
- data/lib/gummi/db_layer/fields/time.rb +17 -0
- data/lib/gummi/db_layer/index.rb +150 -0
- data/lib/gummi/entity_layer/entity.rb +22 -0
- data/lib/gummi/errors.rb +7 -0
- data/lib/gummi/repository_layer/repository.rb +39 -0
- data/lib/gummi/repository_layer/repository/result.rb +42 -0
- data/lib/gummi/version.rb +1 -1
- data/lib/repobahn/repository.rb +25 -33
- data/lib/repobahn/repository/active_record.rb +17 -0
- data/spec/fixtures/admin/auto.rb +6 -0
- data/spec/fixtures/admin/cars.rb +12 -0
- data/spec/fixtures/admin/countries.rb +9 -0
- data/spec/fixtures/admin/country.rb +6 -0
- data/spec/fixtures/admin/db/country.rb +7 -0
- data/spec/fixtures/admin/db/vehicle.rb +7 -0
- data/spec/fixtures/cities.rb +7 -0
- data/spec/fixtures/city.rb +6 -0
- data/spec/fixtures/db/animal.rb +9 -0
- data/spec/fixtures/db/boat.rb +9 -0
- data/spec/fixtures/db/car.rb +9 -0
- data/spec/fixtures/db/city.rb +8 -0
- data/spec/fixtures/db/enemy.rb +10 -0
- data/spec/fixtures/db/game.rb +7 -0
- data/spec/fixtures/db/person.rb +15 -0
- data/spec/fixtures/db/rating.rb +11 -0
- data/spec/fixtures/db/ship.rb +18 -0
- data/spec/{models → fixtures}/people.rb +6 -2
- data/spec/{models → fixtures}/person.rb +3 -2
- data/spec/lib/gummi/db_layer/document_spec.rb +124 -0
- data/spec/lib/gummi/{entity_spec.rb → entity_layer/entity_spec.rb} +3 -1
- data/spec/lib/gummi/repository_layer/repository_spec.rb +63 -0
- data/spec/lib/repobahn/repository_spec.rb +72 -0
- data/spec/spec_helper.rb +37 -9
- metadata +87 -37
- data/lib/gummi/default_index.rb +0 -13
- data/lib/gummi/document.rb +0 -139
- data/lib/gummi/document/attributes.rb +0 -28
- data/lib/gummi/document/object.rb +0 -12
- data/lib/gummi/document/search/filtered.rb +0 -39
- data/lib/gummi/document/search/raw.rb +0 -9
- data/lib/gummi/document/search/result.rb +0 -25
- data/lib/gummi/document/search/searching.rb +0 -45
- data/lib/gummi/entity.rb +0 -20
- data/lib/gummi/fields/boolean.rb +0 -10
- data/lib/gummi/fields/integer.rb +0 -14
- data/lib/gummi/fields/keyword.rb +0 -13
- data/lib/gummi/fields/ngram_and_plain.rb +0 -18
- data/lib/gummi/fields/path_hierarchy.rb +0 -13
- data/lib/gummi/fields/positive_integer.rb +0 -19
- data/lib/gummi/fields/sanitized_string.rb +0 -28
- data/lib/gummi/fields/string.rb +0 -15
- data/lib/gummi/fields/time.rb +0 -15
- data/lib/gummi/index.rb +0 -146
- data/lib/gummi/repository.rb +0 -38
- data/lib/gummi/repository/result.rb +0 -30
- data/spec/lib/gummi/document_spec.rb +0 -73
- data/spec/lib/gummi/repository/result_spec.rb +0 -18
- data/spec/lib/gummi/repository_spec.rb +0 -81
- data/spec/models/db/person.rb +0 -15
@@ -0,0 +1,34 @@
|
|
1
|
+
module Gummi
|
2
|
+
module DbLayer
|
3
|
+
module Document
|
4
|
+
module Search
|
5
|
+
class Result
|
6
|
+
|
7
|
+
attr_reader :took, :total, :hits
|
8
|
+
|
9
|
+
def initialize(response, converter, per_page, page)
|
10
|
+
@response = Hashie::Mash.new response
|
11
|
+
@took = @response.hits.took
|
12
|
+
@total = @response.hits.total
|
13
|
+
@hits = @response.hits.hits
|
14
|
+
@converter = converter
|
15
|
+
@per_page = per_page
|
16
|
+
@page = page
|
17
|
+
end
|
18
|
+
|
19
|
+
def documents
|
20
|
+
@documents ||= begin
|
21
|
+
documents = Array(converter.hits_to_documents(hits)) if hits
|
22
|
+
Leaflet::Collection.new documents, total: total, page: page, per_page: per_page
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
private
|
27
|
+
|
28
|
+
attr_reader :response, :converter, :per_page, :page, :hits
|
29
|
+
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
@@ -0,0 +1,51 @@
|
|
1
|
+
module Gummi
|
2
|
+
module DbLayer
|
3
|
+
module Document
|
4
|
+
module Search
|
5
|
+
module Searching
|
6
|
+
extend ActiveSupport::Concern
|
7
|
+
|
8
|
+
included do
|
9
|
+
include Virtus.model
|
10
|
+
|
11
|
+
attribute :document_class, Class
|
12
|
+
attribute :index, String, default: lambda { |search, attr| Gummi::DbLayer::DefaultIndex.name }
|
13
|
+
attribute :type, String
|
14
|
+
attribute :page, Gummi::DbLayer::Fields::PositiveInteger, default: 1
|
15
|
+
attribute :per_page, Gummi::DbLayer::Fields::PositiveInteger, default: 300
|
16
|
+
attribute :options, Hash, default: {}
|
17
|
+
end
|
18
|
+
|
19
|
+
def to_client_args
|
20
|
+
args = {
|
21
|
+
index: index,
|
22
|
+
from: from,
|
23
|
+
size: size,
|
24
|
+
}
|
25
|
+
args[:type] = type if type
|
26
|
+
args.merge options
|
27
|
+
end
|
28
|
+
|
29
|
+
def execute
|
30
|
+
Gummi::DbLayer::Document::Search::Result.new client.search(to_client_args), document_class, per_page, page
|
31
|
+
end
|
32
|
+
|
33
|
+
private
|
34
|
+
|
35
|
+
def size
|
36
|
+
per_page
|
37
|
+
end
|
38
|
+
|
39
|
+
def from
|
40
|
+
per_page * (page - 1)
|
41
|
+
end
|
42
|
+
|
43
|
+
def client
|
44
|
+
Gummi::API.client
|
45
|
+
end
|
46
|
+
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
@@ -0,0 +1,15 @@
|
|
1
|
+
module Gummi
|
2
|
+
module DbLayer
|
3
|
+
module Fields
|
4
|
+
class Keyword < Virtus::Attribute
|
5
|
+
def coerce(value)
|
6
|
+
value
|
7
|
+
end
|
8
|
+
|
9
|
+
def mapping
|
10
|
+
{ type: 'string', index_analyzer: 'keyword_index_analyzer', search_analyzer: 'keyword_search_analyzer' }
|
11
|
+
end
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
@@ -0,0 +1,20 @@
|
|
1
|
+
module Gummi
|
2
|
+
module DbLayer
|
3
|
+
module Fields
|
4
|
+
class NgramAndPlain < Virtus::Attribute
|
5
|
+
def coerce(value)
|
6
|
+
value
|
7
|
+
end
|
8
|
+
|
9
|
+
def mapping
|
10
|
+
{ type: 'multi_field',
|
11
|
+
fields: {
|
12
|
+
name => { type: 'string', index_analyzer: 'text_index_analyzer', search_analyzer: 'text_search_analyzer' },
|
13
|
+
:plain => { type: 'string', index_analyzer: 'string_index_analyzer', search_analyzer: 'text_search_analyzer' },
|
14
|
+
}
|
15
|
+
}
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
@@ -0,0 +1,21 @@
|
|
1
|
+
module Gummi
|
2
|
+
module DbLayer
|
3
|
+
module Fields
|
4
|
+
class PositiveInteger < Virtus::Attribute
|
5
|
+
|
6
|
+
def coerce(value)
|
7
|
+
coerced = value.to_i
|
8
|
+
if coerced > 0
|
9
|
+
coerced
|
10
|
+
else
|
11
|
+
default_value.value
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
15
|
+
def mapping
|
16
|
+
{ type: 'integer' }
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
@@ -0,0 +1,30 @@
|
|
1
|
+
module Gummi
|
2
|
+
module DbLayer
|
3
|
+
module Fields
|
4
|
+
class SanitizedString < Virtus::Attribute
|
5
|
+
|
6
|
+
def coerce(value)
|
7
|
+
return nil if value.blank?
|
8
|
+
sanitize_string_for_query(value.to_s)
|
9
|
+
end
|
10
|
+
|
11
|
+
def mapping
|
12
|
+
{ type: 'string' }
|
13
|
+
end
|
14
|
+
|
15
|
+
def sanitize_string_for_query(str)
|
16
|
+
# Escape special characters
|
17
|
+
escaped_characters = Regexp.escape('\/\\+-&|!(){}[]^~*?:')
|
18
|
+
str = str.gsub(/([#{escaped_characters}])/) do |match|
|
19
|
+
'\\'+match
|
20
|
+
end
|
21
|
+
|
22
|
+
# Escape odd quotes
|
23
|
+
quote_count = str.count '"'
|
24
|
+
str = str.gsub(/(.*)"(.*)/, '\1\"\3') if quote_count % 2 == 1
|
25
|
+
str
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
@@ -0,0 +1,17 @@
|
|
1
|
+
module Gummi
|
2
|
+
module DbLayer
|
3
|
+
module Fields
|
4
|
+
class Time < Virtus::Attribute
|
5
|
+
|
6
|
+
def coerce(value)
|
7
|
+
return nil unless value.respond_to? :in_time_zone
|
8
|
+
value.in_time_zone 'UTC'
|
9
|
+
end
|
10
|
+
|
11
|
+
def mapping
|
12
|
+
{type: "date"}
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
@@ -0,0 +1,150 @@
|
|
1
|
+
module Gummi
|
2
|
+
module DbLayer
|
3
|
+
module Index
|
4
|
+
extend ActiveSupport::Concern
|
5
|
+
|
6
|
+
module ClassMethods
|
7
|
+
|
8
|
+
# Return true if created or false if already created.
|
9
|
+
#
|
10
|
+
def setup
|
11
|
+
created_settings = client.indices.create index: name, body: { settings: settings }
|
12
|
+
created_settings.present?
|
13
|
+
refresh
|
14
|
+
rescue ::Elasticsearch::Transport::Transport::Errors::BadRequest => exception
|
15
|
+
false
|
16
|
+
end
|
17
|
+
|
18
|
+
# Return true if successful or already teared down.
|
19
|
+
#
|
20
|
+
# Raises NotImplementedError in production.
|
21
|
+
#
|
22
|
+
def teardown
|
23
|
+
raise NotImplementedError if Gummi.env == 'production'
|
24
|
+
response = client.indices.delete index: name
|
25
|
+
response.present?
|
26
|
+
rescue ::Elasticsearch::Transport::Transport::Errors::NotFound
|
27
|
+
true
|
28
|
+
end
|
29
|
+
|
30
|
+
def name
|
31
|
+
raise "Implement me"
|
32
|
+
end
|
33
|
+
|
34
|
+
def refresh
|
35
|
+
client.indices.refresh
|
36
|
+
client.cluster.health wait_for_status: :yellow
|
37
|
+
end
|
38
|
+
|
39
|
+
def settings
|
40
|
+
default_settings
|
41
|
+
end
|
42
|
+
|
43
|
+
def default_settings
|
44
|
+
{
|
45
|
+
index: {
|
46
|
+
# Main Settings
|
47
|
+
number_of_shards: '3',
|
48
|
+
number_of_replicas: (Gummi.env == 'production' ? '2' : '0'),
|
49
|
+
refresh_interval: '1s',
|
50
|
+
store: { type: (Gummi.env == 'test' ? :memory : :niofs) },
|
51
|
+
mapper: { dynamic: false },
|
52
|
+
|
53
|
+
analysis: {
|
54
|
+
|
55
|
+
# Tokenizers are just some sort of "tool" or "module" that can be applied to analyzers.
|
56
|
+
tokenizer: {
|
57
|
+
# This one is a little bit more general and is able to chop any word into all of its components.
|
58
|
+
ngram_tokenizer: {
|
59
|
+
type: 'nGram',
|
60
|
+
min_gram: 1,
|
61
|
+
max_gram: 7,
|
62
|
+
token_chars: [ 'letter', 'digit' ],
|
63
|
+
}
|
64
|
+
|
65
|
+
},
|
66
|
+
|
67
|
+
# Now we are ready to use our tokenizers.
|
68
|
+
# Let's create the most important thing: Analyzers.
|
69
|
+
analyzer: {
|
70
|
+
|
71
|
+
path_hierarchy_analyzer: {
|
72
|
+
type: 'custom',
|
73
|
+
tokenizer: 'path_hierarchy',
|
74
|
+
},
|
75
|
+
# When adding long text to Elastic, we most likely are going to use this
|
76
|
+
# analyzer. This is commonly used for titles and descriptions.
|
77
|
+
text_index_analyzer: {
|
78
|
+
type: 'custom',
|
79
|
+
tokenizer: 'ngram_tokenizer', # Chopping every word up into tokens
|
80
|
+
filter: {
|
81
|
+
0 => 'standard', # Some default transformations
|
82
|
+
1 => 'lowercase', # Make everything lowercase
|
83
|
+
2 => 'word_delimiter', # E.g. "O'Neil" -> "O Neil", "Victoria's" -> "Victoria"
|
84
|
+
2 => 'asciifolding', # Transform everything into ASCII
|
85
|
+
},
|
86
|
+
},
|
87
|
+
|
88
|
+
# For smaller texts, such as the city "stockholm", we don't want any
|
89
|
+
# tokenizing. It's enough to explicitly save the word as it is.
|
90
|
+
# As a matter of fact, if we would tokenize the city, then the facets
|
91
|
+
# would report that we have Transports in "st" "sto" "stoc" etc.
|
92
|
+
string_index_analyzer: {
|
93
|
+
type: 'custom',
|
94
|
+
tokenizer: 'standard',
|
95
|
+
filter: {
|
96
|
+
# The filters, however, are identical to the other analyzer.
|
97
|
+
0 => 'standard',
|
98
|
+
1 => 'lowercase',
|
99
|
+
2 => 'word_delimiter',
|
100
|
+
3 => 'asciifolding',
|
101
|
+
},
|
102
|
+
},
|
103
|
+
|
104
|
+
# For finding Slugs
|
105
|
+
keyword_index_analyzer: {
|
106
|
+
type: 'custom',
|
107
|
+
tokenizer: 'keyword',
|
108
|
+
filter: {
|
109
|
+
0 => 'lowercase',
|
110
|
+
1 => 'asciifolding',
|
111
|
+
},
|
112
|
+
},
|
113
|
+
|
114
|
+
# This is an analyzer that we apply to the search query itself.
|
115
|
+
text_search_analyzer: {
|
116
|
+
type: 'custom',
|
117
|
+
tokenizer: 'standard',
|
118
|
+
filter: {
|
119
|
+
0 => 'standard',
|
120
|
+
1 => 'lowercase',
|
121
|
+
2 => 'word_delimiter',
|
122
|
+
3 => 'asciifolding',
|
123
|
+
},
|
124
|
+
},
|
125
|
+
|
126
|
+
# This is an analyzer that we apply to the search query itself.
|
127
|
+
keyword_search_analyzer: {
|
128
|
+
type: 'custom',
|
129
|
+
tokenizer: 'keyword',
|
130
|
+
filter: {
|
131
|
+
0 => 'lowercase',
|
132
|
+
1 => 'asciifolding',
|
133
|
+
},
|
134
|
+
},
|
135
|
+
|
136
|
+
}
|
137
|
+
}
|
138
|
+
}
|
139
|
+
}
|
140
|
+
end
|
141
|
+
|
142
|
+
def client
|
143
|
+
Gummi::API.client
|
144
|
+
end
|
145
|
+
|
146
|
+
end
|
147
|
+
|
148
|
+
end
|
149
|
+
end
|
150
|
+
end
|
@@ -0,0 +1,22 @@
|
|
1
|
+
module Gummi
|
2
|
+
module EntityLayer
|
3
|
+
module Entity
|
4
|
+
extend ActiveSupport::Concern
|
5
|
+
|
6
|
+
included do
|
7
|
+
include Repobahn::Entity
|
8
|
+
end
|
9
|
+
|
10
|
+
attr_accessor :id
|
11
|
+
attr_accessor :version
|
12
|
+
|
13
|
+
def ==(other)
|
14
|
+
other &&
|
15
|
+
self.id == other.id &&
|
16
|
+
self.version == other.version &&
|
17
|
+
self.attributes == other.attributes
|
18
|
+
end
|
19
|
+
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|