elasticsearch-persistence 0.0.0 → 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (39) hide show
  1. checksums.yaml +15 -0
  2. data/LICENSE.txt +10 -19
  3. data/README.md +432 -14
  4. data/Rakefile +56 -0
  5. data/elasticsearch-persistence.gemspec +45 -17
  6. data/examples/sinatra/.gitignore +7 -0
  7. data/examples/sinatra/Gemfile +28 -0
  8. data/examples/sinatra/README.markdown +36 -0
  9. data/examples/sinatra/application.rb +238 -0
  10. data/examples/sinatra/config.ru +7 -0
  11. data/examples/sinatra/test.rb +118 -0
  12. data/lib/elasticsearch/persistence.rb +88 -2
  13. data/lib/elasticsearch/persistence/client.rb +51 -0
  14. data/lib/elasticsearch/persistence/repository.rb +75 -0
  15. data/lib/elasticsearch/persistence/repository/class.rb +71 -0
  16. data/lib/elasticsearch/persistence/repository/find.rb +73 -0
  17. data/lib/elasticsearch/persistence/repository/naming.rb +115 -0
  18. data/lib/elasticsearch/persistence/repository/response/results.rb +90 -0
  19. data/lib/elasticsearch/persistence/repository/search.rb +60 -0
  20. data/lib/elasticsearch/persistence/repository/serialize.rb +31 -0
  21. data/lib/elasticsearch/persistence/repository/store.rb +95 -0
  22. data/lib/elasticsearch/persistence/version.rb +1 -1
  23. data/test/integration/repository/custom_class_test.rb +85 -0
  24. data/test/integration/repository/customized_class_test.rb +82 -0
  25. data/test/integration/repository/default_class_test.rb +108 -0
  26. data/test/integration/repository/virtus_model_test.rb +114 -0
  27. data/test/test_helper.rb +46 -0
  28. data/test/unit/persistence_test.rb +32 -0
  29. data/test/unit/repository_class_test.rb +51 -0
  30. data/test/unit/repository_client_test.rb +32 -0
  31. data/test/unit/repository_find_test.rb +375 -0
  32. data/test/unit/repository_indexing_test.rb +37 -0
  33. data/test/unit/repository_module_test.rb +144 -0
  34. data/test/unit/repository_naming_test.rb +146 -0
  35. data/test/unit/repository_response_results_test.rb +98 -0
  36. data/test/unit/repository_search_test.rb +97 -0
  37. data/test/unit/repository_serialize_test.rb +57 -0
  38. data/test/unit/repository_store_test.rb +287 -0
  39. 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 |spec|
7
- spec.name = "elasticsearch-persistence"
8
- spec.version = Elasticsearch::Persistence::VERSION
9
- spec.authors = ["Karel Minarik"]
10
- spec.email = ["karel.minarik@elasticsearch.org"]
11
- spec.description = %q{Elasticsearch persistence (WIP)}
12
- spec.summary = spec.description
13
- spec.homepage = ""
14
- spec.license = "Apache 2"
15
-
16
- spec.files = `git ls-files`.split($/)
17
- spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
18
- spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
19
- spec.require_paths = ["lib"]
20
-
21
- spec.add_development_dependency "bundler", "~> 1.3"
22
- spec.add_development_dependency "rake"
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,7 @@
1
+ .DS_Store
2
+ Gemfile.lock
3
+ tmp/*
4
+ log/*
5
+ doc/
6
+ .yardoc
7
+ .vagrant
@@ -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 %>">&rarr; Load next</a></p>
237
+ <% end %>
238
+ </section>
@@ -0,0 +1,7 @@
1
+ #\ --port 3000 --server thin
2
+
3
+ require File.expand_path('../application', __FILE__)
4
+
5
+ map '/' do
6
+ run Application
7
+ end