elasticsearch-persistence 0.0.0 → 0.0.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 +15 -0
- data/LICENSE.txt +10 -19
- data/README.md +432 -14
- data/Rakefile +56 -0
- data/elasticsearch-persistence.gemspec +45 -17
- data/examples/sinatra/.gitignore +7 -0
- data/examples/sinatra/Gemfile +28 -0
- data/examples/sinatra/README.markdown +36 -0
- data/examples/sinatra/application.rb +238 -0
- data/examples/sinatra/config.ru +7 -0
- data/examples/sinatra/test.rb +118 -0
- data/lib/elasticsearch/persistence.rb +88 -2
- data/lib/elasticsearch/persistence/client.rb +51 -0
- data/lib/elasticsearch/persistence/repository.rb +75 -0
- data/lib/elasticsearch/persistence/repository/class.rb +71 -0
- data/lib/elasticsearch/persistence/repository/find.rb +73 -0
- data/lib/elasticsearch/persistence/repository/naming.rb +115 -0
- data/lib/elasticsearch/persistence/repository/response/results.rb +90 -0
- data/lib/elasticsearch/persistence/repository/search.rb +60 -0
- data/lib/elasticsearch/persistence/repository/serialize.rb +31 -0
- data/lib/elasticsearch/persistence/repository/store.rb +95 -0
- data/lib/elasticsearch/persistence/version.rb +1 -1
- data/test/integration/repository/custom_class_test.rb +85 -0
- data/test/integration/repository/customized_class_test.rb +82 -0
- data/test/integration/repository/default_class_test.rb +108 -0
- data/test/integration/repository/virtus_model_test.rb +114 -0
- data/test/test_helper.rb +46 -0
- data/test/unit/persistence_test.rb +32 -0
- data/test/unit/repository_class_test.rb +51 -0
- data/test/unit/repository_client_test.rb +32 -0
- data/test/unit/repository_find_test.rb +375 -0
- data/test/unit/repository_indexing_test.rb +37 -0
- data/test/unit/repository_module_test.rb +144 -0
- data/test/unit/repository_naming_test.rb +146 -0
- data/test/unit/repository_response_results_test.rb +98 -0
- data/test/unit/repository_search_test.rb +97 -0
- data/test/unit/repository_serialize_test.rb +57 -0
- data/test/unit/repository_store_test.rb +287 -0
- metadata +288 -20
data/Rakefile
CHANGED
@@ -1 +1,57 @@
|
|
1
1
|
require "bundler/gem_tasks"
|
2
|
+
|
3
|
+
desc "Run unit tests"
|
4
|
+
task :default => 'test:unit'
|
5
|
+
task :test => 'test:unit'
|
6
|
+
|
7
|
+
# ----- Test tasks ------------------------------------------------------------
|
8
|
+
|
9
|
+
require 'rake/testtask'
|
10
|
+
namespace :test do
|
11
|
+
task :ci_reporter do
|
12
|
+
ENV['CI_REPORTS'] ||= 'tmp/reports'
|
13
|
+
require 'ci/reporter/rake/minitest'
|
14
|
+
Rake::Task['ci:setup:minitest'].invoke
|
15
|
+
end
|
16
|
+
|
17
|
+
Rake::TestTask.new(:unit) do |test|
|
18
|
+
Rake::Task['test:ci_reporter'].invoke if ENV['CI']
|
19
|
+
test.libs << 'lib' << 'test'
|
20
|
+
test.test_files = FileList["test/unit/**/*_test.rb"]
|
21
|
+
# test.verbose = true
|
22
|
+
# test.warning = true
|
23
|
+
end
|
24
|
+
|
25
|
+
Rake::TestTask.new(:integration) do |test|
|
26
|
+
Rake::Task['test:ci_reporter'].invoke if ENV['CI']
|
27
|
+
test.libs << 'lib' << 'test'
|
28
|
+
test.test_files = FileList["test/integration/**/*_test.rb"]
|
29
|
+
end
|
30
|
+
|
31
|
+
Rake::TestTask.new(:all) do |test|
|
32
|
+
Rake::Task['test:ci_reporter'].invoke if ENV['CI']
|
33
|
+
test.libs << 'lib' << 'test'
|
34
|
+
test.test_files = FileList["test/unit/**/*_test.rb", "test/integration/**/*_test.rb"]
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
# ----- Documentation tasks ---------------------------------------------------
|
39
|
+
|
40
|
+
require 'yard'
|
41
|
+
YARD::Rake::YardocTask.new(:doc) do |t|
|
42
|
+
t.options = %w| --embed-mixins --markup=markdown |
|
43
|
+
end
|
44
|
+
|
45
|
+
# ----- Code analysis tasks ---------------------------------------------------
|
46
|
+
|
47
|
+
if defined?(RUBY_VERSION) && RUBY_VERSION > '1.9'
|
48
|
+
begin
|
49
|
+
require 'cane/rake_task'
|
50
|
+
Cane::RakeTask.new(:quality) do |cane|
|
51
|
+
cane.abc_max = 15
|
52
|
+
cane.style_measure = 120
|
53
|
+
end
|
54
|
+
rescue LoadError
|
55
|
+
warn "cane not available, quality task not provided."
|
56
|
+
end
|
57
|
+
end
|
@@ -3,21 +3,49 @@ lib = File.expand_path('../lib', __FILE__)
|
|
3
3
|
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
4
4
|
require 'elasticsearch/persistence/version'
|
5
5
|
|
6
|
-
Gem::Specification.new do |
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
6
|
+
Gem::Specification.new do |s|
|
7
|
+
s.name = "elasticsearch-persistence"
|
8
|
+
s.version = Elasticsearch::Persistence::VERSION
|
9
|
+
s.authors = ["Karel Minarik"]
|
10
|
+
s.email = ["karel.minarik@elasticsearch.org"]
|
11
|
+
s.description = "Persistence layer for Ruby models and Elasticsearch."
|
12
|
+
s.summary = "Persistence layer for Ruby models and Elasticsearch."
|
13
|
+
s.homepage = "https://github.com/elasticsearch/elasticsearch-rails/"
|
14
|
+
s.license = "Apache 2"
|
15
|
+
|
16
|
+
s.files = `git ls-files -z`.split("\x0")
|
17
|
+
s.executables = s.files.grep(%r{^bin/}) { |f| File.basename(f) }
|
18
|
+
s.test_files = s.files.grep(%r{^(test|spec|features)/})
|
19
|
+
s.require_paths = ["lib"]
|
20
|
+
|
21
|
+
s.extra_rdoc_files = [ "README.md", "LICENSE.txt" ]
|
22
|
+
s.rdoc_options = [ "--charset=UTF-8" ]
|
23
|
+
|
24
|
+
s.required_ruby_version = ">= 1.9.3"
|
25
|
+
|
26
|
+
s.add_dependency "elasticsearch", '> 0.4'
|
27
|
+
s.add_dependency "elasticsearch-model", '>= 0.1'
|
28
|
+
s.add_dependency "activesupport", '> 3'
|
29
|
+
s.add_dependency "hashie"
|
30
|
+
|
31
|
+
s.add_development_dependency "bundler", "~> 1.5"
|
32
|
+
s.add_development_dependency "rake"
|
33
|
+
|
34
|
+
s.add_development_dependency "oj"
|
35
|
+
s.add_development_dependency "virtus"
|
36
|
+
|
37
|
+
s.add_development_dependency "elasticsearch-extensions"
|
38
|
+
|
39
|
+
s.add_development_dependency "shoulda-context"
|
40
|
+
s.add_development_dependency "mocha"
|
41
|
+
s.add_development_dependency "turn"
|
42
|
+
s.add_development_dependency "yard"
|
43
|
+
s.add_development_dependency "ruby-prof"
|
44
|
+
s.add_development_dependency "pry"
|
45
|
+
s.add_development_dependency "ci_reporter"
|
46
|
+
|
47
|
+
if defined?(RUBY_VERSION) && RUBY_VERSION > '1.9'
|
48
|
+
s.add_development_dependency "simplecov"
|
49
|
+
s.add_development_dependency "cane"
|
50
|
+
end
|
23
51
|
end
|
@@ -0,0 +1,28 @@
|
|
1
|
+
source 'https://rubygems.org'
|
2
|
+
|
3
|
+
gem 'rake'
|
4
|
+
gem 'ansi'
|
5
|
+
|
6
|
+
gem 'multi_json'
|
7
|
+
gem 'oj'
|
8
|
+
gem 'hashie'
|
9
|
+
|
10
|
+
gem 'patron'
|
11
|
+
gem 'elasticsearch'
|
12
|
+
gem 'elasticsearch-model', path: File.expand_path('../../../../elasticsearch-model', __FILE__)
|
13
|
+
gem 'elasticsearch-persistence', path: File.expand_path('../../../', __FILE__)
|
14
|
+
|
15
|
+
gem 'sinatra', require: false
|
16
|
+
gem 'thin'
|
17
|
+
|
18
|
+
group :development do
|
19
|
+
gem 'sinatra-contrib'
|
20
|
+
end
|
21
|
+
|
22
|
+
group :test do
|
23
|
+
gem 'elasticsearch-extensions'
|
24
|
+
gem 'rack-test'
|
25
|
+
gem 'shoulda-context'
|
26
|
+
gem 'turn'
|
27
|
+
gem 'mocha'
|
28
|
+
end
|
@@ -0,0 +1,36 @@
|
|
1
|
+
Demo Aplication for the Repository Pattern
|
2
|
+
==========================================
|
3
|
+
|
4
|
+
This directory contains a simple demo application for the repository pattern of the `Elasticsearch::Persistence`
|
5
|
+
module in the [Sinatra](http://www.sinatrarb.com) framework.
|
6
|
+
|
7
|
+
To run the application, first install the required gems and start the application:
|
8
|
+
|
9
|
+
```
|
10
|
+
bundle install
|
11
|
+
bundle exec ruby application.rb
|
12
|
+
```
|
13
|
+
|
14
|
+
The application demonstrates:
|
15
|
+
|
16
|
+
* How to use a plain old Ruby object (PORO) as the domain model
|
17
|
+
* How to set up, configure and use the repository instance
|
18
|
+
* How to use the repository in tests
|
19
|
+
|
20
|
+
## License
|
21
|
+
|
22
|
+
This software is licensed under the Apache 2 license, quoted below.
|
23
|
+
|
24
|
+
Copyright (c) 2014 Elasticsearch <http://www.elasticsearch.org>
|
25
|
+
|
26
|
+
Licensed under the Apache License, Version 2.0 (the "License");
|
27
|
+
you may not use this file except in compliance with the License.
|
28
|
+
You may obtain a copy of the License at
|
29
|
+
|
30
|
+
http://www.apache.org/licenses/LICENSE-2.0
|
31
|
+
|
32
|
+
Unless required by applicable law or agreed to in writing, software
|
33
|
+
distributed under the License is distributed on an "AS IS" BASIS,
|
34
|
+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
35
|
+
See the License for the specific language governing permissions and
|
36
|
+
limitations under the License.
|
@@ -0,0 +1,238 @@
|
|
1
|
+
$LOAD_PATH.unshift File.expand_path('../../../lib/', __FILE__)
|
2
|
+
|
3
|
+
require 'sinatra/base'
|
4
|
+
|
5
|
+
require 'multi_json'
|
6
|
+
require 'oj'
|
7
|
+
require 'hashie/mash'
|
8
|
+
|
9
|
+
require 'elasticsearch'
|
10
|
+
require 'elasticsearch/model'
|
11
|
+
require 'elasticsearch/persistence'
|
12
|
+
|
13
|
+
class Note
|
14
|
+
attr_reader :attributes
|
15
|
+
|
16
|
+
def initialize(attributes={})
|
17
|
+
@attributes = Hashie::Mash.new(attributes)
|
18
|
+
__add_date
|
19
|
+
__extract_tags
|
20
|
+
__truncate_text
|
21
|
+
self
|
22
|
+
end
|
23
|
+
|
24
|
+
def method_missing(method_name, *arguments, &block)
|
25
|
+
attributes.respond_to?(method_name) ? attributes.__send__(method_name, *arguments, &block) : super
|
26
|
+
end
|
27
|
+
|
28
|
+
def respond_to?(method_name, include_private=false)
|
29
|
+
attributes.respond_to?(method_name) || super
|
30
|
+
end
|
31
|
+
|
32
|
+
def tags; attributes.tags || []; end
|
33
|
+
|
34
|
+
def to_hash
|
35
|
+
@attributes.to_hash
|
36
|
+
end
|
37
|
+
|
38
|
+
def __extract_tags
|
39
|
+
tags = attributes['text'].scan(/(\[\w+\])/).flatten if attributes['text']
|
40
|
+
unless tags.nil? || tags.empty?
|
41
|
+
attributes.update 'tags' => tags.map { |t| t.tr('[]', '') }
|
42
|
+
attributes['text'].gsub!(/(\[\w+\])/, '').strip!
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
def __add_date
|
47
|
+
attributes['created_at'] ||= Time.now.utc.iso8601
|
48
|
+
end
|
49
|
+
|
50
|
+
def __truncate_text
|
51
|
+
attributes['text'] = attributes['text'][0...80] + ' (...)' if attributes['text'] && attributes['text'].size > 80
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
class NoteRepository
|
56
|
+
include Elasticsearch::Persistence::Repository
|
57
|
+
|
58
|
+
client Elasticsearch::Client.new url: ENV['ELASTICSEARCH_URL'], log: true
|
59
|
+
|
60
|
+
index :notes
|
61
|
+
type :note
|
62
|
+
|
63
|
+
mapping do
|
64
|
+
indexes :text, analyzer: 'snowball'
|
65
|
+
indexes :tags, analyzer: 'keyword'
|
66
|
+
indexes :created_at, type: 'date'
|
67
|
+
end
|
68
|
+
|
69
|
+
create_index!
|
70
|
+
|
71
|
+
def deserialize(document)
|
72
|
+
Note.new document['_source'].merge('id' => document['_id'])
|
73
|
+
end
|
74
|
+
end unless defined?(NoteRepository)
|
75
|
+
|
76
|
+
class Application < Sinatra::Base
|
77
|
+
enable :logging
|
78
|
+
enable :inline_templates
|
79
|
+
enable :method_override
|
80
|
+
|
81
|
+
configure :development do
|
82
|
+
enable :dump_errors
|
83
|
+
disable :show_exceptions
|
84
|
+
|
85
|
+
require 'sinatra/reloader'
|
86
|
+
register Sinatra::Reloader
|
87
|
+
end
|
88
|
+
|
89
|
+
set :repository, NoteRepository.new
|
90
|
+
set :per_page, 25
|
91
|
+
|
92
|
+
get '/' do
|
93
|
+
@page = [ params[:p].to_i, 1 ].max
|
94
|
+
|
95
|
+
@notes = settings.repository.search \
|
96
|
+
query: ->(q, t) do
|
97
|
+
query = if q && !q.empty?
|
98
|
+
{ match: { text: q } }
|
99
|
+
else
|
100
|
+
{ match_all: {} }
|
101
|
+
end
|
102
|
+
|
103
|
+
filter = if t && !t.empty?
|
104
|
+
{ term: { tags: t } }
|
105
|
+
end
|
106
|
+
|
107
|
+
if filter
|
108
|
+
{ filtered: { query: query, filter: filter } }
|
109
|
+
else
|
110
|
+
query
|
111
|
+
end
|
112
|
+
end.(params[:q], params[:t]),
|
113
|
+
|
114
|
+
sort: [{created_at: {order: 'desc'}}],
|
115
|
+
|
116
|
+
size: settings.per_page,
|
117
|
+
from: settings.per_page * (@page-1),
|
118
|
+
|
119
|
+
aggregations: { tags: { terms: { field: 'tags' } } },
|
120
|
+
|
121
|
+
highlight: { fields: { text: { fragment_size: 0, pre_tags: ['<em class="hl">'],post_tags: ['</em>'] } } }
|
122
|
+
|
123
|
+
erb :index
|
124
|
+
end
|
125
|
+
|
126
|
+
post '/' do
|
127
|
+
unless params[:text].empty?
|
128
|
+
@note = Note.new params
|
129
|
+
settings.repository.save(@note, refresh: true)
|
130
|
+
end
|
131
|
+
|
132
|
+
redirect back
|
133
|
+
end
|
134
|
+
|
135
|
+
delete '/:id' do |id|
|
136
|
+
settings.repository.delete(id, refresh: true)
|
137
|
+
redirect back
|
138
|
+
end
|
139
|
+
end
|
140
|
+
|
141
|
+
Application.run! if $0 == __FILE__
|
142
|
+
|
143
|
+
__END__
|
144
|
+
|
145
|
+
@@ layout
|
146
|
+
<!DOCTYPE html>
|
147
|
+
<html>
|
148
|
+
<head>
|
149
|
+
<title>Notes</title>
|
150
|
+
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
|
151
|
+
<style>
|
152
|
+
body { color: #222; background: #fff; font: normal 80%/120% 'Helvetica Neue', sans-serif; margin: 4em; position: relative; }
|
153
|
+
header { color: #666; border-bottom: 2px solid #666; }
|
154
|
+
header:after { display: table; content: ""; line-height: 0; clear: both; }
|
155
|
+
#left { width: 20em; float: left }
|
156
|
+
#main { margin-left: 20em; }
|
157
|
+
header h1 { font-weight: normal; float: left; padding: 0.4em 0 0 0; margin: 0; }
|
158
|
+
header form { margin-left: 19.5em; }
|
159
|
+
header form input { font-size: 120%; width: 40em; border: none; padding: 0.5em; position: relative; bottom: -0.2em; background: transparent; }
|
160
|
+
header form input:focus { outline-width: 0; }
|
161
|
+
|
162
|
+
#left h2 { color: #999; font-size: 160%; font-weight: normal; text-transform: uppercase; letter-spacing: -0.05em; }
|
163
|
+
#left h2 { border-top: 2px solid #999; width: 9.4em; padding: 0.5em 0 0.5em 0; margin: 0; }
|
164
|
+
#left textarea { font: normal 140%/140% monospace; border: 1px solid #999; padding: 0.5em; width: 12em; }
|
165
|
+
#left form p { margin: 0; }
|
166
|
+
#left a { color: #000; }
|
167
|
+
#left small.c { color: #333; background: #ccc; text-align: center; min-width: 1.75em; min-height: 1.5em; border-radius: 1em; display: inline-block; padding-top: 0.25em; float: right; margin-right: 6em; }
|
168
|
+
#left small.i { color: #ccc; background: #333; }
|
169
|
+
|
170
|
+
#facets { list-style-type: none; padding: 0; margin: 0 0 1em 0; }
|
171
|
+
#facets li { padding: 0 0 0.5em 0; }
|
172
|
+
|
173
|
+
.note { border-bottom: 1px solid #999; position: relative; padding: 0.5em 0; }
|
174
|
+
.note p { font-size: 140%; }
|
175
|
+
.note small { font-size: 70%; color: #999; }
|
176
|
+
.note small.d { border-left: 1px solid #999; padding-left: 0.5em; margin-left: 0.5em; }
|
177
|
+
.note em.hl { background: #fcfcad; border-radius: 0.5em; padding: 0.2em 0.4em 0.2em 0.4em; }
|
178
|
+
.note strong.t { color: #fff; background: #999; font-size: 70%; font-weight: bold; border-radius: 0.6em; padding: 0.2em 0.6em 0.3em 0.7em; }
|
179
|
+
.note form { position: absolute; bottom: 1.5em; right: 1em; }
|
180
|
+
|
181
|
+
.pagination { color: #000; font-weight: bold; text-align: right; }
|
182
|
+
.pagination:visited { color: #000; }
|
183
|
+
.pagination a { text-decoration: none; }
|
184
|
+
.pagination:hover a { text-decoration: underline; }
|
185
|
+
}
|
186
|
+
|
187
|
+
</style>
|
188
|
+
</head>
|
189
|
+
<body>
|
190
|
+
<%= yield %>
|
191
|
+
</body>
|
192
|
+
</html>
|
193
|
+
|
194
|
+
@@ index
|
195
|
+
|
196
|
+
<header>
|
197
|
+
<h1>Notes</h1>
|
198
|
+
<form action="/" method='get'>
|
199
|
+
<input type="text" name="q" value="<%= params[:q] %>" id="q" autofocus="autofocus" placeholder="type a search query and press enter..." />
|
200
|
+
</form>
|
201
|
+
</header>
|
202
|
+
|
203
|
+
<section id="left">
|
204
|
+
<p><a href="/">All notes</a> <small class="c i"><%= @notes.size %></small></p>
|
205
|
+
<ul id="facets">
|
206
|
+
<% @notes.response.aggregations.tags.buckets.each do |term| %>
|
207
|
+
<li><a href="/?t=<%= term['key'] %>"><%= term['key'] %></a> <small class="c"><%= term['doc_count'] %></small></li>
|
208
|
+
<% end %>
|
209
|
+
</ul>
|
210
|
+
<h2>Add a note</h2>
|
211
|
+
<form action="/" method='post'>
|
212
|
+
<p><textarea name="text" rows="5"></textarea></p>
|
213
|
+
<p><input type="submit" accesskey="s" value="Save" /></p>
|
214
|
+
</form>
|
215
|
+
</section>
|
216
|
+
|
217
|
+
<section id="main">
|
218
|
+
<% if @notes.empty? %>
|
219
|
+
<p>No notes found.</p>
|
220
|
+
<% end %>
|
221
|
+
|
222
|
+
<% @notes.each_with_hit do |note, hit| %>
|
223
|
+
<div class="note">
|
224
|
+
<p>
|
225
|
+
<%= hit.highlight && hit.highlight.size > 0 ? hit.highlight.text.first : note.text %>
|
226
|
+
|
227
|
+
<% note.tags.each do |tag| %> <strong class="t"><%= tag %></strong><% end %>
|
228
|
+
<small class="d"><%= Time.parse(note.created_at).strftime('%d/%m/%Y %H:%M') %></small>
|
229
|
+
|
230
|
+
<form action="/<%= note.id %>" method="post"><input type="hidden" name="_method" value="delete" /><button>Delete</button></form>
|
231
|
+
</p>
|
232
|
+
</div>
|
233
|
+
<% end %>
|
234
|
+
|
235
|
+
<% if @notes.size > 0 && @page.next <= @notes.total / settings.per_page %>
|
236
|
+
<p class="pagination"><a href="?p=<%= @page.next %>">→ Load next</a></p>
|
237
|
+
<% end %>
|
238
|
+
</section>
|