gummi 0.1.2 → 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (79) hide show
  1. data/.gitignore +1 -0
  2. data/gummi.gemspec +7 -6
  3. data/lib/gummi.rb +32 -26
  4. data/lib/gummi/api.rb +3 -1
  5. data/lib/gummi/db_layer/default_index.rb +15 -0
  6. data/lib/gummi/db_layer/document.rb +206 -0
  7. data/lib/gummi/db_layer/document/attributes.rb +40 -0
  8. data/lib/gummi/db_layer/document/object.rb +15 -0
  9. data/lib/gummi/db_layer/document/search/filtered.rb +42 -0
  10. data/lib/gummi/db_layer/document/search/raw.rb +12 -0
  11. data/lib/gummi/db_layer/document/search/result.rb +34 -0
  12. data/lib/gummi/db_layer/document/search/searching.rb +51 -0
  13. data/lib/gummi/db_layer/fields/boolean.rb +13 -0
  14. data/lib/gummi/db_layer/fields/integer.rb +16 -0
  15. data/lib/gummi/db_layer/fields/keyword.rb +15 -0
  16. data/lib/gummi/db_layer/fields/ngram_and_plain.rb +20 -0
  17. data/lib/gummi/db_layer/fields/path_hierarchy.rb +15 -0
  18. data/lib/gummi/db_layer/fields/positive_integer.rb +21 -0
  19. data/lib/gummi/db_layer/fields/sanitized_string.rb +30 -0
  20. data/lib/gummi/db_layer/fields/string.rb +17 -0
  21. data/lib/gummi/db_layer/fields/time.rb +17 -0
  22. data/lib/gummi/db_layer/index.rb +150 -0
  23. data/lib/gummi/entity_layer/entity.rb +22 -0
  24. data/lib/gummi/errors.rb +7 -0
  25. data/lib/gummi/repository_layer/repository.rb +39 -0
  26. data/lib/gummi/repository_layer/repository/result.rb +42 -0
  27. data/lib/gummi/version.rb +1 -1
  28. data/lib/repobahn/repository.rb +25 -33
  29. data/lib/repobahn/repository/active_record.rb +17 -0
  30. data/spec/fixtures/admin/auto.rb +6 -0
  31. data/spec/fixtures/admin/cars.rb +12 -0
  32. data/spec/fixtures/admin/countries.rb +9 -0
  33. data/spec/fixtures/admin/country.rb +6 -0
  34. data/spec/fixtures/admin/db/country.rb +7 -0
  35. data/spec/fixtures/admin/db/vehicle.rb +7 -0
  36. data/spec/fixtures/cities.rb +7 -0
  37. data/spec/fixtures/city.rb +6 -0
  38. data/spec/fixtures/db/animal.rb +9 -0
  39. data/spec/fixtures/db/boat.rb +9 -0
  40. data/spec/fixtures/db/car.rb +9 -0
  41. data/spec/fixtures/db/city.rb +8 -0
  42. data/spec/fixtures/db/enemy.rb +10 -0
  43. data/spec/fixtures/db/game.rb +7 -0
  44. data/spec/fixtures/db/person.rb +15 -0
  45. data/spec/fixtures/db/rating.rb +11 -0
  46. data/spec/fixtures/db/ship.rb +18 -0
  47. data/spec/{models → fixtures}/people.rb +6 -2
  48. data/spec/{models → fixtures}/person.rb +3 -2
  49. data/spec/lib/gummi/db_layer/document_spec.rb +124 -0
  50. data/spec/lib/gummi/{entity_spec.rb → entity_layer/entity_spec.rb} +3 -1
  51. data/spec/lib/gummi/repository_layer/repository_spec.rb +63 -0
  52. data/spec/lib/repobahn/repository_spec.rb +72 -0
  53. data/spec/spec_helper.rb +37 -9
  54. metadata +87 -37
  55. data/lib/gummi/default_index.rb +0 -13
  56. data/lib/gummi/document.rb +0 -139
  57. data/lib/gummi/document/attributes.rb +0 -28
  58. data/lib/gummi/document/object.rb +0 -12
  59. data/lib/gummi/document/search/filtered.rb +0 -39
  60. data/lib/gummi/document/search/raw.rb +0 -9
  61. data/lib/gummi/document/search/result.rb +0 -25
  62. data/lib/gummi/document/search/searching.rb +0 -45
  63. data/lib/gummi/entity.rb +0 -20
  64. data/lib/gummi/fields/boolean.rb +0 -10
  65. data/lib/gummi/fields/integer.rb +0 -14
  66. data/lib/gummi/fields/keyword.rb +0 -13
  67. data/lib/gummi/fields/ngram_and_plain.rb +0 -18
  68. data/lib/gummi/fields/path_hierarchy.rb +0 -13
  69. data/lib/gummi/fields/positive_integer.rb +0 -19
  70. data/lib/gummi/fields/sanitized_string.rb +0 -28
  71. data/lib/gummi/fields/string.rb +0 -15
  72. data/lib/gummi/fields/time.rb +0 -15
  73. data/lib/gummi/index.rb +0 -146
  74. data/lib/gummi/repository.rb +0 -38
  75. data/lib/gummi/repository/result.rb +0 -30
  76. data/spec/lib/gummi/document_spec.rb +0 -73
  77. data/spec/lib/gummi/repository/result_spec.rb +0 -18
  78. data/spec/lib/gummi/repository_spec.rb +0 -81
  79. 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,13 @@
1
+ module Gummi
2
+ module DbLayer
3
+ module Fields
4
+ class Boolean < Virtus::Attribute::Boolean
5
+
6
+ def mapping
7
+ { type: 'boolean' }
8
+ end
9
+
10
+ end
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,16 @@
1
+ module Gummi
2
+ module DbLayer
3
+ module Fields
4
+ class Integer < Virtus::Attribute
5
+
6
+ def coerce(value)
7
+ value.to_i if value.present?
8
+ end
9
+
10
+ def mapping
11
+ { type: 'integer' }
12
+ end
13
+ end
14
+ end
15
+ end
16
+ 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,15 @@
1
+ module Gummi
2
+ module DbLayer
3
+ module Fields
4
+ class PathHierarchy < Virtus::Attribute
5
+ def coerce(value)
6
+ value
7
+ end
8
+
9
+ def mapping
10
+ {type: 'string', index_analyzer: 'path_hierarchy_analyzer' }
11
+ end
12
+ end
13
+ end
14
+ end
15
+ 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 String < Virtus::Attribute
5
+
6
+
7
+ def coerce(value)
8
+ value
9
+ end
10
+
11
+ def mapping
12
+ { type: 'string' }
13
+ end
14
+ end
15
+ end
16
+ end
17
+ 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
@@ -0,0 +1,7 @@
1
+ module Gummi
2
+ module Errors
3
+
4
+ ImplicitMappingForbidden = Class.new(ArgumentError)
5
+
6
+ end
7
+ end