tire 0.2.0 → 0.2.1

Sign up to get free protection for your applications and to get access to all the features.
data/.travis.yml CHANGED
@@ -6,7 +6,8 @@ script: "bundle exec rake test:unit"
6
6
 
7
7
  rvm:
8
8
  - 1.8.7
9
- - 1.9.2
9
+ - 1.9.3
10
+ - ree
10
11
 
11
12
  notifications:
12
13
  disable: true
data/README.markdown CHANGED
@@ -19,9 +19,9 @@ Installation
19
19
 
20
20
  OK. First, you need a running _ElasticSearch_ server. Thankfully, it's easy. Let's define easy:
21
21
 
22
- $ curl -k -L -o elasticsearch-0.17.2.tar.gz http://github.com/downloads/elasticsearch/elasticsearch/elasticsearch-0.17.2.tar.gz
23
- $ tar -zxvf elasticsearch-0.17.2.tar.gz
24
- $ ./elasticsearch-0.17.2/bin/elasticsearch -f
22
+ $ curl -k -L -o elasticsearch-0.17.6.tar.gz http://github.com/downloads/elasticsearch/elasticsearch/elasticsearch-0.17.6.tar.gz
23
+ $ tar -zxvf elasticsearch-0.17.6.tar.gz
24
+ $ ./elasticsearch-0.17.6/bin/elasticsearch -f
25
25
 
26
26
  See, easy. On a Mac, you can also use _Homebrew_:
27
27
 
@@ -383,7 +383,7 @@ advanced facet aggregation, highlighting, etc:
383
383
  ```
384
384
 
385
385
  Second, dynamic mapping is a godsend when you're prototyping.
386
- For serious usage, though, you'll definitely want to define a custom mapping for your models:
386
+ For serious usage, though, you'll definitely want to define a custom _mapping_ for your models:
387
387
 
388
388
  ```ruby
389
389
  class Article < ActiveRecord::Base
@@ -402,17 +402,63 @@ For serious usage, though, you'll definitely want to define a custom mapping for
402
402
 
403
403
  In this case, _only_ the defined model attributes are indexed. The `mapping` declaration creates the
404
404
  index when the class is loaded or when the importing features are used, and _only_ when it does not yet exist.
405
- (It may well be reasonable to wrap the index creation logic in a class method of your model, so you
406
- have better control on index creation when bootstrapping your application or when setting up the test suite.)
407
405
 
408
- When you want a tight grip on how the attributes are added to the index, just
409
- implement the `to_indexed_json` method in your model:
406
+ Chances are, you want to declare also a custom _settings_ for the index, such as set the number of shards,
407
+ replicas, or create elaborate analyzer chains, such as the hipster's choice: [_ngrams_](https://gist.github.com/1160430).
408
+ In this case, just wrap the `mapping` method in a `settings` one, passing it the settings as a Hash:
410
409
 
411
410
  ```ruby
412
- class Article < ActiveRecord::Base
411
+ class URL < ActiveRecord::Base
413
412
  include Tire::Model::Search
414
413
  include Tire::Model::Callbacks
415
414
 
415
+ settings :number_of_shards => 1,
416
+ :number_of_replicas => 1,
417
+ :analysis => {
418
+ :filter => {
419
+ :url_ngram => {
420
+ "type" => "nGram",
421
+ "max_gram" => 5,
422
+ "min_gram" => 3 }
423
+ },
424
+ :analyzer => {
425
+ :url_analyzer => {
426
+ "tokenizer" => "lowercase",
427
+ "filter" => ["stop", "url_ngram"],
428
+ "type" => "custom" }
429
+ }
430
+ } do
431
+ mapping { indexes :url, :type => 'string', :analyzer => "url_analyzer" }
432
+ end
433
+ end
434
+ ```
435
+
436
+ It may well be reasonable to wrap the index creation logic declared with `Tire.index('urls').create`
437
+ in a class method of your model, in a module method, etc, so have better control on index creation when bootstrapping your application with Rake tasks or when setting up the test suite.
438
+ _Tire_ will not hold that against you.
439
+
440
+ When you want a tight grip on how the attributes are added to the index, just
441
+ implement the `to_indexed_json` method in your model.
442
+
443
+ The easiest way is to customize the `to_json` serialization support of your model:
444
+
445
+ ```ruby
446
+ class Article < ActiveRecord::Base
447
+ # ...
448
+
449
+ include_root_in_json = false
450
+ def to_indexed_json
451
+ to_json :except => ['updated_at'], :methods => ['length']
452
+ end
453
+ end
454
+ ```
455
+
456
+ Of course, it may well be reasonable to define the indexed JSON from the ground up:
457
+
458
+ ```ruby
459
+ class Article < ActiveRecord::Base
460
+ # ...
461
+
416
462
  def to_indexed_json
417
463
  names = author.split(/\W/)
418
464
  last_name = names.pop
@@ -427,7 +473,6 @@ implement the `to_indexed_json` method in your model:
427
473
  }
428
474
  }.to_json
429
475
  end
430
-
431
476
  end
432
477
  ```
433
478
 
