slingshot-rb 0.0.8 → 0.0.9
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 +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
|