New upstream version 12.3.8

This commit is contained in:
Pirate Praveen 2019-12-05 17:51:33 +05:30
parent 1e0aa28929
commit 860976d237
35 changed files with 3183 additions and 0 deletions

17
elasticsearch-rails/.gitignore vendored Normal file
View file

@ -0,0 +1,17 @@
*.gem
*.rbc
.bundle
.config
.yardoc
Gemfile.lock
InstalledFiles
_yardoc
coverage
doc/
lib/bundler/man
pkg
rdoc
spec/reports
test/tmp
test/version_tmp
tmp

View file

@ -0,0 +1,44 @@
## 0.1.9
* Added checks for proper launch order and other updates to the example application templates
* Updated the example application to work with Elasticsearch 2.x
* Used the `suggest` method instead of `response['suggest']` in the application template
## 0.1.8
* Added an example application template that loads settings from a file
* Added missing require in the seeds.rb file for the expert template
* Fixed double include of the aliased method (execute_without_instrumentation)
* Fixed the error when getting the search_controller_test.rb asset in `03-expert.rb` template
* Updated URLs for getting raw assets from Github in the `03-expert.rb` template
## 0.1.7
* Updated dependencies for the gem and example applications
* Fixed various small errors in the `01-basic.rb` template
* Fixed error when inserting the Kaminari gem into Gemfile in the 02-pretty.rb template
* Fixed incorrect regex for adding Rails instrumentation into the application.rb in the `02-pretty.rb` template
* Fixed other small errors in the `02-pretty.rb` template
* Improved and added tests for the generated application from the `02-pretty.rb` template
* Added the `04-dsl.rb` template which uses the `elasticsearch-dsl` gem to build the search definition
## 0.1.6
* Fixed errors in templates for the Rails example applications
* Fixed errors in the importing Rake task
* Refactored and updated the instrumentation support to allow integration with `Persistence::Model`
## 0.1.5
* Fixed an exception when no suggestions were returned in the `03-expert` example application template
## 0.1.2
* Allow passing an ActiveRecord scope to the importing Rake task
## 0.1.1
* Improved the Rake tasks
* Improved the example application templates
## 0.1.0 (Initial Version)

View file

@ -0,0 +1,9 @@
source 'https://rubygems.org'
# Specify your gem's dependencies in elasticsearch-rails.gemspec
gemspec
# TODO: Figure out how to specify dependency on local elasticsearch-model without endless "Resolving dependencies"
# if File.exists? File.expand_path("../../elasticsearch-model", __FILE__)
# gem 'elasticsearch-model', :path => File.expand_path("../../elasticsearch-model", __FILE__), :require => true
# end

View file

@ -0,0 +1,13 @@
Copyright (c) 2014 Elasticsearch
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.

View file