@@ -611,7 +656,6 @@ There are todos, plans and ideas, some of which are listed below, in the order o
611
656
  * Wrap all Tire functionality mixed into a model in a "forwardable" object, and proxy everything via this object. (The immediate problem: [Mongoid](http://mongoid.org/docs/indexing.html))
612
657
  * If we're not stepping on other's toes, bring Tire methods like `index`, `search`, `mapping` also to the class/instance top-level namespace.
613
658
  * Proper RDoc annotations for the source code
614
- * [Histogram](http://www.elasticsearch.org/guide/reference/api/search/facets/histogram-facet.html) facets
615
659
  * [Statistical](http://www.elasticsearch.org/guide/reference/api/search/facets/statistical-facet.html) facets
616
660
  * [Geo Distance](http://www.elasticsearch.org/guide/reference/api/search/facets/geo-distance-facet.html) facets
617
661
  * [Index aliases](http://www.elasticsearch.org/guide/reference/api/admin-indices-aliases.html) management
data/Rakefile CHANGED
@@ -14,24 +14,30 @@ end
14
14
  namespace :test do
15
15
  Rake::TestTask.new(:unit) do |test|
16
16
  test.libs << 'lib' << 'test'
17
- test.pattern = 'test/unit/*_test.rb'
17
+ test.test_files = FileList["test/unit/*_test.rb"]
18
18
  test.verbose = true
19
19
  end
20
20
  Rake::TestTask.new(:integration) do |test|
21
21
  test.libs << 'lib' << 'test'
22
- test.pattern = 'test/integration/*_test.rb'
22
+ test.test_files = FileList["test/integration/*_test.rb"]
23
23
  test.verbose = true
24
24
  end
25
25
  end
26
26
 
27
27
  # Generate documentation
28
- begin; require 'sdoc'; rescue LoadError; end
29
- require 'rdoc/task'
30
- Rake::RDocTask.new do |rdoc|
31
- rdoc.rdoc_dir = 'rdoc'
32
- rdoc.title = "Tire"
33
- rdoc.rdoc_files.include('README.markdown')
34
- rdoc.rdoc_files.include('lib/**/*.rb')
28
+ begin
29
+ begin; require 'sdoc'; rescue LoadError; end
30
+ require 'rdoc/task'
31
+ Rake::RDocTask.new do |rdoc|
32
+ rdoc.rdoc_dir = 'rdoc'
33
+ rdoc.title = "Tire"
34
+ rdoc.rdoc_files.include('README.markdown')
35
+ rdoc.rdoc_files.include('lib/**/*.rb')
36
+ end
37
+ rescue LoadError
38
+ task :rdoc do
39
+ abort "[!] RDoc gem is not available."
40
+ end
35
41
  end
36
42
 
37
43
  # Generate coverage reports
@@ -45,7 +51,7 @@ begin
45
51
  end
46
52
  rescue LoadError
47
53
  task :rcov do
48
- abort "RCov is not available. In order to run rcov, you must: sudo gem install rcov"
54
+ abort "[!] RCov gem is not available."
49
55
  end
50
56
  end
51
57
 
@@ -62,7 +62,7 @@ file ".gitignore", <<-END.gsub(/ /, '')
62
62
  tmp/**/*
63
63
  config/database.yml
64
64
  db/*.sqlite3
65
- vendor/elasticsearch-0.16.0/
65
+ vendor/elasticsearch-0.17.6/
66
66
  END
67
67
 
68
68
  git :init
@@ -71,11 +71,11 @@ git :commit => "-m 'Initial commit: Clean application'"
71
71
 
72
72
  unless (RestClient.get('http://localhost:9200') rescue false)
73
73
  COMMAND = <<-COMMAND.gsub(/^ /, '')
74
- curl -k -L -# -o elasticsearch-0.16.0.tar.gz \
75
- "http://github.com/downloads/elasticsearch/elasticsearch/elasticsearch-0.16.0.tar.gz"
76
- tar -zxf elasticsearch-0.16.0.tar.gz
77
- rm -f elasticsearch-0.16.0.tar.gz
78
- ./elasticsearch-0.16.0/bin/elasticsearch -p #{destination_root}/tmp/pids/elasticsearch.pid
74
+ curl -k -L -# -o elasticsearch-0.17.6.tar.gz \
75
+ "http://github.com/downloads/elasticsearch/elasticsearch/elasticsearch-0.17.6.tar.gz"
76
+ tar -zxf elasticsearch-0.17.6.tar.gz
77
+ rm -f elasticsearch-0.17.6.tar.gz
78
+ ./elasticsearch-0.17.6/bin/elasticsearch -p #{destination_root}/tmp/pids/elasticsearch.pid
79
79
  COMMAND
80
80
 
81
81
  puts "\n"
data/examples/tire-dsl.rb CHANGED
@@ -43,9 +43,9 @@ require 'tire'
43
43
 
44
44
  [ERROR] You don’t appear to have ElasticSearch installed. Please install and launch it with the following commands:
45
45
 
46
- curl -k -L -o elasticsearch-0.17.2.tar.gz http://github.com/downloads/elasticsearch/elasticsearch/elasticsearch-0.17.2.tar.gz
47
- tar -zxvf elasticsearch-0.17.2.tar.gz
48
- ./elasticsearch-0.17.2/bin/elasticsearch -f
46
+ curl -k -L -o elasticsearch-0.17.6.tar.gz http://github.com/downloads/elasticsearch/elasticsearch/elasticsearch-0.17.6.tar.gz
47
+ tar -zxvf elasticsearch-0.17.6.tar.gz
48
+ ./elasticsearch-0.17.6/bin/elasticsearch -f
49
49
  INSTALL
50
50
 
51
51
  ### Storing and indexing documents
@@ -581,6 +581,7 @@ end
581
581
  # * [terms](http://www.elasticsearch.org/guide/reference/api/search/facets/terms-facet.html)
582
582
  # * [date](http://www.elasticsearch.org/guide/reference/api/search/facets/date-histogram-facet.html)
583
583
  # * [range](http://www.elasticsearch.org/guide/reference/api/search/facets/range-facet.html)
584
+ # * [histogram](http://www.elasticsearch.org/guide/reference/api/search/facets/histogram-facet.html)
584
585
 
585
586
  # We have seen that _ElasticSearch_ facets enable us to fetch complex aggregations from our data.
586
587
  #
data/lib/tire/client.rb CHANGED
@@ -15,6 +15,9 @@ module Tire
15
15
  def self.delete(url)
16
16
  ::RestClient.delete url rescue nil
17
17
  end
18
+ def self.head(url)
19
+ ::RestClient.head url
20
+ end
18
21
  end
19
22
 
20
23
  end
data/lib/tire/index.rb CHANGED
@@ -9,7 +9,7 @@ module Tire
9
9
  end
10
10
 
11
11
  def exists?
12
- !!Configuration.client.get("#{Configuration.url}/#{@name}/_status")
12
+ !!Configuration.client.head("#{Configuration.url}/#{@name}")
13
13
  rescue Exception => error
14
14
  false
15
15
  end
@@ -105,7 +105,8 @@ module Tire
105
105
  when method
106
106
  options = {:page => 1, :per_page => 1000}.merge options
107
107
  while documents = klass_or_collection.send(method.to_sym, options.merge(:page => options[:page])) \
108
- and not documents.empty?
108
+ and documents.to_a.length > 0
109
+
109
110
  documents = yield documents if block_given?
110
111
 
111
112
  bulk_store documents
@@ -205,7 +206,7 @@ module Tire
205
206
  end
206
207
 
207
208
  def percolate(*args, &block)
208
- document = args.pop
209
+ document = args.shift
209
210
  type = get_type_from_document(document)
210
211
 
211
212
  document = MultiJson.decode convert_document_to_json(document)
@@ -5,14 +5,19 @@ module Tire
5
5
 
6
6
  module ClassMethods
7
7
 
8
+ def settings(*args)
9
+ @settings ||= {}
10
+ args.empty? ? (return @settings) : @settings = args.pop
11
+ yield if block_given?
12
+ end
13
+
8
14
  def mapping
15
+ @mapping ||= {}
9
16
  if block_given?
10
- @store_mapping = true
11
- yield
12
- @store_mapping = false
13
- create_index_or_update_mapping
17
+ @store_mapping = true and yield and @store_mapping = false
18
+ create_elasticsearch_index
14
19
  else
15
- @mapping ||= {}
20
+ @mapping
16
21
  end
17
22
  end
18
23
 
@@ -39,17 +44,10 @@ module Tire
39
44
  @store_mapping || false
40
45
  end
41
46
 
42
- def create_index_or_update_mapping
43
- # STDERR.puts "Creating index with mapping", mapping_to_hash.inspect
44
- # STDERR.puts "Index exists?, #{index.exists?}"
47
+ def create_elasticsearch_index
45
48
  unless elasticsearch_index.exists?
46
- elasticsearch_index.create :mappings => mapping_to_hash
47
- else
48
- # TODO: Update mapping
49
+ elasticsearch_index.create :mappings => mapping_to_hash, :settings => settings
49
50
  end
50
- rescue Exception => e
51
- # TODO: STDERR + logger
52
- raise
53
51
  end
54
52
 
55
53
  def mapping_to_hash
@@ -10,7 +10,7 @@ module Tire
10
10
  end
11
11
 
12
12
  def on_percolate(pattern=true,&block)
13
- self.percolate!(pattern)
13
+ percolate!(pattern)
14
14
  after_update_elastic_search_index(block)
15
15
  end
16
16
 
@@ -22,7 +22,7 @@ module Tire
22
22
  module InstanceMethods
23
23
 
24
24
  def percolate(&block)
25
- index.percolate document_type, self, block
25
+ index.percolate self, block
26
26
  end
27
27
 
28
28
  def percolate=(pattern)
@@ -60,7 +60,7 @@ module Tire
60
60
  #
61
61
  #
62
62
  def search(*args, &block)
63
- default_options = {:type => document_type}
63
+ default_options = {:type => document_type, :index => elasticsearch_index.name}
64
64
 
65
65
  if block_given?
66
66
  options = args.shift || {}
@@ -72,7 +72,7 @@ module Tire
72
72
  sort = Array( options[:order] || options[:sort] )
73
73
  options = default_options.update(options)
74
74
 
75
- s = Tire::Search::Search.new(elasticsearch_index.name, options)
75
+ s = Tire::Search::Search.new(options.delete(:index), options)
76
76
  s.size( options[:per_page].to_i ) if options[:per_page]
77
77
  s.from( options[:page].to_i <= 1 ? 0 : (options[:per_page].to_i * (options[:page].to_i-1)) ) if options[:page] && options[:per_page]
78
78
  s.sort do
@@ -36,25 +36,26 @@ module Tire
36
36
  end
37
37
  end
38
38
  else
39
- begin
40
- return [] if @response['hits']['total'] == 0
39
+ return [] if @response['hits']['hits'].empty?
41
40
 
42
- type = @response['hits']['hits'].first['_type']
43
- raise NoMethodError, "You have tried to eager load the model instances, " +
44
- "but Tire cannot find the model class because " +
45
- "document has no _type property." unless type
41
+ type = @response['hits']['hits'].first['_type']
42
+ raise NoMethodError, "You have tried to eager load the model instances, " +
43
+ "but Tire cannot find the model class because " +
44
+ "document has no _type property." unless type
46
45
 
46
+ begin
47
47
  klass = type.camelize.constantize
48
- ids = @response['hits']['hits'].map { |h| h['_id'] }
49
- records = @options[:load] === true ? klass.find(ids) : klass.find(ids, @options[:load])
50
-
51
- # Reorder records to preserve order from search results
52
- ids.map { |id| records.detect { |record| record.id.to_s == id.to_s } }
53
48
  rescue NameError => e
54
- raise NameError, "You have tried to eager load the model instances, but" +
49
+ raise NameError, "You have tried to eager load the model instances, but " +
55
50
  "Tire cannot find the model class '#{type.camelize}' " +
56
51
  "based on _type '#{type}'.", e.backtrace
57
52
  end
53
+
54
+ ids = @response['hits']['hits'].map { |h| h['_id'] }
55
+ records = @options[:load] === true ? klass.find(ids) : klass.find(ids, @options[:load])
56
+
57
+ # Reorder records to preserve order from search results
58
+ ids.map { |id| records.detect { |record| record.id.to_s == id.to_s } }
58
59
  end
59
60
  end
60
61
  end
@@ -70,6 +71,7 @@ module Tire
70
71
  def size
71
72
  results.size
72
73
  end
74
+ alias :length :size
73
75
 
74
76
  def [](index)
75
77
  results[index]
@@ -29,6 +29,12 @@ module Tire
29
29
 
30
30
  def range(field, ranges=[], options={})
31
31
  @value = { :range => { :field => field, :ranges => ranges }.update(options) }
32
+ self
33
+ end
34
+
35
+ def histogram(field, options={})
36
+ @value = { :histogram => (options.delete(:histogram) || {:field => field}.update(options)) }
37
+ self
32
38
  end
33
39
 
34
40
  def to_json
data/lib/tire/version.rb CHANGED
@@ -1,13 +1,21 @@
1
1
  module Tire
2
- VERSION = "0.2.0"
2
+ VERSION = "0.2.1"
3
3
 
4
4
  CHANGELOG =<<-END
5
5
  IMPORTANT CHANGES LATELY:
6
6
 
7
- # By default, results are wrapped in Item class (05a1331)
7
+ 0.2.0
8
+ ---------------------------------------------------------
9
+ # By default, results are wrapped in Item class
8
10
  # Completely rewritten ActiveModel/ActiveRecord support
9
- # Added method to items for loading the "real" model from database (f9273bc)
10
- # Added the ':load' option to eagerly load results from database (1e34cde)
11
- # Deprecated the dynamic sort methods, use the 'sort { by :field_name }' syntax
11
+ # Added infrastructure for loading "real" models from database (eagerly or in runtime)
12
+ # Deprecated the dynamic sort methods in favour of the 'sort { by :field_name }' syntax
13
+
14
+ 0.2.1
15
+ ---------------------------------------------------------
16
+ # Lighweight check for index presence
17
+ # Added the 'settings' method for models to define index settings
18
+ # Fixed errors when importing data with will_paginate vs Kaminari (MongoDB)
19
+ # Added support for histogram facets [Paco Guzman]
12
20
  END
13
21
  end
@@ -1 +1 @@
1
- {"title" : "One", "tags" : ["ruby"], "published_on" : "2011-01-01"}
1
+ {"title" : "One", "tags" : ["ruby"], "published_on" : "2011-01-01", "words" : 125}
@@ -1 +1 @@
1
- {"title" : "Two", "tags" : ["ruby", "python"], "published_on" : "2011-01-02"}
1
+ {"title" : "Two", "tags" : ["ruby", "python"], "published_on" : "2011-01-02", "words" : 250}
@@ -1 +1 @@
1
- {"title" : "Three", "tags" : ["java"], "published_on" : "2011-01-02"}
1
+ {"title" : "Three", "tags" : ["java"], "published_on" : "2011-01-02", "words" : 375}
@@ -1 +1 @@
1
- {"title" : "Four", "tags" : ["erlang"], "published_on" : "2011-01-03"}
1
+ {"title" : "Four", "tags" : ["erlang"], "published_on" : "2011-01-03", "words" : 250}
@@ -1 +1 @@
1
- {"title" : "Five", "tags" : ["javascript", "java"], "published_on" : "2011-01-04"}
1
+ {"title" : "Five", "tags" : ["javascript", "java"], "published_on" : "2011-01-04", "words" : 125}
@@ -76,6 +76,23 @@ module Tire
76
76
 
77
77
  end
78
78
 
79
+ context "histogram" do
80
+ should "return aggregated values for all results" do
81
+ s = Tire.search('articles-test') do
82
+ query { all }
83
+ facet 'words' do
84
+ histogram :words, :interval => 100
85
+ end
86
+ end
87
+
88
+ facets = s.results.facets['words']['entries']
89
+ assert_equal 3, facets.size, facets.inspect
90
+ assert_equal({"key" => 100, "count" => 2}, facets.entries[0], facets.inspect)
91
+ assert_equal({"key" => 200, "count" => 2}, facets.entries[1], facets.inspect)
92
+ assert_equal({"key" => 300, "count" => 1}, facets.entries[2], facets.inspect)
93
+ end
94
+ end
95
+
79
96
  end
80
97
 
81
98
  end
data/test/test_helper.rb CHANGED
@@ -7,7 +7,7 @@ require 'yajl/json_gem'
7
7
  require 'sqlite3'
8
8
 
9
9
  require 'shoulda'
10
- require 'turn' unless ENV["TM_FILEPATH"]
10
+ require 'turn' unless ENV["TM_FILEPATH"] || ENV["CI"]
11
11
  require 'mocha'
12
12
 
13
13
  require 'tire'
@@ -15,6 +15,7 @@ module Tire
15
15
  assert_respond_to Client::RestClient, :post
16
16
  assert_respond_to Client::RestClient, :put
17
17
  assert_respond_to Client::RestClient, :delete
18
+ assert_respond_to Client::RestClient, :head
18
19
  end
19
20
 
20
21
  end
@@ -15,12 +15,12 @@ module Tire
15
15
  end
16
16
 
17
17
  should "return true when exists" do
18
- Configuration.client.expects(:get).returns(mock_response('{"dummy":{"document":{"properties":{}}}}'))
18
+ Configuration.client.expects(:head).returns(mock_response(''))
19
19
  assert @index.exists?
20
20
  end
21
21
 
22
22
  should "return false when does not exist" do
23
- Configuration.client.expects(:get).raises(RestClient::ResourceNotFound)
23
+ Configuration.client.expects(:head).raises(RestClient::ResourceNotFound)
24
24
  assert ! @index.exists?
25
25
  end
26
26
 
@@ -247,16 +247,16 @@ module Tire
247
247
 
248
248
  context "when creating" do
249
249
 
250
- # TODO: Find a way to mock JSON paylod for Mocha with disregard to Hash entries ordering.
251
- # Ruby 1.9 brings ordered Hashes, so tests were failing.
252
-
253
250
  should "save the document with generated ID in the database" do
254
- Configuration.client.expects(:post).with("#{Configuration.url}/persistent_articles/persistent_article/",
255
- RUBY_VERSION < "1.9" ?
256
- '{"title":"Test","tags":["one","two"],"published_on":null}' :
257
- '{"published_on":null,"tags":["one","two"],"title":"Test"}'
258
- ).
259
- returns(mock_response('{"ok":true,"_id":"abc123"}'))
251
+ Configuration.client.expects(:post).
252
+ with do |url, payload|
253
+ doc = MultiJson.decode(payload)
254
+ url == "#{Configuration.url}/persistent_articles/persistent_article/" &&
255
+ doc['title'] == 'Test' &&
256
+ doc['tags'] == ['one', 'two']
257
+ doc['published_on'] == nil
258
+ end.
259
+ returns(mock_response('{"ok":true,"_id":"abc123"}'))
260
260
  article = PersistentArticle.create :title => 'Test', :tags => [:one, :two]
261
261
 
262
262
  assert article.persisted?, "#{article.inspect} should be `persisted?`"
@@ -264,12 +264,15 @@ module Tire
264
264
  end
265
265
 
266
266
  should "save the document with custom ID in the database" do
267
- Configuration.client.expects(:post).with("#{Configuration.url}/persistent_articles/persistent_article/r2d2",
268
- RUBY_VERSION < "1.9" ?
269
- '{"title":"Test","id":"r2d2","tags":null,"published_on":null}' :
270
- '{"id":"r2d2","published_on":null,"tags":null,"title":"Test"}'
271
- ).
272
- returns(mock_response('{"ok":true,"_id":"r2d2"}'))
267
+ Configuration.client.expects(:post).
268
+ with do |url, payload|
269
+ doc = MultiJson.decode(payload)
270
+ url == "#{Configuration.url}/persistent_articles/persistent_article/r2d2" &&
271
+ doc['id'] == 'r2d2' &&
272
+ doc['title'] == 'Test' &&
273
+ doc['published_on'] == nil
274
+ end.
275
+ returns(mock_response('{"ok":true,"_id":"r2d2"}'))
273
276
  article = PersistentArticle.create :id => 'r2d2', :title => 'Test'
274
277
 
275
278
  assert_equal 'r2d2', article.id
@@ -286,24 +289,29 @@ module Tire
286
289
  context "when creating" do
287
290
 
288
291
  should "set the id property" do
289
- Configuration.client.expects(:post).with("#{Configuration.url}/persistent_articles/persistent_article/",
290
- RUBY_VERSION < "1.9" ?
291
- {:title => 'Test', :tags => nil, :published_on => nil}.to_json :
292
- {:published_on => nil, :tags => nil, :title => 'Test'}.to_json
293
- ).
294
- returns(mock_response('{"ok":true,"_id":"1"}'))
292
+ Configuration.client.expects(:post).
293
+ with do |url, payload|
294
+ doc = MultiJson.decode(payload)
295
+ url == "#{Configuration.url}/persistent_articles/persistent_article/" &&
296
+ doc['id'] == nil &&
297
+ doc['title'] == 'Test'
298
+ end.
299
+ returns(mock_response('{"ok":true,"_id":"1"}'))
295
300
 
296
301
  article = PersistentArticle.create :title => 'Test'
297
302
  assert_equal '1', article.id
298
303
  end
299
304
 
300
305
  should "not set the id property if already set" do
301
- Configuration.client.expects(:post).with("#{Configuration.url}/persistent_articles/persistent_article/123",
302
- RUBY_VERSION < "1.9" ?
303
- '{"title":"Test","id":"123","tags":null,"published_on":null}' :
304
- '{"id":"123","published_on":null,"tags":null,"title":"Test"}'
305
- ).
306
- returns(mock_response('{"ok":true, "_id":"XXX"}'))
306
+ Configuration.client.expects(:post).
307
+ with do |url, payload|
308
+ doc = MultiJson.decode(payload)
309
+ url == "#{Configuration.url}/persistent_articles/persistent_article/123" &&
310
+ doc['id'] == '123' &&
311
+ doc['title'] == 'Test' &&
312
+ doc['published_on'] == nil
313
+ end.
314
+ returns(mock_response('{"ok":true, "_id":"XXX"}'))
307
315
 
308
316
  article = PersistentArticle.create :id => '123', :title => 'Test'
309
317
  assert_equal '123', article.id
@@ -314,23 +322,30 @@ module Tire
314
322
  context "when saving" do
315
323
 
316
324
  should "save the document with updated attribute" do
317
- article = PersistentArticle.new :id => 1, :title => 'Test'
318
-
319
- Configuration.client.expects(:post).with("#{Configuration.url}/persistent_articles/persistent_article/1",
320
- RUBY_VERSION < "1.9" ?
321
- '{"title":"Test","id":1,"tags":null,"published_on":null}' :
322
- '{"id":1,"published_on":null,"tags":null,"title":"Test"}'
323
- ).
324
- returns(mock_response('{"ok":true,"_id":"1"}'))
325
+ article = PersistentArticle.new :id => '1', :title => 'Test'
326
+
327
+ Configuration.client.expects(:post).
328
+ with do |url, payload|
329
+ doc = MultiJson.decode(payload)
330
+ url == "#{Configuration.url}/persistent_articles/persistent_article/1" &&
331
+ doc['id'] == '1' &&
332
+ doc['title'] == 'Test' &&
333
+ doc['published_on'] == nil
334
+ end.
335
+ returns(mock_response('{"ok":true,"_id":"1"}'))
325
336
  assert article.save
326
337
 
327
338
  article.title = 'Updated'
328
- Configuration.client.expects(:post).with("#{Configuration.url}/persistent_articles/persistent_article/1",
329
- RUBY_VERSION < "1.9" ?
330
- '{"title":"Updated","id":1,"tags":null,"published_on":null}' :
331
- '{"id":1,"published_on":null,"tags":null,"title":"Updated"}'
332
- ).
333
- returns(mock_response('{"ok":true,"_id":"1"}'))
339
+
340
+ Configuration.client.expects(:post).
341
+ with do |url, payload|
342
+ doc = MultiJson.decode(payload)
343
+ p doc
344
+ url == "#{Configuration.url}/persistent_articles/persistent_article/1" &&
345
+ doc['id'] == '1' &&
346
+ doc['title'] == 'Updated'
347
+ end.
348
+ returns(mock_response('{"ok":true,"_id":"1"}'))
334
349
  assert article.save
335
350
  end
336
351
 
@@ -352,17 +367,19 @@ module Tire
352
367
 
353
368
  should "not set the id property if already set" do
354
369
  article = PersistentArticle.new
355
- article.id = '123'
370
+ article.id = '456'
356
371
  article.title = 'Test'
357
372
 
358
- Configuration.client.expects(:post).with("#{Configuration.url}/persistent_articles/persistent_article/123",
359
- RUBY_VERSION < "1.9" ?
360
- '{"title":"Test","id":"123","tags":null,"published_on":null}' :
361
- '{"id":"123","published_on":null,"tags":null,"title":"Test"}'
362
- ).
363
- returns(mock_response('{"ok":true,"_id":"XXX"}'))
373
+ Configuration.client.expects(:post).
374
+ with do |url, payload|
375
+ doc = MultiJson.decode(payload)
376
+ url == "#{Configuration.url}/persistent_articles/persistent_article/456" &&
377
+ doc['id'] == '456' &&
378
+ doc['title'] == 'Test'
379
+ end.
380
+ returns(mock_response('{"ok":true,"_id":"XXX"}'))
364
381
  assert article.save
365
- assert_equal '123', article.id
382
+ assert_equal '456', article.id
366
383
  end
367
384
 
368
385
  end
@@ -370,13 +387,16 @@ module Tire
370
387
  context "when destroying" do
371
388
 
372
389
  should "delete the document from the database" do
373
- Configuration.client.expects(:post).with("#{Configuration.url}/persistent_articles/persistent_article/123",
374
- RUBY_VERSION < "1.9" ?
375
- '{"title":"Test","id":"123","tags":null,"published_on":null}' :
376
- '{"id":"123","published_on":null,"tags":null,"title":"Test"}'
377
- ).
378
- returns(mock_response('{"ok":true,"_id":"123"}'))
379
- Configuration.client.expects(:delete).with("#{Configuration.url}/persistent_articles/persistent_article/123")
390
+ Configuration.client.expects(:post).
391
+ with do |url, payload|
392
+ doc = MultiJson.decode(payload)
393
+ url == "#{Configuration.url}/persistent_articles/persistent_article/123" &&
394
+ doc['id'] == '123' &&
395
+ doc['title'] == 'Test'
396
+ end.returns(mock_response('{"ok":true,"_id":"123"}'))
397
+
398
+ Configuration.client.expects(:delete).
399
+ with("#{Configuration.url}/persistent_articles/persistent_article/123")
380
400
 
381
401
  article = PersistentArticle.new :id => '123', :title => 'Test'
382
402
  article.save
@@ -409,13 +429,14 @@ module Tire
409
429
  context "Persistent model with mapping definition" do
410
430
 
411
431
  should "create the index with mapping" do
412
- expected_mapping = {
432
+ expected = {
433
+ :settings => {},
413
434
  :mappings => { :persistent_article_with_mapping => {
414
435
  :properties => { :title => { :type => 'string', :analyzer => 'snowball', :boost => 10 } }
415
436
  }}
416
437
  }
417
438
 
418
- Tire::Index.any_instance.expects(:create).with(expected_mapping)
439
+ Tire::Index.any_instance.expects(:create).with(expected)
419
440
 
420
441
  class ::PersistentArticleWithMapping
421
442
 
@@ -84,6 +84,30 @@ module Tire
84
84
  ActiveModelArticleWithCustomIndexName.search { query { string 'foo' } }
85
85
  end
86
86
 
87
+ should "allow to pass custom document type" do
88
+ Tire::Search::Search.
89
+ expects(:new).
90
+ with(ActiveModelArticle.index_name, { :type => 'custom_type' }).
91
+ returns(@stub).
92
+ twice
93
+
94
+ ActiveModelArticle.search 'foo', :type => 'custom_type'
95
+ ActiveModelArticle.search( :type => 'custom_type' ) { query { string 'foo' } }
96
+ end
97
+
98
+ should "allow to pass custom index name" do
99
+ Tire::Search::Search.
100
+ expects(:new).
101
+ with('custom_index', { :type => ActiveModelArticle.document_type }).
102
+ returns(@stub).
103
+ twice
104
+
105
+ ActiveModelArticle.search 'foo', :index => 'custom_index'
106
+ ActiveModelArticle.search( :index => 'custom_index' ) do
107
+ query { string 'foo' }
108
+ end
109
+ end
110
+
87
111
  should "allow to refresh index" do
88
112
  Index.any_instance.expects(:refresh)
89
113
 
@@ -106,9 +130,11 @@ module Tire
106
130
  end
107
131
 
108
132
  context "searching with a block" do
133
+ setup do
134
+ Tire::Search::Search.any_instance.expects(:perform).returns(@stub)
135
+ end
109
136
 
110
137
  should "pass on whatever block it received" do
111
- Tire::Search::Search.any_instance.expects(:perform).returns(@stub)
112
138
  Tire::Search::Query.any_instance.expects(:string).with('foo').returns(@stub)
113
139
 
114
140
  ActiveModelArticle.search { query { string 'foo' } }
@@ -116,7 +142,6 @@ module Tire
116
142
 
117
143
  should "allow to pass block with argument to query, allowing to use local variables from outer scope" do
118
144
  Tire::Search::Query.any_instance.expects(:instance_eval).never
119
- Tire::Search::Search.any_instance.expects(:perform).returns(@stub)
120
145
  Tire::Search::Query.any_instance.expects(:string).with('foo').returns(@stub)
121
146
 
122
147
  my_query = 'foo'
@@ -245,13 +270,14 @@ module Tire
245
270
  context "with custom mapping" do
246
271
 
247
272
  should "create the index with mapping" do
248
- expected_mapping = {
273
+ expected = {
274
+ :settings => {},
249
275
  :mappings => { :model_with_custom_mapping => {
250
276
  :properties => { :title => { :type => 'string', :analyzer => 'snowball', :boost => 10 } }
251
277
  }}
252
278
  }
253
279
 
254
- Tire::Index.any_instance.expects(:create).with(expected_mapping)
280
+ Tire::Index.any_instance.expects(:create).with(expected)
255
281
 
256
282
  class ::ModelWithCustomMapping
257
283
  extend ActiveModel::Naming
@@ -270,7 +296,8 @@ module Tire
270
296
  end
271
297
 
272
298
  should "define mapping for nested properties with a block" do
273
- expected_mapping = {
299
+ expected = {
300
+ :settings => {},
274
301
  :mappings => { :model_with_nested_mapping => {
275
302
  :properties => {
276
303
  :title => { :type => 'string' },
@@ -285,7 +312,7 @@ module Tire
285
312
  }
286
313
  }}
287
314
 
288
- Tire::Index.any_instance.expects(:create).with(expected_mapping)
315
+ Tire::Index.any_instance.expects(:create).with(expected)
289
316
 
290
317
  class ::ModelWithNestedMapping
291
318
  extend ActiveModel::Naming
@@ -310,6 +337,39 @@ module Tire
310
337
 
311
338
  end
312
339
 
340
+ context "with settings" do
341
+
342
+ should "create the index with settings and mappings" do
343
+ expected_settings = {
344
+ :settings => { :number_of_shards => 1, :number_of_replicas => 1 }
345
+ }
346
+
347
+ Tire::Index.any_instance.expects(:create).with do |expected|
348
+ expected[:settings][:number_of_shards] == 1 &&
349
+ expected[:mappings].size > 0
350
+ end
351
+
352
+ class ::ModelWithCustomSettings
353
+ extend ActiveModel::Naming
354
+ extend ActiveModel::Callbacks
355
+
356
+ include Tire::Model::Search
357
+ include Tire::Model::Callbacks
358
+
359
+ settings :number_of_shards => 1, :number_of_replicas => 1 do
360
+ mapping do
361
+ indexes :title, :type => 'string'
362
+ end
363
+ end
364
+
365
+ end
366
+
367
+ assert_instance_of Hash, ModelWithCustomSettings.settings
368
+ assert_equal 1, ModelWithCustomSettings.settings[:number_of_shards]
369
+ end
370
+
371
+ end
372
+
313
373
  context "with index update callbacks" do
314
374
  setup do
315
375
  class ::ModelWithIndexCallbacks
@@ -456,9 +516,8 @@ module Tire
456
516
  should "pass the arguments to percolate" do
457
517
  filter = lambda { string 'tag:alerts' }
458
518
 
459
- Tire::Index.any_instance.expects(:percolate).with do |type,doc,query|
460
- # p [type,doc,query]
461
- type == 'active_model_article_with_callbacks' &&
519
+ Tire::Index.any_instance.expects(:percolate).with do |doc,query|
520
+ # p [doc,query]
462
521
  doc == @article &&
463
522
  query == filter
464
523
  end.returns(["alert"])
@@ -22,8 +22,9 @@ module Tire
22
22
  end
23
23
  end
24
24
 
25
- should "have size" do
25
+ should "have size/length" do
26
26
  assert_equal 3, Results::Collection.new(@default_response).size
27
+ assert_equal 3, Results::Collection.new(@default_response).length
27
28
  end
28
29
 
29
30
  should "allow access to items" do
@@ -151,8 +152,8 @@ module Tire
151
152
  {'_id' => 2},
152
153
  {'_id' => 3},
153
154
  {'_id' => 4}],
154
- 'total' => 4,
155
- 'took' => 1 } }
155
+ 'total' => 4 },
156
+ 'took' => 1 }
156
157
  @collection = Results::Collection.new( @default_response, :per_page => 1, :page => 2 )
157
158
  end
158
159
 
@@ -234,6 +235,18 @@ module Tire
234
235
  end
235
236
  end
236
237
 
238
+ should "return empty array for empty hits" do
239
+ response = { 'hits' => {
240
+ 'hits' => [],
241
+ 'total' => 4
242
+ },
243
+ 'took' => 1 }
244
+ @collection = Results::Collection.new( response, :load => true )
245
+ assert @collection.empty?, 'Collection should be empty'
246
+ assert @collection.results.empty?, 'Collection results should be empty'
247
+ assert_equal 0, @collection.size
248
+ end
249
+
237
250
  end
238
251
 
239
252
  end
@@ -22,8 +22,7 @@ module Tire
22
22
 
23
23
  end
24
24
 
25
- should "have a to_json method from Yajl" do
26
- assert defined?(Yajl)
25
+ should "have a to_json method from a JSON serialization library" do
27
26
  assert_respond_to( {}, :to_json )
28
27
  assert_equal '{"one":1}', { :one => 1}.to_json
29
28
  end
@@ -69,6 +69,18 @@ module Tire::Search
69
69
  end
70
70
  end
71
71
 
72
+ context "histogram facet" do
73
+ should "encode facet options with default key" do
74
+ f = Facet.new('histogram') { histogram :age, {:interval => 5} }
75
+ assert_equal({ :histogram => { :histogram => { :field => 'age', :interval => 5 } } }.to_json, f.to_json)
76
+ end
77
+
78
+ should "encode the JSON if define an histogram" do
79
+ f = Facet.new('histogram') { histogram :age, {:histogram => {:key_field => "age", :value_field => "age", :interval => 100}} }
80
+ assert_equal({ :histogram => { :histogram => {:key_field => "age", :value_field => "age", :interval => 100} } }.to_json, f.to_json)
81
+ end
82
+ end
83
+
72
84
  end
73
85
 
74
86
  end
data/tire.gemspec CHANGED
@@ -24,22 +24,31 @@ Gem::Specification.new do |s|
24
24
 
25
25
  s.required_rubygems_version = ">= 1.3.6"
26
26
 
27
+ # = Library dependencies
28
+ #
27
29
  s.add_dependency "rake", ">= 0.8.0"
28
30
  s.add_dependency "rest-client", "~> 1.6.0"
29
31
  s.add_dependency "multi_json", "~> 1.0"
30
32
  s.add_dependency "activemodel", "~> 3.0"
31
33
 
34
+ # = Development dependencies
35
+ #
32
36
  s.add_development_dependency "bundler", "~> 1.0.0"
33
37
  s.add_development_dependency "yajl-ruby", "~> 0.8.0"
34
- s.add_development_dependency "turn"
35
38
  s.add_development_dependency "shoulda"
36
39
  s.add_development_dependency "mocha"
37
- s.add_development_dependency "rdoc"
38
- s.add_development_dependency "sdoc"
39
- s.add_development_dependency "rcov"
40
40
  s.add_development_dependency "activerecord", "~> 3.0.7"
41
- s.add_development_dependency "supermodel"
42
41
  s.add_development_dependency "sqlite3"
42
+ s.add_development_dependency "supermodel"
43
+
44
+ # These gems are not needed for CI at <http://travis-ci.org/#!/karmi/tire>
45
+ #
46
+ unless ENV["CI"]
47
+ s.add_development_dependency "sdoc"
48
+ s.add_development_dependency "rdoc"
49
+ s.add_development_dependency "rcov"
50
+ s.add_development_dependency "turn"
51
+ end
43
52
 
44
53
  s.description = <<-DESC
45
54
  Tire is a Ruby client for the ElasticSearch search engine/database.
metadata CHANGED
@@ -5,8 +5,8 @@ version: !ruby/object:Gem::Version
5
5
  segments:
6
6
  - 0
7
7
  - 2
8
- - 0
9
- version: 0.2.0
8
+ - 1
9
+ version: 0.2.1
10
10
  platform: ruby
11
11
  authors:
12
12
  - Karel Minarik
@@ -14,7 +14,7 @@ autorequire:
14
14
  bindir: bin
15
15
  cert_chain: []
16
16
 
17
- date: 2011-08-21 00:00:00 +02:00
17
+ date: 2011-09-01 00:00:00 +02:00
18
18
  default_executable:
19
19
  dependencies:
20
20
  - !ruby/object:Gem::Dependency
@@ -100,7 +100,7 @@ dependencies:
100
100
  type: :development
101
101
  version_requirements: *id006
102
102
  - !ruby/object:Gem::Dependency
103
- name: turn
103
+ name: shoulda
104
104
  prerelease: false
105
105
  requirement: &id007 !ruby/object:Gem::Requirement
106
106
  requirements:
@@ -112,7 +112,7 @@ dependencies:
112
112
  type: :development
113
113
  version_requirements: *id007
114
114
  - !ruby/object:Gem::Dependency
115
- name: shoulda
115
+ name: mocha
116
116
  prerelease: false
117
117
  requirement: &id008 !ruby/object:Gem::Requirement
118
118
  requirements:
@@ -124,19 +124,21 @@ dependencies:
124
124
  type: :development
125
125
  version_requirements: *id008
126
126
  - !ruby/object:Gem::Dependency
127
- name: mocha
127
+ name: activerecord
128
128
  prerelease: false
129
129
  requirement: &id009 !ruby/object:Gem::Requirement
130
130
  requirements:
131
- - - ">="
131
+ - - ~>
132
132
  - !ruby/object:Gem::Version
133
133
  segments:
134
+ - 3
134
135
  - 0
135
- version: "0"
136
+ - 7
137
+ version: 3.0.7
136
138
  type: :development
137
139
  version_requirements: *id009
138
140
  - !ruby/object:Gem::Dependency
139
- name: rdoc
141
+ name: sqlite3
140
142
  prerelease: false
141
143
  requirement: &id010 !ruby/object:Gem::Requirement
142
144
  requirements:
@@ -148,7 +150,7 @@ dependencies:
148
150
  type: :development
149
151
  version_requirements: *id010
150
152
  - !ruby/object:Gem::Dependency
151
- name: sdoc
153
+ name: supermodel
152
154
  prerelease: false
153
155
  requirement: &id011 !ruby/object:Gem::Requirement
154
156
  requirements:
@@ -160,7 +162,7 @@ dependencies:
160
162
  type: :development
161
163
  version_requirements: *id011
162
164
  - !ruby/object:Gem::Dependency
163
- name: rcov
165
+ name: sdoc
164
166
  prerelease: false
165
167
  requirement: &id012 !ruby/object:Gem::Requirement
166
168
  requirements:
@@ -172,21 +174,19 @@ dependencies:
172
174
  type: :development
173
175
  version_requirements: *id012
174
176
  - !ruby/object:Gem::Dependency
175
- name: activerecord
177
+ name: rdoc
176
178
  prerelease: false
177
179
  requirement: &id013 !ruby/object:Gem::Requirement
178
180
  requirements:
179
- - - ~>
181
+ - - ">="
180
182
  - !ruby/object:Gem::Version
181
183
  segments:
182
- - 3
183
184
  - 0
184
- - 7
185
- version: 3.0.7
185
+ version: "0"
186
186
  type: :development
187
187
  version_requirements: *id013
188
188
  - !ruby/object:Gem::Dependency
189
- name: supermodel
189
+ name: rcov
190
190
  prerelease: false
191
191
  requirement: &id014 !ruby/object:Gem::Requirement
192
192
  requirements:
@@ -198,7 +198,7 @@ dependencies:
198
198
  type: :development
199
199
  version_requirements: *id014
200
200
  - !ruby/object:Gem::Dependency
201
- name: sqlite3
201
+ name: turn
202
202
  prerelease: false
203
203
  requirement: &id015 !ruby/object:Gem::Requirement
204
204
  requirements:
@@ -317,13 +317,21 @@ post_install_message: |
317
317
 
318
318
  IMPORTANT CHANGES LATELY:
319
319
 
320
- # By default, results are wrapped in Item class (05a1331)
320
+ 0.2.0
321
+ ---------------------------------------------------------
322
+ # By default, results are wrapped in Item class
321
323
  # Completely rewritten ActiveModel/ActiveRecord support
322
- # Added method to items for loading the "real" model from database (f9273bc)
323
- # Added the ':load' option to eagerly load results from database (1e34cde)
324
- # Deprecated the dynamic sort methods, use the 'sort { by :field_name }' syntax
324
+ # Added infrastructure for loading "real" models from database (eagerly or in runtime)
325
+ # Deprecated the dynamic sort methods in favour of the 'sort { by :field_name }' syntax
326
+
327
+ 0.2.1
328
+ ---------------------------------------------------------
329
+ # Lighweight check for index presence
330
+ # Added the 'settings' method for models to define index settings
331
+ # Fixed errors when importing data with will_paginate vs Kaminari (MongoDB)
332
+ # Added support for histogram facets [Paco Guzman]
325
333
 
326
- See the full changelog at <http://github.com/karmi/tire/commits/v0.2.0>.
334
+ See the full changelog at <http://github.com/karmi/tire/commits/v0.2.1>.
327
335
 
328
336
  --------------------------------------------------------------------------------
329
337