tire 0.1.0

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 (83) hide show
  1. data/.gitignore +9 -0
  2. data/Gemfile +4 -0
  3. data/MIT-LICENSE +20 -0
  4. data/README.markdown +435 -0
  5. data/Rakefile +75 -0
  6. data/examples/dsl.rb +73 -0
  7. data/examples/rails-application-template.rb +144 -0
  8. data/examples/tire-dsl.rb +617 -0
  9. data/lib/tire.rb +35 -0
  10. data/lib/tire/client.rb +40 -0
  11. data/lib/tire/configuration.rb +29 -0
  12. data/lib/tire/dsl.rb +33 -0
  13. data/lib/tire/index.rb +209 -0
  14. data/lib/tire/logger.rb +60 -0
  15. data/lib/tire/model/callbacks.rb +23 -0
  16. data/lib/tire/model/import.rb +18 -0
  17. data/lib/tire/model/indexing.rb +50 -0
  18. data/lib/tire/model/naming.rb +30 -0
  19. data/lib/tire/model/persistence.rb +34 -0
  20. data/lib/tire/model/persistence/attributes.rb +60 -0
  21. data/lib/tire/model/persistence/finders.rb +61 -0
  22. data/lib/tire/model/persistence/storage.rb +75 -0
  23. data/lib/tire/model/search.rb +97 -0
  24. data/lib/tire/results/collection.rb +56 -0
  25. data/lib/tire/results/item.rb +39 -0
  26. data/lib/tire/results/pagination.rb +30 -0
  27. data/lib/tire/rubyext/hash.rb +3 -0
  28. data/lib/tire/rubyext/symbol.rb +11 -0
  29. data/lib/tire/search.rb +117 -0
  30. data/lib/tire/search/facet.rb +41 -0
  31. data/lib/tire/search/filter.rb +28 -0
  32. data/lib/tire/search/highlight.rb +37 -0
  33. data/lib/tire/search/query.rb +42 -0
  34. data/lib/tire/search/sort.rb +29 -0
  35. data/lib/tire/tasks.rb +88 -0
  36. data/lib/tire/version.rb +3 -0
  37. data/test/fixtures/articles/1.json +1 -0
  38. data/test/fixtures/articles/2.json +1 -0
  39. data/test/fixtures/articles/3.json +1 -0
  40. data/test/fixtures/articles/4.json +1 -0
  41. data/test/fixtures/articles/5.json +1 -0
  42. data/test/integration/active_model_searchable_test.rb +80 -0
  43. data/test/integration/active_record_searchable_test.rb +193 -0
  44. data/test/integration/facets_test.rb +65 -0
  45. data/test/integration/filters_test.rb +46 -0
  46. data/test/integration/highlight_test.rb +52 -0
  47. data/test/integration/index_mapping_test.rb +44 -0
  48. data/test/integration/index_store_test.rb +68 -0
  49. data/test/integration/persistent_model_test.rb +35 -0
  50. data/test/integration/query_string_test.rb +43 -0
  51. data/test/integration/results_test.rb +28 -0
  52. data/test/integration/sort_test.rb +36 -0
  53. data/test/models/active_model_article.rb +31 -0
  54. data/test/models/active_model_article_with_callbacks.rb +49 -0
  55. data/test/models/active_model_article_with_custom_index_name.rb +5 -0
  56. data/test/models/active_record_article.rb +12 -0
  57. data/test/models/article.rb +15 -0
  58. data/test/models/persistent_article.rb +11 -0
  59. data/test/models/persistent_articles_with_custom_index_name.rb +10 -0
  60. data/test/models/supermodel_article.rb +22 -0
  61. data/test/models/validated_model.rb +11 -0
  62. data/test/test_helper.rb +52 -0
  63. data/test/unit/active_model_lint_test.rb +17 -0
  64. data/test/unit/client_test.rb +43 -0
  65. data/test/unit/configuration_test.rb +71 -0
  66. data/test/unit/index_test.rb +390 -0
  67. data/test/unit/logger_test.rb +114 -0
  68. data/test/unit/model_callbacks_test.rb +90 -0
  69. data/test/unit/model_import_test.rb +71 -0
  70. data/test/unit/model_persistence_test.rb +400 -0
  71. data/test/unit/model_search_test.rb +289 -0
  72. data/test/unit/results_collection_test.rb +131 -0
  73. data/test/unit/results_item_test.rb +59 -0
  74. data/test/unit/rubyext_hash_test.rb +19 -0
  75. data/test/unit/search_facet_test.rb +69 -0
  76. data/test/unit/search_filter_test.rb +36 -0
  77. data/test/unit/search_highlight_test.rb +46 -0
  78. data/test/unit/search_query_test.rb +55 -0
  79. data/test/unit/search_sort_test.rb +50 -0
  80. data/test/unit/search_test.rb +204 -0
  81. data/test/unit/tire_test.rb +55 -0
  82. data/tire.gemspec +54 -0
  83. metadata +372 -0
