gummi 0.0.6
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 +18 -0
- data/Gemfile +6 -0
- data/LICENSE.txt +22 -0
- data/README.md +31 -0
- data/Rakefile +1 -0
- data/gummi.gemspec +30 -0
- data/lib/gummi.rb +42 -0
- data/lib/gummi/api.rb +7 -0
- data/lib/gummi/attributes.rb +26 -0
- data/lib/gummi/default_index.rb +13 -0
- data/lib/gummi/document.rb +118 -0
- data/lib/gummi/entity.rb +12 -0
- data/lib/gummi/fields/boolean.rb +10 -0
- data/lib/gummi/fields/integer.rb +14 -0
- data/lib/gummi/fields/keyword.rb +13 -0
- data/lib/gummi/fields/ngram_and_plain.rb +18 -0
- data/lib/gummi/fields/path_hierarchy.rb +13 -0
- data/lib/gummi/fields/positive_integer.rb +19 -0
- data/lib/gummi/fields/sanitized_string.rb +28 -0
- data/lib/gummi/fields/string.rb +15 -0
- data/lib/gummi/fields/time.rb +15 -0
- data/lib/gummi/index.rb +146 -0
- data/lib/gummi/object.rb +10 -0
- data/lib/gummi/repository.rb +31 -0
- data/lib/gummi/search/filtered.rb +36 -0
- data/lib/gummi/search/raw.rb +7 -0
- data/lib/gummi/search/result.rb +23 -0
- data/lib/gummi/search/searching.rb +43 -0
- data/lib/gummi/version.rb +3 -0
- data/lib/repobahn/entity.rb +12 -0
- data/lib/repobahn/repository.rb +57 -0
- data/spec/lib/gummi/document_spec.rb +74 -0
- data/spec/lib/gummi/repository_spec.rb +57 -0
- data/spec/models/db/person.rb +15 -0
- data/spec/models/people.rb +9 -0
- data/spec/models/person.rb +7 -0
- data/spec/spec_helper.rb +19 -0
- metadata +232 -0
data/.gitignore
ADDED
data/Gemfile
ADDED
data/LICENSE.txt
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
Copyright (c) 2013 Jens Norrgrann
|
2
|
+
|
3
|
+
MIT License
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
6
|
+
a copy of this software and associated documentation files (the
|
7
|
+
"Software"), to deal in the Software without restriction, including
|
8
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
9
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
10
|
+
permit persons to whom the Software is furnished to do so, subject to
|
11
|
+
the following conditions:
|
12
|
+
|
13
|
+
The above copyright notice and this permission notice shall be
|
14
|
+
included in all copies or substantial portions of the Software.
|
15
|
+
|
16
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
17
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
18
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
19
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
20
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
21
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
22
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,31 @@
|
|
1
|
+
[](https://codeclimate.com/github/bukowskis/gummi)
|
2
|
+
# Gummi
|
3
|
+
|
4
|
+
A minimal wrapper around elasticsearch-ruby using a repository pattern.
|
5
|
+
Still very much alpha. Use at your own risk...
|
6
|
+
|
7
|
+
## Installation
|
8
|
+
|
9
|
+
Add this line to your application's Gemfile:
|
10
|
+
|
11
|
+
gem 'gummi'
|
12
|
+
|
13
|
+
And then execute:
|
14
|
+
|
15
|
+
$ bundle
|
16
|
+
|
17
|
+
Or install it yourself as:
|
18
|
+
|
19
|
+
$ gem install gummi
|
20
|
+
|
21
|
+
## Usage
|
22
|
+
|
23
|
+
TODO: Write usage instructions here
|
24
|
+
|
25
|
+
## Contributing
|
26
|
+
|
27
|
+
1. Fork it
|
28
|
+
2. Create your feature branch (`git checkout -b my-new-feature`)
|
29
|
+
3. Commit your changes (`git commit -am 'Add some feature'`)
|
30
|
+
4. Push to the branch (`git push origin my-new-feature`)
|
31
|
+
5. Create new Pull Request
|
data/Rakefile
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
require "bundler/gem_tasks"
|
data/gummi.gemspec
ADDED
@@ -0,0 +1,30 @@
|
|
1
|
+
# coding: utf-8
|
2
|
+
lib = File.expand_path('../lib', __FILE__)
|
3
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
4
|
+
require 'gummi/version'
|
5
|
+
|
6
|
+
Gem::Specification.new do |spec|
|
7
|
+
spec.name = "gummi"
|
8
|
+
spec.version = Gummi::VERSION
|
9
|
+
spec.authors = ["bukowskis"]
|
10
|
+
spec.description = %q{A small wrapper around Elasticsearch}
|
11
|
+
spec.summary = %q{A small wrapper around Elasticsearch}
|
12
|
+
spec.homepage = ""
|
13
|
+
spec.license = "MIT"
|
14
|
+
|
15
|
+
spec.files = `git ls-files`.split($/) - ['.travis.yml']
|
16
|
+
spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
|
17
|
+
spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
|
18
|
+
spec.require_paths = ["lib"]
|
19
|
+
|
20
|
+
spec.add_dependency('virtus', '~> 1.0.0')
|
21
|
+
spec.add_dependency('elasticsearch', '~> 0.4.0')
|
22
|
+
spec.add_dependency('activesupport', '>= 3.0')
|
23
|
+
spec.add_dependency('activemodel', '>= 3.0')
|
24
|
+
spec.add_dependency('hooks', '~>0.3.3')
|
25
|
+
spec.add_dependency('leaflet')
|
26
|
+
|
27
|
+
spec.add_development_dependency('bundler', '~> 1.3')
|
28
|
+
spec.add_development_dependency('rake')
|
29
|
+
spec.add_development_dependency('rspec')
|
30
|
+
end
|
data/lib/gummi.rb
ADDED
@@ -0,0 +1,42 @@
|
|
1
|
+
require 'virtus'
|
2
|
+
require 'elasticsearch'
|
3
|
+
require 'active_support/core_ext'
|
4
|
+
require 'active_model'
|
5
|
+
require 'hooks'
|
6
|
+
require 'leaflet'
|
7
|
+
|
8
|
+
require "repobahn/repository"
|
9
|
+
require "repobahn/entity"
|
10
|
+
|
11
|
+
require "gummi/version"
|
12
|
+
require "gummi/api"
|
13
|
+
require "gummi/attributes"
|
14
|
+
require "gummi/document"
|
15
|
+
require "gummi/entity"
|
16
|
+
require "gummi/index"
|
17
|
+
require "gummi/object"
|
18
|
+
require "gummi/repository"
|
19
|
+
require "gummi/fields/boolean"
|
20
|
+
require "gummi/fields/time"
|
21
|
+
require "gummi/fields/integer"
|
22
|
+
require "gummi/fields/positive_integer"
|
23
|
+
require "gummi/fields/keyword"
|
24
|
+
require "gummi/fields/ngram_and_plain"
|
25
|
+
require "gummi/fields/path_hierarchy"
|
26
|
+
require "gummi/fields/string"
|
27
|
+
require "gummi/fields/sanitized_string"
|
28
|
+
require "gummi/default_index"
|
29
|
+
require "gummi/search/searching"
|
30
|
+
require "gummi/search/filtered"
|
31
|
+
require "gummi/search/raw"
|
32
|
+
require "gummi/search/result"
|
33
|
+
|
34
|
+
module Gummi
|
35
|
+
def self.env
|
36
|
+
if defined? Rails
|
37
|
+
Rails.env
|
38
|
+
else
|
39
|
+
RAILS_ENV || "development"
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
data/lib/gummi/api.rb
ADDED
@@ -0,0 +1,26 @@
|
|
1
|
+
module Gummi
|
2
|
+
module Attributes
|
3
|
+
extend ActiveSupport::Concern
|
4
|
+
|
5
|
+
module ClassMethods
|
6
|
+
|
7
|
+
def mapping_for_attribute(attribute)
|
8
|
+
if attribute.is_a? Virtus::Attribute::EmbeddedValue
|
9
|
+
{properties: attribute.primitive.mapping}
|
10
|
+
elsif attribute.is_a? Virtus::Attribute::Collection
|
11
|
+
mapping_for_attribute(attribute.member_type)
|
12
|
+
else
|
13
|
+
attribute.mapping
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
def mapping
|
18
|
+
result = {}
|
19
|
+
attribute_set.each do |attribute|
|
20
|
+
result.merge!({ attribute.name => mapping_for_attribute(attribute)})
|
21
|
+
end
|
22
|
+
result
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
@@ -0,0 +1,118 @@
|
|
1
|
+
module Gummi
|
2
|
+
module Document
|
3
|
+
extend ActiveSupport::Concern
|
4
|
+
|
5
|
+
included do
|
6
|
+
include Virtus.model
|
7
|
+
include Gummi::Attributes
|
8
|
+
end
|
9
|
+
|
10
|
+
attr_accessor :id
|
11
|
+
attr_accessor :version
|
12
|
+
|
13
|
+
def overwrite
|
14
|
+
response = client.index index: index.name, type: document_type, id: id, body: attributes
|
15
|
+
if response["ok"]
|
16
|
+
self.version = response["_version"]
|
17
|
+
self.id = response["_id"]
|
18
|
+
true
|
19
|
+
else
|
20
|
+
false
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
def update
|
25
|
+
response = client.update index: index.name, type: document_type, id: id, retry_on_conflict: 0, version: version, body: { doc: attributes.as_json }
|
26
|
+
if response["ok"]
|
27
|
+
self.version = response["_version"]
|
28
|
+
true
|
29
|
+
else
|
30
|
+
false
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
private
|
35
|
+
|
36
|
+
def client
|
37
|
+
Gummi::API.client
|
38
|
+
end
|
39
|
+
|
40
|
+
def document_type
|
41
|
+
self.class.document_type
|
42
|
+
end
|
43
|
+
|
44
|
+
def index
|
45
|
+
self.class.index
|
46
|
+
end
|
47
|
+
|
48
|
+
module ClassMethods
|
49
|
+
|
50
|
+
def get!(id)
|
51
|
+
response = client.get index: index.name, type: document_type, id: id
|
52
|
+
doc_hash = {id: response["_id"], version: response["_version"]}.merge(response["_source"])
|
53
|
+
lot = self.new(doc_hash)
|
54
|
+
lot.version = response["_version"]
|
55
|
+
lot
|
56
|
+
end
|
57
|
+
|
58
|
+
def get(id)
|
59
|
+
get!(id)
|
60
|
+
rescue ::Elasticsearch::Transport::Transport::Errors::NotFound
|
61
|
+
nil
|
62
|
+
end
|
63
|
+
|
64
|
+
def document_type(type_name)
|
65
|
+
@document_type = type_name
|
66
|
+
end
|
67
|
+
|
68
|
+
def document_type
|
69
|
+
@document_type || name.split('::').last.underscore
|
70
|
+
end
|
71
|
+
|
72
|
+
def index
|
73
|
+
@index || Gummi::DefaultIndex
|
74
|
+
end
|
75
|
+
|
76
|
+
def index=(index)
|
77
|
+
@index = index
|
78
|
+
end
|
79
|
+
|
80
|
+
def parent_document_type
|
81
|
+
nil
|
82
|
+
end
|
83
|
+
|
84
|
+
def sync_mapping!
|
85
|
+
client.indices.put_mapping creation_options
|
86
|
+
end
|
87
|
+
|
88
|
+
def new_filtered_search(options = {})
|
89
|
+
args = {}
|
90
|
+
args[:index] = index.name
|
91
|
+
args[:type] = document_type
|
92
|
+
args.merge! options
|
93
|
+
|
94
|
+
Gummi::Search::Filtered.new args
|
95
|
+
end
|
96
|
+
|
97
|
+
def creation_options
|
98
|
+
result = {
|
99
|
+
index: index.name,
|
100
|
+
type: document_type,
|
101
|
+
body: {
|
102
|
+
document_type => {
|
103
|
+
properties: mapping,
|
104
|
+
}
|
105
|
+
}
|
106
|
+
}
|
107
|
+
result[:body][document_type].merge!(_parent: { type: parent_document_type }) if parent_document_type.present?
|
108
|
+
result
|
109
|
+
end
|
110
|
+
|
111
|
+
private
|
112
|
+
|
113
|
+
def client
|
114
|
+
Gummi::API.client
|
115
|
+
end
|
116
|
+
end
|
117
|
+
end
|
118
|
+
end
|
data/lib/gummi/entity.rb
ADDED
@@ -0,0 +1,18 @@
|
|
1
|
+
module Gummi
|
2
|
+
module Fields
|
3
|
+
class NgramAndPlain < Virtus::Attribute
|
4
|
+
def coerce(value)
|
5
|
+
value
|
6
|
+
end
|
7
|
+
|
8
|
+
def mapping
|
9
|
+
{ type: 'multi_field',
|
10
|
+
fields: {
|
11
|
+
name => { type: 'string', index_analyzer: 'text_index_analyzer', search_analyzer: 'text_search_analyzer' },
|
12
|
+
:plain => { type: 'string', index_analyzer: 'string_index_analyzer', search_analyzer: 'text_search_analyzer' },
|
13
|
+
}
|
14
|
+
}
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
@@ -0,0 +1,19 @@
|
|
1
|
+
module Gummi
|
2
|
+
module Fields
|
3
|
+
class PositiveInteger < Virtus::Attribute
|
4
|
+
|
5
|
+
def coerce(value)
|
6
|
+
coerced = value.to_i
|
7
|
+
if coerced > 0
|
8
|
+
coerced
|
9
|
+
else
|
10
|
+
default_value.value
|
11
|
+
end
|
12
|
+
end
|
13
|
+
|
14
|
+
def mapping
|
15
|
+
{ type: 'integer' }
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
@@ -0,0 +1,28 @@
|
|
1
|
+
module Gummi
|
2
|
+
module Fields
|
3
|
+
class SanitizedString < Virtus::Attribute
|
4
|
+
|
5
|
+
def coerce(value)
|
6
|
+
return nil if value.blank?
|
7
|
+
sanitize_string_for_query(value.to_s)
|
8
|
+
end
|
9
|
+
|
10
|
+
def mapping
|
11
|
+
{ type: 'string' }
|
12
|
+
end
|
13
|
+
|
14
|
+
def sanitize_string_for_query(str)
|
15
|
+
# Escape special characters
|
16
|
+
escaped_characters = Regexp.escape('\/\\+-&|!(){}[]^~*?:')
|
17
|
+
str = str.gsub(/([#{escaped_characters}])/) do |match|
|
18
|
+
'\\'+match
|
19
|
+
end
|
20
|
+
|
21
|
+
# Escape odd quotes
|
22
|
+
quote_count = str.count '"'
|
23
|
+
str = str.gsub(/(.*)"(.*)/, '\1\"\3') if quote_count % 2 == 1
|
24
|
+
str
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
data/lib/gummi/index.rb
ADDED
@@ -0,0 +1,146 @@
|
|
1
|
+
module Gummi
|
2
|
+
module Index
|
3
|
+
extend ActiveSupport::Concern
|
4
|
+
|
5
|
+
module ClassMethods
|
6
|
+
|
7
|
+
# Return true if created or false if already created.
|
8
|
+
#
|
9
|
+
def setup
|
10
|
+
created_settings = client.indices.create index: name, body: { settings: settings }
|
11
|
+
created_settings.present?
|
12
|
+
refresh
|
13
|
+
rescue ::Elasticsearch::Transport::Transport::Errors::BadRequest => exception
|
14
|
+
false
|
15
|
+
end
|
16
|
+
|
17
|
+
# Return true if successful or already teared down.
|
18
|
+
#
|
19
|
+
# Raises NotImplementedError in production.
|
20
|
+
#
|
21
|
+
def teardown
|
22
|
+
raise NotImplementedError unless Gummi.env == 'development' || Gummi.env == 'test'
|
23
|
+
response = client.indices.delete index: name
|
24
|
+
response.present?
|
25
|
+
rescue ::Elasticsearch::Transport::Transport::Errors::NotFound
|
26
|
+
true
|
27
|
+
end
|
28
|
+
|
29
|
+
def name
|
30
|
+
raise "Implement me"
|
31
|
+
end
|
32
|
+
|
33
|
+
def refresh
|
34
|
+
client.indices.refresh
|
35
|
+
client.cluster.health wait_for_status: :yellow
|
36
|
+
end
|
37
|
+
|
38
|
+
def settings
|
39
|
+
default_settings
|
40
|
+
end
|
41
|
+
|
42
|
+
def default_settings
|
43
|
+
{
|
44
|
+
index: {
|
45
|
+
# Main Settings
|
46
|
+
number_of_shards: '3',
|
47
|
+
number_of_replicas: (Gummi.env == 'production' ? '2' : '0'),
|
48
|
+
refresh_interval: '1s',
|
49
|
+
store: { type: (Gummi.env == 'test' ? :memory : :niofs) },
|
50
|
+
mapper: { dynamic: false },
|
51
|
+
|
52
|
+
analysis: {
|
53
|
+
|
54
|
+
# Tokenizers are just some sort of "tool" or "module" that can be applied to analyzers.
|
55
|
+
tokenizer: {
|
56
|
+
# This one is a little bit more general and is able to chop any word into all of its components.
|
57
|
+
ngram_tokenizer: {
|
58
|
+
type: 'nGram',
|
59
|
+
min_gram: 1,
|
60
|
+
max_gram: 7,
|
61
|
+
token_chars: [ 'letter', 'digit' ],
|
62
|
+
}
|
63
|
+
|
64
|
+
},
|
65
|
+
|
66
|
+
# Now we are ready to use our tokenizers.
|
67
|
+
# Let's create the most important thing: Analyzers.
|
68
|
+
analyzer: {
|
69
|
+
|
70
|
+
path_hierarchy_analyzer: {
|
71
|
+
type: 'custom',
|
72
|
+
tokenizer: 'path_hierarchy',
|
73
|
+
},
|
74
|
+
# When adding long text to Elastic, we most likely are going to use this
|
75
|
+
# analyzer. This is commonly used for titles and descriptions.
|
76
|
+
text_index_analyzer: {
|
77
|
+
type: 'custom',
|
78
|
+
tokenizer: 'ngram_tokenizer', # Chopping every word up into tokens
|
79
|
+
filter: {
|
80
|
+
0 => 'standard', # Some default transformations
|
81
|
+
1 => 'lowercase', # Make everything lowercase
|
82
|
+
2 => 'word_delimiter', # E.g. "O'Neil" -> "O Neil", "Victoria's" -> "Victoria"
|
83
|
+
2 => 'asciifolding', # Transform everything into ASCII
|
84
|
+
},
|
85
|
+
},
|
86
|
+
|
87
|
+
# For smaller texts, such as the city "stockholm", we don't want any
|
88
|
+
# tokenizing. It's enough to explicitly save the word as it is.
|
89
|
+
# As a matter of fact, if we would tokenize the city, then the facets
|
90
|
+
# would report that we have Transports in "st" "sto" "stoc" etc.
|
91
|
+
string_index_analyzer: {
|
92
|
+
type: 'custom',
|
93
|
+
tokenizer: 'standard',
|
94
|
+
filter: {
|
95
|
+
# The filters, however, are identical to the other analyzer.
|
96
|
+
0 => 'standard',
|
97
|
+
1 => 'lowercase',
|
98
|
+
2 => 'word_delimiter',
|
99
|
+
3 => 'asciifolding',
|
100
|
+
},
|
101
|
+
},
|
102
|
+
|
103
|
+
# For finding Slugs
|
104
|
+
keyword_index_analyzer: {
|
105
|
+
type: 'custom',
|
106
|
+
tokenizer: 'keyword',
|
107
|
+
filter: {
|
108
|
+
0 => 'lowercase',
|
109
|
+
1 => 'asciifolding',
|
110
|
+
},
|
111
|
+
},
|
112
|
+
|
113
|
+
# This is an analyzer that we apply to the search query itself.
|
114
|
+
text_search_analyzer: {
|
115
|
+
type: 'custom',
|
116
|
+
tokenizer: 'standard',
|
117
|
+
filter: {
|
118
|
+
0 => 'standard',
|
119
|
+
1 => 'lowercase',
|
120
|
+
2 => 'word_delimiter',
|
121
|
+
3 => 'asciifolding',
|
122
|
+
},
|
123
|
+
},
|
124
|
+
|
125
|
+
# This is an analyzer that we apply to the search query itself.
|
126
|
+
keyword_search_analyzer: {
|
127
|
+
type: 'custom',
|
128
|
+
tokenizer: 'keyword',
|
129
|
+
filter: {
|
130
|
+
0 => 'lowercase',
|
131
|
+
1 => 'asciifolding',
|
132
|
+
},
|
133
|
+
},
|
134
|
+
|
135
|
+
}
|
136
|
+
}
|
137
|
+
}
|
138
|
+
}
|
139
|
+
end
|
140
|
+
|
141
|
+
def client
|
142
|
+
Gummi::API.client
|
143
|
+
end
|
144
|
+
end
|
145
|
+
end
|
146
|
+
end
|
data/lib/gummi/object.rb
ADDED
@@ -0,0 +1,31 @@
|
|
1
|
+
|
2
|
+
module Gummi
|
3
|
+
module Repository
|
4
|
+
extend ActiveSupport::Concern
|
5
|
+
|
6
|
+
included do
|
7
|
+
include Repobahn::Repository
|
8
|
+
after_conversion :set_id_and_version
|
9
|
+
end
|
10
|
+
|
11
|
+
|
12
|
+
module ClassMethods
|
13
|
+
|
14
|
+
def get(id)
|
15
|
+
record = db_model.get id
|
16
|
+
to_entity_from_db record if record
|
17
|
+
end
|
18
|
+
|
19
|
+
def overwrite(entity)
|
20
|
+
return false unless entity.valid?
|
21
|
+
db_record = db_model.new(entity.attributes)
|
22
|
+
db_record.overwrite
|
23
|
+
end
|
24
|
+
|
25
|
+
def set_id_and_version(entity, db)
|
26
|
+
entity.id = db.id
|
27
|
+
entity.version = db.version
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
@@ -0,0 +1,36 @@
|
|
1
|
+
module Gummi
|
2
|
+
module Search
|
3
|
+
class Filtered
|
4
|
+
include Gummi::Search::Searching
|
5
|
+
|
6
|
+
attribute :query_string, Gummi::Fields::SanitizedString
|
7
|
+
attribute :query_filters, Array[Hash], default: []
|
8
|
+
attribute :facets, Hash, default: {}
|
9
|
+
|
10
|
+
def to_client_args
|
11
|
+
args = {}
|
12
|
+
args[:index] = index
|
13
|
+
args[:type] = type if type
|
14
|
+
args[:from] = from
|
15
|
+
args[:body] = {query: filtered, facets: facets }
|
16
|
+
args
|
17
|
+
end
|
18
|
+
|
19
|
+
def query
|
20
|
+
{query_string: { query: query_string}} if query_string.present?
|
21
|
+
end
|
22
|
+
|
23
|
+
def filtered
|
24
|
+
{ 'filtered' => { 'query' => query, 'filter' => process_query_filters }}
|
25
|
+
end
|
26
|
+
|
27
|
+
def process_query_filters
|
28
|
+
if query_filters.length > 1
|
29
|
+
{and: query_filters}
|
30
|
+
else
|
31
|
+
query_filters.first
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
@@ -0,0 +1,23 @@
|
|
1
|
+
module Gummi
|
2
|
+
module Search
|
3
|
+
class Result
|
4
|
+
|
5
|
+
attr_reader :took, :total, :hits, :facets
|
6
|
+
|
7
|
+
def initialize(result)
|
8
|
+
@took = result["took"]
|
9
|
+
@total = result["hits"]["total"]
|
10
|
+
@hits = result["hits"]["hits"]
|
11
|
+
@facets = result["facets"]
|
12
|
+
end
|
13
|
+
|
14
|
+
def records
|
15
|
+
hits.map do |hit|
|
16
|
+
model = "DB::#{hit["_type"].humanize}".constantize
|
17
|
+
doc_hash = {id: hit["_id"]}.merge(hit["_source"])
|
18
|
+
model.new(doc_hash)
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
@@ -0,0 +1,43 @@
|
|
1
|
+
module Gummi
|
2
|
+
module Search
|
3
|
+
module Searching
|
4
|
+
extend ActiveSupport::Concern
|
5
|
+
|
6
|
+
included do
|
7
|
+
include Virtus.model
|
8
|
+
|
9
|
+
attribute :type, String
|
10
|
+
attribute :index, String, default: lambda {|search, attr| Gummi::DefaultIndex.name}
|
11
|
+
attribute :page, Gummi::Fields::PositiveInteger, default: 1
|
12
|
+
attribute :per_page, Gummi::Fields::PositiveInteger, default: 300
|
13
|
+
attribute :options, Hash, default: {}
|
14
|
+
end
|
15
|
+
|
16
|
+
def size
|
17
|
+
per_page
|
18
|
+
end
|
19
|
+
|
20
|
+
def from
|
21
|
+
per_page * (page - 1)
|
22
|
+
end
|
23
|
+
|
24
|
+
def execute
|
25
|
+
Gummi::Search::Result.new client.search(to_client_args)
|
26
|
+
end
|
27
|
+
|
28
|
+
def to_client_args
|
29
|
+
args = {}
|
30
|
+
args[:index] = index
|
31
|
+
args[:type] = type if type
|
32
|
+
args[:from] = from
|
33
|
+
args[:size] = size
|
34
|
+
args.merge options
|
35
|
+
end
|
36
|
+
|
37
|
+
private
|
38
|
+
def client
|
39
|
+
Gummi::API.client
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
@@ -0,0 +1,57 @@
|
|
1
|
+
module Repobahn
|
2
|
+
module Repository
|
3
|
+
extend ActiveSupport::Concern
|
4
|
+
|
5
|
+
included do
|
6
|
+
include Hooks
|
7
|
+
define_hook :after_conversion
|
8
|
+
end
|
9
|
+
|
10
|
+
|
11
|
+
module ClassMethods
|
12
|
+
|
13
|
+
def entity_model
|
14
|
+
@entity_model || default_entity_model
|
15
|
+
end
|
16
|
+
|
17
|
+
def entity_model=(klass)
|
18
|
+
@entity_model = klass
|
19
|
+
end
|
20
|
+
|
21
|
+
def db_model
|
22
|
+
@db_model || default_db_model
|
23
|
+
end
|
24
|
+
|
25
|
+
def db_model=(klass)
|
26
|
+
@db_model = klass
|
27
|
+
end
|
28
|
+
|
29
|
+
def find(id)
|
30
|
+
record = db_model.find id
|
31
|
+
to_entity_from_db record if record
|
32
|
+
end
|
33
|
+
|
34
|
+
def to_entity_from_db(records)
|
35
|
+
entities = Array(records).map do |record|
|
36
|
+
entity = entity_model.new(record.attributes)
|
37
|
+
run_hook :after_conversion, entity, record
|
38
|
+
entity
|
39
|
+
end
|
40
|
+
entities.length > 1 ? entities : entities.first
|
41
|
+
end
|
42
|
+
|
43
|
+
private
|
44
|
+
|
45
|
+
def default_entity_model
|
46
|
+
full_name = name.split('::')
|
47
|
+
model_name = full_name.pop.singularize
|
48
|
+
full_name << model_name
|
49
|
+
full_name.join('::').constantize
|
50
|
+
end
|
51
|
+
|
52
|
+
def default_db_model
|
53
|
+
"DB::#{name.split('::').last.singularize}".constantize
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
@@ -0,0 +1,74 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
class ExampleModel
|
4
|
+
include Gummi::Document
|
5
|
+
|
6
|
+
attribute :test, String
|
7
|
+
end
|
8
|
+
|
9
|
+
describe Gummi::Document do
|
10
|
+
|
11
|
+
context "included" do
|
12
|
+
it "should add accessors for id and version" do
|
13
|
+
m = ExampleModel.new
|
14
|
+
m.respond_to?(:version).should be_true
|
15
|
+
m.respond_to?(:id).should be_true
|
16
|
+
end
|
17
|
+
|
18
|
+
it "should add attributes methods" do
|
19
|
+
m = ExampleModel.new(test: 'hello')
|
20
|
+
m.test.should == 'hello'
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
context "attributes" do
|
25
|
+
|
26
|
+
context "date times" do
|
27
|
+
it "should coerce from elastics strings to real Time" do
|
28
|
+
time = Time.now
|
29
|
+
person = DB::Person.new(born_at: time)
|
30
|
+
person.overwrite
|
31
|
+
person_from_es = DB::Person.get person.id
|
32
|
+
person_from_es.born_at.should be_a Time
|
33
|
+
end
|
34
|
+
|
35
|
+
it "should always store time in UTC" do
|
36
|
+
time = Time.now.in_time_zone 'CET'
|
37
|
+
|
38
|
+
person = DB::Person.new(born_at: time)
|
39
|
+
person.born_at.zone.should == 'UTC'
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
context "computed_attributes" do
|
44
|
+
|
45
|
+
let(:person) { DB::Person.new}
|
46
|
+
|
47
|
+
it "should add them to the attributes hash" do
|
48
|
+
person.name = "olof palme"
|
49
|
+
person.attributes[:computed_name].should == 'OLOF PALME'
|
50
|
+
end
|
51
|
+
|
52
|
+
it "should compute every time" do
|
53
|
+
person.name = "olof palme"
|
54
|
+
person.computed_name.should == person.name.upcase
|
55
|
+
person.name = "carl bildt"
|
56
|
+
person.computed_name.should == person.name.upcase
|
57
|
+
end
|
58
|
+
|
59
|
+
it "should provide a mapping" do
|
60
|
+
DB::Person.mapping.should include(:computed_name => {:type=>"string"})
|
61
|
+
end
|
62
|
+
end
|
63
|
+
end
|
64
|
+
|
65
|
+
context 'getting from elastic' do
|
66
|
+
let (:person) { DB::Person.new(name: 'Buzz Lightyear') }
|
67
|
+
|
68
|
+
it "should return an instance of the db_model" do
|
69
|
+
person.overwrite
|
70
|
+
person_from_es = DB::Person.get(person.id)
|
71
|
+
person_from_es.should be_a DB::Person
|
72
|
+
end
|
73
|
+
end
|
74
|
+
end
|
@@ -0,0 +1,57 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe Gummi::Repository do
|
4
|
+
|
5
|
+
let(:repository_model) { People }
|
6
|
+
let(:entity_model) { Person }
|
7
|
+
let(:db_model) { DB::Person }
|
8
|
+
|
9
|
+
describe ".entity_model" do
|
10
|
+
it "should default to singular version of repository_model" do
|
11
|
+
People.entity_model.should == Person
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
15
|
+
describe ".db_model" do
|
16
|
+
it "should default to singular version of repository_model in the DB namespace" do
|
17
|
+
People.db_model.should == DB::Person
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
context "converting from db to entity" do
|
22
|
+
|
23
|
+
let (:db_person) { DB::Person.new(name: 'Buzz Lightyear') }
|
24
|
+
|
25
|
+
it "should map the attributes from db to entity" do
|
26
|
+
person = People.to_entity_from_db(db_person)
|
27
|
+
person.name.should == 'Buzz Lightyear'
|
28
|
+
end
|
29
|
+
|
30
|
+
it "should run hook for after_conversion" do
|
31
|
+
person = People.to_entity_from_db(db_person)
|
32
|
+
person.converted_name.should == db_person.name.reverse
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
describe ".get" do
|
37
|
+
context "existing record" do
|
38
|
+
|
39
|
+
let (:db_person) { DB::Person.new(name: 'Buzz Lightyear') }
|
40
|
+
|
41
|
+
it "should return an entity" do
|
42
|
+
db_person.overwrite
|
43
|
+
person = People.get(db_person.id)
|
44
|
+
person.id.should == db_person.id
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
context "missing record" do
|
49
|
+
it "returns nil" do
|
50
|
+
person = People.get('missing_id')
|
51
|
+
person.should be_nil
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
|
57
|
+
end
|
@@ -0,0 +1,15 @@
|
|
1
|
+
# example db model
|
2
|
+
|
3
|
+
module DB
|
4
|
+
class Person
|
5
|
+
include Gummi::Document
|
6
|
+
|
7
|
+
attribute :name, Gummi::Fields::String
|
8
|
+
attribute :computed_name, Gummi::Fields::String
|
9
|
+
attribute :born_at, Gummi::Fields::Time
|
10
|
+
|
11
|
+
def computed_name
|
12
|
+
name.upcase if name
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
data/spec/spec_helper.rb
ADDED
@@ -0,0 +1,19 @@
|
|
1
|
+
require "codeclimate-test-reporter"
|
2
|
+
CodeClimate::TestReporter.start
|
3
|
+
|
4
|
+
RAILS_ENV = 'test'
|
5
|
+
require 'gummi'
|
6
|
+
require_relative 'models/people'
|
7
|
+
require_relative 'models/person'
|
8
|
+
require_relative 'models/db/person'
|
9
|
+
|
10
|
+
RSpec.configure do |config|
|
11
|
+
config.before(:suite) do
|
12
|
+
Gummi::DefaultIndex.setup
|
13
|
+
DB::Person.sync_mapping!
|
14
|
+
end
|
15
|
+
|
16
|
+
config.after(:suite) do
|
17
|
+
Gummi::DefaultIndex.teardown
|
18
|
+
end
|
19
|
+
end
|
metadata
ADDED
@@ -0,0 +1,232 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: gummi
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.0.6
|
5
|
+
prerelease:
|
6
|
+
platform: ruby
|
7
|
+
authors:
|
8
|
+
- bukowskis
|
9
|
+
autorequire:
|
10
|
+
bindir: bin
|
11
|
+
cert_chain: []
|
12
|
+
date: 2013-11-18 00:00:00.000000000 Z
|
13
|
+
dependencies:
|
14
|
+
- !ruby/object:Gem::Dependency
|
15
|
+
name: virtus
|
16
|
+
requirement: !ruby/object:Gem::Requirement
|
17
|
+
none: false
|
18
|
+
requirements:
|
19
|
+
- - ~>
|
20
|
+
- !ruby/object:Gem::Version
|
21
|
+
version: 1.0.0
|
22
|
+
type: :runtime
|
23
|
+
prerelease: false
|
24
|
+
version_requirements: !ruby/object:Gem::Requirement
|
25
|
+
none: false
|
26
|
+
requirements:
|
27
|
+
- - ~>
|
28
|
+
- !ruby/object:Gem::Version
|
29
|
+
version: 1.0.0
|
30
|
+
- !ruby/object:Gem::Dependency
|
31
|
+
name: elasticsearch
|
32
|
+
requirement: !ruby/object:Gem::Requirement
|
33
|
+
none: false
|
34
|
+
requirements:
|
35
|
+
- - ~>
|
36
|
+
- !ruby/object:Gem::Version
|
37
|
+
version: 0.4.0
|
38
|
+
type: :runtime
|
39
|
+
prerelease: false
|
40
|
+
version_requirements: !ruby/object:Gem::Requirement
|
41
|
+
none: false
|
42
|
+
requirements:
|
43
|
+
- - ~>
|
44
|
+
- !ruby/object:Gem::Version
|
45
|
+
version: 0.4.0
|
46
|
+
- !ruby/object:Gem::Dependency
|
47
|
+
name: activesupport
|
48
|
+
requirement: !ruby/object:Gem::Requirement
|
49
|
+
none: false
|
50
|
+
requirements:
|
51
|
+
- - ! '>='
|
52
|
+
- !ruby/object:Gem::Version
|
53
|
+
version: '3.0'
|
54
|
+
type: :runtime
|
55
|
+
prerelease: false
|
56
|
+
version_requirements: !ruby/object:Gem::Requirement
|
57
|
+
none: false
|
58
|
+
requirements:
|
59
|
+
- - ! '>='
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
version: '3.0'
|
62
|
+
- !ruby/object:Gem::Dependency
|
63
|
+
name: activemodel
|
64
|
+
requirement: !ruby/object:Gem::Requirement
|
65
|
+
none: false
|
66
|
+
requirements:
|
67
|
+
- - ! '>='
|
68
|
+
- !ruby/object:Gem::Version
|
69
|
+
version: '3.0'
|
70
|
+
type: :runtime
|
71
|
+
prerelease: false
|
72
|
+
version_requirements: !ruby/object:Gem::Requirement
|
73
|
+
none: false
|
74
|
+
requirements:
|
75
|
+
- - ! '>='
|
76
|
+
- !ruby/object:Gem::Version
|
77
|
+
version: '3.0'
|
78
|
+
- !ruby/object:Gem::Dependency
|
79
|
+
name: hooks
|
80
|
+
requirement: !ruby/object:Gem::Requirement
|
81
|
+
none: false
|
82
|
+
requirements:
|
83
|
+
- - ~>
|
84
|
+
- !ruby/object:Gem::Version
|
85
|
+
version: 0.3.3
|
86
|
+
type: :runtime
|
87
|
+
prerelease: false
|
88
|
+
version_requirements: !ruby/object:Gem::Requirement
|
89
|
+
none: false
|
90
|
+
requirements:
|
91
|
+
- - ~>
|
92
|
+
- !ruby/object:Gem::Version
|
93
|
+
version: 0.3.3
|
94
|
+
- !ruby/object:Gem::Dependency
|
95
|
+
name: leaflet
|
96
|
+
requirement: !ruby/object:Gem::Requirement
|
97
|
+
none: false
|
98
|
+
requirements:
|
99
|
+
- - ! '>='
|
100
|
+
- !ruby/object:Gem::Version
|
101
|
+
version: '0'
|
102
|
+
type: :runtime
|
103
|
+
prerelease: false
|
104
|
+
version_requirements: !ruby/object:Gem::Requirement
|
105
|
+
none: false
|
106
|
+
requirements:
|
107
|
+
- - ! '>='
|
108
|
+
- !ruby/object:Gem::Version
|
109
|
+
version: '0'
|
110
|
+
- !ruby/object:Gem::Dependency
|
111
|
+
name: bundler
|
112
|
+
requirement: !ruby/object:Gem::Requirement
|
113
|
+
none: false
|
114
|
+
requirements:
|
115
|
+
- - ~>
|
116
|
+
- !ruby/object:Gem::Version
|
117
|
+
version: '1.3'
|
118
|
+
type: :development
|
119
|
+
prerelease: false
|
120
|
+
version_requirements: !ruby/object:Gem::Requirement
|
121
|
+
none: false
|
122
|
+
requirements:
|
123
|
+
- - ~>
|
124
|
+
- !ruby/object:Gem::Version
|
125
|
+
version: '1.3'
|
126
|
+
- !ruby/object:Gem::Dependency
|
127
|
+
name: rake
|
128
|
+
requirement: !ruby/object:Gem::Requirement
|
129
|
+
none: false
|
130
|
+
requirements:
|
131
|
+
- - ! '>='
|
132
|
+
- !ruby/object:Gem::Version
|
133
|
+
version: '0'
|
134
|
+
type: :development
|
135
|
+
prerelease: false
|
136
|
+
version_requirements: !ruby/object:Gem::Requirement
|
137
|
+
none: false
|
138
|
+
requirements:
|
139
|
+
- - ! '>='
|
140
|
+
- !ruby/object:Gem::Version
|
141
|
+
version: '0'
|
142
|
+
- !ruby/object:Gem::Dependency
|
143
|
+
name: rspec
|
144
|
+
requirement: !ruby/object:Gem::Requirement
|
145
|
+
none: false
|
146
|
+
requirements:
|
147
|
+
- - ! '>='
|
148
|
+
- !ruby/object:Gem::Version
|
149
|
+
version: '0'
|
150
|
+
type: :development
|
151
|
+
prerelease: false
|
152
|
+
version_requirements: !ruby/object:Gem::Requirement
|
153
|
+
none: false
|
154
|
+
requirements:
|
155
|
+
- - ! '>='
|
156
|
+
- !ruby/object:Gem::Version
|
157
|
+
version: '0'
|
158
|
+
description: A small wrapper around Elasticsearch
|
159
|
+
email:
|
160
|
+
executables: []
|
161
|
+
extensions: []
|
162
|
+
extra_rdoc_files: []
|
163
|
+
files:
|
164
|
+
- .gitignore
|
165
|
+
- Gemfile
|
166
|
+
- LICENSE.txt
|
167
|
+
- README.md
|
168
|
+
- Rakefile
|
169
|
+
- gummi.gemspec
|
170
|
+
- lib/gummi.rb
|
171
|
+
- lib/gummi/api.rb
|
172
|
+
- lib/gummi/attributes.rb
|
173
|
+
- lib/gummi/default_index.rb
|
174
|
+
- lib/gummi/document.rb
|
175
|
+
- lib/gummi/entity.rb
|
176
|
+
- lib/gummi/fields/boolean.rb
|
177
|
+
- lib/gummi/fields/integer.rb
|
178
|
+
- lib/gummi/fields/keyword.rb
|
179
|
+
- lib/gummi/fields/ngram_and_plain.rb
|
180
|
+
- lib/gummi/fields/path_hierarchy.rb
|
181
|
+
- lib/gummi/fields/positive_integer.rb
|
182
|
+
- lib/gummi/fields/sanitized_string.rb
|
183
|
+
- lib/gummi/fields/string.rb
|
184
|
+
- lib/gummi/fields/time.rb
|
185
|
+
- lib/gummi/index.rb
|
186
|
+
- lib/gummi/object.rb
|
187
|
+
- lib/gummi/repository.rb
|
188
|
+
- lib/gummi/search/filtered.rb
|
189
|
+
- lib/gummi/search/raw.rb
|
190
|
+
- lib/gummi/search/result.rb
|
191
|
+
- lib/gummi/search/searching.rb
|
192
|
+
- lib/gummi/version.rb
|
193
|
+
- lib/repobahn/entity.rb
|
194
|
+
- lib/repobahn/repository.rb
|
195
|
+
- spec/lib/gummi/document_spec.rb
|
196
|
+
- spec/lib/gummi/repository_spec.rb
|
197
|
+
- spec/models/db/person.rb
|
198
|
+
- spec/models/people.rb
|
199
|
+
- spec/models/person.rb
|
200
|
+
- spec/spec_helper.rb
|
201
|
+
homepage: ''
|
202
|
+
licenses:
|
203
|
+
- MIT
|
204
|
+
post_install_message:
|
205
|
+
rdoc_options: []
|
206
|
+
require_paths:
|
207
|
+
- lib
|
208
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
209
|
+
none: false
|
210
|
+
requirements:
|
211
|
+
- - ! '>='
|
212
|
+
- !ruby/object:Gem::Version
|
213
|
+
version: '0'
|
214
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
215
|
+
none: false
|
216
|
+
requirements:
|
217
|
+
- - ! '>='
|
218
|
+
- !ruby/object:Gem::Version
|
219
|
+
version: '0'
|
220
|
+
requirements: []
|
221
|
+
rubyforge_project:
|
222
|
+
rubygems_version: 1.8.23
|
223
|
+
signing_key:
|
224
|
+
specification_version: 3
|
225
|
+
summary: A small wrapper around Elasticsearch
|
226
|
+
test_files:
|
227
|
+
- spec/lib/gummi/document_spec.rb
|
228
|
+
- spec/lib/gummi/repository_spec.rb
|
229
|
+
- spec/models/db/person.rb
|
230
|
+
- spec/models/people.rb
|
231
|
+
- spec/models/person.rb
|
232
|
+
- spec/spec_helper.rb
|