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.
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