tire 0.2.0 → 0.2.1

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