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.
Files changed (65) hide show
  1. data/.gitignore +1 -0
  2. data/README.markdown +276 -50
  3. data/examples/rails-application-template.rb +144 -0
  4. data/examples/slingshot-dsl.rb +272 -102
  5. data/lib/slingshot.rb +13 -0
  6. data/lib/slingshot/client.rb +10 -1
  7. data/lib/slingshot/dsl.rb +17 -1
  8. data/lib/slingshot/index.rb +109 -7
  9. data/lib/slingshot/model/callbacks.rb +23 -0
  10. data/lib/slingshot/model/import.rb +18 -0
  11. data/lib/slingshot/model/indexing.rb +50 -0
  12. data/lib/slingshot/model/naming.rb +30 -0
  13. data/lib/slingshot/model/persistence.rb +34 -0
  14. data/lib/slingshot/model/persistence/attributes.rb +60 -0
  15. data/lib/slingshot/model/persistence/finders.rb +61 -0
  16. data/lib/slingshot/model/persistence/storage.rb +75 -0
  17. data/lib/slingshot/model/search.rb +97 -0
  18. data/lib/slingshot/results/collection.rb +35 -10
  19. data/lib/slingshot/results/item.rb +10 -7
  20. data/lib/slingshot/results/pagination.rb +30 -0
  21. data/lib/slingshot/rubyext/symbol.rb +11 -0
  22. data/lib/slingshot/search.rb +3 -2
  23. data/lib/slingshot/search/facet.rb +8 -6
  24. data/lib/slingshot/search/filter.rb +7 -8
  25. data/lib/slingshot/search/highlight.rb +1 -3
  26. data/lib/slingshot/search/query.rb +4 -0
  27. data/lib/slingshot/search/sort.rb +5 -0
  28. data/lib/slingshot/tasks.rb +88 -0
  29. data/lib/slingshot/version.rb +1 -1
  30. data/slingshot.gemspec +17 -4
  31. data/test/integration/active_model_searchable_test.rb +80 -0
  32. data/test/integration/active_record_searchable_test.rb +193 -0
  33. data/test/integration/highlight_test.rb +1 -1
  34. data/test/integration/index_mapping_test.rb +1 -1
  35. data/test/integration/index_store_test.rb +27 -0
  36. data/test/integration/persistent_model_test.rb +35 -0
  37. data/test/integration/query_string_test.rb +3 -3
  38. data/test/integration/sort_test.rb +2 -2
  39. data/test/models/active_model_article.rb +31 -0
  40. data/test/models/active_model_article_with_callbacks.rb +49 -0
  41. data/test/models/active_model_article_with_custom_index_name.rb +5 -0
  42. data/test/models/active_record_article.rb +12 -0
  43. data/test/models/persistent_article.rb +11 -0
  44. data/test/models/persistent_articles_with_custom_index_name.rb +10 -0
  45. data/test/models/supermodel_article.rb +22 -0
  46. data/test/models/validated_model.rb +11 -0
  47. data/test/test_helper.rb +4 -0
  48. data/test/unit/active_model_lint_test.rb +17 -0
  49. data/test/unit/client_test.rb +4 -0
  50. data/test/unit/configuration_test.rb +4 -0
  51. data/test/unit/index_test.rb +240 -17
  52. data/test/unit/model_callbacks_test.rb +90 -0
  53. data/test/unit/model_import_test.rb +71 -0
  54. data/test/unit/model_persistence_test.rb +400 -0
  55. data/test/unit/model_search_test.rb +289 -0
  56. data/test/unit/results_collection_test.rb +69 -7
  57. data/test/unit/results_item_test.rb +8 -14
  58. data/test/unit/rubyext_hash_test.rb +19 -0
  59. data/test/unit/search_facet_test.rb +25 -7
  60. data/test/unit/search_filter_test.rb +3 -0
  61. data/test/unit/search_query_test.rb +11 -0
  62. data/test/unit/search_sort_test.rb +8 -0
  63. data/test/unit/search_test.rb +14 -0
  64. data/test/unit/slingshot_test.rb +38 -0
  65. metadata +133 -26
@@ -18,9 +18,7 @@ module Slingshot
18
18
  end
19
19
 
20
20
  def to_hash
21
- h = { :fields => @fields }
22
- h.update @options
23
- return h
21
+ { :fields => @fields }.update @options
24
22
  end
25
23
 
26
24
  private
@@ -28,6 +28,10 @@ module Slingshot
28
28
  @value
29
29
  end
30
30
 
31
+ def ids(values, type)
32
+ @value = { :ids => { :values => values, :type => type } }
33
+ end
34
+
31
35
  def to_json
32
36
  @value.to_json
33
37
  end
@@ -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
@@ -1,3 +1,3 @@
1
1
  module Slingshot
2
- VERSION = "0.0.8"
2
+ VERSION = "0.0.9"
3
3
  end
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 API for ElasticSearch"
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.7.9"
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 API for the ElasticSearch search engine/database.
39
- A work in progress, currently.
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