@@ -0,0 +1,44 @@
1
+ require 'test_helper'
2
+
3
+ module Tire
4
+
5
+ class IndexMappingIntegrationTest < Test::Unit::TestCase
6
+ include Test::Integration
7
+
8
+ context "Default mapping" do
9
+ teardown { Tire.index('mapped-index').delete }
10
+
11
+ should "create and return the default mapping" do
12
+
13
+ index = Tire.index 'mapped-index' do
14
+ create
15
+ store :article, :title => 'One'
16
+ refresh
17
+ end
18
+ sleep 1.5
19
+
20
+ assert_equal 'string', index.mapping['article']['properties']['title']['type'], index.mapping.inspect
21
+ assert_nil index.mapping['article']['properties']['title']['boost'], index.mapping.inspect
22
+ end
23
+ end
24
+
25
+ context "Creating index with mapping" do
26
+ teardown { Tire.index('mapped-index').delete; sleep 1 }
27
+
28
+ should "create the specified mapping" do
29
+
30
+ index = Tire.index 'mapped-index' do
31
+ create :mappings => { :article => { :properties => { :title => { :type => 'string', :boost => 2.0, :store => 'yes' } } } }
32
+ end
33
+ sleep 1
34
+
35
+ # p index.mapping
36
+ assert_equal 2.0, index.mapping['article']['properties']['title']['boost'], index.mapping.inspect
37
+ assert_equal 'yes', index.mapping['article']['properties']['title']['store'], index.mapping.inspect
38
+
39
+ end
40
+ end
41
+
42
+ end
43
+
44
+ end
@@ -0,0 +1,68 @@
1
+ require 'test_helper'
2
+
3
+ module Tire
4
+
5
+ class IndexStoreIntegrationTest < Test::Unit::TestCase
6
+ include Test::Integration
7
+
8
+ context "Storing the documents in index" do
9
+
10
+ teardown { Tire.index('articles-test-ids').delete }
11
+
12
+ should "store hashes under their IDs" do
13
+
14
+ Tire.index 'articles-test-ids' do
15
+ delete
16
+ create
17
+
18
+ store :id => 1, :title => 'One'
19
+ store :id => 2, :title => 'Two'
20
+ store :id => 3, :title => 'Three'
21
+ store :id => 4, :title => 'Four'
22
+ store :id => 4, :title => 'Four'
23
+
24
+ refresh
25
+ end
26
+
27
+ s = Tire.search('articles-test-ids') { query { string '*' } }
28
+
29
+ assert_equal 4, s.results.count
30
+
31
+ document = Tire.index('articles-test-ids').retrieve :document, 4
32
+ assert_equal 'Four', document.title
33
+ assert_equal 2, document._version.to_i
34
+
35
+ end
36
+
37
+ end
38
+
39
+ context "Removing documents from the index" do
40
+
41
+ teardown { Tire.index('articles-test-remove').delete }
42
+
43
+ setup do
44
+ Tire.index 'articles-test-remove' do
45
+ delete
46
+ create
47
+ store :id => 1, :title => 'One'
48
+ store :id => 2, :title => 'Two'
49
+ refresh
50
+ end
51
+ end
52
+
53
+ should "remove document from the index" do
54
+
55
+ assert_equal 2, Tire.search('articles-test-remove') { query { string '*' } }.results.count
56
+
57
+ assert_nothing_raised do
58
+ assert Tire.index('articles-test-remove').remove 1
59
+ assert ! Tire.index('articles-test-remove').remove(1)
60
+ end
61
+
62
+ end
63
+
64
+ end
65
+
66
+ end
67
+
68
+ end
@@ -0,0 +1,35 @@
1
+ require 'test_helper'
2
+
3
+ module Tire
4
+
5
+ class PersistentModelIntegrationTest < Test::Unit::TestCase
6
+ include Test::Integration
7
+
8
+ def setup
9
+ super
10
+ PersistentArticle.index.delete
11
+ end
12
+
13
+ def teardown
14
+ super
15
+ PersistentArticle.index.delete
16
+ end
17
+
18
+ context "PersistentModel" do
19
+
20
+ should "save documents into index and find them by IDs" do
21
+ one = PersistentArticle.create :id => 1, :title => 'One'
22
+ two = PersistentArticle.create :id => 2, :title => 'Two'
23
+
24
+ PersistentArticle.index.refresh
25
+ sleep(1.5)
26
+
27
+ results = PersistentArticle.find [1, 2]
28
+
29
+ assert_equal 2, results.size
30
+
31
+ end
32
+ end
33
+
34
+ end
35
+ end
@@ -0,0 +1,43 @@
1
+ require 'test_helper'
2
+
3
+ module Tire
4
+
5
+ class QueryStringIntegrationTest < Test::Unit::TestCase
6
+ include Test::Integration
7
+
8
+ context "Searching for query string" do
9
+
10
+ should "find article by title" do
11
+ q = 'title:one'
12
+ assert_equal 1, search(q).results.count
13
+ assert_equal 'One', search(q).results.first[:title]
14
+ end
15
+
16
+ should "find articles by title with boosting" do
17
+ q = 'title:one^100 OR title:two'
18
+ assert_equal 2, search(q).results.count
19
+ assert_equal 'One', search(q).results.first[:title]
20
+ end
21
+
22
+ should "find articles by tags" do
23
+ q = 'tags:ruby AND tags:python'
24
+ assert_equal 1, search(q).results.count
25
+ assert_equal 'Two', search(q).results.first[:title]
26
+ end
27
+
28
+ should "find any article with tags" do
29
+ q = 'tags:ruby OR tags:python OR tags:java'
30
+ assert_equal 4, search(q).results.count
31
+ end
32
+
33
+ end
34
+
35
+ private
36
+
37
+ def search(q)
38
+ Tire.search('articles-test') { query { string q } }
39
+ end
40
+
41
+ end
42
+
43
+ end
@@ -0,0 +1,28 @@
1
+ require 'test_helper'
2
+
3
+ module Tire
4
+
5
+ class ResultsIntegrationTest < Test::Unit::TestCase
6
+ include Test::Integration
7
+
8
+ context "Query results" do
9
+
10
+ should "allow easy access to returned documents" do
11
+ q = 'title:one'
12
+ s = Tire.search('articles-test') { query { string q } }
13
+ assert_equal 'One', s.results.first.title
14
+ assert_equal 'ruby', s.results.first.tags[0]
15
+ end
16
+
17
+ should "allow easy access to returned documents with limited fields" do
18
+ q = 'title:one'
19
+ s = Tire.search('articles-test') { query { string q }.fields :title }
20
+ assert_equal 'One', s.results.first.title
21
+ assert_nil s.results.first.tags
22
+ end
23
+
24
+ end
25
+
26
+ end
27
+
28
+ end
@@ -0,0 +1,36 @@
1
+ require 'test_helper'
2
+
3
+ module Tire
4
+
5
+ class SortIntegrationTest < Test::Unit::TestCase
6
+ include Test::Integration
7
+
8
+ context "Sort" do
9
+
10
+ should "sort by title" do
11
+ q = '*'
12
+ s = Tire.search('articles-test') do
13
+ query { string q }
14
+ sort { title }
15
+ end
16
+
17
+ assert_equal 5, s.results.count
18
+ assert_equal 'Five', s.results.first[:title]
19
+ end
20
+
21
+ should "sort by title, descending" do
22
+ q = '*'
23
+ s = Tire.search('articles-test') do
24
+ query { string q }
25
+ sort { title :desc }
26
+ end
27
+
28
+ assert_equal 5, s.results.count
29
+ assert_equal 'Two', s.results.first[:title]
30
+ end
31
+
32
+ end
33
+
34
+ end
35
+
36
+ end
@@ -0,0 +1,31 @@
1
+ # Example ActiveModel class
2
+
3
+ require 'rubygems'
4
+ require 'active_model'
5
+
6
+ class ActiveModelArticle
7
+
8
+ extend ActiveModel::Naming
9
+ include ActiveModel::AttributeMethods
10
+ include ActiveModel::Serialization
11
+ include ActiveModel::Serializers::JSON
12
+
13
+ include Tire::Model::Search
14
+
15
+ attr_reader :attributes
16
+
17
+ def initialize(attributes = {})
18
+ @attributes = attributes
19
+ end
20
+
21
+ def id; attributes['id'] || attributes['_id']; end
22
+ def id=(value); attributes['id'] = value; end
23
+
24
+ def method_missing(name, *args, &block)
25
+ attributes[name.to_sym] || attributes[name.to_s] || super
26
+ end
27
+
28
+ def persisted?; true; end
29
+ def save; true; end
30
+
31
+ end
@@ -0,0 +1,49 @@
1
+ # Example ActiveModel class with callbacks
2
+
3
+ require 'rubygems'
4
+ require 'active_model'
5
+
6
+ class ActiveModelArticleWithCallbacks
7
+
8
+ include ActiveModel::AttributeMethods
9
+ include ActiveModel::Validations
10
+ include ActiveModel::Serialization
11
+ include ActiveModel::Serializers::JSON
12
+ include ActiveModel::Naming
13
+
14
+ extend ActiveModel::Callbacks
15
+ define_model_callbacks :save, :destroy
16
+
17
+ include Tire::Model::Search
18
+ include Tire::Model::Callbacks
19
+
20
+ attr_reader :attributes
21
+
22
+ def initialize(attributes = {})
23
+ @attributes = attributes
24
+ end
25
+
26
+ def method_missing(id, *args, &block)
27
+ attributes[id.to_sym] || attributes[id.to_s] || super
28
+ end
29
+
30
+ def persisted?
31
+ true
32
+ end
33
+
34
+ def save
35
+ _run_save_callbacks do
36
+ STDERR.puts "[Saving ...]"
37
+ end
38
+ end
39
+
40
+ def destroy
41
+ _run_destroy_callbacks do
42
+ STDERR.puts "[Destroying ...]"
43
+ @destroyed = true
44
+ end
45
+ end
46
+
47
+ def destroyed?; !!@destroyed; end
48
+
49
+ end
@@ -0,0 +1,5 @@
1
+ # Example ActiveModel class with custom index name
2
+
3
+ class ActiveModelArticleWithCustomIndexName < ActiveModelArticle
4
+ index_name 'custom-index-name'
5
+ end
@@ -0,0 +1,12 @@
1
+ require 'rubygems'
2
+ require 'active_record'
3
+
4
+ class ActiveRecordArticle < ActiveRecord::Base
5
+ include Tire::Model::Search
6
+ include Tire::Model::Callbacks
7
+
8
+ mapping do
9
+ indexes :title, :type => 'string', :boost => 10, :analyzer => 'snowball'
10
+ indexes :created_at, :type => 'date'
11
+ end
12
+ end
@@ -0,0 +1,15 @@
1
+ # Example non-ActiveModel custom wrapper for result item
2
+
3
+ class Article
4
+ attr_reader :id, :title, :body
5
+
6
+ def initialize(attributes={})
7
+ attributes.each { |k,v| instance_variable_set(:"@#{k}", v) }
8
+ end
9
+
10
+ def to_json
11
+ { :id => @id, :title => @title, :body => @body }.to_json
12
+ end
13
+
14
+ alias :to_indexed_json :to_json
15
+ end
@@ -0,0 +1,11 @@
1
+ # Example class with ElasticSearch persistence
2
+
3
+ class PersistentArticle
4
+
5
+ include Tire::Model::Persistence
6
+
7
+ property :title
8
+ property :published_on
9
+ property :tags
10
+
11
+ end
@@ -0,0 +1,10 @@
1
+ # Example class with ElasticSearch persistence and custom index name
2
+
3
+ class PersistentArticleWithCustomIndexName
4
+
5
+ include Tire::Model::Persistence
6
+
7
+ property :title
8
+
9
+ index_name 'custom-index-name'
10
+ end
@@ -0,0 +1,22 @@
1
+ # Example ActiveModel class for testing :searchable mode
2
+
3
+ require 'rubygems'
4
+ require 'supermodel'
5
+
6
+ class SupermodelArticle < SuperModel::Base
7
+ include SuperModel::RandomID
8
+
9
+ include Tire::Model::Search
10
+ include Tire::Model::Callbacks
11
+
12
+ mapping do
13
+ indexes :title, :type => 'string', :boost => 15, :analyzer => 'czech'
14
+ end
15
+
16
+ alias :persisted? :exists?
17
+
18
+ def destroyed?
19
+ !self.class.find(self.id) rescue true
20
+ end
21
+
22
+ end
@@ -0,0 +1,11 @@
1
+ # Example ActiveModel with validations
2
+
3
+ class ValidatedModel
4
+
5
+ include Tire::Model::Persistence
6
+
7
+ property :name
8
+
9
+ validates_presence_of :name
10
+
11
+ end
@@ -0,0 +1,52 @@
1
+ require 'rubygems'
2
+ require 'test/unit'
3
+ require 'shoulda'
4
+ require 'mocha'
5
+ require 'turn' unless ENV["TM_FILEPATH"]
6
+ require 'pathname'
7
+
8
+ require 'tire'
9
+
10
+ Dir[File.dirname(__FILE__) + '/models/**/*.rb'].each { |m| require m }
11
+
12
+ class Test::Unit::TestCase
13
+
14
+ def mock_response(body, code=200)
15
+ stub(:body => body, :code => code)
16
+ end
17
+
18
+ def fixtures_path
19
+ Pathname( File.expand_path( 'fixtures', File.dirname(__FILE__) ) )
20
+ end
21
+
22
+ def fixture_file(path)
23
+ File.read File.expand_path( path, fixtures_path )
24
+ end
25
+
26
+ end
27
+
28
+ module Test::Integration
29
+ URL = "http://localhost:9200"
30
+
31
+ def setup
32
+ begin
33
+ ::RestClient.get URL
34
+ rescue Errno::ECONNREFUSED
35
+ abort "\n\n#{'-'*87}\n[ABORTED] You have to run ElasticSearch on #{URL} for integration tests\n#{'-'*87}\n\n"
36
+ end
37
+
38
+ ::RestClient.delete "#{URL}/articles-test" rescue nil
39
+ ::RestClient.post "#{URL}/articles-test", ''
40
+ fixtures_path.join('articles').entries.each do |f|
41
+ filename = f.to_s
42
+ next if filename =~ /^\./
43
+ ::RestClient.put "#{URL}/articles-test/article/#{File.basename(filename, '.*')}",
44
+ fixtures_path.join('articles').join(f).read
45
+ end
46
+ ::RestClient.post "#{URL}/articles-test/_refresh", ''
47
+ end
48
+
49
+ def teardown
50
+ ::RestClient.delete "#{URL}/articles-test" rescue nil
51
+ end
52
+ end