From c8548374f241b513e79f01059b43bf524fa5434b Mon Sep 17 00:00:00 2001 From: Karel Minarik Date: Tue, 25 Mar 2014 19:22:30 +0100 Subject: [PATCH 01/27] [STORE] Added the blank skeleton of the "elasticsearch-persistence" gem --- elasticsearch-persistence/.gitignore | 17 ++++++ elasticsearch-persistence/Gemfile | 4 ++ elasticsearch-persistence/LICENSE.txt | 13 +++++ elasticsearch-persistence/README.md | 21 +++++++ elasticsearch-persistence/Rakefile | 57 +++++++++++++++++++ .../elasticsearch-persistence.gemspec | 39 +++++++++++++ .../lib/elasticsearch/persistence.rb | 7 +++ .../lib/elasticsearch/persistence/version.rb | 5 ++ elasticsearch-persistence/test/test_helper.rb | 42 ++++++++++++++ 9 files changed, 205 insertions(+) create mode 100644 elasticsearch-persistence/.gitignore create mode 100644 elasticsearch-persistence/Gemfile create mode 100644 elasticsearch-persistence/LICENSE.txt create mode 100644 elasticsearch-persistence/README.md create mode 100644 elasticsearch-persistence/Rakefile create mode 100644 elasticsearch-persistence/elasticsearch-persistence.gemspec create mode 100644 elasticsearch-persistence/lib/elasticsearch/persistence.rb create mode 100644 elasticsearch-persistence/lib/elasticsearch/persistence/version.rb create mode 100644 elasticsearch-persistence/test/test_helper.rb diff --git a/elasticsearch-persistence/.gitignore b/elasticsearch-persistence/.gitignore new file mode 100644 index 000000000..d87d4be66 --- /dev/null +++ b/elasticsearch-persistence/.gitignore @@ -0,0 +1,17 @@ +*.gem +*.rbc +.bundle +.config +.yardoc +Gemfile.lock +InstalledFiles +_yardoc +coverage +doc/ +lib/bundler/man +pkg +rdoc +spec/reports +test/tmp +test/version_tmp +tmp diff --git a/elasticsearch-persistence/Gemfile b/elasticsearch-persistence/Gemfile new file mode 100644 index 000000000..a60150cf4 --- /dev/null +++ b/elasticsearch-persistence/Gemfile @@ -0,0 +1,4 @@ +source 'https://rubygems.org' + +# Specify your gem's dependencies in elasticsearch-persistence.gemspec +gemspec diff --git a/elasticsearch-persistence/LICENSE.txt b/elasticsearch-persistence/LICENSE.txt new file mode 100644 index 000000000..489007102 --- /dev/null +++ b/elasticsearch-persistence/LICENSE.txt @@ -0,0 +1,13 @@ +Copyright (c) 2014 Elasticsearch + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. diff --git a/elasticsearch-persistence/README.md b/elasticsearch-persistence/README.md new file mode 100644 index 000000000..b0eda41b0 --- /dev/null +++ b/elasticsearch-persistence/README.md @@ -0,0 +1,21 @@ +# Elasticsearch::Persistence + +WIP> Persistence layer for Ruby domain objects using the Repository and ActiveRecord patterns + +## License + +This software is licensed under the Apache 2 license, quoted below. + + Copyright (c) 2014 Elasticsearch + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/elasticsearch-persistence/Rakefile b/elasticsearch-persistence/Rakefile new file mode 100644 index 000000000..303f43997 --- /dev/null +++ b/elasticsearch-persistence/Rakefile @@ -0,0 +1,57 @@ +require "bundler/gem_tasks" + +desc "Run unit tests" +task :default => 'test:unit' +task :test => 'test:unit' + +# ----- Test tasks ------------------------------------------------------------ + +require 'rake/testtask' +namespace :test do + task :ci_reporter do + ENV['CI_REPORTS'] ||= 'tmp/reports' + require 'ci/reporter/rake/minitest' + Rake::Task['ci:setup:minitest'].invoke + end + + Rake::TestTask.new(:unit) do |test| + Rake::Task['test:ci_reporter'].invoke if ENV['CI'] + test.libs << 'lib' << 'test' + test.test_files = FileList["test/unit/**/*_test.rb"] + # test.verbose = true + # test.warning = true + end + + Rake::TestTask.new(:integration) do |test| + Rake::Task['test:ci_reporter'].invoke if ENV['CI'] + test.libs << 'lib' << 'test' + test.test_files = FileList["test/integration/**/*_test.rb"] + end + + Rake::TestTask.new(:all) do |test| + Rake::Task['test:ci_reporter'].invoke if ENV['CI'] + test.libs << 'lib' << 'test' + test.test_files = FileList["test/unit/**/*_test.rb", "test/integration/**/*_test.rb"] + end +end + +# ----- Documentation tasks --------------------------------------------------- + +require 'yard' +YARD::Rake::YardocTask.new(:doc) do |t| + t.options = %w| --embed-mixins --markup=markdown | +end + +# ----- Code analysis tasks --------------------------------------------------- + +if defined?(RUBY_VERSION) && RUBY_VERSION > '1.9' + begin + require 'cane/rake_task' + Cane::RakeTask.new(:quality) do |cane| + cane.abc_max = 15 + cane.style_measure = 120 + end + rescue LoadError + warn "cane not available, quality task not provided." + end +end diff --git a/elasticsearch-persistence/elasticsearch-persistence.gemspec b/elasticsearch-persistence/elasticsearch-persistence.gemspec new file mode 100644 index 000000000..2a656f6b6 --- /dev/null +++ b/elasticsearch-persistence/elasticsearch-persistence.gemspec @@ -0,0 +1,39 @@ +# coding: utf-8 +lib = File.expand_path('../lib', __FILE__) +$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib) +require 'elasticsearch/persistence/version' + +Gem::Specification.new do |s| + s.name = "elasticsearch-persistence" + s.version = Elasticsearch::Persistence::VERSION + s.authors = ["Karel Minarik"] + s.email = ["karel.minarik@elasticsearch.org"] + s.description = "Persistence layer for Ruby models and Elasticsearch." + s.summary = "Persistence layer for Ruby models and Elasticsearch." + s.homepage = "https://github.com/elasticsearch/elasticsearch-rails/" + s.license = "Apache 2" + + s.files = `git ls-files -z`.split("\x0") + s.executables = s.files.grep(%r{^bin/}) { |f| File.basename(f) } + s.test_files = s.files.grep(%r{^(test|spec|features)/}) + s.require_paths = ["lib"] + + s.extra_rdoc_files = [ "README.md", "LICENSE.txt" ] + s.rdoc_options = [ "--charset=UTF-8" ] + + s.add_development_dependency "bundler", "~> 1.5" + s.add_development_dependency "rake" + + s.add_development_dependency "shoulda-context" + s.add_development_dependency "mocha" + s.add_development_dependency "turn" + s.add_development_dependency "yard" + s.add_development_dependency "ruby-prof" + s.add_development_dependency "pry" + s.add_development_dependency "ci_reporter" + + if defined?(RUBY_VERSION) && RUBY_VERSION > '1.9' + s.add_development_dependency "simplecov" + s.add_development_dependency "cane" + end +end diff --git a/elasticsearch-persistence/lib/elasticsearch/persistence.rb b/elasticsearch-persistence/lib/elasticsearch/persistence.rb new file mode 100644 index 000000000..03147e68e --- /dev/null +++ b/elasticsearch-persistence/lib/elasticsearch/persistence.rb @@ -0,0 +1,7 @@ +require "elasticsearch/persistence/version" + +module Elasticsearch + module Persistence + # Your code goes here... + end +end diff --git a/elasticsearch-persistence/lib/elasticsearch/persistence/version.rb b/elasticsearch-persistence/lib/elasticsearch/persistence/version.rb new file mode 100644 index 000000000..cf1e84452 --- /dev/null +++ b/elasticsearch-persistence/lib/elasticsearch/persistence/version.rb @@ -0,0 +1,5 @@ +module Elasticsearch + module Persistence + VERSION = "0.0.1" + end +end diff --git a/elasticsearch-persistence/test/test_helper.rb b/elasticsearch-persistence/test/test_helper.rb new file mode 100644 index 000000000..5a76e67ae --- /dev/null +++ b/elasticsearch-persistence/test/test_helper.rb @@ -0,0 +1,42 @@ +RUBY_1_8 = defined?(RUBY_VERSION) && RUBY_VERSION < '1.9' + +exit(0) if RUBY_1_8 + +require 'simplecov' and SimpleCov.start { add_filter "/test|test_/" } if ENV["COVERAGE"] + +# Register `at_exit` handler for integration tests shutdown. +# MUST be called before requiring `test/unit`. +at_exit { Elasticsearch::Test::IntegrationTestCase.__run_at_exit_hooks } if ENV['SERVER'] + +require 'test/unit' +require 'shoulda-context' +require 'mocha/setup' +require 'turn' unless ENV["TM_FILEPATH"] || ENV["NOTURN"] || RUBY_1_8 + +require 'ansi' +require 'oj' + +require 'elasticsearch/extensions/test/cluster' +require 'elasticsearch/extensions/test/startup_shutdown' + +require 'elasticsearch/persistence' + +module Elasticsearch + module Test + class IntegrationTestCase < ::Test::Unit::TestCase + extend Elasticsearch::Extensions::Test::StartupShutdown + + startup { Elasticsearch::Extensions::Test::Cluster.start(nodes: 1) if ENV['SERVER'] and not Elasticsearch::Extensions::Test::Cluster.running? } + shutdown { Elasticsearch::Extensions::Test::Cluster.stop if ENV['SERVER'] && started? } + context "IntegrationTest" do; should "noop on Ruby 1.8" do; end; end if RUBY_1_8 + + def setup + tracer = ::Logger.new(STDERR) + tracer.formatter = lambda { |s, d, p, m| "#{m.gsub(/^.*$/) { |n| ' ' + n }.ansi(:faint)}\n" } + Elasticsearch::Persistence.client = Elasticsearch::Client.new \ + host: "localhost:#{(ENV['TEST_CLUSTER_PORT'] || 9250)}", + tracer: (ENV['QUIET'] ? nil : tracer) + end + end + end +end From 2931bb93c9a3b5e5b7843326c035e1696b7ee586 Mon Sep 17 00:00:00 2001 From: Karel Minarik Date: Tue, 25 Mar 2014 21:11:22 +0100 Subject: [PATCH 02/27] [STORE] Added the Persistence::Repository module and client integration --- .../elasticsearch-persistence.gemspec | 4 +++ .../lib/elasticsearch/persistence.rb | 21 +++++++++++++-- .../lib/elasticsearch/persistence/client.rb | 15 +++++++++++ .../elasticsearch/persistence/repository.rb | 8 ++++++ .../test/unit/persistence_test.rb | 23 ++++++++++++++++ .../test/unit/repository_client_test.rb | 27 +++++++++++++++++++ .../test/unit/repository_module_test.rb | 6 +++++ 7 files changed, 102 insertions(+), 2 deletions(-) create mode 100644 elasticsearch-persistence/lib/elasticsearch/persistence/client.rb create mode 100644 elasticsearch-persistence/lib/elasticsearch/persistence/repository.rb create mode 100644 elasticsearch-persistence/test/unit/persistence_test.rb create mode 100644 elasticsearch-persistence/test/unit/repository_client_test.rb create mode 100644 elasticsearch-persistence/test/unit/repository_module_test.rb diff --git a/elasticsearch-persistence/elasticsearch-persistence.gemspec b/elasticsearch-persistence/elasticsearch-persistence.gemspec index 2a656f6b6..dddd98849 100644 --- a/elasticsearch-persistence/elasticsearch-persistence.gemspec +++ b/elasticsearch-persistence/elasticsearch-persistence.gemspec @@ -21,9 +21,13 @@ Gem::Specification.new do |s| s.extra_rdoc_files = [ "README.md", "LICENSE.txt" ] s.rdoc_options = [ "--charset=UTF-8" ] + s.add_dependency "elasticsearch", '> 0.4' + s.add_development_dependency "bundler", "~> 1.5" s.add_development_dependency "rake" + s.add_development_dependency "elasticsearch-extensions" + s.add_development_dependency "shoulda-context" s.add_development_dependency "mocha" s.add_development_dependency "turn" diff --git a/elasticsearch-persistence/lib/elasticsearch/persistence.rb b/elasticsearch-persistence/lib/elasticsearch/persistence.rb index 03147e68e..1e060a315 100644 --- a/elasticsearch-persistence/lib/elasticsearch/persistence.rb +++ b/elasticsearch-persistence/lib/elasticsearch/persistence.rb @@ -1,7 +1,24 @@ -require "elasticsearch/persistence/version" +require 'elasticsearch' + +require 'elasticsearch/persistence/version' + +require 'elasticsearch/persistence/client' +require 'elasticsearch/persistence/repository' module Elasticsearch module Persistence - # Your code goes here... + + # :nodoc: + module ClassMethods + def client + @client ||= Elasticsearch::Client.new + end + + def client=(client) + @client = client + end + end + + extend ClassMethods end end diff --git a/elasticsearch-persistence/lib/elasticsearch/persistence/client.rb b/elasticsearch-persistence/lib/elasticsearch/persistence/client.rb new file mode 100644 index 000000000..c7a8bfa51 --- /dev/null +++ b/elasticsearch-persistence/lib/elasticsearch/persistence/client.rb @@ -0,0 +1,15 @@ +module Elasticsearch + module Persistence + + module Client + def client client=nil + @client ||= Elasticsearch::Persistence.client + end + + def client=(client) + @client = client + end + end + + end +end diff --git a/elasticsearch-persistence/lib/elasticsearch/persistence/repository.rb b/elasticsearch-persistence/lib/elasticsearch/persistence/repository.rb new file mode 100644 index 000000000..2fff43e22 --- /dev/null +++ b/elasticsearch-persistence/lib/elasticsearch/persistence/repository.rb @@ -0,0 +1,8 @@ +module Elasticsearch + module Persistence + + module Repository + include Elasticsearch::Persistence::Client + end + end +end diff --git a/elasticsearch-persistence/test/unit/persistence_test.rb b/elasticsearch-persistence/test/unit/persistence_test.rb new file mode 100644 index 000000000..9381203d3 --- /dev/null +++ b/elasticsearch-persistence/test/unit/persistence_test.rb @@ -0,0 +1,23 @@ +require 'test_helper' + +class Elasticsearch::Persistence::ModuleTest < Test::Unit::TestCase + context "The Persistence module" do + + context "client" do + should "have a default client" do + client = Elasticsearch::Persistence.client + assert_not_nil client + assert_instance_of Elasticsearch::Transport::Client, client + end + + should "allow to set a client" do + begin + Elasticsearch::Persistence.client = "Foobar" + assert_equal "Foobar", Elasticsearch::Persistence.client + ensure + Elasticsearch::Persistence.client = nil + end + end + end + end +end diff --git a/elasticsearch-persistence/test/unit/repository_client_test.rb b/elasticsearch-persistence/test/unit/repository_client_test.rb new file mode 100644 index 000000000..246631afc --- /dev/null +++ b/elasticsearch-persistence/test/unit/repository_client_test.rb @@ -0,0 +1,27 @@ +require 'test_helper' + +class Elasticsearch::Persistence::RepositoryClientTest < Test::Unit::TestCase + context "A repository client" do + class DummyReposistory + include Elasticsearch::Persistence::Repository + end + + setup do + @shoulda_subject = DummyReposistory.new + end + + should "have a default client" do + assert_not_nil subject.client + assert_instance_of Elasticsearch::Transport::Client, subject.client + end + + should "allow to set a client" do + begin + subject.client = "Foobar" + assert_equal "Foobar", subject.client + ensure + subject.client = nil + end + end + end +end diff --git a/elasticsearch-persistence/test/unit/repository_module_test.rb b/elasticsearch-persistence/test/unit/repository_module_test.rb new file mode 100644 index 000000000..a31d7ac84 --- /dev/null +++ b/elasticsearch-persistence/test/unit/repository_module_test.rb @@ -0,0 +1,6 @@ +require 'test_helper' + +class Elasticsearch::Persistence::RepositoryModuleTest < Test::Unit::TestCase + context "The repository module" do + end +end From 2eacd4156ebe1dec2730cfdbf0e1020aa35ab7cd Mon Sep 17 00:00:00 2001 From: Karel Minarik Date: Sat, 29 Mar 2014 10:56:21 +0100 Subject: [PATCH 03/27] [STORE] Added the default `Repository::Class` for convenience Instead of: class MyRepository include Elasticsearch::Persistence::Repository end repository = MyRepository.new you can do: repository = Elasticsearch::Persistence::Repository.new The module function `new` returns an Elasticsearch::Persistence::Repository::Class instance. --- .../lib/elasticsearch/persistence.rb | 2 ++ .../elasticsearch/persistence/repository.rb | 4 +++ .../persistence/repository/class.rb | 18 +++++++++++ .../test/unit/repository_class_test.rb | 32 +++++++++++++++++++ 4 files changed, 56 insertions(+) create mode 100644 elasticsearch-persistence/lib/elasticsearch/persistence/repository/class.rb create mode 100644 elasticsearch-persistence/test/unit/repository_class_test.rb diff --git a/elasticsearch-persistence/lib/elasticsearch/persistence.rb b/elasticsearch-persistence/lib/elasticsearch/persistence.rb index 1e060a315..021c97473 100644 --- a/elasticsearch-persistence/lib/elasticsearch/persistence.rb +++ b/elasticsearch-persistence/lib/elasticsearch/persistence.rb @@ -5,6 +5,8 @@ require 'elasticsearch/persistence/client' require 'elasticsearch/persistence/repository' +require 'elasticsearch/persistence/repository/class' + module Elasticsearch module Persistence diff --git a/elasticsearch-persistence/lib/elasticsearch/persistence/repository.rb b/elasticsearch-persistence/lib/elasticsearch/persistence/repository.rb index 2fff43e22..c7e8a1d49 100644 --- a/elasticsearch-persistence/lib/elasticsearch/persistence/repository.rb +++ b/elasticsearch-persistence/lib/elasticsearch/persistence/repository.rb @@ -3,6 +3,10 @@ module Persistence module Repository include Elasticsearch::Persistence::Client + + def new(options={}, &block) + Elasticsearch::Persistence::Repository::Class.new options, &block + end; module_function :new end end end diff --git a/elasticsearch-persistence/lib/elasticsearch/persistence/repository/class.rb b/elasticsearch-persistence/lib/elasticsearch/persistence/repository/class.rb new file mode 100644 index 000000000..7ff11e828 --- /dev/null +++ b/elasticsearch-persistence/lib/elasticsearch/persistence/repository/class.rb @@ -0,0 +1,18 @@ +module Elasticsearch + module Persistence + module Repository + + class Class + include Elasticsearch::Persistence::Repository + + attr_reader :options + + def initialize(options={}, &block) + @options = options + block.arity < 1 ? instance_eval(&block) : block.call(self) if block_given? + end + end + + end + end +end diff --git a/elasticsearch-persistence/test/unit/repository_class_test.rb b/elasticsearch-persistence/test/unit/repository_class_test.rb new file mode 100644 index 000000000..b20ac3e8b --- /dev/null +++ b/elasticsearch-persistence/test/unit/repository_class_test.rb @@ -0,0 +1,32 @@ +require 'test_helper' + +class Elasticsearch::Persistence::RepositoryClassTest < Test::Unit::TestCase + context "The default repository class" do + + should "be created from the module" do + repository = Elasticsearch::Persistence::Repository.new + assert_instance_of Elasticsearch::Persistence::Repository::Class, repository + end + + should "store and access the options" do + repository = Elasticsearch::Persistence::Repository::Class.new foo: 'bar' + assert_equal 'bar', repository.options[:foo] + end + + should "instance eval a passed block" do + $foo = 100 + repository = Elasticsearch::Persistence::Repository::Class.new() { $foo += 1 } + assert_equal 101, $foo + end + + should "call a passed block with self" do + foo = 100 + repository = Elasticsearch::Persistence::Repository::Class.new do |r| + assert_instance_of Elasticsearch::Persistence::Repository::Class, r + foo += 1 + end + assert_equal 101, foo + end + + end +end From 9ddafe7c2efaf1d06a1f2bb79c17e3991a9db239 Mon Sep 17 00:00:00 2001 From: Karel Minarik Date: Sat, 29 Mar 2014 14:11:23 +0100 Subject: [PATCH 04/27] [STORE] Added the `Naming` module Provides getting Ruby class from Elasticsearch type and vice versa, ID from the document (Hash), as well as setting the `klass` for the whole Repository instance. --- .../elasticsearch-persistence.gemspec | 1 + .../lib/elasticsearch/persistence.rb | 3 + .../elasticsearch/persistence/repository.rb | 1 + .../persistence/repository/naming.rb | 32 ++++++++++ .../test/unit/repository_naming_test.rb | 64 +++++++++++++++++++ 5 files changed, 101 insertions(+) create mode 100644 elasticsearch-persistence/lib/elasticsearch/persistence/repository/naming.rb create mode 100644 elasticsearch-persistence/test/unit/repository_naming_test.rb diff --git a/elasticsearch-persistence/elasticsearch-persistence.gemspec b/elasticsearch-persistence/elasticsearch-persistence.gemspec index dddd98849..aa7647452 100644 --- a/elasticsearch-persistence/elasticsearch-persistence.gemspec +++ b/elasticsearch-persistence/elasticsearch-persistence.gemspec @@ -22,6 +22,7 @@ Gem::Specification.new do |s| s.rdoc_options = [ "--charset=UTF-8" ] s.add_dependency "elasticsearch", '> 0.4' + s.add_dependency "active_support" s.add_development_dependency "bundler", "~> 1.5" s.add_development_dependency "rake" diff --git a/elasticsearch-persistence/lib/elasticsearch/persistence.rb b/elasticsearch-persistence/lib/elasticsearch/persistence.rb index 021c97473..f629f0f58 100644 --- a/elasticsearch-persistence/lib/elasticsearch/persistence.rb +++ b/elasticsearch-persistence/lib/elasticsearch/persistence.rb @@ -1,8 +1,11 @@ require 'elasticsearch' +require 'active_support/inflector' + require 'elasticsearch/persistence/version' require 'elasticsearch/persistence/client' +require 'elasticsearch/persistence/repository/naming' require 'elasticsearch/persistence/repository' require 'elasticsearch/persistence/repository/class' diff --git a/elasticsearch-persistence/lib/elasticsearch/persistence/repository.rb b/elasticsearch-persistence/lib/elasticsearch/persistence/repository.rb index c7e8a1d49..7855db7da 100644 --- a/elasticsearch-persistence/lib/elasticsearch/persistence/repository.rb +++ b/elasticsearch-persistence/lib/elasticsearch/persistence/repository.rb @@ -3,6 +3,7 @@ module Persistence module Repository include Elasticsearch::Persistence::Client + include Elasticsearch::Persistence::Repository::Naming def new(options={}, &block) Elasticsearch::Persistence::Repository::Class.new options, &block diff --git a/elasticsearch-persistence/lib/elasticsearch/persistence/repository/naming.rb b/elasticsearch-persistence/lib/elasticsearch/persistence/repository/naming.rb new file mode 100644 index 000000000..1d00a575d --- /dev/null +++ b/elasticsearch-persistence/lib/elasticsearch/persistence/repository/naming.rb @@ -0,0 +1,32 @@ +module Elasticsearch + module Persistence + module Repository + + module Naming + def klass + @klass + end + + def klass=klass + @klass = klass + end + + 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 + + def __get_type_from_class(klass) + klass.to_s.underscore + end + + def __get_id_from_document(document) + document[:id] || document['id'] || document[:_id] || document['_id'] + end + end + + end + end +end diff --git a/elasticsearch-persistence/test/unit/repository_naming_test.rb b/elasticsearch-persistence/test/unit/repository_naming_test.rb new file mode 100644 index 000000000..24b290177 --- /dev/null +++ b/elasticsearch-persistence/test/unit/repository_naming_test.rb @@ -0,0 +1,64 @@ +require 'test_helper' + +class Elasticsearch::Persistence::RepositoryNamingTest < Test::Unit::TestCase + context "The repository naming" do + # Fake class for the naming tests + class ::Foobar; end + class ::FooBar; end + module ::Foo; class Bar; end; end + + setup do + @shoulda_subject = Class.new() { include Elasticsearch::Persistence::Repository::Naming }.new + end + + context "get Ruby class from the Elasticsearch type" do + should "get a simple class" do + assert_equal Foobar, subject.__get_klass_from_type('foobar') + end + should "get a camelcased class" do + assert_equal FooBar, subject.__get_klass_from_type('foo_bar') + end + should "get a namespaced class" do + assert_equal Foo::Bar, subject.__get_klass_from_type('foo/bar') + end + should "re-raise a NameError exception" do + assert_raise NameError do + subject.__get_klass_from_type('foobarbazbam') + end + end + end + + context "get Elasticsearch type from the Ruby class" do + should "encode a simple class" do + assert_equal 'foobar', subject.__get_type_from_class(Foobar) + end + should "encode a camelcased class" do + assert_equal 'foo_bar', subject.__get_type_from_class(FooBar) + end + should "encode a namespaced class" do + assert_equal 'foo/bar', subject.__get_type_from_class(Foo::Bar) + end + end + + context "get an ID from the document" do + should "get an ID from Hash" do + assert_equal 1, subject.__get_id_from_document(id: 1) + assert_equal 1, subject.__get_id_from_document(_id: 1) + assert_equal 1, subject.__get_id_from_document('id' => 1) + assert_equal 1, subject.__get_id_from_document('_id' => 1) + end + end + + context " document class name" do + should "be nil by default" do + assert_nil subject.klass + end + + should "be settable" do + subject.klass = Foobar + assert_equal Foobar, subject.klass + end + end + + end +end From dc664225802c15ae9679e64b32539da20a88b840 Mon Sep 17 00:00:00 2001 From: Karel Minarik Date: Sat, 29 Mar 2014 16:37:00 +0100 Subject: [PATCH 05/27] [STORE] Added the `Serialize` module The repository uses two symmetric methods: `serialize` and `deserialize` to convert documents when: 1. passing them to the storage, 2. initializing Ruby objects when retrieving documents from storage Every repository can easily customize (overload) these methods, to provide (de)serialization for complex use-cases, such as storing PDF files or images in the storage. See: * https://www.braintreepayments.com/braintrust/untangle-domain-and-persistence-logic-with-curator * https://github.com/braintree/curator/blob/master/lib/curator/repository.rb --- .../lib/elasticsearch/persistence.rb | 1 + .../elasticsearch/persistence/repository.rb | 1 + .../persistence/repository/serialize.rb | 18 ++++++ .../test/unit/repository_serialize_test.rb | 57 +++++++++++++++++++ 4 files changed, 77 insertions(+) create mode 100644 elasticsearch-persistence/lib/elasticsearch/persistence/repository/serialize.rb create mode 100644 elasticsearch-persistence/test/unit/repository_serialize_test.rb diff --git a/elasticsearch-persistence/lib/elasticsearch/persistence.rb b/elasticsearch-persistence/lib/elasticsearch/persistence.rb index f629f0f58..6975f358e 100644 --- a/elasticsearch-persistence/lib/elasticsearch/persistence.rb +++ b/elasticsearch-persistence/lib/elasticsearch/persistence.rb @@ -6,6 +6,7 @@ require 'elasticsearch/persistence/client' require 'elasticsearch/persistence/repository/naming' +require 'elasticsearch/persistence/repository/serialize' require 'elasticsearch/persistence/repository' require 'elasticsearch/persistence/repository/class' diff --git a/elasticsearch-persistence/lib/elasticsearch/persistence/repository.rb b/elasticsearch-persistence/lib/elasticsearch/persistence/repository.rb index 7855db7da..2e216200e 100644 --- a/elasticsearch-persistence/lib/elasticsearch/persistence/repository.rb +++ b/elasticsearch-persistence/lib/elasticsearch/persistence/repository.rb @@ -4,6 +4,7 @@ module Persistence module Repository include Elasticsearch::Persistence::Client include Elasticsearch::Persistence::Repository::Naming + include Elasticsearch::Persistence::Repository::Serialize def new(options={}, &block) Elasticsearch::Persistence::Repository::Class.new options, &block diff --git a/elasticsearch-persistence/lib/elasticsearch/persistence/repository/serialize.rb b/elasticsearch-persistence/lib/elasticsearch/persistence/repository/serialize.rb new file mode 100644 index 000000000..5e4b4be00 --- /dev/null +++ b/elasticsearch-persistence/lib/elasticsearch/persistence/repository/serialize.rb @@ -0,0 +1,18 @@ +module Elasticsearch + module Persistence + module Repository + + module Serialize + def serialize(document) + document.to_hash + end + + def deserialize(document) + _klass = klass || __get_klass_from_type(document['_type']) + _klass.new document['_source'] + end + end + + end + end +end diff --git a/elasticsearch-persistence/test/unit/repository_serialize_test.rb b/elasticsearch-persistence/test/unit/repository_serialize_test.rb new file mode 100644 index 000000000..48d0323d8 --- /dev/null +++ b/elasticsearch-persistence/test/unit/repository_serialize_test.rb @@ -0,0 +1,57 @@ +require 'test_helper' + +class Elasticsearch::Persistence::RepositorySerializeTest < Test::Unit::TestCase + context "The repository serialization" do + class DummyDocument + def to_hash + { foo: 'bar' } + end + end + + class MyDocument; end + + setup do + @shoulda_subject = Class.new() { include Elasticsearch::Persistence::Repository::Serialize }.new + end + + context "serialize" do + should "call #to_hash on passed object" do + document = DummyDocument.new + assert_equal( { foo: 'bar' }, subject.serialize(document)) + end + end + + context "deserialize" do + should "get the class name from #klass" do + subject.expects(:klass) + .returns(MyDocument) + + MyDocument.expects(:new) + + subject.deserialize( {} ) + end + + should "get the class name from Elasticsearch _type" do + subject.expects(:klass) + .returns(nil) + + subject.expects(:__get_klass_from_type) + .returns(MyDocument) + + MyDocument.expects(:new) + + subject.deserialize( {} ) + end + + should "create the class instance with _source attributes" do + subject.expects(:klass).returns(nil) + + subject.expects(:__get_klass_from_type).returns(MyDocument) + + MyDocument.expects(:new).with({ 'foo' => 'bar' }) + + subject.deserialize( {'_source' => { 'foo' => 'bar' } } ) + end + end + end +end From b3c190a1db703737aee9d4c69acb9c933318317b Mon Sep 17 00:00:00 2001 From: Karel Minarik Date: Sat, 29 Mar 2014 17:28:24 +0100 Subject: [PATCH 06/27] [STORE] Added the `Store` module The `Store` module saves and deletes the documents in Elasticsearch via the `save` and `delete` methods. --- .../lib/elasticsearch/persistence.rb | 1 + .../elasticsearch/persistence/repository.rb | 1 + .../persistence/repository/store.rb | 28 ++++ .../test/unit/repository_store_test.rb | 120 ++++++++++++++++++ 4 files changed, 150 insertions(+) create mode 100644 elasticsearch-persistence/lib/elasticsearch/persistence/repository/store.rb create mode 100644 elasticsearch-persistence/test/unit/repository_store_test.rb diff --git a/elasticsearch-persistence/lib/elasticsearch/persistence.rb b/elasticsearch-persistence/lib/elasticsearch/persistence.rb index 6975f358e..60df2c53d 100644 --- a/elasticsearch-persistence/lib/elasticsearch/persistence.rb +++ b/elasticsearch-persistence/lib/elasticsearch/persistence.rb @@ -7,6 +7,7 @@ require 'elasticsearch/persistence/client' require 'elasticsearch/persistence/repository/naming' require 'elasticsearch/persistence/repository/serialize' +require 'elasticsearch/persistence/repository/store' require 'elasticsearch/persistence/repository' require 'elasticsearch/persistence/repository/class' diff --git a/elasticsearch-persistence/lib/elasticsearch/persistence/repository.rb b/elasticsearch-persistence/lib/elasticsearch/persistence/repository.rb index 2e216200e..8877512be 100644 --- a/elasticsearch-persistence/lib/elasticsearch/persistence/repository.rb +++ b/elasticsearch-persistence/lib/elasticsearch/persistence/repository.rb @@ -5,6 +5,7 @@ module Repository include Elasticsearch::Persistence::Client include Elasticsearch::Persistence::Repository::Naming include Elasticsearch::Persistence::Repository::Serialize + include Elasticsearch::Persistence::Repository::Store def new(options={}, &block) Elasticsearch::Persistence::Repository::Class.new options, &block diff --git a/elasticsearch-persistence/lib/elasticsearch/persistence/repository/store.rb b/elasticsearch-persistence/lib/elasticsearch/persistence/repository/store.rb new file mode 100644 index 000000000..95cbff9ee --- /dev/null +++ b/elasticsearch-persistence/lib/elasticsearch/persistence/repository/store.rb @@ -0,0 +1,28 @@ +module Elasticsearch + module Persistence + module Repository + + module Store + def save(document, options={}) + serialized = serialize(document) + id = __get_id_from_document(serialized) + type = klass || __get_type_from_class(document.class) + client.index( { index: 'test', type: type, id: id, body: serialized }.merge(options) ) + end + + def delete(document, options={}) + if document.is_a?(String) || document.is_a?(Integer) + id = document + type = klass + else + serialized = serialize(document) + id = __get_id_from_document(serialized) + type = klass || __get_type_from_class(document.class) + end + client.delete( { index: 'test', type: type, id: id }.merge(options) ) + end + end + + end + end +end diff --git a/elasticsearch-persistence/test/unit/repository_store_test.rb b/elasticsearch-persistence/test/unit/repository_store_test.rb new file mode 100644 index 000000000..463bf2915 --- /dev/null +++ b/elasticsearch-persistence/test/unit/repository_store_test.rb @@ -0,0 +1,120 @@ +require 'test_helper' + +class Elasticsearch::Persistence::RepositoryStoreTest < Test::Unit::TestCase + context "The repository store" do + class MyDocument; end + + setup do + @shoulda_subject = Class.new() { include Elasticsearch::Persistence::Repository::Store }.new + end + + context "save" do + should "serialize the document, get type from klass and index it" do + subject.expects(:serialize).returns({foo: 'bar'}) + subject.expects(:klass).returns('foo_type') + subject.expects(:__get_id_from_document).returns('1') + + client = mock + client.expects(:index).with do |arguments| + assert_equal 'foo_type', arguments[:type] + assert_equal '1', arguments[:id] + assert_equal({foo: 'bar'}, arguments[:body]) + end + subject.expects(:client).returns(client) + + subject.save({foo: 'bar'}) + end + + should "serialize the document, get type from document class and index it" do + subject.expects(:serialize).returns({foo: 'bar'}) + subject.expects(:klass).returns(nil) + subject.expects(:__get_type_from_class).with(MyDocument).returns('my_document') + subject.expects(:__get_id_from_document).returns('1') + + client = mock + client.expects(:index).with do |arguments| + assert_equal 'my_document', arguments[:type] + assert_equal '1', arguments[:id] + assert_equal({foo: 'bar'}, arguments[:body]) + end + subject.expects(:client).returns(client) + + subject.save(MyDocument.new) + end + + should "pass the options to the client" do + subject.expects(:serialize).returns({foo: 'bar'}) + subject.expects(:klass).returns('foo') + subject.expects(:__get_id_from_document).returns('1') + + client = mock + client.expects(:index).with do |arguments| + assert_equal 'bambam', arguments[:routing] + end + subject.expects(:client).returns(client) + + subject.save({foo: 'bar'}, routing: 'bambam') + end + end + + context "delete" do + should "get type from klass when passed only ID" do + subject.expects(:serialize).never + subject.expects(:klass).returns('foo_type') + subject.expects(:__get_id_from_document).never + + client = mock + client.expects(:delete).with do |arguments| + assert_equal 'foo_type', arguments[:type] + assert_equal '1', arguments[:id] + end + subject.expects(:client).returns(client) + + subject.delete('1') + end + + should "get ID from document and type from klass when passed a document" do + subject.expects(:serialize).returns({id: '1', foo: 'bar'}) + subject.expects(:klass).returns('foo_type') + subject.expects(:__get_id_from_document).with({id: '1', foo: 'bar'}).returns('1') + + client = mock + client.expects(:delete).with do |arguments| + assert_equal 'foo_type', arguments[:type] + assert_equal '1', arguments[:id] + end + subject.expects(:client).returns(client) + + subject.delete({id: '1', foo: 'bar'}) + end + + should "get ID and type from document when passed a document" do + subject.expects(:serialize).returns({id: '1', foo: 'bar'}) + subject.expects(:klass).returns(nil) + subject.expects(:__get_id_from_document).with({id: '1', foo: 'bar'}).returns('1') + subject.expects(:__get_type_from_class).with(MyDocument).returns('my_document') + + client = mock + client.expects(:delete).with do |arguments| + assert_equal 'my_document', arguments[:type] + assert_equal '1', arguments[:id] + end + subject.expects(:client).returns(client) + + subject.delete(MyDocument.new) + end + + should "pass the options to the client" do + subject.expects(:klass).returns('foo') + + client = mock + client.expects(:delete).with do |arguments| + assert_equal 'bambam', arguments[:routing] + end + subject.expects(:client).returns(client) + + subject.delete('1', routing: 'bambam') + end + end + end +end From 413fc8ead5b0f30f86fc7be99f7357c66b2964cb Mon Sep 17 00:00:00 2001 From: Karel Minarik Date: Mon, 31 Mar 2014 12:35:13 +0200 Subject: [PATCH 07/27] [STORE] Added the `Find` module The module provides method to find one or multiple documents and to check for documents existence. The methods return `deserialize`-d Ruby objects based on `klass` or the document Elasticsearch `_type`. Missing documents are kept in the resulting Array as `nil` objects. --- .../lib/elasticsearch/persistence.rb | 1 + .../elasticsearch/persistence/repository.rb | 1 + .../persistence/repository/find.rb | 43 +++ .../test/unit/repository_find_test.rb | 311 ++++++++++++++++++ 4 files changed, 356 insertions(+) create mode 100644 elasticsearch-persistence/lib/elasticsearch/persistence/repository/find.rb create mode 100644 elasticsearch-persistence/test/unit/repository_find_test.rb diff --git a/elasticsearch-persistence/lib/elasticsearch/persistence.rb b/elasticsearch-persistence/lib/elasticsearch/persistence.rb index 60df2c53d..19c997984 100644 --- a/elasticsearch-persistence/lib/elasticsearch/persistence.rb +++ b/elasticsearch-persistence/lib/elasticsearch/persistence.rb @@ -8,6 +8,7 @@ require 'elasticsearch/persistence/repository/naming' require 'elasticsearch/persistence/repository/serialize' require 'elasticsearch/persistence/repository/store' +require 'elasticsearch/persistence/repository/find' require 'elasticsearch/persistence/repository' require 'elasticsearch/persistence/repository/class' diff --git a/elasticsearch-persistence/lib/elasticsearch/persistence/repository.rb b/elasticsearch-persistence/lib/elasticsearch/persistence/repository.rb index 8877512be..8ead25dd3 100644 --- a/elasticsearch-persistence/lib/elasticsearch/persistence/repository.rb +++ b/elasticsearch-persistence/lib/elasticsearch/persistence/repository.rb @@ -6,6 +6,7 @@ module Repository include Elasticsearch::Persistence::Repository::Naming include Elasticsearch::Persistence::Repository::Serialize include Elasticsearch::Persistence::Repository::Store + include Elasticsearch::Persistence::Repository::Find def new(options={}, &block) Elasticsearch::Persistence::Repository::Class.new options, &block diff --git a/elasticsearch-persistence/lib/elasticsearch/persistence/repository/find.rb b/elasticsearch-persistence/lib/elasticsearch/persistence/repository/find.rb new file mode 100644 index 000000000..07ef337a2 --- /dev/null +++ b/elasticsearch-persistence/lib/elasticsearch/persistence/repository/find.rb @@ -0,0 +1,43 @@ +module Elasticsearch + module Persistence + module Repository + class DocumentNotFound < StandardError; end + + module Find + def find(*args) + options = args.last.is_a?(Hash) ? args.pop : {} + ids = args + + if args.size == 1 + id = args.pop + id.is_a?(Array) ? __find_many(id, options) : __find_one(id, options) + else + __find_many args, options + end + end + + def exists?(id, options={}) + type = (klass ? __get_type_from_class(klass) : '_all') + client.exists( { index: 'test', type: type, id: id }.merge(options) ) + end + + def __find_one(id, options={}) + type = (klass ? __get_type_from_class(klass) : '_all') + document = client.get( { index: 'test', type: type, id: id }.merge(options) ) + + deserialize(document) + rescue Elasticsearch::Transport::Transport::Errors::NotFound => e + raise DocumentNotFound, e.message, caller + end + + def __find_many(ids, options={}) + type = (klass ? __get_type_from_class(klass) : '_all') + documents = client.mget( { index: 'test', type: type, body: { ids: ids } }.merge(options) ) + + documents['docs'].map { |document| document['found'] ? deserialize(document) : nil } + end + end + + end + end +end diff --git a/elasticsearch-persistence/test/unit/repository_find_test.rb b/elasticsearch-persistence/test/unit/repository_find_test.rb new file mode 100644 index 000000000..9fa8bb87e --- /dev/null +++ b/elasticsearch-persistence/test/unit/repository_find_test.rb @@ -0,0 +1,311 @@ +require 'test_helper' + +class Elasticsearch::Persistence::RepositoryFindTest < Test::Unit::TestCase + class MyDocument; end + + context "The repository" do + setup do + @shoulda_subject = Class.new() { include Elasticsearch::Persistence::Repository::Find }.new + + @client = mock + @shoulda_subject.stubs(:klass).returns(nil) + @shoulda_subject.stubs(:client).returns(@client) + end + + context "find method" do + should "find one document when passed a single, literal ID" do + subject.expects(:__find_one).with(1, {}) + subject.find(1) + end + + should "find multiple documents when passed multiple IDs" do + subject.expects(:__find_many).with([1, 2], {}) + subject.find(1, 2) + end + + should "find multiple documents when passed an array of IDs" do + subject.expects(:__find_many).with([1, 2], {}) + subject.find([1, 2]) + end + + should "pass the options" do + subject.expects(:__find_one).with(1, { foo: 'bar' }) + subject.find(1, foo: 'bar') + + subject.expects(:__find_many).with([1, 2], { foo: 'bar' }) + subject.find([1, 2], foo: 'bar') + + subject.expects(:__find_many).with([1, 2], { foo: 'bar' }) + subject.find(1, 2, foo: 'bar') + end + end + + context "'exists?' method" do + should "return false when the document does not exist" do + @client.expects(:exists).returns(false) + assert_equal false, subject.exists?('1') + end + + should "return whether document for klass exists" do + subject.expects(:klass).returns(MyDocument).at_least_once + subject.expects(:__get_type_from_class).with(MyDocument).returns('my_document') + + @client + .expects(:exists) + .with do |arguments| + assert_equal 'my_document', arguments[:type] + assert_equal '1', arguments[:id] + end + .returns(true) + + assert_equal true, subject.exists?('1') + end + + should "return whether document exists" do + subject.expects(:klass).returns(nil) + subject.expects(:__get_type_from_class).never + + @client + .expects(:exists) + .with do |arguments| + assert_equal '_all', arguments[:type] + assert_equal '1', arguments[:id] + end + .returns(true) + + assert_equal true, subject.exists?('1') + end + + should "pass options to the client" do + @client.expects(:exists).with do |arguments| + assert_equal 'bambam', arguments[:routing] + end + + subject.exists? '1', routing: 'bambam' + end + end + + context "'__find_one' method" do + should "find document based on klass and return a deserialized object" do + subject.expects(:klass).returns(MyDocument).at_least_once + subject.expects(:__get_type_from_class).with(MyDocument).returns('my_document') + + subject.expects(:deserialize).with({'_source' => {'foo' => 'bar'}}).returns(MyDocument.new) + + @client + .expects(:get) + .with do |arguments| + assert_equal 'my_document', arguments[:type] + assert_equal '1', arguments[:id] + end + .returns({'_source' => { 'foo' => 'bar' }}) + + assert_instance_of MyDocument, subject.__find_one('1') + end + + should "find document and return a deserialized object" do + subject.expects(:klass).returns(nil).at_least_once + subject.expects(:__get_type_from_class).never + + subject.expects(:deserialize).with({'_source' => {'foo' => 'bar'}}).returns(MyDocument.new) + + @client + .expects(:get) + .with do |arguments| + assert_equal '_all', arguments[:type] + assert_equal '1', arguments[:id] + end + .returns({'_source' => { 'foo' => 'bar' }}) + + assert_instance_of MyDocument, subject.__find_one('1') + end + + should "raise DocumentNotFound exception when the document cannot be found" do + subject.expects(:klass).returns(nil).at_least_once + + subject.expects(:deserialize).never + + @client + .expects(:get) + .raises(Elasticsearch::Transport::Transport::Errors::NotFound) + + assert_raise Elasticsearch::Persistence::Repository::DocumentNotFound do + subject.__find_one('foobar') + end + end + + should "pass other exceptions" do + subject.expects(:klass).returns(nil).at_least_once + + subject.expects(:deserialize).never + + @client + .expects(:get) + .raises(RuntimeError) + + assert_raise RuntimeError do + subject.__find_one('foobar') + end + end + + should "pass options to the client" do + subject.expects(:klass).returns(nil).at_least_once + subject.expects(:deserialize) + + @client + .expects(:get) + .with do |arguments| + assert_equal 'bambam', arguments[:routing] + end + .returns({'_source' => { 'foo' => 'bar' }}) + + subject.__find_one '1', routing: 'bambam' + end + end + + context "'__find_many' method" do + setup do + @response = {"docs"=> + [ {"_index"=>"test", + "_type"=>"note", + "_id"=>"1", + "_version"=>1, + "found"=>true, + "_source"=>{"id"=>"1", "title"=>"Test 1"}}, + + {"_index"=>"test", + "_type"=>"note", + "_id"=>"2", + "_version"=>1, + "found"=>true, + "_source"=>{"id"=>"2", "title"=>"Test 2"}} + ]} + end + + should "find documents based on klass and return an Array of deserialized objects" do + subject.expects(:klass).returns(MyDocument).at_least_once + subject.expects(:__get_type_from_class).with(MyDocument).returns('my_document') + + subject + .expects(:deserialize) + .with(@response['docs'][0]) + .returns(MyDocument.new) + + subject + .expects(:deserialize) + .with(@response['docs'][1]) + .returns(MyDocument.new) + + @client + .expects(:mget) + .with do |arguments| + assert_equal 'my_document', arguments[:type] + assert_equal ['1', '2'], arguments[:body][:ids] + end + .returns(@response) + + results = subject.__find_many(['1', '2']) + assert_instance_of MyDocument, results[0] + assert_instance_of MyDocument, results[1] + end + + should "find documents and return an Array of deserialized objects" do + subject.expects(:klass).returns(nil).at_least_once + subject.expects(:__get_type_from_class).never + + subject + .expects(:deserialize) + .with(@response['docs'][0]) + .returns(MyDocument.new) + + subject + .expects(:deserialize) + .with(@response['docs'][1]) + .returns(MyDocument.new) + + @client + .expects(:mget) + .with do |arguments| + assert_equal '_all', arguments[:type] + assert_equal ['1', '2'], arguments[:body][:ids] + end + .returns(@response) + + results = subject.__find_many(['1', '2']) + + assert_equal 2, results.size + + assert_instance_of MyDocument, results[0] + assert_instance_of MyDocument, results[1] + end + + should "find keep missing documents in the result as nil" do + @response = {"docs"=> + [ {"_index"=>"test", + "_type"=>"note", + "_id"=>"1", + "_version"=>1, + "found"=>true, + "_source"=>{"id"=>"1", "title"=>"Test 1"}}, + + {"_index"=>"test", + "_type"=>"note", + "_id"=>"3", + "_version"=>1, + "found"=>false}, + + {"_index"=>"test", + "_type"=>"note", + "_id"=>"2", + "_version"=>1, + "found"=>true, + "_source"=>{"id"=>"2", "title"=>"Test 2"}} + ]} + + subject.expects(:klass).returns(MyDocument).at_least_once + subject.expects(:__get_type_from_class).with(MyDocument).returns('my_document') + + subject + .expects(:deserialize) + .with(@response['docs'][0]) + .returns(MyDocument.new) + + subject + .expects(:deserialize) + .with(@response['docs'][2]) + .returns(MyDocument.new) + + @client + .expects(:mget) + .with do |arguments| + assert_equal 'my_document', arguments[:type] + assert_equal ['1', '3', '2'], arguments[:body][:ids] + end + .returns(@response) + + results = subject.__find_many(['1', '3', '2']) + + assert_equal 3, results.size + + assert_instance_of MyDocument, results[0] + assert_instance_of NilClass, results[1] + assert_instance_of MyDocument, results[2] + end + + should "pass options to the client" do + subject.expects(:klass).returns(nil).at_least_once + subject.expects(:deserialize).twice + + @client + .expects(:mget) + .with do |arguments| + assert_equal 'bambam', arguments[:routing] + end + .returns(@response) + + subject.__find_many ['1', '2'], routing: 'bambam' + end + end + + end +end From 3417fb44bb7bd617e7dd872281dc3c8e48590ede Mon Sep 17 00:00:00 2001 From: Karel Minarik Date: Mon, 31 Mar 2014 17:51:12 +0200 Subject: [PATCH 08/27] [STORE] Added the `Search` module The module provides an interface for getting objects from the repository based on a search query. The results are wrapped in the `Response::Results` instance, which proxies methods to the `results` property. --- .../elasticsearch-persistence.gemspec | 1 + .../lib/elasticsearch/persistence.rb | 3 + .../elasticsearch/persistence/repository.rb | 1 + .../repository/response/results.rb | 54 +++++++++++ .../persistence/repository/search.rb | 23 +++++ .../unit/repository_response_results_test.rb | 94 +++++++++++++++++++ .../test/unit/repository_search_test.rb | 80 ++++++++++++++++ 7 files changed, 256 insertions(+) create mode 100644 elasticsearch-persistence/lib/elasticsearch/persistence/repository/response/results.rb create mode 100644 elasticsearch-persistence/lib/elasticsearch/persistence/repository/search.rb create mode 100644 elasticsearch-persistence/test/unit/repository_response_results_test.rb create mode 100644 elasticsearch-persistence/test/unit/repository_search_test.rb diff --git a/elasticsearch-persistence/elasticsearch-persistence.gemspec b/elasticsearch-persistence/elasticsearch-persistence.gemspec index aa7647452..2260126be 100644 --- a/elasticsearch-persistence/elasticsearch-persistence.gemspec +++ b/elasticsearch-persistence/elasticsearch-persistence.gemspec @@ -23,6 +23,7 @@ Gem::Specification.new do |s| s.add_dependency "elasticsearch", '> 0.4' s.add_dependency "active_support" + s.add_dependency "hashie" s.add_development_dependency "bundler", "~> 1.5" s.add_development_dependency "rake" diff --git a/elasticsearch-persistence/lib/elasticsearch/persistence.rb b/elasticsearch-persistence/lib/elasticsearch/persistence.rb index 19c997984..bbbbb66a8 100644 --- a/elasticsearch-persistence/lib/elasticsearch/persistence.rb +++ b/elasticsearch-persistence/lib/elasticsearch/persistence.rb @@ -1,14 +1,17 @@ require 'elasticsearch' +require 'hashie' require 'active_support/inflector' require 'elasticsearch/persistence/version' require 'elasticsearch/persistence/client' +require 'elasticsearch/persistence/repository/response/results' require 'elasticsearch/persistence/repository/naming' require 'elasticsearch/persistence/repository/serialize' require 'elasticsearch/persistence/repository/store' require 'elasticsearch/persistence/repository/find' +require 'elasticsearch/persistence/repository/search' require 'elasticsearch/persistence/repository' require 'elasticsearch/persistence/repository/class' diff --git a/elasticsearch-persistence/lib/elasticsearch/persistence/repository.rb b/elasticsearch-persistence/lib/elasticsearch/persistence/repository.rb index 8ead25dd3..b255fb968 100644 --- a/elasticsearch-persistence/lib/elasticsearch/persistence/repository.rb +++ b/elasticsearch-persistence/lib/elasticsearch/persistence/repository.rb @@ -7,6 +7,7 @@ module Repository include Elasticsearch::Persistence::Repository::Serialize include Elasticsearch::Persistence::Repository::Store include Elasticsearch::Persistence::Repository::Find + include Elasticsearch::Persistence::Repository::Search def new(options={}, &block) Elasticsearch::Persistence::Repository::Class.new options, &block diff --git a/elasticsearch-persistence/lib/elasticsearch/persistence/repository/response/results.rb b/elasticsearch-persistence/lib/elasticsearch/persistence/repository/response/results.rb new file mode 100644 index 000000000..b269076b6 --- /dev/null +++ b/elasticsearch-persistence/lib/elasticsearch/persistence/repository/response/results.rb @@ -0,0 +1,54 @@ +module Elasticsearch + module Persistence + module Repository + module Response + + class Results + include Enumerable + + attr_reader :repository, :response, :response + + def initialize(repository, response, options={}) + @repository = repository + @response = Hashie::Mash.new(response) + @options = options + end + + def method_missing(method_name, *arguments, &block) + results.respond_to?(method_name) ? results.__send__(method_name, *arguments, &block) : super + end + + def respond_to?(method_name, include_private = false) + results.respond_to?(method_name) || super + end + + def total + response['hits']['total'] + end + + def max_score + response['hits']['max_score'] + end + + # Yields [object, hit] pairs to the block + # + def each_with_hit(&block) + results.zip(response['hits']['hits']).each(&block) + end + + # Yields [object, hit] pairs and returns the result + # + def map_with_hit(&block) + results.zip(response['hits']['hits']).map(&block) + end + + def results + @results ||= response['hits']['hits'].map do |document| + repository.deserialize(document.to_hash) + end + end + end + end + end + end +end diff --git a/elasticsearch-persistence/lib/elasticsearch/persistence/repository/search.rb b/elasticsearch-persistence/lib/elasticsearch/persistence/repository/search.rb new file mode 100644 index 000000000..1e6c25cf3 --- /dev/null +++ b/elasticsearch-persistence/lib/elasticsearch/persistence/repository/search.rb @@ -0,0 +1,23 @@ +module Elasticsearch + module Persistence + module Repository + + module Search + def search(query_or_definition, options={}) + type = (klass ? __get_type_from_class(klass) : nil ) + case + when query_or_definition.respond_to?(:to_hash) + response = client.search( { index: 'test', type: type, body: query_or_definition.to_hash }.merge(options) ) + when query_or_definition.is_a?(String) + response = client.search( { index: 'test', type: type, q: query_or_definition }.merge(options) ) + else + raise ArgumentError, "[!] Pass the search definition as a Hash-like object or pass the query as a String" + + " -- #{query_or_definition.class} given." + end + Response::Results.new(self, response) + end + end + + end + end +end diff --git a/elasticsearch-persistence/test/unit/repository_response_results_test.rb b/elasticsearch-persistence/test/unit/repository_response_results_test.rb new file mode 100644 index 000000000..34a9a3ba4 --- /dev/null +++ b/elasticsearch-persistence/test/unit/repository_response_results_test.rb @@ -0,0 +1,94 @@ +require 'test_helper' + +class Elasticsearch::Persistence::RepositoryResponseResultsTest < Test::Unit::TestCase + include Elasticsearch::Persistence + class MyDocument; end + + context "Response results" do + setup do + @repository = Repository.new + + @response = { "took" => 2, + "timed_out" => false, + "_shards" => {"total" => 5, "successful" => 5, "failed" => 0}, + "hits" => + { "total" => 2, + "max_score" => 0.19, + "hits" => + [{"_index" => "test", + "_type" => "note", + "_id" => "1", + "_score" => 0.19, + "_source" => {"id" => 1, "title" => "Test 1"}}, + + {"_index" => "test", + "_type" => "note", + "_id" => "2", + "_score" => 0.19, + "_source" => {"id" => 2, "title" => "Test 2"}} + ] + } + } + + @shoulda_subject = Repository::Response::Results.new @repository, @response + end + + should "provide the access to the repository" do + assert_instance_of Repository::Class, subject.repository + end + + should "provide the access to the response" do + assert_equal 5, subject.response['_shards']['total'] + end + + should "wrap the response in Hashie::Mash" do + assert_equal 5, subject.response._shards.total + end + + should "return the total" do + assert_equal 2, subject.total + end + + should "return the max_score" do + assert_equal 0.19, subject.max_score + end + + should "delegate methods to results" do + subject.repository + .expects(:deserialize) + .twice + .returns(MyDocument.new) + + assert_equal 2, subject.size + assert_respond_to subject, :each + end + + should "yield each object with hit" do + @shoulda_subject = Repository::Response::Results.new \ + @repository, + { 'hits' => { 'hits' => [{'_id' => '1', 'foo' => 'bar'}] } } + + subject.repository + .expects(:deserialize) + .returns('FOO') + + subject.each_with_hit do |object, hit| + assert_equal 'FOO', object + assert_equal 'bar', hit.foo + end + end + + should "map objects and hits" do + @shoulda_subject = Repository::Response::Results.new \ + @repository, + { 'hits' => { 'hits' => [{'_id' => '1', 'foo' => 'bar'}] } } + + subject.repository + .expects(:deserialize) + .returns('FOO') + + assert_equal ['FOO---bar'], subject.map_with_hit { |object, hit| "#{object}---#{hit.foo}" } + end + end + +end diff --git a/elasticsearch-persistence/test/unit/repository_search_test.rb b/elasticsearch-persistence/test/unit/repository_search_test.rb new file mode 100644 index 000000000..8bcb56f9c --- /dev/null +++ b/elasticsearch-persistence/test/unit/repository_search_test.rb @@ -0,0 +1,80 @@ +require 'test_helper' + +class Elasticsearch::Persistence::RepositorySearchTest < Test::Unit::TestCase + class MyDocument; end + + context "The repository search" do + setup do + @shoulda_subject = Class.new() { include Elasticsearch::Persistence::Repository::Search }.new + + @client = mock + @shoulda_subject.stubs(:klass).returns(nil) + @shoulda_subject.stubs(:client).returns(@client) + end + + should "search in type based on klass" do + subject.expects(:klass).returns(MyDocument).at_least_once + subject.expects(:__get_type_from_class).with(MyDocument).returns('my_document') + + @client.expects(:search).with do |arguments| + assert_equal 'test', arguments[:index] + assert_equal 'my_document', arguments[:type] + + assert_equal({foo: 'bar'}, arguments[:body]) + end + + subject.search foo: 'bar' + end + + should "search across all types" do + subject.expects(:klass).returns(nil).at_least_once + subject.expects(:__get_type_from_class).never + + @client.expects(:search).with do |arguments| + assert_equal 'test', arguments[:index] + assert_equal nil, arguments[:type] + + assert_equal({foo: 'bar'}, arguments[:body]) + end + + assert_instance_of Elasticsearch::Persistence::Repository::Response::Results, + subject.search(foo: 'bar') + end + + should "pass options to the client" do + subject.expects(:klass).returns(nil).at_least_once + subject.expects(:__get_type_from_class).never + + @client.expects(:search).twice.with do |arguments| + assert_equal 'bambam', arguments[:routing] + end + + assert_instance_of Elasticsearch::Persistence::Repository::Response::Results, + subject.search( {foo: 'bar'}, { routing: 'bambam' } ) + assert_instance_of Elasticsearch::Persistence::Repository::Response::Results, + subject.search( 'foobar', { routing: 'bambam' } ) + end + + should "search with simple search" do + subject.expects(:klass).returns(nil).at_least_once + subject.expects(:__get_type_from_class).never + + @client.expects(:search).with do |arguments| + assert_equal 'foobar', arguments[:q] + end + + assert_instance_of Elasticsearch::Persistence::Repository::Response::Results, + subject.search('foobar') + end + + should "raise error for incorrect search definitions" do + subject.expects(:klass).returns(nil).at_least_once + subject.expects(:__get_type_from_class).never + + assert_raise ArgumentError do + subject.search 123 + end + end + end + +end From b3d798e3eda59d7647572b0274a2168474b088e0 Mon Sep 17 00:00:00 2001 From: Karel Minarik Date: Mon, 31 Mar 2014 20:36:37 +0200 Subject: [PATCH 09/27] [STORE] Added the DSL variant of `klass` setter method So: reposistory.klass = Foobar is equivalent to: repository.klass Foobar --- .../lib/elasticsearch/persistence/repository/naming.rb | 4 ++-- .../test/unit/repository_naming_test.rb | 7 ++++++- 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/elasticsearch-persistence/lib/elasticsearch/persistence/repository/naming.rb b/elasticsearch-persistence/lib/elasticsearch/persistence/repository/naming.rb index 1d00a575d..6aef96e18 100644 --- a/elasticsearch-persistence/lib/elasticsearch/persistence/repository/naming.rb +++ b/elasticsearch-persistence/lib/elasticsearch/persistence/repository/naming.rb @@ -3,8 +3,8 @@ module Persistence module Repository module Naming - def klass - @klass + def klass name=nil + @klass = name || @klass end def klass=klass diff --git a/elasticsearch-persistence/test/unit/repository_naming_test.rb b/elasticsearch-persistence/test/unit/repository_naming_test.rb index 24b290177..babb394e7 100644 --- a/elasticsearch-persistence/test/unit/repository_naming_test.rb +++ b/elasticsearch-persistence/test/unit/repository_naming_test.rb @@ -49,7 +49,7 @@ module ::Foo; class Bar; end; end end end - context " document class name" do + context "document class name" do should "be nil by default" do assert_nil subject.klass end @@ -58,6 +58,11 @@ module ::Foo; class Bar; end; end subject.klass = Foobar assert_equal Foobar, subject.klass end + + should "be settable by DSL" do + subject.klass Foobar + assert_equal Foobar, subject.klass + end end end From 2b5b00d6e2da84f9889c8dc63c0e370b0df57b17 Mon Sep 17 00:00:00 2001 From: Karel Minarik Date: Mon, 31 Mar 2014 22:17:38 +0200 Subject: [PATCH 10/27] [STORE] Added the methods from the "elasticsearch-model" gem Included `Elasticsearch::Model::Indexing::ClassMethods` to support setting the index name and document type, and to allow configuring the mappings and settings for the index. See: https://github.com/elasticsearch/elasticsearch-rails/blob/6f4a57a/elasticsearch-model/lib/elasticsearch/model/indexing.rb --- .../elasticsearch-persistence.gemspec | 3 +- .../lib/elasticsearch/persistence.rb | 1 + .../elasticsearch/persistence/repository.rb | 2 + .../persistence/repository/naming.rb | 12 ++++++ .../test/unit/repository_indexing_test.rb | 37 +++++++++++++++++++ .../test/unit/repository_naming_test.rb | 28 ++++++++++++++ 6 files changed, 82 insertions(+), 1 deletion(-) create mode 100644 elasticsearch-persistence/test/unit/repository_indexing_test.rb diff --git a/elasticsearch-persistence/elasticsearch-persistence.gemspec b/elasticsearch-persistence/elasticsearch-persistence.gemspec index 2260126be..cb66916ed 100644 --- a/elasticsearch-persistence/elasticsearch-persistence.gemspec +++ b/elasticsearch-persistence/elasticsearch-persistence.gemspec @@ -22,7 +22,8 @@ Gem::Specification.new do |s| s.rdoc_options = [ "--charset=UTF-8" ] s.add_dependency "elasticsearch", '> 0.4' - s.add_dependency "active_support" + s.add_dependency "elasticsearch-model", '>= 0.1' + s.add_dependency "activesupport", '> 3' s.add_dependency "hashie" s.add_development_dependency "bundler", "~> 1.5" diff --git a/elasticsearch-persistence/lib/elasticsearch/persistence.rb b/elasticsearch-persistence/lib/elasticsearch/persistence.rb index bbbbb66a8..5e86351c7 100644 --- a/elasticsearch-persistence/lib/elasticsearch/persistence.rb +++ b/elasticsearch-persistence/lib/elasticsearch/persistence.rb @@ -1,4 +1,5 @@ require 'elasticsearch' +require 'elasticsearch/model/indexing' require 'hashie' require 'active_support/inflector' diff --git a/elasticsearch-persistence/lib/elasticsearch/persistence/repository.rb b/elasticsearch-persistence/lib/elasticsearch/persistence/repository.rb index b255fb968..d15e45960 100644 --- a/elasticsearch-persistence/lib/elasticsearch/persistence/repository.rb +++ b/elasticsearch-persistence/lib/elasticsearch/persistence/repository.rb @@ -9,6 +9,8 @@ module Repository include Elasticsearch::Persistence::Repository::Find include Elasticsearch::Persistence::Repository::Search + include Elasticsearch::Model::Indexing::ClassMethods + def new(options={}, &block) Elasticsearch::Persistence::Repository::Class.new options, &block end; module_function :new diff --git a/elasticsearch-persistence/lib/elasticsearch/persistence/repository/naming.rb b/elasticsearch-persistence/lib/elasticsearch/persistence/repository/naming.rb index 6aef96e18..82344ad28 100644 --- a/elasticsearch-persistence/lib/elasticsearch/persistence/repository/naming.rb +++ b/elasticsearch-persistence/lib/elasticsearch/persistence/repository/naming.rb @@ -11,6 +11,18 @@ def klass=klass @klass = klass end + def index_name name=nil + @index_name = name || @index_name || self.class.to_s.underscore.gsub(/\//, '-') + end + + def index_name=(name) + @index_name = name + end + + def document_type + klass.to_s.underscore + end + def __get_klass_from_type(type) klass = type.classify klass.constantize diff --git a/elasticsearch-persistence/test/unit/repository_indexing_test.rb b/elasticsearch-persistence/test/unit/repository_indexing_test.rb new file mode 100644 index 000000000..5ee22578d --- /dev/null +++ b/elasticsearch-persistence/test/unit/repository_indexing_test.rb @@ -0,0 +1,37 @@ +require 'test_helper' + +class Elasticsearch::Persistence::RepositoryIndexingTest < Test::Unit::TestCase + context "The repository index methods" do + class MyDocument; end + + setup do + @shoulda_subject = Class.new() { include Elasticsearch::Model::Indexing::ClassMethods }.new + @shoulda_subject.stubs(:index_name).returns('my_index') + @shoulda_subject.stubs(:document_type).returns('my_document') + end + + should "have the convenience index management methods" do + %w( create_index! delete_index! refresh_index! ).each do |method| + assert_respond_to subject, method + end + end + + context "mappings" do + should "configure the mappings for the type" do + subject.mappings do + indexes :title + end + + assert_equal( {:"my_document"=>{:properties=>{:title=>{:type=>"string"}}}}, subject.mappings.to_hash ) + end + end + + context "settings" do + should "configure the settings for the index" do + subject.settings foo: 'bar' + assert_equal( {foo: 'bar'}, subject.settings.to_hash) + end + end + + end +end diff --git a/elasticsearch-persistence/test/unit/repository_naming_test.rb b/elasticsearch-persistence/test/unit/repository_naming_test.rb index babb394e7..b7d588bca 100644 --- a/elasticsearch-persistence/test/unit/repository_naming_test.rb +++ b/elasticsearch-persistence/test/unit/repository_naming_test.rb @@ -65,5 +65,33 @@ module ::Foo; class Bar; end; end end end + context "index_name" do + should "default to the class name" do + subject.instance_eval do + def self.class + 'FakeRepository' + end + end + + assert_equal 'fake_repository', subject.index_name + end + + should "be settable" do + subject.index_name = 'foobar1' + assert_equal 'foobar1', subject.index_name + + subject.index_name 'foobar2' + assert_equal 'foobar2', subject.index_name + end + end + + context "document_type" do + should "default to klass" do + assert_equal '', subject.document_type + + subject.klass Foobar + assert_equal 'foobar', subject.document_type + end + end end end From a9dd556abfd02a149796d28bc475877331b16684 Mon Sep 17 00:00:00 2001 From: Karel Minarik Date: Mon, 31 Mar 2014 22:32:05 +0200 Subject: [PATCH 11/27] [STORE] Refactored the `:index` paramter to use repository `index_name` --- .../persistence/repository/find.rb | 6 ++--- .../persistence/repository/search.rb | 4 +-- .../persistence/repository/store.rb | 4 +-- .../test/unit/repository_find_test.rb | 26 +++++++++++-------- .../unit/repository_response_results_test.rb | 4 +-- .../test/unit/repository_search_test.rb | 1 + .../test/unit/repository_store_test.rb | 7 +++-- 7 files changed, 30 insertions(+), 22 deletions(-) diff --git a/elasticsearch-persistence/lib/elasticsearch/persistence/repository/find.rb b/elasticsearch-persistence/lib/elasticsearch/persistence/repository/find.rb index 07ef337a2..15fde4d69 100644 --- a/elasticsearch-persistence/lib/elasticsearch/persistence/repository/find.rb +++ b/elasticsearch-persistence/lib/elasticsearch/persistence/repository/find.rb @@ -18,12 +18,12 @@ def find(*args) def exists?(id, options={}) type = (klass ? __get_type_from_class(klass) : '_all') - client.exists( { index: 'test', type: type, id: id }.merge(options) ) + client.exists( { index: index_name, type: type, id: id }.merge(options) ) end def __find_one(id, options={}) type = (klass ? __get_type_from_class(klass) : '_all') - document = client.get( { index: 'test', type: type, id: id }.merge(options) ) + document = client.get( { index: index_name, type: type, id: id }.merge(options) ) deserialize(document) rescue Elasticsearch::Transport::Transport::Errors::NotFound => e @@ -32,7 +32,7 @@ def __find_one(id, options={}) def __find_many(ids, options={}) type = (klass ? __get_type_from_class(klass) : '_all') - documents = client.mget( { index: 'test', type: type, body: { ids: ids } }.merge(options) ) + documents = client.mget( { index: index_name, type: type, body: { ids: ids } }.merge(options) ) documents['docs'].map { |document| document['found'] ? deserialize(document) : nil } end diff --git a/elasticsearch-persistence/lib/elasticsearch/persistence/repository/search.rb b/elasticsearch-persistence/lib/elasticsearch/persistence/repository/search.rb index 1e6c25cf3..01bc3916c 100644 --- a/elasticsearch-persistence/lib/elasticsearch/persistence/repository/search.rb +++ b/elasticsearch-persistence/lib/elasticsearch/persistence/repository/search.rb @@ -7,9 +7,9 @@ def search(query_or_definition, options={}) type = (klass ? __get_type_from_class(klass) : nil ) case when query_or_definition.respond_to?(:to_hash) - response = client.search( { index: 'test', type: type, body: query_or_definition.to_hash }.merge(options) ) + response = client.search( { index: index_name, type: type, body: query_or_definition.to_hash }.merge(options) ) when query_or_definition.is_a?(String) - response = client.search( { index: 'test', type: type, q: query_or_definition }.merge(options) ) + response = client.search( { index: index_name, type: type, q: query_or_definition }.merge(options) ) else raise ArgumentError, "[!] Pass the search definition as a Hash-like object or pass the query as a String" + " -- #{query_or_definition.class} given." diff --git a/elasticsearch-persistence/lib/elasticsearch/persistence/repository/store.rb b/elasticsearch-persistence/lib/elasticsearch/persistence/repository/store.rb index 95cbff9ee..ed0320e26 100644 --- a/elasticsearch-persistence/lib/elasticsearch/persistence/repository/store.rb +++ b/elasticsearch-persistence/lib/elasticsearch/persistence/repository/store.rb @@ -7,7 +7,7 @@ def save(document, options={}) serialized = serialize(document) id = __get_id_from_document(serialized) type = klass || __get_type_from_class(document.class) - client.index( { index: 'test', type: type, id: id, body: serialized }.merge(options) ) + client.index( { index: index_name, type: type, id: id, body: serialized }.merge(options) ) end def delete(document, options={}) @@ -19,7 +19,7 @@ def delete(document, options={}) id = __get_id_from_document(serialized) type = klass || __get_type_from_class(document.class) end - client.delete( { index: 'test', type: type, id: id }.merge(options) ) + client.delete( { index: index_name, type: type, id: id }.merge(options) ) end end diff --git a/elasticsearch-persistence/test/unit/repository_find_test.rb b/elasticsearch-persistence/test/unit/repository_find_test.rb index 9fa8bb87e..617fedf00 100644 --- a/elasticsearch-persistence/test/unit/repository_find_test.rb +++ b/elasticsearch-persistence/test/unit/repository_find_test.rb @@ -9,6 +9,7 @@ class MyDocument; end @client = mock @shoulda_subject.stubs(:klass).returns(nil) + @shoulda_subject.stubs(:index_name).returns('my_index') @shoulda_subject.stubs(:client).returns(@client) end @@ -78,10 +79,11 @@ class MyDocument; end should "pass options to the client" do @client.expects(:exists).with do |arguments| - assert_equal 'bambam', arguments[:routing] + assert_equal 'foobarbam', arguments[:index] + assert_equal 'bambam', arguments[:routing] end - subject.exists? '1', routing: 'bambam' + subject.exists? '1', index: 'foobarbam', routing: 'bambam' end end @@ -155,25 +157,26 @@ class MyDocument; end @client .expects(:get) .with do |arguments| - assert_equal 'bambam', arguments[:routing] + assert_equal 'foobarbam', arguments[:index] + assert_equal 'bambam', arguments[:routing] end .returns({'_source' => { 'foo' => 'bar' }}) - subject.__find_one '1', routing: 'bambam' + subject.__find_one '1', index: 'foobarbam', routing: 'bambam' end end context "'__find_many' method" do setup do @response = {"docs"=> - [ {"_index"=>"test", + [ {"_index"=>"my_index", "_type"=>"note", "_id"=>"1", "_version"=>1, "found"=>true, "_source"=>{"id"=>"1", "title"=>"Test 1"}}, - {"_index"=>"test", + {"_index"=>"my_index", "_type"=>"note", "_id"=>"2", "_version"=>1, @@ -241,20 +244,20 @@ class MyDocument; end should "find keep missing documents in the result as nil" do @response = {"docs"=> - [ {"_index"=>"test", + [ {"_index"=>"my_index", "_type"=>"note", "_id"=>"1", "_version"=>1, "found"=>true, "_source"=>{"id"=>"1", "title"=>"Test 1"}}, - {"_index"=>"test", + {"_index"=>"my_index", "_type"=>"note", "_id"=>"3", "_version"=>1, "found"=>false}, - {"_index"=>"test", + {"_index"=>"my_index", "_type"=>"note", "_id"=>"2", "_version"=>1, @@ -299,11 +302,12 @@ class MyDocument; end @client .expects(:mget) .with do |arguments| - assert_equal 'bambam', arguments[:routing] + assert_equal 'foobarbam', arguments[:index] + assert_equal 'bambam', arguments[:routing] end .returns(@response) - subject.__find_many ['1', '2'], routing: 'bambam' + subject.__find_many ['1', '2'], index: 'foobarbam', routing: 'bambam' end end diff --git a/elasticsearch-persistence/test/unit/repository_response_results_test.rb b/elasticsearch-persistence/test/unit/repository_response_results_test.rb index 34a9a3ba4..ea088e019 100644 --- a/elasticsearch-persistence/test/unit/repository_response_results_test.rb +++ b/elasticsearch-persistence/test/unit/repository_response_results_test.rb @@ -15,13 +15,13 @@ class MyDocument; end { "total" => 2, "max_score" => 0.19, "hits" => - [{"_index" => "test", + [{"_index" => "my_index", "_type" => "note", "_id" => "1", "_score" => 0.19, "_source" => {"id" => 1, "title" => "Test 1"}}, - {"_index" => "test", + {"_index" => "my_index", "_type" => "note", "_id" => "2", "_score" => 0.19, diff --git a/elasticsearch-persistence/test/unit/repository_search_test.rb b/elasticsearch-persistence/test/unit/repository_search_test.rb index 8bcb56f9c..711ded396 100644 --- a/elasticsearch-persistence/test/unit/repository_search_test.rb +++ b/elasticsearch-persistence/test/unit/repository_search_test.rb @@ -9,6 +9,7 @@ class MyDocument; end @client = mock @shoulda_subject.stubs(:klass).returns(nil) + @shoulda_subject.stubs(:index_name).returns('test') @shoulda_subject.stubs(:client).returns(@client) end diff --git a/elasticsearch-persistence/test/unit/repository_store_test.rb b/elasticsearch-persistence/test/unit/repository_store_test.rb index 463bf2915..58afd002e 100644 --- a/elasticsearch-persistence/test/unit/repository_store_test.rb +++ b/elasticsearch-persistence/test/unit/repository_store_test.rb @@ -6,6 +6,7 @@ class MyDocument; end setup do @shoulda_subject = Class.new() { include Elasticsearch::Persistence::Repository::Store }.new + @shoulda_subject.stubs(:index_name).returns('test') end context "save" do @@ -49,11 +50,12 @@ class MyDocument; end client = mock client.expects(:index).with do |arguments| + assert_equal 'foobarbam', arguments[:index] assert_equal 'bambam', arguments[:routing] end subject.expects(:client).returns(client) - subject.save({foo: 'bar'}, routing: 'bambam') + subject.save({foo: 'bar'}, { index: 'foobarbam', routing: 'bambam' }) end end @@ -109,11 +111,12 @@ class MyDocument; end client = mock client.expects(:delete).with do |arguments| + assert_equal 'foobarbam', arguments[:index] assert_equal 'bambam', arguments[:routing] end subject.expects(:client).returns(client) - subject.delete('1', routing: 'bambam') + subject.delete('1', index: 'foobarbam', routing: 'bambam') end end end From 9943940f7f1faa994c459a7c57376360f69fa40c Mon Sep 17 00:00:00 2001 From: Karel Minarik Date: Tue, 1 Apr 2014 12:00:52 +0200 Subject: [PATCH 12/27] [STORE] Changed that `document_type` method returns `nil` when no `klass` is set --- .../lib/elasticsearch/persistence/repository/naming.rb | 2 +- .../test/unit/repository_naming_test.rb | 6 ++++-- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/elasticsearch-persistence/lib/elasticsearch/persistence/repository/naming.rb b/elasticsearch-persistence/lib/elasticsearch/persistence/repository/naming.rb index 82344ad28..3ee61232c 100644 --- a/elasticsearch-persistence/lib/elasticsearch/persistence/repository/naming.rb +++ b/elasticsearch-persistence/lib/elasticsearch/persistence/repository/naming.rb @@ -20,7 +20,7 @@ def index_name=(name) end def document_type - klass.to_s.underscore + klass ? klass.to_s.underscore : nil end def __get_klass_from_type(type) diff --git a/elasticsearch-persistence/test/unit/repository_naming_test.rb b/elasticsearch-persistence/test/unit/repository_naming_test.rb index b7d588bca..64998c061 100644 --- a/elasticsearch-persistence/test/unit/repository_naming_test.rb +++ b/elasticsearch-persistence/test/unit/repository_naming_test.rb @@ -86,9 +86,11 @@ def self.class end context "document_type" do - should "default to klass" do - assert_equal '', subject.document_type + should "be nil when no klass is set" do + assert_equal nil, subject.document_type + end + should "default to klass" do subject.klass Foobar assert_equal 'foobar', subject.document_type end From 33efa797c8aaee2f1709d9bf910f7aff781b65e9 Mon Sep 17 00:00:00 2001 From: Karel Minarik Date: Tue, 1 Apr 2014 12:16:16 +0200 Subject: [PATCH 13/27] [STORE] Changed that the `Store` methods reflect that `klass` returns a Ruby class, not a string --- .../persistence/repository/store.rb | 6 ++--- .../test/unit/repository_store_test.rb | 27 +++++++++++-------- 2 files changed, 19 insertions(+), 14 deletions(-) diff --git a/elasticsearch-persistence/lib/elasticsearch/persistence/repository/store.rb b/elasticsearch-persistence/lib/elasticsearch/persistence/repository/store.rb index ed0320e26..3ca7995ce 100644 --- a/elasticsearch-persistence/lib/elasticsearch/persistence/repository/store.rb +++ b/elasticsearch-persistence/lib/elasticsearch/persistence/repository/store.rb @@ -6,18 +6,18 @@ module Store def save(document, options={}) serialized = serialize(document) id = __get_id_from_document(serialized) - type = klass || __get_type_from_class(document.class) + type = __get_type_from_class(klass || document.class) client.index( { index: index_name, type: type, id: id, body: serialized }.merge(options) ) end def delete(document, options={}) if document.is_a?(String) || document.is_a?(Integer) id = document - type = klass + type = __get_type_from_class(klass) else serialized = serialize(document) id = __get_id_from_document(serialized) - type = klass || __get_type_from_class(document.class) + type = __get_type_from_class(klass || document.class) end client.delete( { index: index_name, type: type, id: id }.merge(options) ) end diff --git a/elasticsearch-persistence/test/unit/repository_store_test.rb b/elasticsearch-persistence/test/unit/repository_store_test.rb index 58afd002e..721791981 100644 --- a/elasticsearch-persistence/test/unit/repository_store_test.rb +++ b/elasticsearch-persistence/test/unit/repository_store_test.rb @@ -12,12 +12,13 @@ class MyDocument; end context "save" do should "serialize the document, get type from klass and index it" do subject.expects(:serialize).returns({foo: 'bar'}) - subject.expects(:klass).returns('foo_type') + subject.expects(:klass).at_least_once.returns(MyDocument) + subject.expects(:__get_type_from_class).with(MyDocument).at_least_once.returns('my_document') subject.expects(:__get_id_from_document).returns('1') client = mock client.expects(:index).with do |arguments| - assert_equal 'foo_type', arguments[:type] + assert_equal 'my_document', arguments[:type] assert_equal '1', arguments[:id] assert_equal({foo: 'bar'}, arguments[:body]) end @@ -28,7 +29,7 @@ class MyDocument; end should "serialize the document, get type from document class and index it" do subject.expects(:serialize).returns({foo: 'bar'}) - subject.expects(:klass).returns(nil) + subject.expects(:klass).at_least_once.returns(nil) subject.expects(:__get_type_from_class).with(MyDocument).returns('my_document') subject.expects(:__get_id_from_document).returns('1') @@ -45,7 +46,8 @@ class MyDocument; end should "pass the options to the client" do subject.expects(:serialize).returns({foo: 'bar'}) - subject.expects(:klass).returns('foo') + subject.expects(:klass).at_least_once.returns(MyDocument) + subject.expects(:__get_type_from_class).with(MyDocument).returns('my_document') subject.expects(:__get_id_from_document).returns('1') client = mock @@ -62,12 +64,13 @@ class MyDocument; end context "delete" do should "get type from klass when passed only ID" do subject.expects(:serialize).never - subject.expects(:klass).returns('foo_type') + subject.expects(:klass).at_least_once.returns(MyDocument) + subject.expects(:__get_type_from_class).with(MyDocument).returns('my_document') subject.expects(:__get_id_from_document).never client = mock client.expects(:delete).with do |arguments| - assert_equal 'foo_type', arguments[:type] + assert_equal 'my_document', arguments[:type] assert_equal '1', arguments[:id] end subject.expects(:client).returns(client) @@ -77,12 +80,13 @@ class MyDocument; end should "get ID from document and type from klass when passed a document" do subject.expects(:serialize).returns({id: '1', foo: 'bar'}) - subject.expects(:klass).returns('foo_type') + subject.expects(:klass).at_least_once.returns(MyDocument) + subject.expects(:__get_type_from_class).with(MyDocument).returns('my_document') subject.expects(:__get_id_from_document).with({id: '1', foo: 'bar'}).returns('1') client = mock client.expects(:delete).with do |arguments| - assert_equal 'foo_type', arguments[:type] + assert_equal 'my_document', arguments[:type] assert_equal '1', arguments[:id] end subject.expects(:client).returns(client) @@ -92,9 +96,9 @@ class MyDocument; end should "get ID and type from document when passed a document" do subject.expects(:serialize).returns({id: '1', foo: 'bar'}) - subject.expects(:klass).returns(nil) - subject.expects(:__get_id_from_document).with({id: '1', foo: 'bar'}).returns('1') + subject.expects(:klass).at_least_once.returns(nil) subject.expects(:__get_type_from_class).with(MyDocument).returns('my_document') + subject.expects(:__get_id_from_document).with({id: '1', foo: 'bar'}).returns('1') client = mock client.expects(:delete).with do |arguments| @@ -107,7 +111,8 @@ class MyDocument; end end should "pass the options to the client" do - subject.expects(:klass).returns('foo') + subject.expects(:klass).at_least_once.returns(MyDocument) + subject.expects(:__get_type_from_class).returns('my_document') client = mock client.expects(:delete).with do |arguments| From ae88f2f7fc79c48bce672d303864709a700e8af7 Mon Sep 17 00:00:00 2001 From: Karel Minarik Date: Tue, 1 Apr 2014 12:32:13 +0200 Subject: [PATCH 14/27] [STORE] Added the `index` and `type` aliases for `index_name` and `document_type` --- .../lib/elasticsearch/persistence/repository/naming.rb | 6 +++--- .../test/unit/repository_naming_test.rb | 10 ++++++++++ 2 files changed, 13 insertions(+), 3 deletions(-) diff --git a/elasticsearch-persistence/lib/elasticsearch/persistence/repository/naming.rb b/elasticsearch-persistence/lib/elasticsearch/persistence/repository/naming.rb index 3ee61232c..cd9b9e3ac 100644 --- a/elasticsearch-persistence/lib/elasticsearch/persistence/repository/naming.rb +++ b/elasticsearch-persistence/lib/elasticsearch/persistence/repository/naming.rb @@ -13,15 +13,15 @@ def klass=klass def index_name name=nil @index_name = name || @index_name || self.class.to_s.underscore.gsub(/\//, '-') - end + end; alias :index :index_name def index_name=(name) @index_name = name - end + end; alias :index= :index_name= def document_type klass ? klass.to_s.underscore : nil - end + end; alias :type :document_type def __get_klass_from_type(type) klass = type.classify diff --git a/elasticsearch-persistence/test/unit/repository_naming_test.rb b/elasticsearch-persistence/test/unit/repository_naming_test.rb index 64998c061..3efde1fd4 100644 --- a/elasticsearch-persistence/test/unit/repository_naming_test.rb +++ b/elasticsearch-persistence/test/unit/repository_naming_test.rb @@ -83,6 +83,11 @@ def self.class subject.index_name 'foobar2' assert_equal 'foobar2', subject.index_name end + + should "be aliased as `index`" do + subject.index_name = 'foobar1' + assert_equal 'foobar1', subject.index + end end context "document_type" do @@ -94,6 +99,11 @@ def self.class subject.klass Foobar assert_equal 'foobar', subject.document_type end + + should "be aliased as `type`" do + subject.klass Foobar + assert_equal 'foobar', subject.type + end end end end From 8e527ef827d94516428224cdb2d22a552f7ea384 Mon Sep 17 00:00:00 2001 From: Karel Minarik Date: Tue, 1 Apr 2014 22:11:50 +0200 Subject: [PATCH 15/27] [STORE] Added, that `document_type` can set the document type for repository Also available as `document_type="foo"`. --- .../lib/elasticsearch/persistence/repository/naming.rb | 8 ++++++-- .../test/unit/repository_naming_test.rb | 10 ++++++++++ 2 files changed, 16 insertions(+), 2 deletions(-) diff --git a/elasticsearch-persistence/lib/elasticsearch/persistence/repository/naming.rb b/elasticsearch-persistence/lib/elasticsearch/persistence/repository/naming.rb index cd9b9e3ac..3df35f863 100644 --- a/elasticsearch-persistence/lib/elasticsearch/persistence/repository/naming.rb +++ b/elasticsearch-persistence/lib/elasticsearch/persistence/repository/naming.rb @@ -19,10 +19,14 @@ def index_name=(name) @index_name = name end; alias :index= :index_name= - def document_type - klass ? klass.to_s.underscore : nil + def document_type name=nil + @document_type = name || @document_type || (klass ? klass.to_s.underscore : nil) end; alias :type :document_type + def document_type=(name) + @document_type = name + end; alias :type= :document_type= + def __get_klass_from_type(type) klass = type.classify klass.constantize diff --git a/elasticsearch-persistence/test/unit/repository_naming_test.rb b/elasticsearch-persistence/test/unit/repository_naming_test.rb index 3efde1fd4..c54eaf64f 100644 --- a/elasticsearch-persistence/test/unit/repository_naming_test.rb +++ b/elasticsearch-persistence/test/unit/repository_naming_test.rb @@ -104,6 +104,16 @@ def self.class subject.klass Foobar assert_equal 'foobar', subject.type end + + should "be settable" do + subject.document_type = 'foobar' + assert_equal 'foobar', subject.document_type + end + + should "be settable by DSL" do + subject.document_type 'foobar' + assert_equal 'foobar', subject.document_type + end end end end From 394b72806f12533036523f5c81cb8dbbdc841a56 Mon Sep 17 00:00:00 2001 From: Karel Minarik Date: Tue, 1 Apr 2014 23:10:35 +0200 Subject: [PATCH 16/27] [STORE] Changed, that repository methods respect `document_type` when it's set When the `document_type` is configured for the repository, it is used in `save`, `delete`, `find`, etc method to set the `type` parameter for the client. When the `document_type` is not set, the old behaviour of inferring from `klass`, `document.class`, etc. is preserved. --- .../persistence/repository/find.rb | 6 +- .../persistence/repository/store.rb | 6 +- .../test/unit/repository_find_test.rb | 60 +++++++++++++++++++ .../test/unit/repository_store_test.rb | 48 +++++++++++++++ 4 files changed, 114 insertions(+), 6 deletions(-) diff --git a/elasticsearch-persistence/lib/elasticsearch/persistence/repository/find.rb b/elasticsearch-persistence/lib/elasticsearch/persistence/repository/find.rb index 15fde4d69..8c224da65 100644 --- a/elasticsearch-persistence/lib/elasticsearch/persistence/repository/find.rb +++ b/elasticsearch-persistence/lib/elasticsearch/persistence/repository/find.rb @@ -17,12 +17,12 @@ def find(*args) end def exists?(id, options={}) - type = (klass ? __get_type_from_class(klass) : '_all') + type = document_type || (klass ? __get_type_from_class(klass) : '_all') client.exists( { index: index_name, type: type, id: id }.merge(options) ) end def __find_one(id, options={}) - type = (klass ? __get_type_from_class(klass) : '_all') + type = document_type || (klass ? __get_type_from_class(klass) : '_all') document = client.get( { index: index_name, type: type, id: id }.merge(options) ) deserialize(document) @@ -31,7 +31,7 @@ def __find_one(id, options={}) end def __find_many(ids, options={}) - type = (klass ? __get_type_from_class(klass) : '_all') + type = document_type || (klass ? __get_type_from_class(klass) : '_all') documents = client.mget( { index: index_name, type: type, body: { ids: ids } }.merge(options) ) documents['docs'].map { |document| document['found'] ? deserialize(document) : nil } diff --git a/elasticsearch-persistence/lib/elasticsearch/persistence/repository/store.rb b/elasticsearch-persistence/lib/elasticsearch/persistence/repository/store.rb index 3ca7995ce..936aa1d79 100644 --- a/elasticsearch-persistence/lib/elasticsearch/persistence/repository/store.rb +++ b/elasticsearch-persistence/lib/elasticsearch/persistence/repository/store.rb @@ -6,18 +6,18 @@ module Store def save(document, options={}) serialized = serialize(document) id = __get_id_from_document(serialized) - type = __get_type_from_class(klass || document.class) + type = document_type || __get_type_from_class(klass || document.class) client.index( { index: index_name, type: type, id: id, body: serialized }.merge(options) ) end def delete(document, options={}) if document.is_a?(String) || document.is_a?(Integer) id = document - type = __get_type_from_class(klass) + type = document_type || __get_type_from_class(klass) else serialized = serialize(document) id = __get_id_from_document(serialized) - type = __get_type_from_class(klass || document.class) + type = document_type || __get_type_from_class(klass || document.class) end client.delete( { index: index_name, type: type, id: id }.merge(options) ) end diff --git a/elasticsearch-persistence/test/unit/repository_find_test.rb b/elasticsearch-persistence/test/unit/repository_find_test.rb index 617fedf00..fda7e7105 100644 --- a/elasticsearch-persistence/test/unit/repository_find_test.rb +++ b/elasticsearch-persistence/test/unit/repository_find_test.rb @@ -8,6 +8,7 @@ class MyDocument; end @shoulda_subject = Class.new() { include Elasticsearch::Persistence::Repository::Find }.new @client = mock + @shoulda_subject.stubs(:document_type).returns(nil) @shoulda_subject.stubs(:klass).returns(nil) @shoulda_subject.stubs(:index_name).returns('my_index') @shoulda_subject.stubs(:client).returns(@client) @@ -48,6 +49,7 @@ class MyDocument; end end should "return whether document for klass exists" do + subject.expects(:document_type).returns(nil) subject.expects(:klass).returns(MyDocument).at_least_once subject.expects(:__get_type_from_class).with(MyDocument).returns('my_document') @@ -62,6 +64,22 @@ class MyDocument; end assert_equal true, subject.exists?('1') end + should "return whether document for document_type exists" do + subject.expects(:document_type).returns('my_document') + subject.expects(:klass).returns(MyDocument).at_most_once + subject.expects(:__get_type_from_class).never + + @client + .expects(:exists) + .with do |arguments| + assert_equal 'my_document', arguments[:type] + assert_equal '1', arguments[:id] + end + .returns(true) + + assert_equal true, subject.exists?('1') + end + should "return whether document exists" do subject.expects(:klass).returns(nil) subject.expects(:__get_type_from_class).never @@ -89,6 +107,7 @@ class MyDocument; end context "'__find_one' method" do should "find document based on klass and return a deserialized object" do + subject.expects(:document_type).returns(nil) subject.expects(:klass).returns(MyDocument).at_least_once subject.expects(:__get_type_from_class).with(MyDocument).returns('my_document') @@ -105,7 +124,26 @@ class MyDocument; end assert_instance_of MyDocument, subject.__find_one('1') end + should "find document based on document_type and return a deserialized object" do + subject.expects(:document_type).returns('my_document') + subject.expects(:klass).returns(MyDocument).at_most_once + subject.expects(:__get_type_from_class).never + + subject.expects(:deserialize).with({'_source' => {'foo' => 'bar'}}).returns(MyDocument.new) + + @client + .expects(:get) + .with do |arguments| + assert_equal 'my_document', arguments[:type] + assert_equal '1', arguments[:id] + end + .returns({'_source' => { 'foo' => 'bar' }}) + + assert_instance_of MyDocument, subject.__find_one('1') + end + should "find document and return a deserialized object" do + subject.expects(:document_type).returns(nil) subject.expects(:klass).returns(nil).at_least_once subject.expects(:__get_type_from_class).never @@ -123,6 +161,7 @@ class MyDocument; end end should "raise DocumentNotFound exception when the document cannot be found" do + subject.expects(:document_type).returns(nil) subject.expects(:klass).returns(nil).at_least_once subject.expects(:deserialize).never @@ -186,6 +225,7 @@ class MyDocument; end end should "find documents based on klass and return an Array of deserialized objects" do + subject.expects(:document_type).returns(nil) subject.expects(:klass).returns(MyDocument).at_least_once subject.expects(:__get_type_from_class).with(MyDocument).returns('my_document') @@ -212,7 +252,26 @@ class MyDocument; end assert_instance_of MyDocument, results[1] end + should "find documents based on document_type and return an Array of deserialized objects" do + subject.expects(:document_type).returns('my_document') + subject.expects(:klass).returns(MyDocument).at_most_once + subject.expects(:__get_type_from_class).never + + subject.expects(:deserialize).twice + + @client + .expects(:mget) + .with do |arguments| + assert_equal 'my_document', arguments[:type] + assert_equal ['1', '2'], arguments[:body][:ids] + end + .returns(@response) + + subject.__find_many(['1', '2']) + end + should "find documents and return an Array of deserialized objects" do + subject.expects(:document_type).returns(nil) subject.expects(:klass).returns(nil).at_least_once subject.expects(:__get_type_from_class).never @@ -265,6 +324,7 @@ class MyDocument; end "_source"=>{"id"=>"2", "title"=>"Test 2"}} ]} + subject.expects(:document_type).returns(nil) subject.expects(:klass).returns(MyDocument).at_least_once subject.expects(:__get_type_from_class).with(MyDocument).returns('my_document') diff --git a/elasticsearch-persistence/test/unit/repository_store_test.rb b/elasticsearch-persistence/test/unit/repository_store_test.rb index 721791981..48b916098 100644 --- a/elasticsearch-persistence/test/unit/repository_store_test.rb +++ b/elasticsearch-persistence/test/unit/repository_store_test.rb @@ -12,6 +12,7 @@ class MyDocument; end context "save" do should "serialize the document, get type from klass and index it" do subject.expects(:serialize).returns({foo: 'bar'}) + subject.expects(:document_type).returns(nil) subject.expects(:klass).at_least_once.returns(MyDocument) subject.expects(:__get_type_from_class).with(MyDocument).at_least_once.returns('my_document') subject.expects(:__get_id_from_document).returns('1') @@ -29,6 +30,7 @@ class MyDocument; end should "serialize the document, get type from document class and index it" do subject.expects(:serialize).returns({foo: 'bar'}) + subject.expects(:document_type).returns(nil) subject.expects(:klass).at_least_once.returns(nil) subject.expects(:__get_type_from_class).with(MyDocument).returns('my_document') subject.expects(:__get_id_from_document).returns('1') @@ -44,8 +46,30 @@ class MyDocument; end subject.save(MyDocument.new) end + should "serialize the document, get type from document_type and index it" do + subject.expects(:serialize).returns({foo: 'bar'}) + + subject.expects(:document_type).returns('my_document') + + subject.expects(:klass).never + subject.expects(:__get_type_from_class).never + + subject.expects(:__get_id_from_document).returns('1') + + client = mock + client.expects(:index).with do |arguments| + assert_equal 'my_document', arguments[:type] + assert_equal '1', arguments[:id] + assert_equal({foo: 'bar'}, arguments[:body]) + end + subject.expects(:client).returns(client) + + subject.save(MyDocument.new) + end + should "pass the options to the client" do subject.expects(:serialize).returns({foo: 'bar'}) + subject.expects(:document_type).returns(nil) subject.expects(:klass).at_least_once.returns(MyDocument) subject.expects(:__get_type_from_class).with(MyDocument).returns('my_document') subject.expects(:__get_id_from_document).returns('1') @@ -64,6 +88,7 @@ class MyDocument; end context "delete" do should "get type from klass when passed only ID" do subject.expects(:serialize).never + subject.expects(:document_type).returns(nil) subject.expects(:klass).at_least_once.returns(MyDocument) subject.expects(:__get_type_from_class).with(MyDocument).returns('my_document') subject.expects(:__get_id_from_document).never @@ -80,6 +105,7 @@ class MyDocument; end should "get ID from document and type from klass when passed a document" do subject.expects(:serialize).returns({id: '1', foo: 'bar'}) + subject.expects(:document_type).returns(nil) subject.expects(:klass).at_least_once.returns(MyDocument) subject.expects(:__get_type_from_class).with(MyDocument).returns('my_document') subject.expects(:__get_id_from_document).with({id: '1', foo: 'bar'}).returns('1') @@ -94,8 +120,29 @@ class MyDocument; end subject.delete({id: '1', foo: 'bar'}) end + should "get ID from document and type from document_type when passed a document" do + subject.expects(:serialize).returns({id: '1', foo: 'bar'}) + + subject.expects(:document_type).returns('my_document') + + subject.expects(:klass).never + subject.expects(:__get_type_from_class).never + + subject.expects(:__get_id_from_document).with({id: '1', foo: 'bar'}).returns('1') + + client = mock + client.expects(:delete).with do |arguments| + assert_equal 'my_document', arguments[:type] + assert_equal '1', arguments[:id] + end + subject.expects(:client).returns(client) + + subject.delete({id: '1', foo: 'bar'}) + end + should "get ID and type from document when passed a document" do subject.expects(:serialize).returns({id: '1', foo: 'bar'}) + subject.expects(:document_type).returns(nil) subject.expects(:klass).at_least_once.returns(nil) subject.expects(:__get_type_from_class).with(MyDocument).returns('my_document') subject.expects(:__get_id_from_document).with({id: '1', foo: 'bar'}).returns('1') @@ -111,6 +158,7 @@ class MyDocument; end end should "pass the options to the client" do + subject.expects(:document_type).returns(nil) subject.expects(:klass).at_least_once.returns(MyDocument) subject.expects(:__get_type_from_class).returns('my_document') From eb625ea94652e189b4d814656a2a4b48f4944881 Mon Sep 17 00:00:00 2001 From: Karel Minarik Date: Wed, 2 Apr 2014 10:34:01 +0200 Subject: [PATCH 17/27] [STORE] Implemented the gateway pattern for the repository integration To provide flexibility to the end-user, the integration of the "Repository" module has been refactored to proxy all repository methods via a gateway. This allows users to set up the repository in a custom class, with class methods, in a DSL-like fashion: class NoteRepository include Elasticsearch::Persistence::Repository klass Note index :my_notes mapping do indexes :title, analyzer: 'snowball' end client.transport.logger = Logger.new(STDERR) gateway do def serialize(document) super.merge(special: 'stuff') end end end The bundled Repository class can be configured via a block passed to the initializer: repository = Elasticsearch::Persistence::Repository.new do klass Note index :my_notes mapping do indexes :title, analyzer: 'snowball' end client.transport.logger = Logger.new(STDERR) end The repository methods can be accessed via the class or instance methods: NoteRepository.klass Note repository.klass Note --- .../lib/elasticsearch/persistence.rb | 3 +- .../elasticsearch/persistence/repository.rb | 36 ++++-- .../persistence/repository/class.rb | 9 +- .../test/unit/repository_class_test.rb | 50 ++++++--- .../test/unit/repository_module_test.rb | 106 ++++++++++++++++++ 5 files changed, 176 insertions(+), 28 deletions(-) diff --git a/elasticsearch-persistence/lib/elasticsearch/persistence.rb b/elasticsearch-persistence/lib/elasticsearch/persistence.rb index 5e86351c7..18db3ceb5 100644 --- a/elasticsearch-persistence/lib/elasticsearch/persistence.rb +++ b/elasticsearch-persistence/lib/elasticsearch/persistence.rb @@ -13,9 +13,8 @@ require 'elasticsearch/persistence/repository/store' require 'elasticsearch/persistence/repository/find' require 'elasticsearch/persistence/repository/search' -require 'elasticsearch/persistence/repository' - require 'elasticsearch/persistence/repository/class' +require 'elasticsearch/persistence/repository' module Elasticsearch module Persistence diff --git a/elasticsearch-persistence/lib/elasticsearch/persistence/repository.rb b/elasticsearch-persistence/lib/elasticsearch/persistence/repository.rb index d15e45960..8ffddbf21 100644 --- a/elasticsearch-persistence/lib/elasticsearch/persistence/repository.rb +++ b/elasticsearch-persistence/lib/elasticsearch/persistence/repository.rb @@ -1,15 +1,37 @@ module Elasticsearch module Persistence + module GatewayDelegation + def method_missing(method_name, *arguments, &block) + gateway.respond_to?(method_name) ? gateway.__send__(method_name, *arguments, &block) : super + end + + def respond_to?(method_name, include_private=false) + gateway.respond_to?(method_name) || super + end + end module Repository - include Elasticsearch::Persistence::Client - include Elasticsearch::Persistence::Repository::Naming - include Elasticsearch::Persistence::Repository::Serialize - include Elasticsearch::Persistence::Repository::Store - include Elasticsearch::Persistence::Repository::Find - include Elasticsearch::Persistence::Repository::Search + def self.included(base) + gateway = Elasticsearch::Persistence::Repository::Class.new + + base.class_eval do + define_method :gateway do + @gateway ||= gateway + end + + include GatewayDelegation + end + + (class << base; self; end).class_eval do + define_method :gateway do |&block| + @gateway ||= gateway + @gateway.instance_eval(&block) if block + @gateway + end - include Elasticsearch::Model::Indexing::ClassMethods + include GatewayDelegation + end + end def new(options={}, &block) Elasticsearch::Persistence::Repository::Class.new options, &block diff --git a/elasticsearch-persistence/lib/elasticsearch/persistence/repository/class.rb b/elasticsearch-persistence/lib/elasticsearch/persistence/repository/class.rb index 7ff11e828..8a74df431 100644 --- a/elasticsearch-persistence/lib/elasticsearch/persistence/repository/class.rb +++ b/elasticsearch-persistence/lib/elasticsearch/persistence/repository/class.rb @@ -3,7 +3,14 @@ module Persistence module Repository class Class - include Elasticsearch::Persistence::Repository + include Elasticsearch::Persistence::Client + include Elasticsearch::Persistence::Repository::Naming + include Elasticsearch::Persistence::Repository::Serialize + include Elasticsearch::Persistence::Repository::Store + include Elasticsearch::Persistence::Repository::Find + include Elasticsearch::Persistence::Repository::Search + + include Elasticsearch::Model::Indexing::ClassMethods attr_reader :options diff --git a/elasticsearch-persistence/test/unit/repository_class_test.rb b/elasticsearch-persistence/test/unit/repository_class_test.rb index b20ac3e8b..e4e20028d 100644 --- a/elasticsearch-persistence/test/unit/repository_class_test.rb +++ b/elasticsearch-persistence/test/unit/repository_class_test.rb @@ -3,29 +3,43 @@ class Elasticsearch::Persistence::RepositoryClassTest < Test::Unit::TestCase context "The default repository class" do - should "be created from the module" do - repository = Elasticsearch::Persistence::Repository.new - assert_instance_of Elasticsearch::Persistence::Repository::Class, repository - end + context "when initialized" do + should "be created from the module" do + repository = Elasticsearch::Persistence::Repository.new + assert_instance_of Elasticsearch::Persistence::Repository::Class, repository + end - should "store and access the options" do - repository = Elasticsearch::Persistence::Repository::Class.new foo: 'bar' - assert_equal 'bar', repository.options[:foo] - end + should "store and access the options" do + repository = Elasticsearch::Persistence::Repository::Class.new foo: 'bar' + assert_equal 'bar', repository.options[:foo] + end - should "instance eval a passed block" do - $foo = 100 - repository = Elasticsearch::Persistence::Repository::Class.new() { $foo += 1 } - assert_equal 101, $foo + should "instance eval a passed block" do + $foo = 100 + repository = Elasticsearch::Persistence::Repository::Class.new() { $foo += 1 } + assert_equal 101, $foo + end + + should "call a passed block with self" do + foo = 100 + repository = Elasticsearch::Persistence::Repository::Class.new do |r| + assert_instance_of Elasticsearch::Persistence::Repository::Class, r + foo += 1 + end + assert_equal 101, foo + end end - should "call a passed block with self" do - foo = 100 - repository = Elasticsearch::Persistence::Repository::Class.new do |r| - assert_instance_of Elasticsearch::Persistence::Repository::Class, r - foo += 1 + should "include the repository methods" do + repository = Elasticsearch::Persistence::Repository::Class.new + + %w( index_name document_type klass + mappings settings client client= + create_index! delete_index! refresh_index! + save delete serialize deserialize + exists? find search ).each do |method| + assert_respond_to repository, method end - assert_equal 101, foo end end diff --git a/elasticsearch-persistence/test/unit/repository_module_test.rb b/elasticsearch-persistence/test/unit/repository_module_test.rb index a31d7ac84..f615cb990 100644 --- a/elasticsearch-persistence/test/unit/repository_module_test.rb +++ b/elasticsearch-persistence/test/unit/repository_module_test.rb @@ -2,5 +2,111 @@ class Elasticsearch::Persistence::RepositoryModuleTest < Test::Unit::TestCase context "The repository module" do + + class DummyModel + def initialize(attributes={}) + @attributes = attributes + end + + def to_hash + @attributes + end + + def inspect + "" + end + end + + setup do + class DummyRepository + include Elasticsearch::Persistence::Repository + end + end + + teardown do + Elasticsearch::Persistence::RepositoryModuleTest.__send__ :remove_const, :DummyRepository + end + + context "when included" do + should "set up the gateway for the class and instance" do + assert_respond_to DummyRepository, :gateway + assert_respond_to DummyRepository.new, :gateway + + assert_instance_of Elasticsearch::Persistence::Repository::Class, DummyRepository.gateway + assert_instance_of Elasticsearch::Persistence::Repository::Class, DummyRepository.new.gateway + end + + should "proxy repository methods from the class to the gateway" do + class DummyRepository + include Elasticsearch::Persistence::Repository + + index :foobar + klass DummyModel + type :my_dummy_model + mapping { indexes :title, analyzer: 'snowball' } + end + + repository = DummyRepository.new + + assert_equal :foobar, DummyRepository.index + assert_equal DummyModel, DummyRepository.klass + assert_equal :my_dummy_model, DummyRepository.type + assert_equal 'snowball', DummyRepository.mappings.to_hash[:my_dummy_model][:properties][:title][:analyzer] + + assert_equal :foobar, repository.index + assert_equal DummyModel, repository.klass + assert_equal :my_dummy_model, repository.type + assert_equal 'snowball', repository.mappings.to_hash[:my_dummy_model][:properties][:title][:analyzer] + end + + should "proxy repository methods from the instance to the gateway" do + class DummyRepository + include Elasticsearch::Persistence::Repository + end + + repository = DummyRepository.new + repository.index :foobar + repository.klass DummyModel + repository.type :my_dummy_model + repository.mapping { indexes :title, analyzer: 'snowball' } + + assert_equal :foobar, DummyRepository.index + assert_equal DummyModel, DummyRepository.klass + assert_equal :my_dummy_model, DummyRepository.type + assert_equal 'snowball', DummyRepository.mappings.to_hash[:my_dummy_model][:properties][:title][:analyzer] + + assert_equal :foobar, repository.index + assert_equal DummyModel, repository.klass + assert_equal :my_dummy_model, repository.type + assert_equal 'snowball', repository.mappings.to_hash[:my_dummy_model][:properties][:title][:analyzer] + end + + should "allow to define gateway methods in the class definition" do + class DummyRepository + include Elasticsearch::Persistence::Repository + + gateway do + def serialize(document) + 'FAKE!' + end + end + end + + repository = DummyRepository.new + repository.client.transport.logger = Logger.new(STDERR) + + client = mock + client.expects(:index).with do |arguments| + assert_equal('xxx', arguments[:id]) + assert_equal('FAKE!', arguments[:body]) + end + repository.gateway.expects(:client).returns(client) + + repository.gateway.expects(:__get_id_from_document).returns('xxx') + + repository.save( id: '123', foo: 'bar' ) + end + end + end end From caddce7bb4d131b182fe28a49a8c504c18edb2f3 Mon Sep 17 00:00:00 2001 From: Karel Minarik Date: Wed, 2 Apr 2014 15:15:26 +0200 Subject: [PATCH 18/27] [STORE] Added, that `index_name` is inferred from the including class class NoteRepository include Elasticsearch::Persistence::Repository end NoteRepository.index_name => "note_repository" --- .../lib/elasticsearch/persistence/repository.rb | 2 +- .../lib/elasticsearch/persistence/repository/class.rb | 4 ++++ .../lib/elasticsearch/persistence/repository/naming.rb | 8 +++++++- .../test/unit/repository_module_test.rb | 4 ++++ .../test/unit/repository_naming_test.rb | 6 ++++++ 5 files changed, 22 insertions(+), 2 deletions(-) diff --git a/elasticsearch-persistence/lib/elasticsearch/persistence/repository.rb b/elasticsearch-persistence/lib/elasticsearch/persistence/repository.rb index 8ffddbf21..e35a57d7d 100644 --- a/elasticsearch-persistence/lib/elasticsearch/persistence/repository.rb +++ b/elasticsearch-persistence/lib/elasticsearch/persistence/repository.rb @@ -12,7 +12,7 @@ def respond_to?(method_name, include_private=false) module Repository def self.included(base) - gateway = Elasticsearch::Persistence::Repository::Class.new + gateway = Elasticsearch::Persistence::Repository::Class.new host: base base.class_eval do define_method :gateway do diff --git a/elasticsearch-persistence/lib/elasticsearch/persistence/repository/class.rb b/elasticsearch-persistence/lib/elasticsearch/persistence/repository/class.rb index 8a74df431..bda7e9f30 100644 --- a/elasticsearch-persistence/lib/elasticsearch/persistence/repository/class.rb +++ b/elasticsearch-persistence/lib/elasticsearch/persistence/repository/class.rb @@ -18,6 +18,10 @@ def initialize(options={}, &block) @options = options block.arity < 1 ? instance_eval(&block) : block.call(self) if block_given? end + + def host + options[:host] + end end end diff --git a/elasticsearch-persistence/lib/elasticsearch/persistence/repository/naming.rb b/elasticsearch-persistence/lib/elasticsearch/persistence/repository/naming.rb index 3df35f863..4d58b7248 100644 --- a/elasticsearch-persistence/lib/elasticsearch/persistence/repository/naming.rb +++ b/elasticsearch-persistence/lib/elasticsearch/persistence/repository/naming.rb @@ -12,7 +12,13 @@ def klass=klass end def index_name name=nil - @index_name = name || @index_name || self.class.to_s.underscore.gsub(/\//, '-') + @index_name = name || @index_name || begin + if respond_to?(:host) && host && host.is_a?(Module) + self.host.to_s.underscore.gsub(/\//, '-') + else + self.class.to_s.underscore.gsub(/\//, '-') + end + end end; alias :index :index_name def index_name=(name) diff --git a/elasticsearch-persistence/test/unit/repository_module_test.rb b/elasticsearch-persistence/test/unit/repository_module_test.rb index f615cb990..1850effc7 100644 --- a/elasticsearch-persistence/test/unit/repository_module_test.rb +++ b/elasticsearch-persistence/test/unit/repository_module_test.rb @@ -108,5 +108,9 @@ def serialize(document) end end + should_eventually "configure the index name in the shortcut initializer" do + assert_equal 'repository', Elasticsearch::Persistence::Repository.new.index_name + end + end end diff --git a/elasticsearch-persistence/test/unit/repository_naming_test.rb b/elasticsearch-persistence/test/unit/repository_naming_test.rb index c54eaf64f..0c6f9833e 100644 --- a/elasticsearch-persistence/test/unit/repository_naming_test.rb +++ b/elasticsearch-persistence/test/unit/repository_naming_test.rb @@ -88,6 +88,12 @@ def self.class subject.index_name = 'foobar1' assert_equal 'foobar1', subject.index end + + should "be inferred from the host class" do + class ::MySpecialRepository; end + subject.define_singleton_method(:host) { MySpecialRepository } + assert_equal 'my_special_repository', subject.index_name + end end context "document_type" do From 47efae2a5e56cefe3a5891bb02a33b797d4b6a75 Mon Sep 17 00:00:00 2001 From: Karel Minarik Date: Wed, 2 Apr 2014 15:43:18 +0200 Subject: [PATCH 19/27] [STORE] Added, that the repository class reflects the `:index` option Also, when using the "shortcut" to create the repository, a default name of `repository` is set. --- .../lib/elasticsearch/persistence/repository.rb | 2 +- .../lib/elasticsearch/persistence/repository/class.rb | 1 + elasticsearch-persistence/test/unit/repository_class_test.rb | 5 +++++ .../test/unit/repository_module_test.rb | 2 +- 4 files changed, 8 insertions(+), 2 deletions(-) diff --git a/elasticsearch-persistence/lib/elasticsearch/persistence/repository.rb b/elasticsearch-persistence/lib/elasticsearch/persistence/repository.rb index e35a57d7d..fe1c93a22 100644 --- a/elasticsearch-persistence/lib/elasticsearch/persistence/repository.rb +++ b/elasticsearch-persistence/lib/elasticsearch/persistence/repository.rb @@ -34,7 +34,7 @@ def self.included(base) end def new(options={}, &block) - Elasticsearch::Persistence::Repository::Class.new options, &block + Elasticsearch::Persistence::Repository::Class.new( {index: 'repository'}.merge(options), &block ) end; module_function :new end end diff --git a/elasticsearch-persistence/lib/elasticsearch/persistence/repository/class.rb b/elasticsearch-persistence/lib/elasticsearch/persistence/repository/class.rb index bda7e9f30..0579e779e 100644 --- a/elasticsearch-persistence/lib/elasticsearch/persistence/repository/class.rb +++ b/elasticsearch-persistence/lib/elasticsearch/persistence/repository/class.rb @@ -16,6 +16,7 @@ class Class def initialize(options={}, &block) @options = options + index_name options.delete(:index) block.arity < 1 ? instance_eval(&block) : block.call(self) if block_given? end diff --git a/elasticsearch-persistence/test/unit/repository_class_test.rb b/elasticsearch-persistence/test/unit/repository_class_test.rb index e4e20028d..d29711248 100644 --- a/elasticsearch-persistence/test/unit/repository_class_test.rb +++ b/elasticsearch-persistence/test/unit/repository_class_test.rb @@ -28,6 +28,11 @@ class Elasticsearch::Persistence::RepositoryClassTest < Test::Unit::TestCase end assert_equal 101, foo end + + should "configure the index name based on options" do + repository = Elasticsearch::Persistence::Repository::Class.new index: 'foobar' + assert_equal 'foobar', repository.index_name + end end should "include the repository methods" do diff --git a/elasticsearch-persistence/test/unit/repository_module_test.rb b/elasticsearch-persistence/test/unit/repository_module_test.rb index 1850effc7..d215e7279 100644 --- a/elasticsearch-persistence/test/unit/repository_module_test.rb +++ b/elasticsearch-persistence/test/unit/repository_module_test.rb @@ -108,7 +108,7 @@ def serialize(document) end end - should_eventually "configure the index name in the shortcut initializer" do + should "configure the index name in the shortcut initializer" do assert_equal 'repository', Elasticsearch::Persistence::Repository.new.index_name end From 270cfc5dd920d98b05dba371146b839fcaa63893 Mon Sep 17 00:00:00 2001 From: Karel Minarik Date: Thu, 3 Apr 2014 16:46:38 +0200 Subject: [PATCH 20/27] [STORE] Added that the `client` can be set in a DSL-like way Both: repository.client = MyClient.new and: repository.client MyClient.new are equivalent now. --- .../lib/elasticsearch/persistence.rb | 4 ++-- .../lib/elasticsearch/persistence/client.rb | 17 ++++++++++------- .../persistence/repository/class.rb | 2 +- .../test/unit/persistence_test.rb | 9 +++++++++ .../test/unit/repository_client_test.rb | 17 +++++++++++------ 5 files changed, 33 insertions(+), 16 deletions(-) diff --git a/elasticsearch-persistence/lib/elasticsearch/persistence.rb b/elasticsearch-persistence/lib/elasticsearch/persistence.rb index 18db3ceb5..d27c01c7f 100644 --- a/elasticsearch-persistence/lib/elasticsearch/persistence.rb +++ b/elasticsearch-persistence/lib/elasticsearch/persistence.rb @@ -21,8 +21,8 @@ module Persistence # :nodoc: module ClassMethods - def client - @client ||= Elasticsearch::Client.new + def client client=nil + @client = client || @client || Elasticsearch::Client.new end def client=(client) diff --git a/elasticsearch-persistence/lib/elasticsearch/persistence/client.rb b/elasticsearch-persistence/lib/elasticsearch/persistence/client.rb index c7a8bfa51..7fee129e0 100644 --- a/elasticsearch-persistence/lib/elasticsearch/persistence/client.rb +++ b/elasticsearch-persistence/lib/elasticsearch/persistence/client.rb @@ -1,15 +1,18 @@ module Elasticsearch module Persistence + module Repository - module Client - def client client=nil - @client ||= Elasticsearch::Persistence.client - end + module Client + def client client=nil + @client = client || @client || Elasticsearch::Persistence.client + end - def client=(client) - @client = client + def client=(client) + @client = client + @client + end end - end + end end end diff --git a/elasticsearch-persistence/lib/elasticsearch/persistence/repository/class.rb b/elasticsearch-persistence/lib/elasticsearch/persistence/repository/class.rb index 0579e779e..cce2a9ea2 100644 --- a/elasticsearch-persistence/lib/elasticsearch/persistence/repository/class.rb +++ b/elasticsearch-persistence/lib/elasticsearch/persistence/repository/class.rb @@ -3,7 +3,7 @@ module Persistence module Repository class Class - include Elasticsearch::Persistence::Client + include Elasticsearch::Persistence::Repository::Client include Elasticsearch::Persistence::Repository::Naming include Elasticsearch::Persistence::Repository::Serialize include Elasticsearch::Persistence::Repository::Store diff --git a/elasticsearch-persistence/test/unit/persistence_test.rb b/elasticsearch-persistence/test/unit/persistence_test.rb index 9381203d3..cd17ba7dc 100644 --- a/elasticsearch-persistence/test/unit/persistence_test.rb +++ b/elasticsearch-persistence/test/unit/persistence_test.rb @@ -18,6 +18,15 @@ class Elasticsearch::Persistence::ModuleTest < Test::Unit::TestCase Elasticsearch::Persistence.client = nil end end + + should "allow to set a client with DSL" do + begin + Elasticsearch::Persistence.client "Foobar" + assert_equal "Foobar", Elasticsearch::Persistence.client + ensure + Elasticsearch::Persistence.client = nil + end + end end end end diff --git a/elasticsearch-persistence/test/unit/repository_client_test.rb b/elasticsearch-persistence/test/unit/repository_client_test.rb index 246631afc..88e40193e 100644 --- a/elasticsearch-persistence/test/unit/repository_client_test.rb +++ b/elasticsearch-persistence/test/unit/repository_client_test.rb @@ -1,13 +1,9 @@ require 'test_helper' class Elasticsearch::Persistence::RepositoryClientTest < Test::Unit::TestCase - context "A repository client" do - class DummyReposistory - include Elasticsearch::Persistence::Repository - end - + context "The repository client" do setup do - @shoulda_subject = DummyReposistory.new + @shoulda_subject = Class.new() { include Elasticsearch::Persistence::Repository::Client }.new end should "have a default client" do @@ -23,5 +19,14 @@ class DummyReposistory subject.client = nil end end + + should "allow to set the client with DSL" do + begin + subject.client "Foobar" + assert_equal "Foobar", subject.client + ensure + subject.client = nil + end + end end end From c2bbf1f1c882e4c57909d4bd9a35893e25714fa9 Mon Sep 17 00:00:00 2001 From: Karel Minarik Date: Thu, 3 Apr 2014 16:55:22 +0200 Subject: [PATCH 21/27] [STORE] Added `respond_to_missing?` to the proxy objects See: * http://robots.thoughtbot.com/always-define-respond-to-missing-when-overriding * http://blog.marc-andre.ca/2010/11/15/methodmissing-politely/ --- .../lib/elasticsearch/persistence/repository.rb | 4 ++++ .../test/unit/repository_module_test.rb | 6 +++++- .../test/unit/repository_response_results_test.rb | 4 ++++ 3 files changed, 13 insertions(+), 1 deletion(-) diff --git a/elasticsearch-persistence/lib/elasticsearch/persistence/repository.rb b/elasticsearch-persistence/lib/elasticsearch/persistence/repository.rb index fe1c93a22..7b69a39b5 100644 --- a/elasticsearch-persistence/lib/elasticsearch/persistence/repository.rb +++ b/elasticsearch-persistence/lib/elasticsearch/persistence/repository.rb @@ -8,6 +8,10 @@ def method_missing(method_name, *arguments, &block) def respond_to?(method_name, include_private=false) gateway.respond_to?(method_name) || super end + + def respond_to_missing?(method_name, *) + gateway.respond_to?(method_name) || super + end end module Repository diff --git a/elasticsearch-persistence/test/unit/repository_module_test.rb b/elasticsearch-persistence/test/unit/repository_module_test.rb index d215e7279..d559ae34c 100644 --- a/elasticsearch-persistence/test/unit/repository_module_test.rb +++ b/elasticsearch-persistence/test/unit/repository_module_test.rb @@ -59,6 +59,11 @@ class DummyRepository assert_equal 'snowball', repository.mappings.to_hash[:my_dummy_model][:properties][:title][:analyzer] end + should "correctly delegate to the gateway" do + repository = DummyRepository.new + assert_instance_of Method, repository.method(:index) + end + should "proxy repository methods from the instance to the gateway" do class DummyRepository include Elasticsearch::Persistence::Repository @@ -111,6 +116,5 @@ def serialize(document) should "configure the index name in the shortcut initializer" do assert_equal 'repository', Elasticsearch::Persistence::Repository.new.index_name end - end end diff --git a/elasticsearch-persistence/test/unit/repository_response_results_test.rb b/elasticsearch-persistence/test/unit/repository_response_results_test.rb index ea088e019..294a96efd 100644 --- a/elasticsearch-persistence/test/unit/repository_response_results_test.rb +++ b/elasticsearch-persistence/test/unit/repository_response_results_test.rb @@ -63,6 +63,10 @@ class MyDocument; end assert_respond_to subject, :each end + should "respond to missing" do + assert_instance_of Method, subject.method(:to_a) + end + should "yield each object with hit" do @shoulda_subject = Repository::Response::Results.new \ @repository, From b674c665316914e47fa469e1a6264cc24887b8ba Mon Sep 17 00:00:00 2001 From: Karel Minarik Date: Fri, 4 Apr 2014 20:37:05 +0200 Subject: [PATCH 22/27] [STORE] Added the `method_added` hook to allow defining gateway methods directly in the class Instead of calling the `gateway` method with a block, to redefine the serialize/deserialize methods: class NoteRepository gateway do def serialize(document) Hash[document.to_hash.map() { |k,v| v.upcase! if k == :title; [k,v] }] end def deserialize(document) MyNote.new ActiveSupport::HashWithIndifferentAccess.new(document['_source']).deep_symbolize_keys end end end Define them directly in the class, and they will be intercepted by the hook, and (re)defined directly on the gateway: class NoteRepository def serialize(document) Hash[document.to_hash.map() { |k,v| v.upcase! if k == :title; [k,v] }] end def deserialize(document) MyNote.new ActiveSupport::HashWithIndifferentAccess.new(document['_source']).deep_symbolize_keys end end See: http://www.ruby-doc.org/core-2.1.1/Module.html#method-i-method_added (COMITTED WITH FINGERS CROSSED :) --- .../elasticsearch/persistence/repository.rb | 6 ++++ .../test/unit/repository_module_test.rb | 30 +++++++++++++++++-- 2 files changed, 33 insertions(+), 3 deletions(-) diff --git a/elasticsearch-persistence/lib/elasticsearch/persistence/repository.rb b/elasticsearch-persistence/lib/elasticsearch/persistence/repository.rb index 7b69a39b5..8f08d3fd5 100644 --- a/elasticsearch-persistence/lib/elasticsearch/persistence/repository.rb +++ b/elasticsearch-persistence/lib/elasticsearch/persistence/repository.rb @@ -35,6 +35,12 @@ def self.included(base) include GatewayDelegation end + + def base.method_added(name) + if :gateway != name && respond_to?(:gateway) && (gateway.public_methods - Object.public_methods).include?(name) + gateway.define_singleton_method(name, self.new.method(name).to_proc) + end + end end def new(options={}, &block) diff --git a/elasticsearch-persistence/test/unit/repository_module_test.rb b/elasticsearch-persistence/test/unit/repository_module_test.rb index d559ae34c..d85fcf0a7 100644 --- a/elasticsearch-persistence/test/unit/repository_module_test.rb +++ b/elasticsearch-persistence/test/unit/repository_module_test.rb @@ -86,8 +86,8 @@ class DummyRepository assert_equal 'snowball', repository.mappings.to_hash[:my_dummy_model][:properties][:title][:analyzer] end - should "allow to define gateway methods in the class definition" do - class DummyRepository + should "allow to define gateway methods in the class definition via block passed to the gateway method" do + class DummyRepositoryWithGatewaySerialize include Elasticsearch::Persistence::Repository gateway do @@ -97,7 +97,7 @@ def serialize(document) end end - repository = DummyRepository.new + repository = DummyRepositoryWithGatewaySerialize.new repository.client.transport.logger = Logger.new(STDERR) client = mock @@ -113,6 +113,30 @@ def serialize(document) end end + should "allow to define gateway methods in the class definition via regular method definition" do + class DummyRepositoryWithDirectSerialize + include Elasticsearch::Persistence::Repository + + def serialize(document) + 'FAKE IN CLASS!' + end + end + + repository = DummyRepositoryWithDirectSerialize.new + repository.client.transport.logger = Logger.new(STDERR) + + client = mock + client.expects(:index).with do |arguments| + assert_equal('xxx', arguments[:id]) + assert_equal('FAKE IN CLASS!', arguments[:body]) + end + repository.gateway.expects(:client).returns(client) + + repository.gateway.expects(:__get_id_from_document).returns('xxx') + + repository.save( id: '123', foo: 'bar' ) + end + should "configure the index name in the shortcut initializer" do assert_equal 'repository', Elasticsearch::Persistence::Repository.new.index_name end From f2900aaf32c534f7dacac793cf8fccd4d0c034b5 Mon Sep 17 00:00:00 2001 From: Karel Minarik Date: Fri, 4 Apr 2014 22:19:12 +0200 Subject: [PATCH 23/27] [STORE] Added code annotation, documentation and examples --- .../lib/elasticsearch/persistence.rb | 58 +++++++++++++++++++ .../lib/elasticsearch/persistence/client.rb | 30 ++++++++++ .../persistence/repository/class.rb | 39 +++++++++++++ .../persistence/repository/find.rb | 30 ++++++++++ .../persistence/repository/naming.rb | 37 ++++++++++++ .../repository/response/results.rb | 40 ++++++++++++- .../persistence/repository/search.rb | 36 ++++++++++++ .../persistence/repository/serialize.rb | 13 +++++ .../persistence/repository/store.rb | 7 +++ 9 files changed, 288 insertions(+), 2 deletions(-) diff --git a/elasticsearch-persistence/lib/elasticsearch/persistence.rb b/elasticsearch-persistence/lib/elasticsearch/persistence.rb index d27c01c7f..e6d215ad0 100644 --- a/elasticsearch-persistence/lib/elasticsearch/persistence.rb +++ b/elasticsearch-persistence/lib/elasticsearch/persistence.rb @@ -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 + # => # + # def client=(client) @client = client end diff --git a/elasticsearch-persistence/lib/elasticsearch/persistence/client.rb b/elasticsearch-persistence/lib/elasticsearch/persistence/client.rb index 7fee129e0..ece71b12b 100644 --- a/elasticsearch-persistence/lib/elasticsearch/persistence/client.rb +++ b/elasticsearch-persistence/lib/elasticsearch/persistence/client.rb @@ -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 diff --git a/elasticsearch-persistence/lib/elasticsearch/persistence/repository/class.rb b/elasticsearch-persistence/lib/elasticsearch/persistence/repository/class.rb index cce2a9ea2..55b6bc8d4 100644 --- a/elasticsearch-persistence/lib/elasticsearch/persistence/repository/class.rb +++ b/elasticsearch-persistence/lib/elasticsearch/persistence/repository/class.rb @@ -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 + # # => # + # # > repository.save(my_object) + # # => {"_index"=> ... } + # + # + # @example Shortcut use + # + # repository = Elasticsearch::Persistence::Repository.new + # # => # + # + # @example Configuration via a block + # + # repository = Elasticsearch::Persistence::Repository.new do + # index 'my_notes' + # end + # # => # + # # > 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 @@ -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 diff --git a/elasticsearch-persistence/lib/elasticsearch/persistence/repository/find.rb b/elasticsearch-persistence/lib/elasticsearch/persistence/repository/find.rb index 8c224da65..c6a9a6a4e 100644 --- a/elasticsearch-persistence/lib/elasticsearch/persistence/repository/find.rb +++ b/elasticsearch-persistence/lib/elasticsearch/persistence/repository/find.rb @@ -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) + # # => + # + # @example Retrieve multiple objects by IDs + # + # repository.find(1, 2) + # # => [, + # + # @return [Object,Array] + # def find(*args) options = args.last.is_a?(Hash) ? args.pop : {} ids = args @@ -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) ) @@ -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) ) diff --git a/elasticsearch-persistence/lib/elasticsearch/persistence/repository/naming.rb b/elasticsearch-persistence/lib/elasticsearch/persistence/repository/naming.rb index 4d58b7248..220abbed4 100644 --- a/elasticsearch-persistence/lib/elasticsearch/persistence/repository/naming.rb +++ b/elasticsearch-persistence/lib/elasticsearch/persistence/repository/naming.rb @@ -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) @@ -21,18 +28,32 @@ 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 @@ -40,10 +61,26 @@ def __get_klass_from_type(type) 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 diff --git a/elasticsearch-persistence/lib/elasticsearch/persistence/repository/response/results.rb b/elasticsearch-persistence/lib/elasticsearch/persistence/repository/response/results.rb index b269076b6..fe64ac9b0 100644 --- a/elasticsearch-persistence/lib/elasticsearch/persistence/repository/response/results.rb +++ b/elasticsearch-persistence/lib/elasticsearch/persistence/repository/response/results.rb @@ -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) @@ -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 @@ -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 diff --git a/elasticsearch-persistence/lib/elasticsearch/persistence/repository/search.rb b/elasticsearch-persistence/lib/elasticsearch/persistence/repository/search.rb index 01bc3916c..5f03a6526 100644 --- a/elasticsearch-persistence/lib/elasticsearch/persistence/repository/search.rb +++ b/elasticsearch-persistence/lib/elasticsearch/persistence/repository/search.rb @@ -2,7 +2,43 @@ module Elasticsearch module Persistence module Repository + # Returns a collection of domain objects by an Elasticsearch query + # module Search + + # Returns a collection of domain objects by an Elasticsearch query + # + # Pass the query either as a string or a Hash-like object + # + # @example Return objects matching a simple query + # + # repository.search('fox or dog') + # + # @example Return objects matching a query in the Elasticsearch DSL + # + # repository.search(query: { match: { title: 'fox dog' } }) + # + # @example Define additional search parameters, such as highlighted excerpts + # + # results = repository.search(query: { match: { title: 'fox dog' } }, highlight: { fields: { title: {} } }) + # results.map_with_hit { |d,h| h.highlight.title.join } + # # => ["quick brown fox", "fast white dog"] + # + # @example Perform aggregations as part of the request + # + # 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", ... ] + # + # @example Pass additional options to the search request, such as `size` + # + # repository.search query: { match: { title: 'fox dog' } }, size: 25 + # # GET http://localhost:9200/notes/note/_search + # # > {"query":{"match":{"title":"fox dog"}},"size":25} + # + # @return [Elasticsearch::Persistence::Repository::Response::Results] + # def search(query_or_definition, options={}) type = (klass ? __get_type_from_class(klass) : nil ) case diff --git a/elasticsearch-persistence/lib/elasticsearch/persistence/repository/serialize.rb b/elasticsearch-persistence/lib/elasticsearch/persistence/repository/serialize.rb index 5e4b4be00..027f000b6 100644 --- a/elasticsearch-persistence/lib/elasticsearch/persistence/repository/serialize.rb +++ b/elasticsearch-persistence/lib/elasticsearch/persistence/repository/serialize.rb @@ -2,11 +2,24 @@ module Elasticsearch module Persistence module Repository + # Provide serialization and deserialization between Ruby objects and Elasticsearch documents + # + # Override these methods in your repository class to customize the logic. + # module Serialize + + # Serialize the object for storing it in Elasticsearch + # + # In the default implementation, call the `to_hash` method on the passed object. + # def serialize(document) document.to_hash end + # Deserialize the document retrieved from Elasticsearch into a Ruby object + # + # Use the `klass` property, if defined, otherwise try to get the class from the document's `_type`. + # def deserialize(document) _klass = klass || __get_klass_from_type(document['_type']) _klass.new document['_source'] diff --git a/elasticsearch-persistence/lib/elasticsearch/persistence/repository/store.rb b/elasticsearch-persistence/lib/elasticsearch/persistence/repository/store.rb index 936aa1d79..07bdce569 100644 --- a/elasticsearch-persistence/lib/elasticsearch/persistence/repository/store.rb +++ b/elasticsearch-persistence/lib/elasticsearch/persistence/repository/store.rb @@ -2,7 +2,12 @@ module Elasticsearch module Persistence module Repository + # Save and delete documents in Elasticsearch + # module Store + + # Store the serialized object in Elasticsearch + # def save(document, options={}) serialized = serialize(document) id = __get_id_from_document(serialized) @@ -10,6 +15,8 @@ def save(document, options={}) client.index( { index: index_name, type: type, id: id, body: serialized }.merge(options) ) end + # Remove the serialized object or document with specified ID from Elasticsearch + # def delete(document, options={}) if document.is_a?(String) || document.is_a?(Integer) id = document From 817b64be7bcb4dd6bb04dc791d086d10635744f5 Mon Sep 17 00:00:00 2001 From: Karel Minarik Date: Fri, 4 Apr 2014 20:42:14 +0200 Subject: [PATCH 24/27] [STORE] Added a comprehensive usage information / tutorial to the README --- elasticsearch-persistence/README.md | 405 +++++++++++++++++++++++++++- 1 file changed, 404 insertions(+), 1 deletion(-) diff --git a/elasticsearch-persistence/README.md b/elasticsearch-persistence/README.md index b0eda41b0..b02aa99a7 100644 --- a/elasticsearch-persistence/README.md +++ b/elasticsearch-persistence/README.md @@ -1,6 +1,409 @@ # Elasticsearch::Persistence -WIP> Persistence layer for Ruby domain objects using the Repository and ActiveRecord patterns +This library provides a persistence layer for Ruby domain objects in Elasticsearch, +using the Repository and ActiveRecord patterns. + +The library is compatible with Ruby 1.9.3 (or higher) and Elasticsearch 1.0 (or higher). + +## Installation + +Install the package from [Rubygems](https://rubygems.org): + + gem install elasticsearch-persistence + +To use an unreleased version, either add it to your `Gemfile` for [Bundler](http://bundler.io): + + gem 'elasticsearch-persistence', git: 'git://github.com/elasticsearch/elasticsearch-rails.git' + +or install it from a source code checkout: + + git clone https://github.com/elasticsearch/elasticsearch-rails.git + cd elasticsearch-rails/elasticsearch-persistence + bundle install + rake install + +## Usage + +### The Repository Pattern + +The `Elasticsearch::Persistence::Repository` module provides an implementation of the +[repository pattern](http://martinfowler.com/eaaCatalog/repository.html) and allows +to save, delete, find and search objects stored in Elasticsearch, as well as configure +mappings and settings for the index. + +Let's have a simple, plain old Ruby object (PORO): + +```ruby +class Note + attr_reader :attributes + + def initialize(attributes={}) + @attributes = attributes + end + + def to_hash + @attributes + end +end +``` + +Let's create a default, "dumb" repository, as a first step: + +```ruby +require 'elasticsearch/persistence' +repository = Elasticsearch::Persistence::Repository.new +``` + +We can save a `Note` instance into the repository... + +```ruby +note = Note.new id: 1, text: 'Test' + +repository.save(note) +# PUT http://localhost:9200/repository/note/1 [status:201, request:0.210s, query:n/a] +# > {"id":1,"text":"Test"} +# < {"_index":"repository","_type":"note","_id":"1","_version":1,"created":true} +``` + +...find it... + +```ruby +n = repository.find(1) +# GET http://localhost:9200/repository/_all/1 [status:200, request:0.003s, query:n/a] +# < {"_index":"repository","_type":"note","_id":"1","_version":2,"found":true, "_source" : {"id":1,"text":"Test"}} +=> 1, "text"=>"Test"}> +``` + +...search for it... + +```ruby +repository.search(query: { match: { text: 'test' } }).first +# GET http://localhost:9200/repository/_search [status:200, request:0.005s, query:0.002s] +# > {"query":{"match":{"text":"test"}}} +# < {"took":2, ... "hits":{"total":1, ... "hits":[{ ... "_source" : {"id":1,"text":"Test"}}]}} +=> 1, "text"=>"Test"}> +``` + +...or delete it: + +```ruby +repository.delete(note) +# DELETE http://localhost:9200/repository/note/1 [status:200, request:0.014s, query:n/a] +# < {"found":true,"_index":"repository","_type":"note","_id":"1","_version":3} +=> {"found"=>true, "_index"=>"repository", "_type"=>"note", "_id"=>"1", "_version"=>2} +``` + +The repository module provides a number of features and facilities to configure and customize the behaviour: + +* Configuring the Elasticsearch [client](https://github.com/elasticsearch/elasticsearch-ruby#usage) being used +* Setting the index name, document type, and object class for deserialization +* Composing mappings and settings for the index +* Creating, deleting or refreshing the index +* Finding or searching for documents +* Providing access both to domain objects and hits for search results +* Providing access to the Elasticsearch response for search results (aggregations, total, ...) +* Defining the methods for serialization and deserialization + +You can use the default repository class, or include the module in your own. Let's review it in detail. + +#### The Default Class + +For simple cases, you can use the default, bundled repository class, and configure/customize it: + +```ruby +repository = Elasticsearch::Persistence::Repository.new do + # Configure the Elasticsearch client + client Elasticsearch::Client.new url: ENV['ELASTICSEARCH_URL'], log: true + + # Set a custom index name + index :my_notes + + # Set a custom document type + type :my_note + + # Specify the class to inicialize when deserializing documents + klass Note + + # Configure the settings and mappings for the Elasticsearch index + settings number_of_shards: 1 do + mapping do + indexes :text, analyzer: 'snowball' + end + end + + # Customize the serialization logic + def serialize(document) + super.merge(my_special_key: 'my_special_stuff') + end + + # Customize the de-serialization logic + def deserialize(document) + puts "# ***** CUSTOM DESERIALIZE LOGIC KICKING IN... *****" + super + end +end +``` + +The custom Elasticsearch client will be used now, with a custom index and type names, +as well as the custom serialization and de-serialization logic. + +We can create the index with the desired settings and mappings: + +```ruby +repository.create_index! force: true +# PUT http://localhost:9200/my_notes +# > {"settings":{"number_of_shards":1},"mappings":{ ... {"text":{"analyzer":"snowball","type":"string"}}}}} +``` + +Save the document with extra properties added by the `serialize` method: + +```ruby +repository.save(note) +# PUT http://localhost:9200/my_notes/my_note/1 +# > {"id":1,"text":"Test","my_special_key":"my_special_stuff"} +{"_index"=>"my_notes", "_type"=>"my_note", "_id"=>"1", "_version"=>4, ... } +``` + +And `deserialize` it: + +```ruby +repository.find(1) +# ***** CUSTOM DESERIALIZE LOGIC KICKING IN... ***** +"my_special_stuff"}> +``` + +#### A Custom Class + +In most cases, though, you'll want to use a custom class for the repository, so let's do that: + +```ruby +require 'base64' + +class NoteRepository + include Elasticsearch::Persistence::Repository + + def initialize(options={}) + index options[:index] || 'notes' + client Elasticsearch::Client.new url: options[:url], log: options[:log] + end + + klass Note + + settings number_of_shards: 1 do + mapping do + indexes :text, analyzer: 'snowball' + # Do not index images + indexes :image, index: 'no' + end + end + + # Base64 encode the "image" field in the document + # + def serialize(document) + hash = document.to_hash.clone + hash['image'] = Base64.encode64(hash['image']) if hash['image'] + hash.to_hash + end + + # Base64 decode the "image" field in the document + # + def deserialize(document) + hash = document['_source'] + hash['image'] = Base64.decode64(hash['image']) if hash['image'] + klass.new hash + end +end +``` + +Include the `Elasticsearch::Persistence::Repository` module to add the repository methods into the class. + +You can customize the repository in the familiar way, by calling the DSL-like methods. + +You can implement a custom initializer for your repository, add complex logic in its +class and instance methods -- in general, have all the freedom of a standard Ruby class. + +```ruby +repository = NoteRepository.new url: 'http://localhost:9200', log: true + +# Configure the repository instance +repository.index = 'notes_development' +repository.client.transport.logger.formatter = proc { |s, d, p, m| "\e[2m# #{m}\n\e[0m" } + +repository.create_index! force: true + +note = Note.new 'id' => 1, 'text' => 'Document with image', 'image' => '... BINARY DATA ...' + +repository.save(note) +# PUT http://localhost:9200/notes_development/note/1 +# > {"id":1,"text":"Document with image","image":"Li4uIEJJTkFSWSBEQVRBIC4uLg==\n"} +puts repository.find(1).attributes['image'] +# GET http://localhost:9200/notes_development/note/1 +# < {... "_source" : { ... "image":"Li4uIEJJTkFSWSBEQVRBIC4uLg==\n"}} +# => ... BINARY DATA ... +``` + +#### Methods Provided by the Repository + +##### Client + +The repository uses the standard Elasticsearch [client](https://github.com/elasticsearch/elasticsearch-ruby#usage), +which is accessible with the `client` getter and setter methods: + +```ruby +repository.client = Elasticsearch::Client.new url: 'http://search.server.org' +repository.client.transport.logger = Logger.new(STDERR) +``` + +##### Naming + +The `index` method specifies the Elasticsearch index to use for storage, lookup and search +(when not set, the value is inferred from the repository class name): + +```ruby +repository.index = 'notes_development' +``` + +The `type` method specifies the Elasticsearch document type to use for storage, lookup and search +(when not set, the value is inferred from the document class name, or `_all` is used): + +```ruby +repository.type = 'my_note' +``` + +The `klass` method specifies the Ruby class name to use when initializing objects from +documents retrieved from the repository (when not set, the value is inferred from the +document `_type` as fetched from Elasticsearch): + +```ruby +repository.klass = MyNote +``` + +##### Index Configuration + +The `settings` and `mappings` methods, provided by the +[`elasticsearch-model`](http://rubydoc.info/gems/elasticsearch-model/Elasticsearch/Model/Indexing/ClassMethods) +gem, allow to configure the index properties: + +```ruby +repository.settings number_of_shards: 1 +repository.settings.to_hash +# => {:number_of_shards=>1} + +repository.mappings { indexes :title, analyzer: 'snowball' } +repository.mappings.to_hash +# => { :note => {:properties=> ... }} +``` + +The convenience methods `create_index!`, `delete_index!` and `refresh_index!` allow you to manage the index lifecycle. + +##### Serialization + +The `serialize` and `deserialize` methods allow you to customize the serialization of the document when passing it +to the storage, and the initialization procedure when loading it from the storage: + +```ruby +class NoteRepository + def serialize(document) + Hash[document.to_hash.map() { |k,v| v.upcase! if k == :title; [k,v] }] + end + def deserialize(document) + MyNote.new ActiveSupport::HashWithIndifferentAccess.new(document['_source']).deep_symbolize_keys + end +end +``` + +##### Storage + +The `save` method allows you to store a domain object in the repository: + +```ruby +note = Note.new id: 1, title: 'Quick Brown Fox' +repository.save(note) +# => {"_index"=>"notes_development", "_type"=>"my_note", "_id"=>"1", "_version"=>1, "created"=>true} +``` + +The `delete` method allows to remove objects from the repository (pass either the object itself or its ID): + +```ruby +repository.delete(note) +repository.delete(1) +``` + +##### Finding + +The `find` method allows to find one or many documents in the storage and returns them as deserialized Ruby objects: + +```ruby +repository.save Note.new(id: 2, title: 'Fast White Dog') + +note = repository.find(1) +# => + +notes = repository.find(1, 2) +# => [, ] +``` + +When the document with a specific ID isn't found, a `nil` is returned instead of the deserialized object: + +```ruby +notes = repository.find(1, 3, 2) +# => [, nil, ] +``` + +Handle the missing objects in the application code, or call `compact` on the result. + +##### Search + +The `search` method to retrieve objects from the repository by a query string or definition in the Elasticsearch DSL: + +```ruby +repository.search('fox or dog').to_a +# GET http://localhost:9200/notes_development/my_note/_search?q=fox +# => [, ] + +repository.search(query: { match: { title: 'fox dog' } }).to_a +# GET http://localhost:9200/notes_development/my_note/_search +# > {"query":{"match":{"title":"fox dog"}}} +# => [, ] +``` + +The returned object is an instance of the `Elasticsearch::Persistence::Repository::Response::Results` class, +which provides access to the results, the full returned response and hits. + +```ruby +results = repository.search(query: { match: { title: 'fox dog' } }) + +# Iterate over the objects +# +results.each do |note| + puts "* #{note.attributes[:title]}" +end +# * QUICK BROWN FOX +# * FAST WHITE DOG + +# Iterate over the objects and hits +# +results.each_with_hit do |note, hit| + puts "* #{note.attributes[:title]}, score: #{hit._score}" +end +# * QUICK BROWN FOX, score: 0.29930896 +# * FAST WHITE DOG, score: 0.29930896 + +# Get total results +# +results.total +# => 2 + +# Access the raw response as a Hashie::Mash instance +results.response._shards.failed +# => 0 +``` + +### The ActiveRecord Pattern + +_Work in progress_. The ActiveRecord [pattern](http://www.martinfowler.com/eaaCatalog/activeRecord.html) will work +in a very similar way as `Tire::Model::Persistence`, allowing a drop-in replacement of an Elasticsearch-backed model +in Ruby on Rails applications. ## License From 170b8e2f3659eba6ff582d650e83690a991ae3d9 Mon Sep 17 00:00:00 2001 From: Karel Minarik Date: Mon, 7 Apr 2014 14:24:33 +0200 Subject: [PATCH 25/27] [STORE] Added an example Sinatra web application for the repository pattern --- .../examples/sinatra/.gitignore | 7 + .../examples/sinatra/Gemfile | 28 +++ .../examples/sinatra/README.markdown | 36 +++ .../examples/sinatra/application.rb | 238 ++++++++++++++++++ .../examples/sinatra/config.ru | 7 + .../examples/sinatra/test.rb | 118 +++++++++ 6 files changed, 434 insertions(+) create mode 100644 elasticsearch-persistence/examples/sinatra/.gitignore create mode 100644 elasticsearch-persistence/examples/sinatra/Gemfile create mode 100644 elasticsearch-persistence/examples/sinatra/README.markdown create mode 100644 elasticsearch-persistence/examples/sinatra/application.rb create mode 100644 elasticsearch-persistence/examples/sinatra/config.ru create mode 100644 elasticsearch-persistence/examples/sinatra/test.rb diff --git a/elasticsearch-persistence/examples/sinatra/.gitignore b/elasticsearch-persistence/examples/sinatra/.gitignore new file mode 100644 index 000000000..e9d847d61 --- /dev/null +++ b/elasticsearch-persistence/examples/sinatra/.gitignore @@ -0,0 +1,7 @@ +.DS_Store +Gemfile.lock +tmp/* +log/* +doc/ +.yardoc +.vagrant diff --git a/elasticsearch-persistence/examples/sinatra/Gemfile b/elasticsearch-persistence/examples/sinatra/Gemfile new file mode 100644 index 000000000..63d5d75c5 --- /dev/null +++ b/elasticsearch-persistence/examples/sinatra/Gemfile @@ -0,0 +1,28 @@ +source 'https://rubygems.org' + +gem 'rake' +gem 'ansi' + +gem 'multi_json' +gem 'oj' +gem 'hashie' + +gem 'patron' +gem 'elasticsearch' +gem 'elasticsearch-model', path: File.expand_path('../../../../elasticsearch-model', __FILE__) +gem 'elasticsearch-persistence', path: File.expand_path('../../../', __FILE__) + +gem 'sinatra', require: false +gem 'thin' + +group :development do + gem 'sinatra-contrib' +end + +group :test do + gem 'elasticsearch-extensions' + gem 'rack-test' + gem 'shoulda-context' + gem 'turn' + gem 'mocha' +end diff --git a/elasticsearch-persistence/examples/sinatra/README.markdown b/elasticsearch-persistence/examples/sinatra/README.markdown new file mode 100644 index 000000000..2aa15ec32 --- /dev/null +++ b/elasticsearch-persistence/examples/sinatra/README.markdown @@ -0,0 +1,36 @@ +Demo Aplication for the Repository Pattern +========================================== + +This directory contains a simple demo application for the repository pattern of the `Elasticsearch::Persistence` +module in the [Sinatra](http://www.sinatrarb.com) framework. + +To run the application, first install the required gems and start the application: + +``` +bundle install +bundle exec ruby application.rb +``` + +The application demonstrates: + +* How to use a plain old Ruby object (PORO) as the domain model +* How to set up, configure and use the repository instance +* How to use the repository in tests + +## License + +This software is licensed under the Apache 2 license, quoted below. + + Copyright (c) 2014 Elasticsearch + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/elasticsearch-persistence/examples/sinatra/application.rb b/elasticsearch-persistence/examples/sinatra/application.rb new file mode 100644 index 000000000..50f56dece --- /dev/null +++ b/elasticsearch-persistence/examples/sinatra/application.rb @@ -0,0 +1,238 @@ +$LOAD_PATH.unshift File.expand_path('../../../lib/', __FILE__) + +require 'sinatra/base' + +require 'multi_json' +require 'oj' +require 'hashie/mash' + +require 'elasticsearch' +require 'elasticsearch/model' +require 'elasticsearch/persistence' + +class Note + attr_reader :attributes + + def initialize(attributes={}) + @attributes = Hashie::Mash.new(attributes) + __add_date + __extract_tags + __truncate_text + self + end + + def method_missing(method_name, *arguments, &block) + attributes.respond_to?(method_name) ? attributes.__send__(method_name, *arguments, &block) : super + end + + def respond_to?(method_name, include_private=false) + attributes.respond_to?(method_name) || super + end + + def tags; attributes.tags || []; end + + def to_hash + @attributes.to_hash + end + + def __extract_tags + tags = attributes['text'].scan(/(\[\w+\])/).flatten if attributes['text'] + unless tags.nil? || tags.empty? + attributes.update 'tags' => tags.map { |t| t.tr('[]', '') } + attributes['text'].gsub!(/(\[\w+\])/, '').strip! + end + end + + def __add_date + attributes['created_at'] ||= Time.now.utc.iso8601 + end + + def __truncate_text + attributes['text'] = attributes['text'][0...80] + ' (...)' if attributes['text'] && attributes['text'].size > 80 + end +end + +class NoteRepository + include Elasticsearch::Persistence::Repository + + client Elasticsearch::Client.new url: ENV['ELASTICSEARCH_URL'], log: true + + index :notes + type :note + + mapping do + indexes :text, analyzer: 'snowball' + indexes :tags, analyzer: 'keyword' + indexes :created_at, type: 'date' + end + + create_index! + + def deserialize(document) + Note.new document['_source'].merge('id' => document['_id']) + end +end unless defined?(NoteRepository) + +class Application < Sinatra::Base + enable :logging + enable :inline_templates + enable :method_override + + configure :development do + enable :dump_errors + disable :show_exceptions + + require 'sinatra/reloader' + register Sinatra::Reloader + end + + set :repository, NoteRepository.new + set :per_page, 25 + + get '/' do + @page = [ params[:p].to_i, 1 ].max + + @notes = settings.repository.search \ + query: ->(q, t) do + query = if q && !q.empty? + { match: { text: q } } + else + { match_all: {} } + end + + filter = if t && !t.empty? + { term: { tags: t } } + end + + if filter + { filtered: { query: query, filter: filter } } + else + query + end + end.(params[:q], params[:t]), + + sort: [{created_at: {order: 'desc'}}], + + size: settings.per_page, + from: settings.per_page * (@page-1), + + aggregations: { tags: { terms: { field: 'tags' } } }, + + highlight: { fields: { text: { fragment_size: 0, pre_tags: [''],post_tags: [''] } } } + + erb :index + end + + post '/' do + unless params[:text].empty? + @note = Note.new params + settings.repository.save(@note, refresh: true) + end + + redirect back + end + + delete '/:id' do |id| + settings.repository.delete(id, refresh: true) + redirect back + end +end + +Application.run! if $0 == __FILE__ + +__END__ + +@@ layout + + + + Notes + + + + +<%= yield %> + + + +@@ index + +
+

