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.
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