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