@ -0,0 +1,132 @@
# Elasticsearch::Rails
The `elasticsearch-rails` library is a companion for the
the [`elasticsearch-model`](https://github.com/elasticsearch/elasticsearch-rails/tree/master/elasticsearch-model)
library, providing features suitable for Ruby on Rails applications.
The library is compatible with Ruby 1.9.3 and higher.
## Installation
Install the package from [Rubygems](https://rubygems.org):
gem install elasticsearch-rails
To use an unreleased version, either add it to your `Gemfile` for [Bundler](http://bundler.io):
gem 'elasticsearch-rails', git: 'git://github.com/elasticsearch/elasticsearch-rails.git'
or install it from a source code checkout:
git clone https://github.com/elasticsearch/elasticsearch-rails.git
cd elasticsearch-rails/elasticsearch-rails
bundle install
rake install
## Features
### Rake Tasks
To facilitate importing data from your models into Elasticsearch, require the task definition in your application,
eg. in the `lib/tasks/elasticsearch.rake` file:
```ruby
require 'elasticsearch/rails/tasks/import'
```
To import the records from your `Article` model, run:
```bash
$ bundle exec rake environment elasticsearch:import:model CLASS='Article'
```
To limit the imported records to a certain
ActiveRecord [scope](http://guides.rubyonrails.org/active_record_querying.html#scopes),
pass it to the task:
```bash
$ bundle exec rake environment elasticsearch:import:model CLASS='Article' SCOPE='published'
```
Run this command to display usage instructions:
```bash
$ bundle exec rake -D elasticsearch
```
### ActiveSupport Instrumentation
To display information about the search request (duration, search definition) during development,
and to include the information in the Rails log file, require the component in your `application.rb` file:
```ruby
require 'elasticsearch/rails/instrumentation'
```
You should see an output like this in your application log in development environment:
Article Search (321.3ms) { index: "articles", type: "article", body: { query: ... } }
Also, the total duration of the request to Elasticsearch is displayed in the Rails request breakdown:
Completed 200 OK in 615ms (Views: 230.9ms | ActiveRecord: 0.0ms | Elasticsearch: 321.3ms)
There's a special component for the [Lograge](https://github.com/roidrage/lograge) logger.
Require the component in your `application.rb` file (and set `config.lograge.enabled`):
```ruby
require 'elasticsearch/rails/lograge'
```
You should see the duration of the request to Elasticsearch as part of each log event:
method=GET path=/search ... status=200 duration=380.89 view=99.64 db=0.00 es=279.37
### Rails Application Templates
You can generate a fully working example Ruby on Rails application, with an `Article` model and a search form,
to play with (it even downloads _Elasticsearch_ itself, generates the application skeleton and leaves you with
a _Git_ repository to explore the steps and the code) with the
[`01-basic.rb`](https://github.com/elasticsearch/elasticsearch-rails/blob/master/elasticsearch-rails/lib/rails/templates/01-basic.rb) template:
```bash
rails new searchapp --skip --skip-bundle --template https://raw.github.com/elasticsearch/elasticsearch-rails/master/elasticsearch-rails/lib/rails/templates/01-basic.rb
```
Run the same command again, in the same folder, with the
[`02-pretty`](https://github.com/elasticsearch/elasticsearch-rails/blob/master/elasticsearch-rails/lib/rails/templates/02-pretty.rb)
template to add features such as a custom `Article.search` method, result highlighting and
[_Bootstrap_](http://getbootstrap.com) integration:
```bash
rails new searchapp --skip --skip-bundle --template https://raw.github.com/elasticsearch/elasticsearch-rails/master/elasticsearch-rails/lib/rails/templates/02-pretty.rb
```
Run the same command with the [`03-expert.rb`](https://github.com/elasticsearch/elasticsearch-rails/blob/master/elasticsearch-rails/lib/rails/templates/03-expert.rb)
template to refactor the application into a more complex use case,
with couple of hundreds of The New York Times articles as the example content.
The template will extract the Elasticsearch integration into a `Searchable` "concern" module,
define complex mapping, custom serialization, implement faceted navigation and suggestions as a part of
a complex query, and add a _Sidekiq_-based worker for updating the index in the background.
```bash
rails new searchapp --skip --skip-bundle --template https://raw.github.com/elasticsearch/elasticsearch-rails/master/elasticsearch-rails/lib/rails/templates/03-expert.rb
```
## License
This software is licensed under the Apache 2 license, quoted below.
Copyright (c) 2014 Elasticsearch <http://www.elasticsearch.org>
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.

View file

@ -0,0 +1,53 @@
require "bundler/gem_tasks"
desc "Run unit tests"
task :default => 'test:unit'
task :test => 'test:unit'
# ----- Test tasks ------------------------------------------------------------
require 'rake/testtask'
namespace :test do
task :ci_reporter do
ENV['CI_REPORTS'] ||= 'tmp/reports'
require 'ci/reporter/rake/minitest'
Rake::Task['ci:setup:minitest'].invoke
end
Rake::TestTask.new(:unit) do |test|
Rake::Task['test:ci_reporter'].invoke if ENV['CI']
test.libs << 'lib' << 'test'
test.test_files = FileList["test/unit/**/*_test.rb"]
# test.verbose = true
# test.warning = true
end
Rake::TestTask.new(:integration) do |test|
Rake::Task['test:ci_reporter'].invoke if ENV['CI']
test.libs << 'lib' << 'test'
test.test_files = FileList["test/integration/**/*_test.rb"]
end
Rake::TestTask.new(:all) do |test|
Rake::Task['test:ci_reporter'].invoke if ENV['CI']
test.libs << 'lib' << 'test'
test.test_files = FileList["test/unit/**/*_test.rb", "test/integration/**/*_test.rb"]
end
end
# ----- Documentation tasks ---------------------------------------------------
require 'yard'
YARD::Rake::YardocTask.new(:doc) do |t|
t.options = %w| --embed-mixins --markup=markdown |
end
# ----- Code analysis tasks ---------------------------------------------------
if defined?(RUBY_VERSION) && RUBY_VERSION > '1.9'
require 'cane/rake_task'
Cane::RakeTask.new(:quality) do |cane|
cane.abc_max = 15
cane.no_style = true
end
end

View file

@ -0,0 +1,52 @@
# coding: utf-8
lib = File.expand_path('../lib', __FILE__)
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
require 'elasticsearch/rails/version'
Gem::Specification.new do |s|
s.name = "elasticsearch-rails"
s.version = Elasticsearch::Rails::VERSION
s.authors = ["Karel Minarik"]
s.email = ["karel.minarik@elasticsearch.org"]
s.description = "Ruby on Rails integrations for Elasticsearch."
s.summary = "Ruby on Rails integrations for Elasticsearch."
s.homepage = "https://github.com/elasticsearch/elasticsearch-rails/"
s.license = "Apache 2"
s.files = `git ls-files`.split($/)
s.executables = s.files.grep(%r{^bin/}) { |f| File.basename(f) }
s.test_files = s.files.grep(%r{^(test|spec|features)/})
s.require_paths = ["lib"]
s.extra_rdoc_files = [ "README.md", "LICENSE.txt" ]
s.rdoc_options = [ "--charset=UTF-8" ]
s.required_ruby_version = ">= 1.9.3"
s.add_development_dependency "bundler", "~> 1.3"
s.add_development_dependency "rake", "< 11.0"
s.add_development_dependency "elasticsearch-extensions"
s.add_development_dependency "elasticsearch-model"
s.add_development_dependency "oj"
s.add_development_dependency "rails", ">= 3.1"
s.add_development_dependency "lograge"
s.add_development_dependency "minitest", "~> 4.2"
s.add_development_dependency "test-unit" if defined?(RUBY_VERSION) && RUBY_VERSION > '2.2'
s.add_development_dependency "shoulda-context"
s.add_development_dependency "mocha"
s.add_development_dependency "turn"
s.add_development_dependency "yard"
s.add_development_dependency "ruby-prof"
s.add_development_dependency "pry"
s.add_development_dependency "ci_reporter", "~> 1.9"
if defined?(RUBY_VERSION) && RUBY_VERSION > '1.9'
s.add_development_dependency "simplecov"
s.add_development_dependency "cane"
s.add_development_dependency "require-prof"
end
end

View file

@ -0,0 +1,7 @@
require "elasticsearch/rails/version"
module Elasticsearch
module Rails
# Your code goes here...
end
end

View file

@ -0,0 +1,36 @@
require 'elasticsearch/rails/instrumentation/railtie'
require 'elasticsearch/rails/instrumentation/publishers'
module Elasticsearch
module Rails
# This module adds support for displaying statistics about search duration in the Rails application log
# by integrating with the `ActiveSupport::Notifications` framework and `ActionController` logger.
#
# == Usage
#
# Require the component in your `application.rb` file:
#
# require 'elasticsearch/rails/instrumentation'
#
# You should see an output like this in your application log in development environment:
#
# Article Search (321.3ms) { index: "articles", type: "article", body: { query: ... } }
#
# Also, the total duration of the request to Elasticsearch is displayed in the Rails request breakdown:
#
# Completed 200 OK in 615ms (Views: 230.9ms | ActiveRecord: 0.0ms | Elasticsearch: 321.3ms)
#
# @note The displayed duration includes the HTTP transfer -- the time it took Elasticsearch
# to process your request is available in the `response.took` property.
#
# @see Elasticsearch::Rails::Instrumentation::Publishers
# @see Elasticsearch::Rails::Instrumentation::Railtie
#
# @see http://api.rubyonrails.org/classes/ActiveSupport/Notifications.html
#
#
module Instrumentation
end
end
end

View file

@ -0,0 +1,41 @@
require 'active_support/core_ext/module/attr_internal'
module Elasticsearch
module Rails
module Instrumentation
# Hooks into ActionController to display Elasticsearch runtime
#
# @see https://github.com/rails/rails/blob/master/activerecord/lib/active_record/railties/controller_runtime.rb
#
module ControllerRuntime
extend ActiveSupport::Concern
protected
attr_internal :elasticsearch_runtime
def cleanup_view_runtime
elasticsearch_rt_before_render = Elasticsearch::Rails::Instrumentation::LogSubscriber.reset_runtime
runtime = super
elasticsearch_rt_after_render = Elasticsearch::Rails::Instrumentation::LogSubscriber.reset_runtime
self.elasticsearch_runtime = elasticsearch_rt_before_render + elasticsearch_rt_after_render
runtime - elasticsearch_rt_after_render
end
def append_info_to_payload(payload)
super
payload[:elasticsearch_runtime] = (elasticsearch_runtime || 0) + Elasticsearch::Rails::Instrumentation::LogSubscriber.reset_runtime
end
module ClassMethods
def log_process_action(payload)
messages, elasticsearch_runtime = super, payload[:elasticsearch_runtime]
messages << ("Elasticsearch: %.1fms" % elasticsearch_runtime.to_f) if elasticsearch_runtime
messages
end
end
end
end
end
end

View file

@ -0,0 +1,41 @@
module Elasticsearch
module Rails
module Instrumentation
# A log subscriber to attach to Elasticsearch related events
#
# @see https://github.com/rails/rails/blob/master/activerecord/lib/active_record/log_subscriber.rb
#
class LogSubscriber < ActiveSupport::LogSubscriber
def self.runtime=(value)
Thread.current["elasticsearch_runtime"] = value
end
def self.runtime
Thread.current["elasticsearch_runtime"] ||= 0
end
def self.reset_runtime
rt, self.runtime = runtime, 0
rt
end
# Intercept `search.elasticsearch` events, and display them in the Rails log
#
def search(event)
self.class.runtime += event.duration
return unless logger.debug?
payload = event.payload
name = "#{payload[:klass]} #{payload[:name]} (#{event.duration.round(1)}ms)"
search = payload[:search].inspect.gsub(/:(\w+)=>/, '\1: ')
debug %Q| #{color(name, GREEN, true)} #{colorize_logging ? "\e[2m#{search}\e[0m" : search}|
end
end
end
end
end
Elasticsearch::Rails::Instrumentation::LogSubscriber.attach_to :elasticsearch

View file

@ -0,0 +1,36 @@
module Elasticsearch
module Rails
module Instrumentation
module Publishers
# Wraps the `SearchRequest` methods to perform the instrumentation
#
# @see SearchRequest#execute_with_instrumentation!
# @see http://api.rubyonrails.org/classes/ActiveSupport/Notifications.html
#
module SearchRequest
def self.included(base)
base.class_eval do
unless method_defined?(:execute_without_instrumentation!)
alias_method :execute_without_instrumentation!, :execute!
alias_method :execute!, :execute_with_instrumentation!
end
end
end
# Wrap `Search#execute!` and perform instrumentation
#
def execute_with_instrumentation!
ActiveSupport::Notifications.instrument "search.elasticsearch",
name: 'Search',
klass: (self.klass.is_a?(Elasticsearch::Model::Proxy::ClassMethodsProxy) ? self.klass.target.to_s : self.klass.to_s),
search: self.definition do
execute_without_instrumentation!
end
end
end
end
end
end
end

View file

@ -0,0 +1,31 @@
module Elasticsearch
module Rails
module Instrumentation
# Rails initializer class to require Elasticsearch::Rails::Instrumentation files,
# set up Elasticsearch::Model and hook into ActionController to display Elasticsearch-related duration
#
# @see http://edgeguides.rubyonrails.org/active_support_instrumentation.html
#
class Railtie < ::Rails::Railtie
initializer "elasticsearch.instrumentation" do |app|
require 'elasticsearch/rails/instrumentation/log_subscriber'
require 'elasticsearch/rails/instrumentation/controller_runtime'
Elasticsearch::Model::Searching::SearchRequest.class_eval do
include Elasticsearch::Rails::Instrumentation::Publishers::SearchRequest
end if defined?(Elasticsearch::Model::Searching::SearchRequest)
Elasticsearch::Persistence::Model::Find::SearchRequest.class_eval do
include Elasticsearch::Rails::Instrumentation::Publishers::SearchRequest
end if defined?(Elasticsearch::Persistence::Model::Find::SearchRequest)
ActiveSupport.on_load(:action_controller) do
include Elasticsearch::Rails::Instrumentation::ControllerRuntime
end
end
end
end
end
end

View file

@ -0,0 +1,44 @@
module Elasticsearch
module Rails
module Lograge
# Rails initializer class to require Elasticsearch::Rails::Instrumentation files,
# set up Elasticsearch::Model and add Lograge configuration to display Elasticsearch-related duration
#
# Require the component in your `application.rb` file and enable Lograge:
#
# require 'elasticsearch/rails/lograge'
#
# You should see the full duration of the request to Elasticsearch as part of each log event:
#
# method=GET path=/search ... status=200 duration=380.89 view=99.64 db=0.00 es=279.37
#
# @see https://github.com/roidrage/lograge
#
class Railtie < ::Rails::Railtie
initializer "elasticsearch.lograge" do |app|
require 'elasticsearch/rails/instrumentation/publishers'
require 'elasticsearch/rails/instrumentation/log_subscriber'
require 'elasticsearch/rails/instrumentation/controller_runtime'
Elasticsearch::Model::Searching::SearchRequest.class_eval do
include Elasticsearch::Rails::Instrumentation::Publishers::SearchRequest
end if defined?(Elasticsearch::Model::Searching::SearchRequest)
Elasticsearch::Persistence::Model::Find::SearchRequest.class_eval do
include Elasticsearch::Rails::Instrumentation::Publishers::SearchRequest
end if defined?(Elasticsearch::Persistence::Model::Find::SearchRequest)
ActiveSupport.on_load(:action_controller) do
include Elasticsearch::Rails::Instrumentation::ControllerRuntime
end
config.lograge.custom_options = lambda do |event|
{ es: event.payload[:elasticsearch_runtime].to_f.round(2) }
end
end
end
end
end
end

View file

@ -0,0 +1,112 @@
# A collection of Rake tasks to facilitate importing data from yout models into Elasticsearch.
#
# Add this e.g. into the `lib/tasks/elasticsearch.rake` file in your Rails application:
#
# require 'elasticsearch/rails/tasks/import'
#
# To import the records from your `Article` model, run:
#
# $ bundle exec rake environment elasticsearch:import:model CLASS='MyModel'
#
# Run this command to display usage instructions:
#
# $ bundle exec rake -D elasticsearch
#
STDOUT.sync = true
STDERR.sync = true
begin; require 'ansi/progressbar'; rescue LoadError; end
namespace :elasticsearch do
task :import => 'import:model'
namespace :import do
import_model_desc = <<-DESC.gsub(/ /, '')
Import data from your model (pass name as CLASS environment variable).
$ rake environment elasticsearch:import:model CLASS='MyModel'
Force rebuilding the index (delete and create):
$ rake environment elasticsearch:import:model CLASS='Article' FORCE=y
Customize the batch size:
$ rake environment elasticsearch:import:model CLASS='Article' BATCH=100
Set target index name:
$ rake environment elasticsearch:import:model CLASS='Article' INDEX='articles-new'
Pass an ActiveRecord scope to limit the imported records:
$ rake environment elasticsearch:import:model CLASS='Article' SCOPE='published'
DESC
desc import_model_desc
task :model do
if ENV['CLASS'].to_s == ''
puts '='*90, 'USAGE', '='*90, import_model_desc, ""
exit(1)
end
klass = eval(ENV['CLASS'].to_s)
total = klass.count rescue nil
pbar = ANSI::Progressbar.new(klass.to_s, total) rescue nil
pbar.__send__ :show if pbar
unless ENV['DEBUG']
begin
klass.__elasticsearch__.client.transport.logger.level = Logger::WARN
rescue NoMethodError; end
begin
klass.__elasticsearch__.client.transport.tracer.level = Logger::WARN
rescue NoMethodError; end
end
total_errors = klass.__elasticsearch__.import force: ENV.fetch('FORCE', false),
batch_size: ENV.fetch('BATCH', 1000).to_i,
index: ENV.fetch('INDEX', nil),
type: ENV.fetch('TYPE', nil),
scope: ENV.fetch('SCOPE', nil) do |response|
pbar.inc response['items'].size if pbar
STDERR.flush
STDOUT.flush
end
pbar.finish if pbar
puts "[IMPORT] #{total_errors} errors occurred" unless total_errors.zero?
puts '[IMPORT] Done'
end
desc <<-DESC.gsub(/ /, '')
Import all indices from `app/models` (or use DIR environment variable).
$ rake environment elasticsearch:import:all DIR=app/models
DESC
task :all do
dir = ENV['DIR'].to_s != '' ? ENV['DIR'] : Rails.root.join("app/models")
puts "[IMPORT] Loading models from: #{dir}"
Dir.glob(File.join("#{dir}/**/*.rb")).each do |path|
model_filename = path[/#{Regexp.escape(dir.to_s)}\/([^\.]+).rb/, 1]
next if model_filename.match(/^concerns\//i) # Skip concerns/ folder
begin
klass = model_filename.camelize.constantize
rescue NameError
require(path) ? retry : raise(RuntimeError, "Cannot load class '#{klass}'")
end
# Skip if the class doesn't have Elasticsearch integration
next unless klass.respond_to?(:__elasticsearch__)
puts "[IMPORT] Processing model: #{klass}..."
ENV['CLASS'] = klass.to_s
Rake::Task["elasticsearch:import:model"].invoke
Rake::Task["elasticsearch:import:model"].reenable
puts
end
end
end
end

View file

@ -0,0 +1,5 @@
module Elasticsearch
module Rails
VERSION = "0.1.9"
end
end

View file

@ -0,0 +1,335 @@
# =====================================================================================================
# Template for generating a no-frills Rails application with support for Elasticsearch full-text search
# =====================================================================================================
#
# This file creates a basic, fully working Rails application with support for Elasticsearch full-text
# search via the `elasticsearch-rails` gem; https://github.com/elasticsearch/elasticsearch-rails.
#
# Requirements:
# -------------
#
# * Git
# * Ruby >= 1.9.3
# * Rails >= 4
# * Java >= 7 (for Elasticsearch)
#
# Usage:
# ------
#
# $ rails new searchapp --skip --skip-bundle --template https://raw.github.com/elasticsearch/elasticsearch-rails/master/elasticsearch-rails/lib/rails/templates/01-basic.rb
#
# =====================================================================================================
require 'uri'
require 'net/http'
at_exit do
pid = File.read("#{destination_root}/tmp/pids/elasticsearch.pid") rescue nil
if pid
say_status "Stop", "Elasticsearch", :yellow
run "kill #{pid}"
end
end
run "touch tmp/.gitignore"
append_to_file ".gitignore", "vendor/elasticsearch-1.0.1/\n"
git :init
git add: "."
git commit: "-m 'Initial commit: Clean application'"
# ----- Download Elasticsearch --------------------------------------------------------------------
unless (Net::HTTP.get(URI.parse('http://localhost:9200')) rescue false)
COMMAND = <<-COMMAND.gsub(/^ /, '')
curl -# -O "http://download.elasticsearch.org/elasticsearch/elasticsearch/elasticsearch-1.0.1.tar.gz"
tar -zxf elasticsearch-1.0.1.tar.gz
rm -f elasticsearch-1.0.1.tar.gz
./elasticsearch-1.0.1/bin/elasticsearch -d -p #{destination_root}/tmp/pids/elasticsearch.pid
COMMAND
puts "\n"
say_status "ERROR", "Elasticsearch not running!\n", :red
puts '-'*80
say_status '', "It appears that Elasticsearch is not running on this machine."
say_status '', "Is it installed? Do you want me to install it for you with this command?\n\n"
COMMAND.each_line { |l| say_status '', "$ #{l}" }
puts
say_status '', "(To uninstall, just remove the generated application directory.)"
puts '-'*80, ''
if yes?("Install Elasticsearch?", :bold)
puts
say_status "Install", "Elasticsearch", :yellow
commands = COMMAND.split("\n")
exec = commands.pop
inside("vendor") do
commands.each { |command| run command }
run "(#{exec})" # Launch Elasticsearch in subshell
end
end
end unless ENV['RAILS_NO_ES_INSTALL']
# ----- Add README --------------------------------------------------------------------------------
puts
say_status "README", "Adding Readme...\n", :yellow
puts '-'*80, ''; sleep 0.25
remove_file 'README.rdoc'
create_file 'README.rdoc', <<-README
= Ruby on Rails and Elasticsearch: Example application
This application is an example of integrating the {Elasticsearch}[http://www.elasticsearch.org]
search engine with the {Ruby On Rails}[http://rubyonrails.org] web framework.
It has been generated by application templates available at
https://github.com/elasticsearch/elasticsearch-rails/tree/master/elasticsearch-rails/lib/rails/templates.
== [1] Basic
The `basic` version provides a simple integration for a simple Rails model, `Article`, showing how
to include the search engine support in your model, automatically index changes to records,
and use a form to perform simple search require 'requests.'
README
git add: "."
git commit: "-m '[01] Added README for the application'"
# ----- Use Thin ----------------------------------------------------------------------------------
begin
require 'thin'
puts
say_status "Rubygems", "Adding Thin into Gemfile...\n", :yellow
puts '-'*80, '';
gem 'thin'
rescue LoadError
end
# ----- Auxiliary gems ----------------------------------------------------------------------------
gem 'mocha', group: 'test', require: 'mocha/api'
# ----- Remove CoffeeScript, Sass and "all that jazz" ---------------------------------------------
comment_lines 'Gemfile', /gem 'coffee/
comment_lines 'Gemfile', /gem 'sass/
comment_lines 'Gemfile', /gem 'uglifier/
uncomment_lines 'Gemfile', /gem 'therubyracer/
# ----- Add gems into Gemfile ---------------------------------------------------------------------
puts
say_status "Rubygems", "Adding Elasticsearch libraries into Gemfile...\n", :yellow
puts '-'*80, ''; sleep 0.75
gem 'elasticsearch', git: 'git://github.com/elasticsearch/elasticsearch-ruby.git'
gem 'elasticsearch-model', git: 'git://github.com/elasticsearch/elasticsearch-rails.git'
gem 'elasticsearch-rails', git: 'git://github.com/elasticsearch/elasticsearch-rails.git'
git add: "Gemfile*"
git commit: "-m 'Added libraries into Gemfile'"
# ----- Disable asset logging in development ------------------------------------------------------
puts
say_status "Application", "Disabling asset logging in development...\n", :yellow
puts '-'*80, ''; sleep 0.25
environment 'config.assets.logger = false', env: 'development'
gem 'quiet_assets', group: "development"
git add: "Gemfile*"
git add: "config/"
git commit: "-m 'Disabled asset logging in development'"
# ----- Install gems ------------------------------------------------------------------------------
puts
say_status "Rubygems", "Installing Rubygems...", :yellow
puts '-'*80, ''
run "bundle install"
# ----- Generate Article resource -----------------------------------------------------------------
puts
say_status "Model", "Generating the Article resource...", :yellow
puts '-'*80, ''; sleep 0.75
generate :scaffold, "Article title:string content:text published_on:date"
route "root to: 'articles#index'"
rake "db:migrate"
git add: "."
git commit: "-m 'Added the generated Article resource'"
# ----- Add Elasticsearch integration into the model ----------------------------------------------
puts
say_status "Model", "Adding search support into the Article model...", :yellow
puts '-'*80, ''; sleep 0.25
run "rm -f app/models/article.rb"
file 'app/models/article.rb', <<-CODE
class Article < ActiveRecord::Base
include Elasticsearch::Model
include Elasticsearch::Model::Callbacks
#{'attr_accessible :title, :content, :published_on' if Rails::VERSION::STRING < '4'}
end
CODE
git commit: "-a -m 'Added Elasticsearch support into the Article model'"
# ----- Add Elasticsearch integration into the interface ------------------------------------------
puts
say_status "Controller", "Adding controller action, route, and HTML for searching...", :yellow
puts '-'*80, ''; sleep 0.25
inject_into_file 'app/controllers/articles_controller.rb', before: %r|^\s*# GET /articles/1$| do
<<-CODE
# GET /articles/search
def search
@articles = Article.search(params[:q]).records
render action: "index"
end
CODE
end
inject_into_file 'app/views/articles/index.html.erb', after: %r{<h1>Listing articles</h1>}i do
<<-CODE
<hr>
<%= form_tag search_articles_path, method: 'get' do %>
<%= label_tag :query %>
<%= text_field_tag :q, params[:q] %>
<%= submit_tag :search %>
<% end %>
<hr>
CODE
end
inject_into_file 'app/views/articles/index.html.erb', after: %r{<%= link_to 'New Article', new_article_path %>} do
<<-CODE
<%= link_to 'All Articles', articles_path if params[:q] %>
CODE
end
gsub_file 'config/routes.rb', %r{resources :articles$}, <<-CODE
resources :articles do
collection { get :search }
end
CODE
gsub_file "#{Rails::VERSION::STRING > '4' ? 'test/controllers' : 'test/functional'}/articles_controller_test.rb", %r{setup do.*?end}m, <<-CODE
setup do
@article = articles(:one)
Article.__elasticsearch__.import
Article.__elasticsearch__.refresh_index!
end
CODE
inject_into_file "#{Rails::VERSION::STRING > '4' ? 'test/controllers' : 'test/functional'}/articles_controller_test.rb", after: %r{test "should get index" do.*?end}m do
<<-CODE
test "should get search results" do
get :search, q: 'mystring'
assert_response :success
assert_not_nil assigns(:articles)
assert_equal 2, assigns(:articles).size
end
CODE
end
git commit: "-a -m 'Added search form and controller action'"
# ----- Seed the database -------------------------------------------------------------------------
puts
say_status "Database", "Seeding the database with data...", :yellow
puts '-'*80, ''; sleep 0.25
remove_file "db/seeds.rb"
create_file 'db/seeds.rb', %q{
contents = [
'Lorem ipsum dolor sit amet.',
'Consectetur adipisicing elit, sed do eiusmod tempor incididunt.',
'Labore et dolore magna aliqua.',
'Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris.',
'Excepteur sint occaecat cupidatat non proident.'
]
puts "Deleting all articles..."
Article.delete_all
unless ENV['COUNT']
puts "Creating articles..."
%w[ One Two Three Four Five ].each_with_index do |title, i|
Article.create title: title, content: contents[i], published_on: i.days.ago.utc
end
else
print "Generating articles..."
(1..ENV['COUNT'].to_i).each_with_index do |title, i|
Article.create title: "Title #{title}", content: 'Lorem ipsum dolor', published_on: i.days.ago.utc
print '.' if i % ENV['COUNT'].to_i/10 == 0
end
puts "\n"
end
}
run "rails runner 'Article.__elasticsearch__.create_index! force: true'"
rake "db:seed"
git add: "db/seeds.rb"
git commit: "-m 'Added the database seeding script'"
# ----- Print Git log -----------------------------------------------------------------------------
puts
say_status "Git", "Details about the application:", :yellow
puts '-'*80, ''
git tag: "basic"
git log: "--reverse --oneline"
# ----- Start the application ---------------------------------------------------------------------
unless ENV['RAILS_NO_SERVER_START']
require 'net/http'
if (begin; Net::HTTP.get(URI('http://localhost:3000')); rescue Errno::ECONNREFUSED; false; rescue Exception; true; end)
puts "\n"
say_status "ERROR", "Some other application is running on port 3000!\n", :red
puts '-'*80
port = ask("Please provide free port:", :bold)
else
port = '3000'
end
puts "", "="*80
say_status "DONE", "\e[1mStarting the application.\e[0m", :yellow
puts "="*80, ""
run "rails server --port=#{port}"
end

View file

@ -0,0 +1,311 @@
# $ rails new searchapp --skip --skip-bundle --template https://raw.github.com/elasticsearch/elasticsearch-rails/master/elasticsearch-rails/lib/rails/templates/02-pretty.rb
unless File.read('README.rdoc').include? '== [1] Basic'
say_status "ERROR", "You have to run the 01-basic.rb template first.", :red
exit(1)
end
puts
say_status "README", "Updating Readme...\n", :yellow
puts '-'*80, ''; sleep 0.25
append_to_file 'README.rdoc', <<-README
== [2] Pretty
The `pretty` template builds on the `basic` version and brings couple of improvements:
* Using the [Bootstrap](http://getbootstrap.com) framework to enhance the visual style of the application
* Using an `Article.search` class method to customize the default search definition
* Highlighting matching phrases in search results
* Paginating results with Kaminari
README
git add: "README.rdoc"
git commit: "-m '[02] Updated the application README'"
# ----- Update application.rb ---------------------------------------------------------------------
puts
say_status "Rubygems", "Adding Rails logger integration...\n", :yellow
puts '-'*80, ''; sleep 0.25
insert_into_file 'config/application.rb',
"\n\nrequire 'elasticsearch/rails/instrumentation'",
after: /Bundler\.require.+$/
git add: "config/application.rb"
git commit: "-m 'Added the Rails logger integration to application.rb'"
# ----- Add gems into Gemfile ---------------------------------------------------------------------
puts
say_status "Rubygems", "Adding Rubygems into Gemfile...\n", :yellow
puts '-'*80, ''; sleep 0.25
# NOTE: Kaminari has to be loaded before Elasticsearch::Model so the callbacks are executed
#
insert_into_file 'Gemfile', <<-CODE, before: /gem ["']elasticsearch["'].+$/
# NOTE: Kaminari has to be loaded before Elasticsearch::Model so the callbacks are executed
gem 'kaminari'
CODE
run "bundle install"
git add: "Gemfile*"
git commit: "-m 'Added the Kaminari gem'"
# ----- Add `Article.search` class method ---------------------------------------------------------
puts
say_status "Model", "Adding a `Article.search` class method...\n", :yellow
puts '-'*80, ''; sleep 0.5
insert_into_file 'app/models/article.rb', <<-CODE, after: 'include Elasticsearch::Model::Callbacks'
def self.search(query)
__elasticsearch__.search(
{
query: {
multi_match: {
query: query,
fields: ['title^10', 'content']
}
},
highlight: {
pre_tags: ['<em class="label label-highlight">'],
post_tags: ['</em>'],
fields: {
title: { number_of_fragments: 0 },
content: { fragment_size: 25 }
}
}
}
)
end
CODE
insert_into_file "#{Rails::VERSION::STRING > '4' ? 'test/models' : 'test/unit' }/article_test.rb", <<-CODE, after: /class ArticleTest < ActiveSupport::TestCase$/
teardown do
Article.__elasticsearch__.unstub(:search)
end
CODE
gsub_file "#{Rails::VERSION::STRING > '4' ? 'test/models' : 'test/unit' }/article_test.rb", %r{# test "the truth" do.*?# end}m, <<-CODE
test "has a search method delegating to __elasticsearch__" do
Article.__elasticsearch__.expects(:search).with do |definition|
assert_equal 'foo', definition[:query][:multi_match][:query]
true
end
Article.search 'foo'
end
CODE
git add: "app/models/article.rb"
git add: "test/**/article_test.rb"
git commit: "-m 'Added an `Article.search` method'"
# ----- Add loading Bootstrap assets --------------------------------------------------------------
puts
say_status "Bootstrap", "Adding Bootstrap asset links into the 'application' layout...\n", :yellow
puts '-'*80, ''; sleep 0.5
gsub_file 'app/views/layouts/application.html.erb', %r{<%= yield %>}, <<-CODE unless File.read('app/views/layouts/application.html.erb').include?('class="container"')
<div class="container">
<%= yield %>
</div>
CODE
insert_into_file 'app/views/layouts/application.html.erb', <<-CODE, before: '</head>'
<link rel="stylesheet" href="//netdna.bootstrapcdn.com/bootstrap/3.0.2/css/bootstrap.min.css">
<script src="//netdna.bootstrapcdn.com/bootstrap/3.0.2/js/bootstrap.min.js"></script>
CODE
git commit: "-a -m 'Added loading Bootstrap assets in the application layout'"
# ----- Customize the search form -----------------------------------------------------------------
puts
say_status "Bootstrap", "Customizing the index page...\n", :yellow
puts '-'*80, ''; sleep 0.5
gsub_file 'app/views/articles/index.html.erb', %r{<%= label_tag .* :search %>}m do |match|
<<-CODE
<div class="input-group">
<%= text_field_tag :q, params[:q], class: 'form-control', placeholder: 'Search...' %>
<span class="input-group-btn">
<button type="button" class="btn btn-default">
<span class="glyphicon glyphicon-search"></span>
</button>
</span>
</div>
CODE
end
# ----- Customize the header -----------------------------------------------------------------
gsub_file 'app/views/articles/index.html.erb', %r{<h1>Listing articles</h1>} do |match|
"<h1><%= controller.action_name == 'search' ? 'Searching articles' : 'Listing articles' %></h1>"
end
# ----- Customize the results listing -------------------------------------------------------------
gsub_file 'app/views/articles/index.html.erb', %r{<table>} do |match|
'<table class="table table-hover">'
end
gsub_file 'app/views/articles/index.html.erb', %r{<td><%= link_to [^%]+} do |match|
match.gsub!('<td>', '<td style="width: 50px">')
match.include?("btn") ? match : (match + ", class: 'btn btn-default btn-xs'")
end
gsub_file 'app/views/articles/index.html.erb', %r{<br>\s*(<\%= link_to 'New Article'.*)}m do |content|
replace = content.match(%r{<br>\s*(<\%= link_to 'New Article'.*)}m)[1]
<<-END.gsub(/^ /, '')
<hr>
<p style="text-align: center; margin-bottom: 21px">
#{replace}
</p>
END
end
gsub_file 'app/views/articles/index.html.erb', %r{<%= link_to 'New Article',\s*new_article_path} do |match|
return match if match.include?('btn')
match + ", class: 'btn btn-primary btn-xs', style: 'color: #fff'"
end
gsub_file 'app/views/articles/index.html.erb', %r{<%= link_to 'All Articles',\s*articles_path} do |match|
return match if match.include?('btn')
"\n " + match + ", class: 'btn btn-primary btn-xs', style: 'color: #fff'"
end
git add: "app/views"
git commit: "-m 'Refactored the articles listing to use Bootstrap components'"
# ----- Use highlighted excerpts in the listing ---------------------------------------------------
gsub_file 'app/views/articles/index.html.erb', %r{<% @articles.each do \|article\| %>$} do |match|
"<% @articles.__send__ controller.action_name == 'search' ? :each_with_hit : :each do |article, hit| %>"
end
gsub_file 'app/views/articles/index.html.erb', %r{<td><%= article.title %></td>$} do |match|
"<td><%= hit.try(:highlight).try(:title) ? hit.highlight.title.join.html_safe : article.title %></td>"
end
gsub_file 'app/views/articles/index.html.erb', %r{<td><%= article.content %></td>$} do |match|
"<td><%= hit.try(:highlight).try(:content) ? hit.highlight.content.join('&hellip;').html_safe : article.content %></td>"
end
git commit: "-a -m 'Added highlighting for matches'"
# ----- Paginate the results ----------------------------------------------------------------------
gsub_file 'app/controllers/articles_controller.rb', %r{@articles = Article.all} do |match|
"@articles = Article.page(params[:page])"
end
gsub_file 'app/controllers/articles_controller.rb', %r{@articles = Article.search\(params\[\:q\]\).records} do |match|
"@articles = Article.search(params[:q]).page(params[:page]).records"
end
insert_into_file 'app/views/articles/index.html.erb', after: '</table>' do
<<-CODE.gsub(/^ /, '')
<div class="text-center">
<%= paginate @articles %>
</div>
CODE
end
generate "kaminari:views", "bootstrap2", "--force"
gsub_file 'app/views/kaminari/_paginator.html.erb', %r{<ul>}, '<ul class="pagination">'
git add: "."
git commit: "-m 'Added pagination to articles listing'"
# ----- Custom CSS --------------------------------------------------------------------------------
puts
say_status "CSS", "Adding custom styles...\n", :yellow
puts '-'*80, ''; sleep 0.5
append_to_file 'app/assets/stylesheets/application.css' do
unless File.read('app/assets/stylesheets/application.css').include?('.label-highlight')
<<-CODE
.label-highlight {
font-size: 100% !important;
font-weight: inherit !important;
font-style: inherit !important;
color: #333 !important;
background: #fff401 !important;
}
div.pagination {
text-align: center;
display: block;
}
div.pagination ul {
display: inline-block;
}
CODE
else
''
end
end
git commit: "-a -m 'Added custom style definitions into application.css'"
# ----- Generate 1,000 articles -------------------------------------------------------------------
puts
say_status "Database", "Creating 1,000 articles...", :yellow
puts '-'*80, '';
run "rails runner 'Article.__elasticsearch__.create_index! force: true'"
rake "db:seed COUNT=1_000"
# ----- Print Git log -----------------------------------------------------------------------------
puts
say_status "Git", "Details about the application:", :yellow
puts '-'*80, ''
git tag: "pretty"
git log: "--reverse --oneline pretty...basic"
# ----- Start the application ---------------------------------------------------------------------
unless ENV['RAILS_NO_SERVER_START']
require 'net/http'
if (begin; Net::HTTP.get(URI('http://localhost:3000')); rescue Errno::ECONNREFUSED; false; rescue Exception; true; end)
puts "\n"
say_status "ERROR", "Some other application is running on port 3000!\n", :red
puts '-'*80
port = ask("Please provide free port:", :bold)
else
port = '3000'
end
puts "", "="*80
say_status "DONE", "\e[1mStarting the application. Open http://localhost:#{port}\e[0m", :yellow
puts "="*80, ""
run "rails server --port=#{port}"
end

View file

@ -0,0 +1,349 @@
# $ rails new searchapp --skip --skip-bundle --template https://raw.github.com/elasticsearch/elasticsearch-rails/master/elasticsearch-rails/lib/rails/templates/03-expert.rb
unless File.read('README.rdoc').include? '== [2] Pretty'
say_status "ERROR", "You have to run the 01-basic.rb and 02-pretty.rb templates first.", :red
exit(1)
end
begin
require 'redis'
rescue LoadError
say_status "ERROR", "Please install the 'redis' gem before running this template", :red
exit(1)
end
begin
Redis.new.info
rescue Redis::CannotConnectError
puts
say_status "ERROR", "Redis not available", :red
say_status "", "This template uses an asynchronous indexer via Sidekiq, and requires a running Redis server."
exit(1)
end
append_to_file 'README.rdoc', <<-README
== [3] Expert
The `expert` template changes to a complex database schema with model relationships: article belongs
to a category, has many authors and comments.
* The Elasticsearch integration is refactored into the `Searchable` concern
* A complex mapping for the index is defined
* A custom serialization is defined in `Article#as_indexed_json`
* The `search` method is amended with facets and suggestions
* A [Sidekiq](http://sidekiq.org) worker for handling index updates in background is added
* A custom `SearchController` with associated view is added
* A Rails initializer is added to customize the Elasticsearch client configuration
* Seed script and example data from New York Times is added
README
git add: "README.rdoc"
git commit: "-m '[03] Updated the application README'"
# ----- Add gems into Gemfile ---------------------------------------------------------------------
puts
say_status "Rubygems", "Adding Rubygems into Gemfile...\n", :yellow
puts '-'*80, ''; sleep 0.25
gem "oj"
git add: "Gemfile*"
git commit: "-m 'Added Ruby gems'"
# ----- Customize the Rails console ---------------------------------------------------------------
puts
say_status "Rails", "Customizing `rails console`...\n", :yellow
puts '-'*80, ''; sleep 0.25
gem "pry", group: 'development'
environment nil, env: 'development' do
%q{
console do
config.console = Pry
Pry.config.history.file = Rails.root.join('tmp/console_history.rb').to_s
Pry.config.prompt = [ proc { |obj, nest_level, _| "(#{obj})> " },
proc { |obj, nest_level, _| ' '*obj.to_s.size + ' '*(nest_level+1) + '| ' } ]
end
}
end
git add: "Gemfile*"
git add: "config/"
git commit: "-m 'Added Pry as the console for development'"
# ----- Disable asset logging in development ------------------------------------------------------
puts
say_status "Application", "Disabling asset logging in development...\n", :yellow
puts '-'*80, ''; sleep 0.25
environment 'config.assets.logger = false', env: 'development'
gem 'quiet_assets', group: "development"
git add: "Gemfile*"
git add: "config/"
git commit: "-m 'Disabled asset logging in development'"
# ----- Run bundle install ------------------------------------------------------------------------
run "bundle install"
# ----- Define and generate schema ----------------------------------------------------------------
puts
say_status "Models", "Adding complex schema...\n", :yellow
puts '-'*80, ''
generate :scaffold, "Category title"
generate :scaffold, "Author first_name last_name"
generate :scaffold, "Authorship article:references author:references"
generate :model, "Comment body:text user:string user_location:string stars:integer pick:boolean article:references"
generate :migration, "CreateArticlesCategories article:references category:references"
rake "db:drop"
rake "db:migrate"
insert_into_file "app/models/category.rb", :before => "end" do
<<-CODE
has_and_belongs_to_many :articles
CODE
end
insert_into_file "app/models/author.rb", :before => "end" do
<<-CODE
has_many :authorships
def full_name
[first_name, last_name].join(' ')
end
CODE
end
gsub_file "app/models/authorship.rb", %r{belongs_to :article$}, <<-CODE
belongs_to :article, touch: true
CODE
insert_into_file "app/models/article.rb", after: "ActiveRecord::Base" do
<<-CODE
has_and_belongs_to_many :categories, after_add: [ lambda { |a,c| Indexer.perform_async(:update, a.class.to_s, a.id) } ],
after_remove: [ lambda { |a,c| Indexer.perform_async(:update, a.class.to_s, a.id) } ]
has_many :authorships
has_many :authors, through: :authorships
has_many :comments
CODE
end
gsub_file "app/models/comment.rb", %r{belongs_to :article$}, <<-CODE
belongs_to :article, touch: true
CODE
git add: "."
git commit: "-m 'Generated Category, Author and Comment resources'"
# ----- Add the `abstract` column -----------------------------------------------------------------
puts
say_status "Model", "Adding the `abstract` column to Article...\n", :yellow
puts '-'*80, ''
generate :migration, "AddColumnsToArticle abstract:text url:string shares:integer"
rake "db:migrate"
git add: "db/"
git commit: "-m 'Added additional columns to Article'"
# ----- Move the model integration into a concern -------------------------------------------------
puts
say_status "Model", "Refactoring the model integration...\n", :yellow
puts '-'*80, ''; sleep 0.25
remove_file 'app/models/article.rb'
create_file 'app/models/article.rb', <<-CODE
class Article < ActiveRecord::Base
include Searchable
end
CODE
gsub_file "#{Rails::VERSION::STRING > '4' ? 'test/models' : 'test/unit' }/article_test.rb", %r{assert_equal 'foo', definition\[:query\]\[:multi_match\]\[:query\]}, "assert_equal 'foo', definition.to_hash[:query][:bool][:should][0][:multi_match][:query]"
# copy_file File.expand_path('../searchable.rb', __FILE__), 'app/models/concerns/searchable.rb'
get 'https://raw.githubusercontent.com/elastic/elasticsearch-rails/master/elasticsearch-rails/lib/rails/templates/searchable.rb',
'app/models/concerns/searchable.rb'
insert_into_file "app/models/article.rb", after: "ActiveRecord::Base" do
<<-CODE
has_and_belongs_to_many :categories, after_add: [ lambda { |a,c| Indexer.perform_async(:update, a.class.to_s, a.id) } ],
after_remove: [ lambda { |a,c| Indexer.perform_async(:update, a.class.to_s, a.id) } ]
has_many :authorships
has_many :authors, through: :authorships
has_many :comments
CODE
end
git add: "app/models/ test/models"
git commit: "-m 'Refactored the Elasticsearch integration into a concern\n\nSee:\n\n* http://37signals.com/svn/posts/3372-put-chubby-models-on-a-diet-with-concerns\n* http://joshsymonds.com/blog/2012/10/25/rails-concerns-v-searchable-with-elasticsearch/'"
# ----- Add Sidekiq indexer -----------------------------------------------------------------------
puts
say_status "Application", "Adding Sidekiq worker for updating the index...\n", :yellow
puts '-'*80, ''; sleep 0.25
gem "sidekiq"
run "bundle install"
# copy_file File.expand_path('../indexer.rb', __FILE__), 'app/workers/indexer.rb'
get 'https://raw.githubusercontent.com/elastic/elasticsearch-rails/master/elasticsearch-rails/lib/rails/templates/indexer.rb',
'app/workers/indexer.rb'
insert_into_file "test/test_helper.rb",
"require 'sidekiq/testing'\n\n",
before: "class ActiveSupport::TestCase\n"
git add: "Gemfile* app/workers/ test/test_helper.rb"
git commit: "-m 'Added a Sidekiq indexer\n\nRun:\n\n $ bundle exec sidekiq --queue elasticsearch --verbose\n\nSee http://sidekiq.org'"
# ----- Add SearchController -----------------------------------------------------------------------
puts
say_status "Controllers", "Adding SearchController...\n", :yellow
puts '-'*80, ''; sleep 0.25
create_file 'app/controllers/search_controller.rb' do
<<-CODE.gsub(/^ /, '')
class SearchController < ApplicationController
def index
options = {
category: params[:c],
author: params[:a],
published_week: params[:w],
published_day: params[:d],
sort: params[:s],
comments: params[:comments]
}
@articles = Article.search(params[:q], options).page(params[:page]).results
end
end
CODE
end
# copy_file File.expand_path('../search_controller_test.rb', __FILE__), 'test/controllers/search_controller_test.rb'
get 'https://raw.githubusercontent.com/elastic/elasticsearch-rails/master/elasticsearch-rails/lib/rails/templates/search_controller_test.rb',
'test/controllers/search_controller_test.rb'
route "get '/search', to: 'search#index', as: 'search'"
gsub_file 'config/routes.rb', %r{root to: 'articles#index'$}, "root to: 'search#index'"
# copy_file File.expand_path('../index.html.erb', __FILE__), 'app/views/search/index.html.erb'
get 'https://raw.githubusercontent.com/elastic/elasticsearch-rails/master/elasticsearch-rails/lib/rails/templates/index.html.erb',
'app/views/search/index.html.erb'
# copy_file File.expand_path('../search.css', __FILE__), 'app/assets/stylesheets/search.css'
get 'https://raw.githubusercontent.com/elastic/elasticsearch-rails/master/elasticsearch-rails/lib/rails/templates/search.css',
'app/assets/stylesheets/search.css'
git add: "app/controllers/ test/controllers/ config/routes.rb"
git add: "app/views/search/ app/assets/stylesheets/search.css"
git commit: "-m 'Added SearchController#index'"
# ----- Add initializer ---------------------------------------------------------------------------
puts
say_status "Application", "Adding Elasticsearch configuration in an initializer...\n", :yellow
puts '-'*80, ''; sleep 0.5
create_file 'config/initializers/elasticsearch.rb', <<-CODE
# Connect to specific Elasticsearch cluster
ELASTICSEARCH_URL = ENV['ELASTICSEARCH_URL'] || 'http://localhost:9200'
Elasticsearch::Model.client = Elasticsearch::Client.new host: ELASTICSEARCH_URL
# Print Curl-formatted traces in development into a file
#
if Rails.env.development?
tracer = ActiveSupport::Logger.new('log/elasticsearch.log')
tracer.level = Logger::DEBUG
end
Elasticsearch::Model.client.transport.tracer = tracer
CODE
git add: "config/initializers"
git commit: "-m 'Added Rails initializer with Elasticsearch configuration'"
# ----- Add Rake tasks ----------------------------------------------------------------------------
puts
say_status "Application", "Adding Elasticsearch Rake tasks...\n", :yellow
puts '-'*80, ''; sleep 0.5
create_file 'lib/tasks/elasticsearch.rake', <<-CODE
require 'elasticsearch/rails/tasks/import'
CODE
git add: "lib/tasks"
git commit: "-m 'Added Rake tasks for Elasticsearch'"
# ----- Insert and index data ---------------------------------------------------------------------
puts
say_status "Database", "Re-creating the database with data and importing into Elasticsearch...", :yellow
puts '-'*80, ''; sleep 0.25
# copy_file File.expand_path('../articles.yml.gz', __FILE__), 'db/articles.yml.gz'
get 'https://raw.githubusercontent.com/elastic/elasticsearch-rails/master/elasticsearch-rails/lib/rails/templates/articles.yml.gz',
'db/articles.yml.gz'
remove_file 'db/seeds.rb'
# copy_file File.expand_path('../seeds.rb', __FILE__), 'db/seeds.rb'
get 'https://raw.githubusercontent.com/elastic/elasticsearch-rails/master/elasticsearch-rails/lib/rails/templates/seeds.rb',
'db/seeds.rb'
rake "db:reset"
rake "environment elasticsearch:import:model CLASS='Article' BATCH=100 FORCE=y"
git add: "db/seeds.rb db/articles.yml.gz"
git commit: "-m 'Added a seed script and source data'"
# ----- Print Git log -----------------------------------------------------------------------------
puts
say_status "Git", "Details about the application:", :yellow
puts '-'*80, ''
git tag: "expert"
git log: "--reverse --oneline HEAD...pretty"
# ----- Start the application ---------------------------------------------------------------------
unless ENV['RAILS_NO_SERVER_START']
require 'net/http'
if (begin; Net::HTTP.get(URI('http://localhost:3000')); rescue Errno::ECONNREFUSED; false; rescue Exception; true; end)
puts "\n"
say_status "ERROR", "Some other application is running on port 3000!\n", :red
puts '-'*80
port = ask("Please provide free port:", :bold)
else
port = '3000'
end
puts "", "="*80
say_status "DONE", "\e[1mStarting the application. Open http://localhost:#{port}\e[0m", :yellow
puts "="*80, ""
run "rails server --port=#{port}"
end

View file

@ -0,0 +1,131 @@
# $ rails new searchapp --skip --skip-bundle --template https://raw.githubusercontent.com/elastic/elasticsearch-rails/master/elasticsearch-rails/lib/rails/templates/04-dsl.rb
unless File.read('README.rdoc').include? '== [3] Expert'
say_status "ERROR", "You have to run the 01-basic.rb, 02-pretty.rb and 03-expert.rb templates first.", :red
exit(1)
end
append_to_file 'README.rdoc', <<-README
== [4] DSL
The `dsl` template refactors the search definition in SearchController#index
to use the [`elasticsearch-dsl`](https://github.com/elastic/elasticsearch-ruby/tree/dsl/elasticsearch-dsl)
Rubygem for better expresivity and readability of the code.
README
git add: "README.rdoc"
git commit: "-m '[03] Updated the application README'"
run 'rm -f app/assets/stylesheets/*.scss'
run 'rm -f app/assets/javascripts/*.coffee'
# ----- Add gems into Gemfile ---------------------------------------------------------------------
puts
say_status "Rubygems", "Adding Rubygems into Gemfile...\n", :yellow
puts '-'*80, ''; sleep 0.25
gem "elasticsearch-dsl", git: "git://github.com/elastic/elasticsearch-ruby.git"
git add: "Gemfile*"
git commit: "-m 'Added the `elasticsearch-dsl` gem'"
# ----- Run bundle install ------------------------------------------------------------------------
run "bundle install"
# ----- Change the search definition implementation and associated views and tests ----------------
# copy_file File.expand_path('../searchable.dsl.rb', __FILE__), 'app/models/concerns/searchable.rb', force: true
get 'https://raw.githubusercontent.com/elastic/elasticsearch-rails/master/elasticsearch-rails/lib/rails/templates/searchable.dsl.rb',
'app/models/concerns/searchable.rb', force: true
# copy_file File.expand_path('../index.html.dsl.erb', __FILE__), 'app/views/search/index.html.erb', force: true
get 'https://raw.githubusercontent.com/elastic/elasticsearch-rails/master/elasticsearch-rails/lib/rails/templates/index.html.dsl.erb',
'app/views/search/index.html.erb', force: true
gsub_file "test/controllers/search_controller_test.rb", %r{test "should return facets" do.*?end}m, <<-CODE
test "should return aggregations" do
get :index, q: 'one'
assert_response :success
aggregations = assigns(:articles).response.response['aggregations']
assert_equal 2, aggregations['categories']['categories']['buckets'].size
assert_equal 2, aggregations['authors']['authors']['buckets'].size
assert_equal 2, aggregations['published']['published']['buckets'].size
assert_equal 'John Smith', aggregations['authors']['authors']['buckets'][0]['key']
assert_equal 'One', aggregations['categories']['categories']['buckets'][0]['key']
assert_equal '2015-03-02T00:00:00.000Z', aggregations['published']['published']['buckets'][0]['key_as_string']
end
CODE
gsub_file "test/controllers/search_controller_test.rb", %r{test "should filter search results and the author and published date facets when user selects a category" do.*?end}m, <<-CODE
test "should filter search results and the author and published date facets when user selects a category" do
get :index, q: 'one', c: 'One'
assert_response :success
assert_equal 2, assigns(:articles).size
aggregations = assigns(:articles).response.response['aggregations']
assert_equal 1, aggregations['authors']['authors']['buckets'].size
assert_equal 1, aggregations['published']['published']['buckets'].size
# Do NOT filter the category facet
assert_equal 2, aggregations['categories']['categories']['buckets'].size
end
CODE
gsub_file "test/controllers/search_controller_test.rb", %r{test "should filter search results and the category and published date facets when user selects a category" do.*?end}m, <<-CODE
test "should filter search results and the category and published date facets when user selects a category" do
get :index, q: 'one', a: 'Mary Smith'
assert_response :success
assert_equal 1, assigns(:articles).size
aggregations = assigns(:articles).response.response['aggregations']
assert_equal 1, aggregations['categories']['categories']['buckets'].size
assert_equal 1, aggregations['published']['published']['buckets'].size
# Do NOT filter the authors facet
assert_equal 2, aggregations['authors']['authors']['buckets'].size
end
CODE
git add: "app/models/concerns/ app/views/search/ test/controllers/search_controller_test.rb"
git commit: "-m 'Updated the Article.search method to use the Ruby DSL and updated the associated views and tests'"
# ----- Print Git log -----------------------------------------------------------------------------
puts
say_status "Git", "Details about the application:", :yellow
puts '-'*80, ''
git tag: "dsl"
git log: "--reverse --oneline HEAD...expert"
# ----- Start the application ---------------------------------------------------------------------
unless ENV['RAILS_NO_SERVER_START']
require 'net/http'
if (begin; Net::HTTP.get(URI('http://localhost:3000')); rescue Errno::ECONNREFUSED; false; rescue Exception; true; end)
puts "\n"
say_status "ERROR", "Some other application is running on port 3000!\n", :red
puts '-'*80
port = ask("Please provide free port:", :bold)
else
port = '3000'
end
puts "", "="*80
say_status "DONE", "\e[1mStarting the application. Open http://localhost:#{port}\e[0m", :yellow
puts "="*80, ""
run "rails server --port=#{port}"
end

View file

@ -0,0 +1,77 @@
# $ rails new searchapp --skip --skip-bundle --template https://raw.githubusercontent.com/elastic/elasticsearch-rails/master/elasticsearch-rails/lib/rails/templates/05-settings-files.rb
# (See: 01-basic.rb, 02-pretty.rb, 03-expert.rb, 04-dsl.rb)
append_to_file 'README.rdoc', <<-README
== [5] Settings Files
The `settings-files` template refactors the `Searchable` module to load the index settings
from an external file.
README
git add: "README.rdoc"
git commit: "-m '[05] Updated the application README'"
# ----- Setup the Searchable module to load settings from config/elasticsearch/articles_settings.json
gsub_file "app/models/concerns/searchable.rb",
/index: { number_of_shards: 1, number_of_replicas: 0 }/,
"File.open('config/elasticsearch/articles_settings.json')"
git add: "app/models/concerns/searchable.rb"
git commit: "-m 'Setup the Searchable module to load settings from file'"
# ----- Copy the articles_settings.json file -------------------------------------------------------
# copy_file File.expand_path('../articles_settings.json', __FILE__), 'config/elasticsearch/articles_settings.json'
get 'https://raw.githubusercontent.com/elastic/elasticsearch-rails/master/elasticsearch-rails/lib/rails/templates/articles_settings.json',
'config/elasticsearch/articles_settings.json', force: true
git add: "config/elasticsearch/articles_settings.json"
git commit: "-m 'Create the articles settings file'"
# ----- Temporarily set local repo for testing ----------------------------------------------------
gsub_file "Gemfile",
%r{gem 'elasticsearch-model', git: 'git://github.com/elasticsearch/elasticsearch-rails.git'},
"gem 'elasticsearch-model', path: File.expand_path('../../../../../../elasticsearch-model', __FILE__)"
# ----- Run bundle install ------------------------------------------------------------------------
run "bundle install"
# ----- Recreate the index ------------------------------------------------------------------------
rake "environment elasticsearch:import:model CLASS='Article' BATCH=100 FORCE=y"
# ----- Print Git log -----------------------------------------------------------------------------
puts
say_status "Git", "Details about the application:", :yellow
puts '-'*80, ''
git tag: "settings-files"
git log: "--reverse --oneline HEAD...dsl"
# ----- Start the application ---------------------------------------------------------------------
unless ENV['RAILS_NO_SERVER_START']
require 'net/http'
if (begin; Net::HTTP.get(URI('http://localhost:3000')); rescue Errno::ECONNREFUSED; false; rescue Exception; true; end)
puts "\n"
say_status "ERROR", "Some other application is running on port 3000!\n", :red
puts '-'*80
port = ask("Please provide free port:", :bold)
else
port = '3000'
end
puts "", "="*80
say_status "DONE", "\e[1mStarting the application. Open http://localhost:#{port}\e[0m", :yellow
puts "="*80, ""
run "rails server --port=#{port}"
end

View file

@ -0,0 +1 @@
{ "number_of_shards": 1, "number_of_replicas": 0 }

View file

@ -0,0 +1,160 @@
<div class="col-md-12">
<h1 class="text-right"><%= link_to 'Search New York Times articles', root_path %></h1>
<%= form_tag search_path, method: 'get', role: 'search' do %>
<div class="input-group">
<%= text_field_tag :q, params[:q], class: 'form-control', placeholder: 'Search...' %>
<span class="input-group-btn">
<button type="submit" class="btn btn-default">
<span class="glyphicon glyphicon-search"></span>
</button>
</span>
</div>
<div id="form-options" class="clearfix">
<div class="btn-group pull-left">
<label class="checkbox-inline">
<%= check_box_tag 'comments', 'y', params[:comments] == 'y', onclick: "$(this).closest('form').submit()" %>
Search in comments?
</label>
<% params.slice(:a, :c, :s).each do |name, value| %>
<%= hidden_field_tag name, value %>
<% end %>
</div>
<div class="btn-group pull-right">
<p style="float: left; margin: 0.1em 0 0 0"><small>Displaying <%= (params[:page] || 1).to_i.ordinalize %> page with <%= @articles.size %> articles
of <strong>total <%= @articles.total %></strong></small></p>
<button class="btn btn-default btn-xs dropdown-toggle" type="button" data-toggle="dropdown" style="margin-left: 0.5em">
<% sort = case
when params[:s] then params[:s]
when params[:q].blank? then 'published_on'
else 'relevancy'
end
%>
sorted by <%= sort.humanize.downcase %> <span class="caret"></span>
</button>
<ul class="dropdown-menu" role="menu">
<li><%= link_to "Sort by published on", search_path(params.except(:controller, :action).merge(s: 'published_on')), class: 'btn-xs' %></li>
<li><%= link_to "Sort by relevancy", search_path(params.except(:controller, :action).merge(s: nil)), class: 'btn-xs' %></li>
</ul>
</div>
</div>
<% end %>
<hr>
</div>
<% if @articles.size < 1 && @articles.response.suggestions.present? %>
<div class="col-md-12">
<p class="alert alert-warning">
No documents have been found.
<% if @articles.response.suggestions.terms.present? %>
Maybe you mean
<%= @articles.response.suggestions.terms.map do |term|
link_to term, search_path(params.except(:controller, :action).merge q: term)
end.to_sentence(last_word_connector: ' or ').html_safe %>?
<% end %>
</p>
</div>
<% end %>
<div id="facets" class="col-md-3">
<% unless @articles.size < 1 %>
<div class="categories panel panel-default">
<p class="panel-heading"><%= link_to 'All Sections &rarr;'.html_safe, search_path(params.except(:controller, :action).merge(c: nil))%></p>
<div class="list-group">
<% @articles.response.response['aggregations']['categories']['categories']['buckets'].each do |c| %>
<%=
link_to search_path(params.except(:controller, :action).merge(c: c['key'])),
class: "list-group-item#{' active' if params[:c] == c['key']}" do
c['key'].titleize.html_safe + content_tag(:small, c['doc_count'], class: 'badge').html_safe
end
%>
<% end %>
</div>
</div>
<div class="authors panel panel-default">
<p class="panel-heading"><%= link_to 'All Authors &rarr;'.html_safe, search_path(params.except(:controller, :action).merge(a: nil))%></p>
<div class="list-group">
<% @articles.response.response['aggregations']['authors']['authors']['buckets'].each do |a| %>
<%=
link_to search_path(params.except(:controller, :action).merge(a: a['key'])),
class: "list-group-item#{' active' if params[:a] == a['key']}" do
a['key'].titleize.html_safe + content_tag(:small, a['doc_count'], class: 'badge').html_safe
end
%>
<% end %>
</div>
</div>
<div class="authors panel panel-default">
<p class="panel-heading"><%= link_to 'Any Date &rarr;'.html_safe, search_path(params.except(:controller, :action).merge(w: nil))%></p>
<div class="list-group">
<% @articles.response.response['aggregations']['published']['published']['buckets'].each do |w| %>
<%=
__start = Time.at(w['key']/1000)
__end = __start.end_of_week
__date = __start.to_date.to_s(:iso)
link_to search_path(params.except(:controller, :action).merge(w: __date)),
class: "list-group-item#{' active' if params[:w] == __date}" do
"#{__start.to_date.to_s(:short)} &mdash; #{__end.to_date.to_s(:short)}".html_safe + \
content_tag(:small, w['doc_count'], class: 'badge').html_safe
end
%>
<% end %>
</div>
</div>
<% end %>
</div>
<div class="col-md-9">
<div id="results">
<% @articles.each do |article| %>
<div class="result">
<h3 class="title">
<%= (article.try(:highlight).try(:title) ? article.highlight.title.join.html_safe : article.title) %>
<small class="category"><%= article.categories.to_sentence %></small>
</h3>
<p class="body">
<% if article.try(:highlight).try(:abstract) %>
<%= article.highlight.abstract.join.html_safe %>
<% else %>
<%= article.try(:highlight).try(:content) ? article.highlight.content.join('&hellip;').html_safe : article.abstract %>
<% end %>
</p>
<% if comments = article.try(:highlight) && article.highlight['comments.body'] %>
<p class="comments">
Comments: <%= comments.join('&hellip;').html_safe %>
</p>
<% end %>
<p class="text-muted">
<small>Authors: <%= article.authors.map(&:full_name).to_sentence %></small> |
<small>Published: <%= article.published_on %></small> |
<small>Score: <%= article._score %></small>
</p>
</div>
<% end %>
</div>
<ul class="pager">
<li class="previous"><%= link_to_previous_page @articles, 'Previous Page', params: params.slice(:q, :c, :a, :comments) %></li>
<li class="next"><%= link_to_next_page @articles, 'Next Page', params: params.slice(:q, :c, :a, :comments) %></li>
</ul>
</div>
<div class="footer <%= @articles.size < 1 ? 'col-md-12' : 'col-md-9 col-md-offset-3' %>">
<p><small>Content provided by <a href="http://nytimes.com"><em>The New York Times</em></a>.</small></p>
</div>

View file

@ -0,0 +1,160 @@
<div class="col-md-12">
<h1 class="text-right"><%= link_to 'Search New York Times articles', root_path %></h1>
<%= form_tag search_path, method: 'get', role: 'search' do %>
<div class="input-group">
<%= text_field_tag :q, params[:q], class: 'form-control', placeholder: 'Search...' %>
<span class="input-group-btn">
<button type="submit" class="btn btn-default">
<span class="glyphicon glyphicon-search"></span>
</button>
</span>
</div>
<div id="form-options" class="clearfix">
<div class="btn-group pull-left">
<label class="checkbox-inline">
<%= check_box_tag 'comments', 'y', params[:comments] == 'y', onclick: "$(this).closest('form').submit()" %>
Search in comments?
</label>
<% params.slice(:a, :c, :s).each do |name, value| %>
<%= hidden_field_tag name, value %>
<% end %>
</div>
<div class="btn-group pull-right">
<p style="float: left; margin: 0.1em 0 0 0"><small>Displaying <%= (params[:page] || 1).to_i.ordinalize %> page with <%= @articles.size %> articles
of <strong>total <%= @articles.total %></strong></small></p>
<button class="btn btn-default btn-xs dropdown-toggle" type="button" data-toggle="dropdown" style="margin-left: 0.5em">
<% sort = case
when params[:s] then params[:s]
when params[:q].blank? then 'published_on'
else 'relevancy'
end
%>
sorted by <%= sort.humanize.downcase %> <span class="caret"></span>
</button>
<ul class="dropdown-menu" role="menu">
<li><%= link_to "Sort by published on", search_path(params.except(:controller, :action).merge(s: 'published_on')), class: 'btn-xs' %></li>
<li><%= link_to "Sort by relevancy", search_path(params.except(:controller, :action).merge(s: nil)), class: 'btn-xs' %></li>
</ul>
</div>
</div>
<% end %>
<hr>
</div>
<% if @articles.size < 1 && @articles.response.suggest.present? %>
<div class="col-md-12">
<p class="alert alert-warning">
No documents have been found.
<% if @articles.response.suggest['suggest_title'].present? || @articles.response.suggest['suggest_body'].present? %>
Maybe you mean
<%= @articles.response.suggest.map { |k,v| v.first['options'] }.flatten.map {|v| v['text']}.uniq.map do |term|
link_to term, search_path(params.except(:controller, :action).merge q: term)
end.to_sentence(last_word_connector: ' or ').html_safe %>?
<% end %>
</p>
</div>
<% end %>
<div id="facets" class="col-md-3">
<% unless @articles.size < 1 %>
<div class="categories panel panel-default">
<p class="panel-heading"><%= link_to 'All Sections &rarr;'.html_safe, search_path(params.except(:controller, :action).merge(c: nil))%></p>
<div class="list-group">
<% @articles.response.response['aggregations']['categories']['categories']['buckets'].each do |c| %>
<%=
link_to search_path(params.except(:controller, :action).merge(c: c['key'])),
class: "list-group-item#{' active' if params[:c] == c['key']}" do
c['key'].titleize.html_safe + content_tag(:small, c['doc_count'], class: 'badge').html_safe
end
%>
<% end %>
</div>
</div>
<div class="authors panel panel-default">
<p class="panel-heading"><%= link_to 'All Authors &rarr;'.html_safe, search_path(params.except(:controller, :action).merge(a: nil))%></p>
<div class="list-group">
<% @articles.response.response['aggregations']['authors']['authors']['buckets'].each do |a| %>
<%=
link_to search_path(params.except(:controller, :action).merge(a: a['key'])),
class: "list-group-item#{' active' if params[:a] == a['key']}" do
a['key'].titleize.html_safe + content_tag(:small, a['doc_count'], class: 'badge').html_safe
end
%>
<% end %>
</div>
</div>
<div class="authors panel panel-default">
<p class="panel-heading"><%= link_to 'Any Date &rarr;'.html_safe, search_path(params.except(:controller, :action).merge(w: nil))%></p>
<div class="list-group">
<% @articles.response.response['aggregations']['published']['published']['buckets'].each do |w| %>
<%=
__start = Time.at(w['key']/1000)
__end = __start.end_of_week
__date = __start.to_date.to_s(:iso)
link_to search_path(params.except(:controller, :action).merge(w: __date)),
class: "list-group-item#{' active' if params[:w] == __date}" do
"#{__start.to_date.to_s(:short)} &mdash; #{__end.to_date.to_s(:short)}".html_safe + \
content_tag(:small, w['doc_count'], class: 'badge').html_safe
end
%>
<% end %>
</div>
</div>
<% end %>
</div>
<div class="col-md-9">
<div id="results">
<% @articles.each do |article| %>
<div class="result">
<h3 class="title">
<%= (article.try(:highlight).try(:title) ? article.highlight.title.join.html_safe : article.title) %>
<small class="category"><%= article.categories.to_sentence %></small>
</h3>
<p class="body">
<% if article.try(:highlight).try(:abstract) %>
<%= article.highlight.abstract.join.html_safe %>
<% else %>
<%= article.try(:highlight).try(:content) ? article.highlight.content.join('&hellip;').html_safe : article.abstract %>
<% end %>
</p>
<% if comments = article.try(:highlight) && article.highlight['comments.body'] %>
<p class="comments">
Comments: <%= comments.join('&hellip;').html_safe %>
</p>
<% end %>
<p class="text-muted">
<small>Authors: <%= article.authors.map(&:full_name).to_sentence %></small> |
<small>Published: <%= article.published_on %></small> |
<small>Score: <%= article._score %></small>
</p>
</div>
<% end %>
</div>
<ul class="pager">
<li class="previous"><%= link_to_previous_page @articles, 'Previous Page', params: params.slice(:q, :c, :a, :comments) %></li>
<li class="next"><%= link_to_next_page @articles, 'Next Page', params: params.slice(:q, :c, :a, :comments) %></li>
</ul>
</div>
<div class="footer <%= @articles.size < 1 ? 'col-md-12' : 'col-md-9 col-md-offset-3' %>">
<p><small>Content provided by <a href="http://nytimes.com"><em>The New York Times</em></a>.</small></p>
</div>

View file

@ -0,0 +1,27 @@
# Indexer class for <http://sidekiq.org>
#
# Run me with:
#
# $ bundle exec sidekiq --queue elasticsearch --verbose
#
class Indexer
include Sidekiq::Worker
sidekiq_options queue: 'elasticsearch', retry: false, backtrace: true
Logger = Sidekiq.logger.level == Logger::DEBUG ? Sidekiq.logger : nil
Client = Elasticsearch::Client.new host: (ENV['ELASTICSEARCH_URL'] || 'http://localhost:9200'), logger: Logger
def perform(operation, klass, record_id, options={})
logger.debug [operation, "#{klass}##{record_id} #{options.inspect}"]
case operation.to_s
when /index|update/
record = klass.constantize.find(record_id)
record.__elasticsearch__.client = Client
record.__elasticsearch__.__send__ "#{operation}_document"
when /delete/
Client.delete index: klass.constantize.index_name, type: klass.constantize.document_type, id: record_id
else raise ArgumentError, "Unknown operation '#{operation}'"
end
end
end

View file

@ -0,0 +1,72 @@
h1 {
font-size: 28px !important;
color: #a3a3a3 !important;
text-transform: uppercase;
letter-spacing: -2px;
}
.label-highlight {
background: #f6fbfc !important;
box-shadow: 0px 1px 0px rgba(0,0,0,0.15);
padding: 0.2em 0.4em 0.2em 0.4em !important;
}
h3 .label-highlight {
background: transparent !important;
padding: 0.1em 0.4em 0px 0.4em !important;
border-bottom: 1px solid #999;
box-shadow: 0px 2px 1px rgba(0,0,0,0.15);
border-radius: 0;
}
.comments .label-highlight {
background: #fcfdf0 !important;
}
small.badge {
font-size: 80% !important;
font-weight: normal !important;
display: inline-block;
float: right;
}
form #form-options {
color: #666;
font-size: 95%;
margin-top: 1.5em;
padding: 0 0.25em;
}
form #form-options input {
margin-top: 0.25em;
}
#facets .panel-heading {
margin-bottom: 0;
}
.result {
border-bottom: 1px solid #ccc;
margin: 2em 0 0 0;
padding: 0 0 1em 0;
}
.result:first-child {
margin-top: 0.25em;
}
.result h3.title {
font-family: 'Rokkitt', sans-serif;
margin-top: 0;
}
.result .body {
font-family: Georgia, serif;
}
.result .category {
font-family: 'Rokkitt', sans-serif;
}
.result .comments {
color: #666666;
font-size: 80%;
}

View file

@ -0,0 +1,130 @@
require 'test_helper'
class SearchControllerTest < ActionController::TestCase
setup do
Time.stubs(:now).returns(Time.parse('2015-03-16 10:00:00 UTC'))
Article.delete_all
articles = [
{ title: 'Article One', abstract: 'One', content: 'One', published_on: 1.day.ago, category_title: 'One', author_first_name: 'John', author_last_name: 'Smith' },
{ title: 'Article One Another', abstract: '', content: '', published_on: 2.days.ago, category_title: 'One', author_first_name: 'John', author_last_name: 'Smith' },
{ title: 'Article One Two', abstract: '', content: '', published_on: 10.days.ago, category_title: 'Two', author_first_name: 'Mary', author_last_name: 'Smith' },
{ title: 'Article Two', abstract: '', content: '', published_on: 12.days.ago, category_title: 'Two', author_first_name: 'Mary', author_last_name: 'Smith' },
{ title: 'Article Three', abstract: '', content: '', published_on: 12.days.ago, category_title: 'Three', author_first_name: 'Alice', author_last_name: 'Smith' }
]
articles.each do |a|
article = Article.create! \
title: a[:title],
abstract: a[:abstract],
content: a[:content],
published_on: a[:published_on]
article.categories << Category.find_or_create_by!(title: a[:category_title])
article.authors << Author.find_or_create_by!(first_name: a[:author_first_name], last_name: a[:author_last_name])
article.save!
end
Article.find_by_title('Article Three').comments.create body: 'One'
Sidekiq::Queue.new("elasticsearch").clear
Article.__elasticsearch__.import force: true
Article.__elasticsearch__.refresh_index!
end
test "should return search results" do
get :index, q: 'one'
assert_response :success
assert_equal 3, assigns(:articles).size
end
test "should return search results in comments" do
get :index, q: 'one', comments: 'y'
assert_response :success
assert_equal 4, assigns(:articles).size
end
test "should return highlighted snippets" do
get :index, q: 'one'
assert_response :success
assert_match %r{<em class="label label-highlight">One</em>}, assigns(:articles).first.highlight.title.first
end
test "should return suggestions" do
get :index, q: 'one'
assert_response :success
suggestions = assigns(:articles).response.suggest
assert_equal 'one', suggestions['suggest_title'][0]['text']
end
test "should return aggregations" do
get :index, q: 'one'
assert_response :success
aggregations = assigns(:articles).response.response['aggregations']
assert_equal 2, aggregations['categories']['categories']['buckets'].size
assert_equal 2, aggregations['authors']['authors']['buckets'].size
assert_equal 2, aggregations['published']['published']['buckets'].size
assert_equal 'John Smith', aggregations['authors']['authors']['buckets'][0]['key']
assert_equal 'One', aggregations['categories']['categories']['buckets'][0]['key']
assert_equal '2015-03-02T00:00:00.000Z', aggregations['published']['published']['buckets'][0]['key_as_string']
end
test "should sort on the published date" do
get :index, q: 'one', s: 'published_on'
assert_response :success
assert_equal 3, assigns(:articles).size
assert_equal '2015-03-15', assigns(:articles)[0].published_on
assert_equal '2015-03-14', assigns(:articles)[1].published_on
assert_equal '2015-03-06', assigns(:articles)[2].published_on
end
test "should sort on the published date when no query is provided" do
get :index, q: ''
assert_response :success
assert_equal 5, assigns(:articles).size
assert_equal '2015-03-15', assigns(:articles)[0].published_on
assert_equal '2015-03-14', assigns(:articles)[1].published_on
assert_equal '2015-03-06', assigns(:articles)[2].published_on
end
test "should filter search results and the author and published date facets when user selects a category" do
get :index, q: 'one', c: 'One'
assert_response :success
assert_equal 2, assigns(:articles).size
aggregations = assigns(:articles).response.response['aggregations']
assert_equal 1, aggregations['authors']['authors']['buckets'].size
assert_equal 1, aggregations['published']['published']['buckets'].size
# Do NOT filter the category facet
assert_equal 2, aggregations['categories']['categories']['buckets'].size
end
test "should filter search results and the category and published date facets when user selects a category" do
get :index, q: 'one', a: 'Mary Smith'
assert_response :success
assert_equal 1, assigns(:articles).size
aggregations = assigns(:articles).response.response['aggregations']
assert_equal 1, aggregations['categories']['categories']['buckets'].size
assert_equal 1, aggregations['published']['published']['buckets'].size
# Do NOT filter the authors facet
assert_equal 2, aggregations['authors']['authors']['buckets'].size
end
end

View file

@ -0,0 +1,131 @@
require 'test_helper'
class SearchControllerTest < ActionController::TestCase
setup do
Time.stubs(:now).returns(Time.parse('2015-03-16 10:00:00 UTC'))
Article.delete_all
articles = [
{ title: 'Article One', abstract: 'One', content: 'One', published_on: 1.day.ago, category_title: 'One', author_first_name: 'John', author_last_name: 'Smith' },
{ title: 'Article One Another', abstract: '', content: '', published_on: 2.days.ago, category_title: 'One', author_first_name: 'John', author_last_name: 'Smith' },
{ title: 'Article One Two', abstract: '', content: '', published_on: 10.days.ago, category_title: 'Two', author_first_name: 'Mary', author_last_name: 'Smith' },
{ title: 'Article Two', abstract: '', content: '', published_on: 12.days.ago, category_title: 'Two', author_first_name: 'Mary', author_last_name: 'Smith' },
{ title: 'Article Three', abstract: '', content: '', published_on: 12.days.ago, category_title: 'Three', author_first_name: 'Alice', author_last_name: 'Smith' }
]
articles.each do |a|
article = Article.create! \
title: a[:title],
abstract: a[:abstract],
content: a[:content],
published_on: a[:published_on]
article.categories << Category.find_or_create_by!(title: a[:category_title])
article.authors << Author.find_or_create_by!(first_name: a[:author_first_name], last_name: a[:author_last_name])
article.save!
end
Article.find_by_title('Article Three').comments.create body: 'One'
Sidekiq::Worker.clear_all
Article.__elasticsearch__.import force: true
Article.__elasticsearch__.refresh_index!
end
test "should return search results" do
get :index, q: 'one'
assert_response :success
assert_equal 3, assigns(:articles).size
end
test "should return search results in comments" do
get :index, q: 'one', comments: 'y'
assert_response :success
assert_equal 4, assigns(:articles).size
end
test "should return highlighted snippets" do
get :index, q: 'one'
assert_response :success
assert_match %r{<em class="label label-highlight">One</em>}, assigns(:articles).first.highlight.title.first
end
test "should return suggestions" do
get :index, q: 'one'
assert_response :success
suggestions = assigns(:articles).response.suggest
assert_equal 'one', suggestions['suggest_title'][0]['text']
end
test "should return facets" do
get :index, q: 'one'
assert_response :success
aggregations = assigns(:articles).response.response['aggregations']
assert_equal 2, aggregations['categories']['categories']['buckets'].size
assert_equal 2, aggregations['authors']['authors']['buckets'].size
assert_equal 2, aggregations['published']['published']['buckets'].size
assert_equal 'One', aggregations['categories']['categories']['buckets'][0]['key']
assert_equal 'John Smith', aggregations['authors']['authors']['buckets'][0]['key']
assert_equal 1425254400000, aggregations['published']['published']['buckets'][0]['key']
end
test "should sort on the published date" do
get :index, q: 'one', s: 'published_on'
assert_response :success
assert_equal 3, assigns(:articles).size
assert_equal '2015-03-15', assigns(:articles)[0].published_on
assert_equal '2015-03-14', assigns(:articles)[1].published_on
assert_equal '2015-03-06', assigns(:articles)[2].published_on
end
test "should sort on the published date when no query is provided" do
get :index, q: ''
assert_response :success
assert_equal 5, assigns(:articles).size
assert_equal '2015-03-15', assigns(:articles)[0].published_on
assert_equal '2015-03-14', assigns(:articles)[1].published_on
assert_equal '2015-03-06', assigns(:articles)[2].published_on
end
test "should filter search results and the author and published date facets when user selects a category" do
get :index, q: 'one', c: 'One'
assert_response :success
assert_equal 2, assigns(:articles).size
aggregations = assigns(:articles).response.response['aggregations']
assert_equal 1, aggregations['authors']['authors']['buckets'].size
assert_equal 1, aggregations['published']['published']['buckets'].size
# Do NOT filter the category facet
assert_equal 2, aggregations['categories']['categories']['buckets'].size
end
test "should filter search results and the category and published date facets when user selects a category" do
get :index, q: 'one', a: 'Mary Smith'
assert_response :success
assert_equal 1, assigns(:articles).size
aggregations = assigns(:articles).response.response['aggregations']
assert_equal 1, aggregations['categories']['categories']['buckets'].size
assert_equal 1, aggregations['published']['published']['buckets'].size
# Do NOT filter the authors facet
assert_equal 2, aggregations['authors']['authors']['buckets'].size
end
end

View file

@ -0,0 +1,217 @@
module Searchable
extend ActiveSupport::Concern
included do
include Elasticsearch::Model
# Customize the index name
#
index_name [Rails.application.engine_name, Rails.env].join('_')
# Set up index configuration and mapping
#
settings index: { number_of_shards: 1, number_of_replicas: 0 } do
mapping do
indexes :title, type: 'multi_field' do
indexes :title, analyzer: 'snowball'
indexes :tokenized, analyzer: 'simple'
end
indexes :content, type: 'multi_field' do
indexes :content, analyzer: 'snowball'
indexes :tokenized, analyzer: 'simple'
end
indexes :published_on, type: 'date'
indexes :authors do
indexes :full_name, type: 'multi_field' do
indexes :full_name
indexes :raw, analyzer: 'keyword'
end
end
indexes :categories, analyzer: 'keyword'
indexes :comments, type: 'nested' do
indexes :body, analyzer: 'snowball'
indexes :stars
indexes :pick
indexes :user, analyzer: 'keyword'
indexes :user_location, type: 'multi_field' do
indexes :user_location
indexes :raw, analyzer: 'keyword'
end
end
end
end
# Set up callbacks for updating the index on model changes
#
after_commit lambda { Indexer.perform_async(:index, self.class.to_s, self.id) }, on: :create
after_commit lambda { Indexer.perform_async(:update, self.class.to_s, self.id) }, on: :update
after_commit lambda { Indexer.perform_async(:delete, self.class.to_s, self.id) }, on: :destroy
after_touch lambda { Indexer.perform_async(:update, self.class.to_s, self.id) }
# Customize the JSON serialization for Elasticsearch
#
def as_indexed_json(options={})
hash = self.as_json(
include: { authors: { methods: [:full_name], only: [:full_name] },
comments: { only: [:body, :stars, :pick, :user, :user_location] }
})
hash['categories'] = self.categories.map(&:title)
hash
end
# Return documents matching the user's query, include highlights and aggregations in response,
# and implement a "cross" faceted navigation
#
# @param q [String] The user query
# @return [Elasticsearch::Model::Response::Response]
#
def self.search(q, options={})
@search_definition = Elasticsearch::DSL::Search.search do
query do
# If a user query is present...
#
unless q.blank?
bool do
# ... search in `title`, `abstract` and `content`, boosting `title`
#
should do
multi_match do
query q
fields ['title^10', 'abstract^2', 'content']
operator 'and'
end
end
# ... search in comment body if user checked the comments checkbox
#
if q.present? && options[:comments]
should do
nested do
path :comments
query do
multi_match do
query q
fields 'body'
operator 'and'
end
end
end
end
end
end
# ... otherwise, just return all articles
else
match_all
end
end
# Filter the search results based on user selection
#
post_filter do
bool do
must { term categories: options[:category] } if options[:category]
must { match_all } if options.keys.none? { |k| [:c, :a, :w].include? k }
must { term 'authors.full_name.raw' => options[:author] } if options[:author]
must { range published_on: { gte: options[:published_week], lte: "#{options[:published_week]}||+1w" } } if options[:published_week]
end
end
# Return top categories for faceted navigation
#
aggregation :categories do
# Filter the aggregation with any selected `author` and `published_week`
#
f = Elasticsearch::DSL::Search::Filters::Bool.new
f.must { match_all }
f.must { term 'authors.full_name.raw' => options[:author] } if options[:author]
f.must { range published_on: { gte: options[:published_week], lte: "#{options[:published_week]}||+1w" } } if options[:published_week]
filter f.to_hash do
aggregation :categories do
terms field: 'categories'
end
end
end
# Return top authors for faceted navigation
#
aggregation :authors do
# Filter the aggregation with any selected `category` and `published_week`
#
f = Elasticsearch::DSL::Search::Filters::Bool.new
f.must { match_all }
f.must { term categories: options[:category] } if options[:category]
f.must { range published_on: { gte: options[:published_week], lte: "#{options[:published_week]}||+1w" } } if options[:published_week]
filter f do
aggregation :authors do
terms field: 'authors.full_name.raw'
end
end
end
# Return the published date ranges for faceted navigation
#
aggregation :published do
# Filter the aggregation with any selected `author` and `category`
#
f = Elasticsearch::DSL::Search::Filters::Bool.new
f.must { match_all }
f.must { term 'authors.full_name.raw' => options[:author] } if options[:author]
f.must { term categories: options[:category] } if options[:category]
filter f do
aggregation :published do
date_histogram do
field 'published_on'
interval 'week'
end
end
end
end
# Highlight the snippets in results
#
highlight do
fields title: { number_of_fragments: 0 },
abstract: { number_of_fragments: 0 },
content: { fragment_size: 50 }
field 'comments.body', fragment_size: 50 if q.present? && options[:comments]
pre_tags '<em class="label label-highlight">'
post_tags '</em>'
end
case
# By default, sort by relevance, but when a specific sort option is present, use it ...
#
when options[:sort]
sort options[:sort].to_sym => 'desc'
track_scores true
#
# ... when there's no user query, sort on published date
#
when q.blank?
sort published_on: 'desc'
end
# Return suggestions unless there's no query from the user
unless q.blank?
suggest :suggest_title, text: q, term: { field: 'title.tokenized', suggest_mode: 'always' }
suggest :suggest_body, text: q, term: { field: 'content.tokenized', suggest_mode: 'always' }
end
end
__elasticsearch__.search(@search_definition)
end
end
end

View file

@ -0,0 +1,206 @@
module Searchable
extend ActiveSupport::Concern
included do
include Elasticsearch::Model
# Customize the index name
#
index_name [Rails.application.engine_name, Rails.env].join('_')
# Set up index configuration and mapping
#
settings index: { number_of_shards: 1, number_of_replicas: 0 } do
mapping do
indexes :title, type: 'multi_field' do
indexes :title, analyzer: 'snowball'
indexes :tokenized, analyzer: 'simple'
end
indexes :content, type: 'multi_field' do
indexes :content, analyzer: 'snowball'
indexes :tokenized, analyzer: 'simple'
end
indexes :published_on, type: 'date'
indexes :authors do
indexes :full_name, type: 'multi_field' do
indexes :full_name
indexes :raw, analyzer: 'keyword'
end
end
indexes :categories, analyzer: 'keyword'
indexes :comments, type: 'nested' do
indexes :body, analyzer: 'snowball'
indexes :stars
indexes :pick
indexes :user, analyzer: 'keyword'
indexes :user_location, type: 'multi_field' do
indexes :user_location
indexes :raw, analyzer: 'keyword'
end
end
end
end
# Set up callbacks for updating the index on model changes
#
after_commit lambda { Indexer.perform_async(:index, self.class.to_s, self.id) }, on: :create
after_commit lambda { Indexer.perform_async(:update, self.class.to_s, self.id) }, on: :update
after_commit lambda { Indexer.perform_async(:delete, self.class.to_s, self.id) }, on: :destroy
after_touch lambda { Indexer.perform_async(:update, self.class.to_s, self.id) }
# Customize the JSON serialization for Elasticsearch
#
def as_indexed_json(options={})
hash = self.as_json(
include: { authors: { methods: [:full_name], only: [:full_name] },
comments: { only: [:body, :stars, :pick, :user, :user_location] }
})
hash['categories'] = self.categories.map(&:title)
hash
end
# Search in title and content fields for `query`, include highlights in response
#
# @param query [String] The user query
# @return [Elasticsearch::Model::Response::Response]
#
def self.search(query, options={})
# Prefill and set the filters (top-level `post_filter` and aggregation `filter` elements)
#
__set_filters = lambda do |key, f|
@search_definition[:post_filter][:and] ||= []
@search_definition[:post_filter][:and] |= [f]
@search_definition[:aggregations][key.to_sym][:filter][:bool][:must] ||= []
@search_definition[:aggregations][key.to_sym][:filter][:bool][:must] |= [f]
end
@search_definition = {
query: {},
highlight: {
pre_tags: ['<em class="label label-highlight">'],
post_tags: ['</em>'],
fields: {
title: { number_of_fragments: 0 },
abstract: { number_of_fragments: 0 },
content: { fragment_size: 50 }
}
},
post_filter: {},
aggregations: {
categories: {
filter: { bool: { must: [ match_all: {} ] } },
aggregations: { categories: { terms: { field: 'categories' } } }
},
authors: {
filter: { bool: { must: [ match_all: {} ] } },
aggregations: { authors: { terms: { field: 'authors.full_name.raw' } } }
},
published: {
filter: { bool: { must: [ match_all: {} ] } },
aggregations: {
published: { date_histogram: { field: 'published_on', interval: 'week' } }
}
}
}
}
unless query.blank?
@search_definition[:query] = {
bool: {
should: [
{ multi_match: {
query: query,
fields: ['title^10', 'abstract^2', 'content'],
operator: 'and'
}
}
]
}
}
else
@search_definition[:query] = { match_all: {} }
@search_definition[:sort] = { published_on: 'desc' }
end
if options[:category]
f = { term: { categories: options[:category] } }
__set_filters.(:authors, f)
__set_filters.(:published, f)
end
if options[:author]
f = { term: { 'authors.full_name.raw' => options[:author] } }
__set_filters.(:categories, f)
__set_filters.(:published, f)
end
if options[:published_week]
f = {
range: {
published_on: {
gte: options[:published_week],
lte: "#{options[:published_week]}||+1w"
}
}
}
__set_filters.(:categories, f)
__set_filters.(:authors, f)
end
if query.present? && options[:comments]
@search_definition[:query][:bool][:should] ||= []
@search_definition[:query][:bool][:should] << {
nested: {
path: 'comments',
query: {
multi_match: {
query: query,
fields: ['comments.body'],
operator: 'and'
}
}
}
}
@search_definition[:highlight][:fields].update 'comments.body' => { fragment_size: 50 }
end
if options[:sort]
@search_definition[:sort] = { options[:sort] => 'desc' }
@search_definition[:track_scores] = true
end
unless query.blank?
@search_definition[:suggest] = {
text: query,
suggest_title: {
term: {
field: 'title.tokenized',
suggest_mode: 'always'
}
},
suggest_body: {
term: {
field: 'content.tokenized',
suggest_mode: 'always'
}
}
}
end
__elasticsearch__.search(@search_definition)
end
end
end

View file

@ -0,0 +1,57 @@
require 'zlib'
require 'yaml'
Zlib::GzipReader.open(File.expand_path('../articles.yml.gz', __FILE__)) do |gzip|
puts "Reading articles from gzipped YAML..."
@documents = YAML.load_documents(gzip.read)
end
# Truncate the default ActiveRecord logger output
ActiveRecord::Base.logger = ActiveSupport::Logger.new(STDERR)
ActiveRecord::Base.logger.instance_eval do
@formatter = lambda do |s, d, p, message|
message
.gsub(/\[("content", ".*?")\]/m) { |match| match[0..100] + '..."]' }
.gsub(/\[("body", ".*?")\]/m ) { |match| match[0..100] + '..."]' }
.strip
.concat("\n")
end
end
# Reduce verbosity and truncate the request body of Elasticsearch logger
Article.__elasticsearch__.client.transport.tracer.level = Logger::INFO
Article.__elasticsearch__.client.transport.tracer.formatter = lambda do |s, d, p, message|
"\n\n" + (message.size > 105 ? message[0..105].concat("...}'") : message) + "\n\n"
end
# Skip model callbacks
%w| _touch_callbacks
_commit_callbacks
after_add_for_categories
after_add_for_authorships
after_add_for_authors
after_add_for_comments |.each do |c|
Article.class.__send__ :define_method, c do; []; end
end
@documents.each do |document|
article = Article.create! document.slice(:title, :abstract, :content, :url, :shares, :published_on)
article.categories = document[:categories].map do |d|
Category.find_or_create_by! title: d
end
article.authors = document[:authors].map do |d|
first_name, last_name = d.split(' ').compact.map(&:strip)
Author.find_or_create_by! first_name: first_name, last_name: last_name
end
document[:comments].each { |d| article.comments.create! d }
article.save!
end
# Remove any jobs from the "elasticsearch" Sidekiq queue
#
require 'sidekiq/api'
Sidekiq::Queue.new("elasticsearch").clear

View file

@ -0,0 +1,64 @@
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'
require 'rails/version'
require 'active_record'
require 'active_model'
require 'elasticsearch/model'
require 'elasticsearch/rails'
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| "#{m.ansi(:faint, :cyan)}\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

View file

@ -0,0 +1,61 @@
require 'test_helper'
require 'rails/railtie'
require 'active_support/log_subscriber/test_helper'
require 'elasticsearch/rails/instrumentation'
class Elasticsearch::Rails::InstrumentationTest < Test::Unit::TestCase
include ActiveSupport::LogSubscriber::TestHelper
context "ActiveSupport::Instrumentation integration" do
class ::DummyInstrumentationModel
extend Elasticsearch::Model::Searching::ClassMethods
def self.index_name; 'foo'; end
def self.document_type; 'bar'; end
end
RESPONSE = { 'took' => '5ms', 'hits' => { 'total' => 123, 'max_score' => 456, 'hits' => [] } }
setup do
@search = Elasticsearch::Model::Searching::SearchRequest.new ::DummyInstrumentationModel, '*'
@client = stub('client', search: RESPONSE)
DummyInstrumentationModel.stubs(:client).returns(@client)
Elasticsearch::Rails::Instrumentation::Railtie.run_initializers
end
should "wrap SearchRequest#execute! with instrumentation" do
s = Elasticsearch::Model::Searching::SearchRequest.new ::DummyInstrumentationModel, 'foo'
assert_respond_to s, :execute_without_instrumentation!
assert_respond_to s, :execute_with_instrumentation!
end
should "publish the notification" do
@query = { query: { match: { foo: 'bar' } } }
ActiveSupport::Notifications.expects(:instrument).with do |name, payload|
assert_equal "search.elasticsearch", name
assert_equal 'DummyInstrumentationModel', payload[:klass]
assert_equal @query, payload[:search][:body]
true
end
s = ::DummyInstrumentationModel.search @query
s.response
end
should "print the debug information to the Rails log" do
s = ::DummyInstrumentationModel.search query: { match: { moo: 'bam' } }
s.response
logged = @logger.logged(:debug).first
assert_not_nil logged
assert_match /DummyInstrumentationModel Search \(\d+\.\d+ms\)/, logged
assert_match /body\: \{query\: \{match\: \{moo\: "bam"\}\}\}\}/, logged
end unless defined?(RUBY_VERSION) && RUBY_VERSION > '2.2'
end
end

View file

@ -0,0 +1,21 @@
require 'test_helper'
require 'rails/railtie'
require 'action_pack'
require 'lograge'
require 'elasticsearch/rails/lograge'
class Elasticsearch::Rails::LogrageTest < Test::Unit::TestCase
context "Lograge integration" do
setup do
Elasticsearch::Rails::Lograge::Railtie.run_initializers
end
should "customize the Lograge configuration" do
assert_not_nil Elasticsearch::Rails::Lograge::Railtie.initializers
.select { |i| i.name == 'elasticsearch.lograge' }
.first
end
end
end