Skip to content

Persistence implementation: Repository pattern #71

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 32 commits into from
May 27, 2014
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
Show all changes
32 commits
Select commit Hold shift + click to select a range
6e199e4
[STORE] Added the blank skeleton of the "elasticsearch-persistence" gem
karmi Mar 25, 2014
4488ea5
[STORE] Added the Persistence::Repository module and client integration
karmi Mar 25, 2014
48d7132
[STORE] Added the default `Repository::Class` for convenience
karmi Mar 29, 2014
e5d9ffb
[STORE] Added the `Naming` module
karmi Mar 29, 2014
7165f3d
[STORE] Added the `Serialize` module
karmi Mar 29, 2014
5938ef0
[STORE] Added the `Store` module
karmi Mar 29, 2014
41a3695
[STORE] Added the `Find` module
karmi Mar 31, 2014
7bcd91d
[STORE] Added the `Search` module
karmi Mar 31, 2014
485a4f9
[STORE] Added the DSL variant of `klass` setter method
karmi Mar 31, 2014
c4b1d41
[STORE] Added the methods from the "elasticsearch-model" gem
karmi Mar 31, 2014
5160424
[STORE] Refactored the `:index` paramter to use repository `index_name`
karmi Mar 31, 2014
0989784
[STORE] Changed that `document_type` method returns `nil` when no `kl…
karmi Apr 1, 2014
caecfa9
[STORE] Changed that the `Store` methods reflect that `klass` returns…
karmi Apr 1, 2014
2fbc3aa
[STORE] Added the `index` and `type` aliases for `index_name` and `do…
karmi Apr 1, 2014
6d739dd
[STORE] Added, that `document_type` can set the document type for rep…
karmi Apr 1, 2014
e4e6ff2
[STORE] Changed, that repository methods respect `document_type` when…
karmi Apr 1, 2014
c391a3b
[STORE] Implemented the gateway pattern for the repository integration
karmi Apr 2, 2014
63da64a
[STORE] Added, that `index_name` is inferred from the including class
karmi Apr 2, 2014
1299c0b
[STORE] Added, that the repository class reflects the `:index` option
karmi Apr 2, 2014
a929413
[STORE] Added that the `client` can be set in a DSL-like way
karmi Apr 3, 2014
cbbff17
[STORE] Added `respond_to_missing?` to the proxy objects
karmi Apr 3, 2014
44c9952
[STORE] Added the `method_added` hook to allow defining gateway metho…
karmi Apr 4, 2014
ca1270d
[STORE] Added code annotation, documentation and examples
karmi Apr 4, 2014
94a19de
[STORE] Added a comprehensive usage information / tutorial to the README
karmi Apr 4, 2014
e853785
[STORE] Added an example Sinatra web application for the repository p…
karmi Apr 7, 2014
4881db8
[STORE] Added development dependency on "oj"
karmi Apr 17, 2014
afafa12
[STORE] Fixed, that `search` respects `document_type` set in class
pepe Apr 15, 2014
0e66341
[STORE] Added a unit test for searching in type based on `document_type`
karmi Apr 17, 2014
44f6bca
[STORE] Added integration tests for the Repository pattern
karmi Apr 17, 2014
c44c343
[STORE] Added the `__extract_id_from_document` method to Naming module
karmi Apr 19, 2014
b97491a
[STORE] Added a `update` method for the repository Store module
karmi Apr 19, 2014
91814b3
[STORE] Improved the documentation across the persistence gem
karmi May 27, 2014
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Prev Previous commit
Next Next commit
[STORE] Added code annotation, documentation and examples
  • Loading branch information
karmi committed May 27, 2014
commit ca1270ddcfc3e1234612ea9459d08d0d7d6c3cc0
58 changes: 58 additions & 0 deletions elasticsearch-persistence/lib/elasticsearch/persistence.rb
Original file line number Diff line number Diff line change
Expand Up @@ -17,14 +17,72 @@
require 'elasticsearch/persistence/repository'

module Elasticsearch

# Persistence for Ruby domain objects and models in Elasticsearch
# ===============================================================
#
# `Elasticsearch::Persistence` contains modules for storing and retrieving Ruby domain objects and models
# in Elasticsearch.
#
# == Repository
#
# The repository patterns allows to store and retrieve Ruby objects in Elasticsearch.
#
# require 'elasticsearch/persistence'
#
# class Note
# def to_hash; {foo: 'bar'}; end
# end
#
# repository = Elasticsearch::Persistence::Repository.new
#
# repository.save Note.new
# # => {"_index"=>"repository", "_type"=>"note", "_id"=>"mY108X9mSHajxIy2rzH2CA", ...}
#
# Customize your repository by including the main module in a Ruby class
# class MyRepository
# include Elasticsearch::Persistence::Repository
#
# index 'my_notes'
# klass Note
#
# client Elasticsearch::Client.new log: true
# end
#
# repository = MyRepository.new
#
# repository.save Note.new
# # 2014-04-04 22:15:25 +0200: POST http://localhost:9200/my_notes/note [status:201, request:0.009s, query:n/a]
# # 2014-04-04 22:15:25 +0200: > {"foo":"bar"}
# # 2014-04-04 22:15:25 +0200: < {"_index":"my_notes","_type":"note","_id":"-d28yXLFSlusnTxb13WIZQ", ...}
#
module Persistence

