Update upstream source from tag 'upstream/12.3.8'
Update to upstream version '12.3.8'
with Debian dir 20f45ac713
This commit is contained in:
commit
f2bfe580fb
35 changed files with 3183 additions and 0 deletions
17
elasticsearch-rails/.gitignore
vendored
Normal file
17
elasticsearch-rails/.gitignore
vendored
Normal 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
|
44
elasticsearch-rails/CHANGELOG.md
Normal file
44
elasticsearch-rails/CHANGELOG.md
Normal 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)
|
9
elasticsearch-rails/Gemfile
Normal file
9
elasticsearch-rails/Gemfile
Normal 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
|
13
elasticsearch-rails/LICENSE.txt
Normal file
13
elasticsearch-rails/LICENSE.txt
Normal 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.
|
132
elasticsearch-rails/README.md
Normal file
132
elasticsearch-rails/README.md
Normal 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.
|
53
elasticsearch-rails/Rakefile
Normal file
53
elasticsearch-rails/Rakefile
Normal 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
|
52
elasticsearch-rails/elasticsearch-rails.gemspec
Normal file
52
elasticsearch-rails/elasticsearch-rails.gemspec
Normal 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
|
7
elasticsearch-rails/lib/elasticsearch/rails.rb
Normal file
7
elasticsearch-rails/lib/elasticsearch/rails.rb
Normal file
|
@ -0,0 +1,7 @@
|
|||
require "elasticsearch/rails/version"
|
||||
|
||||
module Elasticsearch
|
||||
module Rails
|
||||
# Your code goes here...
|
||||
end
|
||||
end
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
44
elasticsearch-rails/lib/elasticsearch/rails/lograge.rb
Normal file
44
elasticsearch-rails/lib/elasticsearch/rails/lograge.rb
Normal 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
|
112
elasticsearch-rails/lib/elasticsearch/rails/tasks/import.rb
Normal file
112
elasticsearch-rails/lib/elasticsearch/rails/tasks/import.rb
Normal 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
|
5
elasticsearch-rails/lib/elasticsearch/rails/version.rb
Normal file
5
elasticsearch-rails/lib/elasticsearch/rails/version.rb
Normal file
|
@ -0,0 +1,5 @@
|
|||
module Elasticsearch
|
||||
module Rails
|
||||
VERSION = "0.1.9"
|
||||
end
|
||||
end
|
335
elasticsearch-rails/lib/rails/templates/01-basic.rb
Normal file
335
elasticsearch-rails/lib/rails/templates/01-basic.rb
Normal 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
|
311
elasticsearch-rails/lib/rails/templates/02-pretty.rb
Normal file
311
elasticsearch-rails/lib/rails/templates/02-pretty.rb
Normal 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('…').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
|
349
elasticsearch-rails/lib/rails/templates/03-expert.rb
Normal file
349
elasticsearch-rails/lib/rails/templates/03-expert.rb
Normal 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
|
131
elasticsearch-rails/lib/rails/templates/04-dsl.rb
Normal file
131
elasticsearch-rails/lib/rails/templates/04-dsl.rb
Normal 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
|
77
elasticsearch-rails/lib/rails/templates/05-settings-files.rb
Normal file
77
elasticsearch-rails/lib/rails/templates/05-settings-files.rb
Normal 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
|
BIN
elasticsearch-rails/lib/rails/templates/articles.yml.gz
Normal file
BIN
elasticsearch-rails/lib/rails/templates/articles.yml.gz
Normal file
Binary file not shown.
|
@ -0,0 +1 @@
|
|||
{ "number_of_shards": 1, "number_of_replicas": 0 }
|
160
elasticsearch-rails/lib/rails/templates/index.html.dsl.erb
Normal file
160
elasticsearch-rails/lib/rails/templates/index.html.dsl.erb
Normal 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 →'.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 →'.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 →'.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)} — #{__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('…').html_safe : article.abstract %>
|
||||
<% end %>
|
||||
</p>
|
||||
|
||||
<% if comments = article.try(:highlight) && article.highlight['comments.body'] %>
|
||||
<p class="comments">
|
||||
Comments: <%= comments.join('…').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>
|
160
elasticsearch-rails/lib/rails/templates/index.html.erb
Normal file
160
elasticsearch-rails/lib/rails/templates/index.html.erb
Normal 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 →'.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 →'.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 →'.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)} — #{__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('…').html_safe : article.abstract %>
|
||||
<% end %>
|
||||
</p>
|
||||
|
||||
<% if comments = article.try(:highlight) && article.highlight['comments.body'] %>
|
||||
<p class="comments">
|
||||
Comments: <%= comments.join('…').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>
|
27
elasticsearch-rails/lib/rails/templates/indexer.rb
Normal file
27
elasticsearch-rails/lib/rails/templates/indexer.rb
Normal 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
|
72
elasticsearch-rails/lib/rails/templates/search.css
Normal file
72
elasticsearch-rails/lib/rails/templates/search.css
Normal 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%;
|
||||
}
|
|
@ -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
|
|
@ -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
|
217
elasticsearch-rails/lib/rails/templates/searchable.dsl.rb
Normal file
217
elasticsearch-rails/lib/rails/templates/searchable.dsl.rb
Normal 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
|
206
elasticsearch-rails/lib/rails/templates/searchable.rb
Normal file
206
elasticsearch-rails/lib/rails/templates/searchable.rb
Normal 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
|
57
elasticsearch-rails/lib/rails/templates/seeds.rb
Normal file
57
elasticsearch-rails/lib/rails/templates/seeds.rb
Normal 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
|
64
elasticsearch-rails/test/test_helper.rb
Normal file
64
elasticsearch-rails/test/test_helper.rb
Normal 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
|
|
@ -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
|
|
@ -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
|
Loading…
Reference in a new issue