diff --git a/elasticsearch-model/Rakefile b/elasticsearch-model/Rakefile index bda261913..e479d3599 100644 --- a/elasticsearch-model/Rakefile +++ b/elasticsearch-model/Rakefile @@ -34,6 +34,10 @@ namespace :test do desc "Run unit tests against ActiveModel 3, 4 and 5" task :unit do + end + + desc "Run integration tests against latest stable ActiveModel (5)" + task :integration do ['3.0.gemfile', '4.0.gemfile', '5.0.gemfile'].each do |gemfile| ['bundle exec rake test:run_unit', 'bundle exec rspec'].each do |cmd| sh "BUNDLE_GEMFILE='#{File.expand_path('../gemfiles/'+gemfile, __FILE__)}' #{cmd}" @@ -41,13 +45,6 @@ namespace :test do end end - 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/5.0.gemfile', __FILE__)}' bundle exec rake test:run_integration" - end - desc "Run unit and integration tests" task :all do Rake::Task['test:unit'].invoke diff --git a/elasticsearch-model/gemfiles/4.0.gemfile b/elasticsearch-model/gemfiles/4.0.gemfile index eea429e12..f8acebc38 100644 --- a/elasticsearch-model/gemfiles/4.0.gemfile +++ b/elasticsearch-model/gemfiles/4.0.gemfile @@ -10,6 +10,7 @@ gemspec path: '../' gem 'activemodel', '~> 4' gem 'activerecord', '~> 4' gem 'sqlite3' unless defined?(JRUBY_VERSION) +gem 'mongoid', '~> 5' group :development, :testing do gem 'rspec' diff --git a/elasticsearch-model/gemfiles/5.0.gemfile b/elasticsearch-model/gemfiles/5.0.gemfile index aa8e77655..612f2bbd9 100644 --- a/elasticsearch-model/gemfiles/5.0.gemfile +++ b/elasticsearch-model/gemfiles/5.0.gemfile @@ -10,6 +10,7 @@ gemspec path: '../' gem 'activemodel', '~> 5' gem 'activerecord', '~> 5' gem 'sqlite3' unless defined?(JRUBY_VERSION) +gem 'mongoid', '~> 6' group :development, :testing do gem 'rspec' diff --git a/elasticsearch-model/spec/elasticsearch/model/adapter_spec.rb b/elasticsearch-model/spec/elasticsearch/model/adapter_spec.rb index 01526c12b..71fd2e6b5 100644 --- a/elasticsearch-model/spec/elasticsearch/model/adapter_spec.rb +++ b/elasticsearch-model/spec/elasticsearch/model/adapter_spec.rb @@ -13,12 +13,10 @@ class ::DummyAdapter end after(:all) do - Elasticsearch::Model::Adapter::Adapter.adapters.delete(DummyAdapterClassWithAdapter) - Elasticsearch::Model::Adapter::Adapter.adapters.delete(DummyAdapterClass) - Elasticsearch::Model::Adapter::Adapter.adapters.delete(DummyAdapter) - Object.send(:remove_const, :DummyAdapterClass) if defined?(DummyAdapterClass) - Object.send(:remove_const, :DummyAdapterClassWithAdapter) if defined?(DummyAdapterClassWithAdapter) - Object.send(:remove_const, :DummyAdapter) if defined?(DummyAdapter) + [DummyAdapterClassWithAdapter, DummyAdapterClass, DummyAdapter].each do |adapter| + Elasticsearch::Model::Adapter::Adapter.adapters.delete(adapter) + end + remove_classes(DummyAdapterClass, DummyAdapterClassWithAdapter, DummyAdapter) end describe '#from_class' do diff --git a/elasticsearch-model/spec/elasticsearch/model/adapters/active_record/associations_spec.rb b/elasticsearch-model/spec/elasticsearch/model/adapters/active_record/associations_spec.rb new file mode 100644 index 000000000..0cfed6432 --- /dev/null +++ b/elasticsearch-model/spec/elasticsearch/model/adapters/active_record/associations_spec.rb @@ -0,0 +1,334 @@ +require 'spec_helper' + +describe 'Elasticsearch::Model::Adapter::ActiveRecord Associations' do + + before(:all) do + ActiveRecord::Schema.define(version: 1) do + create_table :categories do |t| + t.string :title + t.timestamps null: false + end + + create_table :categories_posts do |t| + t.references :post, :category + end + + create_table :authors do |t| + t.string :first_name, :last_name + t.timestamps null: false + end + + create_table :authorships do |t| + t.string :first_name, :last_name + t.references :post + t.references :author + t.timestamps null: false + end + + create_table :comments do |t| + t.string :text + t.string :author + t.references :post + t.timestamps null: false + end + + add_index(:comments, :post_id) unless index_exists?(:comments, :post_id) + + create_table :posts do |t| + t.string :title + t.text :text + t.boolean :published + t.timestamps null: false + end + end + + Comment.__send__ :include, Elasticsearch::Model + Comment.__send__ :include, Elasticsearch::Model::Callbacks + end + + before do + clear_tables(:categories, :categories_posts, :authors, :authorships, :comments, :posts) + clear_indices(Post) + Post.__elasticsearch__.create_index!(force: true) + Comment.__elasticsearch__.create_index!(force: true) + end + + after do + clear_tables(Post, Category) + clear_indices(Post) + end + + context 'when a document is created' do + + before do + Post.create!(title: 'Test') + Post.create!(title: 'Testing Coding') + Post.create!(title: 'Coding') + Post.__elasticsearch__.refresh_index! + end + + let(:search_result) do + Post.search('title:test') + end + + it 'indexes the document' do + expect(search_result.results.size).to eq(2) + expect(search_result.results.first.title).to eq('Test') + expect(search_result.records.size).to eq(2) + expect(search_result.records.first.title).to eq('Test') + end + end + + describe 'has_many_and_belongs_to association' do + + context 'when an association is updated' do + + before do + post.categories = [category_a, category_b] + Post.__elasticsearch__.refresh_index! + end + + let(:category_a) do + Category.where(title: "One").first_or_create! + end + + let(:category_b) do + Category.where(title: "Two").first_or_create! + end + + let(:post) do + Post.create! title: "First Post", text: "This is the first post..." + end + + let(:search_result) do + Post.search(query: { + bool: { + must: { + multi_match: { + fields: ['title'], + query: 'first' + } + }, + filter: { + terms: { + categories: ['One'] + } + } + } + } ) + end + + it 'applies the update with' do + expect(search_result.results.size).to eq(1) + expect(search_result.results.first.title).to eq('First Post') + expect(search_result.records.size).to eq(1) + expect(search_result.records.first.title).to eq('First Post') + end + end + + context 'when an association is deleted' do + + before do + post.categories = [category_a, category_b] + post.categories = [category_b] + Post.__elasticsearch__.refresh_index! + end + + let(:category_a) do + Category.where(title: "One").first_or_create! + end + + let(:category_b) do + Category.where(title: "Two").first_or_create! + end + + let(:post) do + Post.create! title: "First Post", text: "This is the first post..." + end + + let(:search_result) do + Post.search(query: { + bool: { + must: { + multi_match: { + fields: ['title'], + query: 'first' + } + }, + filter: { + terms: { + categories: ['One'] + } + } + } + } ) + end + + it 'applies the update with a reindex' do + expect(search_result.results.size).to eq(0) + expect(search_result.records.size).to eq(0) + end + end + end + + describe 'has_many through association' do + + context 'when the association is updated' do + + before do + author_a = Author.where(first_name: "John", last_name: "Smith").first_or_create! + author_b = Author.where(first_name: "Mary", last_name: "Smith").first_or_create! + author_c = Author.where(first_name: "Kobe", last_name: "Griss").first_or_create! + + # Create posts + post_1 = Post.create!(title: "First Post", text: "This is the first post...") + post_2 = Post.create!(title: "Second Post", text: "This is the second post...") + post_3 = Post.create!(title: "Third Post", text: "This is the third post...") + + # Assign authors + post_1.authors = [author_a, author_b] + post_2.authors = [author_a] + post_3.authors = [author_c] + + Post.__elasticsearch__.refresh_index! + end + + context 'if active record is at least 4' do + + let(:search_result) do + Post.search('authors.full_name:john') + end + + it 'applies the update', if: active_record_at_least_4? do + expect(search_result.results.size).to eq(2) + expect(search_result.records.size).to eq(2) + end + end + + context 'if active record is less than 4' do + + let(:search_result) do + Post.search('authors.author.full_name:john') + end + + it 'applies the update', if: !active_record_at_least_4? do + expect(search_result.results.size).to eq(2) + expect(search_result.records.size).to eq(2) + end + end + end + + context 'when an association is added', if: active_record_at_least_4? do + + before do + author_a = Author.where(first_name: "John", last_name: "Smith").first_or_create! + author_b = Author.where(first_name: "Mary", last_name: "Smith").first_or_create! + + # Create posts + post_1 = Post.create!(title: "First Post", text: "This is the first post...") + + # Assign authors + post_1.authors = [author_a] + post_1.authors << author_b + Post.__elasticsearch__.refresh_index! + end + + let(:search_result) do + Post.search('authors.full_name:john') + end + + it 'adds the association' do + expect(search_result.results.size).to eq(1) + expect(search_result.records.size).to eq(1) + end + end + end + + describe 'has_many association' do + + context 'when an association is added', if: active_record_at_least_4? do + + before do + # Create posts + post_1 = Post.create!(title: "First Post", text: "This is the first post...") + post_2 = Post.create!(title: "Second Post", text: "This is the second post...") + + # Add comments + post_1.comments.create!(author: 'John', text: 'Excellent') + post_1.comments.create!(author: 'Abby', text: 'Good') + + post_2.comments.create!(author: 'John', text: 'Terrible') + + post_1.comments.create!(author: 'John', text: 'Or rather just good...') + Post.__elasticsearch__.refresh_index! + end + + let(:search_result) do + Post.search(query: { + nested: { + path: 'comments', + query: { + bool: { + must: [ + { match: { 'comments.author' => 'john' } }, + { match: { 'comments.text' => 'good' } } + ] + } + } + } + }) + end + + it 'adds the association' do + expect(search_result.results.size).to eq(1) + end + end + end + + describe '#touch' do + + context 'when a touch callback is defined on the model' do + + before do + # Create categories + category_a = Category.where(title: "One").first_or_create! + + # Create post + post = Post.create!(title: "First Post", text: "This is the first post...") + + # Assign category + post.categories << category_a + category_a.update_attribute(:title, "Updated") + category_a.posts.each { |p| p.touch } + + Post.__elasticsearch__.refresh_index! + end + + it 'executes the callback after #touch' do + expect(Post.search('categories:One').size).to eq(0) + expect(Post.search('categories:Updated').size).to eq(1) + end + end + end + + describe '#includes' do + + before do + post_1 = Post.create(title: 'One') + post_2 = Post.create(title: 'Two') + post_1.comments.create(text: 'First comment') + post_2.comments.create(text: 'Second comment') + + Comment.__elasticsearch__.refresh_index! + end + + let(:search_result) do + Comment.search('first').records(includes: :post) + end + + it 'eager loads associations' do + expect(search_result.first.association(:post)).to be_loaded + expect(search_result.first.post.title).to eq('One') + end + end +end diff --git a/elasticsearch-model/spec/elasticsearch/model/adapters/active_record/basic_spec.rb b/elasticsearch-model/spec/elasticsearch/model/adapters/active_record/basic_spec.rb new file mode 100644 index 000000000..a4d9c05c5 --- /dev/null +++ b/elasticsearch-model/spec/elasticsearch/model/adapters/active_record/basic_spec.rb @@ -0,0 +1,340 @@ +require 'spec_helper' + +describe Elasticsearch::Model::Adapter::ActiveRecord do + + before(:all) do + ActiveRecord::Schema.define(:version => 1) do + create_table :articles do |t| + t.string :title + t.string :body + t.integer :clicks, :default => 0 + t.datetime :created_at, :default => 'NOW()' + end + end + + Article.delete_all + Article.__elasticsearch__.create_index!(force: true) + + Article.create!(title: 'Test', body: '', clicks: 1) + Article.create!(title: 'Testing Coding', body: '', clicks: 2) + Article.create!(title: 'Coding', body: '', clicks: 3) + + Article.__elasticsearch__.refresh_index! + end + + describe 'indexing a document' do + + let(:search_result) do + Article.search('title:test') + end + + it 'allows searching for documents' do + expect(search_result.results.size).to be(2) + expect(search_result.records.size).to be(2) + end + end + + describe '#results' do + + let(:search_result) do + Article.search('title:test') + end + + it 'returns an instance of Response::Result' do + expect(search_result.results.first).to be_a(Elasticsearch::Model::Response::Result) + end + + it 'prooperly loads the document' do + expect(search_result.results.first.title).to eq('Test') + end + + context 'when the result contains other data' do + + let(:search_result) do + Article.search(query: { match: { title: 'test' } }, highlight: { fields: { title: {} } }) + end + + it 'allows access to the Elasticsearch result' do + expect(search_result.results.first.title).to eq('Test') + expect(search_result.results.first.title?).to be(true) + expect(search_result.results.first.boo?).to be(false) + expect(search_result.results.first.highlight?).to be(true) + expect(search_result.results.first.highlight.title?).to be(true) + expect(search_result.results.first.highlight.boo?).to be(false) + end + end + end + + describe '#records' do + + let(:search_result) do + Article.search('title:test') + end + + it 'returns an instance of the model' do + expect(search_result.records.first).to be_a(Article) + end + + it 'prooperly loads the document' do + expect(search_result.records.first.title).to eq('Test') + end + end + + describe 'Enumerable' do + + let(:search_result) do + Article.search('title:test') + end + + it 'allows iteration over results' do + expect(search_result.results.map(&:_id)).to eq(['1', '2']) + end + + it 'allows iteration over records' do + expect(search_result.records.map(&:id)).to eq([1, 2]) + end + end + + describe '#id' do + + let(:search_result) do + Article.search('title:test') + end + + it 'returns the id' do + expect(search_result.results.first.id).to eq('1') + end + end + + describe '#id' do + + let(:search_result) do + Article.search('title:test') + end + + it 'returns the type' do + expect(search_result.results.first.type).to eq('article') + end + end + + describe '#each_with_hit' do + + let(:search_result) do + Article.search('title:test') + end + + it 'returns the record with the Elasticsearch hit' do + search_result.records.each_with_hit do |r, h| + expect(h._score).not_to be_nil + expect(h._source.title).not_to be_nil + end + end + end + + describe 'search results order' do + + let(:search_result) do + Article.search(query: { match: { title: 'code' }}, sort: { clicks: :desc }) + end + + it 'preserves the search results order when accessing a single record' do + expect(search_result.records[0].clicks).to be(3) + expect(search_result.records[1].clicks).to be(2) + expect(search_result.records.first).to eq(search_result.records[0]) + end + + it 'preserves the search results order for the list of records' do + search_result.records.each_with_hit do |r, h| + expect(r.id.to_s).to eq(h._id) + end + + search_result.records.map_with_hit do |r, h| + expect(r.id.to_s).to eq(h._id) + end + end + end + + describe 'a paged collection' do + + let(:search_result) do + Article.search(query: { match: { title: { query: 'test' } } }, + size: 2, + from: 1) + end + + it 'applies the paged options to the search' do + expect(search_result.results.size).to eq(1) + expect(search_result.results.first.title).to eq('Testing Coding') + expect(search_result.records.size).to eq(1) + expect(search_result.records.first.title).to eq('Testing Coding') + end + end + + describe '#destroy' do + + before do + Article.create!(title: 'destroy', body: '', clicks: 1) + Article.__elasticsearch__.refresh_index! + Article.where(title: 'destroy').first.destroy + + Article.__elasticsearch__.refresh_index! + end + + let(:search_result) do + Article.search('title:test') + end + + it 'removes the document from the index' do + expect(Article.count).to eq(3) + expect(search_result.results.size).to eq(2) + expect(search_result.records.size).to eq(2) + end + end + + describe 'full document updates' do + + before do + article = Article.create!(title: 'update', body: '', clicks: 1) + Article.__elasticsearch__.refresh_index! + article.title = 'Writing' + article.save + + Article.__elasticsearch__.refresh_index! + end + + let(:search_result) do + Article.search('title:write') + end + + it 'applies the update' do + expect(search_result.results.size).to eq(1) + expect(search_result.records.size).to eq(1) + end + end + + describe 'attribute updates' do + + before do + article = Article.create!(title: 'update', body: '', clicks: 1) + Article.__elasticsearch__.refresh_index! + article.title = 'special' + article.save + + Article.__elasticsearch__.refresh_index! + end + + let(:search_result) do + Article.search('title:special') + end + + it 'applies the update' do + expect(search_result.results.size).to eq(1) + expect(search_result.records.size).to eq(1) + end + end + + describe '#save' do + + before do + article = Article.create!(title: 'save', body: '', clicks: 1) + + ActiveRecord::Base.transaction do + article.body = 'dummy' + article.save + + article.title = 'special' + article.save + end + + article.__elasticsearch__.update_document + Article.__elasticsearch__.refresh_index! + end + + let(:search_result) do + Article.search('body:dummy') + end + + it 'applies the save' do + expect(search_result.results.size).to eq(1) + expect(search_result.records.size).to eq(1) + end + end + + describe 'a DSL search' do + + let(:search_result) do + Article.search(query: { match: { title: { query: 'test' } } }) + end + + it 'returns the results' do + expect(search_result.results.size).to eq(2) + expect(search_result.records.size).to eq(2) + end + end + + describe 'chaining SQL queries on response.records' do + + let(:search_result) do + Article.search(query: { match: { title: { query: 'test' } } }) + end + + it 'executes the SQL request with the chained query criteria' do + expect(search_result.records.size).to eq(2) + expect(search_result.records.where(title: 'Test').size).to eq(1) + expect(search_result.records.where(title: 'Test').first.title).to eq('Test') + end + end + + describe 'ordering of SQL queries' do + + context 'when order is called on the ActiveRecord query' do + + let(:search_result) do + Article.search query: { match: { title: { query: 'test' } } } + end + + it 'allows the SQL query to be ordered independent of the Elasticsearch results order', unless: active_record_at_least_4? do + expect(search_result.records.order('title DESC').first.title).to eq('Testing Coding') + expect(search_result.records.order('title DESC')[0].title).to eq('Testing Coding') + end + + it 'allows the SQL query to be ordered independent of the Elasticsearch results order', if: active_record_at_least_4? do + expect(search_result.records.order(title: :desc).first.title).to eq('Testing Coding') + expect(search_result.records.order(title: :desc)[0].title).to eq('Testing Coding') + end + end + + context 'when more methods are chained on the ActiveRecord query' do + + let(:search_result) do + Article.search query: {match: {title: {query: 'test'}}} + end + + it 'allows the SQL query to be ordered independent of the Elasticsearch results order', if: active_record_at_least_4? do + expect(search_result.records.distinct.order(title: :desc).first.title).to eq('Testing Coding') + expect(search_result.records.distinct.order(title: :desc)[0].title).to eq('Testing Coding') + end + end + end + + describe 'access to the response via methods' do + + let(:search_result) do + Article.search(query: { match: { title: { query: 'test' } } }, + aggregations: { + dates: { date_histogram: { field: 'created_at', interval: 'hour' } }, + clicks: { global: {}, aggregations: { min: { min: { field: 'clicks' } } } } + }, + suggest: { text: 'tezt', title: { term: { field: 'title', suggest_mode: 'always' } } }) + end + + it 'allows document keys to be access via methods' do + expect(search_result.aggregations.dates.buckets.first.doc_count).to eq(2) + expect(search_result.aggregations.clicks.doc_count).to eq(6) + expect(search_result.aggregations.clicks.min.value).to eq(1.0) + expect(search_result.aggregations.clicks.max).to be_nil + expect(search_result.suggestions.title.first.options.size).to eq(1) + expect(search_result.suggestions.terms).to eq(['test']) + end + end +end diff --git a/elasticsearch-model/spec/elasticsearch/model/adapters/active_record/dynamic_index_name_spec.rb b/elasticsearch-model/spec/elasticsearch/model/adapters/active_record/dynamic_index_name_spec.rb new file mode 100644 index 000000000..1a116ec7d --- /dev/null +++ b/elasticsearch-model/spec/elasticsearch/model/adapters/active_record/dynamic_index_name_spec.rb @@ -0,0 +1,18 @@ +require 'spec_helper' + +describe 'Elasticsearch::Model::Adapter::ActiveRecord Dynamic Index naming' do + + before do + ArticleWithDynamicIndexName.counter = 0 + end + + it 'exavlues the index_name value' do + expect(ArticleWithDynamicIndexName.index_name).to eq('articles-1') + end + + it 'revaluates the index name with each call' do + expect(ArticleWithDynamicIndexName.index_name).to eq('articles-1') + expect(ArticleWithDynamicIndexName.index_name).to eq('articles-2') + expect(ArticleWithDynamicIndexName.index_name).to eq('articles-3') + end +end diff --git a/elasticsearch-model/spec/elasticsearch/model/adapters/active_record/import_spec.rb b/elasticsearch-model/spec/elasticsearch/model/adapters/active_record/import_spec.rb new file mode 100644 index 000000000..52301b01a --- /dev/null +++ b/elasticsearch-model/spec/elasticsearch/model/adapters/active_record/import_spec.rb @@ -0,0 +1,187 @@ +require 'spec_helper' + +describe 'Elasticsearch::Model::Adapter::ActiveRecord Importing' do + + before(:all) do + ActiveRecord::Schema.define(:version => 1) do + create_table :import_articles do |t| + t.string :title + t.integer :views + t.string :numeric # For the sake of invalid data sent to Elasticsearch + t.datetime :created_at, :default => 'NOW()' + end + end + + ImportArticle.delete_all + ImportArticle.__elasticsearch__.client.cluster.health(wait_for_status: 'yellow') + end + + before do + ImportArticle.__elasticsearch__.create_index! + end + + after do + clear_indices(ImportArticle) + clear_tables(ImportArticle) + end + + describe '#import' do + + context 'when no search criteria is specified' do + + before do + 10.times { |i| ImportArticle.create! title: 'Test', views: "#{i}" } + ImportArticle.import + ImportArticle.__elasticsearch__.refresh_index! + end + + it 'imports all documents' do + expect(ImportArticle.search('*').results.total).to eq(10) + end + end + + context 'when batch size is specified' do + + before do + 10.times { |i| ImportArticle.create! title: 'Test', views: "#{i}" } + end + + let!(:batch_count) do + batches = 0 + errors = ImportArticle.import(batch_size: 5) do |response| + batches += 1 + end + ImportArticle.__elasticsearch__.refresh_index! + batches + end + + it 'imports using the batch size' do + expect(batch_count).to eq(2) + end + + it 'imports all the documents' do + expect(ImportArticle.search('*').results.total).to eq(10) + end + end + + context 'when a scope is specified' do + + before do + 10.times { |i| ImportArticle.create! title: 'Test', views: "#{i}" } + ImportArticle.import(scope: 'popular', force: true) + ImportArticle.__elasticsearch__.refresh_index! + end + + it 'applies the scope' do + expect(ImportArticle.search('*').results.total).to eq(5) + end + end + + context 'when a query is specified' do + + before do + 10.times { |i| ImportArticle.create! title: 'Test', views: "#{i}" } + ImportArticle.import(query: -> { where('views >= 3') }) + ImportArticle.__elasticsearch__.refresh_index! + end + + it 'applies the query' do + expect(ImportArticle.search('*').results.total).to eq(7) + end + end + + context 'when there are invalid documents' do + + let!(:result) do + 10.times { |i| ImportArticle.create! title: 'Test', views: "#{i}" } + new_article + batches = 0 + errors = ImportArticle.__elasticsearch__.import(batch_size: 5) do |response| + batches += 1 + end + ImportArticle.__elasticsearch__.refresh_index! + { batch_size: batches, errors: errors} + end + + let(:new_article) do + ImportArticle.create!(title: "Test INVALID", numeric: "INVALID") + end + + it 'does not import them' do + expect(ImportArticle.search('*').results.total).to eq(10) + expect(result[:batch_size]).to eq(3) + expect(result[:errors]).to eq(1) + end + end + + context 'when a transform proc is specified' do + + before do + 10.times { |i| ImportArticle.create! title: 'Test', views: "#{i}" } + ImportArticle.import( transform: ->(a) {{ index: { data: { name: a.title, foo: 'BAR' } }}} ) + ImportArticle.__elasticsearch__.refresh_index! + end + + it 'transforms the documents' do + expect(ImportArticle.search('*').results.first._source.keys).to include('name') + expect(ImportArticle.search('*').results.first._source.keys).to include('foo') + end + + it 'imports all documents' do + expect(ImportArticle.search('test').results.total).to eq(10) + expect(ImportArticle.search('bar').results.total).to eq(10) + end + end + + context 'when the model has a default scope' do + + around(:all) do |example| + 10.times { |i| ImportArticle.create! title: 'Test', views: "#{i}" } + ImportArticle.instance_eval { default_scope { where('views > 3') } } + example.run + ImportArticle.default_scopes.pop + end + + before do + ImportArticle.__elasticsearch__.import + ImportArticle.__elasticsearch__.refresh_index! + end + + it 'uses the default scope' do + expect(ImportArticle.search('*').results.total).to eq(6) + end + end + + context 'when there is a default scope and a query specified' do + + around(:all) do |example| + 10.times { |i| ImportArticle.create! title: 'Test', views: "#{i}" } + ImportArticle.instance_eval { default_scope { where('views > 3') } } + example.run + ImportArticle.default_scopes.pop + end + + before do + ImportArticle.import(query: -> { where('views <= 4') }) + ImportArticle.__elasticsearch__.refresh_index! + end + + it 'combines the query and the default scope' do + expect(ImportArticle.search('*').results.total).to eq(1) + end + end + + context 'when the batch is empty' do + + before do + ImportArticle.delete_all + ImportArticle.import + ImportArticle.__elasticsearch__.refresh_index! + end + + it 'does not make any requests to create documents' do + expect(ImportArticle.search('*').results.total).to eq(0) + end + end + end +end diff --git a/elasticsearch-model/spec/elasticsearch/model/adapters/active_record/multi_model_spec.rb b/elasticsearch-model/spec/elasticsearch/model/adapters/active_record/multi_model_spec.rb new file mode 100644 index 000000000..96c65fc5c --- /dev/null +++ b/elasticsearch-model/spec/elasticsearch/model/adapters/active_record/multi_model_spec.rb @@ -0,0 +1,110 @@ +require 'spec_helper' + +describe 'Elasticsearch::Model::Adapter::ActiveRecord MultiModel' do + + before(:all) do + ActiveRecord::Schema.define do + create_table Episode.table_name do |t| + t.string :name + t.datetime :created_at, :default => 'NOW()' + end + + create_table Series.table_name do |t| + t.string :name + t.datetime :created_at, :default => 'NOW()' + end + end + end + + before do + models = [ Episode, Series ] + clear_tables(models) + models.each do |model| + model.__elasticsearch__.create_index! force: true + model.create name: "The #{model.name}" + model.create name: "A great #{model.name}" + model.create name: "The greatest #{model.name}" + model.__elasticsearch__.refresh_index! + end + end + + after do + clear_indices(Episode, Series) + clear_tables(Episode, Series) + end + + context 'when the search is across multimodels' do + + let(:search_result) do + Elasticsearch::Model.search(%q<"The greatest Episode"^2 OR "The greatest Series">, [Series, Episode]) + end + + it 'executes the search across models' do + expect(search_result.results.size).to eq(2) + expect(search_result.records.size).to eq(2) + end + + describe '#results' do + + it 'returns an instance of Elasticsearch::Model::Response::Result' do + expect(search_result.results[0]).to be_a(Elasticsearch::Model::Response::Result) + expect(search_result.results[1]).to be_a(Elasticsearch::Model::Response::Result) + end + + it 'returns the correct model instance' do + expect(search_result.results[0].name).to eq('The greatest Episode') + expect(search_result.results[1].name).to eq('The greatest Series') + end + + it 'provides access to the results' do + expect(search_result.results[0].name).to eq('The greatest Episode') + expect(search_result.results[0].name?).to be(true) + expect(search_result.results[0].boo?).to be(false) + + expect(search_result.results[1].name).to eq('The greatest Series') + expect(search_result.results[1].name?).to be(true) + expect(search_result.results[1].boo?).to be(false) + end + end + + describe '#records' do + + it 'returns an instance of Elasticsearch::Model::Response::Result' do + expect(search_result.records[0]).to be_a(Episode) + expect(search_result.records[1]).to be_a(Series) + end + + it 'returns the correct model instance' do + expect(search_result.records[0].name).to eq('The greatest Episode') + expect(search_result.records[1].name).to eq('The greatest Series') + end + + context 'when the data store is changed' do + + before do + Series.find_by_name("The greatest Series").delete + Series.__elasticsearch__.refresh_index! + end + + it 'only returns matching records' do + expect(search_result.results.size).to eq(2) + expect(search_result.records.size).to eq(1 ) + expect(search_result.records[0].name).to eq('The greatest Episode') + end + end + end + + describe 'pagination' do + + let(:search_result) do + Elasticsearch::Model.search('series OR episode', [Series, Episode]) + end + + it 'properly paginates the results' do + expect(search_result.page(1).per(3).results.size).to eq(3) + expect(search_result.page(2).per(3).results.size).to eq(3) + expect(search_result.page(3).per(3).results.size).to eq(0) + end + end + end +end diff --git a/elasticsearch-model/spec/elasticsearch/model/adapters/active_record/namespaced_model_spec.rb b/elasticsearch-model/spec/elasticsearch/model/adapters/active_record/namespaced_model_spec.rb new file mode 100644 index 000000000..ea426d3f2 --- /dev/null +++ b/elasticsearch-model/spec/elasticsearch/model/adapters/active_record/namespaced_model_spec.rb @@ -0,0 +1,38 @@ +require 'spec_helper' + +describe 'Elasticsearch::Model::Adapter::ActiveRecord Namespaced Model' do + + before(:all) do + ActiveRecord::Schema.define(:version => 1) do + create_table :books do |t| + t.string :title + end + end + + MyNamespace::Book.delete_all + MyNamespace::Book.__elasticsearch__.create_index!(force: true) + MyNamespace::Book.create!(title: 'Test') + MyNamespace::Book.__elasticsearch__.refresh_index! + end + + after do + clear_indices(MyNamespace::Book) + clear_tables(MyNamespace::Book) + end + + context 'when the model is namespaced' do + + it 'has the proper index name' do + expect(MyNamespace::Book.index_name).to eq('my_namespace-books') + end + + it 'has the proper document type' do + expect(MyNamespace::Book.document_type).to eq('book') + end + + it 'saves the document into the index' do + expect(MyNamespace::Book.search('title:test').results.size).to eq(1) + expect(MyNamespace::Book.search('title:test').results.first.title).to eq('Test') + end + end +end diff --git a/elasticsearch-model/spec/elasticsearch/model/adapters/active_record/pagination_spec.rb b/elasticsearch-model/spec/elasticsearch/model/adapters/active_record/pagination_spec.rb new file mode 100644 index 000000000..9427fae48 --- /dev/null +++ b/elasticsearch-model/spec/elasticsearch/model/adapters/active_record/pagination_spec.rb @@ -0,0 +1,315 @@ +require 'spec_helper' + +describe 'Elasticsearch::Model::Adapter::ActiveRecord Pagination' do + + before(:all) do + ActiveRecord::Schema.define(:version => 1) do + create_table ArticleForPagination.table_name do |t| + t.string :title + t.datetime :created_at, :default => 'NOW()' + t.boolean :published + end + end + + Kaminari::Hooks.init if defined?(Kaminari::Hooks) + + ArticleForPagination.__elasticsearch__.create_index! force: true + + 68.times do |i| + ArticleForPagination.create! title: "Test #{i}", published: (i % 2 == 0) + end + + ArticleForPagination.import + ArticleForPagination.__elasticsearch__.refresh_index! + end + + context 'when no other page is specified' do + + let(:records) do + ArticleForPagination.search('title:test').page(1).records + end + + describe '#size' do + + it 'returns the correct size' do + expect(records.size).to eq(25) + end + end + + describe '#current_page' do + + it 'returns the correct current page' do + expect(records.current_page).to eq(1) + end + end + + describe '#prev_page' do + + it 'returns the correct previous page' do + expect(records.prev_page).to be_nil + end + end + + describe '#next_page' do + + it 'returns the correct next page' do + expect(records.next_page).to eq(2) + end + end + + describe '#total_pages' do + + it 'returns the correct total pages' do + expect(records.total_pages).to eq(3) + end + end + + describe '#first_page?' do + + it 'returns the correct first page' do + expect(records.first_page?).to be(true) + end + end + + describe '#last_page?' do + + it 'returns the correct last page' do + expect(records.last_page?).to be(false) + end + end + + describe '#out_of_range?' do + + it 'returns whether the pagination is out of range' do + expect(records.out_of_range?).to be(false) + end + end + end + + context 'when a specific page is specified' do + + let(:records) do + ArticleForPagination.search('title:test').page(2).records + end + + describe '#size' do + + it 'returns the correct size' do + expect(records.size).to eq(25) + end + end + + describe '#current_page' do + + it 'returns the correct current page' do + expect(records.current_page).to eq(2) + end + end + + describe '#prev_page' do + + it 'returns the correct previous page' do + expect(records.prev_page).to eq(1) + end + end + + describe '#next_page' do + + it 'returns the correct next page' do + expect(records.next_page).to eq(3) + end + end + + describe '#total_pages' do + + it 'returns the correct total pages' do + expect(records.total_pages).to eq(3) + end + end + + describe '#first_page?' do + + it 'returns the correct first page' do + expect(records.first_page?).to be(false) + end + end + + describe '#last_page?' do + + it 'returns the correct last page' do + expect(records.last_page?).to be(false) + end + end + + describe '#out_of_range?' do + + it 'returns whether the pagination is out of range' do + expect(records.out_of_range?).to be(false) + end + end + end + + context 'when a the last page is specified' do + + let(:records) do + ArticleForPagination.search('title:test').page(3).records + end + + describe '#size' do + + it 'returns the correct size' do + expect(records.size).to eq(18) + end + end + + describe '#current_page' do + + it 'returns the correct current page' do + expect(records.current_page).to eq(3) + end + end + + describe '#prev_page' do + + it 'returns the correct previous page' do + expect(records.prev_page).to eq(2) + end + end + + describe '#next_page' do + + it 'returns the correct next page' do + expect(records.next_page).to be_nil + end + end + + describe '#total_pages' do + + it 'returns the correct total pages' do + expect(records.total_pages).to eq(3) + end + end + + describe '#first_page?' do + + it 'returns the correct first page' do + expect(records.first_page?).to be(false) + end + end + + describe '#last_page?' do + + it 'returns the correct last page' do + expect(records.last_page?).to be(true) + end + end + + describe '#out_of_range?' do + + it 'returns whether the pagination is out of range' do + expect(records.out_of_range?).to be(false) + end + end + end + + context 'when an invalid page is specified' do + + let(:records) do + ArticleForPagination.search('title:test').page(6).records + end + + describe '#size' do + + it 'returns the correct size' do + expect(records.size).to eq(0) + end + end + + describe '#current_page' do + + it 'returns the correct current page' do + expect(records.current_page).to eq(6) + end + end + + describe '#next_page' do + + it 'returns the correct next page' do + expect(records.next_page).to be_nil + end + end + + describe '#total_pages' do + + it 'returns the correct total pages' do + expect(records.total_pages).to eq(3) + end + end + + describe '#first_page?' do + + it 'returns the correct first page' do + expect(records.first_page?).to be(false) + end + end + + describe '#last_page?' do + + it 'returns whether it is the last page', if: !(Kaminari::VERSION < '1') do + expect(records.last_page?).to be(false) + end + + it 'returns whether it is the last page', if: Kaminari::VERSION < '1' do + expect(records.last_page?).to be(true) # Kaminari returns current_page >= total_pages in version < 1.0 + end + end + + describe '#out_of_range?' do + + it 'returns whether the pagination is out of range' do + expect(records.out_of_range?).to be(true) + end + end + end + + context 'when a scope is also specified' do + + let(:records) do + ArticleForPagination.search('title:test').page(2).records.published + end + + describe '#size' do + + it 'returns the correct size' do + expect(records.size).to eq(12) + end + end + end + + context 'when a sorting is specified' do + + let(:search) do + ArticleForPagination.search({ query: { match: { title: 'test' } }, sort: [ { id: 'desc' } ] }) + end + + it 'applies the sort' do + expect(search.page(2).records.first.id).to eq(43) + expect(search.page(3).records.first.id).to eq(18) + expect(search.page(2).per(5).records.first.id).to eq(63) + end + end + + context 'when the model has a specific default per page set' do + + around do |example| + original_default = ArticleForPagination.instance_variable_get(:@_default_per_page) + ArticleForPagination.paginates_per 50 + example.run + ArticleForPagination.paginates_per original_default + end + + it 'uses the default per page setting' do + expect(ArticleForPagination.search('*').page(1).records.size).to eq(50) + end + end +end diff --git a/elasticsearch-model/spec/elasticsearch/model/adapters/active_record/parent_child_spec.rb b/elasticsearch-model/spec/elasticsearch/model/adapters/active_record/parent_child_spec.rb new file mode 100644 index 000000000..647cb6dde --- /dev/null +++ b/elasticsearch-model/spec/elasticsearch/model/adapters/active_record/parent_child_spec.rb @@ -0,0 +1,75 @@ +require 'spec_helper' + +describe 'Elasticsearch::Model::Adapter::ActiveRecord Parent-Child' do + + before(:all) do + ActiveRecord::Schema.define(version: 1) do + create_table :questions do |t| + t.string :title + t.text :text + t.string :author + t.timestamps null: false + end + + create_table :answers do |t| + t.text :text + t.string :author + t.references :question + t.timestamps null: false + end + + add_index(:answers, :question_id) unless index_exists?(:answers, :question_id) + + clear_tables(Question) + ParentChildSearchable.create_index!(force: true) + + q_1 = Question.create!(title: 'First Question', author: 'John') + q_2 = Question.create!(title: 'Second Question', author: 'Jody') + + q_1.answers.create!(text: 'Lorem Ipsum', author: 'Adam') + q_1.answers.create!(text: 'Dolor Sit', author: 'Ryan') + + q_2.answers.create!(text: 'Amet Et', author: 'John') + + Question.__elasticsearch__.refresh_index! + end + end + + describe 'has_child search' do + + let(:search_result) do + Question.search(query: { has_child: { type: 'answer', query: { match: { author: 'john' } } } }) + end + + it 'finds parents by matching on child search criteria' do + expect(search_result.records.first.title).to eq('Second Question') + end + end + + describe 'hash_parent search' do + + let(:search_result) do + Answer.search(query: { has_parent: { parent_type: 'question', query: { match: { author: 'john' } } } }) + end + + it 'finds children by matching in parent criteria' do + expect(search_result.records.map(&:author)).to match(['Adam', 'Ryan']) + end + end + + context 'when a parent is deleted' do + + before do + Question.where(title: 'First Question').each(&:destroy) + Question.__elasticsearch__.refresh_index! + end + + let(:search_result) do + Answer.search(query: { has_parent: { parent_type: 'question', query: { match_all: {} } } }) + end + + it 'deletes the children' do + expect(search_result.results.total).to eq(1) + end + end +end diff --git a/elasticsearch-model/spec/elasticsearch/model/adapters/active_record/serialization_spec.rb b/elasticsearch-model/spec/elasticsearch/model/adapters/active_record/serialization_spec.rb new file mode 100644 index 000000000..e5b2072be --- /dev/null +++ b/elasticsearch-model/spec/elasticsearch/model/adapters/active_record/serialization_spec.rb @@ -0,0 +1,61 @@ +require 'spec_helper' + +describe 'Elasticsearch::Model::Adapter::ActiveRecord Serialization' do + + before(:all) do + ActiveRecord::Schema.define(:version => 1) do + create_table ArticleWithCustomSerialization.table_name do |t| + t.string :title + t.string :status + end + end + + ArticleWithCustomSerialization.delete_all + ArticleWithCustomSerialization.__elasticsearch__.create_index!(force: true) + end + + context 'when the model has a custom serialization defined' do + + before do + ArticleWithCustomSerialization.create!(title: 'Test', status: 'green') + ArticleWithCustomSerialization.__elasticsearch__.refresh_index! + end + + context 'when a document is indexed' do + + let(:search_result) do + ArticleWithCustomSerialization.__elasticsearch__.client.get(index: 'article_with_custom_serializations', + type: '_doc', + id: '1') + end + + it 'applies the serialization when indexing' do + expect(search_result['_source']).to eq('title' => 'Test') + end + end + + context 'when a document is updated' do + + before do + article.update_attributes(title: 'UPDATED', status: 'yellow') + ArticleWithCustomSerialization.__elasticsearch__.refresh_index! + end + + let!(:article) do + art = ArticleWithCustomSerialization.create!(title: 'Test', status: 'red') + ArticleWithCustomSerialization.__elasticsearch__.refresh_index! + art + end + + let(:search_result) do + ArticleWithCustomSerialization.__elasticsearch__.client.get(index: 'article_with_custom_serializations', + type: '_doc', + id: article.id) + end + + it 'applies the serialization when updating' do + expect(search_result['_source']).to eq('title' => 'UPDATED') + end + end + end +end diff --git a/elasticsearch-model/spec/elasticsearch/model/adapters/active_record_spec.rb b/elasticsearch-model/spec/elasticsearch/model/adapters/active_record_spec.rb index 7d18f0b72..6e0cb7d64 100644 --- a/elasticsearch-model/spec/elasticsearch/model/adapters/active_record_spec.rb +++ b/elasticsearch-model/spec/elasticsearch/model/adapters/active_record_spec.rb @@ -8,7 +8,7 @@ class DummyClassForActiveRecord; end after(:all) do Elasticsearch::Model::Adapter::Adapter.adapters.delete(DummyClassForActiveRecord) - Object.send(:remove_const, :DummyClassForActiveRecord) if defined?(DummyClassForActiveRecord) + remove_classes(DummyClassForActiveRecord) end let(:model) do @@ -89,37 +89,6 @@ class DummyClassForActiveRecord; end expect(instance.records).to eq(records) end end - - context 'when an order is not defined for the ActiveRecord query' do - - context 'when the records have a different order than the hits' do - - before do - records.instance_variable_set(:@records, records) - allow(records).to receive(:order_values).and_return([]) - end - - it 'reorders the records based on hits order' do - expect(records.collect(&:id)).to eq([1, 2]) - expect(instance.records.to_a.collect(&:id)).to eq([2, 1]) - end - end - end - - context 'when an order is defined for the ActiveRecord query' do - - context 'when the records have a different order than the hits' do - - before do - records.instance_variable_set(:@records, [record_2, record_1]) - allow(records).to receive(:order_values).and_return([double('order_definition')]) - end - - it 'reorders the records based on hits order' do - expect(instance.records.to_a.collect(&:id)).to eq([2, 1]) - end - end - end end describe 'callbacks registration' do diff --git a/elasticsearch-model/spec/elasticsearch/model/adapters/default_spec.rb b/elasticsearch-model/spec/elasticsearch/model/adapters/default_spec.rb index b0d108538..08064df0f 100644 --- a/elasticsearch-model/spec/elasticsearch/model/adapters/default_spec.rb +++ b/elasticsearch-model/spec/elasticsearch/model/adapters/default_spec.rb @@ -10,7 +10,7 @@ class DummyClassForDefaultAdapter; end after(:all) do Elasticsearch::Model::Adapter::Adapter.adapters.delete(DummyClassForDefaultAdapter) - Object.send(:remove_const, :DummyClassForDefaultAdapter) if defined?(DummyClassForDefaultAdapter) + remove_classes(DummyClassForDefaultAdapter) end let(:instance) do diff --git a/elasticsearch-model/spec/elasticsearch/model/adapters/mongoid/basic_spec.rb b/elasticsearch-model/spec/elasticsearch/model/adapters/mongoid/basic_spec.rb new file mode 100644 index 000000000..f4aefc740 --- /dev/null +++ b/elasticsearch-model/spec/elasticsearch/model/adapters/mongoid/basic_spec.rb @@ -0,0 +1,267 @@ +require 'spec_helper' + +describe Elasticsearch::Model::Adapter::Mongoid, if: test_mongoid? do + + before(:all) do + connect_mongoid('mongoid_test') + Elasticsearch::Model::Adapter.register \ + Elasticsearch::Model::Adapter::Mongoid, + lambda { |klass| !!defined?(::Mongoid::Document) && klass.respond_to?(:ancestors) && klass.ancestors.include?(::Mongoid::Document) } + + MongoidArticle.__elasticsearch__.create_index! force: true + + MongoidArticle.delete_all + + MongoidArticle.__elasticsearch__.refresh_index! + MongoidArticle.__elasticsearch__.client.cluster.health wait_for_status: 'yellow' + end + + after do + clear_indices(MongoidArticle) + clear_tables(MongoidArticle) + end + + describe 'searching' do + + before do + MongoidArticle.create! title: 'Test' + MongoidArticle.create! title: 'Testing Coding' + MongoidArticle.create! title: 'Coding' + MongoidArticle.__elasticsearch__.refresh_index! + end + + let(:search_result) do + MongoidArticle.search('title:test') + end + + it 'find the documents successfully' do + expect(search_result.results.size).to eq(2) + expect(search_result.records.size).to eq(2) + end + + describe '#results' do + + it 'returns a Elasticsearch::Model::Response::Result' do + expect(search_result.results.first).to be_a(Elasticsearch::Model::Response::Result) + end + + it 'retrieves the document from Elasticsearch' do + expect(search_result.results.first.title).to eq('Test') + end + + it 'retrieves all results' do + expect(search_result.results.collect(&:title)).to match(['Test', 'Testing Coding']) + end + end + + describe '#records' do + + it 'returns an instance of the model' do + expect(search_result.records.first).to be_a(MongoidArticle) + end + + it 'retrieves the document from Elasticsearch' do + expect(search_result.records.first.title).to eq('Test') + end + + it 'iterates over the records' do + expect(search_result.records.first.title).to eq('Test') + end + + it 'retrieves all records' do + expect(search_result.records.collect(&:title)).to match(['Test', 'Testing Coding']) + end + + describe '#each_with_hit' do + + it 'yields each hit with the model object' do + search_result.records.each_with_hit do |r, h| + expect(h._source).not_to be_nil + expect(h._source.title).not_to be_nil + end + end + + it 'preserves the search order' do + search_result.records.each_with_hit do |r, h| + expect(r.id.to_s).to eq(h._id) + end + end + end + + describe '#map_with_hit' do + + it 'yields each hit with the model object' do + search_result.records.map_with_hit do |r, h| + expect(h._source).not_to be_nil + expect(h._source.title).not_to be_nil + end + end + + it 'preserves the search order' do + search_result.records.map_with_hit do |r, h| + expect(r.id.to_s).to eq(h._id) + end + end + end + end + end + + describe '#destroy' do + + let(:article) do + MongoidArticle.create!(title: 'Test') + end + + before do + article + MongoidArticle.create!(title: 'Coding') + article.destroy + MongoidArticle.__elasticsearch__.refresh_index! + end + + it 'removes documents from the index' do + expect(MongoidArticle.search('title:test').results.total).to eq(0) + expect(MongoidArticle.search('title:code').results.total).to eq(1) + end + end + + describe 'updates to the document' do + + let(:article) do + MongoidArticle.create!(title: 'Test') + end + + before do + article.title = 'Writing' + article.save + MongoidArticle.__elasticsearch__.refresh_index! + end + + it 'indexes updates' do + expect(MongoidArticle.search('title:write').results.total).to eq(1) + expect(MongoidArticle.search('title:test').results.total).to eq(0) + end + end + + describe 'DSL search' do + + before do + MongoidArticle.create! title: 'Test' + MongoidArticle.create! title: 'Testing Coding' + MongoidArticle.create! title: 'Coding' + MongoidArticle.__elasticsearch__.refresh_index! + end + + let(:search_result) do + MongoidArticle.search(query: { match: { title: { query: 'test' } } }) + end + + it 'finds the matching documents' do + expect(search_result.results.size).to eq(2) + expect(search_result.records.size).to eq(2) + end + end + + describe 'paging a collection' do + + before do + MongoidArticle.create! title: 'Test' + MongoidArticle.create! title: 'Testing Coding' + MongoidArticle.create! title: 'Coding' + MongoidArticle.__elasticsearch__.refresh_index! + end + + let(:search_result) do + MongoidArticle.search(query: { match: { title: { query: 'test' } } }, + size: 2, + from: 1) + end + + it 'applies the size and from parameters' do + expect(search_result.results.size).to eq(1) + expect(search_result.results.first.title).to eq('Testing Coding') + expect(search_result.records.size).to eq(1) + expect(search_result.records.first.title).to eq('Testing Coding') + end + end + + describe 'importing' do + + before do + 97.times { |i| MongoidArticle.create! title: "Test #{i}" } + MongoidArticle.__elasticsearch__.create_index! force: true + MongoidArticle.__elasticsearch__.client.cluster.health wait_for_status: 'yellow' + end + + context 'when there is no default scope' do + + let!(:batch_count) do + batches = 0 + errors = MongoidArticle.import(batch_size: 10) do |response| + batches += 1 + end + MongoidArticle.__elasticsearch__.refresh_index! + batches + end + + it 'imports all the documents' do + expect(MongoidArticle.search('*').results.total).to eq(97) + end + + it 'uses the specified batch size' do + expect(batch_count).to eq(10) + end + end + + context 'when there is a default scope' do + + around(:all) do |example| + 10.times { |i| MongoidArticle.create! title: 'Test', views: "#{i}" } + MongoidArticle.default_scope -> { MongoidArticle.gt(views: 3) } + example.run + MongoidArticle.default_scoping = nil + end + + before do + MongoidArticle.__elasticsearch__.import + MongoidArticle.__elasticsearch__.refresh_index! + end + + it 'uses the default scope' do + expect(MongoidArticle.search('*').results.total).to eq(6) + end + end + + context 'when there is a default scope and a query specified' do + + around(:all) do |example| + 10.times { |i| MongoidArticle.create! title: 'Test', views: "#{i}" } + MongoidArticle.default_scope -> { MongoidArticle.gt(views: 3) } + example.run + MongoidArticle.default_scoping = nil + end + + before do + MongoidArticle.import(query: -> { lte(views: 4) }) + MongoidArticle.__elasticsearch__.refresh_index! + end + + it 'combines the query and the default scope' do + expect(MongoidArticle.search('*').results.total).to eq(1) + end + end + + context 'when the batch is empty' do + + before do + MongoidArticle.delete_all + MongoidArticle.import + MongoidArticle.__elasticsearch__.refresh_index! + end + + it 'does not make any requests to create documents' do + expect(MongoidArticle.search('*').results.total).to eq(0) + end + end + end +end diff --git a/elasticsearch-model/spec/elasticsearch/model/adapters/mongoid/multi_model_spec.rb b/elasticsearch-model/spec/elasticsearch/model/adapters/mongoid/multi_model_spec.rb new file mode 100644 index 000000000..e4308ab98 --- /dev/null +++ b/elasticsearch-model/spec/elasticsearch/model/adapters/mongoid/multi_model_spec.rb @@ -0,0 +1,66 @@ +require 'spec_helper' + +describe 'Elasticsearch::Model::Adapter::ActiveRecord Multimodel', if: test_mongoid? do + + before(:all) do + connect_mongoid('mongoid_test') + + begin + ActiveRecord::Schema.define(:version => 1) do + create_table Episode.table_name do |t| + t.string :name + t.datetime :created_at, :default => 'NOW()' + end + end + rescue + end + end + + before do + clear_tables(Episode, Image) + Episode.__elasticsearch__.create_index! force: true + Episode.create name: "TheEpisode" + Episode.create name: "A great Episode" + Episode.create name: "The greatest Episode" + Episode.__elasticsearch__.refresh_index! + + Image.__elasticsearch__.create_index! force: true + Image.create! name: "The Image" + Image.create! name: "A great Image" + Image.create! name: "The greatest Image" + Image.__elasticsearch__.refresh_index! + Image.__elasticsearch__.client.cluster.health wait_for_status: 'yellow' + end + + after do + [Episode, Image].each do |model| + model.__elasticsearch__.client.delete_by_query(index: model.index_name, q: '*') + model.delete_all + model.__elasticsearch__.refresh_index! + end + end + + context 'when the search is across multimodels with different adapters' do + + let(:search_result) do + Elasticsearch::Model.search(%q<"greatest Episode" OR "greatest Image"^2>, [Episode, Image]) + end + + it 'executes the search across models' do + expect(search_result.results.size).to eq(2) + expect(search_result.records.size).to eq(2) + end + + it 'returns the correct type of model instance' do + expect(search_result.records[0]).to be_a(Image) + expect(search_result.records[1]).to be_a(Episode) + end + + it 'creates the model instances with the correct attributes' do + expect(search_result.results[0].name).to eq('The greatest Image') + expect(search_result.records[0].name).to eq('The greatest Image') + expect(search_result.results[1].name).to eq('The greatest Episode') + expect(search_result.records[1].name).to eq('The greatest Episode') + end + end +end diff --git a/elasticsearch-model/spec/elasticsearch/model/adapters/mongoid_spec.rb b/elasticsearch-model/spec/elasticsearch/model/adapters/mongoid_spec.rb index cb3459341..3a01a6635 100644 --- a/elasticsearch-model/spec/elasticsearch/model/adapters/mongoid_spec.rb +++ b/elasticsearch-model/spec/elasticsearch/model/adapters/mongoid_spec.rb @@ -9,7 +9,7 @@ class DummyClassForMongoid; end after(:all) do Elasticsearch::Model::Adapter::Adapter.adapters.delete(DummyClassForMongoid) - Object.send(:remove_const, :DummyClassForMongoid) if defined?(DummyClassForMongoid) + remove_classes(DummyClassForMongoid) end let(:response) do diff --git a/elasticsearch-model/spec/elasticsearch/model/adapters/multiple_spec.rb b/elasticsearch-model/spec/elasticsearch/model/adapters/multiple_spec.rb index 83a8e6f68..947df1717 100644 --- a/elasticsearch-model/spec/elasticsearch/model/adapters/multiple_spec.rb +++ b/elasticsearch-model/spec/elasticsearch/model/adapters/multiple_spec.rb @@ -58,12 +58,11 @@ def initialize(id) end after(:all) do - Elasticsearch::Model::Adapter::Adapter.adapters.delete(DummyOne) - Elasticsearch::Model::Adapter::Adapter.adapters.delete(Namespace::DummyTwo) - Elasticsearch::Model::Adapter::Adapter.adapters.delete(DummyTwo) - Object.send(:remove_const, :DummyOne) if defined?(DummyOne) - Object.send(:remove_const, :Namespace) if defined?(Namespace::DummyTwo) - Object.send(:remove_const, :DummyTwo) if defined?(DummyTwo) + [DummyOne, Namespace::DummyTwo, DummyTwo].each do |adapter| + Elasticsearch::Model::Adapter::Adapter.adapters.delete(adapter) + end + Namespace.send(:remove_const, :DummyTwo) if defined?(Namespace::DummyTwo) + remove_classes(DummyOne, DummyTwo, Namespace) end let(:hits) do diff --git a/elasticsearch-model/spec/elasticsearch/model/callbacks_spec.rb b/elasticsearch-model/spec/elasticsearch/model/callbacks_spec.rb index 0bbb494e4..d10ce656c 100644 --- a/elasticsearch-model/spec/elasticsearch/model/callbacks_spec.rb +++ b/elasticsearch-model/spec/elasticsearch/model/callbacks_spec.rb @@ -17,8 +17,7 @@ def callbacks_mixin end after(:all) do - Object.send(:remove_const, :DummyCallbacksModel) if defined?(DummyCallbacksModel) - Object.send(:remove_const, :DummyCallbacksAdapter) if defined?(DummyCallbacksAdapter) + remove_classes(DummyCallbacksModel, DummyCallbacksAdapter) end context 'when a model includes the Callbacks module' do diff --git a/elasticsearch-model/spec/elasticsearch/model/client_spec.rb b/elasticsearch-model/spec/elasticsearch/model/client_spec.rb index 2aee4f7b2..ea273af73 100644 --- a/elasticsearch-model/spec/elasticsearch/model/client_spec.rb +++ b/elasticsearch-model/spec/elasticsearch/model/client_spec.rb @@ -10,7 +10,7 @@ class ::DummyClientModel end after(:all) do - Object.send(:remove_const, :DummyClientModel) if defined?(DummyClientModel) + remove_classes(DummyClientModel) end context 'when a class includes the client module class methods' do diff --git a/elasticsearch-model/spec/elasticsearch/model/importing_spec.rb b/elasticsearch-model/spec/elasticsearch/model/importing_spec.rb index 0b629ff22..c46f00ba6 100644 --- a/elasticsearch-model/spec/elasticsearch/model/importing_spec.rb +++ b/elasticsearch-model/spec/elasticsearch/model/importing_spec.rb @@ -23,8 +23,7 @@ def importing_mixin end after(:all) do - Object.send(:remove_const, :DummyImportingModel) if defined?(DummyImportingModel) - Object.send(:remove_const, :DummyImportingAdapter) if defined?(DummyImportingAdapter) + remove_classes(DummyImportingModel, DummyImportingAdapter) end before do diff --git a/elasticsearch-model/spec/elasticsearch/model/indexing_spec.rb b/elasticsearch-model/spec/elasticsearch/model/indexing_spec.rb index 5daadcf15..beaf0dafd 100644 --- a/elasticsearch-model/spec/elasticsearch/model/indexing_spec.rb +++ b/elasticsearch-model/spec/elasticsearch/model/indexing_spec.rb @@ -17,8 +17,7 @@ class NotFound < Exception; end end after(:all) do - Object.send(:remove_const, :DummyIndexingModel) if defined?(DummyIndexingModel) - Object.send(:remove_const, :NotFound) if defined?(NotFound) + remove_classes(DummyIndexingModel, NotFound) end describe 'the Settings class' do diff --git a/elasticsearch-model/spec/elasticsearch/model/module_spec.rb b/elasticsearch-model/spec/elasticsearch/model/module_spec.rb index c3a394534..3162e2ec5 100644 --- a/elasticsearch-model/spec/elasticsearch/model/module_spec.rb +++ b/elasticsearch-model/spec/elasticsearch/model/module_spec.rb @@ -29,15 +29,12 @@ def self.search(query, options={}) "SEARCH" end end - end - after(:all) do - Object.send(:remove_const, :DummyIncludingModel) if defined?(DummyIncludingModel) - Object.send(:remove_const, :DummyIncludingModelWithSearchMethodDefined) if defined?(DummyIncludingModelWithSearchMethodDefined) + DummyIncludingModel.__send__ :include, Elasticsearch::Model end - before do - DummyIncludingModel.__send__ :include, Elasticsearch::Model + after(:all) do + remove_classes(DummyIncludingModel, DummyIncludingModelWithSearchMethodDefined) end it 'should include and set up the proxy' do diff --git a/elasticsearch-model/spec/elasticsearch/model/multimodel_spec.rb b/elasticsearch-model/spec/elasticsearch/model/multimodel_spec.rb index 6fa274ccc..44ee0bd84 100644 --- a/elasticsearch-model/spec/elasticsearch/model/multimodel_spec.rb +++ b/elasticsearch-model/spec/elasticsearch/model/multimodel_spec.rb @@ -40,8 +40,7 @@ class JustAnotherModel end after(:all) do - Object.send(:remove_const, :JustAModel) if defined?(JustAModel) - Object.send(:remove_const, :JustAnotherModel) if defined?(JustAnotherModel) + remove_classes(JustAModel, JustAnotherModel) end let(:multimodel) do diff --git a/elasticsearch-model/spec/elasticsearch/model/naming_inheritance_spec.rb b/elasticsearch-model/spec/elasticsearch/model/naming_inheritance_spec.rb index daf7fb47d..287fc7d42 100644 --- a/elasticsearch-model/spec/elasticsearch/model/naming_inheritance_spec.rb +++ b/elasticsearch-model/spec/elasticsearch/model/naming_inheritance_spec.rb @@ -3,8 +3,6 @@ describe 'naming inheritance' do before(:all) do - Elasticsearch::Model.settings[:inheritance_enabled] = true - class ::TestBase extend ActiveModel::Naming @@ -43,13 +41,17 @@ class ::Cat < ::Animal end after(:all) do - Elasticsearch::Model.settings[:inheritance_enabled] = false - Object.send(:remove_const, :TestBase) if defined?(TestBase) - Object.send(:remove_const, :Animal) if defined?(Animal) - Object.send(:remove_const, :MyNamespace) if defined?(MyNamespace) - Object.send(:remove_const, :Cat) if defined?(Cat) + remove_classes(TestBase, Animal, MyNamespace, Cat) + end + + around(:all) do |example| + original_value = Elasticsearch::Model.settings[:inheritance_enabled] + Elasticsearch::Model.settings[:inheritance_enabled] = true + example.run + Elasticsearch::Model.settings[:inheritance_enabled] = original_value end + describe '#index_name' do it 'returns the default index name' do diff --git a/elasticsearch-model/spec/elasticsearch/model/naming_spec.rb b/elasticsearch-model/spec/elasticsearch/model/naming_spec.rb index 32b9905d1..8e0d8d54d 100644 --- a/elasticsearch-model/spec/elasticsearch/model/naming_spec.rb +++ b/elasticsearch-model/spec/elasticsearch/model/naming_spec.rb @@ -21,8 +21,7 @@ class DummyNamingModelInNamespace end after(:all) do - Object.send(:remove_const, :DummyNamingModel) if defined?(DummyNamingModel) - Object.send(:remove_const, :MyNamespace) if defined?(MyNamespace) + remove_classes(DummyNamingModel, MyNamespace) end it 'returns the default index name' do diff --git a/elasticsearch-model/spec/elasticsearch/model/proxy_spec.rb b/elasticsearch-model/spec/elasticsearch/model/proxy_spec.rb index ea5553af0..6e484f896 100644 --- a/elasticsearch-model/spec/elasticsearch/model/proxy_spec.rb +++ b/elasticsearch-model/spec/elasticsearch/model/proxy_spec.rb @@ -28,15 +28,12 @@ def changes_to_save {:foo => ['One', 'Two']} end end - end - after(:all) do - Object.send(:remove_const, :DummyProxyModel) if defined?(DummyProxyModel) - Object.send(:remove_const, :DummyProxyModelWithCallbacks) if defined?(DummyProxyModelWithCallbacks) + DummyProxyModelWithCallbacks.__send__ :include, Elasticsearch::Model::Proxy end - before do - DummyProxyModelWithCallbacks.__send__ :include, Elasticsearch::Model::Proxy + after(:all) do + remove_classes(DummyProxyModel, DummyProxyModelWithCallbacks) end it 'sets up a proxy method on the class' do diff --git a/elasticsearch-model/spec/elasticsearch/model/response/aggregations_spec.rb b/elasticsearch-model/spec/elasticsearch/model/response/aggregations_spec.rb index 9d20730da..2d1f8f509 100644 --- a/elasticsearch-model/spec/elasticsearch/model/response/aggregations_spec.rb +++ b/elasticsearch-model/spec/elasticsearch/model/response/aggregations_spec.rb @@ -10,7 +10,7 @@ def self.document_type; 'bar'; end end after(:all) do - Object.send(:remove_const, :OriginClass) if defined?(OriginClass) + remove_classes(OriginClass) end let(:response_document) do diff --git a/elasticsearch-model/spec/elasticsearch/model/response/base_spec.rb b/elasticsearch-model/spec/elasticsearch/model/response/base_spec.rb index eae7a8a67..cfd77d7c7 100644 --- a/elasticsearch-model/spec/elasticsearch/model/response/base_spec.rb +++ b/elasticsearch-model/spec/elasticsearch/model/response/base_spec.rb @@ -14,8 +14,7 @@ def self.document_type; 'bar'; end end after(:all) do - Object.send(:remove_const, :DummyBaseClass) if defined?(DummyBaseClass) - Object.send(:remove_const, :OriginClass) if defined?(OriginClass) + remove_classes(DummyBaseClass, OriginClass) end let(:response_document) do diff --git a/elasticsearch-model/spec/elasticsearch/model/response/pagination/kaminari_spec.rb b/elasticsearch-model/spec/elasticsearch/model/response/pagination/kaminari_spec.rb index d288e2dc8..e8fe1f86b 100644 --- a/elasticsearch-model/spec/elasticsearch/model/response/pagination/kaminari_spec.rb +++ b/elasticsearch-model/spec/elasticsearch/model/response/pagination/kaminari_spec.rb @@ -1,6 +1,6 @@ require 'spec_helper' -describe Elasticsearch::Model::Response::Response do +describe 'Elasticsearch::Model::Response::Response Kaminari' do before(:all) do class ModelClass @@ -11,7 +11,7 @@ def self.document_type; 'bar'; end end after(:all) do - Object.send(:remove_const, :ModelClass) if defined?(ModelClass) + remove_classes(ModelClass) end let(:response_document) do diff --git a/elasticsearch-model/spec/elasticsearch/model/response/pagination/will_paginate_spec.rb b/elasticsearch-model/spec/elasticsearch/model/response/pagination/will_paginate_spec.rb index 7cd4fdfd9..0666b0f28 100644 --- a/elasticsearch-model/spec/elasticsearch/model/response/pagination/will_paginate_spec.rb +++ b/elasticsearch-model/spec/elasticsearch/model/response/pagination/will_paginate_spec.rb @@ -1,6 +1,6 @@ require 'spec_helper' -describe Elasticsearch::Model::Response::Response do +describe 'Elasticsearch::Model::Response::Response WillPaginate' do before(:all) do class ModelClass @@ -19,8 +19,7 @@ class WillPaginateResponse < Elasticsearch::Model::Response::Response end after(:all) do - Object.send(:remove_const, :ModelClass) if defined?(ModelClass) - Object.send(:remove_const, :WillPaginateResponse) if defined?(WillPaginateResponse) + remove_classes(ModelClass, WillPaginateResponse) end let(:response_document) do diff --git a/elasticsearch-model/spec/elasticsearch/model/response/records_spec.rb b/elasticsearch-model/spec/elasticsearch/model/response/records_spec.rb index 8687a92b4..882c763f3 100644 --- a/elasticsearch-model/spec/elasticsearch/model/response/records_spec.rb +++ b/elasticsearch-model/spec/elasticsearch/model/response/records_spec.rb @@ -23,8 +23,7 @@ def self.find(*args) end after(:all) do - Object.send(:remove_const, :DummyCollection) if defined?(DummyCollection) - Object.send(:remove_const, :DummyModel) if defined?(DummyModel) + remove_classes(DummyCollection, DummyModel) end let(:response_document) do diff --git a/elasticsearch-model/spec/elasticsearch/model/response/response_spec.rb b/elasticsearch-model/spec/elasticsearch/model/response/response_spec.rb index 2bb96fb8a..32f96a4d2 100644 --- a/elasticsearch-model/spec/elasticsearch/model/response/response_spec.rb +++ b/elasticsearch-model/spec/elasticsearch/model/response/response_spec.rb @@ -10,7 +10,7 @@ def self.document_type; 'bar'; end end after(:all) do - Object.send(:remove_const, :OriginClass) if defined?(OriginClass) + remove_classes(OriginClass) end let(:response_document) do diff --git a/elasticsearch-model/spec/elasticsearch/model/response/results_spec.rb b/elasticsearch-model/spec/elasticsearch/model/response/results_spec.rb index 5168022b3..f7149003d 100644 --- a/elasticsearch-model/spec/elasticsearch/model/response/results_spec.rb +++ b/elasticsearch-model/spec/elasticsearch/model/response/results_spec.rb @@ -10,7 +10,7 @@ def self.document_type; 'bar'; end end after(:all) do - Object.send(:remove_const, :OriginClass) if defined?(OriginClass) + remove_classes(OriginClass) end let(:response_document) do diff --git a/elasticsearch-model/spec/elasticsearch/model/searching_search_request_spec.rb b/elasticsearch-model/spec/elasticsearch/model/searching_search_request_spec.rb index f93dbce01..db3ac6dab 100644 --- a/elasticsearch-model/spec/elasticsearch/model/searching_search_request_spec.rb +++ b/elasticsearch-model/spec/elasticsearch/model/searching_search_request_spec.rb @@ -5,15 +5,13 @@ before(:all) do class ::DummySearchingModel extend Elasticsearch::Model::Searching::ClassMethods - def self.index_name; 'foo'; end def self.document_type; 'bar'; end - end end after(:all) do - Object.send(:remove_const, :DummySearchingModel) if defined?(DummySearchingModel) + remove_classes(DummySearchingModel) end before do diff --git a/elasticsearch-model/spec/elasticsearch/model/searching_spec.rb b/elasticsearch-model/spec/elasticsearch/model/searching_spec.rb index 66ee516a9..ca95f282b 100644 --- a/elasticsearch-model/spec/elasticsearch/model/searching_spec.rb +++ b/elasticsearch-model/spec/elasticsearch/model/searching_spec.rb @@ -12,7 +12,7 @@ def self.document_type; 'bar'; end end after(:all) do - Object.send(:remove_const, :DummySearchingModel) if defined?(DummySearchingModel) + remove_classes(DummySearchingModel) end it 'has the search method' do diff --git a/elasticsearch-model/spec/elasticsearch/model/serializing_spec.rb b/elasticsearch-model/spec/elasticsearch/model/serializing_spec.rb index 6b440e02e..ac4a850f3 100644 --- a/elasticsearch-model/spec/elasticsearch/model/serializing_spec.rb +++ b/elasticsearch-model/spec/elasticsearch/model/serializing_spec.rb @@ -13,7 +13,7 @@ def as_json(options={}) end after(:all) do - Object.send(:remove_const, :DummyClass) if defined?(DummyClass) + remove_classes(DummyClass) end it 'delegates to #as_json by default' do diff --git a/elasticsearch-model/spec/spec_helper.rb b/elasticsearch-model/spec/spec_helper.rb index 9c692211c..178f9c9fe 100644 --- a/elasticsearch-model/spec/spec_helper.rb +++ b/elasticsearch-model/spec/spec_helper.rb @@ -1,13 +1,157 @@ require 'pry-nav' require 'kaminari' +require 'kaminari/version' require 'will_paginate' require 'will_paginate/collection' require 'elasticsearch/model' require 'hashie/version' require 'active_model' +require 'mongoid' require 'yaml' +require 'active_record' RSpec.configure do |config| config.formatter = 'documentation' config.color = true + + config.before(:suite) do + require 'ansi' + tracer = ::Logger.new(STDERR) + tracer.formatter = lambda { |s, d, p, m| "#{m.gsub(/^.*$/) { |n| ' ' + n }.ansi(:faint)}\n" } + Elasticsearch::Model.client = Elasticsearch::Client.new host: "localhost:#{(ENV['TEST_CLUSTER_PORT'] || 9250)}", + tracer: (ENV['QUIET'] ? nil : tracer) + + unless ActiveRecord::Base.connected? + ActiveRecord::Base.establish_connection( :adapter => 'sqlite3', :database => ":memory:" ) + end + require 'support/app' + + if ::ActiveRecord::Base.respond_to?(:raise_in_transactional_callbacks) && ::ActiveRecord::VERSION::MAJOR.to_s < '5' + ::ActiveRecord::Base.raise_in_transactional_callbacks = true + end + end + + config.after(:all) do + drop_all_tables! + delete_all_indices! + end +end + +# Is the ActiveRecord version at least 4.0? +# +# @return [ true, false ] Whether the ActiveRecord version is at least 4.0. +# +# @since 6.0.1 +def active_record_at_least_4? + defined?(::ActiveRecord) && ::ActiveRecord::VERSION::MAJOR >= 4 +end + +# Delete all documents from the indices of the provided list of models. +# +# @param [ Array ] models The list of models. +# +# @return [ true ] +# +# @since 6.0.1 +def clear_indices(*models) + models.each do |model| + begin; Elasticsearch::Model.client.delete_by_query(index: model.index_name, q: '*'); rescue; end + end and true +end + +# Delete all documents from the tables of the provided list of models. +# +# @param [ Array ] models The list of models. +# +# @return [ true ] +# +# @since 6.0.1 +def clear_tables(*models) + begin; models.map(&:delete_all); rescue; end and true +end + +# Drop all tables of models registered as subclasses of ActiveRecord::Base. +# +# @return [ true ] +# +# @since 6.0.1 +def drop_all_tables! + ActiveRecord::Base.descendants.each do |model| + begin + ActiveRecord::Schema.define do + drop_table model + end if model.table_exists? + rescue + end + end and true +end + +# Drop all indices of models registered as subclasses of ActiveRecord::Base. +# +# @return [ true ] +# +# @since 6.0.1 +def delete_all_indices! + client = Elasticsearch::Model.client + ActiveRecord::Base.descendants.each do |model| + begin + client.indices.delete(index: model.index_name) if model.__elasticsearch__.index_exists? + rescue + end + end and true +end + +# Remove all classes. +# +# @param [ Array ] classes The list of classes to remove. +# +# @return [ true ] +# +# @since 6.0.1 +def remove_classes(*classes) + classes.each do |_class| + Object.send(:remove_const, _class.name.to_sym) if defined?(_class) + end and true +end + +# Determine whether the tests with Mongoid should be run. +# Depends on whether MongoDB is running on the default host and port, `localhost:27017`. +# +# @return [ true, false ] +# +# @since 6.0.1 +def test_mongoid? + $mongoid_available ||= begin + require 'mongoid' + if defined?(Mongo) # older versions of Mongoid use the driver, Moped + client = Mongo::Client.new(['localhost:27017']) + Timeout.timeout(1) do + client.database.command(ping: 1) && true + end + end and true + rescue Timeout::Error, LoadError, Mongo::Error => e + client.close + $stderr.puts("MongoDB not installed or running: #{e}") + end +end + +# Connect Mongoid and set up its Logger if Mongoid tests should be run. +# +# @since 6.0.1 +def connect_mongoid(source) + if test_mongoid? + $stderr.puts "Mongoid #{Mongoid::VERSION}", '-'*80 + + if !ENV['QUIET'] == 'true' + logger = ::Logger.new($stderr) + logger.formatter = lambda { |s, d, p, m| " #{m.ansi(:faint, :cyan)}\n" } + logger.level = ::Logger::DEBUG + Mongoid.logger = logger + Mongo::Logger.logger = logger + else + Mongo::Logger.logger.level = ::Logger::WARN + end + + Mongoid.connect_to(source) + end end diff --git a/elasticsearch-model/spec/support/app.rb b/elasticsearch-model/spec/support/app.rb new file mode 100644 index 000000000..c06ca9ca5 --- /dev/null +++ b/elasticsearch-model/spec/support/app.rb @@ -0,0 +1,21 @@ +require 'active_record' + +require 'support/app/question' +require 'support/app/answer' +require 'support/app/parent_and_child_searchable' +require 'support/app/article_with_custom_serialization' +require 'support/app/import_article' +require 'support/app/namespaced_book' +require 'support/app/article_for_pagination' +require 'support/app/article_with_dynamic_index_name' +require 'support/app/episode' +require 'support/app/image' +require 'support/app/series' +require 'support/app/mongoid_article' +require 'support/app/article' +require 'support/app/searchable' +require 'support/app/category' +require 'support/app/author' +require 'support/app/authorship' +require 'support/app/comment' +require 'support/app/post' diff --git a/elasticsearch-model/spec/support/app/answer.rb b/elasticsearch-model/spec/support/app/answer.rb new file mode 100644 index 000000000..7de32dc7b --- /dev/null +++ b/elasticsearch-model/spec/support/app/answer.rb @@ -0,0 +1,33 @@ +class Answer < ActiveRecord::Base + include Elasticsearch::Model + + belongs_to :question + + 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 do + indexes :text + 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_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 \ No newline at end of file diff --git a/elasticsearch-model/spec/support/app/article.rb b/elasticsearch-model/spec/support/app/article.rb new file mode 100644 index 000000000..ddff706ef --- /dev/null +++ b/elasticsearch-model/spec/support/app/article.rb @@ -0,0 +1,22 @@ +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' + indexes :body, type: 'text' + indexes :clicks, type: 'integer' + indexes :created_at, type: 'date' + end + end + + def as_indexed_json(options = {}) + attributes + .symbolize_keys + .slice(:title, :body, :clicks, :created_at) + .merge(suggest_title: title) + end +end diff --git a/elasticsearch-model/spec/support/app/article_for_pagination.rb b/elasticsearch-model/spec/support/app/article_for_pagination.rb new file mode 100644 index 000000000..8bea633c1 --- /dev/null +++ b/elasticsearch-model/spec/support/app/article_for_pagination.rb @@ -0,0 +1,12 @@ +class ::ArticleForPagination < ActiveRecord::Base + include Elasticsearch::Model + + scope :published, -> { where(published: true) } + + settings index: { number_of_shards: 1, number_of_replicas: 0 } do + mapping do + indexes :title, type: 'text', analyzer: 'snowball' + indexes :created_at, type: 'date' + end + end +end diff --git a/elasticsearch-model/spec/support/app/article_with_custom_serialization.rb b/elasticsearch-model/spec/support/app/article_with_custom_serialization.rb new file mode 100644 index 000000000..c03b19ea5 --- /dev/null +++ b/elasticsearch-model/spec/support/app/article_with_custom_serialization.rb @@ -0,0 +1,13 @@ +class ::ArticleWithCustomSerialization < ActiveRecord::Base + include Elasticsearch::Model + include Elasticsearch::Model::Callbacks + + mapping do + indexes :title + end + + def as_indexed_json(options={}) + # as_json(options.merge root: false).slice('title') + { title: self.title } + end +end diff --git a/elasticsearch-model/spec/support/app/article_with_dynamic_index_name.rb b/elasticsearch-model/spec/support/app/article_with_dynamic_index_name.rb new file mode 100644 index 000000000..7c53d04bf --- /dev/null +++ b/elasticsearch-model/spec/support/app/article_with_dynamic_index_name.rb @@ -0,0 +1,15 @@ +class ::ArticleWithDynamicIndexName < ActiveRecord::Base + include Elasticsearch::Model + include Elasticsearch::Model::Callbacks + + def self.counter=(value) + @counter = 0 + end + + def self.counter + (@counter ||= 0) && @counter += 1 + end + + mapping { indexes :title } + index_name { "articles-#{counter}" } +end diff --git a/elasticsearch-model/spec/support/app/author.rb b/elasticsearch-model/spec/support/app/author.rb new file mode 100644 index 000000000..ff1664af7 --- /dev/null +++ b/elasticsearch-model/spec/support/app/author.rb @@ -0,0 +1,9 @@ +class Author < ActiveRecord::Base + has_many :authorships + + after_update { self.authorships.each(&:touch) } + + def full_name + [first_name, last_name].compact.join(' ') + end +end diff --git a/elasticsearch-model/spec/support/app/authorship.rb b/elasticsearch-model/spec/support/app/authorship.rb new file mode 100644 index 000000000..70bc2458f --- /dev/null +++ b/elasticsearch-model/spec/support/app/authorship.rb @@ -0,0 +1,4 @@ +class Authorship < ActiveRecord::Base + belongs_to :author + belongs_to :post, touch: true +end diff --git a/elasticsearch-model/spec/support/app/category.rb b/elasticsearch-model/spec/support/app/category.rb new file mode 100644 index 000000000..751413c0d --- /dev/null +++ b/elasticsearch-model/spec/support/app/category.rb @@ -0,0 +1,3 @@ +class Category < ActiveRecord::Base + has_and_belongs_to_many :posts +end diff --git a/elasticsearch-model/spec/support/app/comment.rb b/elasticsearch-model/spec/support/app/comment.rb new file mode 100644 index 000000000..49a25832c --- /dev/null +++ b/elasticsearch-model/spec/support/app/comment.rb @@ -0,0 +1,3 @@ +class Comment < ActiveRecord::Base + belongs_to :post, touch: true +end diff --git a/elasticsearch-model/spec/support/app/episode.rb b/elasticsearch-model/spec/support/app/episode.rb new file mode 100644 index 000000000..6cd159c26 --- /dev/null +++ b/elasticsearch-model/spec/support/app/episode.rb @@ -0,0 +1,11 @@ +class Episode < ActiveRecord::Base + include Elasticsearch::Model + include Elasticsearch::Model::Callbacks + + settings index: {number_of_shards: 1, number_of_replicas: 0} do + mapping do + indexes :name, type: 'text', analyzer: 'snowball' + indexes :created_at, type: 'date' + end + end +end diff --git a/elasticsearch-model/spec/support/app/image.rb b/elasticsearch-model/spec/support/app/image.rb new file mode 100644 index 000000000..8bddcd08b --- /dev/null +++ b/elasticsearch-model/spec/support/app/image.rb @@ -0,0 +1,19 @@ +class Image + include Mongoid::Document + include Elasticsearch::Model + include Elasticsearch::Model::Callbacks + + field :name, type: String + attr_accessible :name if respond_to? :attr_accessible + + settings index: {number_of_shards: 1, number_of_replicas: 0} do + mapping do + indexes :name, type: 'text', analyzer: 'snowball' + indexes :created_at, type: 'date' + end + end + + def as_indexed_json(options={}) + as_json(except: [:_id]) + end +end diff --git a/elasticsearch-model/spec/support/app/import_article.rb b/elasticsearch-model/spec/support/app/import_article.rb new file mode 100644 index 000000000..d25580786 --- /dev/null +++ b/elasticsearch-model/spec/support/app/import_article.rb @@ -0,0 +1,12 @@ +class ImportArticle < ActiveRecord::Base + include Elasticsearch::Model + + scope :popular, -> { where('views >= 5') } + + mapping do + indexes :title, type: 'text' + indexes :views, type: 'integer' + indexes :numeric, type: 'integer' + indexes :created_at, type: 'date' + end +end diff --git a/elasticsearch-model/spec/support/app/mongoid_article.rb b/elasticsearch-model/spec/support/app/mongoid_article.rb new file mode 100644 index 000000000..cf3a67a84 --- /dev/null +++ b/elasticsearch-model/spec/support/app/mongoid_article.rb @@ -0,0 +1,21 @@ +class ::MongoidArticle + include Mongoid::Document + include Elasticsearch::Model + include Elasticsearch::Model::Callbacks + + field :id, type: String + field :title, type: String + field :views, type: Integer + attr_accessible :title if respond_to? :attr_accessible + + settings index: { number_of_shards: 1, number_of_replicas: 0 } do + mapping do + indexes :title, type: 'text', analyzer: 'snowball' + indexes :created_at, type: 'date' + end + end + + def as_indexed_json(options={}) + as_json(except: [:id, :_id]) + end +end diff --git a/elasticsearch-model/spec/support/app/namespaced_book.rb b/elasticsearch-model/spec/support/app/namespaced_book.rb new file mode 100644 index 000000000..07a500928 --- /dev/null +++ b/elasticsearch-model/spec/support/app/namespaced_book.rb @@ -0,0 +1,10 @@ +module MyNamespace + class Book < ActiveRecord::Base + include Elasticsearch::Model + include Elasticsearch::Model::Callbacks + + document_type 'book' + + mapping { indexes :title } + end +end diff --git a/elasticsearch-model/spec/support/app/parent_and_child_searchable.rb b/elasticsearch-model/spec/support/app/parent_and_child_searchable.rb new file mode 100644 index 000000000..fd2f4417a --- /dev/null +++ b/elasticsearch-model/spec/support/app/parent_and_child_searchable.rb @@ -0,0 +1,24 @@ +module ParentChildSearchable + 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 + 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 } + end + + extend self +end diff --git a/elasticsearch-model/spec/support/app/post.rb b/elasticsearch-model/spec/support/app/post.rb new file mode 100644 index 000000000..0cdbba7bb --- /dev/null +++ b/elasticsearch-model/spec/support/app/post.rb @@ -0,0 +1,14 @@ +class Post < ActiveRecord::Base + include Searchable + + has_and_belongs_to_many :categories, after_add: [ lambda { |a,c| a.__elasticsearch__.index_document } ], + after_remove: [ lambda { |a,c| a.__elasticsearch__.index_document } ] + has_many :authorships + has_many :authors, through: :authorships, + after_add: [ lambda { |a,c| a.__elasticsearch__.index_document } ], + after_remove: [ lambda { |a,c| a.__elasticsearch__.index_document } ] + has_many :comments, after_add: [ lambda { |a,c| a.__elasticsearch__.index_document } ], + after_remove: [ lambda { |a,c| a.__elasticsearch__.index_document } ] + + after_touch() { __elasticsearch__.index_document } +end diff --git a/elasticsearch-model/spec/support/app/question.rb b/elasticsearch-model/spec/support/app/question.rb new file mode 100644 index 000000000..f64a97a92 --- /dev/null +++ b/elasticsearch-model/spec/support/app/question.rb @@ -0,0 +1,27 @@ +class Question < ActiveRecord::Base + include Elasticsearch::Model + + has_many :answers, dependent: :destroy + + 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 + indexes :text + 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 +end diff --git a/elasticsearch-model/spec/support/app/searchable.rb b/elasticsearch-model/spec/support/app/searchable.rb new file mode 100644 index 000000000..826a64875 --- /dev/null +++ b/elasticsearch-model/spec/support/app/searchable.rb @@ -0,0 +1,48 @@ +module Searchable + extend ActiveSupport::Concern + + included do + include Elasticsearch::Model + include Elasticsearch::Model::Callbacks + + # Set up the mapping + # + settings index: { number_of_shards: 1, number_of_replicas: 0 } do + mapping do + indexes :title, analyzer: 'snowball' + indexes :created_at, type: 'date' + + indexes :authors do + indexes :first_name + indexes :last_name + indexes :full_name, type: 'text' do + indexes :raw, type: 'keyword' + end + end + + indexes :categories, type: 'keyword' + + indexes :comments, type: 'nested' do + indexes :text + indexes :author + end + end + end + + # Customize the JSON serialization for Elasticsearch + # + def as_indexed_json(options={}) + { + title: title, + text: text, + categories: categories.map(&:title), + authors: authors.as_json(methods: [:full_name], only: [:full_name, :first_name, :last_name]), + comments: comments.as_json(only: [:text, :author]) + } + end + + # Update document in the index after touch + # + after_touch() { __elasticsearch__.index_document } + end +end diff --git a/elasticsearch-model/spec/support/app/series.rb b/elasticsearch-model/spec/support/app/series.rb new file mode 100644 index 000000000..d10a04748 --- /dev/null +++ b/elasticsearch-model/spec/support/app/series.rb @@ -0,0 +1,11 @@ +class Series < ActiveRecord::Base + include Elasticsearch::Model + include Elasticsearch::Model::Callbacks + + settings index: {number_of_shards: 1, number_of_replicas: 0} do + mapping do + indexes :name, type: 'text', analyzer: 'snowball' + indexes :created_at, type: 'date' + end + end +end 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 deleted file mode 100644 index fb3b07e1d..000000000 --- a/elasticsearch-model/test/integration/active_record_associations_parent_child_test.rb +++ /dev/null @@ -1,188 +0,0 @@ -require 'test_helper' -require 'active_record' - -# Needed for ActiveRecord 3.x ? -ActiveRecord::Base.establish_connection( :adapter => 'sqlite3', :database => ":memory:" ) unless ActiveRecord::Base.connected? - -::ActiveRecord::Base.raise_in_transactional_callbacks = true if ::ActiveRecord::Base.respond_to?(:raise_in_transactional_callbacks) && ::ActiveRecord::VERSION::MAJOR.to_s < '5' - -class Question < ActiveRecord::Base - include Elasticsearch::Model - - has_many :answers, dependent: :destroy - - 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 - indexes :text - 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 -end - -class Answer < ActiveRecord::Base - include Elasticsearch::Model - - belongs_to :question - - 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 do - indexes :text - 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_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'.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 - 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 } - end - - extend self -end - -module Elasticsearch - module Model - class ActiveRecordAssociationsParentChildIntegrationTest < Elasticsearch::Test::IntegrationTestCase - - context "ActiveRecord associations with parent/child modelling" do - setup do - ActiveRecord::Schema.define(version: 1) do - create_table :questions do |t| - t.string :title - t.text :text - t.string :author - t.timestamps null: false - end - - create_table :answers do |t| - t.text :text - t.string :author - t.references :question - t.timestamps null: false - end - - add_index(:answers, :question_id) unless index_exists?(:answers, :question_id) - end - - Question.delete_all - ParentChildSearchable.create_index! force: true - - q_1 = Question.create! title: 'First Question', author: 'John' - q_2 = Question.create! title: 'Second Question', author: 'Jody' - - q_1.answers.create! text: 'Lorem Ipsum', author: 'Adam' - q_1.answers.create! text: 'Dolor Sit', author: 'Ryan' - - q_2.answers.create! text: 'Amet Et', author: 'John' - - Question.__elasticsearch__.refresh_index! - end - - should "find questions by matching answers" do - response = Question.search( - { 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' - } - } - } - } - }) - - assert_same_elements ['Adam', 'Ryan'], response.records.map(&:author) - end - - should "delete answers when the question is deleted" do - Question.where(title: 'First Question').each(&:destroy) - Question.__elasticsearch__.refresh_index! - - 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-model/test/integration/active_record_associations_test.rb b/elasticsearch-model/test/integration/active_record_associations_test.rb deleted file mode 100644 index 87a1301d8..000000000 --- a/elasticsearch-model/test/integration/active_record_associations_test.rb +++ /dev/null @@ -1,339 +0,0 @@ -require 'test_helper' -require 'active_record' - -# Needed for ActiveRecord 3.x ? -ActiveRecord::Base.establish_connection( :adapter => 'sqlite3', :database => ":memory:" ) unless ActiveRecord::Base.connected? - -::ActiveRecord::Base.raise_in_transactional_callbacks = true if ::ActiveRecord::Base.respond_to?(:raise_in_transactional_callbacks) && ::ActiveRecord::VERSION::MAJOR.to_s < '5' - -module Elasticsearch - module Model - class ActiveRecordAssociationsIntegrationTest < Elasticsearch::Test::IntegrationTestCase - - # ----- Search integration via Concern module ----------------------------------------------------- - - module Searchable - extend ActiveSupport::Concern - - included do - include Elasticsearch::Model - include Elasticsearch::Model::Callbacks - - # Set up the mapping - # - settings index: { number_of_shards: 1, number_of_replicas: 0 } do - mapping do - indexes :title, analyzer: 'snowball' - indexes :created_at, type: 'date' - - indexes :authors do - indexes :first_name - indexes :last_name - indexes :full_name, type: 'text' do - indexes :raw, type: 'keyword' - end - end - - indexes :categories, type: 'keyword' - - indexes :comments, type: 'nested' do - indexes :text - indexes :author - end - end - end - - # Customize the JSON serialization for Elasticsearch - # - def as_indexed_json(options={}) - { - title: title, - text: text, - categories: categories.map(&:title), - authors: authors.as_json(methods: [:full_name], only: [:full_name, :first_name, :last_name]), - comments: comments.as_json(only: [:text, :author]) - } - end - - # Update document in the index after touch - # - after_touch() { __elasticsearch__.index_document } - end - end - - context "ActiveRecord associations" do - setup do - - # ----- Schema definition --------------------------------------------------------------- - - ActiveRecord::Schema.define(version: 1) do - create_table :categories do |t| - t.string :title - t.timestamps null: false - end - - create_table :categories_posts, id: false do |t| - t.references :post, :category - end - - create_table :authors do |t| - t.string :first_name, :last_name - t.timestamps null: false - end - - create_table :authorships do |t| - t.string :first_name, :last_name - t.references :post - t.references :author - t.timestamps null: false - end - - create_table :comments do |t| - t.string :text - t.string :author - t.references :post - t.timestamps null: false - end - - add_index(:comments, :post_id) unless index_exists?(:comments, :post_id) - - create_table :posts do |t| - t.string :title - t.text :text - t.boolean :published - t.timestamps null: false - end - end - - # ----- Models definition ------------------------------------------------------------------------- - - class Category < ActiveRecord::Base - has_and_belongs_to_many :posts - end - - class Author < ActiveRecord::Base - has_many :authorships - - after_update { self.authorships.each(&:touch) } - - def full_name - [first_name, last_name].compact.join(' ') - end - end - - class Authorship < ActiveRecord::Base - belongs_to :author - belongs_to :post, touch: true - end - - class Comment < ActiveRecord::Base - belongs_to :post, touch: true - end - - class Post < ActiveRecord::Base - has_and_belongs_to_many :categories, after_add: [ lambda { |a,c| a.__elasticsearch__.index_document } ], - after_remove: [ lambda { |a,c| a.__elasticsearch__.index_document } ] - has_many :authorships - has_many :authors, through: :authorships, - after_add: [ lambda { |a,c| a.__elasticsearch__.index_document } ], - after_remove: [ lambda { |a,c| a.__elasticsearch__.index_document } ] - has_many :comments, after_add: [ lambda { |a,c| a.__elasticsearch__.index_document } ], - after_remove: [ lambda { |a,c| a.__elasticsearch__.index_document } ] - - after_touch() { __elasticsearch__.index_document } - end - - # Include the search integration - # - Post.__send__ :include, Searchable - Comment.__send__ :include, Elasticsearch::Model - Comment.__send__ :include, Elasticsearch::Model::Callbacks - - # ----- Reset the indices ----------------------------------------------------------------- - - Post.delete_all - Post.__elasticsearch__.create_index! force: true - - Comment.delete_all - Comment.__elasticsearch__.create_index! force: true - end - - should "index and find a document" do - Post.create! title: 'Test' - Post.create! title: 'Testing Coding' - Post.create! title: 'Coding' - Post.__elasticsearch__.refresh_index! - - response = Post.search('title:test') - - assert_equal 2, response.results.size - assert_equal 2, response.records.size - - assert_equal 'Test', response.results.first.title - assert_equal 'Test', response.records.first.title - end - - should "reindex a document after categories are changed" do - # Create categories - category_a = Category.where(title: "One").first_or_create! - category_b = Category.where(title: "Two").first_or_create! - - # Create post - post = Post.create! title: "First Post", text: "This is the first post..." - - # Assign categories - post.categories = [category_a, category_b] - - Post.__elasticsearch__.refresh_index! - - query = { query: { - bool: { - must: { - multi_match: { - fields: ['title'], - query: 'first' - } - }, - filter: { - terms: { - categories: ['One'] - } - } - } - } - } - - response = Post.search query - - assert_equal 1, response.results.size - assert_equal 1, response.records.size - - # Remove category "One" - post.categories = [category_b] - - Post.__elasticsearch__.refresh_index! - response = Post.search query - - assert_equal 0, response.results.size - assert_equal 0, response.records.size - end - - should "reindex a document after authors are changed" do - # Create authors - author_a = Author.where(first_name: "John", last_name: "Smith").first_or_create! - author_b = Author.where(first_name: "Mary", last_name: "Smith").first_or_create! - author_c = Author.where(first_name: "Kobe", last_name: "Griss").first_or_create! - - # Create posts - post_1 = Post.create! title: "First Post", text: "This is the first post..." - post_2 = Post.create! title: "Second Post", text: "This is the second post..." - post_3 = Post.create! title: "Third Post", text: "This is the third post..." - - # Assign authors - post_1.authors = [author_a, author_b] - post_2.authors = [author_a] - post_3.authors = [author_c] - - Post.__elasticsearch__.refresh_index! - - response = Post.search 'authors.full_name:john' - - assert_equal 2, response.results.size - assert_equal 2, response.records.size - - post_3.authors << author_a - - Post.__elasticsearch__.refresh_index! - - response = Post.search 'authors.full_name:john' - - assert_equal 3, response.results.size - assert_equal 3, response.records.size - end if defined?(::ActiveRecord) && ::ActiveRecord::VERSION::MAJOR >= 4 - - should "reindex a document after comments are added" do - # Create posts - post_1 = Post.create! title: "First Post", text: "This is the first post..." - post_2 = Post.create! title: "Second Post", text: "This is the second post..." - - # Add comments - post_1.comments.create! author: 'John', text: 'Excellent' - post_1.comments.create! author: 'Abby', text: 'Good' - - post_2.comments.create! author: 'John', text: 'Terrible' - - Post.__elasticsearch__.refresh_index! - - response = Post.search 'comments.author:john AND comments.text:good' - assert_equal 0, response.results.size - - # Add comment - post_1.comments.create! author: 'John', text: 'Or rather just good...' - - Post.__elasticsearch__.refresh_index! - - response = Post.search 'comments.author:john AND comments.text:good' - assert_equal 0, response.results.size - - response = Post.search \ - query: { - nested: { - path: 'comments', - query: { - bool: { - must: [ - { match: { 'comments.author' => 'john' } }, - { match: { 'comments.text' => 'good' } } - ] - } - } - } - } - - assert_equal 1, response.results.size - end if defined?(::ActiveRecord) && ::ActiveRecord::VERSION::MAJOR >= 4 - - should "reindex a document after Post#touch" do - # Create categories - category_a = Category.where(title: "One").first_or_create! - - # Create post - post = Post.create! title: "First Post", text: "This is the first post..." - - # Assign category - post.categories << category_a - - Post.__elasticsearch__.refresh_index! - - assert_equal 1, Post.search('categories:One').size - - # Update category - category_a.update_attribute :title, "Updated" - - # Trigger touch on posts in category - category_a.posts.each { |p| p.touch } - - Post.__elasticsearch__.refresh_index! - - assert_equal 0, Post.search('categories:One').size - assert_equal 1, Post.search('categories:Updated').size - end - - should "eagerly load associated records" do - post_1 = Post.create(title: 'One') - post_2 = Post.create(title: 'Two') - post_1.comments.create text: 'First comment' - post_1.comments.create text: 'Second comment' - - Comment.__elasticsearch__.refresh_index! - - records = Comment.search('first').records(includes: :post) - - assert records.first.association(:post).loaded?, "The associated Post should be eagerly loaded" - assert_equal 'One', records.first.post.title - end - end - - end - end -end diff --git a/elasticsearch-model/test/integration/active_record_basic_test.rb b/elasticsearch-model/test/integration/active_record_basic_test.rb deleted file mode 100644 index d7ecb0586..000000000 --- a/elasticsearch-model/test/integration/active_record_basic_test.rb +++ /dev/null @@ -1,263 +0,0 @@ -require 'test_helper' -require 'active_record' - -puts "ActiveRecord #{ActiveRecord::VERSION::STRING}", '-'*80 - -# Needed for ActiveRecord 3.x ? -ActiveRecord::Base.establish_connection( :adapter => 'sqlite3', :database => ":memory:" ) unless ActiveRecord::Base.connected? - -::ActiveRecord::Base.raise_in_transactional_callbacks = true if ::ActiveRecord::Base.respond_to?(:raise_in_transactional_callbacks) && ::ActiveRecord::VERSION::MAJOR.to_s < '5' - -module Elasticsearch - module Model - class ActiveRecordBasicIntegrationTest < Elasticsearch::Test::IntegrationTestCase - - 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' - indexes :body, type: 'text' - indexes :clicks, type: 'integer' - indexes :created_at, type: 'date' - end - end - - def as_indexed_json(options = {}) - attributes - .symbolize_keys - .slice(:title, :body, :clicks, :created_at) - .merge(suggest_title: title) - end - end - - context "ActiveRecord basic integration" do - setup do - ActiveRecord::Schema.define(:version => 1) do - create_table :articles do |t| - t.string :title - t.string :body - t.integer :clicks, :default => 0 - t.datetime :created_at, :default => 'NOW()' - end - end - - Article.delete_all - Article.__elasticsearch__.create_index! force: true - - ::Article.create! title: 'Test', body: '', clicks: 1 - ::Article.create! title: 'Testing Coding', body: '', clicks: 2 - ::Article.create! title: 'Coding', body: '', clicks: 3 - - Article.__elasticsearch__.refresh_index! - end - - should "index and find a document" do - response = Article.search('title:test') - - assert response.any?, "Response should not be empty: #{response.to_a.inspect}" - - assert_equal 2, response.results.size - assert_equal 2, response.records.size - - assert_instance_of Elasticsearch::Model::Response::Result, response.results.first - assert_instance_of Article, response.records.first - - assert_equal 'Test', response.results.first.title - assert_equal 'Test', response.records.first.title - end - - should "provide access to result" do - response = Article.search query: { match: { title: 'test' } }, highlight: { fields: { title: {} } } - - assert_equal 'Test', response.results.first.title - - assert_equal true, response.results.first.title? - assert_equal false, response.results.first.boo? - - assert_equal true, response.results.first.highlight? - assert_equal true, response.results.first.highlight.title? - assert_equal false, response.results.first.highlight.boo? - end - - should "iterate over results" do - response = Article.search('title:test') - - assert_equal ['1', '2'], response.results.map(&:_id) - assert_equal [1, 2], response.records.map(&:id) - end - - should "return _id and _type as #id and #type" do - response = Article.search('title:test') - - assert_equal '1', response.results.first.id - assert_equal 'article', response.results.first.type - end - - should "access results from records" do - response = Article.search('title:test') - - response.records.each_with_hit do |r, h| - assert_not_nil h._score - assert_not_nil h._source.title - end - end - - should "preserve the search results order for records" do - response = Article.search query: { match: { title: 'code' }}, sort: { clicks: :desc } - - assert_equal response.records[0].clicks, 3 - assert_equal response.records[0], response.records.first - assert_equal response.records[1].clicks, 2 - - response.records.each_with_hit do |r, h| - assert_equal h._id, r.id.to_s - end - - response.records.map_with_hit do |r, h| - assert_equal h._id, r.id.to_s - end - end - - should "remove document from index on destroy" do - article = Article.first - - article.destroy - assert_equal 2, Article.count - - Article.__elasticsearch__.refresh_index! - - response = Article.search 'title:test' - - assert_equal 1, response.results.size - assert_equal 1, response.records.size - end - - should "index updates to the document" do - article = Article.first - - article.title = 'Writing' - article.save - - Article.__elasticsearch__.refresh_index! - - response = Article.search 'title:write' - - assert_equal 1, response.results.size - assert_equal 1, response.records.size - end - - should "update specific attributes" do - article = Article.first - - response = Article.search 'title:special' - - assert_equal 0, response.results.size - assert_equal 0, response.records.size - - article.__elasticsearch__.update_document_attributes title: 'special' - - Article.__elasticsearch__.refresh_index! - - response = Article.search 'title:special' - - assert_equal 1, response.results.size - assert_equal 1, response.records.size - end - - should "update document when save is called multiple times in a transaction" do - article = Article.first - response = Article.search 'body:dummy' - - assert_equal 0, response.results.size - assert_equal 0, response.records.size - - ActiveRecord::Base.transaction do - article.body = 'dummy' - article.save - - article.title = 'special' - article.save - end - - article.__elasticsearch__.update_document - Article.__elasticsearch__.refresh_index! - - response = Article.search 'body:dummy' - assert_equal 1, response.results.size - assert_equal 1, response.records.size - end - - should "return results for a DSL search" do - response = Article.search query: { match: { title: { query: 'test' } } } - - assert_equal 2, response.results.size - assert_equal 2, response.records.size - end - - should "return a paged collection" do - response = Article.search query: { match: { title: { query: 'test' } } }, - size: 2, - from: 1 - - assert_equal 1, response.results.size - assert_equal 1, response.records.size - - assert_equal 'Testing Coding', response.results.first.title - assert_equal 'Testing Coding', response.records.first.title - end - - should "allow chaining SQL commands on response.records" do - response = Article.search query: { match: { title: { query: 'test' } } } - - assert_equal 2, response.records.size - assert_equal 1, response.records.where(title: 'Test').size - assert_equal 'Test', response.records.where(title: 'Test').first.title - end - - should "allow ordering response.records in SQL" do - response = Article.search query: { match: { title: { query: 'test' } } } - - if defined?(::ActiveRecord) && ::ActiveRecord::VERSION::MAJOR >= 4 - assert_equal 'Testing Coding', response.records.order(title: :desc).first.title - assert_equal 'Testing Coding', response.records.order(title: :desc)[0].title - else - assert_equal 'Testing Coding', response.records.order('title DESC').first.title - assert_equal 'Testing Coding', response.records.order('title DESC')[0].title - end - end - - should "allow ordering following any method chain in SQL" do - if defined?(::ActiveRecord) && ::ActiveRecord::VERSION::MAJOR >= 4 - response = Article.search query: { match: { title: { query: 'test' } } } - assert_equal 'Testing Coding', response.records.distinct.order(id: :desc).first.title - end - end - - should "allow dot access to response" do - response = Article.search query: { match: { title: { query: 'test' } } }, - aggregations: { - dates: { date_histogram: { field: 'created_at', interval: 'hour' } }, - clicks: { global: {}, aggregations: { min: { min: { field: 'clicks' } } } } - }, - suggest: { text: 'tezt', title: { term: { field: 'title', suggest_mode: 'always' } } } - - response.response.respond_to?(:aggregations) - assert_equal 2, response.aggregations.dates.buckets.first.doc_count - assert_equal 3, response.aggregations.clicks.doc_count - assert_equal 1.0, response.aggregations.clicks.min.value - assert_nil response.aggregations.clicks.max - - response.response.respond_to?(:suggest) - assert_equal 1, response.suggestions.title.first.options.size - assert_equal ['test'], response.suggestions.terms - end - end - - end - end -end diff --git a/elasticsearch-model/test/integration/active_record_custom_serialization_test.rb b/elasticsearch-model/test/integration/active_record_custom_serialization_test.rb deleted file mode 100644 index 9847967e5..000000000 --- a/elasticsearch-model/test/integration/active_record_custom_serialization_test.rb +++ /dev/null @@ -1,67 +0,0 @@ -require 'test_helper' -require 'active_record' - -# Needed for ActiveRecord 3.x ? -ActiveRecord::Base.establish_connection( :adapter => 'sqlite3', :database => ":memory:" ) unless ActiveRecord::Base.connected? - -::ActiveRecord::Base.raise_in_transactional_callbacks = true if ::ActiveRecord::Base.respond_to?(:raise_in_transactional_callbacks) && ::ActiveRecord::VERSION::MAJOR.to_s < '5' - -module Elasticsearch - module Model - class ActiveRecordCustomSerializationTest < Elasticsearch::Test::IntegrationTestCase - context "ActiveRecord model with custom JSON serialization" do - setup do - class ::ArticleWithCustomSerialization < ActiveRecord::Base - include Elasticsearch::Model - include Elasticsearch::Model::Callbacks - - mapping do - indexes :title - end - - def as_indexed_json(options={}) - # as_json(options.merge root: false).slice('title') - { title: self.title } - end - end - - ActiveRecord::Schema.define(:version => 1) do - create_table ArticleWithCustomSerialization.table_name do |t| - t.string :title - t.string :status - end - end - - ArticleWithCustomSerialization.delete_all - ArticleWithCustomSerialization.__elasticsearch__.create_index! force: true - end - - should "index only the title attribute when creating" do - ArticleWithCustomSerialization.create! title: 'Test', status: 'green' - - a = ArticleWithCustomSerialization.__elasticsearch__.client.get \ - index: 'article_with_custom_serializations', - type: '_doc', - id: '1' - - assert_equal( { 'title' => 'Test' }, a['_source'] ) - end - - should "index only the title attribute when updating" do - ArticleWithCustomSerialization.create! title: 'Test', status: 'green' - - article = ArticleWithCustomSerialization.first - article.update_attributes title: 'UPDATED', status: 'red' - - a = ArticleWithCustomSerialization.__elasticsearch__.client.get \ - index: 'article_with_custom_serializations', - type: '_doc', - id: '1' - - assert_equal( { 'title' => 'UPDATED' }, a['_source'] ) - end - end - - end - end -end diff --git a/elasticsearch-model/test/integration/active_record_import_test.rb b/elasticsearch-model/test/integration/active_record_import_test.rb deleted file mode 100644 index 1e9d9957e..000000000 --- a/elasticsearch-model/test/integration/active_record_import_test.rb +++ /dev/null @@ -1,198 +0,0 @@ -require 'test_helper' -require 'active_record' - -# Needed for ActiveRecord 3.x ? -ActiveRecord::Base.establish_connection( :adapter => 'sqlite3', :database => ":memory:" ) unless ActiveRecord::Base.connected? - -::ActiveRecord::Base.raise_in_transactional_callbacks = true if ::ActiveRecord::Base.respond_to?(:raise_in_transactional_callbacks) && ::ActiveRecord::VERSION::MAJOR.to_s < '5' - -module Elasticsearch - module Model - class ActiveRecordImportIntegrationTest < Elasticsearch::Test::IntegrationTestCase - - context "ActiveRecord importing" do - setup do - Object.send(:remove_const, :ImportArticle) if defined?(ImportArticle) - class ::ImportArticle < ActiveRecord::Base - include Elasticsearch::Model - - scope :popular, -> { where('views >= 50') } - - mapping do - indexes :title, type: 'text' - indexes :views, type: 'integer' - indexes :numeric, type: 'integer' - indexes :created_at, type: 'date' - end - end - - ActiveRecord::Schema.define(:version => 1) do - create_table :import_articles do |t| - t.string :title - t.integer :views - t.string :numeric # For the sake of invalid data sent to Elasticsearch - t.datetime :created_at, :default => 'NOW()' - end - end - - ImportArticle.delete_all - ImportArticle.__elasticsearch__.create_index! force: true - ImportArticle.__elasticsearch__.client.cluster.health wait_for_status: 'yellow' - - 100.times { |i| ImportArticle.create! title: "Test #{i}", views: i } - end - - should "import all the documents" do - assert_equal 100, ImportArticle.count - - ImportArticle.__elasticsearch__.refresh_index! - assert_equal 0, ImportArticle.search('*').results.total - - batches = 0 - errors = ImportArticle.import(batch_size: 10) do |response| - batches += 1 - end - - assert_equal 0, errors - assert_equal 10, batches - - ImportArticle.__elasticsearch__.refresh_index! - assert_equal 100, ImportArticle.search('*').results.total - end - - should "import only documents from a specific scope" do - assert_equal 100, ImportArticle.count - - assert_equal 0, ImportArticle.import(scope: 'popular') - - ImportArticle.__elasticsearch__.refresh_index! - assert_equal 50, ImportArticle.search('*').results.total - end - - should "import only documents from a specific query" do - assert_equal 100, ImportArticle.count - - assert_equal 0, ImportArticle.import(query: -> { where('views >= 30') }) - - ImportArticle.__elasticsearch__.refresh_index! - assert_equal 70, ImportArticle.search('*').results.total - end - - should "report and not store/index invalid documents" do - ImportArticle.create! title: "Test INVALID", numeric: "INVALID" - - assert_equal 101, ImportArticle.count - - ImportArticle.__elasticsearch__.refresh_index! - assert_equal 0, ImportArticle.search('*').results.total - - batches = 0 - errors = ImportArticle.__elasticsearch__.import(batch_size: 10) do |response| - batches += 1 - end - - assert_equal 1, errors - assert_equal 11, batches - - ImportArticle.__elasticsearch__.refresh_index! - assert_equal 100, ImportArticle.search('*').results.total - end - - should "transform documents with the option" do - assert_equal 100, ImportArticle.count - - assert_equal 0, ImportArticle.import( transform: ->(a) {{ index: { data: { name: a.title, foo: 'BAR' } }}} ) - - ImportArticle.__elasticsearch__.refresh_index! - assert_contains ImportArticle.search('*').results.first._source.keys, 'name' - assert_contains ImportArticle.search('*').results.first._source.keys, 'foo' - assert_equal 100, ImportArticle.search('test').results.total - assert_equal 100, ImportArticle.search('bar').results.total - end - end - - context "ActiveRecord importing when the model has a default scope" do - - setup do - Object.send(:remove_const, :ImportArticle) if defined?(ImportArticle) - class ::ImportArticle < ActiveRecord::Base - include Elasticsearch::Model - - default_scope { where('views >= 8') } - - mapping do - indexes :title, type: 'text' - indexes :views, type: 'integer' - indexes :numeric, type: 'integer' - indexes :created_at, type: 'date' - end - end - - ActiveRecord::Schema.define(:version => 1) do - create_table :import_articles do |t| - t.string :title - t.integer :views - t.string :numeric # For the sake of invalid data sent to Elasticsearch - t.datetime :created_at, :default => 'NOW()' - end - end - - ImportArticle.delete_all - ImportArticle.__elasticsearch__.delete_index! force: true - ImportArticle.__elasticsearch__.create_index! force: true - ImportArticle.__elasticsearch__.client.cluster.health wait_for_status: 'yellow' - - 10.times { |i| ImportArticle.create! title: "Test #{i}", views: i } - end - - should "import only documents from the default scope" do - assert_equal 2, ImportArticle.count - - assert_equal 0, ImportArticle.import - - ImportArticle.__elasticsearch__.refresh_index! - assert_equal 2, ImportArticle.search('*').results.total - end - - should "import only documents from a specific query combined with the default scope" do - assert_equal 2, ImportArticle.count - - assert_equal 0, ImportArticle.import(query: -> { where("title = 'Test 9'") }) - - ImportArticle.__elasticsearch__.refresh_index! - assert_equal 1, ImportArticle.search('*').results.total - end - end - - context 'ActiveRecord importing when the batch is empty' do - - setup do - Object.send(:remove_const, :ImportArticle) if defined?(ImportArticle) - class ::ImportArticle < ActiveRecord::Base - include Elasticsearch::Model - mapping { indexes :title, type: 'text' } - end - - ActiveRecord::Schema.define(:version => 1) do - create_table :import_articles do |t| - t.string :title - end - end - - ImportArticle.delete_all - ImportArticle.__elasticsearch__.delete_index! force: true - ImportArticle.__elasticsearch__.create_index! force: true - ImportArticle.__elasticsearch__.client.cluster.health wait_for_status: 'yellow' - end - - should 'not make any requests to create documents to Elasticsearch' do - assert_equal 0, ImportArticle.count - assert_equal 0, ImportArticle.import - - ImportArticle.__elasticsearch__.refresh_index! - assert_equal 0, ImportArticle.search('*').results.total - end - end - end - end -end diff --git a/elasticsearch-model/test/integration/active_record_namespaced_model_test.rb b/elasticsearch-model/test/integration/active_record_namespaced_model_test.rb deleted file mode 100644 index 75e7aa745..000000000 --- a/elasticsearch-model/test/integration/active_record_namespaced_model_test.rb +++ /dev/null @@ -1,56 +0,0 @@ -require 'test_helper' -require 'active_record' - -# Needed for ActiveRecord 3.x ? -ActiveRecord::Base.establish_connection( :adapter => 'sqlite3', :database => ":memory:" ) unless ActiveRecord::Base.connected? - -::ActiveRecord::Base.raise_in_transactional_callbacks = true if ::ActiveRecord::Base.respond_to?(:raise_in_transactional_callbacks) && ::ActiveRecord::VERSION::MAJOR.to_s < '5' - -module Elasticsearch - module Model - class ActiveRecordNamespacedModelIntegrationTest < Elasticsearch::Test::IntegrationTestCase - context "Namespaced ActiveRecord model integration" do - setup do - ActiveRecord::Schema.define(:version => 1) do - create_table :articles do |t| - t.string :title - end - end - - module ::MyNamespace - class Article < ActiveRecord::Base - include Elasticsearch::Model - include Elasticsearch::Model::Callbacks - - document_type 'article' - - mapping { indexes :title } - end - end - - MyNamespace::Article.delete_all - MyNamespace::Article.__elasticsearch__.create_index! force: true - - MyNamespace::Article.create! title: 'Test' - - MyNamespace::Article.__elasticsearch__.refresh_index! - end - - should "have proper index name and document type" do - assert_equal "my_namespace-articles", MyNamespace::Article.index_name - assert_equal "article", MyNamespace::Article.document_type - end - - should "save document into index on save and find it" do - response = MyNamespace::Article.search 'title:test' - - assert response.any?, "No results returned: #{response.inspect}" - assert_equal 1, response.size - - assert_equal 'Test', response.results.first.title - end - end - - end - end -end diff --git a/elasticsearch-model/test/integration/active_record_pagination_test.rb b/elasticsearch-model/test/integration/active_record_pagination_test.rb deleted file mode 100644 index 0b5e259ee..000000000 --- a/elasticsearch-model/test/integration/active_record_pagination_test.rb +++ /dev/null @@ -1,149 +0,0 @@ -require 'test_helper' -require 'active_record' - -# Needed for ActiveRecord 3.x ? -ActiveRecord::Base.establish_connection( :adapter => 'sqlite3', :database => ":memory:" ) unless ActiveRecord::Base.connected? - -::ActiveRecord::Base.raise_in_transactional_callbacks = true if ::ActiveRecord::Base.respond_to?(:raise_in_transactional_callbacks) && ::ActiveRecord::VERSION::MAJOR.to_s < '5' - -module Elasticsearch - module Model - class ActiveRecordPaginationTest < Elasticsearch::Test::IntegrationTestCase - class ::ArticleForPagination < ActiveRecord::Base - include Elasticsearch::Model - - scope :published, -> { where(published: true) } - - settings index: { number_of_shards: 1, number_of_replicas: 0 } do - mapping do - indexes :title, type: 'text', analyzer: 'snowball' - indexes :created_at, type: 'date' - end - end - end - - context "ActiveRecord pagination" do - setup do - ActiveRecord::Schema.define(:version => 1) do - create_table ::ArticleForPagination.table_name do |t| - t.string :title - t.datetime :created_at, :default => 'NOW()' - t.boolean :published - end - end - - Kaminari::Hooks.init if defined?(Kaminari::Hooks) - - ArticleForPagination.delete_all - ArticleForPagination.__elasticsearch__.create_index! force: true - - 68.times do |i| - ::ArticleForPagination.create! title: "Test #{i}", published: (i % 2 == 0) - end - - ArticleForPagination.import - ArticleForPagination.__elasticsearch__.refresh_index! - end - - should "be on the first page by default" do - records = ArticleForPagination.search('title:test').page(1).records - - assert_equal 25, records.size - assert_equal 1, records.current_page - assert_equal nil, records.prev_page - assert_equal 2, records.next_page - assert_equal 3, records.total_pages - - assert records.first_page?, "Should be the first page" - assert ! records.last_page?, "Should NOT be the last page" - assert ! records.out_of_range?, "Should NOT be out of range" - end - - should "load next page" do - records = ArticleForPagination.search('title:test').page(2).records - - assert_equal 25, records.size - assert_equal 2, records.current_page - assert_equal 1, records.prev_page - assert_equal 3, records.next_page - assert_equal 3, records.total_pages - - assert ! records.first_page?, "Should NOT be the first page" - assert ! records.last_page?, "Should NOT be the last page" - assert ! records.out_of_range?, "Should NOT be out of range" - end - - should "load last page" do - records = ArticleForPagination.search('title:test').page(3).records - - assert_equal 18, records.size - assert_equal 3, records.current_page - assert_equal 2, records.prev_page - assert_equal nil, records.next_page - assert_equal 3, records.total_pages - - assert ! records.first_page?, "Should NOT be the first page" - assert records.last_page?, "Should be the last page" - assert ! records.out_of_range?, "Should NOT be out of range" - end - - should "not load invalid page" do - records = ArticleForPagination.search('title:test').page(6).records - - assert_equal 0, records.size - assert_equal 6, records.current_page - - assert_equal nil, records.next_page - assert_equal 3, records.total_pages - - assert ! records.first_page?, "Should NOT be the first page" - assert records.out_of_range?, "Should be out of range" - end - - should "be combined with scopes" do - records = ArticleForPagination.search('title:test').page(2).records.published - assert records.all? { |r| r.published? } - assert_equal 12, records.size - end - - should "respect sort" do - search = ArticleForPagination.search({ query: { match: { title: 'test' } }, sort: [ { id: 'desc' } ] }) - - records = search.page(2).records - assert_equal 43, records.first.id # 68 - 25 = 42 - - records = search.page(3).records - assert_equal 18, records.first.id # 68 - (2 * 25) = 18 - - records = search.page(2).per(5).records - assert_equal 63, records.first.id # 68 - 5 = 63 - end - - should "set the limit per request" do - records = ArticleForPagination.search('title:test').limit(50).page(2).records - - assert_equal 18, records.size - assert_equal 2, records.current_page - assert_equal 1, records.prev_page - assert_equal nil, records.next_page - assert_equal 2, records.total_pages - - assert records.last_page?, "Should be the last page" - end - - context "with specific model settings" do - teardown do - ArticleForPagination.instance_variable_set(:@_default_per_page, nil) - end - - should "respect paginates_per" do - ArticleForPagination.paginates_per 50 - - assert_equal 50, ArticleForPagination.search('*').page(1).records.size - end - end - end - - end - end -end diff --git a/elasticsearch-model/test/integration/dynamic_index_name_test.rb b/elasticsearch-model/test/integration/dynamic_index_name_test.rb deleted file mode 100755 index f87db7978..000000000 --- a/elasticsearch-model/test/integration/dynamic_index_name_test.rb +++ /dev/null @@ -1,52 +0,0 @@ -require 'test_helper' -require 'active_record' - -# Needed for ActiveRecord 3.x ? -ActiveRecord::Base.establish_connection( :adapter => 'sqlite3', :database => ":memory:" ) unless ActiveRecord::Base.connected? - -::ActiveRecord::Base.raise_in_transactional_callbacks = true if ::ActiveRecord::Base.respond_to?(:raise_in_transactional_callbacks) && ::ActiveRecord::VERSION::MAJOR.to_s < '5' - -module Elasticsearch - module Model - class DynamicIndexNameTest < Elasticsearch::Test::IntegrationTestCase - context "Dynamic index name" do - setup do - class ::ArticleWithDynamicIndexName < ActiveRecord::Base - include Elasticsearch::Model - include Elasticsearch::Model::Callbacks - - def self.counter=(value) - @counter = 0 - end - - def self.counter - (@counter ||= 0) && @counter += 1 - end - - mapping { indexes :title } - index_name { "articles-#{counter}" } - end - - ::ActiveRecord::Schema.define(:version => 1) do - create_table ::ArticleWithDynamicIndexName.table_name do |t| - t.string :title - end - end - - ::ArticleWithDynamicIndexName.counter = 0 - end - - should 'evaluate the index_name value' do - assert_equal ArticleWithDynamicIndexName.index_name, "articles-1" - end - - should 're-evaluate the index_name value each time' do - assert_equal ArticleWithDynamicIndexName.index_name, "articles-1" - assert_equal ArticleWithDynamicIndexName.index_name, "articles-2" - assert_equal ArticleWithDynamicIndexName.index_name, "articles-3" - end - end - - end - end -end diff --git a/elasticsearch-model/test/integration/mongoid_basic_test.rb b/elasticsearch-model/test/integration/mongoid_basic_test.rb deleted file mode 100644 index 292c1f1a5..000000000 --- a/elasticsearch-model/test/integration/mongoid_basic_test.rb +++ /dev/null @@ -1,240 +0,0 @@ -require 'test_helper' -MongoDB.setup! - -if MongoDB.available? - MongoDB.connect_to 'mongoid_articles' - - module Elasticsearch - module Model - class MongoidBasicIntegrationTest < Elasticsearch::Test::IntegrationTestCase - - class ::MongoidArticle - include Mongoid::Document - include Elasticsearch::Model - include Elasticsearch::Model::Callbacks - - field :id, type: String - field :title, type: String - attr_accessible :title if respond_to? :attr_accessible - - settings index: { number_of_shards: 1, number_of_replicas: 0 } do - mapping do - indexes :title, type: 'text', analyzer: 'snowball' - indexes :created_at, type: 'date' - end - end - - def as_indexed_json(options={}) - as_json(except: [:id, :_id]) - end - end - - context "Mongoid integration" do - setup do - Elasticsearch::Model::Adapter.register \ - Elasticsearch::Model::Adapter::Mongoid, - lambda { |klass| !!defined?(::Mongoid::Document) && klass.respond_to?(:ancestors) && klass.ancestors.include?(::Mongoid::Document) } - - MongoidArticle.__elasticsearch__.create_index! force: true - - MongoidArticle.delete_all - - MongoidArticle.create! title: 'Test' - MongoidArticle.create! title: 'Testing Coding' - MongoidArticle.create! title: 'Coding' - - MongoidArticle.__elasticsearch__.refresh_index! - MongoidArticle.__elasticsearch__.client.cluster.health wait_for_status: 'yellow' - end - - should "index and find a document" do - response = MongoidArticle.search('title:test') - - assert response.any? - - assert_equal 2, response.results.size - assert_equal 2, response.records.size - - assert_instance_of Elasticsearch::Model::Response::Result, response.results.first - assert_instance_of MongoidArticle, response.records.first - - assert_equal 'Test', response.results.first.title - assert_equal 'Test', response.records.first.title - end - - should "iterate over results" do - response = MongoidArticle.search('title:test') - - assert_equal ['Test', 'Testing Coding'], response.results.map(&:title) - assert_equal ['Test', 'Testing Coding'], response.records.map(&:title) - end - - should "access results from records" do - response = MongoidArticle.search('title:test') - - response.records.each_with_hit do |r, h| - assert_not_nil h._score - assert_not_nil h._source.title - end - end - - should "preserve the search results order for records" do - response = MongoidArticle.search('title:code') - - response.records.each_with_hit do |r, h| - assert_equal h._id, r.id.to_s - end - - response.records.map_with_hit do |r, h| - assert_equal h._id, r.id.to_s - end - end - - should "remove document from index on destroy" do - article = MongoidArticle.first - - article.destroy - assert_equal 2, MongoidArticle.count - - MongoidArticle.__elasticsearch__.refresh_index! - - response = MongoidArticle.search 'title:test' - - assert_equal 1, response.results.size - assert_equal 1, response.records.size - end - - should "index updates to the document" do - article = MongoidArticle.first - - article.title = 'Writing' - article.save - - MongoidArticle.__elasticsearch__.refresh_index! - - response = MongoidArticle.search 'title:write' - - assert_equal 1, response.results.size - assert_equal 1, response.records.size - end - - should "return results for a DSL search" do - response = MongoidArticle.search query: { match: { title: { query: 'test' } } } - - assert_equal 2, response.results.size - assert_equal 2, response.records.size - end - - should "return a paged collection" do - response = MongoidArticle.search query: { match: { title: { query: 'test' } } }, - size: 2, - from: 1 - - assert_equal 1, response.results.size - assert_equal 1, response.records.size - - assert_equal 'Testing Coding', response.results.first.title - assert_equal 'Testing Coding', response.records.first.title - end - - - context "importing" do - setup do - MongoidArticle.delete_all - 97.times { |i| MongoidArticle.create! title: "Test #{i}" } - MongoidArticle.__elasticsearch__.create_index! force: true - MongoidArticle.__elasticsearch__.client.cluster.health wait_for_status: 'yellow' - end - - should "import all the documents" do - assert_equal 97, MongoidArticle.count - - MongoidArticle.__elasticsearch__.refresh_index! - assert_equal 0, MongoidArticle.search('*').results.total - - batches = 0 - errors = MongoidArticle.import(batch_size: 10) do |response| - batches += 1 - end - - assert_equal 0, errors - assert_equal 10, batches - - MongoidArticle.__elasticsearch__.refresh_index! - assert_equal 97, MongoidArticle.search('*').results.total - - response = MongoidArticle.search('test') - assert response.results.any?, "Search has not returned results: #{response.to_a}" - end - end - - context "importing when the model has a default scope" do - class ::MongoidArticleWithDefaultScope - include Mongoid::Document - include Elasticsearch::Model - - default_scope -> { where(status: 'active') } - - field :id, type: String - field :title, type: String - field :status, type: String, default: 'active' - - attr_accessible :title if respond_to? :attr_accessible - attr_accessible :status if respond_to? :attr_accessible - - settings index: { number_of_shards: 1, number_of_replicas: 0 } do - mapping do - indexes :title, type: 'text', analyzer: 'snowball' - indexes :status, type: 'text' - indexes :created_at, type: 'date' - end - end - - def as_indexed_json(options={}) - as_json(except: [:id, :_id]) - end - end - - setup do - Elasticsearch::Model::Adapter.register \ - Elasticsearch::Model::Adapter::Mongoid, - lambda { |klass| !!defined?(::Mongoid::Document) && klass.respond_to?(:ancestors) && klass.ancestors.include?(::Mongoid::Document) } - - MongoidArticleWithDefaultScope.__elasticsearch__.create_index! force: true - - MongoidArticleWithDefaultScope.delete_all - - MongoidArticleWithDefaultScope.create! title: 'Test' - MongoidArticleWithDefaultScope.create! title: 'Testing Coding' - MongoidArticleWithDefaultScope.create! title: 'Coding' - MongoidArticleWithDefaultScope.create! title: 'Test legacy code', status: 'removed' - - MongoidArticleWithDefaultScope.__elasticsearch__.refresh_index! - MongoidArticleWithDefaultScope.__elasticsearch__.client.cluster.health wait_for_status: 'yellow' - end - - should "import only documents from the default scope" do - assert_equal 3, MongoidArticleWithDefaultScope.count - - assert_equal 0, MongoidArticleWithDefaultScope.import - - MongoidArticleWithDefaultScope.__elasticsearch__.refresh_index! - assert_equal 3, MongoidArticleWithDefaultScope.search('*').results.total - end - - should "import only documents from a specific query combined with the default scope" do - assert_equal 3, MongoidArticleWithDefaultScope.count - - assert_equal 0, MongoidArticleWithDefaultScope.import(query: -> { where(title: /^Test/) }) - - MongoidArticleWithDefaultScope.__elasticsearch__.refresh_index! - assert_equal 2, MongoidArticleWithDefaultScope.search('*').results.total - end - end - end - - end - end - end - -end diff --git a/elasticsearch-model/test/integration/multiple_models_test.rb b/elasticsearch-model/test/integration/multiple_models_test.rb deleted file mode 100644 index 02022b60f..000000000 --- a/elasticsearch-model/test/integration/multiple_models_test.rb +++ /dev/null @@ -1,176 +0,0 @@ -require 'test_helper' -require 'active_record' - -# Needed for ActiveRecord 3.x ? -ActiveRecord::Base.establish_connection( :adapter => 'sqlite3', :database => ":memory:" ) unless ActiveRecord::Base.connected? - -::ActiveRecord::Base.raise_in_transactional_callbacks = true if ::ActiveRecord::Base.respond_to?(:raise_in_transactional_callbacks) && ::ActiveRecord::VERSION::MAJOR.to_s < '5' - -MongoDB.setup! - -module Elasticsearch - module Model - class MultipleModelsIntegration < Elasticsearch::Test::IntegrationTestCase - module ::NameSearch - extend ActiveSupport::Concern - - included do - include Elasticsearch::Model - include Elasticsearch::Model::Callbacks - - settings index: {number_of_shards: 1, number_of_replicas: 0} do - mapping do - indexes :name, type: 'text', analyzer: 'snowball' - indexes :created_at, type: 'date' - end - end - end - end - - class ::Episode < ActiveRecord::Base - include NameSearch - end - - class ::Series < ActiveRecord::Base - include NameSearch - end - - context "Multiple models" do - setup do - ActiveRecord::Schema.define(:version => 1) do - create_table :episodes do |t| - t.string :name - t.datetime :created_at, :default => 'NOW()' - end - - create_table :series do |t| - t.string :name - t.datetime :created_at, :default => 'NOW()' - end - end - - [::Episode, ::Series].each do |model| - model.delete_all - model.__elasticsearch__.create_index! force: true - model.create name: "The #{model.name}" - model.create name: "A great #{model.name}" - model.create name: "The greatest #{model.name}" - model.__elasticsearch__.refresh_index! - end - end - - should "find matching documents across multiple models" do - response = Elasticsearch::Model.search(%q<"The greatest Episode"^2 OR "The greatest Series">, [Series, Episode]) - - assert response.any?, "Response should not be empty: #{response.to_a.inspect}" - - assert_equal 2, response.results.size - assert_equal 2, response.records.size - - assert_instance_of Elasticsearch::Model::Response::Result, response.results.first - assert_instance_of Episode, response.records.first - assert_instance_of Series, response.records.last - - assert_equal 'The greatest Episode', response.results[0].name - assert_equal 'The greatest Episode', response.records[0].name - - assert_equal 'The greatest Series', response.results[1].name - assert_equal 'The greatest Series', response.records[1].name - end - - should "provide access to results" do - response = Elasticsearch::Model.search(%q<"A great Episode"^2 OR "A great Series">, [Series, Episode]) - - assert_equal 'A great Episode', response.results[0].name - assert_equal true, response.results[0].name? - assert_equal false, response.results[0].boo? - - assert_equal 'A great Series', response.results[1].name - assert_equal true, response.results[1].name? - assert_equal false, response.results[1].boo? - end - - should "only retrieve records for existing results" do - ::Series.find_by_name("The greatest Series").delete - ::Series.__elasticsearch__.refresh_index! - response = Elasticsearch::Model.search(%q<"The greatest Episode"^2 OR "The greatest Series">, [Series, Episode]) - - assert response.any?, "Response should not be empty: #{response.to_a.inspect}" - - assert_equal 2, response.results.size - assert_equal 1, response.records.size - - assert_instance_of Elasticsearch::Model::Response::Result, response.results.first - assert_instance_of Episode, response.records.first - - assert_equal 'The greatest Episode', response.results[0].name - assert_equal 'The greatest Episode', response.records[0].name - end - - should "paginate the results" do - response = Elasticsearch::Model.search('series OR episode', [Series, Episode]) - - assert_equal 3, response.page(1).per(3).results.size - assert_equal 3, response.page(2).per(3).results.size - assert_equal 0, response.page(3).per(3).results.size - end - - if MongoDB.available? - MongoDB.connect_to 'mongoid_collections' - - context "Across mongoid models" do - setup do - class ::Image - include Mongoid::Document - include Elasticsearch::Model - include Elasticsearch::Model::Callbacks - - field :name, type: String - attr_accessible :name if respond_to? :attr_accessible - - settings index: {number_of_shards: 1, number_of_replicas: 0} do - mapping do - indexes :name, type: 'text', analyzer: 'snowball' - indexes :created_at, type: 'date' - end - end - - def as_indexed_json(options={}) - as_json(except: [:_id]) - end - end - - Image.delete_all - Image.__elasticsearch__.create_index! force: true - Image.create! name: "The Image" - Image.create! name: "A great Image" - Image.create! name: "The greatest Image" - Image.__elasticsearch__.refresh_index! - Image.__elasticsearch__.client.cluster.health wait_for_status: 'yellow' - end - - should "find matching documents across multiple models" do - response = Elasticsearch::Model.search(%q<"greatest Episode" OR "greatest Image"^2>, [Episode, Image]) - - assert response.any?, "Response should not be empty: #{response.to_a.inspect}" - - assert_equal 2, response.results.size - assert_equal 2, response.records.size - - assert_instance_of Elasticsearch::Model::Response::Result, response.results.first - assert_instance_of Image, response.records.first - assert_instance_of Episode, response.records.last - - assert_equal 'The greatest Image', response.results[0].name - assert_equal 'The greatest Image', response.records[0].name - - assert_equal 'The greatest Episode', response.results[1].name - assert_equal 'The greatest Episode', response.records[1].name - end - end - end - - end - end - end -end diff --git a/elasticsearch-model/test/test_helper.rb b/elasticsearch-model/test/test_helper.rb deleted file mode 100644 index 5a727e2b2..000000000 --- a/elasticsearch-model/test/test_helper.rb +++ /dev/null @@ -1,92 +0,0 @@ -RUBY_1_8 = defined?(RUBY_VERSION) && RUBY_VERSION < '1.9' - -exit(0) if RUBY_1_8 - -require 'simplecov' and SimpleCov.start { add_filter "/test|test_/" } if ENV["COVERAGE"] - -# Register `at_exit` handler for integration tests shutdown. -# MUST be called before requiring `test/unit`. -at_exit { Elasticsearch::Test::IntegrationTestCase.__run_at_exit_hooks } - -puts '-'*80 - -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 'active_model' - -require 'kaminari' - -require 'elasticsearch/model' - -require 'elasticsearch/extensions/test/cluster' -require 'elasticsearch/extensions/test/startup_shutdown' - -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 - ActiveRecord::Base.establish_connection( :adapter => 'sqlite3', :database => ":memory:" ) - logger = ::Logger.new(STDERR) - logger.formatter = lambda { |s, d, p, m| "\e[2;36m#{m}\e[0m\n" } - ActiveRecord::Base.logger = logger unless ENV['QUIET'] - - ActiveRecord::LogSubscriber.colorize_logging = false - ActiveRecord::Migration.verbose = false - - tracer = ::Logger.new(STDERR) - tracer.formatter = lambda { |s, d, p, m| "#{m.gsub(/^.*$/) { |n| ' ' + n }.ansi(:faint)}\n" } - - Elasticsearch::Model.client = Elasticsearch::Client.new host: "localhost:#{(ENV['TEST_CLUSTER_PORT'] || 9250)}", - tracer: (ENV['QUIET'] ? nil : tracer) - end - end - end -end - -class MongoDB - def self.setup! - begin - require 'mongoid' - Mongo::Client.new(["localhost:27017"]) - ENV['MONGODB_AVAILABLE'] = 'yes' - rescue LoadError, Mongo::Error => e - $stderr.puts "MongoDB not installed or running: #{e}" - end - end - - def self.available? - !!ENV['MONGODB_AVAILABLE'] - end - - def self.connect_to(source) - $stderr.puts "Mongoid #{Mongoid::VERSION}", '-'*80 - - logger = ::Logger.new($stderr) - logger.formatter = lambda { |s, d, p, m| " #{m.ansi(:faint, :cyan)}\n" } - logger.level = ::Logger::DEBUG - - Mongoid.logger = logger unless ENV['QUIET'] - Mongo::Logger.logger = logger unless ENV['QUIET'] - - Mongoid.connect_to source - end -end