Notes

+
+ +
+
+ +
+

All notes <%= @notes.size %>

+
    + <% @notes.response.aggregations.tags.buckets.each do |term| %> +
  • <%= term['key'] %> <%= term['doc_count'] %>
  • + <% end %> +
+

Add a note

+
+

+

+
+
+ +
+<% if @notes.empty? %> +

No notes found.

+<% end %> + +<% @notes.each_with_hit do |note, hit| %> +
+

+ <%= hit.highlight && hit.highlight.size > 0 ? hit.highlight.text.first : note.text %> + + <% note.tags.each do |tag| %> <%= tag %><% end %> + <%= Time.parse(note.created_at).strftime('%d/%m/%Y %H:%M') %> + +

+

+
+<% end %> + +<% if @notes.size > 0 && @page.next <= @notes.total / settings.per_page %> +

→ Load next

+<% end %> +
diff --git a/elasticsearch-persistence/examples/sinatra/config.ru b/elasticsearch-persistence/examples/sinatra/config.ru new file mode 100644 index 000000000..98f8403ad --- /dev/null +++ b/elasticsearch-persistence/examples/sinatra/config.ru @@ -0,0 +1,7 @@ +#\ --port 3000 --server thin + +require File.expand_path('../application', __FILE__) + +map '/' do + run Application +end diff --git a/elasticsearch-persistence/examples/sinatra/test.rb b/elasticsearch-persistence/examples/sinatra/test.rb new file mode 100644 index 000000000..cb9528747 --- /dev/null +++ b/elasticsearch-persistence/examples/sinatra/test.rb @@ -0,0 +1,118 @@ +ENV['RACK_ENV'] = 'test' + +at_exit { Elasticsearch::Test::IntegrationTestCase.__run_at_exit_hooks } if ENV['SERVER'] + +require 'test/unit' +require 'shoulda-context' +require 'mocha/setup' +require 'rack/test' +require 'turn' + +require 'elasticsearch/extensions/test/cluster' +require 'elasticsearch/extensions/test/startup_shutdown' + +require_relative 'application' + +NoteRepository.index_name = 'notes_test' + +class Elasticsearch::Persistence::ExampleApplicationTest < Test::Unit::TestCase + include Rack::Test::Methods + alias :response :last_response + + def app + Application.new + end + + context "Note" do + should "be initialized with a Hash" do + note = Note.new 'foo' => 'bar' + assert_equal 'bar', note.attributes['foo'] + end + + should "add created_at when it's not passed" do + note = Note.new + assert_not_nil note.created_at + assert_match /#{Time.now.year}/, note.created_at + end + + should "not add created_at when it's passed" do + note = Note.new 'created_at' => 'FOO' + assert_equal 'FOO', note.created_at + end + + should "trim long text" do + assert_equal 'Hello World', Note.new('text' => 'Hello World').text + assert_equal 'FOOFOOFOOFOOFOOFOOFOOFOOFOOFOOFOOFOOFOOFOOFOOFOOFOOFOOFOOFOOFOOFOOFOOFOOFOOFOOFO (...)', + Note.new('text' => 'FOO'*200).text + end + + should "delegate methods to attributes" do + note = Note.new 'foo' => 'bar' + assert_equal 'bar', note.foo + end + + should "have tags" do + assert_not_nil Note.new.tags + end + + should "provide a `to_hash` method" do + note = Note.new 'foo' => 'bar' + assert_instance_of Hash, note.to_hash + assert_equal ['created_at', 'foo'], note.to_hash.keys.sort + end + + should "extract tags from the text" do + note = Note.new 'text' => 'Hello [foo] [bar]' + assert_equal 'Hello', note.text + assert_equal ['foo', 'bar'], note.tags + end + end + + context "Application" do + setup do + app.settings.repository.client = Elasticsearch::Client.new \ + hosts: [{ host: 'localhost', port: ENV.fetch('TEST_CLUSTER_PORT', 9250)}], + log: true + app.settings.repository.client.transport.logger.formatter = proc { |s, d, p, m| "\e[2m#{m}\n\e[0m" } + app.settings.repository.create_index! force: true + app.settings.repository.client.cluster.health wait_for_status: 'yellow' + end + + should "have the correct index name" do + assert_equal 'notes_test', app.settings.repository.index + end + + should "display empty page when there are no notes" do + get '/' + assert response.ok?, response.status.to_s + assert_match /No notes found/, response.body.to_s + end + + should "display the notes" do + app.settings.repository.save Note.new('text' => 'Hello') + app.settings.repository.refresh_index! + + get '/' + assert response.ok?, response.status.to_s + assert_match /

