slingshot-rb 0.0.8 → 0.0.9
Sign up to get free protection for your applications and to get access to all the features.
- data/.gitignore +1 -0
- data/README.markdown +276 -50
- data/examples/rails-application-template.rb +144 -0
- data/examples/slingshot-dsl.rb +272 -102
- data/lib/slingshot.rb +13 -0
- data/lib/slingshot/client.rb +10 -1
- data/lib/slingshot/dsl.rb +17 -1
- data/lib/slingshot/index.rb +109 -7
- data/lib/slingshot/model/callbacks.rb +23 -0
- data/lib/slingshot/model/import.rb +18 -0
- data/lib/slingshot/model/indexing.rb +50 -0
- data/lib/slingshot/model/naming.rb +30 -0
- data/lib/slingshot/model/persistence.rb +34 -0
- data/lib/slingshot/model/persistence/attributes.rb +60 -0
- data/lib/slingshot/model/persistence/finders.rb +61 -0
- data/lib/slingshot/model/persistence/storage.rb +75 -0
- data/lib/slingshot/model/search.rb +97 -0
- data/lib/slingshot/results/collection.rb +35 -10
- data/lib/slingshot/results/item.rb +10 -7
- data/lib/slingshot/results/pagination.rb +30 -0
- data/lib/slingshot/rubyext/symbol.rb +11 -0
- data/lib/slingshot/search.rb +3 -2
- data/lib/slingshot/search/facet.rb +8 -6
- data/lib/slingshot/search/filter.rb +7 -8
- data/lib/slingshot/search/highlight.rb +1 -3
- data/lib/slingshot/search/query.rb +4 -0
- data/lib/slingshot/search/sort.rb +5 -0
- data/lib/slingshot/tasks.rb +88 -0
- data/lib/slingshot/version.rb +1 -1
- data/slingshot.gemspec +17 -4
- data/test/integration/active_model_searchable_test.rb +80 -0
- data/test/integration/active_record_searchable_test.rb +193 -0
- data/test/integration/highlight_test.rb +1 -1
- data/test/integration/index_mapping_test.rb +1 -1
- data/test/integration/index_store_test.rb +27 -0
- data/test/integration/persistent_model_test.rb +35 -0
- data/test/integration/query_string_test.rb +3 -3
- data/test/integration/sort_test.rb +2 -2
- 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/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 +4 -0
- data/test/unit/active_model_lint_test.rb +17 -0
- data/test/unit/client_test.rb +4 -0
- data/test/unit/configuration_test.rb +4 -0
- data/test/unit/index_test.rb +240 -17
- 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 +69 -7
- data/test/unit/results_item_test.rb +8 -14
- data/test/unit/rubyext_hash_test.rb +19 -0
- data/test/unit/search_facet_test.rb +25 -7
- data/test/unit/search_filter_test.rb +3 -0
- data/test/unit/search_query_test.rb +11 -0
- data/test/unit/search_sort_test.rb +8 -0
- data/test/unit/search_test.rb +14 -0
- data/test/unit/slingshot_test.rb +38 -0
- metadata +133 -26
@@ -7,6 +7,11 @@ module Slingshot
|
|
7
7
|
self.instance_eval(&block) if block_given?
|
8
8
|
end
|
9
9
|
|
10
|
+
def field(name, direction=nil)
|
11
|
+
@value << ( direction ? { name => direction } : name )
|
12
|
+
self
|
13
|
+
end
|
14
|
+
|
10
15
|
def method_missing(id, *args, &block)
|
11
16
|
case arg = args.shift
|
12
17
|
when String, Symbol, Hash then @value << { id => arg }
|
@@ -0,0 +1,88 @@
|
|
1
|
+
require 'rake'
|
2
|
+
require 'benchmark'
|
3
|
+
|
4
|
+
namespace :slingshot do
|
5
|
+
|
6
|
+
usage = <<-DESC
|
7
|
+
Import data from your model using paginate: rake environment slingshot:import CLASS='MyModel'
|
8
|
+
|
9
|
+
Pass params for the `paginate` method:
|
10
|
+
$ rake environment slingshot:import CLASS='Article' PARAMS='{:page => 1}'
|
11
|
+
|
12
|
+
Force rebuilding the index (delete and create):
|
13
|
+
$ rake environment slingshot:import CLASS='Article' PARAMS='{:page => 1}' FORCE=1
|
14
|
+
|
15
|
+
Set target index name:
|
16
|
+
$ rake environment slingshot:import CLASS='Article' INDEX='articles-new'
|
17
|
+
|
18
|
+
DESC
|
19
|
+
|
20
|
+
desc usage.split("\n").first.to_s
|
21
|
+
task :import do
|
22
|
+
|
23
|
+
def elapsed_to_human(elapsed)
|
24
|
+
hour = 60*60
|
25
|
+
day = hour*24
|
26
|
+
|
27
|
+
case elapsed
|
28
|
+
when 0..59
|
29
|
+
"#{sprintf("%1.5f", elapsed)} seconds"
|
30
|
+
when 60..hour-1
|
31
|
+
"#{elapsed/60} minutes and #{elapsed % 60} seconds"
|
32
|
+
when hour..day
|
33
|
+
"#{elapsed/hour} hours and #{elapsed % hour} minutes"
|
34
|
+
else
|
35
|
+
"#{elapsed/hour} hours"
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
if ENV['CLASS'].to_s == ''
|
40
|
+
puts '='*80, 'USAGE', '='*80, usage.gsub(/ /, '')
|
41
|
+
exit(1)
|
42
|
+
end
|
43
|
+
|
44
|
+
klass = eval(ENV['CLASS'].to_s)
|
45
|
+
params = eval(ENV['PARAMS'].to_s) || {}
|
46
|
+
|
47
|
+
index = Slingshot::Index.new( ENV['INDEX'] || klass.index.name )
|
48
|
+
|
49
|
+
if ENV['FORCE']
|
50
|
+
puts "[IMPORT] Deleting index '#{index.name}'"
|
51
|
+
index.delete
|
52
|
+
end
|
53
|
+
|
54
|
+
unless index.exists?
|
55
|
+
puts "[IMPORT] Creating index '#{index.name}' with mapping:",
|
56
|
+
Yajl::Encoder.encode(klass.mapping_to_hash, :pretty => true)
|
57
|
+
index.create :mappings => klass.mapping_to_hash
|
58
|
+
end
|
59
|
+
|
60
|
+
STDOUT.sync = true
|
61
|
+
puts "[IMPORT] Starting import for the '#{ENV['CLASS']}' class"
|
62
|
+
tty_cols = 80
|
63
|
+
total = klass.count rescue nil
|
64
|
+
offset = (total.to_s.size*2)+8
|
65
|
+
done = 0
|
66
|
+
|
67
|
+
STDOUT.puts '-'*tty_cols
|
68
|
+
elapsed = Benchmark.realtime do
|
69
|
+
index.import(klass, 'paginate', params) do |documents|
|
70
|
+
|
71
|
+
if total
|
72
|
+
done += documents.size
|
73
|
+
# I CAN HAZ PROGREZ BAR LIEK HOMEBRU!
|
74
|
+
percent = ( (done.to_f / total) * 100 ).to_i
|
75
|
+
glyphs = ( percent * ( (tty_cols-offset).to_f/100 ) ).to_i
|
76
|
+
STDOUT.print( "#" * glyphs )
|
77
|
+
STDOUT.print( "\r"*tty_cols+"#{done}/#{total} | \e[1m#{percent}%\e[0m " )
|
78
|
+
end
|
79
|
+
|
80
|
+
# Don't forget to return the documents collection back!
|
81
|
+
documents
|
82
|
+
end
|
83
|
+
end
|
84
|
+
|
85
|
+
puts "", '='*80, "Import finished in #{elapsed_to_human(elapsed)}"
|
86
|
+
|
87
|
+
end
|
88
|
+
end
|
data/lib/slingshot/version.rb
CHANGED
data/slingshot.gemspec
CHANGED
@@ -6,7 +6,7 @@ Gem::Specification.new do |s|
|
|
6
6
|
s.name = "slingshot-rb"
|
7
7
|
s.version = Slingshot::VERSION
|
8
8
|
s.platform = Gem::Platform::RUBY
|
9
|
-
s.summary = "Ruby
|
9
|
+
s.summary = "Ruby client for ElasticSearch"
|
10
10
|
s.homepage = "http://github.com/karmi/slingshot"
|
11
11
|
s.authors = [ 'Karel Minarik' ]
|
12
12
|
s.email = 'karmi@karmi.cz'
|
@@ -24,18 +24,31 @@ Gem::Specification.new do |s|
|
|
24
24
|
|
25
25
|
s.required_rubygems_version = ">= 1.3.6"
|
26
26
|
|
27
|
+
s.add_dependency "rake", "~> 0.8.0"
|
27
28
|
s.add_dependency "bundler", "~> 1.0.0"
|
28
29
|
s.add_dependency "rest-client", "~> 1.6.0"
|
29
|
-
s.add_dependency "yajl-ruby", "> 0.
|
30
|
+
s.add_dependency "yajl-ruby", "> 0.8.0"
|
31
|
+
s.add_dependency "activemodel", "> 3.0.0"
|
30
32
|
|
31
33
|
s.add_development_dependency "turn"
|
32
34
|
s.add_development_dependency "shoulda"
|
33
35
|
s.add_development_dependency "mocha"
|
34
36
|
s.add_development_dependency "sdoc"
|
35
37
|
s.add_development_dependency "rcov"
|
38
|
+
s.add_development_dependency "activerecord"
|
39
|
+
s.add_development_dependency "supermodel"
|
36
40
|
|
37
41
|
s.description = <<-DESC
|
38
|
-
Ruby
|
39
|
-
|
42
|
+
Slingshot is a Ruby client for the ElasticSearch search engine/database.
|
43
|
+
|
44
|
+
It provides Ruby-like API for fluent communication with the ElasticSearch server
|
45
|
+
and blends with ActiveModel class for convenient usage in Rails applications.
|
46
|
+
|
47
|
+
It allows to delete and create indices, define mapping for them, supports
|
48
|
+
the bulk API, and presents an easy-to-use DSL for constructing your queries.
|
49
|
+
|
50
|
+
It has full ActiveRecord/ActiveModel compatibility, allowing you to index
|
51
|
+
your models (incrementally upon saving, or in bulk), searching and
|
52
|
+
paginating the results.
|
40
53
|
DESC
|
41
54
|
end
|
@@ -0,0 +1,80 @@
|
|
1
|
+
require 'test_helper'
|
2
|
+
|
3
|
+
module Slingshot
|
4
|
+
|
5
|
+
class ActiveModelSearchableIntegrationTest < Test::Unit::TestCase
|
6
|
+
include Test::Integration
|
7
|
+
|
8
|
+
def setup
|
9
|
+
super
|
10
|
+
SupermodelArticle.delete_all
|
11
|
+
@model = SupermodelArticle.new :title => 'Test'
|
12
|
+
end
|
13
|
+
|
14
|
+
def teardown
|
15
|
+
super
|
16
|
+
SupermodelArticle.delete_all
|
17
|
+
end
|
18
|
+
|
19
|
+
context "ActiveModel" do
|
20
|
+
|
21
|
+
setup do
|
22
|
+
Slingshot.index('supermodel_articles').delete
|
23
|
+
load File.expand_path('../../models/supermodel_article.rb', __FILE__)
|
24
|
+
end
|
25
|
+
teardown { Slingshot.index('supermodel_articles').delete }
|
26
|
+
|
27
|
+
should "configure mapping" do
|
28
|
+
assert_equal 'czech', SupermodelArticle.mapping[:title][:analyzer]
|
29
|
+
assert_equal 15, SupermodelArticle.mapping[:title][:boost]
|
30
|
+
|
31
|
+
assert_equal 'czech', SupermodelArticle.index.mapping['supermodel_article']['properties']['title']['analyzer']
|
32
|
+
end
|
33
|
+
|
34
|
+
should "save document into index on save and find it with score" do
|
35
|
+
a = SupermodelArticle.new :title => 'Test'
|
36
|
+
a.save
|
37
|
+
id = a.id
|
38
|
+
|
39
|
+
a.index.refresh
|
40
|
+
sleep(1.5)
|
41
|
+
|
42
|
+
results = SupermodelArticle.search 'test'
|
43
|
+
|
44
|
+
assert_equal 1, results.count
|
45
|
+
assert_instance_of SupermodelArticle, results.first
|
46
|
+
assert_equal 'Test', results.first.title
|
47
|
+
assert_not_nil results.first.score
|
48
|
+
assert_equal id, results.first.id
|
49
|
+
end
|
50
|
+
|
51
|
+
should "remove document from index on destroy" do
|
52
|
+
a = SupermodelArticle.new :title => 'Test'
|
53
|
+
a.save
|
54
|
+
a.destroy
|
55
|
+
|
56
|
+
a.index.refresh
|
57
|
+
sleep(1.25)
|
58
|
+
|
59
|
+
results = SupermodelArticle.search 'test'
|
60
|
+
|
61
|
+
assert_equal 0, results.count
|
62
|
+
end
|
63
|
+
|
64
|
+
should "retrieve sorted documents by IDs returned from search" do
|
65
|
+
SupermodelArticle.create! :title => 'foo'
|
66
|
+
SupermodelArticle.create! :title => 'bar'
|
67
|
+
|
68
|
+
SupermodelArticle.index.refresh
|
69
|
+
results = SupermodelArticle.search 'foo OR bar^100'
|
70
|
+
|
71
|
+
assert_equal 2, results.count
|
72
|
+
|
73
|
+
assert_equal 'bar', results.first.title
|
74
|
+
end
|
75
|
+
|
76
|
+
end
|
77
|
+
|
78
|
+
end
|
79
|
+
|
80
|
+
end
|
@@ -0,0 +1,193 @@
|
|
1
|
+
require 'test_helper'
|
2
|
+
|
3
|
+
module Slingshot
|
4
|
+
|
5
|
+
class ActiveRecordSearchableIntegrationTest < Test::Unit::TestCase
|
6
|
+
include Test::Integration
|
7
|
+
|
8
|
+
def setup
|
9
|
+
super
|
10
|
+
File.delete fixtures_path.join('articles.db') rescue nil
|
11
|
+
|
12
|
+
ActiveRecord::Base.establish_connection( :adapter => 'sqlite3', :database => fixtures_path.join('articles.db') )
|
13
|
+
|
14
|
+
ActiveRecord::Migration.verbose = false
|
15
|
+
ActiveRecord::Schema.define(:version => 1) do
|
16
|
+
create_table :active_record_articles do |t|
|
17
|
+
t.string :title
|
18
|
+
t.datetime :created_at, :default => 'NOW()'
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
def teardown
|
24
|
+
super
|
25
|
+
File.delete fixtures_path.join('articles.db') rescue nil
|
26
|
+
end
|
27
|
+
|
28
|
+
context "ActiveRecord integration" do
|
29
|
+
|
30
|
+
setup do
|
31
|
+
Slingshot.index('active_record_articles').delete
|
32
|
+
load File.expand_path('../../models/active_record_article.rb', __FILE__)
|
33
|
+
end
|
34
|
+
teardown { Slingshot.index('active_record_articles').delete }
|
35
|
+
|
36
|
+
should "configure mapping" do
|
37
|
+
assert_equal 'snowball', ActiveRecordArticle.mapping[:title][:analyzer]
|
38
|
+
assert_equal 10, ActiveRecordArticle.mapping[:title][:boost]
|
39
|
+
|
40
|
+
assert_equal 'snowball', ActiveRecordArticle.index.mapping['active_record_article']['properties']['title']['analyzer']
|
41
|
+
end
|
42
|
+
|
43
|
+
should "save document into index on save and find it" do
|
44
|
+
a = ActiveRecordArticle.new :title => 'Test'
|
45
|
+
a.save!
|
46
|
+
id = a.id
|
47
|
+
|
48
|
+
a.index.refresh
|
49
|
+
sleep(1.5) # Leave ES some breathing room here...
|
50
|
+
|
51
|
+
results = ActiveRecordArticle.search 'test'
|
52
|
+
|
53
|
+
assert_equal 1, results.count
|
54
|
+
|
55
|
+
assert_instance_of ActiveRecordArticle, results.first
|
56
|
+
assert_not_nil results.first.id
|
57
|
+
assert_equal id, results.first.id
|
58
|
+
assert results.first.persisted?, "Record should be persisted"
|
59
|
+
assert_not_nil results.first._score
|
60
|
+
assert_equal 'Test', results.first.title
|
61
|
+
end
|
62
|
+
|
63
|
+
should "remove document from index on destroy" do
|
64
|
+
a = ActiveRecordArticle.new :title => 'Test'
|
65
|
+
a.save!
|
66
|
+
a.destroy
|
67
|
+
|
68
|
+
a.index.refresh
|
69
|
+
results = ActiveRecordArticle.search 'test'
|
70
|
+
|
71
|
+
assert_equal 0, results.count
|
72
|
+
end
|
73
|
+
|
74
|
+
should "return documents with scores" do
|
75
|
+
ActiveRecordArticle.create! :title => 'foo'
|
76
|
+
ActiveRecordArticle.create! :title => 'bar'
|
77
|
+
|
78
|
+
ActiveRecordArticle.index.refresh
|
79
|
+
results = ActiveRecordArticle.search 'foo OR bar^100'
|
80
|
+
assert_equal 2, results.count
|
81
|
+
|
82
|
+
assert_equal 'bar', results.first.title
|
83
|
+
end
|
84
|
+
|
85
|
+
context "with pagination" do
|
86
|
+
setup do
|
87
|
+
1.upto(9) { |number| ActiveRecordArticle.create :title => "Test#{number}" }
|
88
|
+
ActiveRecordArticle.index.refresh
|
89
|
+
end
|
90
|
+
|
91
|
+
context "and parameter searches" do
|
92
|
+
|
93
|
+
should "find first page with five results" do
|
94
|
+
results = ActiveRecordArticle.search 'test*', :sort => 'title', :per_page => 5, :page => 1
|
95
|
+
assert_equal 5, results.size
|
96
|
+
|
97
|
+
assert_equal 2, results.total_pages
|
98
|
+
assert_equal 1, results.current_page
|
99
|
+
assert_equal 0, results.previous_page
|
100
|
+
assert_equal 2, results.next_page
|
101
|
+
|
102
|
+
assert_equal 'Test1', results.first.title
|
103
|
+
end
|
104
|
+
|
105
|
+
should "find next page with five results" do
|
106
|
+
results = ActiveRecordArticle.search 'test*', :sort => 'title', :per_page => 5, :page => 2
|
107
|
+
assert_equal 4, results.size
|
108
|
+
|
109
|
+
assert_equal 2, results.total_pages
|
110
|
+
assert_equal 2, results.current_page
|
111
|
+
assert_equal 1, results.previous_page
|
112
|
+
assert_equal 3, results.next_page
|
113
|
+
|
114
|
+
assert_equal 'Test6', results.first.title
|
115
|
+
end
|
116
|
+
|
117
|
+
should "find not find missing page" do
|
118
|
+
results = ActiveRecordArticle.search 'test*', :sort => 'title', :per_page => 5, :page => 3
|
119
|
+
assert_equal 0, results.size
|
120
|
+
|
121
|
+
assert_equal 2, results.total_pages
|
122
|
+
assert_equal 3, results.current_page
|
123
|
+
assert_equal 2, results.previous_page
|
124
|
+
assert_equal 4, results.next_page
|
125
|
+
|
126
|
+
assert_nil results.first
|
127
|
+
end
|
128
|
+
|
129
|
+
end
|
130
|
+
|
131
|
+
context "and block searches" do
|
132
|
+
setup { @q = 'test*' }
|
133
|
+
|
134
|
+
should "find first page with five results" do
|
135
|
+
results = ActiveRecordArticle.search nil, :per_page => 5, :page => 1 do |search|
|
136
|
+
search.query { |query| query.string @q }
|
137
|
+
search.sort { title }
|
138
|
+
search.from 0
|
139
|
+
search.size 5
|
140
|
+
end
|
141
|
+
assert_equal 5, results.size
|
142
|
+
|
143
|
+
assert_equal 2, results.total_pages
|
144
|
+
assert_equal 1, results.current_page
|
145
|
+
assert_equal 0, results.previous_page
|
146
|
+
assert_equal 2, results.next_page
|
147
|
+
|
148
|
+
assert_equal 'Test1', results.first.title
|
149
|
+
end
|
150
|
+
|
151
|
+
should "find next page with five results" do
|
152
|
+
results = ActiveRecordArticle.search nil, :per_page => 5, :page => 2 do |search|
|
153
|
+
search.query { |query| query.string @q }
|
154
|
+
search.sort { title }
|
155
|
+
search.from 5
|
156
|
+
search.size 5
|
157
|
+
end
|
158
|
+
assert_equal 4, results.size
|
159
|
+
|
160
|
+
assert_equal 2, results.total_pages
|
161
|
+
assert_equal 2, results.current_page
|
162
|
+
assert_equal 1, results.previous_page
|
163
|
+
assert_equal 3, results.next_page
|
164
|
+
|
165
|
+
assert_equal 'Test6', results.first.title
|
166
|
+
end
|
167
|
+
|
168
|
+
should "find not find missing page" do
|
169
|
+
results = ActiveRecordArticle.search nil, :per_page => 5, :page => 3 do |search|
|
170
|
+
search.query { |query| query.string @q }
|
171
|
+
search.sort { title }
|
172
|
+
search.from 10
|
173
|
+
search.size 5
|
174
|
+
end
|
175
|
+
assert_equal 0, results.size
|
176
|
+
|
177
|
+
assert_equal 2, results.total_pages
|
178
|
+
assert_equal 3, results.current_page
|
179
|
+
assert_equal 2, results.previous_page
|
180
|
+
assert_equal 4, results.next_page
|
181
|
+
|
182
|
+
assert_nil results.first
|
183
|
+
end
|
184
|
+
|
185
|
+
end
|
186
|
+
|
187
|
+
end
|
188
|
+
|
189
|
+
end
|
190
|
+
|
191
|
+
end
|
192
|
+
|
193
|
+
end
|