# :nodoc:
module ClassMethods

# Get or set the default client for all repositories and models
#
# @example Set and configure the default client
#
# Elasticsearch::Persistence.client Elasticsearch::Client.new host: 'http://localhost:9200', tracer: true
#
# @example Perform an API request through the client
#
# Elasticsearch::Persistence.client.cluster.health
# # => { "cluster_name" => "elasticsearch" ... }
#
def client client=nil
@client = client || @client || Elasticsearch::Client.new
end

# Set the default client for all repositories and models
#
# @example Set and configure the default client
#
# Elasticsearch::Persistence.client = Elasticsearch::Client.new host: 'http://localhost:9200', tracer: true
# => #<Elasticsearch::Transport::Client:0x007f96a6dd0d80 @transport=... >
#
def client=(client)
@client = client
end
Expand Down
30 changes: 30 additions & 0 deletions elasticsearch-persistence/lib/elasticsearch/persistence/client.rb
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,40 @@ module Persistence
module Repository

module Client

# Get or set the default client for this repository
#
# @example Set and configure the client for the repository class
#
# class MyRepository
# include Elasticsearch::Persistence::Repository
# client Elasticsearch::Client.new host: 'http://localhost:9200', log: true
# end
#
# @example Set and configure the client for this repository instance
#
# repository.client Elasticsearch::Client.new host: 'http://localhost:9200', tracer: true
#
# @example Perform an API request through the client
#
# MyRepository.client.cluster.health
# repository.client.cluster.health
# # => { "cluster_name" => "elasticsearch" ... }
#
def client client=nil
@client = client || @client || Elasticsearch::Persistence.client
end

# Set the default client for this repository
#
# @example Set and configure the client for the repository class
#
# MyRepository.client = Elasticsearch::Client.new host: 'http://localhost:9200', log: true
#
# @example Set and configure the client for this repository instance
#
# repository.client = Elasticsearch::Client.new host: 'http://localhost:9200', tracer: true
#
def client=(client)
@client = client
@client
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,41 @@ module Elasticsearch
module Persistence
module Repository

# The default repository class, to be used either directly, or as a gateway in a custom repository class
#
# @example Standalone use
#
# repository = Elasticsearch::Persistence::Repository::Class.new
# # => #<Elasticsearch::Persistence::Repository::Class ...>
# # > repository.save(my_object)
# # => {"_index"=> ... }
#
#
# @example Shortcut use
#
# repository = Elasticsearch::Persistence::Repository.new
# # => #<Elasticsearch::Persistence::Repository::Class ...>
#
# @example Configuration via a block
#
# repository = Elasticsearch::Persistence::Repository.new do
# index 'my_notes'
# end
# # => #<Elasticsearch::Persistence::Repository::Class ...>
# # > repository.save(my_object)
# # => {"_index"=> ... }
#
# @example Accessing the gateway in a custom class
#
# class MyRepository
# include Elasticsearch::Persistence::Repository
# end
#
# repository = MyRepository.new
#
# repository.gateway.client.info
# => {"status"=>200, "name"=>"Venom", ... }
#
class Class
include Elasticsearch::Persistence::Repository::Client
include Elasticsearch::Persistence::Repository::Naming
Expand All @@ -20,6 +55,10 @@ def initialize(options={}, &block)
block.arity < 1 ? instance_eval(&block) : block.call(self) if block_given?
end

# Return the "host" class, if this repository is a gateway hosted in another class
#
# @return [nil, Class]
#
def host
options[:host]
end
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,24 @@ module Persistence
module Repository
class DocumentNotFound < StandardError; end

# Retrieves one or more domain objects from the repository
#
module Find

# Retrieve a single object or multiple objects from Elasticsearch by ID or IDs
#
# @example Retrieve a single object by ID
#
# repository.find(1)
# # => <Note ...>
#
# @example Retrieve multiple objects by IDs
#
# repository.find(1, 2)
# # => [<Note ...>, <Note ...>
#
# @return [Object,Array]
#
def find(*args)
options = args.last.is_a?(Hash) ? args.pop : {}
ids = args
Expand All @@ -16,11 +33,22 @@ def find(*args)
end
end

