tire 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- data/.gitignore +9 -0
- data/Gemfile +4 -0
- data/MIT-LICENSE +20 -0
- data/README.markdown +435 -0
- data/Rakefile +75 -0
- data/examples/dsl.rb +73 -0
- data/examples/rails-application-template.rb +144 -0
- data/examples/tire-dsl.rb +617 -0
- data/lib/tire.rb +35 -0
- data/lib/tire/client.rb +40 -0
- data/lib/tire/configuration.rb +29 -0
- data/lib/tire/dsl.rb +33 -0
- data/lib/tire/index.rb +209 -0
- data/lib/tire/logger.rb +60 -0
- data/lib/tire/model/callbacks.rb +23 -0
- data/lib/tire/model/import.rb +18 -0
- data/lib/tire/model/indexing.rb +50 -0
- data/lib/tire/model/naming.rb +30 -0
- data/lib/tire/model/persistence.rb +34 -0
- data/lib/tire/model/persistence/attributes.rb +60 -0
- data/lib/tire/model/persistence/finders.rb +61 -0
- data/lib/tire/model/persistence/storage.rb +75 -0
- data/lib/tire/model/search.rb +97 -0
- data/lib/tire/results/collection.rb +56 -0
- data/lib/tire/results/item.rb +39 -0
- data/lib/tire/results/pagination.rb +30 -0
- data/lib/tire/rubyext/hash.rb +3 -0
- data/lib/tire/rubyext/symbol.rb +11 -0
- data/lib/tire/search.rb +117 -0
- data/lib/tire/search/facet.rb +41 -0
- data/lib/tire/search/filter.rb +28 -0
- data/lib/tire/search/highlight.rb +37 -0
- data/lib/tire/search/query.rb +42 -0
- data/lib/tire/search/sort.rb +29 -0
- data/lib/tire/tasks.rb +88 -0
- data/lib/tire/version.rb +3 -0
- data/test/fixtures/articles/1.json +1 -0
- data/test/fixtures/articles/2.json +1 -0
- data/test/fixtures/articles/3.json +1 -0
- data/test/fixtures/articles/4.json +1 -0
- data/test/fixtures/articles/5.json +1 -0
- data/test/integration/active_model_searchable_test.rb +80 -0
- data/test/integration/active_record_searchable_test.rb +193 -0
- data/test/integration/facets_test.rb +65 -0
- data/test/integration/filters_test.rb +46 -0
- data/test/integration/highlight_test.rb +52 -0
- data/test/integration/index_mapping_test.rb +44 -0
- data/test/integration/index_store_test.rb +68 -0
- data/test/integration/persistent_model_test.rb +35 -0
- data/test/integration/query_string_test.rb +43 -0
- data/test/integration/results_test.rb +28 -0
- data/test/integration/sort_test.rb +36 -0
- data/test/models/active_model_article.rb +31 -0
- data/test/models/active_model_article_with_callbacks.rb +49 -0
- data/test/models/active_model_article_with_custom_index_name.rb +5 -0
- data/test/models/active_record_article.rb +12 -0
- data/test/models/article.rb +15 -0
- data/test/models/persistent_article.rb +11 -0
- data/test/models/persistent_articles_with_custom_index_name.rb +10 -0
- data/test/models/supermodel_article.rb +22 -0
- data/test/models/validated_model.rb +11 -0
- data/test/test_helper.rb +52 -0
- data/test/unit/active_model_lint_test.rb +17 -0
- data/test/unit/client_test.rb +43 -0
- data/test/unit/configuration_test.rb +71 -0
- data/test/unit/index_test.rb +390 -0
- data/test/unit/logger_test.rb +114 -0
- data/test/unit/model_callbacks_test.rb +90 -0
- data/test/unit/model_import_test.rb +71 -0
- data/test/unit/model_persistence_test.rb +400 -0
- data/test/unit/model_search_test.rb +289 -0
- data/test/unit/results_collection_test.rb +131 -0
- data/test/unit/results_item_test.rb +59 -0
- data/test/unit/rubyext_hash_test.rb +19 -0
- data/test/unit/search_facet_test.rb +69 -0
- data/test/unit/search_filter_test.rb +36 -0
- data/test/unit/search_highlight_test.rb +46 -0
- data/test/unit/search_query_test.rb +55 -0
- data/test/unit/search_sort_test.rb +50 -0
- data/test/unit/search_test.rb +204 -0
- data/test/unit/tire_test.rb +55 -0
- data/tire.gemspec +54 -0
- metadata +372 -0
data/Rakefile
ADDED
@@ -0,0 +1,75 @@
|
|
1
|
+
require 'bundler'
|
2
|
+
Bundler::GemHelper.install_tasks
|
3
|
+
|
4
|
+
task :default => :test
|
5
|
+
|
6
|
+
require 'rake/testtask'
|
7
|
+
Rake::TestTask.new(:test) do |test|
|
8
|
+
test.libs << 'lib' << 'test'
|
9
|
+
test.test_files = FileList['test/unit/*_test.rb', 'test/integration/*_test.rb']
|
10
|
+
test.verbose = true
|
11
|
+
# test.warning = true
|
12
|
+
end
|
13
|
+
|
14
|
+
namespace :test do
|
15
|
+
Rake::TestTask.new(:unit) do |test|
|
16
|
+
test.libs << 'lib' << 'test'
|
17
|
+
test.pattern = 'test/unit/*_test.rb'
|
18
|
+
test.verbose = true
|
19
|
+
end
|
20
|
+
Rake::TestTask.new(:integration) do |test|
|
21
|
+
test.libs << 'lib' << 'test'
|
22
|
+
test.pattern = 'test/integration/*_test.rb'
|
23
|
+
test.verbose = true
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
# Generate documentation
|
28
|
+
begin
|
29
|
+
require 'sdoc'
|
30
|
+
rescue LoadError
|
31
|
+
end
|
32
|
+
require 'rake/rdoctask'
|
33
|
+
Rake::RDocTask.new do |rdoc|
|
34
|
+
rdoc.rdoc_dir = 'rdoc'
|
35
|
+
rdoc.title = "Tire"
|
36
|
+
rdoc.rdoc_files.include('README.rdoc')
|
37
|
+
rdoc.rdoc_files.include('lib/**/*.rb')
|
38
|
+
end
|
39
|
+
|
40
|
+
# Generate coverage reports
|
41
|
+
begin
|
42
|
+
require 'rcov/rcovtask'
|
43
|
+
Rcov::RcovTask.new do |test|
|
44
|
+
test.libs << 'test'
|
45
|
+
test.rcov_opts = ['--exclude', 'gems/*']
|
46
|
+
test.pattern = 'test/**/*_test.rb'
|
47
|
+
test.verbose = true
|
48
|
+
end
|
49
|
+
rescue LoadError
|
50
|
+
task :rcov do
|
51
|
+
abort "RCov is not available. In order to run rcov, you must: sudo gem install rcov"
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
namespace :web do
|
56
|
+
|
57
|
+
desc "Update the Github website"
|
58
|
+
task :update => :generate do
|
59
|
+
current_branch = `git branch --no-color`.split("\n").select { |line| line =~ /^\* / }.to_s.gsub(/\* (.*)/, '\1')
|
60
|
+
(puts "Unable to determine current branch"; exit(1) ) unless current_branch
|
61
|
+
system "git stash save && git checkout web"
|
62
|
+
system "cp examples/tire-dsl.html index.html"
|
63
|
+
system "git add index.html && git co -m 'Updated Tire website'"
|
64
|
+
system "git push origin web:gh-pages -f"
|
65
|
+
system "git checkout #{current_branch} && git stash pop"
|
66
|
+
end
|
67
|
+
|
68
|
+
desc "Generate the Rocco documentation page"
|
69
|
+
task :generate do
|
70
|
+
system "rocco examples/tire-dsl.rb"
|
71
|
+
html = File.read('examples/tire-dsl.html').gsub!(/tire\-dsl\.rb/, 'tire.rb')
|
72
|
+
File.open('examples/tire-dsl.html', 'w') { |f| f.write html }
|
73
|
+
system "open examples/tire-dsl.html"
|
74
|
+
end
|
75
|
+
end
|
data/examples/dsl.rb
ADDED
@@ -0,0 +1,73 @@
|
|
1
|
+
$LOAD_PATH.unshift File.expand_path('../../lib', __FILE__)
|
2
|
+
|
3
|
+
require 'rubygems'
|
4
|
+
require 'tire'
|
5
|
+
|
6
|
+
extend Tire::DSL
|
7
|
+
|
8
|
+
configure do
|
9
|
+
url "http://localhost:9200"
|
10
|
+
end
|
11
|
+
|
12
|
+
index 'articles' do
|
13
|
+
delete
|
14
|
+
create
|
15
|
+
|
16
|
+
puts "Documents:", "-"*80
|
17
|
+
[
|
18
|
+
{ :title => 'One', :tags => ['ruby'] },
|
19
|
+
{ :title => 'Two', :tags => ['ruby', 'python'] },
|
20
|
+
{ :title => 'Three', :tags => ['java'] },
|
21
|
+
{ :title => 'Four', :tags => ['ruby', 'php'] }
|
22
|
+
].each do |article|
|
23
|
+
puts "Indexing article: #{article.to_json}"
|
24
|
+
store article
|
25
|
+
end
|
26
|
+
|
27
|
+
refresh
|
28
|
+
end
|
29
|
+
|
30
|
+
s = search 'articles' do
|
31
|
+
query do
|
32
|
+
string 'T*'
|
33
|
+
end
|
34
|
+
|
35
|
+
filter :terms, :tags => ['ruby']
|
36
|
+
|
37
|
+
sort do
|
38
|
+
title 'desc'
|
39
|
+
end
|
40
|
+
|
41
|
+
facet 'global-tags' do
|
42
|
+
terms :tags, :global => true
|
43
|
+
end
|
44
|
+
|
45
|
+
facet 'current-tags' do
|
46
|
+
terms :tags
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
puts "", "Query:", "-"*80
|
51
|
+
puts s.to_json
|
52
|
+
|
53
|
+
puts "", "Raw JSON result:", "-"*80
|
54
|
+
puts JSON.pretty_generate(s.response)
|
55
|
+
|
56
|
+
puts "", "Try the query in Curl:", "-"*80
|
57
|
+
puts s.to_curl
|
58
|
+
|
59
|
+
puts "", "Results:", "-"*80
|
60
|
+
s.results.each_with_index do |document, i|
|
61
|
+
puts "#{i+1}. #{ document.title.ljust(10) } [id] #{document._id}"
|
62
|
+
end
|
63
|
+
|
64
|
+
puts "", "Facets: tags distribution across the whole database:", "-"*80
|
65
|
+
s.results.facets['global-tags']['terms'].each do |f|
|
66
|
+
puts "#{f['term'].ljust(13)} #{f['count']}"
|
67
|
+
end
|
68
|
+
|
69
|
+
puts "", "Facets: tags distribution for the current query ",
|
70
|
+
"(Notice that 'java' is included, because of the filter)", "-"*80
|
71
|
+
s.results.facets['current-tags']['terms'].each do |f|
|
72
|
+
puts "#{f['term'].ljust(13)} #{f['count']}"
|
73
|
+
end
|
@@ -0,0 +1,144 @@
|
|
1
|
+
# ===================================================================================================================
|
2
|
+
# Template for generating a no-frills Rails application with support for ElasticSearch full-text search via Tire
|
3
|
+
# ===================================================================================================================
|
4
|
+
#
|
5
|
+
# This file creates a basic Rails application with support for ElasticSearch full-text via the Tire gem
|
6
|
+
#
|
7
|
+
# Run it like this:
|
8
|
+
#
|
9
|
+
# rails new searchapp -m https://github.com/karmi/tire/raw/master/examples/rails-application-template.rb
|
10
|
+
#
|
11
|
+
|
12
|
+
run "rm public/index.html"
|
13
|
+
run "rm public/images/rails.png"
|
14
|
+
run "touch tmp/.gitignore log/.gitignore vendor/.gitignore"
|
15
|
+
|
16
|
+
git :init
|
17
|
+
git :add => '.'
|
18
|
+
git :commit => "-m 'Initial commit: Clean application'"
|
19
|
+
|
20
|
+
puts
|
21
|
+
say_status "Rubygems", "Adding Rubygems into Gemfile...\n", :yellow
|
22
|
+
puts '-'*80, ''
|
23
|
+
|
24
|
+
gem 'tire', :git => 'https://github.com/karmi/tire.git', :branch => 'activemodel'
|
25
|
+
gem 'will_paginate', '~>3.0.pre'
|
26
|
+
git :add => '.'
|
27
|
+
git :commit => "-m 'Added gems'"
|
28
|
+
|
29
|
+
puts
|
30
|
+
say_status "Rubygems", "Installing Rubygems...", :yellow
|
31
|
+
|
32
|
+
puts
|
33
|
+
puts "********************************************************************************"
|
34
|
+
puts " Running `bundle install`. Let's watch a movie!"
|
35
|
+
puts "********************************************************************************", ""
|
36
|
+
|
37
|
+
run "bundle install"
|
38
|
+
|
39
|
+
puts
|
40
|
+
say_status "Model", "Adding search support into the Article model...", :yellow
|
41
|
+
puts '-'*80, ''
|
42
|
+
|
43
|
+
generate :scaffold, "Article title:string content:text published_on:date"
|
44
|
+
route "root :to => 'articles#index'"
|
45
|
+
rake "db:migrate"
|
46
|
+
|
47
|
+
git :add => '.'
|
48
|
+
git :commit => "-m 'Added the Article resource'"
|
49
|
+
|
50
|
+
run "rm -f app/models/article.rb"
|
51
|
+
file 'app/models/article.rb', <<-CODE
|
52
|
+
class Article < ActiveRecord::Base
|
53
|
+
include Tire::Model::Search
|
54
|
+
include Tire::Model::Callbacks
|
55
|
+
end
|
56
|
+
CODE
|
57
|
+
|
58
|
+
initializer 'tire.rb', <<-CODE
|
59
|
+
Tire.configure do
|
60
|
+
logger STDERR
|
61
|
+
end
|
62
|
+
CODE
|
63
|
+
|
64
|
+
git :commit => "-a -m 'Added Tire support into the Article class and an initializer'"
|
65
|
+
|
66
|
+
puts
|
67
|
+
say_status "Controller", "Adding controller action, route, and neccessary HTML for search...", :yellow
|
68
|
+
puts '-'*80, ''
|
69
|
+
|
70
|
+
gsub_file 'app/controllers/articles_controller.rb', %r{# GET /articles/1$}, <<-CODE
|
71
|
+
# GET /articles/search
|
72
|
+
def search
|
73
|
+
@articles = Article.search params[:q]
|
74
|
+
|
75
|
+
render :action => "index"
|
76
|
+
end
|
77
|
+
|
78
|
+
# GET /articles/1
|
79
|
+
CODE
|
80
|
+
|
81
|
+
gsub_file 'app/views/articles/index.html.erb', %r{<h1>Listing articles</h1>}, <<-CODE
|
82
|
+
<h1>Listing articles</h1>
|
83
|
+
|
84
|
+
<%= form_tag search_articles_path, :method => 'get' do %>
|
85
|
+
<%= label_tag :query %>
|
86
|
+
<%= text_field_tag :q, params[:q] %>
|
87
|
+
<%= submit_tag :search %>
|
88
|
+
<% end %>
|
89
|
+
|
90
|
+
<hr>
|
91
|
+
CODE
|
92
|
+
|
93
|
+
gsub_file 'app/views/articles/index.html.erb', %r{<%= link_to 'New Article', new_article_path %>}, <<-CODE
|
94
|
+
<%= link_to 'New Article', new_article_path %>
|
95
|
+
<%= link_to 'Back', articles_path if params[:q] %>
|
96
|
+
CODE
|
97
|
+
|
98
|
+
gsub_file 'config/routes.rb', %r{resources :articles}, <<-CODE
|
99
|
+
resources :articles do
|
100
|
+
collection { get :search }
|
101
|
+
end
|
102
|
+
CODE
|
103
|
+
|
104
|
+
git :commit => "-a -m 'Added Tire support into the frontend of application'"
|
105
|
+
|
106
|
+
puts
|
107
|
+
say_status "Database", "Seeding the database with data...", :yellow
|
108
|
+
puts '-'*80, ''
|
109
|
+
|
110
|
+
run "rm -f db/seeds.rb"
|
111
|
+
file 'db/seeds.rb', <<-CODE
|
112
|
+
contents = [
|
113
|
+
'Lorem ipsum dolor sit amet.',
|
114
|
+
'Consectetur adipisicing elit, sed do eiusmod tempor incididunt.',
|
115
|
+
'Labore et dolore magna aliqua.',
|
116
|
+
'Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris.',
|
117
|
+
'Excepteur sint occaecat cupidatat non proident.'
|
118
|
+
]
|
119
|
+
|
120
|
+
puts "Deleting all articles..."
|
121
|
+
Article.delete_all
|
122
|
+
|
123
|
+
puts "Creating articles:"
|
124
|
+
%w[ One Two Three Four Five ].each_with_index do |title, i|
|
125
|
+
Article.create :title => title, :content => contents[i], :published_on => i.days.ago.utc
|
126
|
+
end
|
127
|
+
CODE
|
128
|
+
|
129
|
+
rake "db:seed"
|
130
|
+
|
131
|
+
git :add => "db/seeds.rb"
|
132
|
+
git :commit => "-m 'Added database seeding script'"
|
133
|
+
|
134
|
+
puts
|
135
|
+
say_status "Index", "Indexing database...", :yellow
|
136
|
+
puts '-'*80, ''
|
137
|
+
|
138
|
+
rake "environment tire:import CLASS='Article' FORCE=true"
|
139
|
+
|
140
|
+
puts "", "="*80
|
141
|
+
say_status "DONE", "\e[1mStarting the application. Open http://localhost:3000 and search for something...\e[0m", :yellow
|
142
|
+
puts "="*80, ""
|
143
|
+
|
144
|
+
run "rails server"
|
@@ -0,0 +1,617 @@
|
|
1
|
+
# **Tire** provides rich and comfortable Ruby API for the
|
2
|
+
# [_ElasticSearch_](http://www.elasticsearch.org/) search engine/database.
|
3
|
+
#
|
4
|
+
# _ElasticSearch_ is a scalable, distributed, cloud-ready, highly-available
|
5
|
+
# full-text search engine and database, communicating by JSON over RESTful HTTP,
|
6
|
+
# based on [Lucene](http://lucene.apache.org/), written in Java.
|
7
|
+
#
|
8
|
+
# <img src="http://github.com/favicon.ico" style="position:relative; top:2px">
|
9
|
+
# _Tire_ is open source, and you can download or clone the source code
|
10
|
+
# from <https://github.com/karmi/tire>.
|
11
|
+
#
|
12
|
+
# By following these instructions you should have the search running
|
13
|
+
# on a sane operation system in less then 10 minutes.
|
14
|
+
|
15
|
+
# Note, that this file can be executed directly:
|
16
|
+
#
|
17
|
+
# ruby examples/tire-dsl.rb
|
18
|
+
#
|
19
|
+
|
20
|
+
|
21
|
+
#### Installation
|
22
|
+
|
23
|
+
# Install _Tire_ with Rubygems.
|
24
|
+
|
25
|
+
#
|
26
|
+
# gem install tire
|
27
|
+
#
|
28
|
+
require 'rubygems'
|
29
|
+
require 'tire'
|
30
|
+
|
31
|
+
#### Prerequisites
|
32
|
+
|
33
|
+
# You'll need a working and running _ElasticSearch_ server. Thankfully, that's easy.
|
34
|
+
( puts <<-"INSTALL" ; exit(1) ) unless (RestClient.get('http://localhost:9200') rescue false)
|
35
|
+
|
36
|
+
[ERROR] You don’t appear to have ElasticSearch installed. Please install and launch it with the following commands:
|
37
|
+
|
38
|
+
curl -k -L -o elasticsearch-0.16.0.tar.gz http://github.com/downloads/elasticsearch/elasticsearch/elasticsearch-0.16.0.tar.gz
|
39
|
+
tar -zxvf elasticsearch-0.16.0.tar.gz
|
40
|
+
./elasticsearch-0.16.0/bin/elasticsearch -f
|
41
|
+
INSTALL
|
42
|
+
|
43
|
+
### Storing and indexing documents
|
44
|
+
|
45
|
+
# Let's initialize an index named “articles”.
|
46
|
+
#
|
47
|
+
Tire.index 'articles' do
|
48
|
+
# To make sure it's fresh, let's delete any existing index with the same name.
|
49
|
+
#
|
50
|
+
delete
|
51
|
+
# And then, let's create it.
|
52
|
+
#
|
53
|
+
create
|
54
|
+
|
55
|
+
# We want to store and index some articles with `title`, `tags` and `published_on` properties.
|
56
|
+
# Simple Hashes are OK.
|
57
|
+
#
|
58
|
+
store :title => 'One', :tags => ['ruby'], :published_on => '2011-01-01'
|
59
|
+
store :title => 'Two', :tags => ['ruby', 'python'], :published_on => '2011-01-02'
|
60
|
+
store :title => 'Three', :tags => ['java'], :published_on => '2011-01-02'
|
61
|
+
store :title => 'Four', :tags => ['ruby', 'php'], :published_on => '2011-01-03'
|
62
|
+
|
63
|
+
# We force refreshing the index, so we can query it immediately.
|
64
|
+
#
|
65
|
+
refresh
|
66
|
+
end
|
67
|
+
|
68
|
+
# We may want to define a specific [mapping](http://www.elasticsearch.org/guide/reference/api/admin-indices-create-index.html)
|
69
|
+
# for the index.
|
70
|
+
|
71
|
+
Tire.index 'articles' do
|
72
|
+
# To do so, just pass a Hash containing the specified mapping to the `Index#create` method.
|
73
|
+
#
|
74
|
+
create :mappings => {
|
75
|
+
|
76
|
+
# Specify for which type of documents this mapping should be used.
|
77
|
+
# (The documents must provide a `type` method or property then.)
|
78
|
+
#
|
79
|
+
:article => {
|
80
|
+
:properties => {
|
81
|
+
|
82
|
+
# Specify the type of the field, whether it should be analyzed, etc.
|
83
|
+
#
|
84
|
+
:id => { :type => 'string', :index => 'not_analyzed', :include_in_all => false },
|
85
|
+
|
86
|
+
# Set the boost or analyzer settings for the field, ... The _ElasticSearch_ guide
|
87
|
+
# has [more information](http://elasticsearch.org/guide/reference/mapping/index.html)
|
88
|
+
# about this. Proper mapping is key to efficient and effective search.
|
89
|
+
# But don't fret about getting the mapping right the first time, you won't.
|
90
|
+
# In most cases, the default mapping is just fine for prototyping.
|
91
|
+
#
|
92
|
+
:title => { :type => 'string', :analyzer => 'snowball', :boost => 2.0 },
|
93
|
+
:tags => { :type => 'string', :analyzer => 'keyword' },
|
94
|
+
:content => { :type => 'string', :analyzer => 'czech' }
|
95
|
+
}
|
96
|
+
}
|
97
|
+
}
|
98
|
+
end
|
99
|
+
|
100
|
+
#### Bulk Storage
|
101
|
+
|
102
|
+
# Of course, we may have large amounts of data, and adding them to the index one by one really isn't the best idea.
|
103
|
+
# We can use _ElasticSearch's_ [bulk storage](http://www.elasticsearch.org/guide/reference/api/bulk.html)
|
104
|
+
# for importing the data.
|
105
|
+
|
106
|
+
# So, for demonstration purposes, let's suppose we have a plain collection of hashes to store.
|
107
|
+
#
|
108
|
+
articles = [
|
109
|
+
|
110
|
+
# Notice that such objects must have an `id` property!
|
111
|
+
#
|
112
|
+
{ :id => '1', :title => 'one', :tags => ['ruby'], :published_on => '2011-01-01' },
|
113
|
+
{ :id => '2', :title => 'two', :tags => ['ruby', 'python'], :published_on => '2011-01-02' },
|
114
|
+
{ :id => '3', :title => 'three', :tags => ['java'], :published_on => '2011-01-02' },
|
115
|
+
{ :id => '4', :title => 'four', :tags => ['ruby', 'php'], :published_on => '2011-01-03' }
|
116
|
+
]
|
117
|
+
|
118
|
+
# We can just push them into the index in one go.
|
119
|
+
#
|
120
|
+
Tire.index 'articles' do
|
121
|
+
import articles
|
122
|
+
end
|
123
|
+
|
124
|
+
# Of course, we can easily manipulate the documents before storing them in the index.
|
125
|
+
#
|
126
|
+
Tire.index 'articles' do
|
127
|
+
delete
|
128
|
+
|
129
|
+
# ... by just passing a block to the `import` method. The collection will
|
130
|
+
# be available in the block argument.
|
131
|
+
#
|
132
|
+
import articles do |documents|
|
133
|
+
|
134
|
+
# We will capitalize every _title_ and return the manipulated collection
|
135
|
+
# back to the `import` method.
|
136
|
+
#
|
137
|
+
documents.map { |document| document.update(:title => document[:title].capitalize) }
|
138
|
+
end
|
139
|
+
|
140
|
+
refresh
|
141
|
+
end
|
142
|
+
|
143
|
+
### Searching
|
144
|
+
|
145
|
+
# With the documents indexed and stored in the _ElasticSearch_ database, we can search them, finally.
|
146
|
+
#
|
147
|
+
# Tire exposes the search interface via simple domain-specific language.
|
148
|
+
|
149
|
+
|
150
|
+
#### Simple Query String Searches
|
151
|
+
|
152
|
+
# We can do simple searches, like searching for articles containing “One” in their title.
|
153
|
+
#
|
154
|
+
s = Tire.search('articles') do
|
155
|
+
query do
|
156
|
+
string "title:One"
|
157
|
+
end
|
158
|
+
end
|
159
|
+
|
160
|
+
# The results:
|
161
|
+
# * One [tags: ruby]
|
162
|
+
#
|
163
|
+
s.results.each do |document|
|
164
|
+
puts "* #{ document.title } [tags: #{document.tags.join(', ')}]"
|
165
|
+
end
|
166
|
+
|
167
|
+
# Or, we can search for articles published between January, 1st and January, 2nd.
|
168
|
+
#
|
169
|
+
s = Tire.search('articles') do
|
170
|
+
query do
|
171
|
+
string "published_on:[2011-01-01 TO 2011-01-02]"
|
172
|
+
end
|
173
|
+
end
|
174
|
+
|
175
|
+
# The results:
|
176
|
+
# * One [published: 2011-01-01]
|
177
|
+
# * Two [published: 2011-01-02]
|
178
|
+
# * Three [published: 2011-01-02]
|
179
|
+
#
|
180
|
+
s.results.each do |document|
|
181
|
+
puts "* #{ document.title } [published: #{document.published_on}]"
|
182
|
+
end
|
183
|
+
|
184
|
+
# Of course, we may write the blocks in shorter notation.
|
185
|
+
# Local variables from outer scope are passed down the chain.
|
186
|
+
|
187
|
+
# Let's search for articles whose titles begin with letter “T”.
|
188
|
+
#
|
189
|
+
q = "title:T*"
|
190
|
+
s = Tire.search('articles') { query { string q } }
|
191
|
+
|
192
|
+
# The results:
|
193
|
+
# * Two [tags: ruby, python]
|
194
|
+
# * Three [tags: java]
|
195
|
+
#
|
196
|
+
s.results.each do |document|
|
197
|
+
puts "* #{ document.title } [tags: #{document.tags.join(', ')}]"
|
198
|
+
end
|
199
|
+
|
200
|
+
# In fact, we can use any valid [Lucene query syntax](http://lucene.apache.org/java/3_0_3/queryparsersyntax.html)
|
201
|
+
# for the query string queries.
|
202
|
+
|
203
|
+
# For debugging, we can display the JSON which is being sent to _ElasticSearch_.
|
204
|
+
#
|
205
|
+
# {"query":{"query_string":{"query":"title:T*"}}}
|
206
|
+
#
|
207
|
+
puts "", "Query:", "-"*80
|
208
|
+
puts s.to_json
|
209
|
+
|
210
|
+
# Or better, we may display a complete `curl` command to recreate the request in terminal,
|
211
|
+
# so we can see the naked response, tweak request parameters and meditate on problems.
|
212
|
+
#
|
213
|
+
# curl -X POST "http://localhost:9200/articles/_search?pretty=true" \
|
214
|
+
# -d '{"query":{"query_string":{"query":"title:T*"}}}'
|
215
|
+
#
|
216
|
+
puts "", "Try the query in Curl:", "-"*80
|
217
|
+
puts s.to_curl
|
218
|
+
|
219
|
+
|
220
|
+
### Logging
|
221
|
+
|
222
|
+
# For debugging more complex situations, we can enable logging, so requests and responses
|
223
|
+
# will be logged using this `curl`-friendly format.
|
224
|
+
|
225
|
+
Tire.configure do
|
226
|
+
|
227
|
+
# By default, at the _info_ level, only the `curl`-format of request and
|
228
|
+
# basic information about the response will be logged:
|
229
|
+
#
|
230
|
+
# # 2011-04-24 11:34:01:150 [CREATE] ("articles")
|
231
|
+
# #
|
232
|
+
# curl -X POST "http://localhost:9200/articles"
|
233
|
+
#
|
234
|
+
# # 2011-04-24 11:34:01:152 [200]
|
235
|
+
#
|
236
|
+
logger 'elasticsearch.log'
|
237
|
+
|
238
|
+
# For debugging, we can switch to the _debug_ level, which will log the complete JSON responses.
|
239
|
+
#
|
240
|
+
# That's very convenient if we want to post a recreation of some problem or solution
|
241
|
+
# to the mailing list, IRC channel, etc.
|
242
|
+
#
|
243
|
+
logger 'elasticsearch.log', :level => 'debug'
|
244
|
+
|
245
|
+
# Note that we can pass any [`IO`](http://www.ruby-doc.org/core/classes/IO.html)-compatible Ruby object as a logging device.
|
246
|
+
#
|
247
|
+
logger STDERR
|
248
|
+
end
|
249
|
+
|
250
|
+
### Configuration
|
251
|
+
|
252
|
+
# As we have just seen with logging, we can configure various parts of _Tire_.
|
253
|
+
#
|
254
|
+
Tire.configure do
|
255
|
+
|
256
|
+
# First of all, we can configure the URL for _ElasticSearch_.
|
257
|
+
#
|
258
|
+
url "http://search.example.com"
|
259
|
+
|
260
|
+
# Second, we may want to wrap the result items in our own class.
|
261
|
+
#
|
262
|
+
class MySpecialWrapper; end
|
263
|
+
wrapper MySpecialWrapper
|
264
|
+
|
265
|
+
# Finally, we can reset one or all configuration settings to their defaults.
|
266
|
+
#
|
267
|
+
reset
|
268
|
+
|
269
|
+
end
|
270
|
+
|
271
|
+
|
272
|
+
### Complex Searching
|
273
|
+
|
274
|
+
#### Other Types of Queries
|
275
|
+
|
276
|
+
# Query strings are convenient for simple searches, but we may want to define our queries more expressively,
|
277
|
+
# using the _ElasticSearch_ [Query DSL](http://www.elasticsearch.org/guide/reference/query-dsl/index.html).
|
278
|
+
#
|
279
|
+
s = Tire.search('articles') do
|
280
|
+
|
281
|
+
# Let's suppose we want to search for articles with specific _tags_, in our case “ruby” _or_ “python”.
|
282
|
+
#
|
283
|
+
query do
|
284
|
+
|
285
|
+
# That's a great excuse to use a [_terms_](http://elasticsearch.org/guide/reference/query-dsl/terms-query.html)
|
286
|
+
# query.
|
287
|
+
#
|
288
|
+
terms :tags, ['ruby', 'python']
|
289
|
+
end
|
290
|
+
end
|
291
|
+
|
292
|
+
# The search, as expected, returns three articles, all tagged “ruby” — among other tags:
|
293
|
+
#
|
294
|
+
# * Two [tags: ruby, python]
|
295
|
+
# * One [tags: ruby]
|
296
|
+
# * Four [tags: ruby, php]
|
297
|
+
#
|
298
|
+
s.results.each do |document|
|
299
|
+
puts "* #{ document.title } [tags: #{document.tags.join(', ')}]"
|
300
|
+
end
|
301
|
+
|
302
|
+
# What if we wanted to search for articles tagged both “ruby” _and_ “python”?
|
303
|
+
#
|
304
|
+
s = Tire.search('articles') do
|
305
|
+
query do
|
306
|
+
|
307
|
+
# That's a great excuse to specify `minimum_match` for the query.
|
308
|
+
#
|
309
|
+
terms :tags, ['ruby', 'python'], :minimum_match => 2
|
310
|
+
end
|
311
|
+
end
|
312
|
+
|
313
|
+
# The search, as expected, returns one article, tagged with _both_ “ruby” and “python”:
|
314
|
+
#
|
315
|
+
# * Two [tags: ruby, python]
|
316
|
+
#
|
317
|
+
s.results.each do |document|
|
318
|
+
puts "* #{ document.title } [tags: #{document.tags.join(', ')}]"
|
319
|
+
end
|
320
|
+
|
321
|
+
# _ElasticSearch_ supports many types of [queries](http://www.elasticsearch.org/guide/reference/query-dsl/).
|
322
|
+
#
|
323
|
+
# Eventually, _Tire_ will support all of them. So far, only these are supported:
|
324
|
+
#
|
325
|
+
# * [string](http://www.elasticsearch.org/guide/reference/query-dsl/query-string-query.html)
|
326
|
+
# * [term](http://elasticsearch.org/guide/reference/query-dsl/term-query.html)
|
327
|
+
# * [terms](http://elasticsearch.org/guide/reference/query-dsl/terms-query.html)
|
328
|
+
# * [all](http://www.elasticsearch.org/guide/reference/query-dsl/match-all-query.html)
|
329
|
+
# * [ids](http://www.elasticsearch.org/guide/reference/query-dsl/ids-query.html)
|
330
|
+
|
331
|
+
#### Faceted Search
|
332
|
+
|
333
|
+
# _ElasticSearch_ makes it trivial to retrieve complex aggregated data from our index/database,
|
334
|
+
# so called [_facets_](http://www.elasticsearch.org/guide/reference/api/search/facets/index.html).
|
335
|
+
|
336
|
+
# Let's say we want to display article counts for every tag in the database.
|
337
|
+
# For that, we'll use a _terms_ facet.
|
338
|
+
|
339
|
+
#
|
340
|
+
s = Tire.search 'articles' do
|
341
|
+
|
342
|
+
# We will search for articles whose title begins with letter “T”,
|
343
|
+
#
|
344
|
+
query { string 'title:T*' }
|
345
|
+
|
346
|
+
# and retrieve the counts “bucketed” by `tags`.
|
347
|
+
#
|
348
|
+
facet 'tags' do
|
349
|
+
terms :tags
|
350
|
+
end
|
351
|
+
end
|
352
|
+
|
353
|
+
# As we see, our query has found two articles, and if you recall our articles from above,
|
354
|
+
# _Two_ is tagged with “ruby” and “python”, while _Three_ is tagged with “java”.
|
355
|
+
#
|
356
|
+
# Found 2 articles: Three, Two
|
357
|
+
#
|
358
|
+
# The counts shouldn't surprise us:
|
359
|
+
#
|
360
|
+
# Counts by tag:
|
361
|
+
# -------------------------
|
362
|
+
# ruby 1
|
363
|
+
# python 1
|
364
|
+
# java 1
|
365
|
+
#
|
366
|
+
puts "Found #{s.results.count} articles: #{s.results.map(&:title).join(', ')}"
|
367
|
+
puts "Counts by tag:", "-"*25
|
368
|
+
s.results.facets['tags']['terms'].each do |f|
|
369
|
+
puts "#{f['term'].ljust(10)} #{f['count']}"
|
370
|
+
end
|
371
|
+
|
372
|
+
# These counts are based on the scope of our current query.
|
373
|
+
# What if we wanted to display aggregated counts by `tags` across the whole database?
|
374
|
+
|
375
|
+
#
|
376
|
+
s = Tire.search 'articles' do
|
377
|
+
|
378
|
+
# Let's repeat the search for “T”...
|
379
|
+
#
|
380
|
+
query { string 'title:T*' }
|
381
|
+
|
382
|
+
facet 'global-tags' do
|
383
|
+
|
384
|
+
# ...but set the `global` scope for the facet in this case.
|
385
|
+
#
|
386
|
+
terms :tags, :global => true
|
387
|
+
end
|
388
|
+
|
389
|
+
# We can even _combine_ facets scoped to the current query
|
390
|
+
# with globally scoped facets — we'll just use a different name.
|
391
|
+
#
|
392
|
+
facet 'current-tags' do
|
393
|
+
terms :tags
|
394
|
+
end
|
395
|
+
end
|
396
|
+
|
397
|
+
# Aggregated results for the current query are the same as previously:
|
398
|
+
#
|
399
|
+
# Current query facets:
|
400
|
+
# -------------------------
|
401
|
+
# ruby 1
|
402
|
+
# python 1
|
403
|
+
# java 1
|
404
|
+
#
|
405
|
+
puts "Current query facets:", "-"*25
|
406
|
+
s.results.facets['current-tags']['terms'].each do |f|
|
407
|
+
puts "#{f['term'].ljust(10)} #{f['count']}"
|
408
|
+
end
|
409
|
+
|
410
|
+
# On the other hand, aggregated results for the global scope include also
|
411
|
+
# tags for articles not matched by the query, such as “java” or “php”:
|
412
|
+
#
|
413
|
+
# Global facets:
|
414
|
+
# -------------------------
|
415
|
+
# ruby 3
|
416
|
+
# python 1
|
417
|
+
# php 1
|
418
|
+
# java 1
|
419
|
+
#
|
420
|
+
puts "Global facets:", "-"*25
|
421
|
+
s.results.facets['global-tags']['terms'].each do |f|
|
422
|
+
puts "#{f['term'].ljust(10)} #{f['count']}"
|
423
|
+
end
|
424
|
+
|
425
|
+
# _ElasticSearch_ supports many advanced types of facets, such as those for computing statistics or geographical distance.
|
426
|
+
#
|
427
|
+
# Eventually, _Tire_ will support all of them. So far, only these are supported:
|
428
|
+
#
|
429
|
+
# * [terms](http://www.elasticsearch.org/guide/reference/api/search/facets/terms-facet.html)
|
430
|
+
# * [date](http://www.elasticsearch.org/guide/reference/api/search/facets/date-histogram-facet.html)
|
431
|
+
|
432
|
+
# We have seen that _ElasticSearch_ facets enable us to fetch complex aggregations from our data.
|
433
|
+
#
|
434
|
+
# They are frequently used for another feature, „faceted navigation“.
|
435
|
+
# We can be combine query and facets with
|
436
|
+
# [filters](http://elasticsearch.org/guide/reference/api/search/filter.html),
|
437
|
+
# so the returned documents are restricted by certain criteria — for example to a specific category —,
|
438
|
+
# but the aggregation calculations are still based on the original query.
|
439
|
+
|
440
|
+
|
441
|
+
#### Filtered Search
|
442
|
+
|
443
|
+
# So, let's make our search a bit more complex. Let's search for articles whose titles begin
|
444
|
+
# with letter “T”, again, but filter the results, so only the articles tagged “ruby”
|
445
|
+
# are returned.
|
446
|
+
#
|
447
|
+
s = Tire.search 'articles' do
|
448
|
+
|
449
|
+
# We will use just the same **query** as before.
|
450
|
+
#
|
451
|
+
query { string 'title:T*' }
|
452
|
+
|
453
|
+
# But we will add a _terms_ **filter** based on tags.
|
454
|
+
#
|
455
|
+
filter :terms, :tags => ['ruby']
|
456
|
+
|
457
|
+
# And, of course, our facet definition.
|
458
|
+
#
|
459
|
+
facet('tags') { terms :tags }
|
460
|
+
|
461
|
+
end
|
462
|
+
|
463
|
+
# We see that only the article _Two_ (tagged “ruby” and “python”) is returned,
|
464
|
+
# _not_ the article _Three_ (tagged “java”):
|
465
|
+
#
|
466
|
+
# * Two [tags: ruby, python]
|
467
|
+
#
|
468
|
+
s.results.each do |document|
|
469
|
+
puts "* #{ document.title } [tags: #{document.tags.join(', ')}]"
|
470
|
+
end
|
471
|
+
|
472
|
+
# The _count_ for article _Three_'s tags, “java”, on the other hand, _is_ in fact included:
|
473
|
+
#
|
474
|
+
# Counts by tag:
|
475
|
+
# -------------------------
|
476
|
+
# ruby 1
|
477
|
+
# python 1
|
478
|
+
# java 1
|
479
|
+
#
|
480
|
+
puts "Counts by tag:", "-"*25
|
481
|
+
s.results.facets['tags']['terms'].each do |f|
|
482
|
+
puts "#{f['term'].ljust(10)} #{f['count']}"
|
483
|
+
end
|
484
|
+
|
485
|
+
#### Sorting
|
486
|
+
|
487
|
+
# By default, the results are sorted according to their relevancy.
|
488
|
+
#
|
489
|
+
s = Tire.search('articles') { query { string 'tags:ruby' } }
|
490
|
+
|
491
|
+
s.results.each do |document|
|
492
|
+
puts "* #{ document.title } " +
|
493
|
+
"[tags: #{document.tags.join(', ')}; " +
|
494
|
+
|
495
|
+
# The score is available as the `_score` property.
|
496
|
+
#
|
497
|
+
"score: #{document._score}]"
|
498
|
+
end
|
499
|
+
|
500
|
+
# The results:
|
501
|
+
#
|
502
|
+
# * One [tags: ruby; score: 0.30685282]
|
503
|
+
# * Four [tags: ruby, php; score: 0.19178301]
|
504
|
+
# * Two [tags: ruby, python; score: 0.19178301]
|
505
|
+
|
506
|
+
# But, what if we want to sort the results based on some other criteria,
|
507
|
+
# such as published date or product price? We can do that.
|
508
|
+
#
|
509
|
+
s = Tire.search 'articles' do
|
510
|
+
|
511
|
+
# We will search for articles tagged “ruby”, again, ...
|
512
|
+
#
|
513
|
+
query { string 'tags:ruby' }
|
514
|
+
|
515
|
+
# ... but will sort them by their `title`, in descending order.
|
516
|
+
#
|
517
|
+
sort { title 'desc' }
|
518
|
+
end
|
519
|
+
|
520
|
+
# The results:
|
521
|
+
#
|
522
|
+
# * Two
|
523
|
+
# * One
|
524
|
+
# * Four
|
525
|
+
#
|
526
|
+
s.results.each do |document|
|
527
|
+
puts "* #{ document.title }"
|
528
|
+
end
|
529
|
+
|
530
|
+
# Of course, it's possible to combine more fields in the sorting definition.
|
531
|
+
|
532
|
+
s = Tire.search 'articles' do
|
533
|
+
|
534
|
+
# We will just get all articles in this case.
|
535
|
+
#
|
536
|
+
query { all }
|
537
|
+
|
538
|
+
sort do
|
539
|
+
|
540
|
+
# We will sort the results by their `published_on` property in _ascending_ order (the default),
|
541
|
+
#
|
542
|
+
published_on
|
543
|
+
|
544
|
+
# and by their `title` property, in _descending_ order.
|
545
|
+
#
|
546
|
+
title 'desc'
|
547
|
+
end
|
548
|
+
end
|
549
|
+
|
550
|
+
# The results:
|
551
|
+
# * One (Published on: 2011-01-01)
|
552
|
+
# * Two (Published on: 2011-01-02)
|
553
|
+
# * Three (Published on: 2011-01-02)
|
554
|
+
# * Four (Published on: 2011-01-03)
|
555
|
+
#
|
556
|
+
s.results.each do |document|
|
557
|
+
puts "* #{ document.title.ljust(10) } (Published on: #{ document.published_on })"
|
558
|
+
end
|
559
|
+
|
560
|
+
#### Highlighting
|
561
|
+
|
562
|
+
# Often, we want to highlight the snippets matching our query in the displayed results.
|
563
|
+
# _ElasticSearch_ provides rich
|
564
|
+
# [highlighting](http://www.elasticsearch.org/guide/reference/api/search/highlighting.html)
|
565
|
+
# features, and _Tire_ makes them trivial to use.
|
566
|
+
#
|
567
|
+
s = Tire.search 'articles' do
|
568
|
+
|
569
|
+
# Let's search for documents containing word “Two” in their titles,
|
570
|
+
query { string 'title:Two' }
|
571
|
+
|
572
|
+
# and instruct _ElasticSearch_ to highlight relevant snippets.
|
573
|
+
#
|
574
|
+
highlight :title
|
575
|
+
end
|
576
|
+
|
577
|
+
# The results:
|
578
|
+
# Title: Two; Highlighted: <em>Two</em>
|
579
|
+
#
|
580
|
+
s.results.each do |document|
|
581
|
+
puts "Title: #{ document.title }; Highlighted: #{document.highlight.title}"
|
582
|
+
end
|
583
|
+
|
584
|
+
# We can configure many options for highlighting, such as:
|
585
|
+
#
|
586
|
+
s = Tire.search 'articles' do
|
587
|
+
query { string 'title:Two' }
|
588
|
+
|
589
|
+
# • specify the fields to highlight
|
590
|
+
#
|
591
|
+
highlight :title, :body
|
592
|
+
|
593
|
+
# • specify their individual options
|
594
|
+
#
|
595
|
+
highlight :title, :body => { :number_of_fragments => 0 }
|
596
|
+
|
597
|
+
# • or specify global highlighting options, such as the wrapper tag
|
598
|
+
#
|
599
|
+
highlight :title, :body, :options => { :tag => '<strong class="highlight">' }
|
600
|
+
end
|
601
|
+
|
602
|
+
|
603
|
+
### ActiveModel Integration
|
604
|
+
|
605
|
+
# As you can see, [_Tire_](https://github.com/karmi/tire) supports the
|
606
|
+
# main features of _ElasticSearch_ in Ruby.
|
607
|
+
#
|
608
|
+
# It allows you to create and delete indices, add documents, search them, retrieve the facets, highlight the results,
|
609
|
+
# and comes with a usable logging facility.
|
610
|
+
#
|
611
|
+
# Of course, the holy grail of any search library is easy, painless integration with your Ruby classes, and,
|
612
|
+
# most importantly, with ActiveRecord/ActiveModel classes.
|
613
|
+
#
|
614
|
+
# Please, check out the [README](https://github.com/karmi/tire/tree/master#readme) file for instructions
|
615
|
+
# how to include _Tire_-based search in your models..
|
616
|
+
#
|
617
|
+
# Send any feedback via Github issues, or ask questions in the [#elasticsearch](irc://irc.freenode.net/#elasticsearch) IRC channel.
|