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.
- 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
@@ -0,0 +1,118 @@
|
|
1
|
+
ENV['RACK_ENV'] = 'test'
|
2
|
+
|
3
|
+
at_exit { Elasticsearch::Test::IntegrationTestCase.__run_at_exit_hooks } if ENV['SERVER']
|
4
|
+
|
5
|
+
require 'test/unit'
|
6
|
+
require 'shoulda-context'
|
7
|
+
require 'mocha/setup'
|
8
|
+
require 'rack/test'
|
9
|
+
require 'turn'
|
10
|
+
|
11
|
+
require 'elasticsearch/extensions/test/cluster'
|
12
|
+
require 'elasticsearch/extensions/test/startup_shutdown'
|
13
|
+
|
14
|
+
require_relative 'application'
|
15
|
+
|
16
|
+
NoteRepository.index_name = 'notes_test'
|
17
|
+
|
18
|
+
class Elasticsearch::Persistence::ExampleApplicationTest < Test::Unit::TestCase
|
19
|
+
include Rack::Test::Methods
|
20
|
+
alias :response :last_response
|
21
|
+
|
22
|
+
def app
|
23
|
+
Application.new
|
24
|
+
end
|
25
|
+
|
26
|
+
context "Note" do
|
27
|
+
should "be initialized with a Hash" do
|
28
|
+
note = Note.new 'foo' => 'bar'
|
29
|
+
assert_equal 'bar', note.attributes['foo']
|
30
|
+
end
|
31
|
+
|
32
|
+
should "add created_at when it's not passed" do
|
33
|
+
note = Note.new
|
34
|
+
assert_not_nil note.created_at
|
35
|
+
assert_match /#{Time.now.year}/, note.created_at
|
36
|
+
end
|
37
|
+
|
38
|
+
should "not add created_at when it's passed" do
|
39
|
+
note = Note.new 'created_at' => 'FOO'
|
40
|
+
assert_equal 'FOO', note.created_at
|
41
|
+
end
|
42
|
+
|
43
|
+
should "trim long text" do
|
44
|
+
assert_equal 'Hello World', Note.new('text' => 'Hello World').text
|
45
|
+
assert_equal 'FOOFOOFOOFOOFOOFOOFOOFOOFOOFOOFOOFOOFOOFOOFOOFOOFOOFOOFOOFOOFOOFOOFOOFOOFOOFOOFO (...)',
|
46
|
+
Note.new('text' => 'FOO'*200).text
|
47
|
+
end
|
48
|
+
|
49
|
+
should "delegate methods to attributes" do
|
50
|
+
note = Note.new 'foo' => 'bar'
|
51
|
+
assert_equal 'bar', note.foo
|
52
|
+
end
|
53
|
+
|
54
|
+
should "have tags" do
|
55
|
+
assert_not_nil Note.new.tags
|
56
|
+
end
|
57
|
+
|
58
|
+
should "provide a `to_hash` method" do
|
59
|
+
note = Note.new 'foo' => 'bar'
|
60
|
+
assert_instance_of Hash, note.to_hash
|
61
|
+
assert_equal ['created_at', 'foo'], note.to_hash.keys.sort
|
62
|
+
end
|
63
|
+
|
64
|
+
should "extract tags from the text" do
|
65
|
+
note = Note.new 'text' => 'Hello [foo] [bar]'
|
66
|
+
assert_equal 'Hello', note.text
|
67
|
+
assert_equal ['foo', 'bar'], note.tags
|
68
|
+
end
|
69
|
+
end
|
70
|
+
|
71
|
+
context "Application" do
|
72
|
+
setup do
|
73
|
+
app.settings.repository.client = Elasticsearch::Client.new \
|
74
|
+
hosts: [{ host: 'localhost', port: ENV.fetch('TEST_CLUSTER_PORT', 9250)}],
|
75
|
+
log: true
|
76
|
+
app.settings.repository.client.transport.logger.formatter = proc { |s, d, p, m| "\e[2m#{m}\n\e[0m" }
|
77
|
+
app.settings.repository.create_index! force: true
|
78
|
+
app.settings.repository.client.cluster.health wait_for_status: 'yellow'
|
79
|
+
end
|
80
|
+
|
81
|
+
should "have the correct index name" do
|
82
|
+
assert_equal 'notes_test', app.settings.repository.index
|
83
|
+
end
|
84
|
+
|
85
|
+
should "display empty page when there are no notes" do
|
86
|
+
get '/'
|
87
|
+
assert response.ok?, response.status.to_s
|
88
|
+
assert_match /No notes found/, response.body.to_s
|
89
|
+
end
|
90
|
+
|
91
|
+
should "display the notes" do
|
92
|
+
app.settings.repository.save Note.new('text' => 'Hello')
|
93
|
+
app.settings.repository.refresh_index!
|
94
|
+
|
95
|
+
get '/'
|
96
|
+
assert response.ok?, response.status.to_s
|
97
|
+
assert_match /<p>\s*Hello/, response.body.to_s
|
98
|
+
end
|
99
|
+
|
100
|
+
should "create a note" do
|
101
|
+
post '/', { 'text' => 'Hello World' }
|
102
|
+
follow_redirect!
|
103
|
+
|
104
|
+
assert response.ok?, response.status.to_s
|
105
|
+
assert_match /Hello World/, response.body.to_s
|
106
|
+
end
|
107
|
+
|
108
|
+
should "delete a note" do
|
109
|
+
app.settings.repository.save Note.new('id' => 'foobar', 'text' => 'Perish...')
|
110
|
+
delete "/foobar"
|
111
|
+
follow_redirect!
|
112
|
+
|
113
|
+
assert response.ok?, response.status.to_s
|
114
|
+
assert_no_match /Perish/, response.body.to_s
|
115
|
+
end
|
116
|
+
end
|
117
|
+
|
118
|
+
end
|
@@ -1,7 +1,93 @@
|
|
1
|
-
require
|
1
|
+
require 'elasticsearch'
|
2
|
+
require 'elasticsearch/model/indexing'
|
3
|
+
require 'hashie'
|
4
|
+
|
5
|
+
require 'active_support/inflector'
|
6
|
+
|
7
|
+
require 'elasticsearch/persistence/version'
|
8
|
+
|
9
|
+
require 'elasticsearch/persistence/client'
|
10
|
+
require 'elasticsearch/persistence/repository/response/results'
|
11
|
+
require 'elasticsearch/persistence/repository/naming'
|
12
|
+
require 'elasticsearch/persistence/repository/serialize'
|
13
|
+
require 'elasticsearch/persistence/repository/store'
|
14
|
+
require 'elasticsearch/persistence/repository/find'
|
15
|
+
require 'elasticsearch/persistence/repository/search'
|
16
|
+
require 'elasticsearch/persistence/repository/class'
|
17
|
+
require 'elasticsearch/persistence/repository'
|
2
18
|
|
3
19
|
module Elasticsearch
|
20
|
+
|
21
|
+
# Persistence for Ruby domain objects and models in Elasticsearch
|
22
|
+
# ===============================================================
|
23
|
+
#
|
24
|
+
# `Elasticsearch::Persistence` contains modules for storing and retrieving Ruby domain objects and models
|
25
|
+
# in Elasticsearch.
|
26
|
+
#
|
27
|
+
# == Repository
|
28
|
+
#
|
29
|
+
# The repository patterns allows to store and retrieve Ruby objects in Elasticsearch.
|
30
|
+
#
|
31
|
+
# require 'elasticsearch/persistence'
|
32
|
+
#
|
33
|
+
# class Note
|
34
|
+
# def to_hash; {foo: 'bar'}; end
|
35
|
+
# end
|
36
|
+
#
|
37
|
+
# repository = Elasticsearch::Persistence::Repository.new
|
38
|
+
#
|
39
|
+
# repository.save Note.new
|
40
|
+
# # => {"_index"=>"repository", "_type"=>"note", "_id"=>"mY108X9mSHajxIy2rzH2CA", ...}
|
41
|
+
#
|
42
|
+
# Customize your repository by including the main module in a Ruby class
|
43
|
+
# class MyRepository
|
44
|
+
# include Elasticsearch::Persistence::Repository
|
45
|
+
#
|
46
|
+
# index 'my_notes'
|
47
|
+
# klass Note
|
48
|
+
#
|
49
|
+
# client Elasticsearch::Client.new log: true
|
50
|
+
# end
|
51
|
+
#
|
52
|
+
# repository = MyRepository.new
|
53
|
+
#
|
54
|
+
# repository.save Note.new
|
55
|
+
# # 2014-04-04 22:15:25 +0200: POST http://localhost:9200/my_notes/note [status:201, request:0.009s, query:n/a]
|
56
|
+
# # 2014-04-04 22:15:25 +0200: > {"foo":"bar"}
|
57
|
+
# # 2014-04-04 22:15:25 +0200: < {"_index":"my_notes","_type":"note","_id":"-d28yXLFSlusnTxb13WIZQ", ...}
|
58
|
+
#
|
4
59
|
module Persistence
|
5
|
-
|
60
|
+
|
61
|
+
# :nodoc:
|
62
|
+
module ClassMethods
|
63
|
+
|
64
|
+
# Get or set the default client for all repositories and models
|
65
|
+
#
|
66
|
+
# @example Set and configure the default client
|
67
|
+
#
|
68
|
+
# Elasticsearch::Persistence.client Elasticsearch::Client.new host: 'http://localhost:9200', tracer: true
|
69
|
+
#
|
70
|
+
# @example Perform an API request through the client
|
71
|
+
#
|
72
|
+
# Elasticsearch::Persistence.client.cluster.health
|
73
|
+
# # => { "cluster_name" => "elasticsearch" ... }
|
74
|
+
#
|
75
|
+
def client client=nil
|
76
|
+
@client = client || @client || Elasticsearch::Client.new
|
77
|
+
end
|
78
|
+
|
79
|
+
# Set the default client for all repositories and models
|
80
|
+
#
|
81
|
+
# @example Set and configure the default client
|
82
|
+
#
|
83
|
+
# Elasticsearch::Persistence.client = Elasticsearch::Client.new host: 'http://localhost:9200', tracer: true
|
84
|
+
# => #<Elasticsearch::Transport::Client:0x007f96a6dd0d80 @transport=... >
|
85
|
+
#
|
86
|
+
def client=(client)
|
87
|
+
@client = client
|
88
|
+
end
|
89
|
+
end
|
90
|
+
|
91
|
+
extend ClassMethods
|
6
92
|
end
|
7
93
|
end
|
@@ -0,0 +1,51 @@
|
|
1
|
+
module Elasticsearch
|
2
|
+
module Persistence
|
3
|
+
module Repository
|
4
|
+
|
5
|
+
# Wraps the Elasticsearch Ruby
|
6
|
+
# [client](https://github.com/elasticsearch/elasticsearch-ruby/tree/master/elasticsearch#usage)
|
7
|
+
#
|
8
|
+
module Client
|
9
|
+
|
10
|
+
# Get or set the default client for this repository
|
11
|
+
#
|
12
|
+
# @example Set and configure the client for the repository class
|
13
|
+
#
|
14
|
+
# class MyRepository
|
15
|
+
# include Elasticsearch::Persistence::Repository
|
16
|
+
# client Elasticsearch::Client.new host: 'http://localhost:9200', log: true
|
17
|
+
# end
|
18
|
+
#
|
19
|
+
# @example Set and configure the client for this repository instance
|
20
|
+
#
|
21
|
+
# repository.client Elasticsearch::Client.new host: 'http://localhost:9200', tracer: true
|
22
|
+
#
|
23
|
+
# @example Perform an API request through the client
|
24
|
+
#
|
25
|
+
# MyRepository.client.cluster.health
|
26
|
+
# repository.client.cluster.health
|
27
|
+
# # => { "cluster_name" => "elasticsearch" ... }
|
28
|
+
#
|
29
|
+
def client client=nil
|
30
|
+
@client = client || @client || Elasticsearch::Persistence.client
|
31
|
+
end
|
32
|
+
|
33
|
+
# Set the default client for this repository
|
34
|
+
#
|
35
|
+
# @example Set and configure the client for the repository class
|
36
|
+
#
|
37
|
+
# MyRepository.client = Elasticsearch::Client.new host: 'http://localhost:9200', log: true
|
38
|
+
#
|
39
|
+
# @example Set and configure the client for this repository instance
|
40
|
+
#
|
41
|
+
# repository.client = Elasticsearch::Client.new host: 'http://localhost:9200', tracer: true
|
42
|
+
#
|
43
|
+
def client=(client)
|
44
|
+
@client = client
|
45
|
+
@client
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
@@ -0,0 +1,75 @@
|
|
1
|
+
module Elasticsearch
|
2
|
+
module Persistence
|
3
|
+
|
4
|
+
# Delegate methods to the repository (acting as a gateway)
|
5
|
+
#
|
6
|
+
module GatewayDelegation
|
7
|
+
def method_missing(method_name, *arguments, &block)
|
8
|
+
gateway.respond_to?(method_name) ? gateway.__send__(method_name, *arguments, &block) : super
|
9
|
+
end
|
10
|
+
|
11
|
+
def respond_to?(method_name, include_private=false)
|
12
|
+
gateway.respond_to?(method_name) || super
|
13
|
+
end
|
14
|
+
|
15
|
+
def respond_to_missing?(method_name, *)
|
16
|
+
gateway.respond_to?(method_name) || super
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
# When included, creates an instance of the {Repository::Class} class as a "gateway"
|
21
|
+
#
|
22
|
+
# @example Include the repository in a custom class
|
23
|
+
#
|
24
|
+
# class MyRepository
|
25
|
+
# include Elasticsearch::Persistence::Repository
|
26
|
+
# end
|
27
|
+
#
|
28
|
+
module Repository
|
29
|
+
def self.included(base)
|
30
|
+
gateway = Elasticsearch::Persistence::Repository::Class.new host: base
|
31
|
+
|
32
|
+
# Define the instance level gateway
|
33
|
+
#
|
34
|
+
base.class_eval do
|
35
|
+
define_method :gateway do
|
36
|
+
@gateway ||= gateway
|
37
|
+
end
|
38
|
+
|
39
|
+
include GatewayDelegation
|
40
|
+
end
|
41
|
+
|
42
|
+
# Define the class level gateway
|
43
|
+
#
|
44
|
+
(class << base; self; end).class_eval do
|
45
|
+
define_method :gateway do |&block|
|
46
|
+
@gateway ||= gateway
|
47
|
+
@gateway.instance_eval(&block) if block
|
48
|
+
@gateway
|
49
|
+
end
|
50
|
+
|
51
|
+
include GatewayDelegation
|
52
|
+
end
|
53
|
+
|
54
|
+
# Catch repository methods (such as `serialize` and others) defined in the receiving class,
|
55
|
+
# and overload the default definition in the gateway
|
56
|
+
#
|
57
|
+
def base.method_added(name)
|
58
|
+
if :gateway != name && respond_to?(:gateway) && (gateway.public_methods - Object.public_methods).include?(name)
|
59
|
+
gateway.define_singleton_method(name, self.new.method(name).to_proc)
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
64
|
+
# Shortcut method to allow concise repository initialization
|
65
|
+
#
|
66
|
+
# @example Create a new default repository
|
67
|
+
#
|
68
|
+
# repository = Elasticsearch::Persistence::Repository.new
|
69
|
+
#
|
70
|
+
def new(options={}, &block)
|
71
|
+
Elasticsearch::Persistence::Repository::Class.new( {index: 'repository'}.merge(options), &block )
|
72
|
+
end; module_function :new
|
73
|
+
end
|
74
|
+
end
|
75
|
+
end
|
@@ -0,0 +1,71 @@
|
|
1
|
+
module Elasticsearch
|
2
|
+
module Persistence
|
3
|
+
module Repository
|
4
|
+
|
5
|
+
# The default repository class, to be used either directly, or as a gateway in a custom repository class
|
6
|
+
#
|
7
|
+
# @example Standalone use
|
8
|
+
#
|
9
|
+
# repository = Elasticsearch::Persistence::Repository::Class.new
|
10
|
+
# # => #<Elasticsearch::Persistence::Repository::Class ...>
|
11
|
+
# repository.save(my_object)
|
12
|
+
# # => {"_index"=> ... }
|
13
|
+
#
|
14
|
+
# @example Shortcut use
|
15
|
+
#
|
16
|
+
# repository = Elasticsearch::Persistence::Repository.new
|
17
|
+
# # => #<Elasticsearch::Persistence::Repository::Class ...>
|
18
|
+
#
|
19
|
+
# @example Configuration via a block
|
20
|
+
#
|
21
|
+
# repository = Elasticsearch::Persistence::Repository.new do
|
22
|
+
# index 'my_notes'
|
23
|
+
# end
|
24
|
+
#
|
25
|
+
# # => #<Elasticsearch::Persistence::Repository::Class ...>
|
26
|
+
# # > repository.save(my_object)
|
27
|
+
# # => {"_index"=> ... }
|
28
|
+
#
|
29
|
+
# @example Accessing the gateway in a custom class
|
30
|
+
#
|
31
|
+
# class MyRepository
|
32
|
+
# include Elasticsearch::Persistence::Repository
|
33
|
+
# end
|
34
|
+
#
|
35
|
+
# repository = MyRepository.new
|
36
|
+
#
|
37
|
+
# repository.gateway.client.info
|
38
|
+
# # => {"status"=>200, "name"=>"Venom", ... }
|
39
|
+
#
|
40
|
+
class Class
|
41
|
+
include Elasticsearch::Persistence::Repository::Client
|
42
|
+
include Elasticsearch::Persistence::Repository::Naming
|
43
|
+
include Elasticsearch::Persistence::Repository::Serialize
|
44
|
+
include Elasticsearch::Persistence::Repository::Store
|
45
|
+
include Elasticsearch::Persistence::Repository::Find
|
46
|
+
include Elasticsearch::Persistence::Repository::Search
|
47
|
+
|
48
|
+
include Elasticsearch::Model::Indexing::ClassMethods
|
49
|
+
|
50
|
+
attr_reader :options
|
51
|
+
|
52
|
+
def initialize(options={}, &block)
|
53
|
+
@options = options
|
54
|
+
index_name options.delete(:index)
|
55
|
+
block.arity < 1 ? instance_eval(&block) : block.call(self) if block_given?
|
56
|
+
end
|
57
|
+
|
58
|
+
# Return the "host" class, if this repository is a gateway hosted in another class
|
59
|
+
#
|
60
|
+
# @return [nil, Class]
|
61
|
+
#
|
62
|
+
# @api private
|
63
|
+
#
|
64
|
+
def host
|
65
|
+
options[:host]
|
66
|
+
end
|
67
|
+
end
|
68
|
+
|
69
|
+
end
|
70
|
+
end
|
71
|
+
end
|
@@ -0,0 +1,73 @@
|
|
1
|
+
module Elasticsearch
|
2
|
+
module Persistence
|
3
|
+
module Repository
|
4
|
+
class DocumentNotFound < StandardError; end
|
5
|
+
|
6
|
+
# Retrieves one or more domain objects from the repository
|
7
|
+
#
|
8
|
+
module Find
|
9
|
+
|
10
|
+
# Retrieve a single object or multiple objects from Elasticsearch by ID or IDs
|
11
|
+
#
|
12
|
+
# @example Retrieve a single object by ID
|
13
|
+
#
|
14
|
+
# repository.find(1)
|
15
|
+
# # => <Note ...>
|
16
|
+
#
|
17
|
+
# @example Retrieve multiple objects by IDs
|
18
|
+
#
|
19
|
+
# repository.find(1, 2)
|
20
|
+
# # => [<Note ...>, <Note ...>
|
21
|
+
#
|
22
|
+
# @return [Object,Array]
|
23
|
+
#
|
24
|
+
def find(*args)
|
25
|
+
options = args.last.is_a?(Hash) ? args.pop : {}
|
26
|
+
ids = args
|
27
|
+
|
28
|
+
if args.size == 1
|
29
|
+
id = args.pop
|
30
|
+
id.is_a?(Array) ? __find_many(id, options) : __find_one(id, options)
|
31
|
+
else
|
32
|
+
__find_many args, options
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
# Return if object exists in the repository
|
37
|
+
#
|
38
|
+
# @example
|
39
|
+
#
|
40
|
+
# repository.exists?(1)
|
41
|
+
# => true
|
42
|
+
#
|
43
|
+
# @return [true, false]
|
44
|
+
#
|
45
|
+
def exists?(id, options={})
|
46
|
+
type = document_type || (klass ? __get_type_from_class(klass) : '_all')
|
47
|
+
client.exists( { index: index_name, type: type, id: id }.merge(options) )
|
48
|
+
end
|
49
|
+
|
50
|
+
# @api private
|
51
|
+
#
|
52
|
+
def __find_one(id, options={})
|
53
|
+
type = document_type || (klass ? __get_type_from_class(klass) : '_all')
|
54
|
+
document = client.get( { index: index_name, type: type, id: id }.merge(options) )
|
55
|
+
|
56
|
+
deserialize(document)
|
57
|
+
rescue Elasticsearch::Transport::Transport::Errors::NotFound => e
|
58
|
+
raise DocumentNotFound, e.message, caller
|
59
|
+
end
|
60
|
+
|
61
|
+
# @api private
|
62
|
+
#
|
63
|
+
def __find_many(ids, options={})
|
64
|
+
type = document_type || (klass ? __get_type_from_class(klass) : '_all')
|
65
|
+
documents = client.mget( { index: index_name, type: type, body: { ids: ids } }.merge(options) )
|
66
|
+
|
67
|
+
documents['docs'].map { |document| document['found'] ? deserialize(document) : nil }
|
68
|
+
end
|
69
|
+
end
|
70
|
+
|
71
|
+
end
|
72
|
+
end
|
73
|
+
end
|