# Return if object exists in the repository
#
# @example
#
# repository.exists?(1)
# => true
#
# @return [true, false]
#
def exists?(id, options={})
type = document_type || (klass ? __get_type_from_class(klass) : '_all')
client.exists( { index: index_name, type: type, id: id }.merge(options) )
end

# @api private
#
def __find_one(id, options={})
type = document_type || (klass ? __get_type_from_class(klass) : '_all')
document = client.get( { index: index_name, type: type, id: id }.merge(options) )
Expand All @@ -30,6 +58,8 @@ def __find_one(id, options={})
raise DocumentNotFound, e.message, caller
end

# @api private
#
def __find_many(ids, options={})
type = document_type || (klass ? __get_type_from_class(klass) : '_all')
documents = client.mget( { index: index_name, type: type, body: { ids: ids } }.merge(options) )
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,21 @@ module Persistence
module Repository

module Naming

# Get or set the class used to initialize domain objects when deserializing them
#
def klass name=nil
@klass = name || @klass
end

# Set the class used to initialize domain objects when deserializing them
#
def klass=klass
@klass = klass
end

# Get or set the index name used when storing and retrieving documents
#
def index_name name=nil
@index_name = name || @index_name || begin
if respond_to?(:host) && host && host.is_a?(Module)
Expand All @@ -21,29 +28,59 @@ def index_name name=nil
end
end; alias :index :index_name

# Set the index name used when storing and retrieving documents
#
def index_name=(name)
@index_name = name
end; alias :index= :index_name=

# Get or set the document type used when storing and retrieving documents
#
def document_type name=nil
@document_type = name || @document_type || (klass ? klass.to_s.underscore : nil)
end; alias :type :document_type

# Set the document type used when storing and retrieving documents
#
def document_type=(name)
@document_type = name
end; alias :type= :document_type=

# Get the Ruby class from the Elasticsearch `_type`
#
# @example
# repository.__get_klass_from_type 'note'
# => Note
#
# @api private
#
def __get_klass_from_type(type)
klass = type.classify
klass.constantize
rescue NameError => e
raise NameError, "Attempted to get class '#{klass}' from the '#{type}' type, but no such class can be found."
end

# Get the Elasticsearch `_type` from the Ruby class
#
# @example
# repository.__get_type_from_class Note
# => "note"
#
# @api private
#
def __get_type_from_class(klass)
klass.to_s.underscore
end

# Get a document ID from the document (assuming Hash or Hash-like object)
#
# @example
# repository.__get_id_from_document title: 'Test', id: 'abc123'
# => "abc123"
#
# @api private
#
def __get_id_from_document(document)
document[:id] || document['id'] || document[:_id] || document['_id']
end
Expand Down
Original file line number Diff line number Diff line change
@@ -1,13 +1,21 @@
module Elasticsearch
module Persistence
module Repository
module Response
module Response # :nodoc:

# Encapsulates the domain objects and documents returned from Elasticsearch when searching
#
# Implements `Enumerable` and forwards its methods to the {#results} object.
#
class Results
include Enumerable

attr_reader :repository, :response, :response
attr_reader :repository

# @param repository [Elasticsearch::Persistence::Repository::Class] The repository instance
# @param response [Hash] The full response returned from the Elasticsearch client
# @param options [Hash] Optional parameters
#
def initialize(repository, response, options={})
@repository = repository
@response = Hashie::Mash.new(response)
Expand All @@ -22,10 +30,14 @@ def respond_to?(method_name, include_private = false)
results.respond_to?(method_name) || super
end

# The number of total hits for a query
#
def total
response['hits']['total']
end

# The maximum score for a query
#
def max_score
response['hits']['max_score']
end
Expand All @@ -42,11 +54,35 @@ def map_with_hit(&block)
results.zip(response['hits']['hits']).map(&block)
end

# Return the collection of domain objects
#
# @example Iterate over the results
#
# results.map { |r| r.attributes[:title] }
# => ["Fox", "Dog"]
#
# @return [Array]
#
def results
@results ||= response['hits']['hits'].map do |document|
repository.deserialize(document.to_hash)
end
end

# Access the response returned from Elasticsearch by the client
#
# @example Access the aggregations in the response
#
# results = repository.search query: { match: { title: 'fox dog' } },
# aggregations: { titles: { terms: { field: 'title' } } }
# results.response.aggregations.titles.buckets.map { |term| "#{term['key']}: #{term['doc_count']}" }
# # => ["brown: 1", "dog: 1", ...]
#
# @return [Hashie::Mash]
#
def response
@response
end
end
end
end
Expand Down
Loading