From 03083bbedd973f12c2645fa58ee527bac660544e Mon Sep 17 00:00:00 2001 From: Emily Stolfo Date: Wed, 27 Jun 2018 12:32:59 +0200 Subject: [PATCH 01/87] Update gitignore --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index 6076dc96c..43ddc2e30 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,7 @@ .DS_Store *.log tmp/ +.idea/* .yardoc/ _yardoc/ From 719026cbcdfb9c34ed8a6ff70419c6d6cae0d755 Mon Sep 17 00:00:00 2001 From: Emily Stolfo Date: Wed, 27 Jun 2018 12:35:11 +0200 Subject: [PATCH 02/87] Update versions to 6.0.0.beta --- elasticsearch-model/lib/elasticsearch/model/version.rb | 2 +- .../lib/elasticsearch/persistence/version.rb | 2 +- elasticsearch-rails/lib/elasticsearch/rails/version.rb | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/elasticsearch-model/lib/elasticsearch/model/version.rb b/elasticsearch-model/lib/elasticsearch/model/version.rb index cfa7e4ab6..2ee49f5ad 100644 --- a/elasticsearch-model/lib/elasticsearch/model/version.rb +++ b/elasticsearch-model/lib/elasticsearch/model/version.rb @@ -1,5 +1,5 @@ module Elasticsearch module Model - VERSION = "6.0.0.alpha1" + VERSION = "6.0.0.beta" end end diff --git a/elasticsearch-persistence/lib/elasticsearch/persistence/version.rb b/elasticsearch-persistence/lib/elasticsearch/persistence/version.rb index d54a3a37f..e80ab0457 100644 --- a/elasticsearch-persistence/lib/elasticsearch/persistence/version.rb +++ b/elasticsearch-persistence/lib/elasticsearch/persistence/version.rb @@ -1,5 +1,5 @@ module Elasticsearch module Persistence - VERSION = "6.0.0.alpha1" + VERSION = "6.0.0.beta" end end diff --git a/elasticsearch-rails/lib/elasticsearch/rails/version.rb b/elasticsearch-rails/lib/elasticsearch/rails/version.rb index 4b65f5196..535aaaa0a 100644 --- a/elasticsearch-rails/lib/elasticsearch/rails/version.rb +++ b/elasticsearch-rails/lib/elasticsearch/rails/version.rb @@ -1,5 +1,5 @@ module Elasticsearch module Rails - VERSION = "6.0.0.alpha1" + VERSION = "6.0.0.beta" end end From 3ae2f832a8a39be2e6caf1fd69da84be133d41d0 Mon Sep 17 00:00:00 2001 From: Emily Stolfo Date: Wed, 27 Jun 2018 14:32:06 +0200 Subject: [PATCH 03/87] [CI] Test 6.x branch in travis --- .travis.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.travis.yml b/.travis.yml index fed580991..cf39473ad 100644 --- a/.travis.yml +++ b/.travis.yml @@ -16,6 +16,7 @@ branches: - master - travis - 5.x + - 6.x - 2.x matrix: From 35aa3acaadb56b48025c1e6b0afcae984c0e2848 Mon Sep 17 00:00:00 2001 From: Emily S Date: Wed, 27 Jun 2018 15:54:04 +0200 Subject: [PATCH 04/87] [CI] Use docker to launch Elasticsearch in rake task (#803) --- .travis.yml | 9 ++++----- Rakefile | 25 ++++++++++++++++++++++++- 2 files changed, 28 insertions(+), 6 deletions(-) diff --git a/.travis.yml b/.travis.yml index cf39473ad..99a2571de 100644 --- a/.travis.yml +++ b/.travis.yml @@ -33,16 +33,15 @@ matrix: jdk: oraclejdk8 env: TEST_SUITE=unit - - rvm: 2.3.3 + - rvm: 2.5.1 jdk: oraclejdk8 - env: TEST_SUITE=integration QUIET=y SERVER=start TEST_CLUSTER_LOGS=/tmp/log TEST_CLUSTER_COMMAND=/tmp/elasticsearch-6.3.0/bin/elasticsearch + env: TEST_SUITE=integration QUIET=y before_install: - - gem update --system --no-rdoc --no-ri + - gem update --system -q + - gem update bundler -q - gem --version - - gem install bundler -v 1.14.3 --no-rdoc --no-ri - bundle version - - curl -sS https://artifacts.elastic.co/downloads/elasticsearch/elasticsearch-6.3.0.tar.gz | tar xz -C /tmp install: - bundle install diff --git a/Rakefile b/Rakefile index b4c90d710..76479929d 100644 --- a/Rakefile +++ b/Rakefile @@ -59,8 +59,31 @@ namespace :test do end end + desc "Run Elasticsearch (Docker)" + task :setup_elasticsearch do + begin + sh <<-COMMAND.gsub(/^\s*/, '').gsub(/\s{1,}/, ' ') + docker stop $(docker ps -aq); + docker rm $(docker ps -aq); + docker rmi $(docker images -q); + docker run -d=true \ + --env "discovery.type=single-node" \ + --env "cluster.name=elasticsearch-rails" \ + --env "http.port=9200" \ + --env "cluster.routing.allocation.disk.threshold_enabled=false" \ + --publish 9250:9200 \ + --rm \ + docker.elastic.co/elasticsearch/elasticsearch:6.3.0 + COMMAND + require 'elasticsearch/extensions/test/cluster' + Elasticsearch::Extensions::Test::Cluster::Cluster.new(version: '6.3.0', + number_of_nodes: 1).wait_for_green + rescue + end + end + desc "Run integration tests in all subprojects" - task :integration do + task :integration => :setup_elasticsearch do # 1/ elasticsearch-model # puts '-'*80 From af894c14ef4dcf285d37737daa8792ff25adc5e8 Mon Sep 17 00:00:00 2001 From: Emily Stolfo Date: Thu, 28 Jun 2018 10:20:01 +0200 Subject: [PATCH 05/87] [CI] Don't delete all images and containers in docker rake task --- Rakefile | 3 --- 1 file changed, 3 deletions(-) diff --git a/Rakefile b/Rakefile index 76479929d..c953c5c48 100644 --- a/Rakefile +++ b/Rakefile @@ -63,9 +63,6 @@ namespace :test do task :setup_elasticsearch do begin sh <<-COMMAND.gsub(/^\s*/, '').gsub(/\s{1,}/, ' ') - docker stop $(docker ps -aq); - docker rm $(docker ps -aq); - docker rmi $(docker images -q); docker run -d=true \ --env "discovery.type=single-node" \ --env "cluster.name=elasticsearch-rails" \ From 6cc201e3e3cb8705f7270183d80d4ba7ee7d6bff Mon Sep 17 00:00:00 2001 From: Emily S Date: Thu, 28 Jun 2018 15:45:46 +0200 Subject: [PATCH 06/87] [MODEL] Update child-parent integration test to use single index type for ES 6.3 (#805) * [MODEL] Update child-parent integration test to use single index type for ES 6.3 * [CI] Expand unit test matrix and only test integration on latest stable Rails, latest stable Ruby * [CI] Don't test JRuby in Travis until newer version of elasticsearch-extensions gem is released without oj/patron dependencies --- .travis.yml | 16 ++- elasticsearch-model/Rakefile | 6 +- .../elasticsearch-model.gemspec | 2 +- ...e_record_associations_parent_child_test.rb | 109 ++++++++++++------ .../elasticsearch-persistence.gemspec | 2 +- .../elasticsearch-rails.gemspec | 2 +- 6 files changed, 93 insertions(+), 44 deletions(-) diff --git a/.travis.yml b/.travis.yml index 99a2571de..51766c1e1 100644 --- a/.travis.yml +++ b/.travis.yml @@ -21,19 +21,27 @@ branches: matrix: include: - - rvm: 2.2.10 + - rvm: 2.2 jdk: oraclejdk8 env: TEST_SUITE=unit - - rvm: 2.3.7 + - rvm: 2.3 jdk: oraclejdk8 env: TEST_SUITE=unit - - rvm: 2.5.1 + - rvm: 2.4 jdk: oraclejdk8 env: TEST_SUITE=unit - - rvm: 2.5.1 + - rvm: 2.5 + jdk: oraclejdk8 + env: TEST_SUITE=unit + +# - rvm: jruby-9.1 +# jdk: oraclejdk8 +# env: TEST_SUITE=unit + + - rvm: 2.5 jdk: oraclejdk8 env: TEST_SUITE=integration QUIET=y diff --git a/elasticsearch-model/Rakefile b/elasticsearch-model/Rakefile index 1efad46da..f80c46b23 100644 --- a/elasticsearch-model/Rakefile +++ b/elasticsearch-model/Rakefile @@ -38,10 +38,10 @@ namespace :test do sh "BUNDLE_GEMFILE='#{File.expand_path('../gemfiles/5.0.gemfile', __FILE__)}' bundle exec rake test:run_unit" end - desc "Run integration tests against ActiveModel 3, 4 and 5" + desc "Run integration tests against latest stable ActiveModel (5)" task :integration do - sh "BUNDLE_GEMFILE='#{File.expand_path('../gemfiles/3.0.gemfile', __FILE__)}' bundle exec rake test:run_integration" - sh "BUNDLE_GEMFILE='#{File.expand_path('../gemfiles/4.0.gemfile', __FILE__)}' bundle exec rake test:run_integration" + #sh "BUNDLE_GEMFILE='#{File.expand_path('../gemfiles/3.0.gemfile', __FILE__)}' bundle exec rake test:run_integration" + #sh "BUNDLE_GEMFILE='#{File.expand_path('../gemfiles/4.0.gemfile', __FILE__)}' bundle exec rake test:run_integration" sh "BUNDLE_GEMFILE='#{File.expand_path('../gemfiles/5.0.gemfile', __FILE__)}' bundle exec rake test:run_integration" end diff --git a/elasticsearch-model/elasticsearch-model.gemspec b/elasticsearch-model/elasticsearch-model.gemspec index e058ac43d..5cd1bcec1 100644 --- a/elasticsearch-model/elasticsearch-model.gemspec +++ b/elasticsearch-model/elasticsearch-model.gemspec @@ -35,7 +35,7 @@ Gem::Specification.new do |s| s.add_development_dependency "sqlite3" s.add_development_dependency "activemodel", "> 3" - s.add_development_dependency "oj" + s.add_development_dependency "oj" unless defined?(JRUBY_VERSION) s.add_development_dependency "kaminari" s.add_development_dependency "will_paginate" diff --git a/elasticsearch-model/test/integration/active_record_associations_parent_child_test.rb b/elasticsearch-model/test/integration/active_record_associations_parent_child_test.rb index de5db34b0..fb3b07e1d 100644 --- a/elasticsearch-model/test/integration/active_record_associations_parent_child_test.rb +++ b/elasticsearch-model/test/integration/active_record_associations_parent_child_test.rb @@ -11,7 +11,11 @@ class Question < ActiveRecord::Base has_many :answers, dependent: :destroy - index_name 'questions_and_answers' + JOIN_TYPE = 'question'.freeze + JOIN_METADATA = { join_field: JOIN_TYPE}.freeze + + index_name 'questions_and_answers'.freeze + document_type 'doc'.freeze mapping do indexes :title @@ -19,6 +23,12 @@ class Question < ActiveRecord::Base indexes :author end + def as_indexed_json(options={}) + # This line is necessary for differences between ActiveModel::Serializers::JSON#as_json versions + json = as_json(options)[JOIN_TYPE] || as_json(options) + json.merge(JOIN_METADATA) + end + after_commit lambda { __elasticsearch__.index_document }, on: :create after_commit lambda { __elasticsearch__.update_document }, on: :update after_commit lambda { __elasticsearch__.delete_document }, on: :destroy @@ -29,32 +39,55 @@ class Answer < ActiveRecord::Base belongs_to :question - index_name 'questions_and_answers' + JOIN_TYPE = 'answer'.freeze + + index_name 'questions_and_answers'.freeze + document_type 'doc'.freeze + + before_create :randomize_id + + def randomize_id + begin + self.id = SecureRandom.random_number(1_000_000) + end while Answer.where(id: self.id).exists? + end - mapping _parent: { type: 'question' }, _routing: { required: true } do + mapping do indexes :text indexes :author end - after_commit lambda { __elasticsearch__.index_document(parent: question_id) }, on: :create - after_commit lambda { __elasticsearch__.update_document(parent: question_id) }, on: :update - after_commit lambda { __elasticsearch__.delete_document(parent: question_id) }, on: :destroy + def as_indexed_json(options={}) + # This line is necessary for differences between ActiveModel::Serializers::JSON#as_json versions + json = as_json(options)[JOIN_TYPE] || as_json(options) + json.merge(join_field: { name: JOIN_TYPE, parent: question_id }) + end + + after_commit lambda { __elasticsearch__.index_document(routing: (question_id || 1)) }, on: :create + after_commit lambda { __elasticsearch__.update_document(routing: (question_id || 1)) }, on: :update + after_commit lambda {__elasticsearch__.delete_document(routing: (question_id || 1)) }, on: :destroy end module ParentChildSearchable - INDEX_NAME = 'questions_and_answers' + INDEX_NAME = 'questions_and_answers'.freeze + JOIN = 'join'.freeze def create_index!(options={}) client = Question.__elasticsearch__.client client.indices.delete index: INDEX_NAME rescue nil if options[:force] settings = Question.settings.to_hash.merge Answer.settings.to_hash - mappings = Question.mappings.to_hash.merge Answer.mappings.to_hash + mapping_properties = { join_field: { type: JOIN, + relations: { Question::JOIN_TYPE => Answer::JOIN_TYPE } } } + + merged_properties = mapping_properties.merge(Question.mappings.to_hash[:doc][:properties]).merge( + Answer.mappings.to_hash[:doc][:properties]) + mappings = { doc: { properties: merged_properties }} client.indices.create index: INDEX_NAME, body: { - settings: settings.to_hash, - mappings: mappings.to_hash } + settings: settings.to_hash, + mappings: mappings } end extend self @@ -100,34 +133,34 @@ class ActiveRecordAssociationsParentChildIntegrationTest < Elasticsearch::Test:: should "find questions by matching answers" do response = Question.search( - { query: { - has_child: { - type: 'answer', - query: { - match: { - author: 'john' - } - } - } - } - }) + { query: { + has_child: { + type: 'answer', + query: { + match: { + author: 'john' + } + } + } + } + }) assert_equal 'Second Question', response.records.first.title end should "find answers for matching questions" do response = Answer.search( - { query: { - has_parent: { - parent_type: 'question', - query: { - match: { - author: 'john' - } - } - } - } - }) + { query: { + has_parent: { + parent_type: 'question', + query: { + match: { + author: 'john' + } + } + } + } + }) assert_same_elements ['Adam', 'Ryan'], response.records.map(&:author) end @@ -136,12 +169,20 @@ class ActiveRecordAssociationsParentChildIntegrationTest < Elasticsearch::Test:: Question.where(title: 'First Question').each(&:destroy) Question.__elasticsearch__.refresh_index! - response = Answer.search query: { match_all: {} } + response = Answer.search( + { query: { + has_parent: { + parent_type: 'question', + query: { + match_all: {} + } + } + } + }) assert_equal 1, response.results.total end end - end end end diff --git a/elasticsearch-persistence/elasticsearch-persistence.gemspec b/elasticsearch-persistence/elasticsearch-persistence.gemspec index 311325fdc..38ba93594 100644 --- a/elasticsearch-persistence/elasticsearch-persistence.gemspec +++ b/elasticsearch-persistence/elasticsearch-persistence.gemspec @@ -33,7 +33,7 @@ Gem::Specification.new do |s| s.add_development_dependency "bundler", "~> 1.5" s.add_development_dependency "rake", "~> 11.1" - s.add_development_dependency "oj" + s.add_development_dependency "oj" unless defined?(JRUBY_VERSION) s.add_development_dependency "rails", '> 4' diff --git a/elasticsearch-rails/elasticsearch-rails.gemspec b/elasticsearch-rails/elasticsearch-rails.gemspec index 71b4c1abb..aa976c675 100644 --- a/elasticsearch-rails/elasticsearch-rails.gemspec +++ b/elasticsearch-rails/elasticsearch-rails.gemspec @@ -29,7 +29,7 @@ Gem::Specification.new do |s| s.add_development_dependency "elasticsearch-extensions" s.add_development_dependency "elasticsearch-model" - s.add_development_dependency "oj" + s.add_development_dependency "oj" unless defined?(JRUBY_VERSION) s.add_development_dependency "rails", ">= 3.1" s.add_development_dependency "lograge" From a9e20b1127e020c3b0cedfb5c360f36cf5563a9e Mon Sep 17 00:00:00 2001 From: Emily S Date: Fri, 29 Jun 2018 11:37:30 +0200 Subject: [PATCH 07/87] [CI] Update references to MongoDB Ruby driver (#807) --- .../test/integration/mongoid_basic_test.rb | 7 +++---- .../test/integration/multiple_models_test.rb | 6 +++--- elasticsearch-model/test/test_helper.rb | 9 ++++----- 3 files changed, 10 insertions(+), 12 deletions(-) diff --git a/elasticsearch-model/test/integration/mongoid_basic_test.rb b/elasticsearch-model/test/integration/mongoid_basic_test.rb index d46a75d05..c8b44dda0 100644 --- a/elasticsearch-model/test/integration/mongoid_basic_test.rb +++ b/elasticsearch-model/test/integration/mongoid_basic_test.rb @@ -1,9 +1,8 @@ require 'test_helper' +MongoDB.setup! -Mongo.setup! - -if Mongo.available? - Mongo.connect_to 'mongoid_articles' +if MongoDB.available? + MongoDB.connect_to 'mongoid_articles' module Elasticsearch module Model diff --git a/elasticsearch-model/test/integration/multiple_models_test.rb b/elasticsearch-model/test/integration/multiple_models_test.rb index 7d3a62705..02022b60f 100644 --- a/elasticsearch-model/test/integration/multiple_models_test.rb +++ b/elasticsearch-model/test/integration/multiple_models_test.rb @@ -6,7 +6,7 @@ ::ActiveRecord::Base.raise_in_transactional_callbacks = true if ::ActiveRecord::Base.respond_to?(:raise_in_transactional_callbacks) && ::ActiveRecord::VERSION::MAJOR.to_s < '5' -Mongo.setup! +MongoDB.setup! module Elasticsearch module Model @@ -115,8 +115,8 @@ class ::Series < ActiveRecord::Base assert_equal 0, response.page(3).per(3).results.size end - if Mongo.available? - Mongo.connect_to 'mongoid_collections' + if MongoDB.available? + MongoDB.connect_to 'mongoid_collections' context "Across mongoid models" do setup do diff --git a/elasticsearch-model/test/test_helper.rb b/elasticsearch-model/test/test_helper.rb index ff3a6d935..7471e475d 100644 --- a/elasticsearch-model/test/test_helper.rb +++ b/elasticsearch-model/test/test_helper.rb @@ -62,14 +62,13 @@ def setup end end -class Mongo +class MongoDB def self.setup! begin require 'mongoid' - session = Moped::Connection.new("localhost", 27017, 0.5) - session.connect + Mongo::Client.new(["localhost:27017"]) ENV['MONGODB_AVAILABLE'] = 'yes' - rescue LoadError, Moped::Errors::ConnectionFailure => e + rescue LoadError, Mongo::Error => e $stderr.puts "MongoDB not installed or running: #{e}" end end @@ -86,7 +85,7 @@ def self.connect_to(source) logger.level = ::Logger::DEBUG Mongoid.logger = logger unless ENV['QUIET'] - Moped.logger = logger unless ENV['QUIET'] + Mongo::Logger.logger = logger unless ENV['QUIET'] Mongoid.connect_to source end From f4a5825dd3195b941e27279f1d8b4d72d7b7e928 Mon Sep 17 00:00:00 2001 From: Emily Stolfo Date: Mon, 2 Jul 2018 15:44:47 +0200 Subject: [PATCH 08/87] [STORE] Depend on version >= 6 of elasticsearch gems --- elasticsearch-persistence/elasticsearch-persistence.gemspec | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/elasticsearch-persistence/elasticsearch-persistence.gemspec b/elasticsearch-persistence/elasticsearch-persistence.gemspec index 38ba93594..f732dc65c 100644 --- a/elasticsearch-persistence/elasticsearch-persistence.gemspec +++ b/elasticsearch-persistence/elasticsearch-persistence.gemspec @@ -23,8 +23,8 @@ Gem::Specification.new do |s| s.required_ruby_version = ">= 1.9.3" - s.add_dependency "elasticsearch", '~> 5' - s.add_dependency "elasticsearch-model", '~> 5' + s.add_dependency "elasticsearch", '~> 6' + s.add_dependency "elasticsearch-model", '~> 6' s.add_dependency "activesupport", '> 4' s.add_dependency "activemodel", '> 4' s.add_dependency "hashie" From 94a97896e89b29c0d3e00d1b214ca229dab88911 Mon Sep 17 00:00:00 2001 From: Emily Stolfo Date: Tue, 3 Jul 2018 15:12:59 +0200 Subject: [PATCH 09/87] [STORE] Undo last commit; depend on version 5 of elasticsearch gems --- elasticsearch-persistence/elasticsearch-persistence.gemspec | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/elasticsearch-persistence/elasticsearch-persistence.gemspec b/elasticsearch-persistence/elasticsearch-persistence.gemspec index f732dc65c..38ba93594 100644 --- a/elasticsearch-persistence/elasticsearch-persistence.gemspec +++ b/elasticsearch-persistence/elasticsearch-persistence.gemspec @@ -23,8 +23,8 @@ Gem::Specification.new do |s| s.required_ruby_version = ">= 1.9.3" - s.add_dependency "elasticsearch", '~> 6' - s.add_dependency "elasticsearch-model", '~> 6' + s.add_dependency "elasticsearch", '~> 5' + s.add_dependency "elasticsearch-model", '~> 5' s.add_dependency "activesupport", '> 4' s.add_dependency "activemodel", '> 4' s.add_dependency "hashie" From 9ebd32fbf9f2d629f612c9efd87e99f07764d3b0 Mon Sep 17 00:00:00 2001 From: Emily Stolfo Date: Thu, 5 Jul 2018 11:10:48 +0200 Subject: [PATCH 10/87] Add yard as development dependency in Gemfile --- Gemfile | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/Gemfile b/Gemfile index 4a7fe0473..5cb504908 100644 --- a/Gemfile +++ b/Gemfile @@ -8,3 +8,7 @@ gem 'elasticsearch-extensions' gem "pry" gem "ansi" gem "cane" + +group :development do + gem 'yard' +end From 92179ecdea45a95bc2b743f3e9e83bfce4de9852 Mon Sep 17 00:00:00 2001 From: Emily S Date: Mon, 9 Jul 2018 13:28:11 +0200 Subject: [PATCH 11/87] [STORE] Reduce repeated string instantiation (#813) --- .../persistence/repository/find.rb | 22 +++++++++++++++---- .../persistence/repository/naming.rb | 14 ++++++++++-- .../repository/response/results.rb | 22 ++++++++++++++----- .../persistence/repository/search.rb | 6 ++++- .../persistence/repository/serialize.rb | 15 ++++++++++--- .../test/unit/repository_naming_test.rb | 2 +- 6 files changed, 65 insertions(+), 16 deletions(-) diff --git a/elasticsearch-persistence/lib/elasticsearch/persistence/repository/find.rb b/elasticsearch-persistence/lib/elasticsearch/persistence/repository/find.rb index c6a9a6a4e..493ec1ba1 100644 --- a/elasticsearch-persistence/lib/elasticsearch/persistence/repository/find.rb +++ b/elasticsearch-persistence/lib/elasticsearch/persistence/repository/find.rb @@ -7,6 +7,20 @@ class DocumentNotFound < StandardError; end # module Find + # The default type of document. + # + ALL = '_all'.freeze + + # The key for accessing the document found and returned from an + # Elasticsearch _mget query. + # + DOCS = 'docs'.freeze + + # The key for the boolean value indicating whether a particular id + # has been successfully found in an Elasticsearch _mget query. + # + FOUND = 'found'.freeze + # Retrieve a single object or multiple objects from Elasticsearch by ID or IDs # # @example Retrieve a single object by ID @@ -43,14 +57,14 @@ def find(*args) # @return [true, false] # def exists?(id, options={}) - type = document_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 # @api private # def __find_one(id, options={}) - type = document_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) @@ -61,10 +75,10 @@ def __find_one(id, options={}) # @api private # def __find_many(ids, options={}) - type = document_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 } + documents[DOCS].map { |document| document[FOUND] ? deserialize(document) : nil } end end diff --git a/elasticsearch-persistence/lib/elasticsearch/persistence/repository/naming.rb b/elasticsearch-persistence/lib/elasticsearch/persistence/repository/naming.rb index 4c5794446..ba82505a1 100644 --- a/elasticsearch-persistence/lib/elasticsearch/persistence/repository/naming.rb +++ b/elasticsearch-persistence/lib/elasticsearch/persistence/repository/naming.rb @@ -6,6 +6,10 @@ module Repository # module Naming + # The possible keys for a document id. + # + IDS = [:id, 'id', :_id, '_id'].freeze + # Get or set the class used to initialize domain objects when deserializing them # def klass name=nil @@ -89,7 +93,7 @@ def __get_type_from_class(klass) # @api private # def __get_id_from_document(document) - document[:id] || document['id'] || document[:_id] || document['_id'] + document[IDS.find { |id| document[id] }] end # Extract a document ID from the document (assuming Hash or Hash-like object) @@ -106,7 +110,13 @@ def __get_id_from_document(document) # @api private # def __extract_id_from_document(document) - document.delete(:id) || document.delete('id') || document.delete(:_id) || document.delete('_id') + IDS.inject(nil) do |deleted, id| + if document[id] + document.delete(id) + else + deleted + end + end end end diff --git a/elasticsearch-persistence/lib/elasticsearch/persistence/repository/response/results.rb b/elasticsearch-persistence/lib/elasticsearch/persistence/repository/response/results.rb index 73fb4f836..169ecd42e 100644 --- a/elasticsearch-persistence/lib/elasticsearch/persistence/repository/response/results.rb +++ b/elasticsearch-persistence/lib/elasticsearch/persistence/repository/response/results.rb @@ -12,6 +12,18 @@ class Results attr_reader :repository + # The key for accessing the results in an Elasticsearch query response. + # + HITS = 'hits'.freeze + + # The key for accessing the total number of hits in an Elasticsearch query response. + # + TOTAL = 'total'.freeze + + # The key for accessing the maximum score in an Elasticsearch query response. + # + MAX_SCORE = 'max_score'.freeze + # @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 @@ -33,25 +45,25 @@ def respond_to?(method_name, include_private = false) # The number of total hits for a query # def total - response['hits']['total'] + response[HITS][TOTAL] end # The maximum score for a query # def max_score - response['hits']['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) + 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) + results.zip(response[HITS][HITS]).map(&block) end # Return the collection of domain objects @@ -64,7 +76,7 @@ def map_with_hit(&block) # @return [Array] # def results - @results ||= response['hits']['hits'].map do |document| + @results ||= response[HITS][HITS].map do |document| repository.deserialize(document.to_hash) end end diff --git a/elasticsearch-persistence/lib/elasticsearch/persistence/repository/search.rb b/elasticsearch-persistence/lib/elasticsearch/persistence/repository/search.rb index de9e01aac..381b4d981 100644 --- a/elasticsearch-persistence/lib/elasticsearch/persistence/repository/search.rb +++ b/elasticsearch-persistence/lib/elasticsearch/persistence/repository/search.rb @@ -6,6 +6,10 @@ module Repository # module Search + # The key for accessing the count in a Elasticsearch query response. + # + COUNT = 'count'.freeze + # Returns a collection of domain objects by an Elasticsearch query # # Pass the query either as a string or a Hash-like object @@ -86,7 +90,7 @@ def count(query_or_definition=nil, options={}) raise ArgumentError, "[!] Pass the search definition as a Hash-like object or pass the query as a String, not as [#{query_or_definition.class}]" end - response['count'] + response[COUNT] end end diff --git a/elasticsearch-persistence/lib/elasticsearch/persistence/repository/serialize.rb b/elasticsearch-persistence/lib/elasticsearch/persistence/repository/serialize.rb index 027f000b6..dc0865fd2 100644 --- a/elasticsearch-persistence/lib/elasticsearch/persistence/repository/serialize.rb +++ b/elasticsearch-persistence/lib/elasticsearch/persistence/repository/serialize.rb @@ -8,6 +8,16 @@ module Repository # module Serialize + # The key for document fields in an Elasticsearch query response. + # + SOURCE = '_source'.freeze + + # The key for the document type in an Elasticsearch query response. + # Note that it will be removed eventually, as multiple types in a single + # index are deprecated as of Elasticsearch 6.0. + # + TYPE = '_type'.freeze + # Serialize the object for storing it in Elasticsearch # # In the default implementation, call the `to_hash` method on the passed object. @@ -21,11 +31,10 @@ def serialize(document) # 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'] + _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_naming_test.rb b/elasticsearch-persistence/test/unit/repository_naming_test.rb index a4845674d..4110205fb 100644 --- a/elasticsearch-persistence/test/unit/repository_naming_test.rb +++ b/elasticsearch-persistence/test/unit/repository_naming_test.rb @@ -50,7 +50,7 @@ module ::Foo; class Bar; end; end end context "extract an ID from the document" do - should "delete the key from theHash" do + should "delete the key from the Hash" do d1 = { :id => 1 } d2 = { :_id => 1 } d3 = { 'id' => 1 } From bed4c8a84681519ee74c0f02c037587bb35b2f57 Mon Sep 17 00:00:00 2001 From: Emily S Date: Thu, 19 Jul 2018 12:16:24 +0200 Subject: [PATCH 12/87] [MODEL] Use default doc type: _doc (#814) --- elasticsearch-model/lib/elasticsearch/model/naming.rb | 4 +++- .../test/integration/active_record_basic_test.rb | 2 ++ .../integration/active_record_custom_serialization_test.rb | 4 ++-- .../test/integration/active_record_namespaced_model_test.rb | 2 ++ elasticsearch-model/test/unit/indexing_test.rb | 4 ++-- elasticsearch-model/test/unit/naming_inheritance_test.rb | 4 ++-- elasticsearch-model/test/unit/naming_test.rb | 4 ++-- 7 files changed, 15 insertions(+), 9 deletions(-) diff --git a/elasticsearch-model/lib/elasticsearch/model/naming.rb b/elasticsearch-model/lib/elasticsearch/model/naming.rb index 7bf24f089..2c5151886 100644 --- a/elasticsearch-model/lib/elasticsearch/model/naming.rb +++ b/elasticsearch-model/lib/elasticsearch/model/naming.rb @@ -5,6 +5,8 @@ module Model # module Naming + DEFAULT_DOC_TYPE= '_doc'.freeze + module ClassMethods # Get or set the name of the index @@ -90,7 +92,7 @@ def default_index_name end def default_document_type - self.model_name.element + DEFAULT_DOC_TYPE end end diff --git a/elasticsearch-model/test/integration/active_record_basic_test.rb b/elasticsearch-model/test/integration/active_record_basic_test.rb index 26c5785f2..02c7b24c3 100644 --- a/elasticsearch-model/test/integration/active_record_basic_test.rb +++ b/elasticsearch-model/test/integration/active_record_basic_test.rb @@ -16,6 +16,8 @@ class ::Article < ActiveRecord::Base include Elasticsearch::Model include Elasticsearch::Model::Callbacks + document_type 'article' + settings index: { number_of_shards: 1, number_of_replicas: 0 } do mapping do indexes :title, type: 'text', analyzer: 'snowball' diff --git a/elasticsearch-model/test/integration/active_record_custom_serialization_test.rb b/elasticsearch-model/test/integration/active_record_custom_serialization_test.rb index cb706cf99..9847967e5 100644 --- a/elasticsearch-model/test/integration/active_record_custom_serialization_test.rb +++ b/elasticsearch-model/test/integration/active_record_custom_serialization_test.rb @@ -41,7 +41,7 @@ def as_indexed_json(options={}) a = ArticleWithCustomSerialization.__elasticsearch__.client.get \ index: 'article_with_custom_serializations', - type: 'article_with_custom_serialization', + type: '_doc', id: '1' assert_equal( { 'title' => 'Test' }, a['_source'] ) @@ -55,7 +55,7 @@ def as_indexed_json(options={}) a = ArticleWithCustomSerialization.__elasticsearch__.client.get \ index: 'article_with_custom_serializations', - type: 'article_with_custom_serialization', + type: '_doc', id: '1' assert_equal( { 'title' => 'UPDATED' }, a['_source'] ) diff --git a/elasticsearch-model/test/integration/active_record_namespaced_model_test.rb b/elasticsearch-model/test/integration/active_record_namespaced_model_test.rb index 9885b3a1a..75e7aa745 100644 --- a/elasticsearch-model/test/integration/active_record_namespaced_model_test.rb +++ b/elasticsearch-model/test/integration/active_record_namespaced_model_test.rb @@ -22,6 +22,8 @@ class Article < ActiveRecord::Base include Elasticsearch::Model include Elasticsearch::Model::Callbacks + document_type 'article' + mapping { indexes :title } end end diff --git a/elasticsearch-model/test/unit/indexing_test.rb b/elasticsearch-model/test/unit/indexing_test.rb index e3bfad868..591977751 100644 --- a/elasticsearch-model/test/unit/indexing_test.rb +++ b/elasticsearch-model/test/unit/indexing_test.rb @@ -149,7 +149,7 @@ class NotFound < Exception; end should "update and return the index mappings" do DummyIndexingModel.mappings foo: 'boo' DummyIndexingModel.mappings bar: 'bam' - assert_equal( { dummy_indexing_model: { foo: "boo", bar: "bam", properties: {} } }, + assert_equal( { _doc: { foo: "boo", bar: "bam", properties: {} } }, DummyIndexingModel.mappings.to_hash ) end @@ -544,7 +544,7 @@ class ::DummyIndexingModelForRecreate indices.expects(:create).with do |payload| assert_equal 'dummy_indexing_model_for_recreates', payload[:index] assert_equal 1, payload[:body][:settings][:index][:number_of_shards] - assert_equal 'keyword', payload[:body][:mappings][:dummy_indexing_model_for_recreate][:properties][:foo][:analyzer] + assert_equal 'keyword', payload[:body][:mappings][:_doc][:properties][:foo][:analyzer] true end.returns({}) diff --git a/elasticsearch-model/test/unit/naming_inheritance_test.rb b/elasticsearch-model/test/unit/naming_inheritance_test.rb index b66d415a0..1b40e9cfd 100644 --- a/elasticsearch-model/test/unit/naming_inheritance_test.rb +++ b/elasticsearch-model/test/unit/naming_inheritance_test.rb @@ -69,8 +69,8 @@ class ::Cat < ::Animal end should "return the default document_type" do - assert_equal "test_base", TestBase.document_type - assert_equal "test_base", TestBase.new.document_type + assert_equal "_doc", TestBase.document_type + assert_equal "_doc", TestBase.new.document_type end should "return the explicit document_type" do diff --git a/elasticsearch-model/test/unit/naming_test.rb b/elasticsearch-model/test/unit/naming_test.rb index 424adf7cc..3f2368891 100644 --- a/elasticsearch-model/test/unit/naming_test.rb +++ b/elasticsearch-model/test/unit/naming_test.rb @@ -29,8 +29,8 @@ class DummyNamingModelInNamespace end should "return the default document_type" do - assert_equal 'dummy_naming_model', DummyNamingModel.document_type - assert_equal 'dummy_naming_model', DummyNamingModel.new.document_type + assert_equal '_doc', DummyNamingModel.document_type + assert_equal '_doc', DummyNamingModel.new.document_type end should "set and return the index_name" do From 30aa7873a6f2d681b0cc458a6139d1e0812fd60c Mon Sep 17 00:00:00 2001 From: Emily Stolfo Date: Thu, 19 Jul 2018 12:34:37 +0200 Subject: [PATCH 13/87] minor: Fix spacing --- elasticsearch-model/lib/elasticsearch/model/naming.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/elasticsearch-model/lib/elasticsearch/model/naming.rb b/elasticsearch-model/lib/elasticsearch/model/naming.rb index 2c5151886..cef23e810 100644 --- a/elasticsearch-model/lib/elasticsearch/model/naming.rb +++ b/elasticsearch-model/lib/elasticsearch/model/naming.rb @@ -5,7 +5,7 @@ module Model # module Naming - DEFAULT_DOC_TYPE= '_doc'.freeze + DEFAULT_DOC_TYPE = '_doc'.freeze module ClassMethods From cae7ba7716b660df6e3ece662c70e5c1a153bc18 Mon Sep 17 00:00:00 2001 From: Emily S Date: Mon, 23 Jul 2018 15:12:44 +0200 Subject: [PATCH 14/87] [STORE] Make default doc type '_doc' in preparation for deprecation of mapping types (#816) * [STORE] Require document_type or method options to set type in find requests * [STORE] Require document_type for a Repository to have a document type set, otherwise use _doc * [STORE] Use document_type or type in options for search requests * [STORE] Require #klass to be defined on a repository for deserialization * [STORE] Only rely on document_type setting in storage requests * [STORE] Fix up tests to work with default document_type and klass on Repository object --- .../persistence/repository/find.rb | 6 +- .../persistence/repository/naming.rb | 44 ++------- .../persistence/repository/search.rb | 4 +- .../persistence/repository/serialize.rb | 12 ++- .../persistence/repository/store.rb | 9 +- .../repository/custom_class_test.rb | 3 + .../repository/default_class_test.rb | 3 + .../repository/virtus_model_test.rb | 1 + .../test/unit/repository_find_test.rb | 99 ++----------------- .../test/unit/repository_naming_test.rb | 39 +------- .../test/unit/repository_search_test.rb | 23 +---- .../test/unit/repository_serialize_test.rb | 22 +---- .../test/unit/repository_store_test.rb | 98 ++---------------- 13 files changed, 62 insertions(+), 301 deletions(-) diff --git a/elasticsearch-persistence/lib/elasticsearch/persistence/repository/find.rb b/elasticsearch-persistence/lib/elasticsearch/persistence/repository/find.rb index 493ec1ba1..5e214cfb7 100644 --- a/elasticsearch-persistence/lib/elasticsearch/persistence/repository/find.rb +++ b/elasticsearch-persistence/lib/elasticsearch/persistence/repository/find.rb @@ -57,14 +57,14 @@ def find(*args) # @return [true, false] # def exists?(id, options={}) - type = document_type || (klass ? __get_type_from_class(klass) : ALL) + type = document_type || 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) + type = document_type || ALL document = client.get( { index: index_name, type: type, id: id }.merge(options) ) deserialize(document) @@ -75,7 +75,7 @@ def __find_one(id, options={}) # @api private # def __find_many(ids, options={}) - type = document_type || (klass ? __get_type_from_class(klass) : ALL) + type = document_type || 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/naming.rb b/elasticsearch-persistence/lib/elasticsearch/persistence/repository/naming.rb index ba82505a1..d559d8d51 100644 --- a/elasticsearch-persistence/lib/elasticsearch/persistence/repository/naming.rb +++ b/elasticsearch-persistence/lib/elasticsearch/persistence/repository/naming.rb @@ -10,10 +10,16 @@ module Naming # IDS = [:id, 'id', :_id, '_id'].freeze + DEFAULT_DOC_TYPE = '_doc'.freeze + # Get or set the class used to initialize domain objects when deserializing them # - def klass name=nil - @klass = name || @klass + def klass(name=nil) + if name + @klass = name + else + @klass + end end # Set the class used to initialize domain objects when deserializing them @@ -43,7 +49,7 @@ def index_name=(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) + @document_type = name || @document_type || DEFAULT_DOC_TYPE end; alias :type :document_type # Set the document type used when storing and retrieving documents @@ -52,38 +58,6 @@ 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 - # - # @return [Class] The class corresponding to the passed type - # @raise [NameError] if the class cannot be found - # - # @api private - # - def __get_klass_from_type(type) - klass = type.classify - klass.constantize - rescue NameError => e - raise NameError, "Attempted to get class '#{klass}' from the '#{type}' type, but no such class can be found." - end - - # Get the Elasticsearch `_type` from the Ruby class - # - # @example - # repository.__get_type_from_class Note - # => "note" - # - # @return [String] The type corresponding to the passed class - # - # @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 diff --git a/elasticsearch-persistence/lib/elasticsearch/persistence/repository/search.rb b/elasticsearch-persistence/lib/elasticsearch/persistence/repository/search.rb index 381b4d981..07c0e4d3d 100644 --- a/elasticsearch-persistence/lib/elasticsearch/persistence/repository/search.rb +++ b/elasticsearch-persistence/lib/elasticsearch/persistence/repository/search.rb @@ -44,7 +44,7 @@ module Search # @return [Elasticsearch::Persistence::Repository::Response::Results] # def search(query_or_definition, options={}) - type = document_type || (klass ? __get_type_from_class(klass) : nil ) + type = document_type case when query_or_definition.respond_to?(:to_hash) @@ -79,7 +79,7 @@ def search(query_or_definition, options={}) # def count(query_or_definition=nil, options={}) query_or_definition ||= { query: { match_all: {} } } - type = document_type || (klass ? __get_type_from_class(klass) : nil ) + type = document_type case when query_or_definition.respond_to?(:to_hash) diff --git a/elasticsearch-persistence/lib/elasticsearch/persistence/repository/serialize.rb b/elasticsearch-persistence/lib/elasticsearch/persistence/repository/serialize.rb index dc0865fd2..e9a8d875e 100644 --- a/elasticsearch-persistence/lib/elasticsearch/persistence/repository/serialize.rb +++ b/elasticsearch-persistence/lib/elasticsearch/persistence/repository/serialize.rb @@ -8,6 +8,14 @@ module Repository # module Serialize + # Error message raised when documents are attempted to be deserialized and no klass is defined for + # the Repository. + # + # @since 6.0.0 + NO_CLASS_ERROR_MESSAGE = "No class is defined for deserializing documents. " + + "Please define a 'klass' for the Repository or define a custom " + + "deserialize method.".freeze + # The key for document fields in an Elasticsearch query response. # SOURCE = '_source'.freeze @@ -31,8 +39,8 @@ def serialize(document) # 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] + raise NameError.new(NO_CLASS_ERROR_MESSAGE) unless klass + klass.new document[SOURCE] end end end diff --git a/elasticsearch-persistence/lib/elasticsearch/persistence/repository/store.rb b/elasticsearch-persistence/lib/elasticsearch/persistence/repository/store.rb index 5bec487ce..2327d23a6 100644 --- a/elasticsearch-persistence/lib/elasticsearch/persistence/repository/store.rb +++ b/elasticsearch-persistence/lib/elasticsearch/persistence/repository/store.rb @@ -17,7 +17,7 @@ module Store def save(document, options={}) serialized = serialize(document) id = __get_id_from_document(serialized) - type = document_type || __get_type_from_class(klass || document.class) + type = document_type client.index( { index: index_name, type: type, id: id, body: serialized }.merge(options) ) end @@ -48,8 +48,7 @@ def update(document, options={}) type = options.delete(:type) || \ (defined?(serialized) && serialized && serialized.delete(:type)) || \ - document_type || \ - __get_type_from_class(klass) + document_type if defined?(serialized) && serialized body = if serialized[:script] @@ -80,11 +79,11 @@ def update(document, options={}) def delete(document, options={}) if document.is_a?(String) || document.is_a?(Integer) id = document - type = document_type || __get_type_from_class(klass) + type = document_type else serialized = serialize(document) id = __get_id_from_document(serialized) - type = document_type || __get_type_from_class(klass || document.class) + type = document_type end client.delete( { index: index_name, type: type, id: id }.merge(options) ) end diff --git a/elasticsearch-persistence/test/integration/repository/custom_class_test.rb b/elasticsearch-persistence/test/integration/repository/custom_class_test.rb index 0aaae91ff..6528dd438 100644 --- a/elasticsearch-persistence/test/integration/repository/custom_class_test.rb +++ b/elasticsearch-persistence/test/integration/repository/custom_class_test.rb @@ -30,6 +30,7 @@ class ::MyNotesRepository include Elasticsearch::Persistence::Repository klass MyNote + document_type 'my_note' settings number_of_shards: 1 do mapping do @@ -45,6 +46,8 @@ def deserialize(document) end @repository = MyNotesRepository.new + @repository.klass = MyNotesRepository.klass + @repository.document_type = MyNotesRepository.document_type @repository.client.cluster.health wait_for_status: 'yellow' end diff --git a/elasticsearch-persistence/test/integration/repository/default_class_test.rb b/elasticsearch-persistence/test/integration/repository/default_class_test.rb index c27d587be..1674a3a42 100644 --- a/elasticsearch-persistence/test/integration/repository/default_class_test.rb +++ b/elasticsearch-persistence/test/integration/repository/default_class_test.rb @@ -19,6 +19,8 @@ def to_hash context "The default repository class" do setup do @repository = Elasticsearch::Persistence::Repository.new + @repository.klass = ::Note + @repository.document_type = 'note' @repository.client.cluster.health wait_for_status: 'yellow' end @@ -105,6 +107,7 @@ def to_hash end should "save and find a plain hash" do + @repository.klass = Hash @repository.save id: 1, title: 'Hash' result = @repository.find(1) assert_equal 'Hash', result['_source']['title'] diff --git a/elasticsearch-persistence/test/integration/repository/virtus_model_test.rb b/elasticsearch-persistence/test/integration/repository/virtus_model_test.rb index fdefe2d45..c6ad88177 100644 --- a/elasticsearch-persistence/test/integration/repository/virtus_model_test.rb +++ b/elasticsearch-persistence/test/integration/repository/virtus_model_test.rb @@ -25,6 +25,7 @@ def set_id(id) @repository = Elasticsearch::Persistence::Repository.new do index :pages klass Page + document_type 'page' def deserialize(document) page = klass.new document['_source'] diff --git a/elasticsearch-persistence/test/unit/repository_find_test.rb b/elasticsearch-persistence/test/unit/repository_find_test.rb index 3af93ef3c..b20006ca1 100644 --- a/elasticsearch-persistence/test/unit/repository_find_test.rb +++ b/elasticsearch-persistence/test/unit/repository_find_test.rb @@ -48,27 +48,8 @@ class MyDocument; end assert_equal false, subject.exists?('1') 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') - - @client - .expects(:exists) - .with do |arguments| - assert_equal 'my_document', arguments[:type] - assert_equal '1', arguments[:id] - true - end - .returns(true) - - 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) @@ -82,9 +63,7 @@ class MyDocument; end 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 + should "return whether document exists using _all type" do @client .expects(:exists) @@ -100,39 +79,20 @@ class MyDocument; end should "pass options to the client" do @client.expects(:exists).with do |arguments| + assert_equal 'my_document', arguments[:type] assert_equal 'foobarbam', arguments[:index] assert_equal 'bambam', arguments[:routing] true end - subject.exists? '1', index: 'foobarbam', routing: 'bambam' + subject.exists? '1', index: 'foobarbam', routing: 'bambam', type: 'my_document' end 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') - 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] - true - end - .returns({'_source' => { 'foo' => 'bar' }}) - - assert_instance_of MyDocument, subject.__find_one('1') - end - - should "find document based on document_type and return a deserialized object" do + should "find a 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) @@ -148,10 +108,8 @@ class MyDocument; end assert_instance_of MyDocument, subject.__find_one('1') end - should "find document and return a deserialized object" do + should "find a document using _all if document_type is not defined" do subject.expects(:document_type).returns(nil) - 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) @@ -169,7 +127,6 @@ class MyDocument; 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 @@ -183,8 +140,6 @@ class MyDocument; end end should "pass other exceptions" do - subject.expects(:klass).returns(nil).at_least_once - subject.expects(:deserialize).never @client @@ -197,7 +152,6 @@ class MyDocument; end end should "pass options to the client" do - subject.expects(:klass).returns(nil).at_least_once subject.expects(:deserialize) @client @@ -209,7 +163,7 @@ class MyDocument; end end .returns({'_source' => { 'foo' => 'bar' }}) - subject.__find_one '1', index: 'foobarbam', routing: 'bambam' + subject.__find_one '1', index: 'foobarbam', routing: 'bambam', type: 'my_document' end end @@ -232,39 +186,8 @@ 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') - - 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] - true - 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 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 @@ -282,8 +205,6 @@ class MyDocument; 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 subject .expects(:deserialize) @@ -335,9 +256,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') + subject.expects(:document_type).returns('my_document') subject .expects(:deserialize) @@ -368,19 +287,19 @@ class MyDocument; end 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 'my_document', arguments[:type] assert_equal 'foobarbam', arguments[:index] assert_equal 'bambam', arguments[:routing] true end .returns(@response) - subject.__find_many ['1', '2'], index: 'foobarbam', routing: 'bambam' + subject.__find_many ['1', '2'], index: 'foobarbam', routing: 'bambam', type: 'my_document' end end diff --git a/elasticsearch-persistence/test/unit/repository_naming_test.rb b/elasticsearch-persistence/test/unit/repository_naming_test.rb index 4110205fb..aed34598f 100644 --- a/elasticsearch-persistence/test/unit/repository_naming_test.rb +++ b/elasticsearch-persistence/test/unit/repository_naming_test.rb @@ -11,35 +11,6 @@ module ::Foo; class Bar; end; end @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) @@ -118,17 +89,17 @@ class ::MySpecialRepository; end end context "document_type" do - should "be nil when no klass is set" do - assert_equal nil, subject.document_type + should "be the default doc type when no klass is set" do + assert_equal '_doc', subject.document_type end - should "default to klass" do + should "does not use the klass" do subject.klass Foobar - assert_equal 'foobar', subject.document_type + assert_equal '_doc', subject.document_type end should "be aliased as `type`" do - subject.klass Foobar + subject.document_type 'foobar' assert_equal 'foobar', subject.type end diff --git a/elasticsearch-persistence/test/unit/repository_search_test.rb b/elasticsearch-persistence/test/unit/repository_search_test.rb index d0d5beef0..5cf6d5ab3 100644 --- a/elasticsearch-persistence/test/unit/repository_search_test.rb +++ b/elasticsearch-persistence/test/unit/repository_search_test.rb @@ -14,13 +14,9 @@ class MyDocument; end @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') - + should "search in type does not use the klass setting" do @client.expects(:search).with do |arguments| assert_equal 'test', arguments[:index] - assert_equal 'my_document', arguments[:type] assert_equal({foo: 'bar'}, arguments[:body]) true end @@ -30,7 +26,6 @@ class MyDocument; end should "search in type based on document_type" do subject.expects(:document_type).returns('my_special_document').at_least_once - subject.expects(:__get_type_from_class).never @client.expects(:search).with do |arguments| assert_equal 'test', arguments[:index] @@ -44,24 +39,19 @@ class MyDocument; end should "search across all types" do subject.expects(:document_type).returns(nil).at_least_once - 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 '_all', arguments[:type] assert_equal({foo: 'bar'}, arguments[:body]) true end assert_instance_of Elasticsearch::Persistence::Repository::Response::Results, - subject.search(foo: 'bar') + subject.search({ foo: 'bar' }, type: '_all') 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] true @@ -74,9 +64,6 @@ class MyDocument; end 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] true @@ -87,9 +74,6 @@ class MyDocument; end 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 @@ -113,5 +97,4 @@ class MyDocument; end assert_equal 1, subject.count( { query: { match: { foo: 'bar' } } }, { ignore_unavailable: true } ) end end - end diff --git a/elasticsearch-persistence/test/unit/repository_serialize_test.rb b/elasticsearch-persistence/test/unit/repository_serialize_test.rb index 48d0323d8..a69145464 100644 --- a/elasticsearch-persistence/test/unit/repository_serialize_test.rb +++ b/elasticsearch-persistence/test/unit/repository_serialize_test.rb @@ -24,33 +24,17 @@ class MyDocument; end context "deserialize" do should "get the class name from #klass" do subject.expects(:klass) - .returns(MyDocument) + .returns(MyDocument).twice 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 + should "raise an error when klass isn't set" 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' } } ) + assert_raise(NameError) { subject.deserialize( {} ) } end end end diff --git a/elasticsearch-persistence/test/unit/repository_store_test.rb b/elasticsearch-persistence/test/unit/repository_store_test.rb index 69f3defc3..b886eecdf 100644 --- a/elasticsearch-persistence/test/unit/repository_store_test.rb +++ b/elasticsearch-persistence/test/unit/repository_store_test.rb @@ -13,52 +13,12 @@ class MyDocument; end 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') - - 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]) - true - 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(: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') - - 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]) - true - end - subject.expects(:client).returns(client) - - 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 @@ -76,19 +36,18 @@ class MyDocument; 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') client = mock client.expects(:index).with do |arguments| + assert_equal 'my_document', arguments[:type] assert_equal 'foobarbam', arguments[:index] assert_equal 'bambam', arguments[:routing] true end subject.expects(:client).returns(client) - subject.save({foo: 'bar'}, { index: 'foobarbam', routing: 'bambam' }) + subject.save({foo: 'bar'}, { index: 'foobarbam', routing: 'bambam', type: 'my_document' }) end end @@ -208,50 +167,12 @@ class MyDocument; end 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 - - client = mock - client.expects(:delete).with do |arguments| - assert_equal 'my_document', arguments[:type] - assert_equal '1', arguments[:id] - true - 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(: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') - - client = mock - client.expects(:delete).with do |arguments| - assert_equal 'my_document', arguments[:type] - assert_equal '1', arguments[:id] - true - end - subject.expects(:client).returns(client) - - 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 @@ -265,16 +186,14 @@ class MyDocument; end subject.delete({id: '1', foo: 'bar'}) end - should "get ID and type from document when passed a document" do + should "get ID and uses the default document type" 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(:document_type).returns('_doc') 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 '_doc', arguments[:type] assert_equal '1', arguments[:id] true end @@ -284,19 +203,16 @@ 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') - client = mock client.expects(:delete).with do |arguments| + assert_equal 'my_document', arguments[:type] assert_equal 'foobarbam', arguments[:index] assert_equal 'bambam', arguments[:routing] true end subject.expects(:client).returns(client) - subject.delete('1', index: 'foobarbam', routing: 'bambam') + subject.delete('1', index: 'foobarbam', routing: 'bambam', type: 'my_document') end end end From b47f16aabf74bcbd59e7d6d0a585ab21bd5142bc Mon Sep 17 00:00:00 2001 From: Emily S Date: Wed, 25 Jul 2018 13:52:21 +0200 Subject: [PATCH 15/87] Update various gemspecs to conditionally depend on gems incompatible with JRuby (#810) * [MODEL] Update dependencies for JRuby compatibility * [STORE] Update dependencies for JRuby compatibility * [RAILS] Update dependencies for JRuby compatibility --- elasticsearch-model/elasticsearch-model.gemspec | 4 ++-- elasticsearch-model/gemfiles/3.0.gemfile | 2 +- elasticsearch-model/gemfiles/4.0.gemfile | 2 +- elasticsearch-model/gemfiles/5.0.gemfile | 2 +- elasticsearch-persistence/elasticsearch-persistence.gemspec | 2 +- elasticsearch-rails/elasticsearch-rails.gemspec | 3 +-- 6 files changed, 7 insertions(+), 8 deletions(-) diff --git a/elasticsearch-model/elasticsearch-model.gemspec b/elasticsearch-model/elasticsearch-model.gemspec index 5cd1bcec1..9e52c400a 100644 --- a/elasticsearch-model/elasticsearch-model.gemspec +++ b/elasticsearch-model/elasticsearch-model.gemspec @@ -32,7 +32,7 @@ Gem::Specification.new do |s| s.add_development_dependency "elasticsearch-extensions" - s.add_development_dependency "sqlite3" + s.add_development_dependency "sqlite3" unless defined?(JRUBY_VERSION) s.add_development_dependency "activemodel", "> 3" s.add_development_dependency "oj" unless defined?(JRUBY_VERSION) @@ -45,7 +45,7 @@ Gem::Specification.new do |s| 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 "ruby-prof" unless defined?(JRUBY_VERSION) s.add_development_dependency "pry" s.add_development_dependency "simplecov" diff --git a/elasticsearch-model/gemfiles/3.0.gemfile b/elasticsearch-model/gemfiles/3.0.gemfile index 23cbdf53d..97ea5f60a 100644 --- a/elasticsearch-model/gemfiles/3.0.gemfile +++ b/elasticsearch-model/gemfiles/3.0.gemfile @@ -10,4 +10,4 @@ gemspec path: '../' gem 'activemodel', '>= 3.0' gem 'activerecord', '~> 3.2' gem 'mongoid', '>= 3.0' -gem 'sqlite3' +gem 'sqlite3' unless defined?(JRUBY_VERSION) diff --git a/elasticsearch-model/gemfiles/4.0.gemfile b/elasticsearch-model/gemfiles/4.0.gemfile index 89044bb19..fa0cc73e9 100644 --- a/elasticsearch-model/gemfiles/4.0.gemfile +++ b/elasticsearch-model/gemfiles/4.0.gemfile @@ -9,4 +9,4 @@ gemspec path: '../' gem 'activemodel', '~> 4' gem 'activerecord', '~> 4' -gem 'sqlite3' +gem 'sqlite3' unless defined?(JRUBY_VERSION) diff --git a/elasticsearch-model/gemfiles/5.0.gemfile b/elasticsearch-model/gemfiles/5.0.gemfile index 75b8a7ca9..77e10c7bb 100644 --- a/elasticsearch-model/gemfiles/5.0.gemfile +++ b/elasticsearch-model/gemfiles/5.0.gemfile @@ -9,4 +9,4 @@ gemspec path: '../' gem 'activemodel', '~> 5' gem 'activerecord', '~> 5' -gem 'sqlite3' +gem 'sqlite3' unless defined?(JRUBY_VERSION) diff --git a/elasticsearch-persistence/elasticsearch-persistence.gemspec b/elasticsearch-persistence/elasticsearch-persistence.gemspec index 38ba93594..e90f2afb1 100644 --- a/elasticsearch-persistence/elasticsearch-persistence.gemspec +++ b/elasticsearch-persistence/elasticsearch-persistence.gemspec @@ -45,7 +45,7 @@ Gem::Specification.new do |s| 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 "ruby-prof" unless defined?(JRUBY_VERSION) s.add_development_dependency "pry" s.add_development_dependency "simplecov" diff --git a/elasticsearch-rails/elasticsearch-rails.gemspec b/elasticsearch-rails/elasticsearch-rails.gemspec index aa976c675..c7dbf32d5 100644 --- a/elasticsearch-rails/elasticsearch-rails.gemspec +++ b/elasticsearch-rails/elasticsearch-rails.gemspec @@ -29,7 +29,6 @@ Gem::Specification.new do |s| s.add_development_dependency "elasticsearch-extensions" s.add_development_dependency "elasticsearch-model" - s.add_development_dependency "oj" unless defined?(JRUBY_VERSION) s.add_development_dependency "rails", ">= 3.1" s.add_development_dependency "lograge" @@ -40,7 +39,7 @@ Gem::Specification.new do |s| 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 "ruby-prof" unless defined?(JRUBY_VERSION) s.add_development_dependency "pry" s.add_development_dependency "simplecov" From a065f2dfe4817e666e45f6e406bf352002b796c2 Mon Sep 17 00:00:00 2001 From: Emily Stolfo Date: Wed, 25 Jul 2018 14:30:09 +0200 Subject: [PATCH 16/87] [RAILS] Add 'oj' back as a development dependency in gemspec --- elasticsearch-rails/elasticsearch-rails.gemspec | 1 + 1 file changed, 1 insertion(+) diff --git a/elasticsearch-rails/elasticsearch-rails.gemspec b/elasticsearch-rails/elasticsearch-rails.gemspec index c7dbf32d5..1fad14c36 100644 --- a/elasticsearch-rails/elasticsearch-rails.gemspec +++ b/elasticsearch-rails/elasticsearch-rails.gemspec @@ -39,6 +39,7 @@ Gem::Specification.new do |s| s.add_development_dependency "mocha" s.add_development_dependency "turn" s.add_development_dependency "yard" + s.add_development_dependency "oj" unless defined?(JRUBY_VERSION) s.add_development_dependency "ruby-prof" unless defined?(JRUBY_VERSION) s.add_development_dependency "pry" From 493059b345438298067b83fb34a298af95c40eec Mon Sep 17 00:00:00 2001 From: Emily S Date: Thu, 26 Jul 2018 19:03:20 +0200 Subject: [PATCH 17/87] [STORE] Remove Elasticsearch::Persistence::Model (ActiveRecord persistence pattern) (#812) --- README.md | 14 - elasticsearch-persistence/README.md | 259 +------- .../lib/elasticsearch/persistence/model.rb | 135 ---- .../elasticsearch/persistence/model/base.rb | 87 --- .../elasticsearch/persistence/model/errors.rb | 8 - .../elasticsearch/persistence/model/find.rb | 180 ------ .../elasticsearch/persistence/model/rails.rb | 47 -- .../elasticsearch/persistence/model/store.rb | 254 -------- .../elasticsearch/persistence/model/utils.rb | 0 .../elasticsearch/model/model_generator.rb | 21 - .../elasticsearch/model/templates/model.rb.tt | 9 - .../generators/elasticsearch_generator.rb | 2 - .../integration/model/model_basic_test.rb | 233 ------- .../test/unit/model_base_test.rb | 72 --- .../test/unit/model_find_test.rb | 153 ----- .../test/unit/model_gateway_test.rb | 101 --- .../test/unit/model_rails_test.rb | 112 ---- .../test/unit/model_store_test.rb | 576 ------------------ .../rails/instrumentation/railtie.rb | 4 - .../lib/elasticsearch/rails/lograge.rb | 4 - 20 files changed, 2 insertions(+), 2269 deletions(-) delete mode 100644 elasticsearch-persistence/lib/elasticsearch/persistence/model.rb delete mode 100644 elasticsearch-persistence/lib/elasticsearch/persistence/model/base.rb delete mode 100644 elasticsearch-persistence/lib/elasticsearch/persistence/model/errors.rb delete mode 100644 elasticsearch-persistence/lib/elasticsearch/persistence/model/find.rb delete mode 100644 elasticsearch-persistence/lib/elasticsearch/persistence/model/rails.rb delete mode 100644 elasticsearch-persistence/lib/elasticsearch/persistence/model/store.rb delete mode 100644 elasticsearch-persistence/lib/elasticsearch/persistence/model/utils.rb delete mode 100644 elasticsearch-persistence/lib/rails/generators/elasticsearch/model/model_generator.rb delete mode 100644 elasticsearch-persistence/lib/rails/generators/elasticsearch/model/templates/model.rb.tt delete mode 100644 elasticsearch-persistence/lib/rails/generators/elasticsearch_generator.rb delete mode 100644 elasticsearch-persistence/test/integration/model/model_basic_test.rb delete mode 100644 elasticsearch-persistence/test/unit/model_base_test.rb delete mode 100644 elasticsearch-persistence/test/unit/model_find_test.rb delete mode 100644 elasticsearch-persistence/test/unit/model_gateway_test.rb delete mode 100644 elasticsearch-persistence/test/unit/model_rails_test.rb delete mode 100644 elasticsearch-persistence/test/unit/model_store_test.rb diff --git a/README.md b/README.md index f7c8e1151..ef1a8476e 100644 --- a/README.md +++ b/README.md @@ -98,20 +98,6 @@ repository.save Article.new(title: 'Test') # => {"_index"=>"repository", "_type"=>"article", "_id"=>"Ak75E0U9Q96T5Y999_39NA", ...} ``` -Example of using Elasticsearch as a persistence layer for a Ruby model: - -```ruby -require 'elasticsearch/persistence/model' -class Article - include Elasticsearch::Persistence::Model - attribute :title, String, mapping: { analyzer: 'snowball' } -end - -Article.create title: 'Test' -# POST http://localhost:9200/articles/article -# => #
-``` - **Please refer to each library documentation for detailed information and examples.** ### Model diff --git a/elasticsearch-persistence/README.md b/elasticsearch-persistence/README.md index 4c6dead10..e95a7f005 100644 --- a/elasticsearch-persistence/README.md +++ b/elasticsearch-persistence/README.md @@ -38,7 +38,6 @@ or install it from a source code checkout: The library provides two different patterns for adding persistence to your Ruby objects: * [Repository Pattern](#the-repository-pattern) -* [ActiveRecord Pattern](#the-activerecord-pattern) ### The Repository Pattern @@ -445,262 +444,8 @@ and demonstrates a rich set of features: ### The ActiveRecord Pattern -The `Elasticsearch::Persistence::Model` module provides an implementation of the -active record [pattern](http://www.martinfowler.com/eaaCatalog/activeRecord.html), -with a familiar interface for using Elasticsearch as a persistence layer in -Ruby on Rails applications. - -All the methods are documented with comprehensive examples in the source code, -available also online at . - -#### Installation/Usage - -To use the library in a Rails application, add it to your `Gemfile` with a `require` statement: - -```ruby -gem "elasticsearch-persistence", require: 'elasticsearch/persistence/model' -``` - -To use the library without Bundler, install it, and require the file: - -```bash -gem install elasticsearch-persistence -``` - -```ruby -# In your code -require 'elasticsearch/persistence/model' -``` - -#### Model Definition - -The integration is implemented by including the module in a Ruby class. -The model attribute definition support is implemented with the -[_Virtus_](https://github.com/solnic/virtus) Rubygem, and the -naming, validation, etc. features with the -[_ActiveModel_](https://github.com/rails/rails/tree/master/activemodel) Rubygem. - -```ruby -class Article - include Elasticsearch::Persistence::Model - - # Define a plain `title` attribute - # - attribute :title, String - - # Define an `author` attribute, with multiple analyzers for this field - # - attribute :author, String, mapping: { fields: { - author: { type: 'text'}, - raw: { type: 'keyword' } - } } - - - # Define a `views` attribute, with default value - # - attribute :views, Integer, default: 0, mapping: { type: 'integer' } - - # Validate the presence of the `title` attribute - # - validates :title, presence: true - - # Execute code after saving the model. - # - after_save { puts "Successfully saved: #{self}" } -end -``` - -Attribute validations work like for any other _ActiveModel_-compatible implementation: - -```ruby -article = Article.new # => #
- -article.valid? -# => false - -article.errors.to_a -# => ["Title can't be blank"] -``` - -#### Persistence - -We can create a new article in the database... - -```ruby -Article.create id: 1, title: 'Test', author: 'John' -# PUT http://localhost:9200/articles/article/1 [status:201, request:0.015s, query:n/a] -``` - -... and find it: - -```ruby -article = Article.find(1) -# => #
- -article._index -# => "articles" - -article.id -# => "1" - -article.title -# => "Test" -``` - -To update the model, either update the attribute and save the model: - -```ruby -article.title = 'Updated' - -article.save -# => {"_index"=>"articles", "_type"=>"article", "_id"=>"1", "_version"=>2, "created"=>false} -``` - -... or use the `update_attributes` method: - -```ruby -article.update_attributes title: 'Test', author: 'Mary' -# => {"_index"=>"articles", "_type"=>"article", "_id"=>"1", "_version"=>3} -``` - -The implementation supports the familiar interface for updating model timestamps: - -```ruby -article.touch -# => => { ... "_version"=>4} -``` - -... and numeric attributes: - -```ruby -article.views -# => 0 - -article.increment :views -article.views -# => 1 -``` - -Any callbacks defined in the model will be triggered during the persistence operations: - -```ruby -article.save -# Successfully saved: #
-``` - -The model also supports familiar `find_in_batches` and `find_each` methods to efficiently -retrieve big collections of model instances, using the Elasticsearch's _Scan API_: - -```ruby -Article.find_each(_source_include: 'title') { |a| puts "===> #{a.title.upcase}" } -# GET http://localhost:9200/articles/article/_search?scroll=5m&size=20 -# GET http://localhost:9200/_search/scroll?scroll=5m&scroll_id=c2Nhb... -# ===> TEST -# GET http://localhost:9200/_search/scroll?scroll=5m&scroll_id=c2Nhb... -# => "c2Nhb..." -``` - -#### Search - -The model class provides a `search` method to retrieve model instances with a regular -search definition, including highlighting, aggregations, etc: - -```ruby -results = Article.search query: { match: { title: 'test' } }, - aggregations: { authors: { terms: { field: 'author.raw' } } }, - highlight: { fields: { title: {} } } - -puts results.first.title -# Test - -puts results.first.hit.highlight['title'] -# Test - -puts results.response.aggregations.authors.buckets.each { |b| puts "#{b['key']} : #{b['doc_count']}" } -# John : 1 -``` - -#### The Elasticsearch Client - -The module will set up a [client](https://github.com/elastic/elasticsearch-ruby/tree/master/elasticsearch), -connected to `localhost:9200`, by default. - -To use a client with different configuration: - -```ruby -Elasticsearch::Persistence.client = Elasticsearch::Client.new log: true -``` - -To set up a specific client for a specific model: - -```ruby -Article.gateway.client = Elasticsearch::Client.new host: 'api.server.org' -``` - -You might want to do this during you application bootstrap process, e.g. in a Rails initializer. - -Please refer to the -[`elasticsearch-transport`](https://github.com/elasticsearch/elasticsearch-ruby/tree/master/elasticsearch-transport) -library documentation for all the configuration options, and to the -[`elasticsearch-api`](http://rubydoc.info/gems/elasticsearch-api) library documentation -for information about the Ruby client API. - -#### Accessing the Repository Gateway and the Client - -The integration with Elasticsearch is implemented by embedding the repository object in the model. -You can access it through the `gateway` method: - -```ruby -Artist.gateway.client.info -# GET http://localhost:9200/ [status:200, request:0.011s, query:n/a] -# => {"status"=>200, "name"=>"Lightspeed", ...} -``` - -#### Rails Compatibility - -The model instances are fully compatible with Rails' conventions and helpers: - -```ruby -url_for article -# => "http://localhost:3000/articles/1" - -div_for article -# => '
' -``` - -... as well as form values for dates and times: - -```ruby -article = Article.new "title" => "Date", "published(1i)"=>"2014", "published(2i)"=>"1", "published(3i)"=>"1" - -article.published.iso8601 -# => "2014-01-01" -``` - -The library provides a Rails ORM generator to facilitate building the application scaffolding: - -```bash -rails generate scaffold Person name:String email:String birthday:Date --orm=elasticsearch -``` - -#### Example application - -A fully working Ruby on Rails application can be generated with the following command: - -```bash -rails new music --force --skip --skip-bundle --skip-active-record --template https://raw.githubusercontent.com/elastic/elasticsearch-rails/master/elasticsearch-persistence/examples/music/template.rb -``` - -The application demonstrates: - -* How to set up model attributes with custom mappings -* How to define model relationships with Elasticsearch's parent/child -* How to configure models to use a common index, and create the index with proper mappings -* How to use Elasticsearch's completion suggester to drive auto-complete functionality -* How to use Elasticsearch-persisted models in Rails' views and forms -* How to write controller tests - -The source files for the application are available in the [`examples/music`](examples/music) folder. +The ActiveRecord pattern has been deprecated as of version 6.0.0 of this gem. Please use the +[Repository Pattern](#the-repository-pattern) instead. ## License diff --git a/elasticsearch-persistence/lib/elasticsearch/persistence/model.rb b/elasticsearch-persistence/lib/elasticsearch/persistence/model.rb deleted file mode 100644 index f6cd50eb0..000000000 --- a/elasticsearch-persistence/lib/elasticsearch/persistence/model.rb +++ /dev/null @@ -1,135 +0,0 @@ -require 'active_support/core_ext/module/delegation' - -require 'active_model' -require 'virtus' - -require 'elasticsearch/persistence' -require 'elasticsearch/persistence/model/base' -require 'elasticsearch/persistence/model/errors' -require 'elasticsearch/persistence/model/store' -require 'elasticsearch/persistence/model/find' - -module Elasticsearch - module Persistence - - # When included, extends a plain Ruby class with persistence-related features via the ActiveRecord pattern - # - # @example Include the repository in a custom class - # - # require 'elasticsearch/persistence/model' - # - # class MyObject - # include Elasticsearch::Persistence::Model - # end - # - module Model - def self.included(base) - base.class_eval do - include ActiveModel::Naming - include ActiveModel::Conversion - include ActiveModel::Serialization - include ActiveModel::Serializers::JSON - include ActiveModel::Validations - - include Virtus.model - - extend ActiveModel::Callbacks - define_model_callbacks :create, :save, :update, :destroy - define_model_callbacks :find, :touch, only: :after - - include Elasticsearch::Persistence::Model::Base::InstanceMethods - - extend Elasticsearch::Persistence::Model::Store::ClassMethods - include Elasticsearch::Persistence::Model::Store::InstanceMethods - - extend Elasticsearch::Persistence::Model::Find::ClassMethods - - class << self - # Re-define the Virtus' `attribute` method, to configure Elasticsearch mapping as well - # - def attribute(name, type=nil, options={}, &block) - mapping = options.delete(:mapping) || {} - super - - gateway.mapping do - indexes name, {type: Utils::lookup_type(type)}.merge(mapping) - end - - gateway.mapping(&block) if block_given? - end - - # Return the {Repository::Class} instance - # - def gateway(&block) - @gateway ||= Elasticsearch::Persistence::Repository::Class.new host: self - block.arity < 1 ? @gateway.instance_eval(&block) : block.call(@gateway) if block_given? - @gateway - end - - # Delegate methods to repository - # - delegate :settings, - :mappings, - :mapping, - :document_type=, - :index_name, - :index_name=, - :find, - :exists?, - :create_index!, - :refresh_index!, - to: :gateway - - # forward document type to mappings when set - def document_type(type = nil) - return gateway.document_type unless type - gateway.document_type type - mapping.type = type - end - end - - # Configure the repository based on the model (set up index_name, etc) - # - gateway do - klass base - index_name base.model_name.collection.gsub(/\//, '-') - document_type base.model_name.element - - def serialize(document) - document.to_hash.except(:id, 'id') - end - - def deserialize(document) - object = klass.new document['_source'] - - # Set the meta attributes when fetching the document from Elasticsearch - # - object.instance_variable_set :@_id, document['_id'] - object.instance_variable_set :@_index, document['_index'] - object.instance_variable_set :@_type, document['_type'] - object.instance_variable_set :@_version, document['_version'] - object.instance_variable_set :@_source, document['_source'] - - # Store the "hit" information (highlighting, score, ...) - # - object.instance_variable_set :@hit, - Elasticsearch::Model::HashWrapper.new(document.except('_index', '_type', '_id', '_version', '_source')) - - object.instance_variable_set(:@persisted, true) - object - end - end - - # Set up common attributes - # - attribute :created_at, Time, default: lambda { |o,a| Time.now.utc } - attribute :updated_at, Time, default: lambda { |o,a| Time.now.utc } - - attr_reader :hit - end - - end - end - - end -end diff --git a/elasticsearch-persistence/lib/elasticsearch/persistence/model/base.rb b/elasticsearch-persistence/lib/elasticsearch/persistence/model/base.rb deleted file mode 100644 index a0af32411..000000000 --- a/elasticsearch-persistence/lib/elasticsearch/persistence/model/base.rb +++ /dev/null @@ -1,87 +0,0 @@ -module Elasticsearch - module Persistence - module Model - # This module contains the base interface for models - # - module Base - module InstanceMethods - - # Model initializer sets the `@id` variable if passed - # - def initialize(attributes={}) - @_id = attributes[:id] || attributes['id'] - super - end - - # Return model attributes as a Hash, merging in the `id` - # - def attributes - super.merge id: id - end - - # Return the document `_id` - # - def id - @_id - end; alias :_id :id - - # Set the document `_id` - # - def id=(value) - @_id = value - end; alias :_id= :id= - - # Return the document `_index` - # - def _index - @_index - end - - # Return the document `_type` - # - def _type - @_type - end - - # Return the document `_version` - # - def _version - @_version - end - - # Return the raw document `_source` - # - def _source - @_source - end - - def to_s - "#<#{self.class} #{attributes.to_hash.inspect.gsub(/:(\w+)=>/, '\1: ')}>" - end; alias :inspect :to_s - end - end - - # Utility methods for {Elasticsearch::Persistence::Model} - # - module Utils - - # Return Elasticsearch type based on passed Ruby class (used in the `attribute` method) - # - def lookup_type(type) - case - when type == String - 'text' - when type == Integer - 'integer' - when type == Float - 'float' - when type == Date || type == Time || type == DateTime - 'date' - when type == Virtus::Attribute::Boolean - 'boolean' - end - end; module_function :lookup_type - end - end - end -end diff --git a/elasticsearch-persistence/lib/elasticsearch/persistence/model/errors.rb b/elasticsearch-persistence/lib/elasticsearch/persistence/model/errors.rb deleted file mode 100644 index 3d8ebb88e..000000000 --- a/elasticsearch-persistence/lib/elasticsearch/persistence/model/errors.rb +++ /dev/null @@ -1,8 +0,0 @@ -module Elasticsearch - module Persistence - module Model - class DocumentNotSaved < StandardError; end - class DocumentNotPersisted < StandardError; end - end - end -end diff --git a/elasticsearch-persistence/lib/elasticsearch/persistence/model/find.rb b/elasticsearch-persistence/lib/elasticsearch/persistence/model/find.rb deleted file mode 100644 index c09c9ac38..000000000 --- a/elasticsearch-persistence/lib/elasticsearch/persistence/model/find.rb +++ /dev/null @@ -1,180 +0,0 @@ -module Elasticsearch - module Persistence - module Model - - module Find - class SearchRequest < Elasticsearch::Model::Searching::SearchRequest - def execute! - klass.gateway.search(definition[:body] || definition[:q], options) - end - end - - module ClassMethods - - def search(query_or_definition, options={}) - SearchRequest.new(self, query_or_definition, options).execute! - end - - # Returns all models (up to 10,000) - # - # @example Retrieve all people - # - # Person.all - # # => [# [# 2 - # - # @example Return the count of models matching a simple query - # - # Person.count('fox or dog') - # # => 1 - # - # @example Return the count of models matching a query in the Elasticsearch DSL - # - # Person.search(query: { match: { title: 'fox dog' } }) - # # => 1 - # - # @return [Integer] - # - def count(query_or_definition=nil, options={}) - gateway.count( query_or_definition, options ) - end - - # Returns all models efficiently via the Elasticsearch's scroll API - # - # You can restrict the models being returned with a query. - # - # The {http://rubydoc.info/gems/elasticsearch-api/Elasticsearch/API/Actions#search-instance_method Search API} - # options are passed to the search method as parameters, all remaining options are passed - # as the `:body` parameter. - # - # The full {Persistence::Repository::Response::Results} instance is yielded to the passed - # block in each batch, so you can access any of its properties; calling `to_a` will - # convert the object to an Array of model instances. - # - # @example Return all models in batches of 20 x number of primary shards - # - # Person.find_in_batches { |batch| puts batch.map(&:name) } - # - # @example Return all models in batches of 100 x number of primary shards - # - # Person.find_in_batches(size: 100) { |batch| puts batch.map(&:name) } - # - # @example Return all models matching a specific query - # - # Person.find_in_batches(query: { match: { name: 'test' } }) { |batch| puts batch.map(&:name) } - # - # @example Return all models, fetching only the `name` attribute from Elasticsearch - # - # Person.find_in_batches( _source_include: 'name') { |_| puts _.response.hits.hits.map(&:to_hash) } - # - # @example Leave out the block to return an Enumerator instance - # - # Person.find_in_batches(size: 100).map { |batch| batch.size } - # # => [100, 100, 100, ... ] - # - # @return [String,Enumerator] The `scroll_id` for the request or Enumerator when the block is not passed - # - def find_in_batches(options={}, &block) - return to_enum(:find_in_batches, options) unless block_given? - - search_params = options.extract!( - :index, - :type, - :scroll, - :size, - :explain, - :ignore_indices, - :ignore_unavailable, - :allow_no_indices, - :expand_wildcards, - :preference, - :q, - :routing, - :source, - :_source, - :_source_include, - :_source_exclude, - :stats, - :timeout) - - scroll = search_params.delete(:scroll) || '5m' - - body = options - - # Get the initial batch of documents and the scroll_id - # - response = gateway.client.search( { - index: gateway.index_name, - type: gateway.document_type, - scroll: scroll, - sort: ['_doc'], - size: 20, - body: body }.merge(search_params) ) - - - # Scroll the search object and break when receiving an empty array of hits - # - while response['hits']['hits'].any? do - yield Repository::Response::Results.new(gateway, response) - - response = gateway.client.scroll( { scroll_id: response['_scroll_id'], scroll: scroll } ) - end - - return response['_scroll_id'] - end - - # Iterate effectively over models using the `find_in_batches` method. - # - # All the options are passed to `find_in_batches` and each result is yielded to the passed block. - # - # @example Print out the people's names by scrolling through the index - # - # Person.find_each { |person| puts person.name } - # - # # # GET http://localhost:9200/people/person/_search?scroll=5m&size=20 - # # # GET http://localhost:9200/_search/scroll?scroll=5m&scroll_id=c2Nhbj... - # # Test 0 - # # Test 1 - # # Test 2 - # # ... - # # # GET http://localhost:9200/_search/scroll?scroll=5m&scroll_id=c2Nhbj... - # # Test 20 - # # Test 21 - # # Test 22 - # - # @example Leave out the block to return an Enumerator instance - # - # Person.find_each.select { |person| person.name =~ /John/ } - # # => => [#] - # - # @return [String,Enumerator] The `scroll_id` for the request or Enumerator when the block is not passed - # - def find_each(options = {}) - return to_enum(:find_each, options) unless block_given? - - find_in_batches(options) do |batch| - batch.each { |result| yield result } - end - end - end - end - - end - end -end diff --git a/elasticsearch-persistence/lib/elasticsearch/persistence/model/rails.rb b/elasticsearch-persistence/lib/elasticsearch/persistence/model/rails.rb deleted file mode 100644 index 5ed510e21..000000000 --- a/elasticsearch-persistence/lib/elasticsearch/persistence/model/rails.rb +++ /dev/null @@ -1,47 +0,0 @@ -module Elasticsearch - module Persistence - module Model - - # Make the `Persistence::Model` models compatible with Ruby On Rails applications - # - module Rails - def self.included(base) - base.class_eval do - - def initialize(attributes={}) - super(__convert_rails_dates(attributes)) - end - - def update(attributes={}, options={}) - super(__convert_rails_dates(attributes), options) - end - end - end - - # Decorates the passed in `attributes` so they extract the date & time values from Rails forms - # - # @example Correctly combine the date and time to a datetime string - # - # params = { "published_on(1i)"=>"2014", - # "published_on(2i)"=>"1", - # "published_on(3i)"=>"1", - # "published_on(4i)"=>"12", - # "published_on(5i)"=>"00" - # } - # MyRailsModel.new(params).published_on.iso8601 - # # => "2014-01-01T12:00:00+00:00" - # - def __convert_rails_dates(attributes={}) - day = attributes.select { |p| p =~ /\([1-3]/ }.reduce({}) { |sum, item| (sum[item.first.gsub(/\(.+\)/, '')] ||= '' )<< item.last+'-'; sum } - time = attributes.select { |p| p =~ /\([4-6]/ }.reduce({}) { |sum, item| (sum[item.first.gsub(/\(.+\)/, '')] ||= '' )<< item.last+':'; sum } - unless day.empty? - attributes.update day.reduce({}) { |sum, item| sum[item.first] = item.last; sum[item.first] += ' ' + time[item.first] unless time.empty?; sum } - end - - return attributes - end; module_function :__convert_rails_dates - end - - end - end -end diff --git a/elasticsearch-persistence/lib/elasticsearch/persistence/model/store.rb b/elasticsearch-persistence/lib/elasticsearch/persistence/model/store.rb deleted file mode 100644 index a6c44c998..000000000 --- a/elasticsearch-persistence/lib/elasticsearch/persistence/model/store.rb +++ /dev/null @@ -1,254 +0,0 @@ -module Elasticsearch - module Persistence - module Model - - # This module contains the storage related features of {Elasticsearch::Persistence::Model} - # - module Store - module ClassMethods #:nodoc: - - # Creates a class instance, saves it, if validations pass, and returns it - # - # @example Create a new person - # - # Person.create name: 'John Smith' - # # => # - # - # @return [Object] The model instance - # - def create(attributes, options={}) - object = self.new(attributes) - object.run_callbacks :create do - object.save(options) - object - end - end - end - - module InstanceMethods - - # Saves the model (if validations pass) and returns the response (or `false`) - # - # @example Save a valid model instance - # - # p = Person.new(name: 'John') - # p.save - # => {"_index"=>"people", ... "_id"=>"RzFSXFR0R8u1CZIWNs2Gvg", "_version"=>1, "created"=>true} - # - # @example Save an invalid model instance - # - # p = Person.new(name: nil) - # p.save - # # => false - # - # @return [Hash,FalseClass] The Elasticsearch response as a Hash or `false` - # - def save(options={}) - unless options.delete(:validate) == false - return false unless valid? - end - - run_callbacks :save do - options.update id: self.id - options.update index: self._index if self._index - options.update type: self._type if self._type - - self[:updated_at] = Time.now.utc - - response = self.class.gateway.save(self, options) - - @_id = response['_id'] - @_index = response['_index'] - @_type = response['_type'] - @_version = response['_version'] - @persisted = true - - response - end - end - - # Deletes the model from Elasticsearch (if it's persisted), freezes it, and returns the response - # - # @example Delete a model instance - # - # p.destroy - # => {"_index"=>"people", ... "_id"=>"RzFSXFR0R8u1CZIWNs2Gvg", "_version"=>2 ...} - # - # @return [Hash] The Elasticsearch response as a Hash - # - def destroy(options={}) - raise DocumentNotPersisted, "Object not persisted: #{self.inspect}" unless persisted? - - run_callbacks :destroy do - options.update index: self._index if self._index - options.update type: self._type if self._type - - response = self.class.gateway.delete(self.id, options) - - @destroyed = true - @persisted = false - self.freeze - response - end - end; alias :delete :destroy - - # Updates the model (via Elasticsearch's "Update" API) and returns the response - # - # @example Update a model with partial attributes - # - # p.update name: 'UPDATED' - # => {"_index"=>"people", ... "_version"=>2} - # - # @example Pass a version for concurrency control - # - # p.update( { name: 'UPDATED' }, { version: 2 } ) - # => {"_index"=>"people", ... "_version"=>3} - # - # @example An exception is raised when the version doesn't match - # - # p.update( { name: 'UPDATED' }, { version: 2 } ) - # => Elasticsearch::Transport::Transport::Errors::Conflict: [409] {"error" ... } - # - # @return [Hash] The Elasticsearch response as a Hash - # - def update(attributes={}, options={}) - unless options.delete(:validate) == false - return false unless valid? - end - raise DocumentNotPersisted, "Object not persisted: #{self.inspect}" unless persisted? - - run_callbacks :update do - options.update index: self._index if self._index - options.update type: self._type if self._type - - attributes.update( { updated_at: Time.now.utc } ) - response = self.class.gateway.update(self.id, { doc: attributes}.merge(options)) - - self.attributes = self.attributes.merge(attributes) - @_index = response['_index'] - @_type = response['_type'] - @_version = response['_version'] - - response - end - end; alias :update_attributes :update - - # Increments a numeric attribute (via Elasticsearch's "Update" API) and returns the response - # - # @example Increment the `salary` attribute by 1 - # - # p.increment :salary - # - # @example Increment the `salary` attribute by 100 - # - # p.increment :salary, 100 - # - # @return [Hash] The Elasticsearch response as a Hash - # - def increment(attribute, value=1, options={}) - raise DocumentNotPersisted, "Object not persisted: #{self.inspect}" unless persisted? - - options.update index: self._index if self._index - options.update type: self._type if self._type - - response = self.class.gateway.update(self.id, { script: "ctx._source.#{attribute} += #{value}"}.merge(options)) - - self[attribute] += value - - @_index = response['_index'] - @_type = response['_type'] - @_version = response['_version'] - - response - end - - # Decrements a numeric attribute (via Elasticsearch's "Update" API) and returns the response - # - # @example Decrement the `salary` attribute by 1 - # - # p.decrement :salary - # - # @example Decrement the `salary` attribute by 100 - # - # p.decrement :salary, 100 - # - # @return [Hash] The Elasticsearch response as a Hash - # - def decrement(attribute, value=1, options={}) - raise DocumentNotPersisted, "Object not persisted: #{self.inspect}" unless persisted? - - options.update index: self._index if self._index - options.update type: self._type if self._type - - response = self.class.gateway.update(self.id, { script: "ctx._source.#{attribute} = ctx._source.#{attribute} - #{value}"}.merge(options)) - self[attribute] -= value - - @_index = response['_index'] - @_type = response['_type'] - @_version = response['_version'] - - response - end - - # Updates the `updated_at` attribute, saves the model and returns the response - # - # @example Update the `updated_at` attribute (default) - # - # p.touch - # - # @example Update a custom attribute: `saved_on` - # - # p.touch :saved_on - # - # @return [Hash] The Elasticsearch response as a Hash - # - def touch(attribute=:updated_at, options={}) - raise DocumentNotPersisted, "Object not persisted: #{self.inspect}" unless persisted? - raise ArgumentError, "Object does not have '#{attribute}' attribute" unless respond_to?(attribute) - - run_callbacks :touch do - options.update index: self._index if self._index - options.update type: self._type if self._type - - value = Time.now.utc - response = self.class.gateway.update(self.id, { doc: { attribute => value.iso8601 }}.merge(options)) - - self[attribute] = value - - @_index = response['_index'] - @_type = response['_type'] - @_version = response['_version'] - - response - end - end - - # Returns true when the model has been destroyed, false otherwise - # - # @return [TrueClass,FalseClass] - # - def destroyed? - !!@destroyed - end - - # Returns true when the model has been already saved to the database, false otherwise - # - # @return [TrueClass,FalseClass] - # - def persisted? - !!@persisted && !destroyed? - end - - # Returns true when the model has not been saved yet, false otherwise - # - # @return [TrueClass,FalseClass] - # - def new_record? - !persisted? && !destroyed? - end - end - end - - end - end -end diff --git a/elasticsearch-persistence/lib/elasticsearch/persistence/model/utils.rb b/elasticsearch-persistence/lib/elasticsearch/persistence/model/utils.rb deleted file mode 100644 index e69de29bb..000000000 diff --git a/elasticsearch-persistence/lib/rails/generators/elasticsearch/model/model_generator.rb b/elasticsearch-persistence/lib/rails/generators/elasticsearch/model/model_generator.rb deleted file mode 100644 index 84fb632cd..000000000 --- a/elasticsearch-persistence/lib/rails/generators/elasticsearch/model/model_generator.rb +++ /dev/null @@ -1,21 +0,0 @@ -require "rails/generators/elasticsearch_generator" - -module Elasticsearch - module Generators - class ModelGenerator < ::Rails::Generators::NamedBase - source_root File.expand_path('../templates', __FILE__) - - desc "Creates an Elasticsearch::Persistence model" - argument :attributes, type: :array, default: [], banner: "attribute:type attribute:type" - - check_class_collision - - def create_model_file - @padding = attributes.map { |a| a.name.size }.max - template "model.rb.tt", File.join("app/models", class_path, "#{file_name}.rb") - end - - hook_for :test_framework - end - end -end diff --git a/elasticsearch-persistence/lib/rails/generators/elasticsearch/model/templates/model.rb.tt b/elasticsearch-persistence/lib/rails/generators/elasticsearch/model/templates/model.rb.tt deleted file mode 100644 index 0597174da..000000000 --- a/elasticsearch-persistence/lib/rails/generators/elasticsearch/model/templates/model.rb.tt +++ /dev/null @@ -1,9 +0,0 @@ -<% module_namespacing do -%> -class <%= class_name %> - include Elasticsearch::Persistence::Model - -<% attributes.each do |attribute| -%> - <%= "attribute :#{attribute.name},".ljust(@padding+12) %> <%= attribute.type %> -<% end -%> -end -<% end -%> diff --git a/elasticsearch-persistence/lib/rails/generators/elasticsearch_generator.rb b/elasticsearch-persistence/lib/rails/generators/elasticsearch_generator.rb deleted file mode 100644 index 20072ed93..000000000 --- a/elasticsearch-persistence/lib/rails/generators/elasticsearch_generator.rb +++ /dev/null @@ -1,2 +0,0 @@ -require "rails/generators/named_base" -require "rails/generators/active_model" diff --git a/elasticsearch-persistence/test/integration/model/model_basic_test.rb b/elasticsearch-persistence/test/integration/model/model_basic_test.rb deleted file mode 100644 index 33555c980..000000000 --- a/elasticsearch-persistence/test/integration/model/model_basic_test.rb +++ /dev/null @@ -1,233 +0,0 @@ -require 'test_helper' - -require 'elasticsearch/persistence/model' -require 'elasticsearch/persistence/model/rails' - -module Elasticsearch - module Persistence - class PersistenceModelBasicIntegrationTest < Elasticsearch::Test::IntegrationTestCase - - class ::Person - include Elasticsearch::Persistence::Model - include Elasticsearch::Persistence::Model::Rails - - settings index: { number_of_shards: 1 } - document_type 'human_being' - - attribute :name, String, - mapping: { fields: { - name: { type: 'text', analyzer: 'snowball' }, - raw: { type: 'keyword' } - } } - - attribute :birthday, Date - attribute :department, String - attribute :salary, Integer - attribute :admin, Boolean, default: false - - validates :name, presence: true - end - - context "A basic persistence model" do - setup do - Person.create_index! force: true - end - - should "save the object with custom ID" do - person = Person.new id: 1, name: 'Number One' - person.save - - document = Person.find(1) - assert_not_nil document - assert_equal 'Number One', document.name - end - - should "create the object with custom ID" do - person = Person.create id: 1, name: 'Number One' - - document = Person.find(1) - assert_not_nil document - assert_equal 'Number One', document.name - end - - should "save and find the object" do - person = Person.new name: 'John Smith', birthday: Date.parse('1970-01-01') - assert person.save - - assert_not_nil person.id - document = Person.find(person.id) - - assert_instance_of Person, document - assert_equal 'John Smith', document.name - assert_equal 'John Smith', Person.find(person.id).name - - assert_not_nil Elasticsearch::Persistence.client.get index: 'people', type: 'human_being', id: person.id - end - - should "not save an invalid object" do - person = Person.new name: nil - assert ! person.save - end - - should "save an invalid object with the :validate option" do - person = Person.new name: nil, salary: 100 - assert person.save validate: false - - assert_not_nil person.id - document = Person.find(person.id) - assert_equal 100, document.salary - end - - should "delete the object" do - person = Person.create name: 'John Smith', birthday: Date.parse('1970-01-01') - - person.destroy - assert person.frozen? - - assert_raise Elasticsearch::Transport::Transport::Errors::NotFound do - Elasticsearch::Persistence.client.get index: 'people', type: 'person', id: person.id - end - end - - should "update an object attribute" do - person = Person.create name: 'John Smith' - - person.update name: 'UPDATED' - - assert_equal 'UPDATED', person.name - assert_equal 'UPDATED', Person.find(person.id).name - end - - should "create the model with correct Date form Rails' form attributes" do - params = { "birthday(1i)"=>"2014", - "birthday(2i)"=>"1", - "birthday(3i)"=>"1" - } - person = Person.create params.merge(name: 'TEST') - - assert_equal Date.parse('2014-01-01'), person.birthday - assert_equal Date.parse('2014-01-01'), Person.find(person.id).birthday - end - - should_eventually "update the model with correct Date form Rails' form attributes" do - params = { "birthday(1i)"=>"2014", - "birthday(2i)"=>"1", - "birthday(3i)"=>"1" - } - person = Person.create params.merge(name: 'TEST') - - person.update params.merge('birthday(1i)' => '2015') - - assert_equal Date.parse('2015-01-01'), person.birthday - assert_equal Date.parse('2015-01-01'), Person.find(person.id).birthday - end - - should "increment an object attribute" do - person = Person.create name: 'John Smith', salary: 1_000 - - person.increment :salary - - assert_equal 1_001, person.salary - assert_equal 1_001, Person.find(person.id).salary - end - - should "update the object timestamp" do - person = Person.create name: 'John Smith' - updated_at = person.updated_at - - sleep 1 - person.touch - - assert person.updated_at > updated_at, [person.updated_at, updated_at].inspect - - found = Person.find(person.id) - assert found.updated_at > updated_at, [found.updated_at, updated_at].inspect - end - - should 'update the object timestamp on save' do - person = Person.create name: 'John Smith' - person.admin = true - sleep 1 - person.save - - Person.gateway.refresh_index! - - found = Person.find(person.id) - - # Compare without milliseconds - assert_equal person.updated_at.to_i, found.updated_at.to_i - end - - should "respect the version" do - person = Person.create name: 'John Smith' - - person.update( { name: 'UPDATE 1' }) - - assert_raise Elasticsearch::Transport::Transport::Errors::Conflict do - person.update( { name: 'UPDATE 2' }, { version: 1 } ) - end - end - - should "find all instances" do - Person.create name: 'John Smith' - Person.create name: 'Mary Smith' - Person.gateway.refresh_index! - - people = Person.all - - assert_equal 2, people.total - assert_equal 2, people.size - end - - should "find instances by search" do - Person.create name: 'John Smith' - Person.create name: 'Mary Smith' - Person.gateway.refresh_index! - - people = Person.search query: { match: { name: 'smith' } }, - highlight: { fields: { name: {} } } - - assert_equal 2, people.total - assert_equal 2, people.size - - assert people.map_with_hit { |o,h| h._score }.all? { |s| s > 0 } - - assert_not_nil people.first.hit - assert_match /smith/i, people.first.hit.highlight['name'].first - end - - should "find instances in batches" do - 50.times { |i| Person.create name: "John #{i+1}" } - Person.gateway.refresh_index! - - @batches = 0 - @results = [] - - Person.find_in_batches(_source_include: 'name') do |batch| - @batches += 1 - @results += batch.map(&:name) - end - - assert_equal 3, @batches - assert_equal 50, @results.size - assert_contains @results, 'John 1' - end - - should "find each instance" do - 50.times { |i| Person.create name: "John #{i+1}" } - Person.gateway.refresh_index! - - @results = [] - - Person.find_each(_source_include: 'name') do |person| - @results << person.name - end - - assert_equal 50, @results.size - assert_contains @results, 'John 1' - end - end - - end - end -end diff --git a/elasticsearch-persistence/test/unit/model_base_test.rb b/elasticsearch-persistence/test/unit/model_base_test.rb deleted file mode 100644 index 2d55e1620..000000000 --- a/elasticsearch-persistence/test/unit/model_base_test.rb +++ /dev/null @@ -1,72 +0,0 @@ -require 'test_helper' - -require 'elasticsearch/persistence/model' -require 'elasticsearch/persistence/model/rails' - -class Elasticsearch::Persistence::ModelBaseTest < Test::Unit::TestCase - context "The model" do - setup do - class DummyBaseModel - include Elasticsearch::Persistence::Model - - attribute :name, String - end - end - - should "respond to id, _id, _index, _type and _version" do - model = DummyBaseModel.new - - [:id, :_id, :_index, :_type, :_version].each { |method| assert_respond_to model, method } - end - - should "set the ID from attributes during initialization" do - model = DummyBaseModel.new id: 1 - assert_equal 1, model.id - - model = DummyBaseModel.new 'id' => 2 - assert_equal 2, model.id - end - - should "set the ID using setter method" do - model = DummyBaseModel.new id: 1 - assert_equal 1, model.id - - model.id = 2 - assert_equal 2, model.id - end - - should "have ID in attributes" do - model = DummyBaseModel.new id: 1, name: 'Test' - assert_equal 1, model.attributes[:id] - end - - should "have the customized inspect method" do - model = DummyBaseModel.new name: 'Test' - assert_match /name\: "Test"/, model.inspect - end - - context "with custom document_type" do - setup do - @model = DummyBaseModel - @gateway = mock() - @mapping = mock() - @model.stubs(:gateway).returns(@gateway) - @gateway.stubs(:mapping).returns(@mapping) - @document_type = 'dummybase' - end - - should "forward the argument to mapping" do - @gateway.expects(:document_type).with(@document_type).once - @mapping.expects(:type=).with(@document_type).once - @model.document_type @document_type - end - - should "return the value from the gateway" do - @gateway.expects(:document_type).once.returns(@document_type) - @mapping.expects(:type=).never - returned_type = @model.document_type - assert_equal @document_type, returned_type - end - end - end -end diff --git a/elasticsearch-persistence/test/unit/model_find_test.rb b/elasticsearch-persistence/test/unit/model_find_test.rb deleted file mode 100644 index 92370c1e4..000000000 --- a/elasticsearch-persistence/test/unit/model_find_test.rb +++ /dev/null @@ -1,153 +0,0 @@ -require 'test_helper' - -require 'active_model' -require 'virtus' - -require 'elasticsearch/persistence/model/errors' -require 'elasticsearch/persistence/model/find' - -class Elasticsearch::Persistence::ModelFindTest < Test::Unit::TestCase - context "The model find module," do - - class DummyFindModel - include ActiveModel::Naming - include ActiveModel::Conversion - include ActiveModel::Serialization - include ActiveModel::Serializers::JSON - include ActiveModel::Validations - - include Virtus.model - - extend Elasticsearch::Persistence::Model::Find::ClassMethods - - extend ActiveModel::Callbacks - define_model_callbacks :create, :save, :update, :destroy - define_model_callbacks :find, :touch, only: :after - - attribute :title, String - attribute :count, Integer, default: 0 - attribute :created_at, DateTime, default: lambda { |o,a| Time.now.utc } - attribute :updated_at, DateTime, default: lambda { |o,a| Time.now.utc } - end - - setup do - @gateway = stub(client: stub(), index_name: 'foo', document_type: 'bar') - DummyFindModel.stubs(:gateway).returns(@gateway) - - DummyFindModel.stubs(:index_name).returns('foo') - DummyFindModel.stubs(:document_type).returns('bar') - - @response = MultiJson.load <<-JSON - { - "took": 14, - "timed_out": false, - "_shards": { - "total": 5, - "successful": 5, - "failed": 0 - }, - "hits": { - "total": 1, - "max_score": 1.0, - "hits": [ - { - "_index": "dummy", - "_type": "dummy", - "_id": "abc123", - "_score": 1.0, - "_source": { - "name": "TEST" - } - } - ] - } - } - JSON - end - - should "find all records" do - DummyFindModel - .stubs(:search) - .with({ query: { match_all: {} }, size: 10_000 }, {}) - .returns(@response) - - DummyFindModel.all - end - - should "pass options when finding all records" do - DummyFindModel - .expects(:search) - .with({ query: { match: { title: 'test' } }, size: 10_000 }, { routing: 'abc123' }) - .returns(@response) - - DummyFindModel.all( { query: { match: { title: 'test' } } }, { routing: 'abc123' } ) - end - - context "finding via scroll" do - setup do - @gateway - .expects(:deserialize) - .returns('_source' => {'foo' => 'bar'}) - .at_least_once - - # 1. Initial batch of documents and the scroll_id - @gateway.client - .expects(:search) - .with do |arguments| - assert_equal 'foo', arguments[:index] - assert_equal 'bar', arguments[:type] - true - end - .returns(MultiJson.load('{"_scroll_id":"abc123==", "hits":{"hits":[{"_source":{"foo":"bar_1"}}]}}')) - - # 2. Second batch of documents and the scroll_id - # 3. Last, empty batch of documents - @gateway.client - .expects(:scroll) - .twice - .returns(MultiJson.load('{"_scroll_id":"abc456==", "hits":{"hits":[{"_source":{"foo":"bar_2"}}]}}')) - .then - .returns(MultiJson.load('{"_scroll_id":"abc789==", "hits":{"hits":[]}}')) - end - - should "find all records in batches" do - @doc = nil - result = DummyFindModel.find_in_batches { |batch| @doc = batch.first['_source']['foo'] } - - assert_equal 'abc789==', result - assert_equal 'bar', @doc - end - - should "return an Enumerator for find in batches" do - @doc = nil - assert_nothing_raised do - e = DummyFindModel.find_in_batches - assert_instance_of Enumerator, e - - e.each { |batch| @doc = batch.first['_source']['foo'] } - assert_equal 'bar', @doc - end - end - - should "find each" do - @doc = nil - result = DummyFindModel.find_each { |doc| @doc = doc['_source']['foo'] } - - assert_equal 'abc789==', result - assert_equal 'bar', @doc - end - - should "return an Enumerator for find each" do - @doc = nil - assert_nothing_raised do - e = DummyFindModel.find_each - assert_instance_of Enumerator, e - - e.each { |doc| @doc = doc['_source']['foo'] } - assert_equal 'bar', @doc - end - end - end - - end -end diff --git a/elasticsearch-persistence/test/unit/model_gateway_test.rb b/elasticsearch-persistence/test/unit/model_gateway_test.rb deleted file mode 100644 index 2451241d6..000000000 --- a/elasticsearch-persistence/test/unit/model_gateway_test.rb +++ /dev/null @@ -1,101 +0,0 @@ -require 'test_helper' - -require 'elasticsearch/persistence/model' -require 'elasticsearch/persistence/model/rails' - -class Elasticsearch::Persistence::ModelGatewayTest < Test::Unit::TestCase - context "The model gateway" do - setup do - class DummyGatewayModel - include Elasticsearch::Persistence::Model - end - end - - teardown do - Elasticsearch::Persistence::ModelGatewayTest.__send__ :remove_const, :DummyGatewayModel \ - rescue NameError; nil - end - - should "be accessible" do - assert_instance_of Elasticsearch::Persistence::Repository::Class, DummyGatewayModel.gateway - - $a = 0 - DummyGatewayModel.gateway { $a += 1 } - assert_equal 1, $a - - @b = 0 - def run!; DummyGatewayModel.gateway { |g| @b += 1 }; end - run! - assert_equal 1, @b - - assert_equal DummyGatewayModel, DummyGatewayModel.gateway.klass - end - - should "define common attributes" do - d = DummyGatewayModel.new - - assert_respond_to d, :updated_at - assert_respond_to d, :created_at - end - - should "allow to configure settings" do - DummyGatewayModel.settings(number_of_shards: 1) - - assert_equal 1, DummyGatewayModel.settings.to_hash[:number_of_shards] - end - - should "allow to configure mappings" do - DummyGatewayModel.mapping { indexes :name, analyzer: 'snowball' } - - assert_equal 'snowball', - DummyGatewayModel.mapping.to_hash[:dummy_gateway_model][:properties][:name][:analyzer] - end - - should "configure the mapping via attribute" do - DummyGatewayModel.attribute :name, String, mapping: { analyzer: 'snowball' } - - assert_respond_to DummyGatewayModel, :name - assert_equal 'snowball', - DummyGatewayModel.mapping.to_hash[:dummy_gateway_model][:properties][:name][:analyzer] - end - - should "configure the mapping via an attribute block" do - DummyGatewayModel.attribute :name, String do - indexes :name, analyzer: 'custom' - end - - assert_respond_to DummyGatewayModel, :name - assert_equal 'custom', - DummyGatewayModel.mapping.to_hash[:dummy_gateway_model][:properties][:name][:analyzer] - end - - should "properly look up types for classes" do - assert_equal 'text', Elasticsearch::Persistence::Model::Utils::lookup_type(String) - assert_equal 'integer', Elasticsearch::Persistence::Model::Utils::lookup_type(Integer) - assert_equal 'float', Elasticsearch::Persistence::Model::Utils::lookup_type(Float) - assert_equal 'date', Elasticsearch::Persistence::Model::Utils::lookup_type(Date) - assert_equal 'boolean', Elasticsearch::Persistence::Model::Utils::lookup_type(Virtus::Attribute::Boolean) - end - - should "remove IDs from hash when serializing" do - assert_equal( {foo: 'bar'}, DummyGatewayModel.gateway.serialize(id: '123', foo: 'bar') ) - end - - should "set IDs from hash when deserializing" do - assert_equal 'abc123', DummyGatewayModel.gateway.deserialize('_id' => 'abc123', '_source' => {}).id - end - - should "set @persisted variable from hash when deserializing" do - assert DummyGatewayModel.gateway.deserialize('_id' => 'abc123', '_source' => {}).instance_variable_get(:@persisted) - end - - should "allow accessing the raw _source" do - assert_equal 'bar', DummyGatewayModel.gateway.deserialize('_source' => { 'foo' => 'bar' })._source['foo'] - end - - should "allow to access the raw hit from results as Hashie::Mash" do - assert_equal 0.42, DummyGatewayModel.gateway.deserialize('_score' => 0.42, '_source' => {}).hit._score - end - - end -end diff --git a/elasticsearch-persistence/test/unit/model_rails_test.rb b/elasticsearch-persistence/test/unit/model_rails_test.rb deleted file mode 100644 index 3f0ce913b..000000000 --- a/elasticsearch-persistence/test/unit/model_rails_test.rb +++ /dev/null @@ -1,112 +0,0 @@ -require 'test_helper' - -require 'elasticsearch/persistence/model' -require 'elasticsearch/persistence/model/rails' - -require 'rails' -require 'action_controller/railtie' -require 'action_view/railtie' - -class ::MyRailsModel - include Elasticsearch::Persistence::Model - include Elasticsearch::Persistence::Model::Rails - - attribute :name, String, mapping: { analyzer: 'english' } - attribute :published_at, DateTime - attribute :published_on, Date -end - -class Application < Rails::Application - config.eager_load = false - config.root = File.dirname(File.expand_path('../../../tmp', __FILE__)) - config.logger = Logger.new($stderr) - - routes.append do - resources :my_rails_models - end -end - -class ApplicationController < ActionController::Base - include Application.routes.url_helpers - include ActionController::UrlFor -end -ApplicationController.default_url_options = { host: 'localhost' } -ApplicationController._routes.append { resources :my_rails_models } - -class MyRailsModelController < ApplicationController; end - -Application.initialize! - -class Elasticsearch::Persistence::ModelRailsTest < Test::Unit::TestCase - context "The model in a Rails application" do - - should "generate proper URLs and paths" do - model = MyRailsModel.new name: 'Test' - model.stubs(:id).returns(1) - model.stubs(:persisted?).returns(true) - - controller = MyRailsModelController.new - controller.request = ActionDispatch::Request.new({}) - - assert_equal 'http://localhost/my_rails_models/1', controller.url_for(model) - assert_equal '/my_rails_models/1/edit', controller.edit_my_rails_model_path(model) - end - - should "generate a link" do - class MyView; include ActionView::Helpers::UrlHelper; end - - model = MyRailsModel.new name: 'Test' - view = MyView.new - view.expects(:url_for).with(model).returns('foo/bar') - - assert_equal 'Show', view.link_to('Show', model) - end - - should "parse DateTime from Rails forms" do - params = { "published_at(1i)"=>"2014", - "published_at(2i)"=>"1", - "published_at(3i)"=>"1", - "published_at(4i)"=>"12", - "published_at(5i)"=>"00" - } - - assert_equal '2014-1-1- 12:00:', - Elasticsearch::Persistence::Model::Rails.__convert_rails_dates(params)['published_at'] - - m = MyRailsModel.new params - assert_equal "2014-01-01T12:00:00+00:00", m.published_at.iso8601 - end - - should "parse Date from Rails forms" do - params = { "published_on(1i)"=>"2014", - "published_on(2i)"=>"1", - "published_on(3i)"=>"1" - } - - assert_equal '2014-1-1-', - Elasticsearch::Persistence::Model::Rails.__convert_rails_dates(params)['published_on'] - - - m = MyRailsModel.new params - assert_equal "2014-01-01", m.published_on.iso8601 - end - - context "when updating," do - should "pass the options to gateway" do - model = MyRailsModel.new name: 'Test' - model.stubs(:persisted?).returns(true) - - model.class.gateway - .expects(:update) - .with do |object, options| - assert_equal 'ABC', options[:routing] - true - end - .returns({'_id' => 'abc123'}) - - assert model.update( { title: 'UPDATED' }, { routing: 'ABC' } ) - end - end - - end -end diff --git a/elasticsearch-persistence/test/unit/model_store_test.rb b/elasticsearch-persistence/test/unit/model_store_test.rb deleted file mode 100644 index 442ab56ee..000000000 --- a/elasticsearch-persistence/test/unit/model_store_test.rb +++ /dev/null @@ -1,576 +0,0 @@ -require 'test_helper' - -require 'active_model' -require 'virtus' - -require 'elasticsearch/persistence/model/base' -require 'elasticsearch/persistence/model/errors' -require 'elasticsearch/persistence/model/store' - -class Elasticsearch::Persistence::ModelStoreTest < Test::Unit::TestCase - context "The model store module," do - - class DummyStoreModel - include ActiveModel::Naming - include ActiveModel::Conversion - include ActiveModel::Serialization - include ActiveModel::Serializers::JSON - include ActiveModel::Validations - - include Virtus.model - - include Elasticsearch::Persistence::Model::Base::InstanceMethods - extend Elasticsearch::Persistence::Model::Store::ClassMethods - include Elasticsearch::Persistence::Model::Store::InstanceMethods - - extend ActiveModel::Callbacks - define_model_callbacks :create, :save, :update, :destroy - define_model_callbacks :find, :touch, only: :after - - attribute :title, String - attribute :count, Integer, default: 0 - attribute :created_at, DateTime, default: lambda { |o,a| Time.now.utc } - attribute :updated_at, DateTime, default: lambda { |o,a| Time.now.utc } - end - - setup do - @shoulda_subject = DummyStoreModel.new title: 'Test' - @gateway = stub - DummyStoreModel.stubs(:gateway).returns(@gateway) - end - - teardown do - Elasticsearch::Persistence::ModelStoreTest.__send__ :remove_const, :DummyStoreModelWithCallback \ - rescue NameError; nil - end - - should "be new_record" do - assert subject.new_record? - end - - context "when creating," do - should "save the object and return it" do - DummyStoreModel.any_instance.expects(:save).returns({'_id' => 'X'}) - - assert_instance_of DummyStoreModel, DummyStoreModel.create(title: 'Test') - end - - should "execute the callbacks" do - DummyStoreModelWithCallback = Class.new(DummyStoreModel) - @gateway.expects(:save).returns({'_id' => 'X'}) - - DummyStoreModelWithCallback.after_create { $stderr.puts "CREATED" } - DummyStoreModelWithCallback.after_save { $stderr.puts "SAVED" } - - $stderr.expects(:puts).with('CREATED') - $stderr.expects(:puts).with('SAVED') - - DummyStoreModelWithCallback.create name: 'test' - end - end - - context "when saving," do - should "save the model" do - @gateway - .expects(:save) - .with do |object, options| - assert_equal subject, object - assert_equal nil, options[:id] - true - end - .returns({'_id' => 'abc123'}) - - assert subject.save - end - - should "save the model and set the ID" do - @gateway - .expects(:save) - .returns({'_id' => 'abc123'}) - - assert_nil subject.id - - subject.save - assert_equal 'abc123', subject.id - end - - should "save the model and update the timestamp" do - now = Time.parse('2014-01-01T00:00:00Z') - Time.expects(:now).returns(now).at_least_once - @gateway - .expects(:save) - .returns({'_id' => 'abc123'}) - - subject.save - assert_equal Time.parse('2014-01-01T00:00:00Z'), subject.updated_at - end - - should "not save an invalid model" do - @gateway - .expects(:save) - .never - - subject.instance_eval do - def valid?; false; end; - end - - assert ! subject.save - assert ! subject.persisted? - end - - should "skip the validation with the :validate option" do - @gateway - .expects(:save) - .with do |object, options| - assert_equal subject, object - assert_equal nil, options[:id] - true - end - .returns({'_id' => 'abc123'}) - - subject.instance_eval do - def valid?; false; end; - end - - assert subject.save validate: false - assert subject.persisted? - end - - should "pass the options to gateway" do - @gateway - .expects(:save) - .with do |object, options| - assert_equal 'ABC', options[:routing] - true - end - .returns({'_id' => 'abc123'}) - - assert subject.save routing: 'ABC' - end - - should "return the response" do - @gateway - .expects(:save) - .returns('FOOBAR') - - assert_equal 'FOOBAR', subject.save - end - - should "execute the callbacks" do - @gateway.expects(:save).returns({'_id' => 'abc'}) - DummyStoreModelWithCallback = Class.new(DummyStoreModel) - - DummyStoreModelWithCallback.after_save { $stderr.puts "SAVED" } - - $stderr.expects(:puts).with('SAVED') - d = DummyStoreModelWithCallback.new name: 'Test' - d.save - end - - should "save the model to its own index" do - @gateway.expects(:save) - .with do |model, options| - assert_equal 'my_custom_index', options[:index] - assert_equal 'my_custom_type', options[:type] - true - end - .returns({'_id' => 'abc'}) - - d = DummyStoreModel.new name: 'Test' - d.instance_variable_set(:@_index, 'my_custom_index') - d.instance_variable_set(:@_type, 'my_custom_type') - d.save - end - - should "set the meta attributes from response" do - @gateway.expects(:save) - .with do |model, options| - assert_equal 'my_custom_index', options[:index] - assert_equal 'my_custom_type', options[:type] - true - end - .returns({'_id' => 'abc', '_index' => 'foo', '_type' => 'bar', '_version' => '100'}) - - d = DummyStoreModel.new name: 'Test' - d.instance_variable_set(:@_index, 'my_custom_index') - d.instance_variable_set(:@_type, 'my_custom_type') - d.save - - assert_equal 'foo', d._index - assert_equal 'bar', d._type - assert_equal '100', d._version - end - end - - context "when destroying," do - should "remove the model from Elasticsearch" do - subject.expects(:persisted?).returns(true) - subject.expects(:id).returns('abc123') - subject.expects(:freeze).returns(subject) - - @gateway - .expects(:delete) - .with('abc123', {}) - .returns({'_id' => 'abc123', 'version' => 2}) - - assert subject.destroy - assert subject.destroyed? - end - - should "pass the options to gateway" do - subject.expects(:persisted?).returns(true) - subject.expects(:freeze).returns(subject) - - @gateway - .expects(:delete) - .with do |object, options| - assert_equal 'ABC', options[:routing] - true - end - .returns({'_id' => 'abc123'}) - - assert subject.destroy routing: 'ABC' - end - - should "return the response" do - subject.expects(:persisted?).returns(true) - subject.expects(:freeze).returns(subject) - - @gateway - .expects(:delete) - .returns('FOOBAR') - - assert_equal 'FOOBAR', subject.destroy - end - - should "execute the callbacks" do - @gateway.expects(:delete).returns({'_id' => 'abc'}) - DummyStoreModelWithCallback = Class.new(DummyStoreModel) - - DummyStoreModelWithCallback.after_destroy { $stderr.puts "DELETED" } - - $stderr.expects(:puts).with('DELETED') - d = DummyStoreModelWithCallback.new name: 'Test' - d.expects(:persisted?).returns(true) - d.expects(:freeze).returns(d) - - d.destroy - end - - should "remove the model from its own index" do - @gateway.expects(:delete) - .with do |model, options| - assert_equal 'my_custom_index', options[:index] - assert_equal 'my_custom_type', options[:type] - true - end - .returns({'_id' => 'abc'}) - - d = DummyStoreModel.new name: 'Test' - d.instance_variable_set(:@_index, 'my_custom_index') - d.instance_variable_set(:@_type, 'my_custom_type') - d.expects(:persisted?).returns(true) - d.expects(:freeze).returns(d) - - d.destroy - end - end - - context "when updating," do - should "update the document with partial attributes" do - subject.expects(:persisted?).returns(true) - subject.expects(:id).returns('abc123').at_least_once - - @gateway - .expects(:update) - .with do |id, options| - assert_equal 'abc123', id - assert_equal 'UPDATED', options[:doc][:title] - true - end - .returns({'_id' => 'abc123', 'version' => 2}) - - assert subject.update title: 'UPDATED' - - assert_equal 'UPDATED', subject.title - end - - should "allow to update the document with a custom script" do - subject.expects(:persisted?).returns(true) - subject.expects(:id).returns('abc123').at_least_once - - @gateway - .expects(:update) - .with do |id, options| - assert_equal 'abc123', id - assert_equal 'EXEC', options[:script] - true - end - .returns({'_id' => 'abc123', 'version' => 2}) - - assert subject.update( {}, { script: 'EXEC' } ) - end - - should "not update an invalid model" do - @gateway - .expects(:update) - .never - - subject.instance_eval do - def valid?; false; end; - end - - assert ! subject.update(title: 'INVALID') - end - - should "skip the validation with the :validate option" do - subject.expects(:persisted?).returns(true).at_least_once - subject.expects(:id).returns('abc123').at_least_once - - @gateway - .expects(:update) - .with do |object, options| - assert_equal 'abc123', object - assert_equal nil, options[:id] - assert_equal 'INVALID', options[:doc][:title] - true - end - .returns({'_id' => 'abc123'}) - - subject.instance_eval do - def valid?; false; end; - end - - assert subject.update( { title: 'INVALID' }, { validate: false } ) - assert subject.persisted? - end - - should "pass the options to gateway" do - subject.expects(:persisted?).returns(true) - - @gateway - .expects(:update) - .with do |object, options| - assert_equal 'ABC', options[:routing] - true - end - .returns({'_id' => 'abc123'}) - - assert subject.update( { title: 'UPDATED' }, { routing: 'ABC' } ) - end - - should "return the response" do - subject.expects(:persisted?).returns(true) - - @gateway - .expects(:update) - .returns('FOOBAR') - - assert_equal 'FOOBAR', subject.update - end - - should "execute the callbacks" do - @gateway.expects(:update).returns({'_id' => 'abc'}) - DummyStoreModelWithCallback = Class.new(DummyStoreModel) - - DummyStoreModelWithCallback.after_update { $stderr.puts "UPDATED" } - - $stderr.expects(:puts).with('UPDATED') - d = DummyStoreModelWithCallback.new name: 'Test' - d.expects(:persisted?).returns(true) - d.update name: 'Update' - end - - should "update the model in its own index" do - @gateway.expects(:update) - .with do |model, options| - assert_equal 'my_custom_index', options[:index] - assert_equal 'my_custom_type', options[:type] - true - end - .returns({'_id' => 'abc'}) - - d = DummyStoreModel.new name: 'Test' - d.instance_variable_set(:@_index, 'my_custom_index') - d.instance_variable_set(:@_type, 'my_custom_type') - d.expects(:persisted?).returns(true) - - d.update name: 'Update' - end - - should "set the meta attributes from response" do - @gateway.expects(:update) - .with do |model, options| - assert_equal 'my_custom_index', options[:index] - assert_equal 'my_custom_type', options[:type] - true - end - .returns({'_id' => 'abc', '_index' => 'foo', '_type' => 'bar', '_version' => '100'}) - - d = DummyStoreModel.new name: 'Test' - d.instance_variable_set(:@_index, 'my_custom_index') - d.instance_variable_set(:@_type, 'my_custom_type') - d.expects(:persisted?).returns(true) - - d.update name: 'Update' - - assert_equal 'foo', d._index - assert_equal 'bar', d._type - assert_equal '100', d._version - end - end - - context "when incrementing," do - should "increment the attribute" do - subject.expects(:persisted?).returns(true) - - @gateway - .expects(:update) - .with do |id, options| - assert_equal 'ctx._source.count += 1', options[:script] - true - end - .returns({'_id' => 'abc123', 'version' => 2}) - - assert subject.increment :count - - assert_equal 1, subject.count - end - - should "set the meta attributes from response" do - subject.expects(:persisted?).returns(true) - - @gateway.expects(:update) - .with do |model, options| - assert_equal 'my_custom_index', options[:index] - assert_equal 'my_custom_type', options[:type] - true - end - .returns({'_id' => 'abc', '_index' => 'foo', '_type' => 'bar', '_version' => '100'}) - - subject.instance_variable_set(:@_index, 'my_custom_index') - subject.instance_variable_set(:@_type, 'my_custom_type') - - subject.increment :count - - assert_equal 'foo', subject._index - assert_equal 'bar', subject._type - assert_equal '100', subject._version - end - end - - context "when decrement," do - should "decrement the attribute" do - subject.expects(:persisted?).returns(true) - - @gateway - .expects(:update) - .with do |id, options| - assert_equal 'ctx._source.count = ctx._source.count - 1', options[:script] - true - end - .returns({'_id' => 'abc123', 'version' => 2}) - - assert subject.decrement :count - - assert_equal -1, subject.count - end - - should "set the meta attributes from response" do - subject.expects(:persisted?).returns(true) - - @gateway.expects(:update) - .with do |model, options| - assert_equal 'my_custom_index', options[:index] - assert_equal 'my_custom_type', options[:type] - true - end - .returns({'_id' => 'abc', '_index' => 'foo', '_type' => 'bar', '_version' => '100'}) - - subject.instance_variable_set(:@_index, 'my_custom_index') - subject.instance_variable_set(:@_type, 'my_custom_type') - - subject.decrement :count - - assert_equal 'foo', subject._index - assert_equal 'bar', subject._type - assert_equal '100', subject._version - end - end - - context "when touching," do - should "raise exception when touching not existing attribute" do - subject.expects(:persisted?).returns(true) - assert_raise(ArgumentError) { subject.touch :foobar } - end - - should "update updated_at by default" do - subject.expects(:persisted?).returns(true) - now = Time.parse('2014-01-01T00:00:00Z') - Time.expects(:now).returns(now).at_least_once - - @gateway - .expects(:update) - .with do |id, options| - assert_equal '2014-01-01T00:00:00Z', options[:doc][:updated_at] - true - end - .returns({'_id' => 'abc123', 'version' => 2}) - - subject.touch - assert_equal Time.parse('2014-01-01T00:00:00Z'), subject.updated_at - end - - should "update a custom attribute by default" do - subject.expects(:persisted?).returns(true) - now = Time.parse('2014-01-01T00:00:00Z') - Time.expects(:now).returns(now).at_least_once - - @gateway - .expects(:update) - .with do |id, options| - assert_equal '2014-01-01T00:00:00Z', options[:doc][:created_at] - true - end - .returns({'_id' => 'abc123', 'version' => 2}) - - subject.touch :created_at - assert_equal Time.parse('2014-01-01T00:00:00Z'), subject.created_at - end - - should "execute the callbacks" do - @gateway.expects(:update).returns({'_id' => 'abc'}) - DummyStoreModelWithCallback = Class.new(DummyStoreModel) - - DummyStoreModelWithCallback.after_touch { $stderr.puts "TOUCHED" } - - $stderr.expects(:puts).with('TOUCHED') - d = DummyStoreModelWithCallback.new name: 'Test' - d.expects(:persisted?).returns(true) - d.touch - end - - should "set the meta attributes from response" do - subject.expects(:persisted?).returns(true) - - @gateway.expects(:update) - .with do |model, options| - assert_equal 'my_custom_index', options[:index] - assert_equal 'my_custom_type', options[:type] - true - end - .returns({'_id' => 'abc', '_index' => 'foo', '_type' => 'bar', '_version' => '100'}) - - subject.instance_variable_set(:@_index, 'my_custom_index') - subject.instance_variable_set(:@_type, 'my_custom_type') - - subject.touch - - assert_equal 'foo', subject._index - assert_equal 'bar', subject._type - assert_equal '100', subject._version - end - end - - end -end diff --git a/elasticsearch-rails/lib/elasticsearch/rails/instrumentation/railtie.rb b/elasticsearch-rails/lib/elasticsearch/rails/instrumentation/railtie.rb index dbcd0fc38..6aeb6a866 100644 --- a/elasticsearch-rails/lib/elasticsearch/rails/instrumentation/railtie.rb +++ b/elasticsearch-rails/lib/elasticsearch/rails/instrumentation/railtie.rb @@ -16,10 +16,6 @@ class Railtie < ::Rails::Railtie include Elasticsearch::Rails::Instrumentation::Publishers::SearchRequest end if defined?(Elasticsearch::Model::Searching::SearchRequest) - Elasticsearch::Persistence::Model::Find::SearchRequest.class_eval do - include Elasticsearch::Rails::Instrumentation::Publishers::SearchRequest - end if defined?(Elasticsearch::Persistence::Model::Find::SearchRequest) - ActiveSupport.on_load(:action_controller) do include Elasticsearch::Rails::Instrumentation::ControllerRuntime end diff --git a/elasticsearch-rails/lib/elasticsearch/rails/lograge.rb b/elasticsearch-rails/lib/elasticsearch/rails/lograge.rb index a8edd8084..aa0f4f1f3 100644 --- a/elasticsearch-rails/lib/elasticsearch/rails/lograge.rb +++ b/elasticsearch-rails/lib/elasticsearch/rails/lograge.rb @@ -25,10 +25,6 @@ class Railtie < ::Rails::Railtie include Elasticsearch::Rails::Instrumentation::Publishers::SearchRequest end if defined?(Elasticsearch::Model::Searching::SearchRequest) - Elasticsearch::Persistence::Model::Find::SearchRequest.class_eval do - include Elasticsearch::Rails::Instrumentation::Publishers::SearchRequest - end if defined?(Elasticsearch::Persistence::Model::Find::SearchRequest) - ActiveSupport.on_load(:action_controller) do include Elasticsearch::Rails::Instrumentation::ControllerRuntime end From 3e9d73b56215966434a4485ff8b9ebe405bf59c6 Mon Sep 17 00:00:00 2001 From: Emily S Date: Mon, 30 Jul 2018 15:56:18 +0200 Subject: [PATCH 18/87] [STORE] Deprecate _all field in ES 6.x (#820) --- .../persistence/repository/find.rb | 22 ++++++++----------- .../test/unit/repository_find_test.rb | 18 +++++++-------- 2 files changed, 18 insertions(+), 22 deletions(-) diff --git a/elasticsearch-persistence/lib/elasticsearch/persistence/repository/find.rb b/elasticsearch-persistence/lib/elasticsearch/persistence/repository/find.rb index 5e214cfb7..1ea0233fd 100644 --- a/elasticsearch-persistence/lib/elasticsearch/persistence/repository/find.rb +++ b/elasticsearch-persistence/lib/elasticsearch/persistence/repository/find.rb @@ -7,10 +7,6 @@ class DocumentNotFound < StandardError; end # module Find - # The default type of document. - # - ALL = '_all'.freeze - # The key for accessing the document found and returned from an # Elasticsearch _mget query. # @@ -57,16 +53,17 @@ def find(*args) # @return [true, false] # def exists?(id, options={}) - type = document_type || ALL - client.exists( { index: index_name, type: type, id: id }.merge(options) ) + request = { index: index_name, id: id } + request[:type] = document_type if document_type + client.exists(request.merge(options)) end # @api private # def __find_one(id, options={}) - type = document_type || ALL - document = client.get( { index: index_name, type: type, id: id }.merge(options) ) - + request = { index: index_name, id: id } + request[:type] = document_type if document_type + document = client.get(request.merge(options)) deserialize(document) rescue Elasticsearch::Transport::Transport::Errors::NotFound => e raise DocumentNotFound, e.message, caller @@ -75,13 +72,12 @@ def __find_one(id, options={}) # @api private # def __find_many(ids, options={}) - type = document_type || ALL - documents = client.mget( { index: index_name, type: type, body: { ids: ids } }.merge(options) ) - + request = { index: index_name, body: { ids: ids } } + request[:type] = document_type if document_type + documents = client.mget(request.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 index b20006ca1..bcf7f8a1f 100644 --- a/elasticsearch-persistence/test/unit/repository_find_test.rb +++ b/elasticsearch-persistence/test/unit/repository_find_test.rb @@ -49,7 +49,7 @@ class MyDocument; end end should "return whether document for document_type exists" do - subject.expects(:document_type).returns('my_document') + subject.expects(:document_type).twice.returns('my_document') @client .expects(:exists) @@ -63,12 +63,12 @@ class MyDocument; end assert_equal true, subject.exists?('1') end - should "return whether document exists using _all type" do + should "return whether document exists using no document type" do @client .expects(:exists) .with do |arguments| - assert_equal '_all', arguments[:type] + assert_equal nil, arguments[:type] assert_equal '1', arguments[:id] true end @@ -92,7 +92,7 @@ class MyDocument; end context "'__find_one' method" do should "find a document based on document_type and return a deserialized object" do - subject.expects(:document_type).returns('my_document') + subject.expects(:document_type).twice.returns('my_document') subject.expects(:deserialize).with({'_source' => {'foo' => 'bar'}}).returns(MyDocument.new) @@ -108,7 +108,7 @@ class MyDocument; end assert_instance_of MyDocument, subject.__find_one('1') end - should "find a document using _all if document_type is not defined" do + should "find a document using no type if document_type is not defined" do subject.expects(:document_type).returns(nil) subject.expects(:deserialize).with({'_source' => {'foo' => 'bar'}}).returns(MyDocument.new) @@ -116,7 +116,7 @@ class MyDocument; end @client .expects(:get) .with do |arguments| - assert_equal '_all', arguments[:type] + assert_equal nil, arguments[:type] assert_equal '1', arguments[:id] true end @@ -187,7 +187,7 @@ class MyDocument; end 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(:document_type).twice.returns('my_document') subject.expects(:deserialize).twice @@ -219,7 +219,7 @@ class MyDocument; end @client .expects(:mget) .with do |arguments| - assert_equal '_all', arguments[:type] + assert_equal nil, arguments[:type] assert_equal ['1', '2'], arguments[:body][:ids] true end @@ -256,7 +256,7 @@ class MyDocument; end "_source"=>{"id"=>"2", "title"=>"Test 2"}} ]} - subject.expects(:document_type).returns('my_document') + subject.expects(:document_type).twice.returns('my_document') subject .expects(:deserialize) From b65f605b4eb3ed2900d8a5102fe7f674c6368dab Mon Sep 17 00:00:00 2001 From: Emily Stolfo Date: Tue, 31 Jul 2018 10:59:27 +0200 Subject: [PATCH 19/87] Update versions --- elasticsearch-model/lib/elasticsearch/model/version.rb | 2 +- .../lib/elasticsearch/persistence/version.rb | 2 +- elasticsearch-rails/lib/elasticsearch/rails/version.rb | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/elasticsearch-model/lib/elasticsearch/model/version.rb b/elasticsearch-model/lib/elasticsearch/model/version.rb index 2ee49f5ad..59b1306cf 100644 --- a/elasticsearch-model/lib/elasticsearch/model/version.rb +++ b/elasticsearch-model/lib/elasticsearch/model/version.rb @@ -1,5 +1,5 @@ module Elasticsearch module Model - VERSION = "6.0.0.beta" + VERSION = "6.0.0" end end diff --git a/elasticsearch-persistence/lib/elasticsearch/persistence/version.rb b/elasticsearch-persistence/lib/elasticsearch/persistence/version.rb index e80ab0457..862c815a6 100644 --- a/elasticsearch-persistence/lib/elasticsearch/persistence/version.rb +++ b/elasticsearch-persistence/lib/elasticsearch/persistence/version.rb @@ -1,5 +1,5 @@ module Elasticsearch module Persistence - VERSION = "6.0.0.beta" + VERSION = "6.0.0" end end diff --git a/elasticsearch-rails/lib/elasticsearch/rails/version.rb b/elasticsearch-rails/lib/elasticsearch/rails/version.rb index 535aaaa0a..c1acc4629 100644 --- a/elasticsearch-rails/lib/elasticsearch/rails/version.rb +++ b/elasticsearch-rails/lib/elasticsearch/rails/version.rb @@ -1,5 +1,5 @@ module Elasticsearch module Rails - VERSION = "6.0.0.beta" + VERSION = "6.0.0" end end From 32d99a3bfbc569e8d678749cb156be1db85af4dc Mon Sep 17 00:00:00 2001 From: Emily Stolfo Date: Fri, 3 Aug 2018 17:10:41 +0200 Subject: [PATCH 20/87] [STORE] Remove development dependency on virtus, include explicitly in Gemfile for integration test --- elasticsearch-persistence/Gemfile | 3 +++ elasticsearch-persistence/elasticsearch-persistence.gemspec | 1 - 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/elasticsearch-persistence/Gemfile b/elasticsearch-persistence/Gemfile index a60150cf4..04b2d31d4 100644 --- a/elasticsearch-persistence/Gemfile +++ b/elasticsearch-persistence/Gemfile @@ -2,3 +2,6 @@ source 'https://rubygems.org/' # Specify your gem's dependencies in elasticsearch-persistence.gemspec gemspec + + +gem 'virtus' diff --git a/elasticsearch-persistence/elasticsearch-persistence.gemspec b/elasticsearch-persistence/elasticsearch-persistence.gemspec index e90f2afb1..8e6e5fb27 100644 --- a/elasticsearch-persistence/elasticsearch-persistence.gemspec +++ b/elasticsearch-persistence/elasticsearch-persistence.gemspec @@ -28,7 +28,6 @@ Gem::Specification.new do |s| s.add_dependency "activesupport", '> 4' s.add_dependency "activemodel", '> 4' s.add_dependency "hashie" - s.add_dependency "virtus" s.add_development_dependency "bundler", "~> 1.5" s.add_development_dependency "rake", "~> 11.1" From c2ffc1cd109151a612240d3cbfd99c05fe010296 Mon Sep 17 00:00:00 2001 From: Edward Anderson Date: Tue, 7 Aug 2018 10:36:50 -0500 Subject: [PATCH 21/87] [MODEL] Avoid making an update when no attributes are changed (#762) Closes #743 --- .../lib/elasticsearch/model/indexing.rb | 2 +- .../test/unit/indexing_test.rb | 33 +++++++++++++++++++ 2 files changed, 34 insertions(+), 1 deletion(-) diff --git a/elasticsearch-model/lib/elasticsearch/model/indexing.rb b/elasticsearch-model/lib/elasticsearch/model/indexing.rb index 39ad06bc3..0763f1c2b 100644 --- a/elasticsearch-model/lib/elasticsearch/model/indexing.rb +++ b/elasticsearch-model/lib/elasticsearch/model/indexing.rb @@ -397,7 +397,7 @@ def delete_document(options={}) # @see http://rubydoc.info/gems/elasticsearch-api/Elasticsearch/API/Actions:update # def update_document(options={}) - if attributes_in_database = self.instance_variable_get(:@__changed_model_attributes) + if attributes_in_database = self.instance_variable_get(:@__changed_model_attributes).presence attributes = if respond_to?(:as_indexed_json) self.as_indexed_json.select { |k,v| attributes_in_database.keys.map(&:to_s).include? k.to_s } else diff --git a/elasticsearch-model/test/unit/indexing_test.rb b/elasticsearch-model/test/unit/indexing_test.rb index 591977751..f09186a93 100644 --- a/elasticsearch-model/test/unit/indexing_test.rb +++ b/elasticsearch-model/test/unit/indexing_test.rb @@ -176,6 +176,19 @@ def changes_to_save end end + class ::DummyIndexingModelWithNoChanges + extend Elasticsearch::Model::Indexing::ClassMethods + include Elasticsearch::Model::Indexing::InstanceMethods + + def self.before_save(&block) + (@callbacks ||= {})[block.hash] = block + end + + def changes_to_save + {} + end + end + class ::DummyIndexingModelWithCallbacksAndCustomAsIndexedJson extend Elasticsearch::Model::Indexing::ClassMethods include Elasticsearch::Model::Indexing::InstanceMethods @@ -393,6 +406,26 @@ def changes instance.update_document end + should "index instead of update when nothing was changed" do + client = mock('client') + instance = ::DummyIndexingModelWithNoChanges.new + + # Set the fake `changes` hash + instance.instance_variable_set(:@__changed_model_attributes, {}) + # Overload as_indexed_json for running index + instance.expects(:as_indexed_json).returns({ 'foo' => 'BAR' }) + + client.expects(:index) + client.expects(:update).never + + instance.expects(:client).returns(client) + instance.expects(:index_name).returns('foo') + instance.expects(:document_type).returns('bar') + instance.expects(:id).returns('1') + + instance.update_document({}) + end + should "update only the specific attributes" do client = mock('client') instance = ::DummyIndexingModelWithCallbacks.new From 7edcb74e157343498ea0b796b28ee366b5ec9cd3 Mon Sep 17 00:00:00 2001 From: Emily Stolfo Date: Tue, 7 Aug 2018 17:45:36 +0200 Subject: [PATCH 22/87] [RAILS] Remove reference to ActiveRecord persistence pattern in README --- README.md | 1 - 1 file changed, 1 deletion(-) diff --git a/README.md b/README.md index ef1a8476e..788428638 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,6 @@ This repository contains various Ruby and Rails integrations for [Elasticsearch] * ActiveModel integration with adapters for ActiveRecord and Mongoid * _Repository pattern_ based persistence layer for Ruby objects -* _Active Record pattern_ based persistence layer for Ruby models * Enumerable-based wrapper for search results * ActiveRecord::Relation-based wrapper for returning search results as records * Convenience model methods such as `search`, `mapping`, `import`, etc From de321eede3756f43e5f63e3f7f8352fab7c7f231 Mon Sep 17 00:00:00 2001 From: Joe Francis Date: Wed, 8 Aug 2018 09:15:42 -0500 Subject: [PATCH 23/87] Move badges to top of README (#747) --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 788428638..283f18e02 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,7 @@ # Elasticsearch +[![Build Status](https://travis-ci.org/elastic/elasticsearch-rails.svg?branch=master)](https://travis-ci.org/elastic/elasticsearch-rails) [![Code Climate](https://codeclimate.com/github/elastic/elasticsearch-rails/badges/gpa.svg)](https://codeclimate.com/github/elastic/elasticsearch-rails) + This repository contains various Ruby and Rails integrations for [Elasticsearch](http://elasticsearch.org): * ActiveModel integration with adapters for ActiveRecord and Mongoid @@ -119,8 +121,6 @@ repository.save Article.new(title: 'Test') ## Development -[![Build Status](https://travis-ci.org/elastic/elasticsearch-rails.svg?branch=master)](https://travis-ci.org/elastic/elasticsearch-rails) [![Code Climate](https://codeclimate.com/github/elastic/elasticsearch-rails/badges/gpa.svg)](https://codeclimate.com/github/elastic/elasticsearch-rails) - To work on the code, clone the repository and install all dependencies first: ``` From 690a24b5702cff3c490540b49413ee7dc474762e Mon Sep 17 00:00:00 2001 From: Emily Stolfo Date: Wed, 8 Aug 2018 16:39:23 +0200 Subject: [PATCH 24/87] Use local as source for gem dependencies when possible --- elasticsearch-persistence/Gemfile | 1 + elasticsearch-persistence/elasticsearch-persistence.gemspec | 4 ++-- elasticsearch-rails/Gemfile | 5 +++++ elasticsearch-rails/elasticsearch-rails.gemspec | 1 - 4 files changed, 8 insertions(+), 3 deletions(-) diff --git a/elasticsearch-persistence/Gemfile b/elasticsearch-persistence/Gemfile index 04b2d31d4..c88afe19f 100644 --- a/elasticsearch-persistence/Gemfile +++ b/elasticsearch-persistence/Gemfile @@ -3,5 +3,6 @@ source 'https://rubygems.org/' # Specify your gem's dependencies in elasticsearch-persistence.gemspec gemspec +gem 'elasticsearch-model', :path => File.expand_path("../../elasticsearch-model", __FILE__), :require => false gem 'virtus' diff --git a/elasticsearch-persistence/elasticsearch-persistence.gemspec b/elasticsearch-persistence/elasticsearch-persistence.gemspec index 8e6e5fb27..775c3b9e5 100644 --- a/elasticsearch-persistence/elasticsearch-persistence.gemspec +++ b/elasticsearch-persistence/elasticsearch-persistence.gemspec @@ -23,8 +23,8 @@ Gem::Specification.new do |s| s.required_ruby_version = ">= 1.9.3" - s.add_dependency "elasticsearch", '~> 5' - s.add_dependency "elasticsearch-model", '~> 5' + s.add_dependency "elasticsearch", '~> 6' + s.add_dependency "elasticsearch-model", '~> 6' s.add_dependency "activesupport", '> 4' s.add_dependency "activemodel", '> 4' s.add_dependency "hashie" diff --git a/elasticsearch-rails/Gemfile b/elasticsearch-rails/Gemfile index 1aeec6c9a..81fc68ac4 100644 --- a/elasticsearch-rails/Gemfile +++ b/elasticsearch-rails/Gemfile @@ -7,3 +7,8 @@ gemspec # if File.exists? File.expand_path("../../elasticsearch-model", __FILE__) # gem 'elasticsearch-model', :path => File.expand_path("../../elasticsearch-model", __FILE__), :require => true # end + + + +gem 'elasticsearch-model', :path => File.expand_path("../../elasticsearch-model", __FILE__), :require => false +gem 'elasticsearch-persistence', :path => File.expand_path("../../elasticsearch-persistence", __FILE__), :require => false diff --git a/elasticsearch-rails/elasticsearch-rails.gemspec b/elasticsearch-rails/elasticsearch-rails.gemspec index 1fad14c36..e4ad351ba 100644 --- a/elasticsearch-rails/elasticsearch-rails.gemspec +++ b/elasticsearch-rails/elasticsearch-rails.gemspec @@ -27,7 +27,6 @@ Gem::Specification.new do |s| s.add_development_dependency "rake", "~> 11.1" s.add_development_dependency "elasticsearch-extensions" - s.add_development_dependency "elasticsearch-model" s.add_development_dependency "rails", ">= 3.1" From 82ff295bca5e4ae1dab34f4d45c9cc14a2dfb491 Mon Sep 17 00:00:00 2001 From: Emily Stolfo Date: Thu, 9 Aug 2018 12:23:59 +0200 Subject: [PATCH 25/87] Only require 'oj' gem if not using JRuby --- elasticsearch-model/test/test_helper.rb | 2 +- elasticsearch-persistence/test/test_helper.rb | 2 +- elasticsearch-rails/test/test_helper.rb | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/elasticsearch-model/test/test_helper.rb b/elasticsearch-model/test/test_helper.rb index 7471e475d..5a727e2b2 100644 --- a/elasticsearch-model/test/test_helper.rb +++ b/elasticsearch-model/test/test_helper.rb @@ -23,7 +23,7 @@ require 'turn' unless ENV["TM_FILEPATH"] || ENV["NOTURN"] || defined?(RUBY_VERSION) && RUBY_VERSION > '2.2' require 'ansi' -require 'oj' +require 'oj' unless defined?(JRUBY_VERSION) require 'active_model' diff --git a/elasticsearch-persistence/test/test_helper.rb b/elasticsearch-persistence/test/test_helper.rb index fd317fce8..c881ac02e 100644 --- a/elasticsearch-persistence/test/test_helper.rb +++ b/elasticsearch-persistence/test/test_helper.rb @@ -23,7 +23,7 @@ require 'turn' unless ENV["TM_FILEPATH"] || ENV["NOTURN"] || defined?(RUBY_VERSION) && RUBY_VERSION > '2.2' require 'ansi' -require 'oj' +require 'oj' unless defined?(JRUBY_VERSION) require 'elasticsearch/extensions/test/cluster' require 'elasticsearch/extensions/test/startup_shutdown' diff --git a/elasticsearch-rails/test/test_helper.rb b/elasticsearch-rails/test/test_helper.rb index bf9c55bd1..e06554fc0 100644 --- a/elasticsearch-rails/test/test_helper.rb +++ b/elasticsearch-rails/test/test_helper.rb @@ -23,7 +23,7 @@ require 'turn' unless ENV["TM_FILEPATH"] || ENV["NOTURN"] || defined?(RUBY_VERSION) && RUBY_VERSION > '2.2' require 'ansi' -require 'oj' +require 'oj' unless defined?(JRUBY_VERSION) require 'rails/version' require 'active_record' From 437234d9149c547316de2f045f9a2344ac4a8043 Mon Sep 17 00:00:00 2001 From: Emily S Date: Fri, 10 Aug 2018 11:25:38 +0200 Subject: [PATCH 26/87] [CI] Run unit tests using JRuby on Travis (#819) --- .travis.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.travis.yml b/.travis.yml index 51766c1e1..426ccf85a 100644 --- a/.travis.yml +++ b/.travis.yml @@ -37,9 +37,9 @@ matrix: jdk: oraclejdk8 env: TEST_SUITE=unit -# - rvm: jruby-9.1 -# jdk: oraclejdk8 -# env: TEST_SUITE=unit + - rvm: jruby-9.1 + jdk: oraclejdk8 + env: TEST_SUITE=unit - rvm: 2.5 jdk: oraclejdk8 From cb51159ef0638320f8c16d9cda78cc892bbd9926 Mon Sep 17 00:00:00 2001 From: Emily S Date: Mon, 13 Aug 2018 13:43:24 +0200 Subject: [PATCH 27/87] [STORE] Refactor Repository as mixin (#824) * [STORE] Refactor Repository * [STORE] Add more tests and update documentation * [STORE] Remove #with method * [STORE] Raise NotImplementedError if administrative index methods are called on a Repository class * [STORE] Put class-level index admin methods into a DSL module, to be included separately * [STORE] Fix documentation and minor typos in tests * [STORE] Add one more test for ArgumentError on #search * [STORE] Remove test unit files and define only test 'integration' and 'all' rake tasks --- elasticsearch-persistence/.rspec | 2 + elasticsearch-persistence/Gemfile | 5 + elasticsearch-persistence/Rakefile | 16 +- .../lib/elasticsearch/persistence.rb | 112 +-- .../lib/elasticsearch/persistence/client.rb | 51 -- .../elasticsearch/persistence/repository.rb | 255 +++++-- .../persistence/repository/class.rb | 71 -- .../persistence/repository/dsl.rb | 94 +++ .../persistence/repository/find.rb | 29 +- .../persistence/repository/naming.rb | 99 --- .../persistence/repository/search.rb | 17 +- .../persistence/repository/serialize.rb | 73 +- .../persistence/repository/store.rb | 81 +- .../lib/elasticsearch/persistence/version.rb | 2 +- .../spec/repository/find_spec.rb | 179 +++++ .../spec/repository/search_spec.rb | 181 +++++ .../spec/repository/serialize_spec.rb | 53 ++ .../spec/repository/store_spec.rb | 327 ++++++++ .../spec/repository_spec.rb | 716 ++++++++++++++++++ elasticsearch-persistence/spec/spec_helper.rb | 28 + .../repository/custom_class_test.rb | 88 --- .../repository/customized_class_test.rb | 82 -- .../repository/default_class_test.rb | 119 --- .../repository/virtus_model_test.rb | 119 --- elasticsearch-persistence/test/test_helper.rb | 55 -- .../test/unit/persistence_test.rb | 32 - .../test/unit/repository_class_test.rb | 51 -- .../test/unit/repository_client_test.rb | 32 - .../test/unit/repository_find_test.rb | 307 -------- .../test/unit/repository_indexing_test.rb | 37 - .../test/unit/repository_module_test.rb | 146 ---- .../test/unit/repository_naming_test.rb | 117 --- .../unit/repository_response_results_test.rb | 98 --- .../test/unit/repository_search_test.rb | 100 --- .../test/unit/repository_serialize_test.rb | 41 - .../test/unit/repository_store_test.rb | 219 ------ 36 files changed, 1922 insertions(+), 2112 deletions(-) create mode 100644 elasticsearch-persistence/.rspec delete mode 100644 elasticsearch-persistence/lib/elasticsearch/persistence/client.rb delete mode 100644 elasticsearch-persistence/lib/elasticsearch/persistence/repository/class.rb create mode 100644 elasticsearch-persistence/lib/elasticsearch/persistence/repository/dsl.rb delete mode 100644 elasticsearch-persistence/lib/elasticsearch/persistence/repository/naming.rb create mode 100644 elasticsearch-persistence/spec/repository/find_spec.rb create mode 100644 elasticsearch-persistence/spec/repository/search_spec.rb create mode 100644 elasticsearch-persistence/spec/repository/serialize_spec.rb create mode 100644 elasticsearch-persistence/spec/repository/store_spec.rb create mode 100644 elasticsearch-persistence/spec/repository_spec.rb create mode 100644 elasticsearch-persistence/spec/spec_helper.rb delete mode 100644 elasticsearch-persistence/test/integration/repository/custom_class_test.rb delete mode 100644 elasticsearch-persistence/test/integration/repository/customized_class_test.rb delete mode 100644 elasticsearch-persistence/test/integration/repository/default_class_test.rb delete mode 100644 elasticsearch-persistence/test/integration/repository/virtus_model_test.rb delete mode 100644 elasticsearch-persistence/test/test_helper.rb delete mode 100644 elasticsearch-persistence/test/unit/persistence_test.rb delete mode 100644 elasticsearch-persistence/test/unit/repository_class_test.rb delete mode 100644 elasticsearch-persistence/test/unit/repository_client_test.rb delete mode 100644 elasticsearch-persistence/test/unit/repository_find_test.rb delete mode 100644 elasticsearch-persistence/test/unit/repository_indexing_test.rb delete mode 100644 elasticsearch-persistence/test/unit/repository_module_test.rb delete mode 100644 elasticsearch-persistence/test/unit/repository_naming_test.rb delete mode 100644 elasticsearch-persistence/test/unit/repository_response_results_test.rb delete mode 100644 elasticsearch-persistence/test/unit/repository_search_test.rb delete mode 100644 elasticsearch-persistence/test/unit/repository_serialize_test.rb delete mode 100644 elasticsearch-persistence/test/unit/repository_store_test.rb diff --git a/elasticsearch-persistence/.rspec b/elasticsearch-persistence/.rspec new file mode 100644 index 000000000..77d185827 --- /dev/null +++ b/elasticsearch-persistence/.rspec @@ -0,0 +1,2 @@ +--tty +--colour diff --git a/elasticsearch-persistence/Gemfile b/elasticsearch-persistence/Gemfile index c88afe19f..de011df05 100644 --- a/elasticsearch-persistence/Gemfile +++ b/elasticsearch-persistence/Gemfile @@ -6,3 +6,8 @@ gemspec gem 'elasticsearch-model', :path => File.expand_path("../../elasticsearch-model", __FILE__), :require => false gem 'virtus' + +group :development, :testing do + gem 'rspec' + gem 'pry-nav' +end diff --git a/elasticsearch-persistence/Rakefile b/elasticsearch-persistence/Rakefile index 61038d08c..660e57211 100644 --- a/elasticsearch-persistence/Rakefile +++ b/elasticsearch-persistence/Rakefile @@ -7,24 +7,24 @@ task :test => 'test:unit' # ----- Test tasks ------------------------------------------------------------ require 'rake/testtask' +require 'rspec/core/rake_task' + namespace :test do + + RSpec::Core::RakeTask.new(:spec) Rake::TestTask.new(:unit) do |test| - test.libs << 'lib' << 'test' - test.test_files = FileList["test/unit/**/*_test.rb"] - test.verbose = false - test.warning = false end Rake::TestTask.new(:integration) do |test| - test.libs << 'lib' << 'test' - test.test_files = FileList["test/integration/**/*_test.rb"] test.verbose = false test.warning = false + test.deps = [ :spec ] end Rake::TestTask.new(:all) do |test| - test.libs << 'lib' << 'test' - test.test_files = FileList["test/unit/**/*_test.rb", "test/integration/**/*_test.rb"] + test.verbose = false + test.warning = false + test.deps = [ :spec ] end end diff --git a/elasticsearch-persistence/lib/elasticsearch/persistence.rb b/elasticsearch-persistence/lib/elasticsearch/persistence.rb index 986ef1bc4..ce0d25b60 100644 --- a/elasticsearch-persistence/lib/elasticsearch/persistence.rb +++ b/elasticsearch-persistence/lib/elasticsearch/persistence.rb @@ -1,116 +1,8 @@ require 'hashie/mash' require 'elasticsearch' - -require 'elasticsearch/model/hash_wrapper' -require 'elasticsearch/model/indexing' -require 'elasticsearch/model/searching' - -require 'active_support/inflector' +require 'elasticsearch/model' 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/class' 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", ...} - # - # == Model - # - # The active record pattern allows to use the interface familiar from ActiveRecord models: - # - # require 'elasticsearch/persistence' - # - # class Article - # attribute :title, String, mapping: { analyzer: 'snowball' } - # end - # - # article = Article.new id: 1, title: 'Test' - # article.save - # - # Article.find(1) - # - # article.update_attributes title: 'Update' - # - # article.destroy - # - 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 - end - - extend ClassMethods - end -end +require 'elasticsearch/persistence/repository/response/results' diff --git a/elasticsearch-persistence/lib/elasticsearch/persistence/client.rb b/elasticsearch-persistence/lib/elasticsearch/persistence/client.rb deleted file mode 100644 index 3c9d618d4..000000000 --- a/elasticsearch-persistence/lib/elasticsearch/persistence/client.rb +++ /dev/null @@ -1,51 +0,0 @@ -module Elasticsearch - module Persistence - module Repository - - # Wraps the Elasticsearch Ruby - # [client](https://github.com/elasticsearch/elasticsearch-ruby/tree/master/elasticsearch#usage) - # - 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 - end - end - - end - end -end diff --git a/elasticsearch-persistence/lib/elasticsearch/persistence/repository.rb b/elasticsearch-persistence/lib/elasticsearch/persistence/repository.rb index f6a202029..33cbdc2c4 100644 --- a/elasticsearch-persistence/lib/elasticsearch/persistence/repository.rb +++ b/elasticsearch-persistence/lib/elasticsearch/persistence/repository.rb @@ -1,77 +1,224 @@ +require 'elasticsearch/persistence/repository/dsl' +require 'elasticsearch/persistence/repository/find' +require 'elasticsearch/persistence/repository/store' +require 'elasticsearch/persistence/repository/serialize' +require 'elasticsearch/persistence/repository/search' + module Elasticsearch module Persistence - # Delegate methods to the repository (acting as a gateway) + # The base Repository mixin. This module should be included in classes that + # represent an Elasticsearch repository. # - module GatewayDelegation - def method_missing(method_name, *arguments, &block) - gateway.respond_to?(method_name) ? gateway.__send__(method_name, *arguments, &block) : super - end + # @since 6.0.0 + module Repository + include Store + include Serialize + include Find + include Search + include Elasticsearch::Model::Indexing::ClassMethods - def respond_to?(method_name, include_private=false) - gateway.respond_to?(method_name) || super + def self.included(base) + base.send(:extend, ClassMethods) end - def respond_to_missing?(method_name, *) - gateway.respond_to?(method_name) || super - end - end + module ClassMethods - # When included, creates an instance of the {Repository::Class Repository} class as a "gateway" - # - # @example Include the repository in a custom class - # - # require 'elasticsearch/persistence' - # - # class MyRepository - # include Elasticsearch::Persistence::Repository - # end - # - module Repository - def self.included(base) - gateway = Elasticsearch::Persistence::Repository::Class.new host: base - - # Define the instance level gateway + # Initialize a repository instance. Optionally provide a block to define index mappings or + # settings on the repository instance. + # + # @example Create a new repository. + # MyRepository.create(index_name: 'notes', klass: Note) + # + # @example Create a new repository and evaluate a block on it. + # MyRepository.create(index_name: 'notes', klass: Note) do + # mapping dynamic: 'strict' do + # indexes :title + # end + # end # - base.class_eval do - define_method :gateway do - @gateway ||= gateway + # @param [ Hash ] options The options to use. + # @param [ Proc ] block A block to evaluate on the new repository instance. + # + # @option options [ Symbol, String ] :index_name The name of the index. + # @option options [ Symbol, String ] :document_type The type of documents persisted in this repository. + # @option options [ Symbol, String ] :client The client used to handle requests to and from Elasticsearch. + # @option options [ Symbol, String ] :klass The class used to instantiate an object when documents are + # deserialized. The default is nil, in which case the raw document will be returned as a Hash. + # @option options [ Elasticsearch::Model::Indexing::Mappings, Hash ] :mapping The mapping for this index. + # @option options [ Elasticsearch::Model::Indexing::Settings, Hash ] :settings The settings for this index. + # + # @since 6.0.0 + def create(options = {}, &block) + new(options).tap do |obj| + obj.instance_eval(&block) if block_given? end - - include GatewayDelegation end + end - # Define the class level gateway - # - (class << base; self; end).class_eval do - define_method :gateway do |&block| - @gateway ||= gateway - @gateway.instance_eval(&block) if block - @gateway - end + # The default index name. + # + # @return [ String ] The default index name. + # + # @since 6.0.0 + DEFAULT_INDEX_NAME = 'repository'.freeze - include GatewayDelegation - end + # The default document type. + # + # @return [ String ] The default document type. + # + # @note the document type will no longer be configurable in future versions + # of Elasticsearch. + # + # @since 6.0.0 + DEFAULT_DOC_TYPE = '_doc'.freeze - # Catch repository methods (such as `serialize` and others) defined in the receiving class, - # and overload the default definition in the gateway - # - 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) + # The repository options. + # + # @return [ Hash ] + # + # @since 6.0.0 + attr_reader :options + + # Initialize a repository instance. + # + # @example Initialize the repository. + # MyRepository.new(index_name: 'notes', klass: Note) + # + # @param [ Hash ] options The options to use. + # + # @option options [ Symbol, String ] :index_name The name of the index. + # @option options [ Symbol, String ] :document_type The type of documents persisted in this repository. + # @option options [ Symbol, String ] :client The client used to handle requests to and from Elasticsearch. + # @option options [ Symbol, String ] :klass The class used to instantiate an object when documents are + # deserialized. The default is nil, in which case the raw document will be returned as a Hash. + # @option options [ Elasticsearch::Model::Indexing::Mappings, Hash ] :mapping The mapping for this index. + # @option options [ Elasticsearch::Model::Indexing::Settings, Hash ] :settings The settings for this index. + # + # @since 6.0.0 + def initialize(options = {}) + @options = options + end + + # Get the client used by the repository. + # + # @example + # repository.client + # + # @return [ Elasticsearch::Transport::Client ] The repository's client. + # + # @since 6.0.0 + def client + @client ||= @options[:client] || + __get_class_value(:client) || + Elasticsearch::Transport::Client.new + end + + # Get the document type used by the repository object. + # + # @example + # repository.document_type + # + # @return [ String, Symbol ] The repository's document type. + # + # @since 6.0.0 + def document_type + @document_type ||= @options[:document_type] || + __get_class_value(:document_type) || + DEFAULT_DOC_TYPE + end + + # Get the index name used by the repository. + # + # @example + # repository.index_name + # + # @return [ String, Symbol ] The repository's index name. + # + # @since 6.0.0 + def index_name + @index_name ||= @options[:index_name] || + __get_class_value(:index_name) || + DEFAULT_INDEX_NAME + end + + # Get the class used by the repository when deserializing. + # + # @example + # repository.klass + # + # @return [ Class ] The repository's klass for deserializing. + # + # @since 6.0.0 + def klass + @klass ||= @options[:klass] || __get_class_value(:klass) + end + + # Get the index mapping. Optionally pass a block to define the mappings. + # + # @example + # repository.mapping + # + # @example Set the mappings with a block. + # repository.mapping dynamic: 'strict' do + # indexes :foo + # end + # end + # + # @note If mappings were set when the repository was created, a block passed to this + # method will not be evaluated. + # + # @return [ Elasticsearch::Model::Indexing::Mappings ] The index mappings. + # + # @since 6.0.0 + def mapping(*args) + @memoized_mapping ||= @options[:mapping] || (begin + if _mapping = __get_class_value(:mapping) + _mapping.instance_variable_set(:@type, document_type) + _mapping end - end + end) || (super && @mapping) + end + alias :mappings :mapping + + # Get the index settings. + # + # @example + # repository.settings + # + # @example Set the settings with a block. + # repository.settings number_of_shards: 1, number_of_replicas: 0 do + # mapping dynamic: 'strict' do + # indexes :foo do + # indexes :bar + # end + # end + # end + # + # @return [ Elasticsearch::Model::Indexing::Settings ] The index settings. + # + # @since 6.0.0 + def settings(*args) + @memoized_settings ||= @options[:settings] || __get_class_value(:settings) || (super && @settings) end - # Shortcut method to allow concise repository initialization + # Determine whether the index with this repository's index name exists. # - # @example Create a new default repository + # @example + # repository.index_exists? # - # repository = Elasticsearch::Persistence::Repository.new + # @return [ true, false ] Whether the index exists. # - def new(options={}, &block) - Elasticsearch::Persistence::Repository::Class.new( {index: 'repository'}.merge(options), &block ) - end; module_function :new + # @since 6.0.0 + def index_exists?(*args) + super(index_name: index_name) + end + + private + + def __get_class_value(_method_) + self.class.send(_method_) if self.class.respond_to?(_method_) + end end end end diff --git a/elasticsearch-persistence/lib/elasticsearch/persistence/repository/class.rb b/elasticsearch-persistence/lib/elasticsearch/persistence/repository/class.rb deleted file mode 100644 index 0dcd2d576..000000000 --- a/elasticsearch-persistence/lib/elasticsearch/persistence/repository/class.rb +++ /dev/null @@ -1,71 +0,0 @@ -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 - 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 - - def initialize(options={}, &block) - @options = options - index_name options.delete(:index) - 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] - # - # @api private - # - def host - options[:host] - end - end - - end - end -end diff --git a/elasticsearch-persistence/lib/elasticsearch/persistence/repository/dsl.rb b/elasticsearch-persistence/lib/elasticsearch/persistence/repository/dsl.rb new file mode 100644 index 000000000..0c880d7ec --- /dev/null +++ b/elasticsearch-persistence/lib/elasticsearch/persistence/repository/dsl.rb @@ -0,0 +1,94 @@ +module Elasticsearch + module Persistence + module Repository + + # Include this module to get class-level methods for repository configuration. + # + # @since 6.0.0 + module DSL + + def self.included(base) + base.send(:extend, Elasticsearch::Model::Indexing::ClassMethods) + base.send(:extend, ClassMethods) + end + + # These methods are necessary to define at the class-level so that the methods available + # via Elasticsearch::Model::Indexing::ClassMethods have the references they depend on. + # + # @since 6.0.0 + module ClassMethods + + # Get or set the class-level document type setting. + # + # @example + # MyRepository.document_type + # + # @return [ String, Symbol ] _type The repository's document type. + # + # @since 6.0.0 + def document_type(_type = nil) + @document_type ||= (_type || DEFAULT_DOC_TYPE) + end + + # Get or set the class-level index name setting. + # + # @example + # MyRepository.index_name + # + # @return [ String, Symbol ] _name The repository's index name. + # + # @since 6.0.0 + def index_name(_name = nil) + @index_name ||= (_name || DEFAULT_INDEX_NAME) + end + + # Get or set the class-level setting for the class used by the repository when deserializing. + # + # @example + # MyRepository.klass + # + # @return [ Class ] _class The repository's klass for deserializing. + # + # @since 6.0.0 + def klass(_class = nil) + instance_variables.include?(:@klass) ? @klass : @klass = _class + end + + # Get or set the class-level setting for the client used by the repository. + # + # @example + # MyRepository.client + # + # @return [ Class ] _client The repository's client. + # + # @since 6.0.0 + def client(_client = nil) + @client ||= (_client || Elasticsearch::Transport::Client.new) + end + + def create_index!(*args) + __raise_not_implemented_error(__method__) + end + + def delete_index!(*args) + __raise_not_implemented_error(__method__) + end + + def refresh_index!(*args) + __raise_not_implemented_error(__method__) + end + + def index_exists?(*args) + __raise_not_implemented_error(__method__) + end + + private + + def __raise_not_implemented_error(_method_) + raise NotImplementedError, "The '#{_method_}' method is not implemented on the Repository class." + end + end + end + end + end +end diff --git a/elasticsearch-persistence/lib/elasticsearch/persistence/repository/find.rb b/elasticsearch-persistence/lib/elasticsearch/persistence/repository/find.rb index 1ea0233fd..cbd264ae2 100644 --- a/elasticsearch-persistence/lib/elasticsearch/persistence/repository/find.rb +++ b/elasticsearch-persistence/lib/elasticsearch/persistence/repository/find.rb @@ -7,16 +7,6 @@ class DocumentNotFound < StandardError; end # module Find - # The key for accessing the document found and returned from an - # Elasticsearch _mget query. - # - DOCS = 'docs'.freeze - - # The key for the boolean value indicating whether a particular id - # has been successfully found in an Elasticsearch _mget query. - # - FOUND = 'found'.freeze - # Retrieve a single object or multiple objects from Elasticsearch by ID or IDs # # @example Retrieve a single object by ID @@ -50,6 +40,9 @@ def find(*args) # repository.exists?(1) # => true # + # @param [ String, Integer ] id The id to search. + # @param [ Hash ] options The options. + # # @return [true, false] # def exists?(id, options={}) @@ -58,6 +51,18 @@ def exists?(id, options={}) client.exists(request.merge(options)) end + private + + # The key for accessing the document found and returned from an + # Elasticsearch _mget query. + # + DOCS = 'docs'.freeze + + # The key for the boolean value indicating whether a particular id + # has been successfully found in an Elasticsearch _mget query. + # + FOUND = 'found'.freeze + # @api private # def __find_one(id, options={}) @@ -75,7 +80,9 @@ def __find_many(ids, options={}) request = { index: index_name, body: { ids: ids } } request[:type] = document_type if document_type documents = client.mget(request.merge(options)) - documents[DOCS].map { |document| document[FOUND] ? deserialize(document) : nil } + documents[DOCS].map do |document| + deserialize(document) if document[FOUND] + end end end end diff --git a/elasticsearch-persistence/lib/elasticsearch/persistence/repository/naming.rb b/elasticsearch-persistence/lib/elasticsearch/persistence/repository/naming.rb deleted file mode 100644 index d559d8d51..000000000 --- a/elasticsearch-persistence/lib/elasticsearch/persistence/repository/naming.rb +++ /dev/null @@ -1,99 +0,0 @@ -module Elasticsearch - module Persistence - module Repository - - # Wraps all naming-related features of the repository (index name, the domain object class, etc) - # - module Naming - - # The possible keys for a document id. - # - IDS = [:id, 'id', :_id, '_id'].freeze - - DEFAULT_DOC_TYPE = '_doc'.freeze - - # Get or set the class used to initialize domain objects when deserializing them - # - def klass(name=nil) - if name - @klass = name - else - @klass - end - 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) - self.host.to_s.underscore.gsub(/\//, '-') - else - self.class.to_s.underscore.gsub(/\//, '-') - end - 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 || DEFAULT_DOC_TYPE - 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 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[IDS.find { |id| document[id] }] - end - - # Extract a document ID from the document (assuming Hash or Hash-like object) - # - # @note Calling this method will *remove* the `id` or `_id` key from the passed object. - # - # @example - # options = { title: 'Test', id: 'abc123' } - # repository.__extract_id_from_document options - # # => "abc123" - # options - # # => { title: 'Test' } - # - # @api private - # - def __extract_id_from_document(document) - IDS.inject(nil) do |deleted, id| - if document[id] - document.delete(id) - else - deleted - 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 index 07c0e4d3d..203a7e246 100644 --- a/elasticsearch-persistence/lib/elasticsearch/persistence/repository/search.rb +++ b/elasticsearch-persistence/lib/elasticsearch/persistence/repository/search.rb @@ -6,10 +6,6 @@ module Repository # module Search - # The key for accessing the count in a Elasticsearch query response. - # - COUNT = 'count'.freeze - # Returns a collection of domain objects by an Elasticsearch query # # Pass the query either as a string or a Hash-like object @@ -41,6 +37,9 @@ module Search # # GET http://localhost:9200/notes/note/_search # # > {"query":{"match":{"title":"fox dog"}},"size":25} # + # @param [ Hash, String ] query_or_definition The query or search definition. + # @param [ Hash ] options The search options. + # # @return [Elasticsearch::Persistence::Repository::Response::Results] # def search(query_or_definition, options={}) @@ -75,6 +74,9 @@ def search(query_or_definition, options={}) # repository.search(query: { match: { title: 'fox dog' } }) # # => 1 # + # @param [ Hash, String ] query_or_definition The query or search definition. + # @param [ Hash ] options The search options. + # # @return [Integer] # def count(query_or_definition=nil, options={}) @@ -92,8 +94,13 @@ def count(query_or_definition=nil, options={}) response[COUNT] end - end + private + + # The key for accessing the count in a Elasticsearch query response. + # + COUNT = 'count'.freeze + end end end end diff --git a/elasticsearch-persistence/lib/elasticsearch/persistence/repository/serialize.rb b/elasticsearch-persistence/lib/elasticsearch/persistence/repository/serialize.rb index e9a8d875e..067a7daad 100644 --- a/elasticsearch-persistence/lib/elasticsearch/persistence/repository/serialize.rb +++ b/elasticsearch-persistence/lib/elasticsearch/persistence/repository/serialize.rb @@ -2,19 +2,40 @@ module Elasticsearch module Persistence module Repository - # Provide serialization and deserialization between Ruby objects and Elasticsearch documents + # Provide serialization and deserialization between Ruby objects and Elasticsearch documents. # # Override these methods in your repository class to customize the logic. # module Serialize - # Error message raised when documents are attempted to be deserialized and no klass is defined for - # the Repository. + # Serialize the object for storing it in Elasticsearch. # - # @since 6.0.0 - NO_CLASS_ERROR_MESSAGE = "No class is defined for deserializing documents. " + - "Please define a 'klass' for the Repository or define a custom " + - "deserialize method.".freeze + # In the default implementation, call the `to_hash` method on the passed object. + # + # @param [ Object ] document The Ruby object to serialize. + # + # @return [ Hash ] The serialized document. + # + def serialize(document) + document.to_hash + end + + # Deserialize the document retrieved from Elasticsearch into a Ruby object. + # If no klass is set for the Repository then the raw document '_source' field will be returned. + # + # def deserialize(document) + # Note.new document[SOURCE] + # end + # + # @param [ Hash ] document The raw document. + # + # @return [ Object ] The deserialized object. + # + def deserialize(document) + klass ? klass.new(document[SOURCE]) : document[SOURCE] + end + + private # The key for document fields in an Elasticsearch query response. # @@ -26,21 +47,41 @@ module Serialize # TYPE = '_type'.freeze - # Serialize the object for storing it in Elasticsearch + IDS = [:id, 'id', :_id, '_id'].freeze + + # Get a document ID from the document (assuming Hash or Hash-like object) # - # In the default implementation, call the `to_hash` method on the passed object. + # @example + # repository.__get_id_from_document title: 'Test', id: 'abc123' + # => "abc123" # - def serialize(document) - document.to_hash + # @api private + # + def __get_id_from_document(document) + document[IDS.find { |id| document[id] }] end - # Deserialize the document retrieved from Elasticsearch into a Ruby object + # Extract a document ID from the document (assuming Hash or Hash-like object) # - # Use the `klass` property, if defined, otherwise try to get the class from the document's `_type`. + # @note Calling this method will *remove* the `id` or `_id` key from the passed object. # - def deserialize(document) - raise NameError.new(NO_CLASS_ERROR_MESSAGE) unless klass - klass.new document[SOURCE] + # @example + # options = { title: 'Test', id: 'abc123' } + # repository.__extract_id_from_document options + # # => "abc123" + # options + # # => { title: 'Test' } + # + # @api private + # + def __extract_id_from_document(document) + IDS.inject(nil) do |deleted, id| + if document[id] + document.delete(id) + else + deleted + end + end end end end diff --git a/elasticsearch-persistence/lib/elasticsearch/persistence/repository/store.rb b/elasticsearch-persistence/lib/elasticsearch/persistence/repository/store.rb index 2327d23a6..6571e1078 100644 --- a/elasticsearch-persistence/lib/elasticsearch/persistence/repository/store.rb +++ b/elasticsearch-persistence/lib/elasticsearch/persistence/repository/store.rb @@ -12,13 +12,19 @@ module Store # repository.save(myobject) # => {"_index"=>"...", "_type"=>"...", "_id"=>"...", "_version"=>1, "created"=>true} # - # @return {Hash} The response from Elasticsearch + # @param [ Object ] document The document to save into Elasticsearch. + # @param [ Hash ] options The save request options. + # + # @return [ Hash ] The response from Elasticsearch # def save(document, options={}) serialized = serialize(document) - id = __get_id_from_document(serialized) - type = document_type - client.index( { index: index_name, type: type, id: id, body: serialized }.merge(options) ) + id = __get_id_from_document(serialized) + request = { index: index_name, + id: id, + body: serialized } + request[:type] = document_type if document_type + client.index(request.merge(options)) end # Update the serialized object in Elasticsearch with partial data or script @@ -33,38 +39,27 @@ def save(document, options={}) # repository.update 1, script: 'ctx._source.views += 1' # # => {"_index"=>"...", "_type"=>"...", "_id"=>"1", "_version"=>3} # - # @return {Hash} The response from Elasticsearch + # @param [ Object ] document_or_id The document to update or the id of the document to update. + # @param [ Hash ] options The update request options. # - def update(document, options={}) - case - when document.is_a?(String) || document.is_a?(Integer) - id = document - when document.respond_to?(:to_hash) - serialized = document.to_hash - id = __extract_id_from_document(serialized) - else - raise ArgumentError, "Expected a document ID or a Hash-like object, #{document.class} given" - end - - type = options.delete(:type) || \ - (defined?(serialized) && serialized && serialized.delete(:type)) || \ - document_type - - if defined?(serialized) && serialized - body = if serialized[:script] - serialized.select { |k, v| [:script, :params, :upsert].include? k } - else - { doc: serialized } - end + # @return [ Hash ] The response from Elasticsearch + # + def update(document_or_id, options = {}) + if document_or_id.is_a?(String) || document_or_id.is_a?(Integer) + id = document_or_id + body = options + type = document_type else - body = {} - body.update( doc: options.delete(:doc)) if options[:doc] - body.update( script: options.delete(:script)) if options[:script] - body.update( params: options.delete(:params)) if options[:params] - body.update( upsert: options.delete(:upsert)) if options[:upsert] + document = serialize(document_or_id) + id = __extract_id_from_document(document) + if options[:script] + body = options + else + body = { doc: document }.merge(options) + end + type = document.delete(:type) || document_type end - - client.update( { index: index_name, type: type, id: id, body: body }.merge(options) ) + client.update(index: index_name, id: id, type: type, body: body) end # Remove the serialized object or document with specified ID from Elasticsearch @@ -74,21 +69,21 @@ def update(document, options={}) # repository.delete(1) # # => {"_index"=>"...", "_type"=>"...", "_id"=>"1", "_version"=>4} # - # @return {Hash} The response from Elasticsearch + # @param [ Object ] document_or_id The document to delete or the id of the document to delete. + # @param [ Hash ] options The delete request options. # - def delete(document, options={}) - if document.is_a?(String) || document.is_a?(Integer) - id = document - type = document_type + # @return [ Hash ] The response from Elasticsearch + # + def delete(document_or_id, options = {}) + if document_or_id.is_a?(String) || document_or_id.is_a?(Integer) + id = document_or_id else - serialized = serialize(document) - id = __get_id_from_document(serialized) - type = document_type + serialized = serialize(document_or_id) + id = __get_id_from_document(serialized) end - client.delete( { index: index_name, type: type, id: id }.merge(options) ) + client.delete({ index: index_name, type: document_type, id: id }.merge(options)) end end - end end end diff --git a/elasticsearch-persistence/lib/elasticsearch/persistence/version.rb b/elasticsearch-persistence/lib/elasticsearch/persistence/version.rb index 862c815a6..fea31b99c 100644 --- a/elasticsearch-persistence/lib/elasticsearch/persistence/version.rb +++ b/elasticsearch-persistence/lib/elasticsearch/persistence/version.rb @@ -1,5 +1,5 @@ module Elasticsearch module Persistence - VERSION = "6.0.0" + VERSION = '6.0.0' end end diff --git a/elasticsearch-persistence/spec/repository/find_spec.rb b/elasticsearch-persistence/spec/repository/find_spec.rb new file mode 100644 index 000000000..b630d45a2 --- /dev/null +++ b/elasticsearch-persistence/spec/repository/find_spec.rb @@ -0,0 +1,179 @@ +require 'spec_helper' + +describe Elasticsearch::Persistence::Repository::Find do + + after do + begin; repository.delete_index!; rescue; end + end + + let(:repository) do + DEFAULT_REPOSITORY + end + + describe '#exists?' do + + context 'when the document exists' do + + let(:id) do + repository.save(a: 1)['_id'] + end + + it 'returns true' do + expect(repository.exists?(id)).to be(true) + end + end + + context 'when the document does not exist' do + + it 'returns false' do + expect(repository.exists?('1')).to be(false) + end + end + + context 'when options are provided' do + + let(:id) do + repository.save(a: 1)['_id'] + end + + it 'applies the options' do + expect(repository.exists?(id, type: 'other_type')).to be(false) + end + end + end + + describe '#find' do + + context 'when options are not provided' do + + context 'when a single id is provided' do + + let!(:id) do + repository.save(a: 1)['_id'] + end + + it 'retrieves the document' do + expect(repository.find(id)).to eq('a' => 1) + end + end + + context 'when an array of ids is provided' do + + let!(:ids) do + 3.times.collect do |i| + repository.save(a: i)['_id'] + end + end + + it 'retrieves the documents' do + expect(repository.find(ids)).to eq([{ 'a' =>0 }, + { 'a' => 1 }, + { 'a' => 2 }]) + end + + context 'when some documents are found and some are not' do + + before do + ids[1] = 22 + ids + end + + it 'returns nil in the result list for the documents not found' do + expect(repository.find(ids)).to eq([{ 'a' =>0 }, + nil, + { 'a' => 2 }]) + end + end + end + + context 'when multiple ids are provided' do + + let!(:ids) do + 3.times.collect do |i| + repository.save(a: i)['_id'] + end + end + + it 'retrieves the documents' do + expect(repository.find(*ids)).to eq([{ 'a' =>0 }, + { 'a' => 1 }, + { 'a' => 2 }]) + end + end + + context 'when the document cannot be found' do + + before do + begin; repository.create_index!; rescue; end + end + + it 'raises a DocumentNotFound exception' do + expect { + repository.find(1) + }.to raise_exception(Elasticsearch::Persistence::Repository::DocumentNotFound) + end + end + end + + context 'when options are provided' do + + context 'when a single id is passed' do + + let!(:id) do + repository.save(a: 1)['_id'] + end + + it 'applies the options' do + expect { + repository.find(id, type: 'none') + }.to raise_exception(Elasticsearch::Persistence::Repository::DocumentNotFound) + end + end + + context 'when an array of ids is passed' do + + let!(:ids) do + 3.times.collect do |i| + repository.save(a: i)['_id'] + end + end + + it 'applies the options' do + expect(repository.find(ids, type: 'none')).to eq([nil, nil, nil]) + end + end + + context 'when multiple ids are passed' do + + let!(:ids) do + 3.times.collect do |i| + repository.save(a: i)['_id'] + end + end + + it 'applies the options' do + expect(repository.find(*ids, type: 'none')).to eq([nil, nil, nil]) + end + end + end + + context 'when a document_type is defined on the class' do + + let(:repository) do + MyTestRepository.new(document_type:'other_type', client: DEFAULT_CLIENT, index_name: 'test') + end + + let!(:ids) do + 3.times.collect do |i| + repository.save(a: i)['_id'] + end + end + + it 'uses the document type in the query' do + expect(repository.find(ids)).to eq([{ 'a' =>0 }, + { 'a' => 1 }, + { 'a' => 2 }]) + end + end + end +end diff --git a/elasticsearch-persistence/spec/repository/search_spec.rb b/elasticsearch-persistence/spec/repository/search_spec.rb new file mode 100644 index 000000000..939f09731 --- /dev/null +++ b/elasticsearch-persistence/spec/repository/search_spec.rb @@ -0,0 +1,181 @@ +require 'spec_helper' + +describe Elasticsearch::Persistence::Repository::Search do + + after do + begin; repository.delete_index!; rescue; end + end + + describe '#search' do + + let(:repository) do + DEFAULT_REPOSITORY + end + + context 'when the repository does not have a type set' do + + before do + repository.save({ name: 'user' }, refresh: true) + end + + context 'when a query definition is provided as a hash' do + + it 'uses the default document type' do + expect(repository.search({ query: { match: { name: 'user' } } }).first).to eq('name' => 'user') + end + end + + context 'when a query definition is provided as a string' do + + it 'uses the default document type' do + expect(repository.search('user').first).to eq('name' => 'user') + end + end + + context 'when the query definition is neither a String nor a Hash' do + + it 'raises an ArgumentError' do + expect { + repository.search(1) + }.to raise_exception(ArgumentError) + end + end + + context 'when options are provided' do + + context 'when a query definition is provided as a hash' do + + it 'uses the default document type' do + expect(repository.search({ query: { match: { name: 'user' } } }, type: 'other').first).to be_nil + end + end + + context 'when a query definition is provided as a string' do + + it 'uses the default document type' do + expect(repository.search('user', type: 'other').first).to be_nil + end + end + + context 'when the query definition is neither a String nor a Hash' do + + it 'raises an ArgumentError' do + expect { + repository.search(1) + }.to raise_exception(ArgumentError) + end + end + end + end + + context 'when the repository does have a type set' do + + let(:repository) do + MyTestRepository.new(document_type: 'other_note') + end + + before do + repository.save({ name: 'user' }, refresh: true) + end + + context 'when options are provided' do + + context 'when a query definition is provided as a hash' do + + it 'uses the options' do + expect(repository.search({ query: { match: { name: 'user' } } }, type: 'other').first).to be_nil + end + end + + context 'when a query definition is provided as a string' do + + it 'uses the options' do + expect(repository.search('user', type: 'other').first).to be_nil + end + end + + context 'when the query definition is neither a String nor a Hash' do + + it 'raises an ArgumentError' do + expect { + repository.search(1) + }.to raise_exception(ArgumentError) + end + end + end + end + end + + describe '#count' do + + context 'when the repository does not have a type set' do + + let(:repository) do + DEFAULT_REPOSITORY + end + + before do + repository.save({ name: 'user' }, refresh: true) + end + + context 'when a query definition is provided as a hash' do + + it 'uses the default document type' do + expect(repository.count({ query: { match: { name: 'user' } } })).to eq(1) + end + end + + context 'when a query definition is provided as a string' do + + it 'uses the default document type' do + expect(repository.count('user')).to eq(1) + end + end + + context 'when options are provided' do + + context 'when a query definition is provided as a hash' do + + it 'uses the options' do + expect(repository.count({ query: { match: { name: 'user' } } }, type: 'other')).to eq(0) + end + end + + context 'when a query definition is provided as a string' do + + it 'uses the options' do + expect(repository.count('user', type: 'other')).to eq(0) + end + end + end + end + + context 'when the repository does have a type set' do + + let(:repository) do + MyTestRepository.new(document_type: 'other_note') + end + + before do + repository.save({ name: 'user' }, refresh: true) + end + + context 'when options are provided' do + + context 'when a query definition is provided as a hash' do + + it 'uses the options' do + expect(repository.count({ query: { match: { name: 'user' } } }, type: 'other')).to eq(0) + end + end + + context 'when a query definition is provided as a string' do + + it 'uses the options' do + expect(repository.count('user', type: 'other')).to eq(0) + end + end + end + end + end +end diff --git a/elasticsearch-persistence/spec/repository/serialize_spec.rb b/elasticsearch-persistence/spec/repository/serialize_spec.rb new file mode 100644 index 000000000..bf0d7175e --- /dev/null +++ b/elasticsearch-persistence/spec/repository/serialize_spec.rb @@ -0,0 +1,53 @@ +require 'spec_helper' + +describe Elasticsearch::Persistence::Repository::Serialize do + + let(:repository) do + DEFAULT_REPOSITORY + end + + describe '#serialize' do + + before do + class MyDocument + def to_hash + { a: 1 } + end + end + end + + it 'calls #to_hash on the object' do + expect(repository.serialize(MyDocument.new)).to eq(a: 1) + end + end + + describe '#deserialize' do + + context 'when klass is defined on the Repository' do + + let(:repository) do + require 'set' + MyTestRepository.new(klass: Set) + end + + it 'instantiates an object of the klass' do + expect(repository.deserialize('_source' => { a: 1 })).to be_a(Set) + end + + it 'uses the source field to instantiate the object' do + expect(repository.deserialize('_source' => { a: 1 })).to eq(Set.new({ a: 1})) + end + end + + context 'when klass is not defined on the Repository' do + + it 'returns the raw Hash' do + expect(repository.deserialize('_source' => { a: 1 })).to be_a(Hash) + end + + it 'uses the source field to instantiate the object' do + expect(repository.deserialize('_source' => { a: 1 })).to eq(a: 1) + end + end + end +end diff --git a/elasticsearch-persistence/spec/repository/store_spec.rb b/elasticsearch-persistence/spec/repository/store_spec.rb new file mode 100644 index 000000000..c42c811d2 --- /dev/null +++ b/elasticsearch-persistence/spec/repository/store_spec.rb @@ -0,0 +1,327 @@ +require 'spec_helper' + +describe Elasticsearch::Persistence::Repository::Store do + + let(:repository) do + DEFAULT_REPOSITORY + end + + after do + begin; repository.delete_index!; rescue; end + end + + describe '#save' do + + let(:document) do + { a: 1 } + end + + let(:response) do + repository.save(document) + end + + it 'saves the document' do + expect(repository.find(response['_id'])).to eq('a' => 1) + end + + context 'when the repository defines a custom serialize method' do + + before do + class OtherNoteRepository + include Elasticsearch::Persistence::Repository + def serialize(document) + { b: 1 } + end + end + end + + after do + if defined?(OtherNoteRepository) + Object.send(:remove_const, OtherNoteRepository.name) + end + end + + let(:repository) do + OtherNoteRepository.new(client: DEFAULT_CLIENT) + end + + let(:response) do + repository.save(document) + end + + it 'saves the document' do + expect(repository.find(response['_id'])).to eq('b' => 1) + end + end + + context 'when options are provided' do + + let!(:response) do + repository.save(document, type: 'other_note') + end + + it 'saves the document using the options' do + expect { + repository.find(response['_id']) + }.to raise_exception(Elasticsearch::Persistence::Repository::DocumentNotFound) + expect(repository.find(response['_id'], type: 'other_note')).to eq('a' => 1) + end + end + end + + describe '#update' do + + before(:all) do + class Note + def to_hash + { text: 'testing', views: 0 } + end + end + end + + after(:all) do + if defined?(Note) + Object.send(:remove_const, :Note) + end + end + + context 'when the document exists' do + + let!(:id) do + repository.save(Note.new)['_id'] + end + + context 'when an id is provided' do + + context 'when a doc is specified in the options' do + + before do + repository.update(id, doc: { text: 'testing_2' }) + end + + it 'updates using the doc parameter' do + expect(repository.find(id)).to eq('text' => 'testing_2', 'views' => 0) + end + end + + context 'when a script is specified in the options' do + + before do + repository.update(id, script: { inline: 'ctx._source.views += 1' }) + end + + it 'updates using the script parameter' do + expect(repository.find(id)).to eq('text' => 'testing', 'views' => 1) + end + end + + context 'when params are specified in the options' do + + before do + repository.update(id, script: { inline: 'ctx._source.views += params.count', + params: { count: 2 } }) + end + + it 'updates using the script parameter' do + expect(repository.find(id)).to eq('text' => 'testing', 'views' => 2) + end + end + + context 'when upsert is specified in the options' do + + before do + repository.update(id, script: { inline: 'ctx._source.views += 1' }, + upsert: { text: 'testing_2' }) + end + + it 'executes the script' do + expect(repository.find(id)).to eq('text' => 'testing', 'views' => 1) + end + end + + context 'when doc_as_upsert is specified in the options' do + + before do + repository.update(id, doc: { text: 'testing_2' }, + doc_as_upsert: true) + end + + it 'performs an upsert' do + expect(repository.find(id)).to eq('text' => 'testing_2', 'views' => 0) + end + end + end + + context 'when a document is provided as the query criteria' do + + context 'when no options are provided' do + + before do + repository.update(id: id, text: 'testing_2') + end + + it 'updates using the id and the document as the doc parameter' do + expect(repository.find(id)).to eq('text' => 'testing_2', 'views' => 0) + end + end + + context 'when options are provided' do + + context 'when a doc is specified in the options' do + + before do + repository.update({ id: id, text: 'testing' }, doc: { text: 'testing_2' }) + end + + it 'updates using the id and the doc in the options' do + expect(repository.find(id)).to eq('text' => 'testing_2', 'views' => 0) + end + end + + context 'when a script is specified in the options' do + + before do + repository.update({ id: id, text: 'testing' }, + script: { inline: 'ctx._source.views += 1' }) + end + + it 'updates using the id and script from the options' do + expect(repository.find(id)).to eq('text' => 'testing', 'views' => 1) + end + end + + context 'when params are specified in the options' do + + before do + repository.update({ id: id, text: 'testing' }, + script: { inline: 'ctx._source.views += params.count', + params: { count: 2 } }) + end + + it 'updates using the id and script and params from the options' do + expect(repository.find(id)).to eq('text' => 'testing', 'views' => 2) + end + end + + context 'when upsert is specified in the options' do + + before do + repository.update({ id: id, text: 'testing_2' }, + doc_as_upsert: true) + end + + it 'updates using the id and script and params from the options' do + expect(repository.find(id)).to eq('text' => 'testing_2', 'views' => 0) + end + end + end + end + end + + context 'when the document does not exist' do + + context 'when an id is provided 'do + + it 'raises an exception' do + expect { + repository.update(1, doc: { text: 'testing_2' }) + }.to raise_exception(Elasticsearch::Transport::Transport::Errors::NotFound) + end + + context 'when upsert is provided' do + + before do + repository.update(1, doc: { text: 'testing' }, doc_as_upsert: true) + end + + it 'upserts the document' do + expect(repository.find(1)).to eq('text' => 'testing') + end + end + end + + context 'when a document is provided' do + + it 'raises an exception' do + expect { + repository.update(id: 1, text: 'testing_2') + }.to raise_exception(Elasticsearch::Transport::Transport::Errors::NotFound) + end + + context 'when upsert is provided' do + + before do + repository.update({ id: 1, text: 'testing' }, doc_as_upsert: true) + end + + it 'upserts the document' do + expect(repository.find(1)).to eq('text' => 'testing') + end + end + end + end + end + + describe '#delete' do + + before(:all) do + class Note + def to_hash + { text: 'testing', views: 0 } + end + end + end + + after(:all) do + if defined?(Note) + Object.send(:remove_const, :Note) + end + end + + context 'when the document exists' do + + let!(:id) do + repository.save(Note.new)['_id'] + end + + context 'an id is provided' do + + before do + repository.delete(id) + end + + it 'deletes the document using the id' do + expect { + repository.find(id) + }.to raise_exception(Elasticsearch::Persistence::Repository::DocumentNotFound) + end + end + + context 'when a document is provided' do + + before do + repository.delete(id: id, text: 'testing') + end + + it 'deletes the document using the document' do + expect { + repository.find(id) + }.to raise_exception(Elasticsearch::Persistence::Repository::DocumentNotFound) + end + end + end + + context 'when the document does not exist' do + + before do + repository.create_index! + end + + it 'raises an exception' do + expect { + repository.delete(1) + }.to raise_exception(Elasticsearch::Transport::Transport::Errors::NotFound) + end + end + end +end diff --git a/elasticsearch-persistence/spec/repository_spec.rb b/elasticsearch-persistence/spec/repository_spec.rb new file mode 100644 index 000000000..2d2d4d4ca --- /dev/null +++ b/elasticsearch-persistence/spec/repository_spec.rb @@ -0,0 +1,716 @@ +require 'spec_helper' + +describe Elasticsearch::Persistence::Repository do + + describe '#create' do + + before(:all) do + class RepositoryWithoutDSL + include Elasticsearch::Persistence::Repository + end + end + + after(:all) do + if defined?(RepositoryWithoutDSL) + Object.send(:remove_const, RepositoryWithoutDSL.name) + end + end + + it 'creates a repository object' do + expect(RepositoryWithoutDSL.create).to be_a(RepositoryWithoutDSL) + end + + context 'when options are provided' do + + let(:repository) do + RepositoryWithoutDSL.create(document_type: 'note') + end + + it 'sets the options on the instance' do + expect(repository.document_type).to eq('note') + end + end + + context 'when a block is passed' do + + let(:repository) do + RepositoryWithoutDSL.create(document_type: 'note') do + mapping dynamic: 'strict' do + indexes :foo + end + end + end + + it 'executes the block on the instance' do + expect(repository.mapping.to_hash).to eq(note: { dynamic: 'strict', properties: { foo: { type: 'text' } } }) + end + + context 'when options are provided in the args and set in the block' do + + let(:repository) do + RepositoryWithoutDSL.create(mapping: double('mapping', to_hash: {}), document_type: 'note') do + mapping dynamic: 'strict' do + indexes :foo + end + end + end + + it 'uses the options from the args' do + expect(repository.mapping.to_hash).to eq({}) + end + end + end + end + + describe '#initialize' do + + before(:all) do + class RepositoryWithoutDSL + include Elasticsearch::Persistence::Repository + end + end + + after(:all) do + if defined?(RepositoryWithoutDSL) + Object.send(:remove_const, RepositoryWithoutDSL.name) + end + end + + after do + begin; repository.delete_index!; rescue; end + end + + context 'when options are not provided' do + + let(:repository) do + RepositoryWithoutDSL.new + end + + it 'sets a default client' do + expect(repository.client).to be_a(Elasticsearch::Transport::Client) + end + + it 'sets a default document type' do + expect(repository.document_type).to eq('_doc') + end + + it 'sets a default index name' do + expect(repository.index_name).to eq('repository') + end + + it 'does not set a klass' do + expect(repository.klass).to be_nil + end + end + + context 'when options are provided' do + + let(:client) do + Elasticsearch::Transport::Client.new + end + + let(:repository) do + RepositoryWithoutDSL.new(client: client, document_type: 'user', index_name: 'users', klass: Array) + end + + it 'sets the client' do + expect(repository.client).to be(client) + end + + it 'sets document type' do + expect(repository.document_type).to eq('user') + end + + it 'sets index name' do + expect(repository.index_name).to eq('users') + end + + it 'sets the klass' do + expect(repository.klass).to eq(Array) + end + end + end + + context 'when the DSL module is included' do + + before(:all) do + class RepositoryWithDSL + include Elasticsearch::Persistence::Repository + include Elasticsearch::Persistence::Repository::DSL + + document_type 'note' + index_name 'notes_repo' + klass Hash + client DEFAULT_CLIENT + + settings number_of_shards: 1, number_of_replicas: 0 do + mapping dynamic: 'strict' do + indexes :foo do + indexes :bar + end + indexes :baz + end + end + end + end + + after(:all) do + if defined?(RepositoryWithDSL) + Object.send(:remove_const, RepositoryWithDSL.name) + end + end + + after do + begin; repository.delete_index; rescue; end + end + + context '#client' do + + it 'allows the value to be set only once on the class' do + RepositoryWithDSL.client(double('client', class: 'other_client')) + expect(RepositoryWithDSL.client).to be(DEFAULT_CLIENT) + end + + it 'sets the value at the class level' do + expect(RepositoryWithDSL.client).to be(DEFAULT_CLIENT) + end + + it 'sets the value as the default at the instance level' do + expect(RepositoryWithDSL.new.client).to be(DEFAULT_CLIENT) + end + + it 'allows the value to be overridden with options on the instance' do + expect(RepositoryWithDSL.new(client: double('client', instance: 'other')).client.instance).to eq('other') + end + end + + context '#klass' do + + it 'allows the value to be set only once on the class' do + RepositoryWithDSL.klass(Array) + expect(RepositoryWithDSL.klass).to eq(Hash) + end + + it 'sets the value at the class level' do + expect(RepositoryWithDSL.klass).to eq(Hash) + end + + it 'sets the value as the default at the instance level' do + expect(RepositoryWithDSL.new.klass).to eq(Hash) + end + + it 'allows the value to be overridden with options on the instance' do + expect(RepositoryWithDSL.new(klass: Array).klass).to eq(Array) + end + + context 'when nil is passed to the method' do + + before do + RepositoryWithDSL.klass(nil) + end + + it 'allows the value to be set only once' do + expect(RepositoryWithDSL.klass).to eq(Hash) + end + end + end + + context '#document_type' do + + it 'allows the value to be set only once on the class' do + RepositoryWithDSL.document_type('other_note') + expect(RepositoryWithDSL.document_type).to eq('note') + end + + it 'sets the value at the class level' do + expect(RepositoryWithDSL.document_type).to eq('note') + end + + it 'sets the value as the default at the instance level' do + expect(RepositoryWithDSL.new.document_type).to eq('note') + end + + it 'allows the value to be overridden with options on the instance' do + expect(RepositoryWithDSL.new(document_type: 'other_note').document_type).to eq('other_note') + end + end + + context '#index_name' do + + it 'allows the value to be set only once on the class' do + RepositoryWithDSL.index_name('other_name') + expect(RepositoryWithDSL.index_name).to eq('notes_repo') + end + + it 'sets the value at the class level' do + expect(RepositoryWithDSL.index_name).to eq('notes_repo') + end + + it 'sets the value as the default at the instance level' do + expect(RepositoryWithDSL.new.index_name).to eq('notes_repo') + end + + it 'allows the value to be overridden with options on the instance' do + expect(RepositoryWithDSL.new(index_name: 'other_notes_repo').index_name).to eq('other_notes_repo') + end + end + + describe '#create_index!' do + + context 'when the method is called on an instance' do + + let(:repository) do + RepositoryWithDSL.new + end + + before do + begin; repository.delete_index!; rescue; end + repository.create_index! + end + + it 'creates the index' do + expect(repository.index_exists?).to be(true) + end + end + + context 'when the method is called on the class' do + + it 'raises a NotImplementedError' do + expect { + RepositoryWithDSL.create_index! + }.to raise_exception(NotImplementedError) + end + end + end + + describe '#delete_index!' do + + context 'when the method is called on an instance' do + + let(:repository) do + RepositoryWithDSL.new + end + + before do + repository.create_index! + begin; repository.delete_index!; rescue; end + end + + it 'deletes the index' do + expect(repository.index_exists?).to be(false) + end + end + + context 'when the method is called on the class' do + + it 'raises a NotImplementedError' do + expect { + RepositoryWithDSL.delete_index! + }.to raise_exception(NotImplementedError) + end + end + end + + describe '#refresh_index!' do + + context 'when the method is called on an instance' do + + let(:repository) do + RepositoryWithDSL.new + end + + before do + repository.create_index! + end + + it 'refreshes the index' do + expect(repository.refresh_index!['_shards']).to be_a(Hash) + end + end + + context 'when the method is called on the class' do + + it 'raises a NotImplementedError' do + expect { + RepositoryWithDSL.refresh_index! + }.to raise_exception(NotImplementedError) + end + end + end + + describe '#index_exists?' do + + context 'when the method is called on an instance' do + + let(:repository) do + RepositoryWithDSL.new + end + + before do + repository.create_index! + end + + it 'determines if the index exists' do + expect(repository.index_exists?).to be(true) + end + end + + context 'when the method is called on the class' do + + it 'raises a NotImplementedError' do + expect { + RepositoryWithDSL.index_exists? + }.to raise_exception(NotImplementedError) + end + end + end + + describe '#mapping' do + + let(:expected_mapping) do + { note: { dynamic: 'strict', + properties: { foo: { type: 'object', + properties: { bar: { type: 'text' } } }, + baz: { type: 'text' } } + } + } + end + + it 'sets the value at the class level' do + expect(RepositoryWithDSL.mapping.to_hash).to eq(expected_mapping) + end + + it 'sets the value as the default at the instance level' do + expect(RepositoryWithDSL.new.mapping.to_hash).to eq(expected_mapping) + end + + it 'allows the value to be overridden with options on the instance' do + expect(RepositoryWithDSL.new(mapping: double('mapping', to_hash: { note: {} })).mapping.to_hash).to eq(note: {}) + end + + context 'when the instance has a different document type' do + + let(:expected_mapping) do + { other_note: { dynamic: 'strict', + properties: { foo: { type: 'object', + properties: { bar: { type: 'text' } } }, + baz: { type: 'text' } } + } + } + end + + it 'updates the mapping to use the document type' do + expect(RepositoryWithDSL.new(document_type: 'other_note').mapping.to_hash).to eq(expected_mapping) + end + end + end + + describe '#settings' do + + it 'sets the value at the class level' do + expect(RepositoryWithDSL.settings.to_hash).to eq(number_of_shards: 1, number_of_replicas: 0) + end + + it 'sets the value as the default at the instance level' do + expect(RepositoryWithDSL.new.settings.to_hash).to eq(number_of_shards: 1, number_of_replicas: 0) + end + + it 'allows the value to be overridden with options on the instance' do + expect(RepositoryWithDSL.new(settings: { number_of_shards: 3 }).settings.to_hash).to eq({number_of_shards: 3}) + end + end + end + + context 'when the DSL module is not included' do + + before(:all) do + class RepositoryWithoutDSL + include Elasticsearch::Persistence::Repository + end + end + + after(:all) do + if defined?(RepositoryWithoutDSL) + Object.send(:remove_const, RepositoryWithoutDSL.name) + end + end + + context '#client' do + + it 'does not define the method at the class level' do + expect { + RepositoryWithoutDSL.client + }.to raise_exception(NoMethodError) + end + + it 'sets a default on the instance' do + expect(RepositoryWithoutDSL.new.client).to be_a(Elasticsearch::Transport::Client) + end + + it 'allows the value to be overridden with options on the instance' do + expect(RepositoryWithoutDSL.new(client: double('client', object_id: 123)).client.object_id).to eq(123) + end + end + + context '#klass' do + + it 'does not define the method at the class level' do + expect { + RepositoryWithoutDSL.klass + }.to raise_exception(NoMethodError) + end + + it 'does not set a default on an instance' do + expect(RepositoryWithoutDSL.new.klass).to be_nil + end + + it 'allows the value to be overridden with options on the instance' do + expect(RepositoryWithoutDSL.new(klass: Array).klass).to eq(Array) + end + end + + context '#document_type' do + + it 'does not define the method at the class level' do + expect { + RepositoryWithoutDSL.document_type + }.to raise_exception(NoMethodError) + end + + it 'sets a default on the instance' do + expect(RepositoryWithoutDSL.new.document_type).to eq('_doc') + end + + it 'allows the value to be overridden with options on the instance' do + expect(RepositoryWithoutDSL.new(document_type: 'notes').document_type).to eq('notes') + end + end + + context '#index_name' do + + it 'does not define the method at the class level' do + expect { + RepositoryWithoutDSL.index_name + }.to raise_exception(NoMethodError) + end + + it 'sets a default on the instance' do + expect(RepositoryWithoutDSL.new.index_name).to eq('repository') + end + + it 'allows the value to be overridden with options on the instance' do + expect(RepositoryWithoutDSL.new(index_name: 'notes_repository').index_name).to eq('notes_repository') + end + end + + describe '#create_index!' do + + let(:repository) do + RepositoryWithoutDSL.new(client: DEFAULT_CLIENT) + end + + after do + begin; repository.delete_index!; rescue; end + end + + it 'does not define the method at the class level' do + expect { + RepositoryWithoutDSL.create_index! + }.to raise_exception(NoMethodError) + end + + it 'creates an index' do + repository.create_index! + expect(repository.index_exists?).to eq(true) + end + end + + describe '#delete_index!' do + + let(:repository) do + RepositoryWithoutDSL.new(client: DEFAULT_CLIENT) + end + + it 'does not define the method at the class level' do + expect { + RepositoryWithoutDSL.delete_index! + }.to raise_exception(NoMethodError) + end + + it 'deletes an index' do + repository.create_index! + repository.delete_index! + expect(repository.index_exists?).to eq(false) + end + end + + describe '#refresh_index!' do + + let(:repository) do + RepositoryWithoutDSL.new(client: DEFAULT_CLIENT) + end + + after do + begin; repository.delete_index!; rescue; end + end + + it 'does not define the method at the class level' do + expect { + RepositoryWithoutDSL.refresh_index! + }.to raise_exception(NoMethodError) + end + + it 'refreshes an index' do + repository.create_index! + expect(repository.refresh_index!['_shards']).to be_a(Hash) + end + end + + describe '#index_exists?' do + + let(:repository) do + RepositoryWithoutDSL.new(client: DEFAULT_CLIENT) + end + + after do + begin; repository.delete_index!; rescue; end + end + + it 'does not define the method at the class level' do + expect { + RepositoryWithoutDSL.index_exists? + }.to raise_exception(NoMethodError) + end + + it 'returns whether the index exists' do + repository.create_index! + expect(repository.index_exists?).to be(true) + end + end + + describe '#mapping' do + + it 'does not define the method at the class level' do + expect { + RepositoryWithoutDSL.mapping + }.to raise_exception(NoMethodError) + end + + it 'sets a default on an instance' do + expect(RepositoryWithoutDSL.new.mapping.to_hash).to eq(_doc: { properties: {} }) + end + + it 'allows the mapping to be set as an option' do + expect(RepositoryWithoutDSL.new(mapping: double('mapping', to_hash: { note: {} })).mapping.to_hash).to eq(note: {}) + end + + context 'when a block is passed to the create method' do + + let(:expected_mapping) do + { note: { dynamic: 'strict', + properties: { foo: { type: 'object', + properties: { bar: { type: 'text' } } }, + baz: { type: 'text' } } + } + } + end + + let(:repository) do + RepositoryWithoutDSL.create(document_type: 'note') do + mapping dynamic: 'strict' do + indexes :foo do + indexes :bar + end + indexes :baz + end + end + end + + it 'allows the mapping to be set in the block' do + expect(repository.mapping.to_hash).to eq(expected_mapping) + end + + context 'when the mapping is set in the options' do + + let(:repository) do + RepositoryWithoutDSL.create(mapping: double('mapping', to_hash: { note: {} })) do + mapping dynamic: 'strict' do + indexes :foo do + indexes :bar + end + indexes :baz + end + end + end + + it 'uses the mapping from the options' do + expect(repository.mapping.to_hash).to eq(note: {}) + end + end + end + end + + describe '#settings' do + + it 'does not define the method at the class level' do + expect { + RepositoryWithoutDSL.settings + }.to raise_exception(NoMethodError) + end + + it 'sets a default on an instance' do + expect(RepositoryWithoutDSL.new.settings.to_hash).to eq({}) + end + + it 'allows the settings to be set as an option' do + expect(RepositoryWithoutDSL.new(settings: double('settings', to_hash: {})).settings.to_hash).to eq({}) + end + + context 'when a block is passed to the #create method' do + + let(:repository) do + RepositoryWithoutDSL.create(document_type: 'note') do + settings number_of_shards: 1, number_of_replicas: 0 + end + end + + it 'allows the settings to be set with a block' do + expect(repository.settings.to_hash).to eq(number_of_shards: 1, number_of_replicas: 0) + end + + context 'when a mapping is set in the block as well' do + + let(:expected_mapping) do + { note: { dynamic: 'strict', + properties: { foo: { type: 'object', + properties: { bar: { type: 'text' } } }, + baz: { type: 'text' } } + } + } + end + + let(:repository) do + RepositoryWithoutDSL.create(document_type: 'note') do + settings number_of_shards: 1, number_of_replicas: 0 do + mapping dynamic: 'strict' do + indexes :foo do + indexes :bar + end + indexes :baz + end + end + end + end + + it 'allows the settings to be set with a block' do + expect(repository.settings.to_hash).to eq(number_of_shards: 1, number_of_replicas: 0) + end + + it 'allows the mapping to be set with a block' do + expect(repository.mappings.to_hash).to eq(expected_mapping) + end + end + end + end + end +end diff --git a/elasticsearch-persistence/spec/spec_helper.rb b/elasticsearch-persistence/spec/spec_helper.rb new file mode 100644 index 000000000..9f2f0f5df --- /dev/null +++ b/elasticsearch-persistence/spec/spec_helper.rb @@ -0,0 +1,28 @@ +require 'pry-nav' +require 'elasticsearch/persistence' + +RSpec.configure do |config| + config.formatter = 'documentation' + config.color = true + + config.after(:suite) do + DEFAULT_CLIENT.indices.delete(index: '_all') + end +end + +# The default client to be used by the repositories. +# +# @since 6.0.0 +DEFAULT_CLIENT = Elasticsearch::Client.new(host: "localhost:#{(ENV['TEST_CLUSTER_PORT'] || 9250)}", + tracer: (ENV['QUIET'] ? nil : ::Logger.new(STDERR))) + +class MyTestRepository + include Elasticsearch::Persistence::Repository + include Elasticsearch::Persistence::Repository::DSL + client DEFAULT_CLIENT +end + +# The default repository to be used by tests. +# +# @since 6.0.0 +DEFAULT_REPOSITORY = MyTestRepository.new(index_name: 'my_test_repository', document_type: 'test') diff --git a/elasticsearch-persistence/test/integration/repository/custom_class_test.rb b/elasticsearch-persistence/test/integration/repository/custom_class_test.rb deleted file mode 100644 index 6528dd438..000000000 --- a/elasticsearch-persistence/test/integration/repository/custom_class_test.rb +++ /dev/null @@ -1,88 +0,0 @@ -require 'test_helper' - -module Elasticsearch - module Persistence - class RepositoryCustomClassIntegrationTest < Elasticsearch::Test::IntegrationTestCase - - class ::MyNote - attr_reader :attributes - - def initialize(attributes={}) - @attributes = Hashie::Mash.new(attributes) - 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 to_hash - @attributes - end - end - - context "A custom repository class" do - setup do - class ::MyNotesRepository - include Elasticsearch::Persistence::Repository - - klass MyNote - document_type 'my_note' - - settings number_of_shards: 1 do - mapping do - indexes :title, analyzer: 'snowball' - end - end - - create_index! - - def deserialize(document) - klass.new document.merge(document['_source']) - end - end - - @repository = MyNotesRepository.new - @repository.klass = MyNotesRepository.klass - @repository.document_type = MyNotesRepository.document_type - - @repository.client.cluster.health wait_for_status: 'yellow' - end - - should "save the object under a correct index and type" do - @repository.save MyNote.new(id: '1', title: 'Test') - result = @repository.find(1) - - assert_instance_of MyNote, result - assert_equal 'Test', result.title - - assert_not_nil Elasticsearch::Persistence.client.get index: 'my_notes_repository', - type: 'my_note', - id: '1' - end - - should "delete the object" do - note = MyNote.new id: 1, title: 'Test' - @repository.save note - - assert_not_nil @repository.find(1) - - @repository.delete(note) - assert_raise(Elasticsearch::Persistence::Repository::DocumentNotFound) { @repository.find(1) } - end - - should "retrieve the object via a search query" do - note = MyNote.new title: 'Testing' - @repository.save note, refresh: true - - results = @repository.search query: { match: { title: 'Test' } } - assert_equal 'Testing', results.first.title - end - end - - end - end -end diff --git a/elasticsearch-persistence/test/integration/repository/customized_class_test.rb b/elasticsearch-persistence/test/integration/repository/customized_class_test.rb deleted file mode 100644 index 06289b12d..000000000 --- a/elasticsearch-persistence/test/integration/repository/customized_class_test.rb +++ /dev/null @@ -1,82 +0,0 @@ -require 'test_helper' - -module Elasticsearch - module Persistence - class RepositoryCustomizedClassIntegrationTest < Elasticsearch::Test::IntegrationTestCase - - module ::My - class Note - attr_reader :attributes - - def initialize(attributes={}) - @attributes = attributes - end - - def to_hash - @attributes - end - end - end - - context "A custom repository class" do - setup do - @repository = Elasticsearch::Persistence::Repository.new do - index 'my_notes' - type 'my_note' - klass My::Note - - settings number_of_shards: 1 do - mapping do - indexes :title, analyzer: 'snowball' - end - end - - create_index! - end - - @repository.client.cluster.health wait_for_status: 'yellow' - end - - should "save the object under a correct index and type" do - @repository.save My::Note.new(id: '1', title: 'Test') - - assert_instance_of My::Note, @repository.find(1) - assert_not_nil Elasticsearch::Persistence.client.get index: 'my_notes', type: 'my_note', id: '1' - end - - should "update the document" do - @repository.save Note.new(id: 1, title: 'Test') - - @repository.update 1, doc: { title: 'UPDATED' } - assert_equal 'UPDATED', @repository.find(1).attributes['title'] - end - - should "update the document with a script" do - @repository.save Note.new(id: 1, title: 'Test') - - @repository.update 1, script: 'ctx._source.title = "UPDATED"' - assert_equal 'UPDATED', @repository.find(1).attributes['title'] - end - - should "delete the object" do - note = My::Note.new id: 1, title: 'Test' - @repository.save note - - assert_not_nil @repository.find(1) - - @repository.delete(note) - assert_raise(Elasticsearch::Persistence::Repository::DocumentNotFound) { @repository.find(1) } - end - - should "create the index with correct mapping" do - note = My::Note.new title: 'Testing' - @repository.save note, refresh: true - - results = @repository.search query: { match: { title: 'Test' } } - assert_equal 'Testing', results.first.attributes['title'] - end - end - - end - end -end diff --git a/elasticsearch-persistence/test/integration/repository/default_class_test.rb b/elasticsearch-persistence/test/integration/repository/default_class_test.rb deleted file mode 100644 index 1674a3a42..000000000 --- a/elasticsearch-persistence/test/integration/repository/default_class_test.rb +++ /dev/null @@ -1,119 +0,0 @@ -require 'test_helper' - -module Elasticsearch - module Persistence - class RepositoryDefaultClassIntegrationTest < Elasticsearch::Test::IntegrationTestCase - - class ::Note - attr_reader :attributes - - def initialize(attributes={}) - @attributes = attributes - end - - def to_hash - @attributes - end - end - - context "The default repository class" do - setup do - @repository = Elasticsearch::Persistence::Repository.new - @repository.klass = ::Note - @repository.document_type = 'note' - @repository.client.cluster.health wait_for_status: 'yellow' - end - - should "save the object with a custom ID and find it" do - @repository.save Note.new(id: '1', title: 'Test') - - assert_equal 'Test', @repository.find(1).attributes['title'] - end - - should "save the object with an auto-generated ID and find it" do - response = @repository.save Note.new(title: 'Test') - - assert_equal 'Test', @repository.find(response['_id']).attributes['title'] - end - - should "update the document" do - @repository.save Note.new(id: 1, title: 'Test') - - @repository.update 1, type: 'note', doc: { title: 'UPDATED' } - assert_equal 'UPDATED', @repository.find(1).attributes['title'] - end - - should "update the document with a script" do - @repository.save Note.new(id: 1, title: 'Test') - - @repository.update 1, type: 'note', script: 'ctx._source.title = "UPDATED"' - assert_equal 'UPDATED', @repository.find(1).attributes['title'] - end - - should "save the document with an upsert" do - @repository.update 1, type: 'note', script: 'ctx._source.clicks += 1', upsert: { clicks: 1 } - assert_equal 1, @repository.find(1).attributes['clicks'] - end - - should "delete an object" do - note = Note.new(id: '1', title: 'Test') - - @repository.save(note) - assert_not_nil @repository.find(1) - @repository.delete(note) - assert_raise(Elasticsearch::Persistence::Repository::DocumentNotFound) { @repository.find(1) } - end - - should "find multiple objects" do - (1..5).each { |i| @repository.save Note.new(id: i, title: "Test #{i}") } - - assert_equal 5, @repository.find(1, 2, 3, 4, 5).size - assert_equal 5, @repository.find([1, 2, 3, 4, 5]).size - end - - should "pass options to save and find" do - note = Note.new(id: '1', title: 'Test') - @repository.save note, routing: 'ABC' - - @repository.client.cluster.health level: 'indices', wait_for_status: 'yellow' - - assert_raise Elasticsearch::Persistence::Repository::DocumentNotFound do - @repository.find(1, routing: 'DEF') - end - - assert_nothing_raised do - note = @repository.find(1, routing: 'ABC') - assert_instance_of Note, note - end - end - - should "find notes with full text search" do - @repository.save Note.new(title: 'Test') - @repository.save Note.new(title: 'Test Test') - @repository.save Note.new(title: 'Crust') - @repository.client.indices.refresh index: @repository.index_name - - results = @repository.search 'test' - assert_equal 2, results.size - - results = @repository.search query: { match: { title: 'Test' } } - assert_equal 2, results.size - end - - should "count notes" do - @repository.save Note.new(title: 'Test') - @repository.client.indices.refresh index: @repository.index_name - assert_equal 1, @repository.count - end - - should "save and find a plain hash" do - @repository.klass = Hash - @repository.save id: 1, title: 'Hash' - result = @repository.find(1) - assert_equal 'Hash', result['_source']['title'] - end - end - - end - end -end diff --git a/elasticsearch-persistence/test/integration/repository/virtus_model_test.rb b/elasticsearch-persistence/test/integration/repository/virtus_model_test.rb deleted file mode 100644 index c6ad88177..000000000 --- a/elasticsearch-persistence/test/integration/repository/virtus_model_test.rb +++ /dev/null @@ -1,119 +0,0 @@ -require 'test_helper' - -require 'virtus' - -module Elasticsearch - module Persistence - class RepositoryWithVirtusIntegrationTest < Elasticsearch::Test::IntegrationTestCase - - class ::Page - include Virtus.model - - attribute :id, String, writer: :private - attribute :title, String - attribute :views, Integer, default: 0 - attribute :published, Boolean, default: false - attribute :slug, String, default: lambda { |page, attr| page.title.downcase.gsub(' ', '-') } - - def set_id(id) - self.id = id - end - end - - context "The repository with a Virtus model" do - setup do - @repository = Elasticsearch::Persistence::Repository.new do - index :pages - klass Page - document_type 'page' - - def deserialize(document) - page = klass.new document['_source'] - page.set_id document['_id'] - page - end - end - end - - should "save and find the object" do - page = Page.new title: 'Test Page' - - response = @repository.save page - id = response['_id'] - - result = @repository.find(id) - - assert_instance_of Page, result - assert_equal 'Test Page', result.title - assert_equal 0, result.views - - assert_not_nil Elasticsearch::Persistence.client.get index: 'pages', - type: 'page', - id: id - end - - should "update the object with a partial document" do - response = @repository.save Page.new(title: 'Test') - id = response['_id'] - - page = @repository.find(id) - - assert_equal 'Test', page.title - - @repository.update page.id, doc: { title: 'UPDATE' } - - page = @repository.find(id) - assert_equal 'UPDATE', page.title - end - - should "update the object with a Hash" do - response = @repository.save Page.new(title: 'Test') - id = response['_id'] - - page = @repository.find(id) - - assert_equal 'Test', page.title - - @repository.update id: page.id, title: 'UPDATE' - - page = @repository.find(id) - assert_equal 'UPDATE', page.title - end - - should "update the object with a script" do - response = @repository.save Page.new(title: 'Test Page') - id = response['_id'] - - page = @repository.find(id) - - assert_not_nil page.id - assert_equal 0, page.views - - @repository.update page.id, script: 'ctx._source.views += 1' - - page = @repository.find(id) - assert_equal 1, page.views - - @repository.update id: page.id, script: 'ctx._source.views += 1' - - page = @repository.find(id) - assert_equal 2, page.views - end - - should "update the object with a script and params" do - response = @repository.save Page.new(title: 'Test Page') - - @repository.update id: response['_id'], - script: { - inline: 'ctx._source.views += params.count', - params: { count: 3 } - } - - page = @repository.find(response['_id']) - assert_equal 3, page.views - end - end - - end - end -end diff --git a/elasticsearch-persistence/test/test_helper.rb b/elasticsearch-persistence/test/test_helper.rb deleted file mode 100644 index c881ac02e..000000000 --- a/elasticsearch-persistence/test/test_helper.rb +++ /dev/null @@ -1,55 +0,0 @@ -RUBY_1_8 = defined?(RUBY_VERSION) && RUBY_VERSION < '1.9' - -exit(0) if RUBY_1_8 - -$LOAD_PATH.unshift File.expand_path('../../../elasticsearch-model/lib', __FILE__) if File.exists? File.expand_path('../../../elasticsearch-model/lib', __FILE__) - -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'] - -if defined?(RUBY_VERSION) && RUBY_VERSION > '2.2' - require 'test-unit' - require 'mocha/test_unit' -else - require 'minitest/autorun' - require 'mocha/mini_test' -end - -require 'shoulda-context' - -require 'turn' unless ENV["TM_FILEPATH"] || ENV["NOTURN"] || defined?(RUBY_VERSION) && RUBY_VERSION > '2.2' - -require 'ansi' -require 'oj' unless defined?(JRUBY_VERSION) - -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 - - def teardown - Elasticsearch::Persistence.client.indices.delete index: '_all' - end - end - end -end diff --git a/elasticsearch-persistence/test/unit/persistence_test.rb b/elasticsearch-persistence/test/unit/persistence_test.rb deleted file mode 100644 index cd17ba7dc..000000000 --- a/elasticsearch-persistence/test/unit/persistence_test.rb +++ /dev/null @@ -1,32 +0,0 @@ -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 - - 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_class_test.rb b/elasticsearch-persistence/test/unit/repository_class_test.rb deleted file mode 100644 index d29711248..000000000 --- a/elasticsearch-persistence/test/unit/repository_class_test.rb +++ /dev/null @@ -1,51 +0,0 @@ -require 'test_helper' - -class Elasticsearch::Persistence::RepositoryClassTest < Test::Unit::TestCase - context "The default repository class" do - - 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 "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 - - 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 - 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 - end - - end -end diff --git a/elasticsearch-persistence/test/unit/repository_client_test.rb b/elasticsearch-persistence/test/unit/repository_client_test.rb deleted file mode 100644 index 88e40193e..000000000 --- a/elasticsearch-persistence/test/unit/repository_client_test.rb +++ /dev/null @@ -1,32 +0,0 @@ -require 'test_helper' - -class Elasticsearch::Persistence::RepositoryClientTest < Test::Unit::TestCase - context "The repository client" do - setup do - @shoulda_subject = Class.new() { include Elasticsearch::Persistence::Repository::Client }.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 - - 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 diff --git a/elasticsearch-persistence/test/unit/repository_find_test.rb b/elasticsearch-persistence/test/unit/repository_find_test.rb deleted file mode 100644 index bcf7f8a1f..000000000 --- a/elasticsearch-persistence/test/unit/repository_find_test.rb +++ /dev/null @@ -1,307 +0,0 @@ -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(:document_type).returns(nil) - @shoulda_subject.stubs(:klass).returns(nil) - @shoulda_subject.stubs(:index_name).returns('my_index') - @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 document_type exists" do - subject.expects(:document_type).twice.returns('my_document') - - @client - .expects(:exists) - .with do |arguments| - assert_equal 'my_document', arguments[:type] - assert_equal '1', arguments[:id] - true - end - .returns(true) - - assert_equal true, subject.exists?('1') - end - - should "return whether document exists using no document type" do - - @client - .expects(:exists) - .with do |arguments| - assert_equal nil, arguments[:type] - assert_equal '1', arguments[:id] - true - 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 'my_document', arguments[:type] - assert_equal 'foobarbam', arguments[:index] - assert_equal 'bambam', arguments[:routing] - true - end - - subject.exists? '1', index: 'foobarbam', routing: 'bambam', type: 'my_document' - end - end - - context "'__find_one' method" do - - should "find a document based on document_type and return a deserialized object" do - subject.expects(:document_type).twice.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] - true - end - .returns({'_source' => { 'foo' => 'bar' }}) - - assert_instance_of MyDocument, subject.__find_one('1') - end - - should "find a document using no type if document_type is not defined" do - subject.expects(:document_type).returns(nil) - - subject.expects(:deserialize).with({'_source' => {'foo' => 'bar'}}).returns(MyDocument.new) - - @client - .expects(:get) - .with do |arguments| - assert_equal nil, arguments[:type] - assert_equal '1', arguments[:id] - true - 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(:document_type).returns(nil) - - 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(: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(:deserialize) - - @client - .expects(:get) - .with do |arguments| - assert_equal 'foobarbam', arguments[:index] - assert_equal 'bambam', arguments[:routing] - true - end - .returns({'_source' => { 'foo' => 'bar' }}) - - subject.__find_one '1', index: 'foobarbam', routing: 'bambam', type: 'my_document' - end - end - - context "'__find_many' method" do - setup do - @response = {"docs"=> - [ {"_index"=>"my_index", - "_type"=>"note", - "_id"=>"1", - "_version"=>1, - "found"=>true, - "_source"=>{"id"=>"1", "title"=>"Test 1"}}, - - {"_index"=>"my_index", - "_type"=>"note", - "_id"=>"2", - "_version"=>1, - "found"=>true, - "_source"=>{"id"=>"2", "title"=>"Test 2"}} - ]} - end - - should "find documents based on document_type and return an Array of deserialized objects" do - subject.expects(:document_type).twice.returns('my_document') - - subject.expects(:deserialize).twice - - @client - .expects(:mget) - .with do |arguments| - assert_equal 'my_document', arguments[:type] - assert_equal ['1', '2'], arguments[:body][:ids] - true - 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(: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 nil, arguments[:type] - assert_equal ['1', '2'], arguments[:body][:ids] - true - 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"=>"my_index", - "_type"=>"note", - "_id"=>"1", - "_version"=>1, - "found"=>true, - "_source"=>{"id"=>"1", "title"=>"Test 1"}}, - - {"_index"=>"my_index", - "_type"=>"note", - "_id"=>"3", - "_version"=>1, - "found"=>false}, - - {"_index"=>"my_index", - "_type"=>"note", - "_id"=>"2", - "_version"=>1, - "found"=>true, - "_source"=>{"id"=>"2", "title"=>"Test 2"}} - ]} - - subject.expects(:document_type).twice.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] - true - 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(:deserialize).twice - - @client - .expects(:mget) - .with do |arguments| - assert_equal 'my_document', arguments[:type] - assert_equal 'foobarbam', arguments[:index] - assert_equal 'bambam', arguments[:routing] - true - end - .returns(@response) - - subject.__find_many ['1', '2'], index: 'foobarbam', routing: 'bambam', type: 'my_document' - end - end - - end -end diff --git a/elasticsearch-persistence/test/unit/repository_indexing_test.rb b/elasticsearch-persistence/test/unit/repository_indexing_test.rb deleted file mode 100644 index 51b2b8b21..000000000 --- a/elasticsearch-persistence/test/unit/repository_indexing_test.rb +++ /dev/null @@ -1,37 +0,0 @@ -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=>"text"}}}}, 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_module_test.rb b/elasticsearch-persistence/test/unit/repository_module_test.rb deleted file mode 100644 index 2c4cd44c5..000000000 --- a/elasticsearch-persistence/test/unit/repository_module_test.rb +++ /dev/null @@ -1,146 +0,0 @@ -require 'test_helper' - -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 "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 - 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 via block passed to the gateway method" do - class DummyRepositoryWithGatewaySerialize - include Elasticsearch::Persistence::Repository - - gateway do - def serialize(document) - 'FAKE!' - end - end - end - - repository = DummyRepositoryWithGatewaySerialize.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]) - true - end - repository.gateway.expects(:client).returns(client) - - repository.gateway.expects(:__get_id_from_document).returns('xxx') - - repository.save( id: '123', foo: 'bar' ) - 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]) - true - 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 - end -end diff --git a/elasticsearch-persistence/test/unit/repository_naming_test.rb b/elasticsearch-persistence/test/unit/repository_naming_test.rb deleted file mode 100644 index aed34598f..000000000 --- a/elasticsearch-persistence/test/unit/repository_naming_test.rb +++ /dev/null @@ -1,117 +0,0 @@ -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 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 "extract an ID from the document" do - should "delete the key from the Hash" do - d1 = { :id => 1 } - d2 = { :_id => 1 } - d3 = { 'id' => 1 } - d4 = { '_id' => 1 } - - assert_equal 1, subject.__extract_id_from_document(d1) - assert_nil d1[:id] - - assert_equal 1, subject.__extract_id_from_document(d2) - assert_nil d1[:_id] - - assert_equal 1, subject.__extract_id_from_document(d3) - assert_nil d1['id'] - - assert_equal 1, subject.__extract_id_from_document(d4) - assert_nil d1['_id'] - 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 - - should "be settable by DSL" do - subject.klass Foobar - assert_equal Foobar, subject.klass - 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 - - should "be aliased as `index`" do - 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 - should "be the default doc type when no klass is set" do - assert_equal '_doc', subject.document_type - end - - should "does not use the klass" do - subject.klass Foobar - assert_equal '_doc', subject.document_type - end - - should "be aliased as `type`" do - subject.document_type '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 diff --git a/elasticsearch-persistence/test/unit/repository_response_results_test.rb b/elasticsearch-persistence/test/unit/repository_response_results_test.rb deleted file mode 100644 index df8e08ae5..000000000 --- a/elasticsearch-persistence/test/unit/repository_response_results_test.rb +++ /dev/null @@ -1,98 +0,0 @@ -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" => "my_index", - "_type" => "note", - "_id" => "1", - "_score" => 0.19, - "_source" => {"id" => 1, "title" => "Test 1"}}, - - {"_index" => "my_index", - "_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 HashWrapper" 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 "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, - { '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 deleted file mode 100644 index 5cf6d5ab3..000000000 --- a/elasticsearch-persistence/test/unit/repository_search_test.rb +++ /dev/null @@ -1,100 +0,0 @@ -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(:document_type).returns(nil) - @shoulda_subject.stubs(:klass).returns(nil) - @shoulda_subject.stubs(:index_name).returns('test') - @shoulda_subject.stubs(:client).returns(@client) - end - - should "search in type does not use the klass setting" do - @client.expects(:search).with do |arguments| - assert_equal 'test', arguments[:index] - assert_equal({foo: 'bar'}, arguments[:body]) - true - end - - subject.search foo: 'bar' - end - - should "search in type based on document_type" do - subject.expects(:document_type).returns('my_special_document').at_least_once - - @client.expects(:search).with do |arguments| - assert_equal 'test', arguments[:index] - assert_equal 'my_special_document', arguments[:type] - assert_equal({foo: 'bar'}, arguments[:body]) - true - end - - subject.search foo: 'bar' - end - - should "search across all types" do - subject.expects(:document_type).returns(nil).at_least_once - - @client.expects(:search).with do |arguments| - assert_equal 'test', arguments[:index] - assert_equal '_all', arguments[:type] - assert_equal({foo: 'bar'}, arguments[:body]) - true - end - - assert_instance_of Elasticsearch::Persistence::Repository::Response::Results, - subject.search({ foo: 'bar' }, type: '_all') - end - - should "pass options to the client" do - @client.expects(:search).twice.with do |arguments| - assert_equal 'bambam', arguments[:routing] - true - 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 - @client.expects(:search).with do |arguments| - assert_equal 'foobar', arguments[:q] - true - end - - assert_instance_of Elasticsearch::Persistence::Repository::Response::Results, - subject.search('foobar') - end - - should "raise error for incorrect search definitions" do - assert_raise ArgumentError do - subject.search 123 - end - end - - should "return the number of domain objects" do - subject.client.expects(:count).returns({'count' => 1}) - assert_equal 1, subject.count - end - - should "pass arguments to count" do - subject.client.expects(:count) - .with do |arguments| - assert_equal 'test', arguments[:index] - assert_equal 'bar', arguments[:body][:query][:match][:foo] - assert_equal true, arguments[:ignore_unavailable] - true - end - .returns({'count' => 1}) - - assert_equal 1, subject.count( { query: { match: { foo: 'bar' } } }, { ignore_unavailable: true } ) - end - end -end diff --git a/elasticsearch-persistence/test/unit/repository_serialize_test.rb b/elasticsearch-persistence/test/unit/repository_serialize_test.rb deleted file mode 100644 index a69145464..000000000 --- a/elasticsearch-persistence/test/unit/repository_serialize_test.rb +++ /dev/null @@ -1,41 +0,0 @@ -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).twice - - MyDocument.expects(:new) - - subject.deserialize( {} ) - end - - should "raise an error when klass isn't set" do - subject.expects(:klass).returns(nil) - - assert_raise(NameError) { subject.deserialize( {} ) } - end - end - end -end diff --git a/elasticsearch-persistence/test/unit/repository_store_test.rb b/elasticsearch-persistence/test/unit/repository_store_test.rb deleted file mode 100644 index b886eecdf..000000000 --- a/elasticsearch-persistence/test/unit/repository_store_test.rb +++ /dev/null @@ -1,219 +0,0 @@ -require 'test_helper' - -class Elasticsearch::Persistence::RepositoryStoreTest < Test::Unit::TestCase - context "The repository store" do - class MyDocument; end - - setup do - @shoulda_subject = Class.new() do - include Elasticsearch::Persistence::Repository::Store - include Elasticsearch::Persistence::Repository::Naming - end.new - @shoulda_subject.stubs(:index_name).returns('test') - end - - context "save" do - - 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(:__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]) - true - 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(:__get_id_from_document).returns('1') - - client = mock - client.expects(:index).with do |arguments| - assert_equal 'my_document', arguments[:type] - assert_equal 'foobarbam', arguments[:index] - assert_equal 'bambam', arguments[:routing] - true - end - subject.expects(:client).returns(client) - - subject.save({foo: 'bar'}, { index: 'foobarbam', routing: 'bambam', type: 'my_document' }) - end - end - - context "update" do - should "get the ID from first argument and :doc from options" do - subject.expects(:serialize).never - subject.expects(:document_type).returns('mydoc') - subject.expects(:__extract_id_from_document).never - - client = mock - client.expects(:update).with do |arguments| - assert_equal '1', arguments[:id] - assert_equal 'mydoc', arguments[:type] - assert_equal({doc: { foo: 'bar' }}, arguments[:body]) - true - end - subject.expects(:client).returns(client) - - subject.update('1', doc: { foo: 'bar' }) - end - - should "get the ID from first argument and :script from options" do - subject.expects(:document_type).returns('mydoc') - subject.expects(:__extract_id_from_document).never - - client = mock - client.expects(:update).with do |arguments| - assert_equal '1', arguments[:id] - assert_equal 'mydoc', arguments[:type] - assert_equal({script: 'ctx._source.foo += 1'}, arguments[:body]) - true - end - subject.expects(:client).returns(client) - - subject.update('1', script: 'ctx._source.foo += 1') - end - - should "get the ID from first argument and :script with :upsert from options" do - subject.expects(:document_type).returns('mydoc') - subject.expects(:__extract_id_from_document).never - - client = mock - client.expects(:update).with do |arguments| - assert_equal '1', arguments[:id] - assert_equal 'mydoc', arguments[:type] - assert_equal({script: 'ctx._source.foo += 1', upsert: { foo: 1 }}, arguments[:body]) - true - end - subject.expects(:client).returns(client) - - subject.update('1', script: 'ctx._source.foo += 1', upsert: { foo: 1 }) - end - - should "get the ID and :doc from document" do - subject.expects(:document_type).returns('mydoc') - - client = mock - client.expects(:update).with do |arguments| - assert_equal '1', arguments[:id] - assert_equal 'mydoc', arguments[:type] - assert_equal({doc: { foo: 'bar' }}, arguments[:body]) - true - end - subject.expects(:client).returns(client) - - subject.update(id: '1', foo: 'bar') - end - - should "get the ID and :script from document" do - subject.expects(:document_type).returns('mydoc') - - client = mock - client.expects(:update).with do |arguments| - assert_equal '1', arguments[:id] - assert_equal 'mydoc', arguments[:type] - assert_equal({script: 'ctx._source.foo += 1'}, arguments[:body]) - true - end - subject.expects(:client).returns(client) - - subject.update(id: '1', script: 'ctx._source.foo += 1') - end - - should "get the ID and :script with :upsert from document" do - subject.expects(:document_type).returns('mydoc') - - client = mock - client.expects(:update).with do |arguments| - assert_equal '1', arguments[:id] - assert_equal 'mydoc', arguments[:type] - assert_equal({script: 'ctx._source.foo += 1', upsert: { foo: 1 } }, arguments[:body]) - true - end - subject.expects(:client).returns(client) - - subject.update(id: '1', script: 'ctx._source.foo += 1', upsert: { foo: 1 }) - end - - should "override the type from params" do - subject.expects(:document_type).never - - client = mock - client.expects(:update).with do |arguments| - assert_equal '1', arguments[:id] - assert_equal 'foo', arguments[:type] - assert_equal({script: 'ctx._source.foo += 1'}, arguments[:body]) - true - end - subject.expects(:client).returns(client) - - subject.update(id: '1', script: 'ctx._source.foo += 1', type: 'foo') - end - - should "raise an exception when passed incorrect argument" do - assert_raise(ArgumentError) { subject.update(MyDocument.new, foo: 'bar') } - end - end - - context "delete" do - - 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(:__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] - true - end - subject.expects(:client).returns(client) - - subject.delete({id: '1', foo: 'bar'}) - end - - should "get ID and uses the default document type" do - subject.expects(:serialize).returns({id: '1', foo: 'bar'}) - subject.expects(:document_type).returns('_doc') - subject.expects(:__get_id_from_document).with({id: '1', foo: 'bar'}).returns('1') - - client = mock - client.expects(:delete).with do |arguments| - assert_equal '_doc', arguments[:type] - assert_equal '1', arguments[:id] - true - end - subject.expects(:client).returns(client) - - subject.delete(MyDocument.new) - end - - should "pass the options to the client" do - client = mock - client.expects(:delete).with do |arguments| - assert_equal 'my_document', arguments[:type] - assert_equal 'foobarbam', arguments[:index] - assert_equal 'bambam', arguments[:routing] - true - end - subject.expects(:client).returns(client) - - subject.delete('1', index: 'foobarbam', routing: 'bambam', type: 'my_document') - end - end - end -end From 8e202105f5a354470afe65271a594d83c19ce115 Mon Sep 17 00:00:00 2001 From: Emily Stolfo Date: Mon, 13 Aug 2018 14:42:48 +0200 Subject: [PATCH 28/87] [STORE] Add missing Repository::Response::Results spec --- .../spec/repository/response/results_spec.rb | 105 ++++++++++++++++++ 1 file changed, 105 insertions(+) create mode 100644 elasticsearch-persistence/spec/repository/response/results_spec.rb diff --git a/elasticsearch-persistence/spec/repository/response/results_spec.rb b/elasticsearch-persistence/spec/repository/response/results_spec.rb new file mode 100644 index 000000000..66a2cc6c0 --- /dev/null +++ b/elasticsearch-persistence/spec/repository/response/results_spec.rb @@ -0,0 +1,105 @@ +require 'spec_helper' + +describe Elasticsearch::Persistence::Repository::Response::Results do + + before(:all) do + class MyRepository + include Elasticsearch::Persistence::Repository + + def deserialize(document) + 'Object' + end + end + end + + let(:repository) do + MyRepository.new + end + + after(:all) do + if defined?(MyRepository) + Object.send(:remove_const, MyRepository.name) + end + end + + let(:response) do + { "took" => 2, + "timed_out" => false, + "_shards" => {"total" => 5, "successful" => 5, "failed" => 0}, + "hits" => + { "total" => 2, + "max_score" => 0.19, + "hits" => + [{"_index" => "my_index", + "_type" => "note", + "_id" => "1", + "_score" => 0.19, + "_source" => {"id" => 1, "title" => "Test 1"}}, + + {"_index" => "my_index", + "_type" => "note", + "_id" => "2", + "_score" => 0.19, + "_source" => {"id" => 2, "title" => "Test 2"}} + ] + } + } + end + + let(:results) do + described_class.new(repository, response) + end + + describe '#repository' do + + it 'should return the repository' do + expect(results.repository).to be(repository) + end + end + + describe '#response' do + + it 'returns the response' do + expect(results.response).to eq(response) + end + + it 'wraps the response in a HashWrapper' do + expect(results.response._shards.total).to eq(5) + end + end + + describe '#total' do + + it 'returns the total' do + expect(results.total).to eq(2) + end + end + + describe '#max_score' do + + it 'returns the max score' do + expect(results.max_score).to eq(0.19) + end + end + + describe '#each' do + + it 'delegates the method to the results' do + expect(results.size).to eq(2) + end + end + + describe '#each_with_hit' do + + it 'returns each deserialized object with the raw document' do + expect(results.each_with_hit { |pair| pair[0] = 'Obj'}).to eq(['Obj', 'Obj'].zip(response['hits']['hits'])) + end + end + + describe '#map_with_hit' do + + it 'returns the result of the block called on a pair of each raw document and the deserialized object' do + expect(results.map_with_hit { |pair| pair[0] }).to eq(['Object', 'Object']) + end + end +end From e3316f2d9d65c6f66e37e78a2dbb244895887741 Mon Sep 17 00:00:00 2001 From: Emily Stolfo Date: Mon, 13 Aug 2018 15:06:04 +0200 Subject: [PATCH 29/87] Update versions to .pre --- elasticsearch-model/lib/elasticsearch/model/version.rb | 2 +- elasticsearch-persistence/elasticsearch-persistence.gemspec | 2 +- .../lib/elasticsearch/persistence/version.rb | 2 +- elasticsearch-rails/lib/elasticsearch/rails/version.rb | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/elasticsearch-model/lib/elasticsearch/model/version.rb b/elasticsearch-model/lib/elasticsearch/model/version.rb index 59b1306cf..c7ad15f56 100644 --- a/elasticsearch-model/lib/elasticsearch/model/version.rb +++ b/elasticsearch-model/lib/elasticsearch/model/version.rb @@ -1,5 +1,5 @@ module Elasticsearch module Model - VERSION = "6.0.0" + VERSION = '6.0.0.pre' end end diff --git a/elasticsearch-persistence/elasticsearch-persistence.gemspec b/elasticsearch-persistence/elasticsearch-persistence.gemspec index 775c3b9e5..9b171ec44 100644 --- a/elasticsearch-persistence/elasticsearch-persistence.gemspec +++ b/elasticsearch-persistence/elasticsearch-persistence.gemspec @@ -24,7 +24,7 @@ Gem::Specification.new do |s| s.required_ruby_version = ">= 1.9.3" s.add_dependency "elasticsearch", '~> 6' - s.add_dependency "elasticsearch-model", '~> 6' + s.add_dependency "elasticsearch-model", '>= 5' s.add_dependency "activesupport", '> 4' s.add_dependency "activemodel", '> 4' s.add_dependency "hashie" diff --git a/elasticsearch-persistence/lib/elasticsearch/persistence/version.rb b/elasticsearch-persistence/lib/elasticsearch/persistence/version.rb index fea31b99c..f455335e9 100644 --- a/elasticsearch-persistence/lib/elasticsearch/persistence/version.rb +++ b/elasticsearch-persistence/lib/elasticsearch/persistence/version.rb @@ -1,5 +1,5 @@ module Elasticsearch module Persistence - VERSION = '6.0.0' + VERSION = '6.0.0.pre' end end diff --git a/elasticsearch-rails/lib/elasticsearch/rails/version.rb b/elasticsearch-rails/lib/elasticsearch/rails/version.rb index c1acc4629..1a57f56ad 100644 --- a/elasticsearch-rails/lib/elasticsearch/rails/version.rb +++ b/elasticsearch-rails/lib/elasticsearch/rails/version.rb @@ -1,5 +1,5 @@ module Elasticsearch module Rails - VERSION = "6.0.0" + VERSION = '6.0.0.pre' end end From 868a7120bbbb552ada3c2200029286633e78e858 Mon Sep 17 00:00:00 2001 From: Emily Stolfo Date: Mon, 13 Aug 2018 17:06:32 +0200 Subject: [PATCH 30/87] [STORE] Update README for Repository mixin refactor --- elasticsearch-persistence/README.md | 225 ++++++++++++++++++++-------- 1 file changed, 162 insertions(+), 63 deletions(-) diff --git a/elasticsearch-persistence/README.md b/elasticsearch-persistence/README.md index e95a7f005..d933c9519 100644 --- a/elasticsearch-persistence/README.md +++ b/elasticsearch-persistence/README.md @@ -1,6 +1,6 @@ # Elasticsearch::Persistence -Persistence layer for Ruby domain objects in Elasticsearch, using the Repository and ActiveRecord patterns. +Persistence layer for Ruby domain objects in Elasticsearch, using the Repository pattern. ## Compatibility @@ -14,6 +14,7 @@ is compatible with the Elasticsearch `master` branch, therefore, with the next m | 0.1 | → | 1.x | | 2.x | → | 2.x | | 5.x | → | 5.x | +| 6.x | → | 6.x | | master | → | master | ## Installation @@ -24,7 +25,7 @@ Install the package from [Rubygems](https://rubygems.org): To use an unreleased version, either add it to your `Gemfile` for [Bundler](http://bundler.io): - gem 'elasticsearch-persistence', git: 'git://github.com/elastic/elasticsearch-rails.git', branch: '5.x' + gem 'elasticsearch-persistence', git: 'git://github.com/elastic/elasticsearch-rails.git', branch: '6.x' or install it from a source code checkout: @@ -35,9 +36,7 @@ or install it from a source code checkout: ## Usage -The library provides two different patterns for adding persistence to your Ruby objects: - -* [Repository Pattern](#the-repository-pattern) +The library provides the Repository pattern for adding persistence to your Ruby objects. ### The Repository Pattern @@ -67,7 +66,8 @@ Let's create a default, "dumb" repository, as a first step: ```ruby require 'elasticsearch/persistence' -repository = Elasticsearch::Persistence::Repository.new +class MyRepository; include Elasticsearch::Persistence::Repository; end +repository = MyRepository.new ``` We can save a `Note` instance into the repository... @@ -120,32 +120,17 @@ The repository module provides a number of features and facilities to configure * 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. +There are two mixins you can include in your Repository class. The first `Elasticsearch::Persistence::Repository`, +provides the basic methods and settings you'll need. The second, `Elasticsearch::Persistence::Repository::DSL` adds +some additional class methods that allow you to set options that instances of the class will share. -#### The Default Class +#### Basic Repository mixin -For simple cases, you can use the default, bundled repository class, and configure/customize it: +For simple cases, you can just include the Elasticsearch::Persistence::Repository mixin to your class: ```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 initialize 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 +class MyRepository + include Elasticsearch::Persistence::Repository # Customize the serialization logic def serialize(document) @@ -154,10 +139,18 @@ repository = Elasticsearch::Persistence::Repository.new do # Customize the de-serialization logic def deserialize(document) - puts "# ***** CUSTOM DESERIALIZE LOGIC KICKING IN... *****" + puts "# ***** CUSTOM DESERIALIZE LOGIC... *****" super end end + +client = Elasticsearch::Client.new(url: ENV['ELASTICSEARCH_URL'], log: true) +repository = MyRepository.new(client: client, index_name: :my_notes, type: :my_note, klass: Note) +repository.settings number_of_shards: 1 do + mapping do + indexes :text, analyzer: 'snowball' + end +end ``` The custom Elasticsearch client will be used now, with a custom index and type names, @@ -188,28 +181,27 @@ repository.find(1) "my_special_stuff"}> ``` -#### A Custom Class +#### The DSL mixin -In most cases, though, you'll want to use a custom class for the repository, so let's do that: +In some cases, you'll want to set some of the repository configurations at the class level. This makes +most sense when the instances of the repository will use that same configuration: ```ruby require 'base64' class NoteRepository include Elasticsearch::Persistence::Repository + include Elasticsearch::Persistence::Repository::DSL - def initialize(options={}) - index options[:index] || 'notes' - client Elasticsearch::Client.new url: options[:url], log: options[:log] - end - + index_name 'notes' + document_type 'note' klass Note settings number_of_shards: 1 do mapping do indexes :text, analyzer: 'snowball' # Do not index images - indexes :image, index: 'no' + indexes :image, index: false end end @@ -231,23 +223,26 @@ class NoteRepository end ``` -Include the `Elasticsearch::Persistence::Repository` module to add the repository methods into the class. +You can create an instance of this custom class and get each of the configurations. -You can customize the repository in the familiar way, by calling the DSL-like methods. +```ruby +client = Elasticsearch::Client.new(url: 'http://localhost:9200/', log: true) +repository = NoteRepository.new(client: client) +repository.index_name +# => 'notes' -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. +``` + +You can also override the default configuration with options passed to the initialize method: ```ruby -repository = NoteRepository.new url: 'http://localhost:9200/', log: true +client = Elasticsearch::Client.new(url: 'http://localhost:9250/', log: true) +client.transport.logger.formatter = proc { |s, d, p, m| "\e[2m# #{m}\n\e[0m" } +repository = NoteRepository.new(client: client, index_name: 'notes_development') -# 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) -repository.create_index! force: true - -note = Note.new 'id' => 1, 'text' => 'Document with image', 'image' => '... BINARY DATA ...' +note = Note.new('id' => 1, 'text' => 'Document with image', 'image' => '... BINARY DATA ...') repository.save(note) # PUT http://localhost:9200/notes_development/note/1 @@ -258,47 +253,110 @@ puts repository.find(1).attributes['image'] # => ... BINARY DATA ... ``` -#### Methods Provided by the Repository +#### Functionality Provided by the Repository mixin + +Each of the following configurations can be set for a repository instance. +If you have included the `Elasticsearch::Persistence::Repository::DSL` mixin, then you can use the class-level DSL +methods to set each configuration. You can override the configuration for any instance by passing options to the +`#initialize` method. +If you don't use the DSL mixin, you can set also the instance configuration with options passed the `#initialize` method. ##### Client -The repository uses the standard Elasticsearch [client](https://github.com/elastic/elasticsearch-ruby#usage), -which is accessible with the `client` getter and setter methods: +The repository uses the standard Elasticsearch [client](https://github.com/elastic/elasticsearch-ruby#usage). ```ruby -repository.client = Elasticsearch::Client.new url: 'http://search.server.org/' +client = Elasticsearch::Client.new(url: 'http://search.server.org/') +repository = NoteRepository.new(client: client) repository.client.transport.logger = Logger.new(STDERR) ``` +or with the DSL mixin: + +```ruby +class NoteRepository + include Elasticsearch::Persistence::Repository + include Elasticsearch::Persistence::Repository::DSL + + client Elasticsearch::Client.new url: 'http://search.server.org/' +end + +repository = NoteRepository.new + +``` + ##### 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): +The `index_name` method specifies the Elasticsearch index to use for storage, lookup and search. The default index name +is 'repository'. + +```ruby +repository = NoteRepository.new(index_name: 'notes_development') +``` + +or with the DSL mixin: + +```ruby +class NoteRepository + include Elasticsearch::Persistence::Repository + include Elasticsearch::Persistence::Repository::DSL + + index_name 'notes_development' +end + +repository = NoteRepository.new + +``` + +The `type` method specifies the Elasticsearch document type to use for storage, lookup and search. The default value is +'_doc'. Keep in mind that future versions of Elasticsearch will not allow you to set this yourself and will use the type, +'_doc'. ```ruby -repository.index = 'notes_development' +repository = NoteRepository.new(document_type: 'note') ``` -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): +or with the DSL mixin: ```ruby -repository.type = 'my_note' +class NoteRepository + include Elasticsearch::Persistence::Repository + include Elasticsearch::Persistence::Repository::DSL + + document_type 'note' +end + +repository = NoteRepository.new + ``` 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): +documents retrieved from the repository. If this value is not set, a Hash representation of the document will be +returned instead. + +```ruby +repository = NoteRepository.new(klass: Note) +``` + +or with the DSL mixin: ```ruby -repository.klass = MyNote +class NoteRepository + include Elasticsearch::Persistence::Repository + include Elasticsearch::Persistence::Repository::DSL + + klass Note +end + +repository = NoteRepository.new + ``` ##### 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: +gem, allow you to configure the index properties: ```ruby repository.settings number_of_shards: 1 @@ -310,7 +368,39 @@ repository.mappings.to_hash # => { :note => {:properties=> ... }} ``` +or with the DSL mixin: + +```ruby +class NoteRepository + include Elasticsearch::Persistence::Repository + include Elasticsearch::Persistence::Repository::DSL + + mappings { indexes :title, analyzer: 'snowball' } + settings number_of_shards: 1 +end + +repository = NoteRepository.new + +``` + +You can also use the `#create` method defined on the repository class to create and set the mappings and settings +on an instance with a block in one call: + +```ruby +repository = NoteRepository.create(index_name: 'notes_development') do + settings number_of_shards: 1, number_of_replicas: 0 do + mapping dynamic: 'strict' do + indexes :foo do + indexes :bar + end + indexes :baz + end + end +end +``` + The convenience methods `create_index!`, `delete_index!` and `refresh_index!` allow you to manage the index lifecycle. +These methods can only be called on repository instances and are not implemented at the class level. ##### Serialization @@ -319,9 +409,12 @@ to the storage, and the initialization procedure when loading it from the storag ```ruby class NoteRepository + include Elasticsearch::Persistence::Repository + 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 @@ -426,9 +519,15 @@ end results.total # => 2 -# Access the raw response as a Hashie::Mash instance +# Access the raw response as a Hashie::Mash instance. +# Note that a Hashie::Mash will only be created if the 'response' method is called on the results. results.response._shards.failed # => 0 + +# Access the raw response +results.raw_response +# => 0 + ``` #### Example Application From 69fa50a77e6c6bb09f5f382bc24241639523da60 Mon Sep 17 00:00:00 2001 From: Emily Stolfo Date: Mon, 13 Aug 2018 17:13:56 +0200 Subject: [PATCH 31/87] [STORE] Minor typo in README --- elasticsearch-persistence/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/elasticsearch-persistence/README.md b/elasticsearch-persistence/README.md index d933c9519..637313d1f 100644 --- a/elasticsearch-persistence/README.md +++ b/elasticsearch-persistence/README.md @@ -526,7 +526,7 @@ results.response._shards.failed # Access the raw response results.raw_response -# => 0 +# => {...} ``` From 5d7c43c780765e7f3149fd67bf7bae91cf11317c Mon Sep 17 00:00:00 2001 From: Emily Stolfo Date: Mon, 13 Aug 2018 17:21:46 +0200 Subject: [PATCH 32/87] [STORE] Add #inspect method for Repository --- .../lib/elasticsearch/persistence/repository.rb | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/elasticsearch-persistence/lib/elasticsearch/persistence/repository.rb b/elasticsearch-persistence/lib/elasticsearch/persistence/repository.rb index 33cbdc2c4..6aec19951 100644 --- a/elasticsearch-persistence/lib/elasticsearch/persistence/repository.rb +++ b/elasticsearch-persistence/lib/elasticsearch/persistence/repository.rb @@ -214,6 +214,18 @@ def index_exists?(*args) super(index_name: index_name) end + # Get the nicer formatted string for use in inspection. + # + # @example Inspect the repository. + # repository.inspect + # + # @return [ String ] The repository inspection. + # + # @since 6.0.0 + def inspect + "#<#{self.class}:0x#{object_id} index_name=#{index_name} document_type=#{document_type} klass=#{klass}>" + end + private def __get_class_value(_method_) From b26f1010d9c7f0149581c08341252c55ecd381a9 Mon Sep 17 00:00:00 2001 From: Emily Stolfo Date: Mon, 13 Aug 2018 17:28:22 +0200 Subject: [PATCH 33/87] [STORE] Update references to Elasticsearch::Client --- .../lib/elasticsearch/persistence/repository.rb | 4 ++-- .../lib/elasticsearch/persistence/repository/dsl.rb | 2 +- elasticsearch-persistence/spec/repository_spec.rb | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/elasticsearch-persistence/lib/elasticsearch/persistence/repository.rb b/elasticsearch-persistence/lib/elasticsearch/persistence/repository.rb index 6aec19951..23e34c3e5 100644 --- a/elasticsearch-persistence/lib/elasticsearch/persistence/repository.rb +++ b/elasticsearch-persistence/lib/elasticsearch/persistence/repository.rb @@ -105,13 +105,13 @@ def initialize(options = {}) # @example # repository.client # - # @return [ Elasticsearch::Transport::Client ] The repository's client. + # @return [ Elasticsearch::Client ] The repository's client. # # @since 6.0.0 def client @client ||= @options[:client] || __get_class_value(:client) || - Elasticsearch::Transport::Client.new + Elasticsearch::Client.new end # Get the document type used by the repository object. diff --git a/elasticsearch-persistence/lib/elasticsearch/persistence/repository/dsl.rb b/elasticsearch-persistence/lib/elasticsearch/persistence/repository/dsl.rb index 0c880d7ec..f997e63ce 100644 --- a/elasticsearch-persistence/lib/elasticsearch/persistence/repository/dsl.rb +++ b/elasticsearch-persistence/lib/elasticsearch/persistence/repository/dsl.rb @@ -63,7 +63,7 @@ def klass(_class = nil) # # @since 6.0.0 def client(_client = nil) - @client ||= (_client || Elasticsearch::Transport::Client.new) + @client ||= (_client || Elasticsearch::Client.new) end def create_index!(*args) diff --git a/elasticsearch-persistence/spec/repository_spec.rb b/elasticsearch-persistence/spec/repository_spec.rb index 2d2d4d4ca..292eb5d06 100644 --- a/elasticsearch-persistence/spec/repository_spec.rb +++ b/elasticsearch-persistence/spec/repository_spec.rb @@ -106,7 +106,7 @@ class RepositoryWithoutDSL context 'when options are provided' do let(:client) do - Elasticsearch::Transport::Client.new + Elasticsearch::Client.new end let(:repository) do From 9d35cc4d879a066e6d813e4f5496d7c36322e086 Mon Sep 17 00:00:00 2001 From: Emily Stolfo Date: Mon, 13 Aug 2018 17:57:24 +0200 Subject: [PATCH 34/87] Release 6.0.0.pre --- CHANGELOG.md | 56 ++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 56 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index d81c0e69e..752ae970d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,59 @@ +## 6.0.0.pre + +* Added the "Compatibility" chapter to the READMEs +* Updated the Bundler instructions and Github URLs in the READMEs +* Updated the version on the `master` branch to `6.0.0.alpha1` +* Update versions to 6.0.0.beta +* minor: Fix spacing +* Update various gemspecs to conditionally depend on gems incompatible with JRuby (#810) +* Update versions +* Use local as source for gem dependencies when possible +* Only require 'oj' gem if not using JRuby +* Update versions to .pre + +### ActiveModel + +* Added an example with a custom "pattern" analyzer +* Added a "trigram" custom analyzer to the example +* Fix README typo (s/situation/situations) +* Fix reference to @ids in example and README +* Add Callbacks to the example datamapper adapter +* Fix `Asynchronous Callbacks` example +* Fixed a typo in the README +* Improved the custom analyzer example +* Removed left-overs from previous implementation in the "completion suggester" example +* Updated the `changes` method name in `Indexing` to `changes_to_save` for compatibility with Rails 5.1 +* Fixed the handling of changed attributes in `Indexing` to work with older Rails versions +* Update child-parent integration test to use single index type for ES 6.3 (#805) +* Use default doc type: _doc (#814) +* Avoid making an update when no attributes are changed (#762) + +### Persistence + +* Updated the failing integration tests for Elasticsearch 5.x +* Updated the dependency for "elasticsearch" and "elasticsearch-model" to `5.x` +* Documentation for Model should include Model and not Repository +* Depend on version >= 6 of elasticsearch gems +* Undo last commit; depend on version 5 of elasticsearch gems +* Reduce repeated string instantiation (#813) +* Make default doc type '_doc' in preparation for deprecation of mapping types (#816) +* Remove Elasticsearch::Persistence::Model (ActiveRecord persistence pattern) (#812) +* Deprecate _all field in ES 6.x (#820) +* Remove development dependency on virtus, include explicitly in Gemfile for integration test +* Refactor Repository as mixin (#824) +* Add missing Repository::Response::Results spec +* Update README for Repository mixin refactor +* Minor typo in README +* Add #inspect method for Repository +* Update references to Elasticsearch::Client + +### Ruby on Rails + +* Fixed typo in README +* Fix typo in rake import task +* Updated the templates for example Rails applications +* Add 'oj' back as a development dependency in gemspec + ## 6.0.0.alpha1 * Updated the Rake dependency to 11.1 From 784c0bf4bd7ba7f684c2ae0ec6a30f55a140c448 Mon Sep 17 00:00:00 2001 From: Emily Stolfo Date: Mon, 13 Aug 2018 18:02:22 +0200 Subject: [PATCH 35/87] [RAILS] Add rspec to Gemfile --- Gemfile | 1 + 1 file changed, 1 insertion(+) diff --git a/Gemfile b/Gemfile index 5cb504908..536ee1c11 100644 --- a/Gemfile +++ b/Gemfile @@ -11,4 +11,5 @@ gem "cane" group :development do gem 'yard' + gem 'rspec' end From 2e9d57f61a5f50b06e458f8c742911fa25dab42c Mon Sep 17 00:00:00 2001 From: Emily Stolfo Date: Wed, 15 Aug 2018 15:55:27 +0200 Subject: [PATCH 36/87] [STORE] Minor refactor in Repository::Search --- .../persistence/repository/search.rb | 35 ++++++++++--------- 1 file changed, 18 insertions(+), 17 deletions(-) diff --git a/elasticsearch-persistence/lib/elasticsearch/persistence/repository/search.rb b/elasticsearch-persistence/lib/elasticsearch/persistence/repository/search.rb index 203a7e246..c3893d74f 100644 --- a/elasticsearch-persistence/lib/elasticsearch/persistence/repository/search.rb +++ b/elasticsearch-persistence/lib/elasticsearch/persistence/repository/search.rb @@ -43,18 +43,18 @@ module Search # @return [Elasticsearch::Persistence::Repository::Response::Results] # def search(query_or_definition, options={}) - type = document_type - - 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) ) - when query_or_definition.is_a?(String) - response = client.search( { index: index_name, type: type, q: query_or_definition }.merge(options) ) + request = { index: index_name, + type: document_type } + if query_or_definition.respond_to?(:to_hash) + request[:body] = query_or_definition.to_hash + elsif query_or_definition.is_a?(String) + request[:q] = query_or_definition else raise ArgumentError, "[!] Pass the search definition as a Hash-like object or pass the query as a String" + - " -- #{query_or_definition.class} given." + " -- #{query_or_definition.class} given." end - Response::Results.new(self, response) + + Response::Results.new(self, client.search(request.merge(options))) end # Return the number of domain object in the index @@ -81,18 +81,19 @@ def search(query_or_definition, options={}) # def count(query_or_definition=nil, options={}) query_or_definition ||= { query: { match_all: {} } } - type = document_type + request = { index: index_name, + type: document_type } - case - when query_or_definition.respond_to?(:to_hash) - response = client.count( { index: index_name, type: type, body: query_or_definition.to_hash }.merge(options) ) - when query_or_definition.is_a?(String) - response = client.count( { index: index_name, type: type, q: query_or_definition }.merge(options) ) + if query_or_definition.respond_to?(:to_hash) + request[:body] = query_or_definition.to_hash + elsif query_or_definition.is_a?(String) + request[:q] = query_or_definition else - raise ArgumentError, "[!] Pass the search definition as a Hash-like object or pass the query as a String, not as [#{query_or_definition.class}]" + 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[COUNT] + client.count(request.merge(options))[COUNT] end private From ffbcca1624c35d2e427706c39f5e186f05c3a2dd Mon Sep 17 00:00:00 2001 From: Emily Stolfo Date: Wed, 15 Aug 2018 16:06:39 +0200 Subject: [PATCH 37/87] [STORE] Remove example music app that demonstrates the AR pattern --- .../examples/music/album.rb | 54 --- .../examples/music/artist.rb | 70 --- .../examples/music/artists/_form.html.erb | 8 - .../music/artists/artists_controller.rb | 67 --- .../music/artists/artists_controller_test.rb | 53 --- .../examples/music/artists/index.html.erb | 60 --- .../examples/music/artists/show.html.erb | 54 --- .../examples/music/assets/application.css | 257 ----------- .../examples/music/assets/autocomplete.css | 48 -- .../examples/music/assets/blank_artist.png | Bin 30857 -> 0 bytes .../examples/music/assets/blank_cover.png | Bin 22778 -> 0 bytes .../examples/music/assets/form.css | 113 ----- .../examples/music/index_manager.rb | 73 --- .../examples/music/search/index.html.erb | 95 ---- .../music/search/search_controller.rb | 41 -- .../music/search/search_controller_test.rb | 12 - .../examples/music/search/search_helper.rb | 15 - .../examples/music/suggester.rb | 69 --- .../examples/music/template.rb | 430 ------------------ .../assets/jquery-ui-1.10.4.custom.min.css | 7 - .../assets/jquery-ui-1.10.4.custom.min.js | 6 - .../ui-bg_highlight-soft_100_eeeeee_1x100.png | Bin 90 -> 0 bytes 22 files changed, 1532 deletions(-) delete mode 100644 elasticsearch-persistence/examples/music/album.rb delete mode 100644 elasticsearch-persistence/examples/music/artist.rb delete mode 100644 elasticsearch-persistence/examples/music/artists/_form.html.erb delete mode 100644 elasticsearch-persistence/examples/music/artists/artists_controller.rb delete mode 100644 elasticsearch-persistence/examples/music/artists/artists_controller_test.rb delete mode 100644 elasticsearch-persistence/examples/music/artists/index.html.erb delete mode 100644 elasticsearch-persistence/examples/music/artists/show.html.erb delete mode 100644 elasticsearch-persistence/examples/music/assets/application.css delete mode 100644 elasticsearch-persistence/examples/music/assets/autocomplete.css delete mode 100644 elasticsearch-persistence/examples/music/assets/blank_artist.png delete mode 100644 elasticsearch-persistence/examples/music/assets/blank_cover.png delete mode 100644 elasticsearch-persistence/examples/music/assets/form.css delete mode 100644 elasticsearch-persistence/examples/music/index_manager.rb delete mode 100644 elasticsearch-persistence/examples/music/search/index.html.erb delete mode 100644 elasticsearch-persistence/examples/music/search/search_controller.rb delete mode 100644 elasticsearch-persistence/examples/music/search/search_controller_test.rb delete mode 100644 elasticsearch-persistence/examples/music/search/search_helper.rb delete mode 100644 elasticsearch-persistence/examples/music/suggester.rb delete mode 100644 elasticsearch-persistence/examples/music/template.rb delete mode 100755 elasticsearch-persistence/examples/music/vendor/assets/jquery-ui-1.10.4.custom.min.css delete mode 100755 elasticsearch-persistence/examples/music/vendor/assets/jquery-ui-1.10.4.custom.min.js delete mode 100644 elasticsearch-persistence/examples/music/vendor/assets/stylesheets/ui-lightness/images/ui-bg_highlight-soft_100_eeeeee_1x100.png diff --git a/elasticsearch-persistence/examples/music/album.rb b/elasticsearch-persistence/examples/music/album.rb deleted file mode 100644 index a805a2111..000000000 --- a/elasticsearch-persistence/examples/music/album.rb +++ /dev/null @@ -1,54 +0,0 @@ -class Meta - include Virtus.model - - attribute :rating - attribute :have - attribute :want - attribute :formats -end - -class Album - include Elasticsearch::Persistence::Model - - index_name [Rails.application.engine_name, Rails.env].join('-') - - - mapping _parent: { type: 'artist' } do - end - - attribute :artist - attribute :artist_id, String, mapping: { index: 'not_analyzed' } - attribute :label, Hash, mapping: { type: 'object' } - - attribute :title - attribute :released, Date - attribute :notes - attribute :uri - - attribute :tracklist, Array, mapping: { type: 'object' } - - attribute :styles - attribute :meta, Meta, mapping: { type: 'object' } - - attribute :suggest, Hashie::Mash, mapping: { - type: 'object', - properties: { - title: { - type: 'object', - properties: { - input: { type: 'completion' }, - output: { type: 'keyword', index: false }, - payload: { type: 'object', enabled: false } - } - }, - track: { - type: 'object', - properties: { - input: { type: 'completion' }, - output: { type: 'keyword', index: false }, - payload: { type: 'object', enabled: false } - } - } - } - } -end diff --git a/elasticsearch-persistence/examples/music/artist.rb b/elasticsearch-persistence/examples/music/artist.rb deleted file mode 100644 index 17c525619..000000000 --- a/elasticsearch-persistence/examples/music/artist.rb +++ /dev/null @@ -1,70 +0,0 @@ -class Artist - include Elasticsearch::Persistence::Model - - index_name [Rails.application.engine_name, Rails.env].join('-') - - analyzed_and_raw = { fields: { - name: { type: 'text', analyzer: 'snowball' }, - raw: { type: 'keyword' } - } } - - attribute :name, String, mapping: analyzed_and_raw - - attribute :profile - attribute :date, Date - - attribute :members, String, default: [], mapping: analyzed_and_raw - attribute :members_combined, String, default: [], mapping: { analyzer: 'snowball' } - - attribute :urls, String, default: [] - attribute :album_count, Integer, default: 0 - - attribute :suggest, Hashie::Mash, mapping: { - type: 'object', - properties: { - name: { - type: 'object', - properties: { - input: { type: 'completion' }, - output: { type: 'keyword', index: false }, - payload: { type: 'object', enabled: false } - } - }, - member: { - type: 'object', - properties: { - input: { type: 'completion' }, - output: { type: 'keyword', index: false }, - payload: { type: 'object', enabled: false } - } - } - } - } - - validates :name, presence: true - - def albums - Album.search( - { query: { - has_parent: { - type: 'artist', - query: { - bool: { - filter: { - ids: { values: [ self.id ] } - } - } - } - } - }, - sort: 'released', - size: 100 - }, - { type: 'album' } - ) - end - - def to_param - [id, name.parameterize].join('-') - end -end diff --git a/elasticsearch-persistence/examples/music/artists/_form.html.erb b/elasticsearch-persistence/examples/music/artists/_form.html.erb deleted file mode 100644 index 55273679c..000000000 --- a/elasticsearch-persistence/examples/music/artists/_form.html.erb +++ /dev/null @@ -1,8 +0,0 @@ -<%= simple_form_for @artist do |f| %> - <%= f.input :name %> - <%= f.input :profile, as: :text %> - <%= f.input :date, as: :date %> - <%= f.input :members, hint: 'Separate names by comma', input_html: { value: f.object.members.join(', ') } %> - - <%= f.button :submit %> -<% end %> diff --git a/elasticsearch-persistence/examples/music/artists/artists_controller.rb b/elasticsearch-persistence/examples/music/artists/artists_controller.rb deleted file mode 100644 index 458c243f7..000000000 --- a/elasticsearch-persistence/examples/music/artists/artists_controller.rb +++ /dev/null @@ -1,67 +0,0 @@ -class ArtistsController < ApplicationController - before_action :set_artist, only: [:show, :edit, :update, :destroy] - - rescue_from Elasticsearch::Persistence::Repository::DocumentNotFound do - render file: "public/404.html", status: 404, layout: false - end - - def index - @artists = Artist.all sort: 'name.raw', _source: ['name', 'album_count'] - end - - def show - @albums = @artist.albums - end - - def new - @artist = Artist.new - end - - def edit - end - - def create - @artist = Artist.new(artist_params) - - respond_to do |format| - if @artist.save refresh: true - format.html { redirect_to @artist, notice: 'Artist was successfully created.' } - format.json { render :show, status: :created, location: @artist } - else - format.html { render :new } - format.json { render json: @artist.errors, status: :unprocessable_entity } - end - end - end - - def update - respond_to do |format| - if @artist.update(artist_params, refresh: true) - format.html { redirect_to @artist, notice: 'Artist was successfully updated.' } - format.json { render :show, status: :ok, location: @artist } - else - format.html { render :edit } - format.json { render json: @artist.errors, status: :unprocessable_entity } - end - end - end - - def destroy - @artist.destroy refresh: true - respond_to do |format| - format.html { redirect_to artists_url, notice: 'Artist was successfully destroyed.' } - format.json { head :no_content } - end - end - - private - def set_artist - @artist = Artist.find(params[:id].split('-').first) - end - - def artist_params - a = params.require(:artist) - a[:members] = a[:members].split(/,\s?/) unless a[:members].is_a?(Array) || a[:members].blank? - return a - end -end diff --git a/elasticsearch-persistence/examples/music/artists/artists_controller_test.rb b/elasticsearch-persistence/examples/music/artists/artists_controller_test.rb deleted file mode 100644 index 3307f5e47..000000000 --- a/elasticsearch-persistence/examples/music/artists/artists_controller_test.rb +++ /dev/null @@ -1,53 +0,0 @@ -require 'test_helper' - -class ArtistsControllerTest < ActionController::TestCase - setup do - IndexManager.create_index force: true - @artist = Artist.create(id: 1, name: 'TEST') - Artist.gateway.refresh_index! - end - - test "should get index" do - get :index - assert_response :success - assert_not_nil assigns(:artists) - end - - test "should get new" do - get :new - assert_response :success - end - - test "should create artist" do - assert_difference('Artist.count') do - post :create, artist: { name: @artist.name } - Artist.gateway.refresh_index! - end - - assert_redirected_to artist_path(assigns(:artist)) - end - - test "should show artist" do - get :show, id: @artist - assert_response :success - end - - test "should get edit" do - get :edit, id: @artist - assert_response :success - end - - test "should update artist" do - patch :update, id: @artist, artist: { name: @artist.name } - assert_redirected_to artist_path(assigns(:artist)) - end - - test "should destroy artist" do - assert_difference('Artist.count', -1) do - delete :destroy, id: @artist - Artist.gateway.refresh_index! - end - - assert_redirected_to artists_path - end -end diff --git a/elasticsearch-persistence/examples/music/artists/index.html.erb b/elasticsearch-persistence/examples/music/artists/index.html.erb deleted file mode 100644 index 6747b3056..000000000 --- a/elasticsearch-persistence/examples/music/artists/index.html.erb +++ /dev/null @@ -1,60 +0,0 @@ -
-

- Artists - <%= button_to 'New Artist', new_artist_path, method: 'get', tabindex: 5 %> -

-
- - - -
- <% @artists.each do |artist| %> - <%= div_for artist, class: 'result clearfix' do %> -

- <%= image_tag "http://ruby.elastic.co.s3-website-us-east-1.amazonaws.com/demo/music/bands/#{artist.id}.jpeg", height: '50px', class: 'band' %> - <%= link_to artist do %> - <%= artist.name %> - <%= pluralize artist.album_count, 'album' %> - <% end %> -

-
- <%= button_to 'Edit', edit_artist_path(artist), method: 'get' %> - <%= button_to 'Destroy', artist, method: :delete, data: { confirm: 'Are you sure?' } %> -
- <% end %> - <% end %> -
- -<% if @artists.empty? %> -
-

The search hasn't returned any results...

-
-<% end %> - - - - diff --git a/elasticsearch-persistence/examples/music/artists/show.html.erb b/elasticsearch-persistence/examples/music/artists/show.html.erb deleted file mode 100644 index e1a9cdf01..000000000 --- a/elasticsearch-persistence/examples/music/artists/show.html.erb +++ /dev/null @@ -1,54 +0,0 @@ -
-
-

- <%= link_to "〈".html_safe, artists_path, title: "Back" %> - <%= image_tag "http://ruby.elastic.co.s3-website-us-east-1.amazonaws.com/demo/music/bands/#{@artist.id}.jpeg", height: '50px', class: 'band' %> - <%= @artist.name %> - <%= button_to 'Edit', edit_artist_path(@artist), method: 'get' %> -

-
- -

<%= notice %>

- -
- <%= @artist.members.to_sentence last_word_connector: ' and ' %> | - <%= pluralize @albums.size, 'album' %> -

<%= @artist.profile %>

-
- -
- <% @albums.each do |album| %> - <%= div_for album, class: 'clearfix' do %> -

- <%= album.title %> -
- <%= album.meta.formats.join(', ') %> - <%= album.released %> -
-

- -
- <%= image_tag "http://ruby.elastic.co.s3-website-us-east-1.amazonaws.com/demo/music/covers/#{album.id}.jpeg", width: '100px', class: 'cover' %> -
- -
- <% album.tracklist.in_groups_of(album.tracklist.size/2+1).each_with_index do |half, g| %> -
    start="<%= g < 1 ? 1 : album.tracklist.size/2+2 %>"> - <% half.compact.each_with_index do |track, i| %> -
  • - <%= g < 1 ? i+1 : i+(g*album.tracklist.size/2+2) %> - <%= track['title'] %> - <%= track['duration'] %> -
  • - <% end %> -
- <% end %> -
- <% end %> - - <% end %> - - - -
-
diff --git a/elasticsearch-persistence/examples/music/assets/application.css b/elasticsearch-persistence/examples/music/assets/application.css deleted file mode 100644 index 816ebff8c..000000000 --- a/elasticsearch-persistence/examples/music/assets/application.css +++ /dev/null @@ -1,257 +0,0 @@ -/* - *= require_tree . - *= require_self - *= require ui-lightness/jquery-ui-1.10.4.custom.min.css - */ - -.clearfix { - *zoom: 1; -} - -.clearfix:before, -.clearfix:after { - display: table; - line-height: 0; - content: ""; -} - -.clearfix:after { - clear: both; -} - -body { - font-family: 'Helvetica Neue', Helvetica, sans-serif !important; - margin: 2em 4em; -} - -header { - margin: 0; - padding: 0 0 1em 0; - border-bottom: 1px solid #666; -} - -header h1 { - color: #999; - font-weight: 100; - text-transform: uppercase; - margin: 0; padding: 0; -} - -header a { - color: #0b6aff; - text-decoration: none; -} - -header .back { - font-size: 100%; - margin: 0 0.5em 0 -0.5em; -} - -h1 form { - float: right; -} - -#searchbox { - border-bottom: 1px solid #666; -} - -#searchbox input { - color: #444; - font-size: 100%; - font-weight: 100; - border: none; - padding: 1em 0 1em 0; - width: 100%; -} - -#searchbox input:focus { - outline-width: 0; -} - -.actions form { - float: right; - position: relative; - top: 0.2em; -} - -.no-results { - font-weight: 200; - font-size: 200%; -} - -.result, -.artist { - padding: 1em 0 1em 0; - margin: 0; - border-bottom: 1px solid #999; -} - -.result:hover, -.artist:hover { - background: #f9f9f9; -} - -.result h2, -.artist h2 { - color: #444; - margin: 0; - padding: 0; -} - -.artist h2 { - float: left; - margin-left: 50px; -} - -.artist.search.result h2 { - float: none; -} - -.artist h1 .back { - margin-right: 65px; -} - -.artist h1 img.band { - left: 120px; - top: 50px; -} - -.result h2 a, -.artist h2 a { - color: #444; -} - -.result h2 small, -.artist h2 small { - font-size: 70%; - font-weight: 100; - margin-left: 0.5em; -} - -.result h2 a, -.artist h2 a { - text-decoration: none; -} - -.result h2 a:hover name, -.artist h2 a:hover .name { - text-decoration: underline; -} - -.result .highlight.small { - font-size: 90%; - font-weight: 200; - padding: 0; - margin: 0.25em 0 0.25em 50px; -} - -.result .small .label { - color: #999; - font-size: 80%; - /*min-width: 5em;*/ - display: inline-block; -} - -.artist-info { - color: #5f5f5f; - text-transform: uppercase; - font-weight: 200; - border-bottom: 1px solid #666; - padding: 0 0 1em 0; - margin: 0 0 1em 0; -} - -.artist-profile { - color: #999; - font-size: 95%; - font-weight: 100; - text-transform: none; - padding: 0; - margin: 0.25em 0 0 0; -} - -.artist img.band { - position: absolute; - left: 85px; - margin-top: 14px; - transform: translate(-50%,-50%); - clip-path: circle(20px at center); -} - -.album { - margin: 0 0 4em 0; -} - -.album.search.result { - margin: 0; -} - -.album .cover { - float: left; - width: 150px; -} - -.album.search.result .cover { - width: 40px; - margin-right: 10px; -} - -.album .cover img { - border: 1px solid rgba(0,0,0,0.15); - box-shadow: 0px 0px 1px 0px rgba(0,0,0,0.05); -} - -.album .content { - float: left; - margin-left: 25px; -} - -.album .content ul { - float: left; - margin: 0 2em 0 0; - padding: 0; - min-width: 18em; -} - -.album .content ul li { - line-height: 1.5em; - padding: 0.5em 0 0.5em 0; - border-bottom:1px solid #f8f8f8; - list-style: none; -} - -.album .content ul li .counter { - color: #999; - font-style: normal; - font-size: 80%; - font-weight: 100; - margin-right: 0.5em; -} - -.album h3 { - margin: 0; padding: 0; - border-bottom: 2px solid #e0e0e0; - padding: 0 0 0.5em 0; - margin: 0 0 1em 0; -} - -.album h3 .title { - text-transform: uppercase; - font-weight: 200; -} - -.album small { - color: #a3a3a3; - font-weight: 200; -} - -.album .info { - float: right; -} - -em[class^=hl] { - font-style: normal; - background: #e6efff; - padding: 0.15em 0.35em; - border-radius: 5px; -} \ No newline at end of file diff --git a/elasticsearch-persistence/examples/music/assets/autocomplete.css b/elasticsearch-persistence/examples/music/assets/autocomplete.css deleted file mode 100644 index 7f2340969..000000000 --- a/elasticsearch-persistence/examples/music/assets/autocomplete.css +++ /dev/null @@ -1,48 +0,0 @@ -.ui-autocomplete { - font-family: 'Helvetica Neue', Helvetica, sans-serif !important; - border: none !important; - border-radius: 0 !important; - background-color: #fff !important; - margin: 0 !important; - padding: 0 !important; - box-shadow: 0px 3px 3px 0px rgba(0,0,0,0.75); -} - -.ui-autocomplete-category { - color: #fff; - background: #222; - font-size: 90%; - font-weight: 300; - text-transform: uppercase; - margin: 0 !important; - padding: 0.25em 0.5em 0.25em 0.5em; -} - -.ui-autocomplete-item { - border-bottom: 1px solid #000; - margin: 0 !important; - padding: 0 !important; -} - -.ui-autocomplete-item:hover, -.ui-autocomplete-item:focus { - color: #fff !important; - background: #0b6aff !important; -} - -.ui-state-focus, -.ui-state-focus a, -.ui-state-active, -.ui-state-active a, -.ui-autocomplete-item:hover a { - color: #fff !important; - background: #0b6aff !important; - outline: none !important; - border: none !important; - border-radius: 0 !important; -} - -a.ui-state-focus, -a.ui-state-active { - margin: 0px !important; -} diff --git a/elasticsearch-persistence/examples/music/assets/blank_artist.png b/elasticsearch-persistence/examples/music/assets/blank_artist.png deleted file mode 100644 index 0dfda13bdea1b87f705ed12e378be32a9a7b0bbf..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 30857 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 IzvuIR05xHvF#rGn diff --git a/elasticsearch-persistence/examples/music/assets/blank_cover.png b/elasticsearch-persistence/examples/music/assets/blank_cover.png deleted file mode 100644 index 8c513407a0456537db0fd720756e7ba7a5da1737..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 22778 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 diff --git a/elasticsearch-persistence/examples/music/assets/form.css b/elasticsearch-persistence/examples/music/assets/form.css deleted file mode 100644 index 3a937e310..000000000 --- a/elasticsearch-persistence/examples/music/assets/form.css +++ /dev/null @@ -1,113 +0,0 @@ -/* Based on https://github.com/plataformatec/simple_form/wiki/CSS-for-simple_form */ - -body.edit h1, -body.new h1 { - color: #999; - font-size: 100%; - text-transform: uppercase; - margin: 0 0 1em 5.5em; -} - -body.edit a[href^="/artists"], -body.new a[href^="/artists"], -body.edit a[href^="/music/artists"], -body.new a[href^="/music/artists"] { - color: #222; - background: #ccc; - text-decoration: none; - border-radius: 0.3em; - padding: 0.25em 0.5em; - margin: 2em 0 0 5.5em; - display: inline-block; -} - -body.edit a[href^="/artists"]:hover, -body.new a[href^="/artists"]:hover, -body.edit a[href^="/music/artists"]:hover, -body.new a[href^="/music/artists"]:hover { - color: #fff; - background: #333; -} - -body.edit a[href^="/artists"]:last-child, -body.new a[href^="/artists"]:last-child, -body.edit a[href^="/music/artists"]:last-child, -body.new a[href^="/music/artists"]:last-child { - margin-left: 0; -} - -.simple_form div.input { - margin-bottom: 1em; - clear: both; -} - -.simple_form label { - color: #878787; - font-size: 80%; - text-transform: uppercase; - font-weight: 200; - float: left; - width: 5em; - text-align: right; - margin: 0.25em 1em; -} - -div.boolean, .simple_form input[type='submit'] { - margin-left: 8.5em; -} - -.field_with_errors input { - border: 2px solid #c70008 !important; -} - -.simple_form .error { - color: #fff !important; - background: #c70008; - font-weight: bold; - clear: left; - display: block; - padding: 0.25em 0.5em; - margin-left: 5.6em; - width: 27.45em; -} - -.simple_form .hint { - color: #878787; - font-size: 80%; - font-style: italic; - display: block; - margin: 0.25em 0 0 7em; - clear: left; -} - -input { - margin: 0; -} - -input.radio { - margin-right: 5px; - vertical-align: -3px; -} - -input.check_boxes { - margin-left: 3px; - vertical-align: -3px; -} - -label.collection_check_boxes { - float: none; - margin: 0; - vertical-align: -2px; - margin-left: 2px; -} - -input.string, -textarea.text { - padding: 0.5em; - min-width: 40em; - border: 1px solid #ccc; -} - -textarea.text { - min-height: 5em; -} diff --git a/elasticsearch-persistence/examples/music/index_manager.rb b/elasticsearch-persistence/examples/music/index_manager.rb deleted file mode 100644 index cbf9e09ee..000000000 --- a/elasticsearch-persistence/examples/music/index_manager.rb +++ /dev/null @@ -1,73 +0,0 @@ -require 'open-uri' - -class IndexManager - def self.create_index(options={}) - client = Artist.gateway.client - index_name = Artist.index_name - - client.indices.delete index: index_name rescue nil if options[:force] - - settings = Artist.settings.to_hash.merge(Album.settings.to_hash) - mappings = Artist.mappings.to_hash.merge(Album.mappings.to_hash) - - client.indices.create index: index_name, - body: { - settings: settings.to_hash, - mappings: mappings.to_hash } - end - - def self.import_from_yaml(source, options={}) - create_index force: true if options[:force] - - input = open(source) - artists = YAML.load_documents input - - artists.each do |artist| - Artist.create artist.update( - 'album_count' => artist['releases'].size, - 'members_combined' => artist['members'].join(', '), - 'suggest' => { - 'name' => { - 'input' => { 'input' => artist['namevariations'].unshift(artist['name']).reject { |d| d.to_s.empty? } }, - 'output' => artist['name'], - 'payload' => { - 'url' => "/artists/#{artist['id']}" - } - }, - 'member' => { - 'input' => { 'input' => artist['members'] }, - 'output' => artist['name'], - 'payload' => { - 'url' => "/artists/#{artist['id']}" - } - } - } - ) - - artist['releases'].each do |album| - album.update( - 'suggest' => { - 'title' => { - 'input' => { 'input' => album['title'] }, - 'output' => album['title'], - 'payload' => { - 'url' => "/artists/#{artist['id']}#album_#{album['id']}" - } - }, - 'track' => { - 'input' => { 'input' => album['tracklist'].map { |d| d['title'] }.reject { |d| d.to_s.empty? } }, - 'output' => album['title'], - 'payload' => { - 'url' => "/artists/#{artist['id']}#album_#{album['id']}" - } - } - } - ) - album['notes'] = album['notes'].to_s.gsub(/<.+?>/, '').gsub(/ {2,}/, '') - album['released'] = nil if album['released'] < 1 - - Album.create album, id: album['id'], parent: artist['id'] - end - end - end -end diff --git a/elasticsearch-persistence/examples/music/search/index.html.erb b/elasticsearch-persistence/examples/music/search/index.html.erb deleted file mode 100644 index 098f626e5..000000000 --- a/elasticsearch-persistence/examples/music/search/index.html.erb +++ /dev/null @@ -1,95 +0,0 @@ -

-

- <%= link_to "〈".html_safe, :back, title: "Back" %> - Artists & Albums -

-
- - - -
- <% @artists.each do |artist| %> - <%= content_tag :div, class: 'artist search result clearfix' do %> -

- <%= image_tag "http://ruby.elastic.co.s3-website-us-east-1.amazonaws.com/demo/music/bands/#{artist.id}.jpeg", height: '45px', class: 'band' %> - <%= link_to artist do %> - <%= highlighted(artist, :name) %> - <%= pluralize artist.album_count, 'album' %> - <% end %> -

- <% if highlight = highlight(artist, :members_combined) %> -

- Members - <%= highlight.first.html_safe %> -

- <% end %> - <% if highlight = highlight(artist, :profile) %> -

- Profile - <%= highlight.join('…').html_safe %> -

- <% end %> - <% end %> - <% end %> -
- -
- <% @albums.each do |album| %> - <%= content_tag :div, class: 'album search result clearfix' do %> -

- <%= image_tag "http://ruby.elastic.co.s3-website-us-east-1.amazonaws.com/demo/music/covers/#{album.id}.jpeg", width: '45px', class: 'cover' %> - <%= link_to artist_path(album.artist_id, anchor: "album_#{album.id}") do %> - <%= highlighted(album, :title) %> - <%= album.artist %> - (<%= [album.meta.formats.first, album.released].compact.join(' ') %>) - <% end %> -

- - <% if highlight = highlight(album, 'tracklist.title') %> -

- Tracks - <%= highlight.join('…').html_safe %> -

- <% end %> - - <% if highlight = highlight(album, :notes) %> -

- Notes - <%= highlight.map { |d| d.gsub(/^\.\s?/, '') }.join('…').html_safe %> -

- <% end %> - <% end %> - <% end %> -
- -<% if @artists.empty? && @albums.empty? %> -
-

The search hasn't returned any results...

-
-<% end %> - - diff --git a/elasticsearch-persistence/examples/music/search/search_controller.rb b/elasticsearch-persistence/examples/music/search/search_controller.rb deleted file mode 100644 index bb845c5b6..000000000 --- a/elasticsearch-persistence/examples/music/search/search_controller.rb +++ /dev/null @@ -1,41 +0,0 @@ -class SearchController < ApplicationController - - def index - tags = { pre_tags: '', post_tags: '' } - @artists = Artist.search \ - query: { - multi_match: { - query: params[:q], - fields: ['name^10','members^2','profile'] - } - }, - highlight: { - tags_schema: 'styled', - fields: { - name: { number_of_fragments: 0 }, - members_combined: { number_of_fragments: 0 }, - profile: { fragment_size: 50 } - } - } - - @albums = Album.search \ - query: { - multi_match: { - query: params[:q], - fields: ['title^100','tracklist.title^10','notes^1'] - } - }, - highlight: { - tags_schema: 'styled', - fields: { - title: { number_of_fragments: 0 }, - 'tracklist.title' => { number_of_fragments: 0 }, - notes: { fragment_size: 50 } - } - } - end - - def suggest - render json: Suggester.new(params) - end -end diff --git a/elasticsearch-persistence/examples/music/search/search_controller_test.rb b/elasticsearch-persistence/examples/music/search/search_controller_test.rb deleted file mode 100644 index a1c95cd0c..000000000 --- a/elasticsearch-persistence/examples/music/search/search_controller_test.rb +++ /dev/null @@ -1,12 +0,0 @@ -require 'test_helper' - -class SearchControllerTest < ActionController::TestCase - setup do - IndexManager.create_index force: true - end - - test "should get suggest" do - get :suggest, term: 'foo' - assert_response :success - end -end diff --git a/elasticsearch-persistence/examples/music/search/search_helper.rb b/elasticsearch-persistence/examples/music/search/search_helper.rb deleted file mode 100644 index 65a57c322..000000000 --- a/elasticsearch-persistence/examples/music/search/search_helper.rb +++ /dev/null @@ -1,15 +0,0 @@ -module SearchHelper - - def highlight(object, field) - object.try(:hit).try(:highlight).try(field) - end - - def highlighted(object, field) - if h = object.try(:hit).try(:highlight).try(field).try(:first) - h.html_safe - else - field.to_s.split('.').reduce(object) { |result,item| result.try(item) } - end - end - -end diff --git a/elasticsearch-persistence/examples/music/suggester.rb b/elasticsearch-persistence/examples/music/suggester.rb deleted file mode 100644 index 5438cf11a..000000000 --- a/elasticsearch-persistence/examples/music/suggester.rb +++ /dev/null @@ -1,69 +0,0 @@ -class Suggester - attr_reader :response - - def initialize(params={}) - @term = params[:term] - end - - def response - @response ||= begin - Elasticsearch::Persistence.client.search \ - index: Artist.index_name, - body: { - suggest: { - artists: { - text: @term, - completion: { field: 'suggest.name.input', size: 25 } - }, - members: { - text: @term, - completion: { field: 'suggest.member.input', size: 25 } - }, - albums: { - text: @term, - completion: { field: 'suggest.title.input', size: 25 } - }, - tracks: { - text: @term, - completion: { field: 'suggest.track.input', size: 25 } - } - }, - _source: ['suggest.*'] - } - end - end - - def as_json(options={}) - return [] unless response['suggest'] - - output = [ - { label: 'Bands', - value: response['suggest']['artists'][0]['options'].map do |d| - { text: d['_source']['suggest']['name']['output'], - url: d['_source']['suggest']['name']['payload']['url'] } - end - }, - - { label: 'Albums', - value: response['suggest']['albums'][0]['options'].map do |d| - { text: d['_source']['suggest']['title']['output'], - url: d['_source']['suggest']['title']['payload']['url'] } - end - }, - - { label: 'Band Members', - value: response['suggest']['members'][0]['options'].map do |d| - { text: "#{d['text']} (#{d['_source']['suggest']['member']['output']})", - url: d['_source']['suggest']['member']['payload']['url'] } - end - }, - - { label: 'Album Tracks', - value: response['suggest']['tracks'][0]['options'].map do |d| - { text: "#{d['text']} (#{d['_source']['suggest']['track']['output']})", - url: d['_source']['suggest']['track']['payload']['url'] } - end - } - ] - end -end diff --git a/elasticsearch-persistence/examples/music/template.rb b/elasticsearch-persistence/examples/music/template.rb deleted file mode 100644 index 4759b642d..000000000 --- a/elasticsearch-persistence/examples/music/template.rb +++ /dev/null @@ -1,430 +0,0 @@ -# ====================================================================================== -# Template for generating a Rails application with support for Elasticsearch persistence -# ====================================================================================== -# -# This file creates a fully working Rails application with support for storing and retrieving models -# in Elasticsearch, using the `elasticsearch-persistence` gem -# (https://github.com/elasticsearch/elasticsearch-rails/tree/master/elasticsearch-persistence). -# -# Requirements: -# ------------- -# -# * Git -# * Ruby >= 1.9.3 -# * Rails >= 5 -# * Java >= 8 (for Elasticsearch) -# -# Usage: -# ------ -# -# $ time rails new music --force --skip --skip-bundle --skip-active-record --template https://raw.githubusercontent.com/elastic/elasticsearch-rails/master/elasticsearch-persistence/examples/music/template.rb -# -# ===================================================================================================== - -STDOUT.sync = true -STDERR.sync = true - -require 'uri' -require 'json' -require 'net/http' - -at_exit do - pid = File.read("#{destination_root}/tmp/pids/elasticsearch.pid") rescue nil - if pid - say_status "Stop", "Elasticsearch", :yellow - run "kill #{pid}" - end -end - -$elasticsearch_url = ENV.fetch('http://github.com/ELASTICSEARCH_URL', 'http://localhost:9200/') - -# ----- Check & download Elasticsearch ------------------------------------------------------------ - -cluster_info = Net::HTTP.get(URI.parse($elasticsearch_url)) rescue nil -cluster_info = JSON.parse(cluster_info) if cluster_info - -if cluster_info.nil? || cluster_info['version']['number'] < '5' - # Change the port when incompatible Elasticsearch version is running on localhost:9200 - if $elasticsearch_url == 'http://localhost:9200/' && cluster_info && cluster_info['version']['number'] < '5' - $change_port = '9280' - $elasticsearch_url = "http://localhost/#{$change_port}" - end - - COMMAND = <<-COMMAND.gsub(/^ /, '') - curl -# -O "https://artifacts.elastic.co/downloads/elasticsearch/elasticsearch-5.2.1.tar.gz" - tar -zxf elasticsearch-5.2.1.tar.gz - rm -f elasticsearch-5.2.1.tar.gz - ./elasticsearch-5.2.1/bin/elasticsearch -d -p #{destination_root}/tmp/pids/elasticsearch.pid #{$change_port.nil? ? '' : "-E http.port=#{$change_port}" } - COMMAND - - puts "\n" - say_status "ERROR", "Elasticsearch not running!\n", :red - puts '-'*80 - say_status '', "It appears that Elasticsearch 5 is not running on this machine." - say_status '', "Is it installed? Do you want me to install and run it for you with this command?\n\n" - COMMAND.each_line { |l| say_status '', "$ #{l}" } - puts - say_status '', "(To uninstall, just remove the generated application directory.)" - puts '-'*80, '' - - if yes?("Install Elasticsearch?", :bold) - puts - say_status "Install", "Elasticsearch", :yellow - - java_info = `java -version 2>&1` - - unless java_info.match /1\.[8-9]/ - puts - say_status "ERROR", "Required Java version (1.8) not found, exiting...", :red - exit(1) - end - - commands = COMMAND.split("\n") - exec = commands.pop - inside("vendor") do - commands.each { |command| run command } - run "(#{exec})" # Launch Elasticsearch in subshell - end - - # Wait for Elasticsearch to be up... - # - system <<-COMMAND - until $(curl --silent --head --fail #{$elasticsearch_url} > /dev/null 2>&1); do - printf '.'; sleep 1 - done - COMMAND - end -end unless ENV['RAILS_NO_ES_INSTALL'] - -# ----- Application skeleton ---------------------------------------------------------------------- - -run "touch tmp/.gitignore" - -append_to_file ".gitignore", "vendor/elasticsearch-5.2.1/\n" - -git :init -git add: "." -git commit: "-m 'Initial commit: Clean application'" - -# ----- Add README -------------------------------------------------------------------------------- - -puts -say_status "README", "Adding Readme...\n", :yellow -puts '-'*80, ''; sleep 0.25 - -remove_file 'README.md' - -create_file 'README.md', <<-README -= Ruby on Rails and Elasticsearch persistence: Example application - -README - - -git add: "." -git commit: "-m 'Added README for the application'" - -# ----- Use Pry as the Rails console -------------------------------------------------------------- - -puts -say_status "Rubygems", "Adding Pry into Gemfile...\n", :yellow -puts '-'*80, ''; - -gem_group :development do - gem 'pry' - gem 'pry-rails' -end - -git add: "Gemfile*" -git commit: "-m 'Added Pry into the Gemfile'" - -# ----- Auxiliary gems ---------------------------------------------------------------------------- - -puts -say_status "Rubygems", "Adding libraries into the Gemfile...\n", :yellow -puts '-'*80, ''; sleep 0.75 - -gem "simple_form" - -git add: "Gemfile*" -git commit: "-m 'Added auxiliary libraries into the Gemfile'" - -# ----- Remove CoffeeScript, Sass and "all that jazz" --------------------------------------------- - -comment_lines 'Gemfile', /gem 'coffee/ -comment_lines 'Gemfile', /gem 'sass/ -comment_lines 'Gemfile', /gem 'uglifier/ -uncomment_lines 'Gemfile', /gem 'therubyracer/ - -# ----- Add gems into Gemfile --------------------------------------------------------------------- - -puts -say_status "Rubygems", "Adding Elasticsearch libraries into Gemfile...\n", :yellow -puts '-'*80, ''; sleep 0.75 - -gem 'elasticsearch', git: 'https://github.com/elastic/elasticsearch-ruby.git' -gem 'elasticsearch-model', git: 'https://github.com/elastic/elasticsearch-rails.git', require: 'elasticsearch/model' -gem 'elasticsearch-persistence', git: 'https://github.com/elastic/elasticsearch-rails.git', require: 'elasticsearch/persistence/model' -gem 'elasticsearch-rails', git: 'https://github.com/elastic/elasticsearch-rails.git' - -git add: "Gemfile*" -git commit: "-m 'Added the Elasticsearch libraries into the Gemfile'" - -# ----- Install gems ------------------------------------------------------------------------------ - -puts -say_status "Rubygems", "Installing Rubygems...", :yellow -puts '-'*80, '' - -run "bundle install" - -# ----- Autoload ./lib ---------------------------------------------------------------------------- - -puts -say_status "Application", "Adding autoloading of ./lib...", :yellow -puts '-'*80, '' - -insert_into_file 'config/application.rb', - ' - config.autoload_paths += %W(#{config.root}/lib) - -', - after: 'class Application < Rails::Application' - -git commit: "-a -m 'Added autoloading of the ./lib folder'" - -# ----- Add jQuery UI ---------------------------------------------------------------------------- - -puts -say_status "Assets", "Adding jQuery UI...", :yellow -puts '-'*80, ''; sleep 0.25 - -if ENV['LOCAL'] - copy_file File.expand_path('../vendor/assets/jquery-ui-1.10.4.custom.min.js', __FILE__), - 'vendor/assets/javascripts/jquery-ui-1.10.4.custom.min.js' - copy_file File.expand_path('../vendor/assets/jquery-ui-1.10.4.custom.min.css', __FILE__), - 'vendor/assets/stylesheets/ui-lightness/jquery-ui-1.10.4.custom.min.css' - copy_file File.expand_path('../vendor/assets/stylesheets/ui-lightness/images/ui-bg_highlight-soft_100_eeeeee_1x100.png', __FILE__), - 'vendor/assets/stylesheets/ui-lightness/images/ui-bg_highlight-soft_100_eeeeee_1x100.png' -else - get 'https://raw.githubusercontent.com/elasticsearch/elasticsearch-rails/master/elasticsearch-persistence/examples/music/vendor/assets/jquery-ui-1.10.4.custom.min.js', - 'vendor/assets/javascripts/jquery-ui-1.10.4.custom.min.js' - get 'https://raw.githubusercontent.com/elasticsearch/elasticsearch-rails/master/elasticsearch-persistence/examples/music/vendor/assets/jquery-ui-1.10.4.custom.min.css', - 'vendor/assets/stylesheets/ui-lightness/jquery-ui-1.10.4.custom.min.css' - get 'https://raw.githubusercontent.com/elasticsearch/elasticsearch-rails/master/elasticsearch-persistence/examples/music/vendor/assets/stylesheets/ui-lightness/images/ui-bg_highlight-soft_100_eeeeee_1x100.png', - 'vendor/assets/stylesheets/ui-lightness/images/ui-bg_highlight-soft_100_eeeeee_1x100.png' -end - -append_to_file 'app/assets/javascripts/application.js', "//= require jquery-ui-1.10.4.custom.min.js" - -git commit: "-a -m 'Added jQuery UI'" - -# ----- Generate Artist scaffold ------------------------------------------------------------------ - -puts -say_status "Model", "Generating the Artist scaffold...", :yellow -puts '-'*80, ''; sleep 0.25 - -generate :scaffold, "Artist name:String --orm=elasticsearch" -route "root to: 'artists#index'" - -git add: "." -git commit: "-m 'Added the generated Artist scaffold'" - -# ----- Generate Album model ---------------------------------------------------------------------- - -puts -say_status "Model", "Generating the Album model...", :yellow -puts '-'*80, ''; sleep 0.25 - -generate :model, "Album --orm=elasticsearch" - -git add: "." -git commit: "-m 'Added the generated Album model'" - -# ----- Add proper model classes ------------------------------------------------------------------ - -puts -say_status "Model", "Adding Album, Artist and Suggester models implementation...", :yellow -puts '-'*80, ''; sleep 0.25 - -if ENV['LOCAL'] - copy_file File.expand_path('../artist.rb', __FILE__), 'app/models/artist.rb' - copy_file File.expand_path('../album.rb', __FILE__), 'app/models/album.rb' - copy_file File.expand_path('../suggester.rb', __FILE__), 'app/models/suggester.rb' -else - get 'https://raw.githubusercontent.com/elasticsearch/elasticsearch-rails/master/elasticsearch-persistence/examples/music/artist.rb', - 'app/models/artist.rb' - get 'https://raw.githubusercontent.com/elasticsearch/elasticsearch-rails/master/elasticsearch-persistence/examples/music/album.rb', - 'app/models/album.rb' - get 'https://raw.githubusercontent.com/elasticsearch/elasticsearch-rails/master/elasticsearch-persistence/examples/music/suggester.rb', - 'app/models/suggester.rb' -end - -git add: "./app/models" -git commit: "-m 'Added Album, Artist and Suggester models implementation'" - -# ----- Add controllers and views ----------------------------------------------------------------- - -puts -say_status "Views", "Adding ArtistsController and views...", :yellow -puts '-'*80, ''; sleep 0.25 - -if ENV['LOCAL'] - copy_file File.expand_path('../artists/artists_controller.rb', __FILE__), 'app/controllers/artists_controller.rb' - copy_file File.expand_path('../artists/index.html.erb', __FILE__), 'app/views/artists/index.html.erb' - copy_file File.expand_path('../artists/show.html.erb', __FILE__), 'app/views/artists/show.html.erb' - copy_file File.expand_path('../artists/_form.html.erb', __FILE__), 'app/views/artists/_form.html.erb' - copy_file File.expand_path('../artists/artists_controller_test.rb', __FILE__), - 'test/controllers/artists_controller_test.rb' -else - get 'https://raw.githubusercontent.com/elasticsearch/elasticsearch-rails/master/elasticsearch-persistence/examples/music/artists/artists_controller.rb', - 'app/controllers/artists_controller.rb' - get 'https://raw.githubusercontent.com/elasticsearch/elasticsearch-rails/master/elasticsearch-persistence/examples/music/artists/index.html.erb', - 'app/views/artists/index.html.erb' - get 'https://raw.githubusercontent.com/elasticsearch/elasticsearch-rails/master/elasticsearch-persistence/examples/music/artists/show.html.erb', - 'app/views/artists/show.html.erb' - get 'https://raw.githubusercontent.com/elasticsearch/elasticsearch-rails/master/elasticsearch-persistence/examples/music/artists/_form.html.erb', - 'app/views/artists/_form.html.erb' - get 'https://raw.githubusercontent.com/elasticsearch/elasticsearch-rails/master/elasticsearch-persistence/examples/music/artists/artists_controller_test.rb', - 'test/controllers/artists_controller_test.rb' -end - -git commit: "-a -m 'Added ArtistsController and related views'" - -puts -say_status "Views", "Adding SearchController and views...", :yellow -puts '-'*80, ''; sleep 0.25 - -if ENV['LOCAL'] - copy_file File.expand_path('../search/search_controller.rb', __FILE__), 'app/controllers/search_controller.rb' - copy_file File.expand_path('../search/search_helper.rb', __FILE__), 'app/helpers/search_helper.rb' - copy_file File.expand_path('../search/index.html.erb', __FILE__), 'app/views/search/index.html.erb' - copy_file File.expand_path('../search/search_controller_test.rb', __FILE__), - 'test/controllers/search_controller_test.rb' -else - get 'https://raw.githubusercontent.com/elasticsearch/elasticsearch-rails/master/elasticsearch-persistence/examples/music/search/search_controller.rb', - 'app/controllers/search_controller.rb' - get 'https://raw.githubusercontent.com/elasticsearch/elasticsearch-rails/master/elasticsearch-persistence/examples/music/search/search_helper.rb', - 'app/helpers/search_helper.rb' - get 'https://raw.githubusercontent.com/elasticsearch/elasticsearch-rails/master/elasticsearch-persistence/examples/music/search/index.html.erb', - 'app/views/search/index.html.erb' - get 'https://raw.githubusercontent.com/elasticsearch/elasticsearch-rails/master/elasticsearch-persistence/examples/music/search/search_controller_test.rb', - 'test/controllers/search_controller_test.rb' -end - -route "get 'search', to: 'search#index'" -route "get 'suggest', to: 'search#suggest'" - -comment_lines 'test/test_helper.rb', /fixtures \:all/ - -git add: "." -git commit: "-m 'Added SearchController and related views'" - -# ----- Add assets ----------------------------------------------------------------- - -puts -say_status "Views", "Adding application assets...", :yellow -puts '-'*80, ''; sleep 0.25 - -git rm: 'app/assets/stylesheets/scaffold.css' - -gsub_file 'app/views/layouts/application.html.erb', //, '' - -if ENV['LOCAL'] - copy_file File.expand_path('../assets/application.css', __FILE__), 'app/assets/stylesheets/application.css' - copy_file File.expand_path('../assets/autocomplete.css', __FILE__), 'app/assets/stylesheets/autocomplete.css' - copy_file File.expand_path('../assets/form.css', __FILE__), 'app/assets/stylesheets/form.css' - copy_file File.expand_path('../assets/blank_cover.png', __FILE__), 'public/images/blank_cover.png' - copy_file File.expand_path('../assets/blank_artist.png', __FILE__), 'public/images/blank_artist.png' -else - get 'https://raw.githubusercontent.com/elasticsearch/elasticsearch-rails/master/elasticsearch-persistence/examples/music/assets/application.css', - 'app/assets/stylesheets/application.css' - get 'https://raw.githubusercontent.com/elasticsearch/elasticsearch-rails/master/elasticsearch-persistence/examples/music/assets/autocomplete.css', - 'app/assets/stylesheets/autocomplete.css' - get 'https://raw.githubusercontent.com/elasticsearch/elasticsearch-rails/master/elasticsearch-persistence/examples/music/assets/form.css', - 'app/assets/stylesheets/form.css' - get 'https://raw.githubusercontent.com/elasticsearch/elasticsearch-rails/master/elasticsearch-persistence/examples/music/assets/blank_cover.png', - 'public/images/blank_cover.png' - get 'https://raw.githubusercontent.com/elasticsearch/elasticsearch-rails/master/elasticsearch-persistence/examples/music/assets/blank_artist.png', - 'public/images/blank_artist.png' -end - -git add: "." -git commit: "-m 'Added application assets'" - -# ----- Add an Elasticsearch initializer ---------------------------------------------------------- - -puts -say_status "Initializer", "Adding an Elasticsearch initializer...", :yellow -puts '-'*80, ''; sleep 0.25 - -initializer 'elasticsearch.rb', %q{ - Elasticsearch::Persistence.client = Elasticsearch::Client.new host: ENV['ELASTICSEARCH_URL'] || 'localhost:9200' - - if Rails.env.development? - logger = ActiveSupport::Logger.new(STDERR) - logger.level = Logger::INFO - logger.formatter = proc { |s, d, p, m| "\e[2m#{m}\n\e[0m" } - Elasticsearch::Persistence.client.transport.logger = logger - end -}.gsub(/^ /, '') - -git add: "./config" -git commit: "-m 'Added an Elasticsearch initializer'" - -# ----- Add IndexManager ----------------------------------------------------------------- - -puts -say_status "Application", "Adding the IndexManager class...", :yellow -puts '-'*80, ''; sleep 0.25 - -if ENV['LOCAL'] - copy_file File.expand_path('../index_manager.rb', __FILE__), 'lib/index_manager.rb' -else - get 'https://raw.githubusercontent.com/elasticsearch/elasticsearch-rails/master/elasticsearch-persistence/examples/music/index_manager.rb', - 'lib/index_manager.rb' -end - -# TODO: get 'https://raw.github.com/...', '...' - -git add: "." -git commit: "-m 'Added the IndexManager class'" - -# ----- Import the data --------------------------------------------------------------------------- - -puts -say_status "Data", "Import the data...", :yellow -puts '-'*80, ''; sleep 0.25 - -source = ENV.fetch('http://github.com/DATA_SOURCE', 'https://github.com/elastic/elasticsearch-rails/releases/download/dischord.yml/dischord.yml') - -run "ELASTICSEARCH_URL=#{$elasticsearch_url} rails runner 'IndexManager.import_from_yaml(\"#{source}\", force: true)'" - -# ----- Print Git log ----------------------------------------------------------------------------- - -puts -say_status "Git", "Details about the application:", :yellow -puts '-'*80, '' - -run "git --no-pager log --reverse --oneline" - -# ----- Start the application --------------------------------------------------------------------- - -unless ENV['RAILS_NO_SERVER_START'] - require 'net/http' - if (begin; Net::HTTP.get(URI('http://localhost:3000/')); rescue Errno::ECONNREFUSED; false; rescue Exception; true; end) - puts "\n" - say_status "ERROR", "Some other application is running on port 3000!\n", :red - puts '-'*80 - - port = ask("Please provide free port:", :bold) - else - port = '3000' - end - - puts "", "="*80 - say_status "DONE", "\e[1mStarting the application.\e[0m", :yellow - puts "="*80, "" - - run "ELASTICSEARCH_URL=#{$elasticsearch_url} rails server --port=#{port}" -end diff --git a/elasticsearch-persistence/examples/music/vendor/assets/jquery-ui-1.10.4.custom.min.css b/elasticsearch-persistence/examples/music/vendor/assets/jquery-ui-1.10.4.custom.min.css deleted file mode 100755 index 672cea658..000000000 --- a/elasticsearch-persistence/examples/music/vendor/assets/jquery-ui-1.10.4.custom.min.css +++ /dev/null @@ -1,7 +0,0 @@ -/*! jQuery UI - v1.10.4 - 2014-06-04 -* http://jqueryui.com -* Includes: jquery.ui.core.css, jquery.ui.autocomplete.css, jquery.ui.menu.css, jquery.ui.theme.css -* To view and modify this theme, visit http://jqueryui.com/themeroller/?ffDefault=Trebuchet%20MS%2CTahoma%2CVerdana%2CArial%2Csans-serif&fwDefault=bold&fsDefault=1.1em&cornerRadius=4px&bgColorHeader=f6a828&bgTextureHeader=gloss_wave&bgImgOpacityHeader=35&borderColorHeader=e78f08&fcHeader=ffffff&iconColorHeader=ffffff&bgColorContent=eeeeee&bgTextureContent=highlight_soft&bgImgOpacityContent=100&borderColorContent=dddddd&fcContent=333333&iconColorContent=222222&bgColorDefault=f6f6f6&bgTextureDefault=glass&bgImgOpacityDefault=100&borderColorDefault=cccccc&fcDefault=1c94c4&iconColorDefault=ef8c08&bgColorHover=fdf5ce&bgTextureHover=glass&bgImgOpacityHover=100&borderColorHover=fbcb09&fcHover=c77405&iconColorHover=ef8c08&bgColorActive=ffffff&bgTextureActive=glass&bgImgOpacityActive=65&borderColorActive=fbd850&fcActive=eb8f00&iconColorActive=ef8c08&bgColorHighlight=ffe45c&bgTextureHighlight=highlight_soft&bgImgOpacityHighlight=75&borderColorHighlight=fed22f&fcHighlight=363636&iconColorHighlight=228ef1&bgColorError=b81900&bgTextureError=diagonals_thick&bgImgOpacityError=18&borderColorError=cd0a0a&fcError=ffffff&iconColorError=ffd27a&bgColorOverlay=666666&bgTextureOverlay=diagonals_thick&bgImgOpacityOverlay=20&opacityOverlay=50&bgColorShadow=000000&bgTextureShadow=flat&bgImgOpacityShadow=10&opacityShadow=20&thicknessShadow=5px&offsetTopShadow=-5px&offsetLeftShadow=-5px&cornerRadiusShadow=5px -* Copyright 2014 jQuery Foundation and other contributors; Licensed MIT */ - -.ui-helper-hidden{display:none}.ui-helper-hidden-accessible{border:0;clip:rect(0 0 0 0);height:1px;margin:-1px;overflow:hidden;padding:0;position:absolute;width:1px}.ui-helper-reset{margin:0;padding:0;border:0;outline:0;line-height:1.3;text-decoration:none;font-size:100%;list-style:none}.ui-helper-clearfix:before,.ui-helper-clearfix:after{content:"";display:table;border-collapse:collapse}.ui-helper-clearfix:after{clear:both}.ui-helper-clearfix{min-height:0}.ui-helper-zfix{width:100%;height:100%;top:0;left:0;position:absolute;opacity:0;filter:Alpha(Opacity=0)}.ui-front{z-index:100}.ui-state-disabled{cursor:default!important}.ui-icon{display:block;text-indent:-99999px;overflow:hidden;background-repeat:no-repeat}.ui-widget-overlay{position:fixed;top:0;left:0;width:100%;height:100%}.ui-autocomplete{position:absolute;top:0;left:0;cursor:default}.ui-menu{list-style:none;padding:2px;margin:0;display:block;outline:none}.ui-menu .ui-menu{margin-top:-3px;position:absolute}.ui-menu .ui-menu-item{margin:0;padding:0;width:100%;list-style-image:url(data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7)}.ui-menu .ui-menu-divider{margin:5px -2px 5px -2px;height:0;font-size:0;line-height:0;border-width:1px 0 0 0}.ui-menu .ui-menu-item a{text-decoration:none;display:block;padding:2px .4em;line-height:1.5;min-height:0;font-weight:normal}.ui-menu .ui-menu-item a.ui-state-focus,.ui-menu .ui-menu-item a.ui-state-active{font-weight:normal;margin:-1px}.ui-menu .ui-state-disabled{font-weight:normal;margin:.4em 0 .2em;line-height:1.5}.ui-menu .ui-state-disabled a{cursor:default}.ui-menu-icons{position:relative}.ui-menu-icons .ui-menu-item a{position:relative;padding-left:2em}.ui-menu .ui-icon{position:absolute;top:.2em;left:.2em}.ui-menu .ui-menu-icon{position:static;float:right}.ui-widget{font-family:Trebuchet MS,Tahoma,Verdana,Arial,sans-serif;font-size:1.1em}.ui-widget .ui-widget{font-size:1em}.ui-widget input,.ui-widget select,.ui-widget textarea,.ui-widget button{font-family:Trebuchet MS,Tahoma,Verdana,Arial,sans-serif;font-size:1em}.ui-widget-content{border:1px solid #ddd;background:#eee url("http://github.com/images/ui-bg_highlight-soft_100_eeeeee_1x100.png") 50% top repeat-x;color:#333}.ui-widget-content a{color:#333}.ui-widget-header{border:1px solid #e78f08;background:#f6a828 url("http://github.com/images/ui-bg_gloss-wave_35_f6a828_500x100.png") 50% 50% repeat-x;color:#fff;font-weight:bold}.ui-widget-header a{color:#fff}.ui-state-default,.ui-widget-content .ui-state-default,.ui-widget-header .ui-state-default{border:1px solid #ccc;background:#f6f6f6 url("http://github.com/images/ui-bg_glass_100_f6f6f6_1x400.png") 50% 50% repeat-x;font-weight:bold;color:#1c94c4}.ui-state-default a,.ui-state-default a:link,.ui-state-default a:visited{color:#1c94c4;text-decoration:none}.ui-state-hover,.ui-widget-content .ui-state-hover,.ui-widget-header .ui-state-hover,.ui-state-focus,.ui-widget-content .ui-state-focus,.ui-widget-header .ui-state-focus{border:1px solid #fbcb09;background:#fdf5ce url("http://github.com/images/ui-bg_glass_100_fdf5ce_1x400.png") 50% 50% repeat-x;font-weight:bold;color:#c77405}.ui-state-hover a,.ui-state-hover a:hover,.ui-state-hover a:link,.ui-state-hover a:visited,.ui-state-focus a,.ui-state-focus a:hover,.ui-state-focus a:link,.ui-state-focus a:visited{color:#c77405;text-decoration:none}.ui-state-active,.ui-widget-content .ui-state-active,.ui-widget-header .ui-state-active{border:1px solid #fbd850;background:#fff url("http://github.com/images/ui-bg_glass_65_ffffff_1x400.png") 50% 50% repeat-x;font-weight:bold;color:#eb8f00}.ui-state-active a,.ui-state-active a:link,.ui-state-active a:visited{color:#eb8f00;text-decoration:none}.ui-state-highlight,.ui-widget-content .ui-state-highlight,.ui-widget-header .ui-state-highlight{border:1px solid #fed22f;background:#ffe45c url("http://github.com/images/ui-bg_highlight-soft_75_ffe45c_1x100.png") 50% top repeat-x;color:#363636}.ui-state-highlight a,.ui-widget-content .ui-state-highlight a,.ui-widget-header .ui-state-highlight a{color:#363636}.ui-state-error,.ui-widget-content .ui-state-error,.ui-widget-header .ui-state-error{border:1px solid #cd0a0a;background:#b81900 url("http://github.com/images/ui-bg_diagonals-thick_18_b81900_40x40.png") 50% 50% repeat;color:#fff}.ui-state-error a,.ui-widget-content .ui-state-error a,.ui-widget-header .ui-state-error a{color:#fff}.ui-state-error-text,.ui-widget-content .ui-state-error-text,.ui-widget-header .ui-state-error-text{color:#fff}.ui-priority-primary,.ui-widget-content .ui-priority-primary,.ui-widget-header .ui-priority-primary{font-weight:bold}.ui-priority-secondary,.ui-widget-content .ui-priority-secondary,.ui-widget-header .ui-priority-secondary{opacity:.7;filter:Alpha(Opacity=70);font-weight:normal}.ui-state-disabled,.ui-widget-content .ui-state-disabled,.ui-widget-header .ui-state-disabled{opacity:.35;filter:Alpha(Opacity=35);background-image:none}.ui-state-disabled .ui-icon{filter:Alpha(Opacity=35)}.ui-icon{width:16px;height:16px}.ui-icon,.ui-widget-content .ui-icon{background-image:url("http://github.com/images/ui-icons_222222_256x240.png")}.ui-widget-header .ui-icon{background-image:url("http://github.com/images/ui-icons_ffffff_256x240.png")}.ui-state-default .ui-icon{background-image:url("http://github.com/images/ui-icons_ef8c08_256x240.png")}.ui-state-hover .ui-icon,.ui-state-focus .ui-icon{background-image:url("http://github.com/images/ui-icons_ef8c08_256x240.png")}.ui-state-active .ui-icon{background-image:url("http://github.com/images/ui-icons_ef8c08_256x240.png")}.ui-state-highlight .ui-icon{background-image:url("http://github.com/images/ui-icons_228ef1_256x240.png")}.ui-state-error .ui-icon,.ui-state-error-text .ui-icon{background-image:url("http://github.com/images/ui-icons_ffd27a_256x240.png")}.ui-icon-blank{background-position:16px 16px}.ui-icon-carat-1-n{background-position:0 0}.ui-icon-carat-1-ne{background-position:-16px 0}.ui-icon-carat-1-e{background-position:-32px 0}.ui-icon-carat-1-se{background-position:-48px 0}.ui-icon-carat-1-s{background-position:-64px 0}.ui-icon-carat-1-sw{background-position:-80px 0}.ui-icon-carat-1-w{background-position:-96px 0}.ui-icon-carat-1-nw{background-position:-112px 0}.ui-icon-carat-2-n-s{background-position:-128px 0}.ui-icon-carat-2-e-w{background-position:-144px 0}.ui-icon-triangle-1-n{background-position:0 -16px}.ui-icon-triangle-1-ne{background-position:-16px -16px}.ui-icon-triangle-1-e{background-position:-32px -16px}.ui-icon-triangle-1-se{background-position:-48px -16px}.ui-icon-triangle-1-s{background-position:-64px -16px}.ui-icon-triangle-1-sw{background-position:-80px -16px}.ui-icon-triangle-1-w{background-position:-96px -16px}.ui-icon-triangle-1-nw{background-position:-112px -16px}.ui-icon-triangle-2-n-s{background-position:-128px -16px}.ui-icon-triangle-2-e-w{background-position:-144px -16px}.ui-icon-arrow-1-n{background-position:0 -32px}.ui-icon-arrow-1-ne{background-position:-16px -32px}.ui-icon-arrow-1-e{background-position:-32px -32px}.ui-icon-arrow-1-se{background-position:-48px -32px}.ui-icon-arrow-1-s{background-position:-64px -32px}.ui-icon-arrow-1-sw{background-position:-80px -32px}.ui-icon-arrow-1-w{background-position:-96px -32px}.ui-icon-arrow-1-nw{background-position:-112px -32px}.ui-icon-arrow-2-n-s{background-position:-128px -32px}.ui-icon-arrow-2-ne-sw{background-position:-144px -32px}.ui-icon-arrow-2-e-w{background-position:-160px -32px}.ui-icon-arrow-2-se-nw{background-position:-176px -32px}.ui-icon-arrowstop-1-n{background-position:-192px -32px}.ui-icon-arrowstop-1-e{background-position:-208px -32px}.ui-icon-arrowstop-1-s{background-position:-224px -32px}.ui-icon-arrowstop-1-w{background-position:-240px -32px}.ui-icon-arrowthick-1-n{background-position:0 -48px}.ui-icon-arrowthick-1-ne{background-position:-16px -48px}.ui-icon-arrowthick-1-e{background-position:-32px -48px}.ui-icon-arrowthick-1-se{background-position:-48px -48px}.ui-icon-arrowthick-1-s{background-position:-64px -48px}.ui-icon-arrowthick-1-sw{background-position:-80px -48px}.ui-icon-arrowthick-1-w{background-position:-96px -48px}.ui-icon-arrowthick-1-nw{background-position:-112px -48px}.ui-icon-arrowthick-2-n-s{background-position:-128px -48px}.ui-icon-arrowthick-2-ne-sw{background-position:-144px -48px}.ui-icon-arrowthick-2-e-w{background-position:-160px -48px}.ui-icon-arrowthick-2-se-nw{background-position:-176px -48px}.ui-icon-arrowthickstop-1-n{background-position:-192px -48px}.ui-icon-arrowthickstop-1-e{background-position:-208px -48px}.ui-icon-arrowthickstop-1-s{background-position:-224px -48px}.ui-icon-arrowthickstop-1-w{background-position:-240px -48px}.ui-icon-arrowreturnthick-1-w{background-position:0 -64px}.ui-icon-arrowreturnthick-1-n{background-position:-16px -64px}.ui-icon-arrowreturnthick-1-e{background-position:-32px -64px}.ui-icon-arrowreturnthick-1-s{background-position:-48px -64px}.ui-icon-arrowreturn-1-w{background-position:-64px -64px}.ui-icon-arrowreturn-1-n{background-position:-80px -64px}.ui-icon-arrowreturn-1-e{background-position:-96px -64px}.ui-icon-arrowreturn-1-s{background-position:-112px -64px}.ui-icon-arrowrefresh-1-w{background-position:-128px -64px}.ui-icon-arrowrefresh-1-n{background-position:-144px -64px}.ui-icon-arrowrefresh-1-e{background-position:-160px -64px}.ui-icon-arrowrefresh-1-s{background-position:-176px -64px}.ui-icon-arrow-4{background-position:0 -80px}.ui-icon-arrow-4-diag{background-position:-16px -80px}.ui-icon-extlink{background-position:-32px -80px}.ui-icon-newwin{background-position:-48px -80px}.ui-icon-refresh{background-position:-64px -80px}.ui-icon-shuffle{background-position:-80px -80px}.ui-icon-transfer-e-w{background-position:-96px -80px}.ui-icon-transferthick-e-w{background-position:-112px -80px}.ui-icon-folder-collapsed{background-position:0 -96px}.ui-icon-folder-open{background-position:-16px -96px}.ui-icon-document{background-position:-32px -96px}.ui-icon-document-b{background-position:-48px -96px}.ui-icon-note{background-position:-64px -96px}.ui-icon-mail-closed{background-position:-80px -96px}.ui-icon-mail-open{background-position:-96px -96px}.ui-icon-suitcase{background-position:-112px -96px}.ui-icon-comment{background-position:-128px -96px}.ui-icon-person{background-position:-144px -96px}.ui-icon-print{background-position:-160px -96px}.ui-icon-trash{background-position:-176px -96px}.ui-icon-locked{background-position:-192px -96px}.ui-icon-unlocked{background-position:-208px -96px}.ui-icon-bookmark{background-position:-224px -96px}.ui-icon-tag{background-position:-240px -96px}.ui-icon-home{background-position:0 -112px}.ui-icon-flag{background-position:-16px -112px}.ui-icon-calendar{background-position:-32px -112px}.ui-icon-cart{background-position:-48px -112px}.ui-icon-pencil{background-position:-64px -112px}.ui-icon-clock{background-position:-80px -112px}.ui-icon-disk{background-position:-96px -112px}.ui-icon-calculator{background-position:-112px -112px}.ui-icon-zoomin{background-position:-128px -112px}.ui-icon-zoomout{background-position:-144px -112px}.ui-icon-search{background-position:-160px -112px}.ui-icon-wrench{background-position:-176px -112px}.ui-icon-gear{background-position:-192px -112px}.ui-icon-heart{background-position:-208px -112px}.ui-icon-star{background-position:-224px -112px}.ui-icon-link{background-position:-240px -112px}.ui-icon-cancel{background-position:0 -128px}.ui-icon-plus{background-position:-16px -128px}.ui-icon-plusthick{background-position:-32px -128px}.ui-icon-minus{background-position:-48px -128px}.ui-icon-minusthick{background-position:-64px -128px}.ui-icon-close{background-position:-80px -128px}.ui-icon-closethick{background-position:-96px -128px}.ui-icon-key{background-position:-112px -128px}.ui-icon-lightbulb{background-position:-128px -128px}.ui-icon-scissors{background-position:-144px -128px}.ui-icon-clipboard{background-position:-160px -128px}.ui-icon-copy{background-position:-176px -128px}.ui-icon-contact{background-position:-192px -128px}.ui-icon-image{background-position:-208px -128px}.ui-icon-video{background-position:-224px -128px}.ui-icon-script{background-position:-240px -128px}.ui-icon-alert{background-position:0 -144px}.ui-icon-info{background-position:-16px -144px}.ui-icon-notice{background-position:-32px -144px}.ui-icon-help{background-position:-48px -144px}.ui-icon-check{background-position:-64px -144px}.ui-icon-bullet{background-position:-80px -144px}.ui-icon-radio-on{background-position:-96px -144px}.ui-icon-radio-off{background-position:-112px -144px}.ui-icon-pin-w{background-position:-128px -144px}.ui-icon-pin-s{background-position:-144px -144px}.ui-icon-play{background-position:0 -160px}.ui-icon-pause{background-position:-16px -160px}.ui-icon-seek-next{background-position:-32px -160px}.ui-icon-seek-prev{background-position:-48px -160px}.ui-icon-seek-end{background-position:-64px -160px}.ui-icon-seek-start{background-position:-80px -160px}.ui-icon-seek-first{background-position:-80px -160px}.ui-icon-stop{background-position:-96px -160px}.ui-icon-eject{background-position:-112px -160px}.ui-icon-volume-off{background-position:-128px -160px}.ui-icon-volume-on{background-position:-144px -160px}.ui-icon-power{background-position:0 -176px}.ui-icon-signal-diag{background-position:-16px -176px}.ui-icon-signal{background-position:-32px -176px}.ui-icon-battery-0{background-position:-48px -176px}.ui-icon-battery-1{background-position:-64px -176px}.ui-icon-battery-2{background-position:-80px -176px}.ui-icon-battery-3{background-position:-96px -176px}.ui-icon-circle-plus{background-position:0 -192px}.ui-icon-circle-minus{background-position:-16px -192px}.ui-icon-circle-close{background-position:-32px -192px}.ui-icon-circle-triangle-e{background-position:-48px -192px}.ui-icon-circle-triangle-s{background-position:-64px -192px}.ui-icon-circle-triangle-w{background-position:-80px -192px}.ui-icon-circle-triangle-n{background-position:-96px -192px}.ui-icon-circle-arrow-e{background-position:-112px -192px}.ui-icon-circle-arrow-s{background-position:-128px -192px}.ui-icon-circle-arrow-w{background-position:-144px -192px}.ui-icon-circle-arrow-n{background-position:-160px -192px}.ui-icon-circle-zoomin{background-position:-176px -192px}.ui-icon-circle-zoomout{background-position:-192px -192px}.ui-icon-circle-check{background-position:-208px -192px}.ui-icon-circlesmall-plus{background-position:0 -208px}.ui-icon-circlesmall-minus{background-position:-16px -208px}.ui-icon-circlesmall-close{background-position:-32px -208px}.ui-icon-squaresmall-plus{background-position:-48px -208px}.ui-icon-squaresmall-minus{background-position:-64px -208px}.ui-icon-squaresmall-close{background-position:-80px -208px}.ui-icon-grip-dotted-vertical{background-position:0 -224px}.ui-icon-grip-dotted-horizontal{background-position:-16px -224px}.ui-icon-grip-solid-vertical{background-position:-32px -224px}.ui-icon-grip-solid-horizontal{background-position:-48px -224px}.ui-icon-gripsmall-diagonal-se{background-position:-64px -224px}.ui-icon-grip-diagonal-se{background-position:-80px -224px}.ui-corner-all,.ui-corner-top,.ui-corner-left,.ui-corner-tl{border-top-left-radius:4px}.ui-corner-all,.ui-corner-top,.ui-corner-right,.ui-corner-tr{border-top-right-radius:4px}.ui-corner-all,.ui-corner-bottom,.ui-corner-left,.ui-corner-bl{border-bottom-left-radius:4px}.ui-corner-all,.ui-corner-bottom,.ui-corner-right,.ui-corner-br{border-bottom-right-radius:4px}.ui-widget-overlay{background:#666 url("http://github.com/images/ui-bg_diagonals-thick_20_666666_40x40.png") 50% 50% repeat;opacity:.5;filter:Alpha(Opacity=50)}.ui-widget-shadow{margin:-5px 0 0 -5px;padding:5px;background:#000 url("http://github.com/images/ui-bg_flat_10_000000_40x100.png") 50% 50% repeat-x;opacity:.2;filter:Alpha(Opacity=20);border-radius:5px} \ No newline at end of file diff --git a/elasticsearch-persistence/examples/music/vendor/assets/jquery-ui-1.10.4.custom.min.js b/elasticsearch-persistence/examples/music/vendor/assets/jquery-ui-1.10.4.custom.min.js deleted file mode 100755 index 8af84cb1e..000000000 --- a/elasticsearch-persistence/examples/music/vendor/assets/jquery-ui-1.10.4.custom.min.js +++ /dev/null @@ -1,6 +0,0 @@ -/*! jQuery UI - v1.10.4 - 2014-06-05 -* http://jqueryui.com -* Includes: jquery.ui.core.js, jquery.ui.widget.js, jquery.ui.position.js, jquery.ui.autocomplete.js, jquery.ui.menu.js, jquery.ui.effect.js, jquery.ui.effect-highlight.js -* Copyright 2014 jQuery Foundation and other contributors; Licensed MIT */ - -(function(e,t){function i(t,i){var s,a,o,r=t.nodeName.toLowerCase();return"area"===r?(s=t.parentNode,a=s.name,t.href&&a&&"map"===s.nodeName.toLowerCase()?(o=e("img[usemap=#"+a+"]")[0],!!o&&n(o)):!1):(/input|select|textarea|button|object/.test(r)?!t.disabled:"a"===r?t.href||i:i)&&n(t)}function n(t){return e.expr.filters.visible(t)&&!e(t).parents().addBack().filter(function(){return"hidden"===e.css(this,"visibility")}).length}var s=0,a=/^ui-id-\d+$/;e.ui=e.ui||{},e.extend(e.ui,{version:"1.10.4",keyCode:{BACKSPACE:8,COMMA:188,DELETE:46,DOWN:40,END:35,ENTER:13,ESCAPE:27,HOME:36,LEFT:37,NUMPAD_ADD:107,NUMPAD_DECIMAL:110,NUMPAD_DIVIDE:111,NUMPAD_ENTER:108,NUMPAD_MULTIPLY:106,NUMPAD_SUBTRACT:109,PAGE_DOWN:34,PAGE_UP:33,PERIOD:190,RIGHT:39,SPACE:32,TAB:9,UP:38}}),e.fn.extend({focus:function(t){return function(i,n){return"number"==typeof i?this.each(function(){var t=this;setTimeout(function(){e(t).focus(),n&&n.call(t)},i)}):t.apply(this,arguments)}}(e.fn.focus),scrollParent:function(){var t;return t=e.ui.ie&&/(static|relative)/.test(this.css("position"))||/absolute/.test(this.css("position"))?this.parents().filter(function(){return/(relative|absolute|fixed)/.test(e.css(this,"position"))&&/(auto|scroll)/.test(e.css(this,"overflow")+e.css(this,"overflow-y")+e.css(this,"overflow-x"))}).eq(0):this.parents().filter(function(){return/(auto|scroll)/.test(e.css(this,"overflow")+e.css(this,"overflow-y")+e.css(this,"overflow-x"))}).eq(0),/fixed/.test(this.css("position"))||!t.length?e(document):t},zIndex:function(i){if(i!==t)return this.css("zIndex",i);if(this.length)for(var n,s,a=e(this[0]);a.length&&a[0]!==document;){if(n=a.css("position"),("absolute"===n||"relative"===n||"fixed"===n)&&(s=parseInt(a.css("zIndex"),10),!isNaN(s)&&0!==s))return s;a=a.parent()}return 0},uniqueId:function(){return this.each(function(){this.id||(this.id="ui-id-"+ ++s)})},removeUniqueId:function(){return this.each(function(){a.test(this.id)&&e(this).removeAttr("id")})}}),e.extend(e.expr[":"],{data:e.expr.createPseudo?e.expr.createPseudo(function(t){return function(i){return!!e.data(i,t)}}):function(t,i,n){return!!e.data(t,n[3])},focusable:function(t){return i(t,!isNaN(e.attr(t,"tabindex")))},tabbable:function(t){var n=e.attr(t,"tabindex"),s=isNaN(n);return(s||n>=0)&&i(t,!s)}}),e("").outerWidth(1).jquery||e.each(["Width","Height"],function(i,n){function s(t,i,n,s){return e.each(a,function(){i-=parseFloat(e.css(t,"padding"+this))||0,n&&(i-=parseFloat(e.css(t,"border"+this+"Width"))||0),s&&(i-=parseFloat(e.css(t,"margin"+this))||0)}),i}var a="Width"===n?["Left","Right"]:["Top","Bottom"],o=n.toLowerCase(),r={innerWidth:e.fn.innerWidth,innerHeight:e.fn.innerHeight,outerWidth:e.fn.outerWidth,outerHeight:e.fn.outerHeight};e.fn["inner"+n]=function(i){return i===t?r["inner"+n].call(this):this.each(function(){e(this).css(o,s(this,i)+"px")})},e.fn["outer"+n]=function(t,i){return"number"!=typeof t?r["outer"+n].call(this,t):this.each(function(){e(this).css(o,s(this,t,!0,i)+"px")})}}),e.fn.addBack||(e.fn.addBack=function(e){return this.add(null==e?this.prevObject:this.prevObject.filter(e))}),e("").data("a-b","a").removeData("a-b").data("a-b")&&(e.fn.removeData=function(t){return function(i){return arguments.length?t.call(this,e.camelCase(i)):t.call(this)}}(e.fn.removeData)),e.ui.ie=!!/msie [\w.]+/.exec(navigator.userAgent.toLowerCase()),e.support.selectstart="onselectstart"in document.createElement("div"),e.fn.extend({disableSelection:function(){return this.bind((e.support.selectstart?"selectstart":"mousedown")+".ui-disableSelection",function(e){e.preventDefault()})},enableSelection:function(){return this.unbind(".ui-disableSelection")}}),e.extend(e.ui,{plugin:{add:function(t,i,n){var s,a=e.ui[t].prototype;for(s in n)a.plugins[s]=a.plugins[s]||[],a.plugins[s].push([i,n[s]])},call:function(e,t,i){var n,s=e.plugins[t];if(s&&e.element[0].parentNode&&11!==e.element[0].parentNode.nodeType)for(n=0;s.length>n;n++)e.options[s[n][0]]&&s[n][1].apply(e.element,i)}},hasScroll:function(t,i){if("hidden"===e(t).css("overflow"))return!1;var n=i&&"left"===i?"scrollLeft":"scrollTop",s=!1;return t[n]>0?!0:(t[n]=1,s=t[n]>0,t[n]=0,s)}})})(jQuery);(function(t,e){var i=0,s=Array.prototype.slice,n=t.cleanData;t.cleanData=function(e){for(var i,s=0;null!=(i=e[s]);s++)try{t(i).triggerHandler("remove")}catch(o){}n(e)},t.widget=function(i,s,n){var o,a,r,h,l={},c=i.split(".")[0];i=i.split(".")[1],o=c+"-"+i,n||(n=s,s=t.Widget),t.expr[":"][o.toLowerCase()]=function(e){return!!t.data(e,o)},t[c]=t[c]||{},a=t[c][i],r=t[c][i]=function(t,i){return this._createWidget?(arguments.length&&this._createWidget(t,i),e):new r(t,i)},t.extend(r,a,{version:n.version,_proto:t.extend({},n),_childConstructors:[]}),h=new s,h.options=t.widget.extend({},h.options),t.each(n,function(i,n){return t.isFunction(n)?(l[i]=function(){var t=function(){return s.prototype[i].apply(this,arguments)},e=function(t){return s.prototype[i].apply(this,t)};return function(){var i,s=this._super,o=this._superApply;return this._super=t,this._superApply=e,i=n.apply(this,arguments),this._super=s,this._superApply=o,i}}(),e):(l[i]=n,e)}),r.prototype=t.widget.extend(h,{widgetEventPrefix:a?h.widgetEventPrefix||i:i},l,{constructor:r,namespace:c,widgetName:i,widgetFullName:o}),a?(t.each(a._childConstructors,function(e,i){var s=i.prototype;t.widget(s.namespace+"."+s.widgetName,r,i._proto)}),delete a._childConstructors):s._childConstructors.push(r),t.widget.bridge(i,r)},t.widget.extend=function(i){for(var n,o,a=s.call(arguments,1),r=0,h=a.length;h>r;r++)for(n in a[r])o=a[r][n],a[r].hasOwnProperty(n)&&o!==e&&(i[n]=t.isPlainObject(o)?t.isPlainObject(i[n])?t.widget.extend({},i[n],o):t.widget.extend({},o):o);return i},t.widget.bridge=function(i,n){var o=n.prototype.widgetFullName||i;t.fn[i]=function(a){var r="string"==typeof a,h=s.call(arguments,1),l=this;return a=!r&&h.length?t.widget.extend.apply(null,[a].concat(h)):a,r?this.each(function(){var s,n=t.data(this,o);return n?t.isFunction(n[a])&&"_"!==a.charAt(0)?(s=n[a].apply(n,h),s!==n&&s!==e?(l=s&&s.jquery?l.pushStack(s.get()):s,!1):e):t.error("no such method '"+a+"' for "+i+" widget instance"):t.error("cannot call methods on "+i+" prior to initialization; "+"attempted to call method '"+a+"'")}):this.each(function(){var e=t.data(this,o);e?e.option(a||{})._init():t.data(this,o,new n(a,this))}),l}},t.Widget=function(){},t.Widget._childConstructors=[],t.Widget.prototype={widgetName:"widget",widgetEventPrefix:"",defaultElement:"
",options:{disabled:!1,create:null},_createWidget:function(e,s){s=t(s||this.defaultElement||this)[0],this.element=t(s),this.uuid=i++,this.eventNamespace="."+this.widgetName+this.uuid,this.options=t.widget.extend({},this.options,this._getCreateOptions(),e),this.bindings=t(),this.hoverable=t(),this.focusable=t(),s!==this&&(t.data(s,this.widgetFullName,this),this._on(!0,this.element,{remove:function(t){t.target===s&&this.destroy()}}),this.document=t(s.style?s.ownerDocument:s.document||s),this.window=t(this.document[0].defaultView||this.document[0].parentWindow)),this._create(),this._trigger("create",null,this._getCreateEventData()),this._init()},_getCreateOptions:t.noop,_getCreateEventData:t.noop,_create:t.noop,_init:t.noop,destroy:function(){this._destroy(),this.element.unbind(this.eventNamespace).removeData(this.widgetName).removeData(this.widgetFullName).removeData(t.camelCase(this.widgetFullName)),this.widget().unbind(this.eventNamespace).removeAttr("aria-disabled").removeClass(this.widgetFullName+"-disabled "+"ui-state-disabled"),this.bindings.unbind(this.eventNamespace),this.hoverable.removeClass("ui-state-hover"),this.focusable.removeClass("ui-state-focus")},_destroy:t.noop,widget:function(){return this.element},option:function(i,s){var n,o,a,r=i;if(0===arguments.length)return t.widget.extend({},this.options);if("string"==typeof i)if(r={},n=i.split("."),i=n.shift(),n.length){for(o=r[i]=t.widget.extend({},this.options[i]),a=0;n.length-1>a;a++)o[n[a]]=o[n[a]]||{},o=o[n[a]];if(i=n.pop(),1===arguments.length)return o[i]===e?null:o[i];o[i]=s}else{if(1===arguments.length)return this.options[i]===e?null:this.options[i];r[i]=s}return this._setOptions(r),this},_setOptions:function(t){var e;for(e in t)this._setOption(e,t[e]);return this},_setOption:function(t,e){return this.options[t]=e,"disabled"===t&&(this.widget().toggleClass(this.widgetFullName+"-disabled ui-state-disabled",!!e).attr("aria-disabled",e),this.hoverable.removeClass("ui-state-hover"),this.focusable.removeClass("ui-state-focus")),this},enable:function(){return this._setOption("disabled",!1)},disable:function(){return this._setOption("disabled",!0)},_on:function(i,s,n){var o,a=this;"boolean"!=typeof i&&(n=s,s=i,i=!1),n?(s=o=t(s),this.bindings=this.bindings.add(s)):(n=s,s=this.element,o=this.widget()),t.each(n,function(n,r){function h(){return i||a.options.disabled!==!0&&!t(this).hasClass("ui-state-disabled")?("string"==typeof r?a[r]:r).apply(a,arguments):e}"string"!=typeof r&&(h.guid=r.guid=r.guid||h.guid||t.guid++);var l=n.match(/^(\w+)\s*(.*)$/),c=l[1]+a.eventNamespace,u=l[2];u?o.delegate(u,c,h):s.bind(c,h)})},_off:function(t,e){e=(e||"").split(" ").join(this.eventNamespace+" ")+this.eventNamespace,t.unbind(e).undelegate(e)},_delay:function(t,e){function i(){return("string"==typeof t?s[t]:t).apply(s,arguments)}var s=this;return setTimeout(i,e||0)},_hoverable:function(e){this.hoverable=this.hoverable.add(e),this._on(e,{mouseenter:function(e){t(e.currentTarget).addClass("ui-state-hover")},mouseleave:function(e){t(e.currentTarget).removeClass("ui-state-hover")}})},_focusable:function(e){this.focusable=this.focusable.add(e),this._on(e,{focusin:function(e){t(e.currentTarget).addClass("ui-state-focus")},focusout:function(e){t(e.currentTarget).removeClass("ui-state-focus")}})},_trigger:function(e,i,s){var n,o,a=this.options[e];if(s=s||{},i=t.Event(i),i.type=(e===this.widgetEventPrefix?e:this.widgetEventPrefix+e).toLowerCase(),i.target=this.element[0],o=i.originalEvent)for(n in o)n in i||(i[n]=o[n]);return this.element.trigger(i,s),!(t.isFunction(a)&&a.apply(this.element[0],[i].concat(s))===!1||i.isDefaultPrevented())}},t.each({show:"fadeIn",hide:"fadeOut"},function(e,i){t.Widget.prototype["_"+e]=function(s,n,o){"string"==typeof n&&(n={effect:n});var a,r=n?n===!0||"number"==typeof n?i:n.effect||i:e;n=n||{},"number"==typeof n&&(n={duration:n}),a=!t.isEmptyObject(n),n.complete=o,n.delay&&s.delay(n.delay),a&&t.effects&&t.effects.effect[r]?s[e](n):r!==e&&s[r]?s[r](n.duration,n.easing,o):s.queue(function(i){t(this)[e](),o&&o.call(s[0]),i()})}})})(jQuery);(function(t,e){function i(t,e,i){return[parseFloat(t[0])*(p.test(t[0])?e/100:1),parseFloat(t[1])*(p.test(t[1])?i/100:1)]}function s(e,i){return parseInt(t.css(e,i),10)||0}function n(e){var i=e[0];return 9===i.nodeType?{width:e.width(),height:e.height(),offset:{top:0,left:0}}:t.isWindow(i)?{width:e.width(),height:e.height(),offset:{top:e.scrollTop(),left:e.scrollLeft()}}:i.preventDefault?{width:0,height:0,offset:{top:i.pageY,left:i.pageX}}:{width:e.outerWidth(),height:e.outerHeight(),offset:e.offset()}}t.ui=t.ui||{};var a,o=Math.max,r=Math.abs,l=Math.round,h=/left|center|right/,c=/top|center|bottom/,u=/[\+\-]\d+(\.[\d]+)?%?/,d=/^\w+/,p=/%$/,f=t.fn.position;t.position={scrollbarWidth:function(){if(a!==e)return a;var i,s,n=t("
"),o=n.children()[0];return t("body").append(n),i=o.offsetWidth,n.css("overflow","scroll"),s=o.offsetWidth,i===s&&(s=n[0].clientWidth),n.remove(),a=i-s},getScrollInfo:function(e){var i=e.isWindow||e.isDocument?"":e.element.css("overflow-x"),s=e.isWindow||e.isDocument?"":e.element.css("overflow-y"),n="scroll"===i||"auto"===i&&e.widths?"left":i>0?"right":"center",vertical:0>a?"top":n>0?"bottom":"middle"};u>p&&p>r(i+s)&&(l.horizontal="center"),d>g&&g>r(n+a)&&(l.vertical="middle"),l.important=o(r(i),r(s))>o(r(n),r(a))?"horizontal":"vertical",e.using.call(this,t,l)}),c.offset(t.extend(M,{using:h}))})},t.ui.position={fit:{left:function(t,e){var i,s=e.within,n=s.isWindow?s.scrollLeft:s.offset.left,a=s.width,r=t.left-e.collisionPosition.marginLeft,l=n-r,h=r+e.collisionWidth-a-n;e.collisionWidth>a?l>0&&0>=h?(i=t.left+l+e.collisionWidth-a-n,t.left+=l-i):t.left=h>0&&0>=l?n:l>h?n+a-e.collisionWidth:n:l>0?t.left+=l:h>0?t.left-=h:t.left=o(t.left-r,t.left)},top:function(t,e){var i,s=e.within,n=s.isWindow?s.scrollTop:s.offset.top,a=e.within.height,r=t.top-e.collisionPosition.marginTop,l=n-r,h=r+e.collisionHeight-a-n;e.collisionHeight>a?l>0&&0>=h?(i=t.top+l+e.collisionHeight-a-n,t.top+=l-i):t.top=h>0&&0>=l?n:l>h?n+a-e.collisionHeight:n:l>0?t.top+=l:h>0?t.top-=h:t.top=o(t.top-r,t.top)}},flip:{left:function(t,e){var i,s,n=e.within,a=n.offset.left+n.scrollLeft,o=n.width,l=n.isWindow?n.scrollLeft:n.offset.left,h=t.left-e.collisionPosition.marginLeft,c=h-l,u=h+e.collisionWidth-o-l,d="left"===e.my[0]?-e.elemWidth:"right"===e.my[0]?e.elemWidth:0,p="left"===e.at[0]?e.targetWidth:"right"===e.at[0]?-e.targetWidth:0,f=-2*e.offset[0];0>c?(i=t.left+d+p+f+e.collisionWidth-o-a,(0>i||r(c)>i)&&(t.left+=d+p+f)):u>0&&(s=t.left-e.collisionPosition.marginLeft+d+p+f-l,(s>0||u>r(s))&&(t.left+=d+p+f))},top:function(t,e){var i,s,n=e.within,a=n.offset.top+n.scrollTop,o=n.height,l=n.isWindow?n.scrollTop:n.offset.top,h=t.top-e.collisionPosition.marginTop,c=h-l,u=h+e.collisionHeight-o-l,d="top"===e.my[1],p=d?-e.elemHeight:"bottom"===e.my[1]?e.elemHeight:0,f="top"===e.at[1]?e.targetHeight:"bottom"===e.at[1]?-e.targetHeight:0,g=-2*e.offset[1];0>c?(s=t.top+p+f+g+e.collisionHeight-o-a,t.top+p+f+g>c&&(0>s||r(c)>s)&&(t.top+=p+f+g)):u>0&&(i=t.top-e.collisionPosition.marginTop+p+f+g-l,t.top+p+f+g>u&&(i>0||u>r(i))&&(t.top+=p+f+g))}},flipfit:{left:function(){t.ui.position.flip.left.apply(this,arguments),t.ui.position.fit.left.apply(this,arguments)},top:function(){t.ui.position.flip.top.apply(this,arguments),t.ui.position.fit.top.apply(this,arguments)}}},function(){var e,i,s,n,a,o=document.getElementsByTagName("body")[0],r=document.createElement("div");e=document.createElement(o?"div":"body"),s={visibility:"hidden",width:0,height:0,border:0,margin:0,background:"none"},o&&t.extend(s,{position:"absolute",left:"-1000px",top:"-1000px"});for(a in s)e.style[a]=s[a];e.appendChild(r),i=o||document.documentElement,i.insertBefore(e,i.firstChild),r.style.cssText="position: absolute; left: 10.7432222px;",n=t(r).offset().left,t.support.offsetFractions=n>10&&11>n,e.innerHTML="",i.removeChild(e)}()})(jQuery);(function(e){e.widget("ui.autocomplete",{version:"1.10.4",defaultElement:"",options:{appendTo:null,autoFocus:!1,delay:300,minLength:1,position:{my:"left top",at:"left bottom",collision:"none"},source:null,change:null,close:null,focus:null,open:null,response:null,search:null,select:null},requestIndex:0,pending:0,_create:function(){var t,i,s,n=this.element[0].nodeName.toLowerCase(),a="textarea"===n,o="input"===n;this.isMultiLine=a?!0:o?!1:this.element.prop("isContentEditable"),this.valueMethod=this.element[a||o?"val":"text"],this.isNewMenu=!0,this.element.addClass("ui-autocomplete-input").attr("autocomplete","off"),this._on(this.element,{keydown:function(n){if(this.element.prop("readOnly"))return t=!0,s=!0,i=!0,undefined;t=!1,s=!1,i=!1;var a=e.ui.keyCode;switch(n.keyCode){case a.PAGE_UP:t=!0,this._move("previousPage",n);break;case a.PAGE_DOWN:t=!0,this._move("nextPage",n);break;case a.UP:t=!0,this._keyEvent("previous",n);break;case a.DOWN:t=!0,this._keyEvent("next",n);break;case a.ENTER:case a.NUMPAD_ENTER:this.menu.active&&(t=!0,n.preventDefault(),this.menu.select(n));break;case a.TAB:this.menu.active&&this.menu.select(n);break;case a.ESCAPE:this.menu.element.is(":visible")&&(this._value(this.term),this.close(n),n.preventDefault());break;default:i=!0,this._searchTimeout(n)}},keypress:function(s){if(t)return t=!1,(!this.isMultiLine||this.menu.element.is(":visible"))&&s.preventDefault(),undefined;if(!i){var n=e.ui.keyCode;switch(s.keyCode){case n.PAGE_UP:this._move("previousPage",s);break;case n.PAGE_DOWN:this._move("nextPage",s);break;case n.UP:this._keyEvent("previous",s);break;case n.DOWN:this._keyEvent("next",s)}}},input:function(e){return s?(s=!1,e.preventDefault(),undefined):(this._searchTimeout(e),undefined)},focus:function(){this.selectedItem=null,this.previous=this._value()},blur:function(e){return this.cancelBlur?(delete this.cancelBlur,undefined):(clearTimeout(this.searching),this.close(e),this._change(e),undefined)}}),this._initSource(),this.menu=e("