\s*Hello/, response.body.to_s + end + + should "create a note" do + post '/', { 'text' => 'Hello World' } + follow_redirect! + + assert response.ok?, response.status.to_s + assert_match /Hello World/, response.body.to_s + end + + should "delete a note" do + app.settings.repository.save Note.new('id' => 'foobar', 'text' => 'Perish...') + delete "/foobar" + follow_redirect! + + assert response.ok?, response.status.to_s + assert_no_match /Perish/, response.body.to_s + end + end + +end From 5ba85330a0c3e4258388e7a9d8b6a6f9ccce02d0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Josef=20Pospi=CC=81s=CC=8Cil?= Date: Tue, 15 Apr 2014 17:20:22 +0200 Subject: [PATCH 26/27] [FIX] Fix search with type specified in the class --- .../lib/elasticsearch/persistence/repository/search.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/elasticsearch-persistence/lib/elasticsearch/persistence/repository/search.rb b/elasticsearch-persistence/lib/elasticsearch/persistence/repository/search.rb index 5f03a6526..a3843df51 100644 --- a/elasticsearch-persistence/lib/elasticsearch/persistence/repository/search.rb +++ b/elasticsearch-persistence/lib/elasticsearch/persistence/repository/search.rb @@ -40,7 +40,7 @@ module Search # @return [Elasticsearch::Persistence::Repository::Response::Results] # def search(query_or_definition, options={}) - type = (klass ? __get_type_from_class(klass) : nil ) + type = document_type || (klass ? __get_type_from_class(klass) : nil ) case when query_or_definition.respond_to?(:to_hash) response = client.search( { index: index_name, type: type, body: query_or_definition.to_hash }.merge(options) ) From d6b1bcb63c4fd9ab25ca593371159f8d1bf06fa8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Josef=20Pospi=CC=81s=CC=8Cil?= Date: Tue, 15 Apr 2014 17:48:11 +0200 Subject: [PATCH 27/27] [FIX] Add stub for document type for search test --- elasticsearch-persistence/test/unit/repository_search_test.rb | 1 + 1 file changed, 1 insertion(+) diff --git a/elasticsearch-persistence/test/unit/repository_search_test.rb b/elasticsearch-persistence/test/unit/repository_search_test.rb index 711ded396..8229075bb 100644 --- a/elasticsearch-persistence/test/unit/repository_search_test.rb +++ b/elasticsearch-persistence/test/unit/repository_search_test.rb @@ -8,6 +8,7 @@ class MyDocument; end @shoulda_subject = Class.new() { include Elasticsearch::Persistence::Repository::Search }.new @client = mock + @shoulda_subject.stubs(:document_type).returns(nil) @shoulda_subject.stubs(:klass).returns(nil) @shoulda_subject.stubs(:index_name).returns('test') @shoulda_subject.stubs(:client).returns(@client)