elasticsearch-model 0.0.1 → 0.1.0.rc1

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 (64) hide show
  1. data/.gitignore +3 -0
  2. data/LICENSE.txt +1 -1
  3. data/README.md +669 -8
  4. data/Rakefile +52 -0
  5. data/elasticsearch-model.gemspec +48 -17
  6. data/examples/activerecord_article.rb +77 -0
  7. data/examples/activerecord_associations.rb +153 -0
  8. data/examples/couchbase_article.rb +66 -0
  9. data/examples/datamapper_article.rb +71 -0
  10. data/examples/mongoid_article.rb +68 -0
  11. data/examples/ohm_article.rb +70 -0
  12. data/examples/riak_article.rb +52 -0
  13. data/gemfiles/3.gemfile +11 -0
  14. data/gemfiles/4.gemfile +11 -0
  15. data/lib/elasticsearch/model.rb +151 -1
  16. data/lib/elasticsearch/model/adapter.rb +145 -0
  17. data/lib/elasticsearch/model/adapters/active_record.rb +97 -0
  18. data/lib/elasticsearch/model/adapters/default.rb +44 -0
  19. data/lib/elasticsearch/model/adapters/mongoid.rb +90 -0
  20. data/lib/elasticsearch/model/callbacks.rb +35 -0
  21. data/lib/elasticsearch/model/client.rb +61 -0
  22. data/lib/elasticsearch/model/importing.rb +94 -0
  23. data/lib/elasticsearch/model/indexing.rb +332 -0
  24. data/lib/elasticsearch/model/naming.rb +101 -0
  25. data/lib/elasticsearch/model/proxy.rb +127 -0
  26. data/lib/elasticsearch/model/response.rb +70 -0
  27. data/lib/elasticsearch/model/response/base.rb +44 -0
  28. data/lib/elasticsearch/model/response/pagination.rb +96 -0
  29. data/lib/elasticsearch/model/response/records.rb +71 -0
  30. data/lib/elasticsearch/model/response/result.rb +50 -0
  31. data/lib/elasticsearch/model/response/results.rb +32 -0
  32. data/lib/elasticsearch/model/searching.rb +107 -0
  33. data/lib/elasticsearch/model/serializing.rb +35 -0
  34. data/lib/elasticsearch/model/support/forwardable.rb +44 -0
  35. data/lib/elasticsearch/model/version.rb +1 -1
  36. data/test/integration/active_record_associations_parent_child.rb +138 -0
  37. data/test/integration/active_record_associations_test.rb +306 -0
  38. data/test/integration/active_record_basic_test.rb +139 -0
  39. data/test/integration/active_record_import_test.rb +74 -0
  40. data/test/integration/active_record_namespaced_model_test.rb +49 -0
  41. data/test/integration/active_record_pagination_test.rb +109 -0
  42. data/test/integration/mongoid_basic_test.rb +178 -0
  43. data/test/test_helper.rb +57 -0
  44. data/test/unit/adapter_active_record_test.rb +93 -0
  45. data/test/unit/adapter_default_test.rb +31 -0
  46. data/test/unit/adapter_mongoid_test.rb +87 -0
  47. data/test/unit/adapter_test.rb +69 -0
  48. data/test/unit/callbacks_test.rb +30 -0
  49. data/test/unit/client_test.rb +27 -0
  50. data/test/unit/importing_test.rb +97 -0
  51. data/test/unit/indexing_test.rb +364 -0
  52. data/test/unit/module_test.rb +46 -0
  53. data/test/unit/naming_test.rb +76 -0
  54. data/test/unit/proxy_test.rb +88 -0
  55. data/test/unit/response_base_test.rb +40 -0
  56. data/test/unit/response_pagination_test.rb +159 -0
  57. data/test/unit/response_records_test.rb +87 -0
  58. data/test/unit/response_result_test.rb +52 -0
  59. data/test/unit/response_results_test.rb +31 -0
  60. data/test/unit/response_test.rb +57 -0
  61. data/test/unit/searching_search_request_test.rb +73 -0
  62. data/test/unit/searching_test.rb +39 -0
  63. data/test/unit/serializing_test.rb +17 -0
  64. metadata +418 -11
data/Rakefile CHANGED
@@ -1 +1,53 @@
1
1
  require "bundler/gem_tasks"
2
+
3
+ desc "Run unit tests"
4
+ task :default => 'test:unit'
5
+ task :test => 'test:unit'
6
+
7
+ # ----- Test tasks ------------------------------------------------------------
8
+
9
+ require 'rake/testtask'
10
+ namespace :test do
11
+ task :ci_reporter do
12
+ ENV['CI_REPORTS'] ||= 'tmp/reports'
13
+ require 'ci/reporter/rake/minitest'
14
+ Rake::Task['ci:setup:minitest'].invoke
15
+ end
16
+
17
+ Rake::TestTask.new(:unit) do |test|
18
+ Rake::Task['test:ci_reporter'].invoke if ENV['CI']
19
+ test.libs << 'lib' << 'test'
20
+ test.test_files = FileList["test/unit/**/*_test.rb"]
21
+ # test.verbose = true
22
+ # test.warning = true
23
+ end
24
+
25
+ Rake::TestTask.new(:integration) do |test|
26
+ Rake::Task['test:ci_reporter'].invoke if ENV['CI']
27
+ test.libs << 'lib' << 'test'
28
+ test.test_files = FileList["test/integration/**/*_test.rb"]
29
+ end
30
+
31
+ Rake::TestTask.new(:all) do |test|
32
+ Rake::Task['test:ci_reporter'].invoke if ENV['CI']
33
+ test.libs << 'lib' << 'test'
34
+ test.test_files = FileList["test/unit/**/*_test.rb", "test/integration/**/*_test.rb"]
35
+ end
36
+ end
37
+
38
+ # ----- Documentation tasks ---------------------------------------------------
39
+
40
+ require 'yard'
41
+ YARD::Rake::YardocTask.new(:doc) do |t|
42
+ t.options = %w| --embed-mixins --markup=markdown |
43
+ end
44
+
45
+ # ----- Code analysis tasks ---------------------------------------------------
46
+
47
+ if defined?(RUBY_VERSION) && RUBY_VERSION > '1.9'
48
+ require 'cane/rake_task'
49
+ Cane::RakeTask.new(:quality) do |cane|
50
+ cane.abc_max = 15
51
+ cane.no_style = true
52
+ end
53
+ end
@@ -3,21 +3,52 @@ lib = File.expand_path('../lib', __FILE__)
3
3
  $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
4
  require 'elasticsearch/model/version'
5
5
 
6
- Gem::Specification.new do |spec|
7
- spec.name = "elasticsearch-model"
8
- spec.version = Elasticsearch::Model::VERSION
9
- spec.authors = ["Karel Minarik"]
10
- spec.email = ["karel.minarik@elasticsearch.org"]
11
- spec.description = %q{Elasticsearch gem integration for ActiveModel/ActiveRecord (WIP)}
12
- spec.summary = spec.description
13
- spec.homepage = ""
14
- spec.license = "Apache 2"
15
-
16
- spec.files = `git ls-files`.split($/)
17
- spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
18
- spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
19
- spec.require_paths = ["lib"]
20
-
21
- spec.add_development_dependency "bundler", "~> 1.3"
22
- spec.add_development_dependency "rake"
6
+ Gem::Specification.new do |s|
7
+ s.name = "elasticsearch-model"
8
+ s.version = Elasticsearch::Model::VERSION
9
+ s.authors = ["Karel Minarik"]
10
+ s.email = ["karel.minarik@elasticsearch.org"]
11
+ s.description = "ActiveModel/Record integrations for Elasticsearch."
12
+ s.summary = "ActiveModel/Record integrations for Elasticsearch."
13
+ s.homepage = "https://github.com/elasticsearch/elasticsearch-rails/"
14
+ s.license = "Apache 2"
15
+
16
+ s.files = `git ls-files`.split($/)
17
+ s.executables = s.files.grep(%r{^bin/}) { |f| File.basename(f) }
18
+ s.test_files = s.files.grep(%r{^(test|spec|features)/})
19
+ s.require_paths = ["lib"]
20
+
21
+ s.extra_rdoc_files = [ "README.md", "LICENSE.txt" ]
22
+ s.rdoc_options = [ "--charset=UTF-8" ]
23
+
24
+ s.add_dependency "elasticsearch", '~> 0.4'
25
+ s.add_dependency "hashie"
26
+
27
+ s.add_development_dependency "bundler", "~> 1.3"
28
+ s.add_development_dependency "rake"
29
+
30
+ s.add_development_dependency "elasticsearch-extensions"
31
+
32
+ s.add_development_dependency "sqlite3"
33
+ s.add_development_dependency "activesupport", "> 3.0"
34
+ s.add_development_dependency "activemodel", "> 3.0"
35
+ s.add_development_dependency "activerecord", "> 4.0"
36
+
37
+ s.add_development_dependency "oj"
38
+ s.add_development_dependency "kaminari"
39
+
40
+ s.add_development_dependency "shoulda-context"
41
+ s.add_development_dependency "mocha"
42
+ s.add_development_dependency "turn"
43
+ s.add_development_dependency "yard"
44
+ s.add_development_dependency "ruby-prof"
45
+ s.add_development_dependency "pry"
46
+ s.add_development_dependency "ci_reporter"
47
+
48
+ if defined?(RUBY_VERSION) && RUBY_VERSION > '1.9'
49
+ s.add_development_dependency "simplecov"
50
+ s.add_development_dependency "cane"
51
+ s.add_development_dependency "require-prof"
52
+ s.add_development_dependency "coveralls"
53
+ end
23
54
  end
@@ -0,0 +1,77 @@
1
+ # ActiveRecord and Elasticsearch
2
+ # ==============================
3
+ #
4
+ # https://github.com/rails/rails/tree/master/activerecord
5
+
6
+ $LOAD_PATH.unshift File.expand_path('../../lib', __FILE__)
7
+
8
+ require 'pry'
9
+ Pry.config.history.file = File.expand_path('../../tmp/elasticsearch_development.pry', __FILE__)
10
+
11
+ require 'logger'
12
+ require 'ansi/core'
13
+ require 'active_record'
14
+ require 'kaminari'
15
+
16
+ require 'elasticsearch/model'
17
+
18
+ ActiveRecord::Base.logger = ActiveSupport::Logger.new(STDOUT)
19
+ ActiveRecord::Base.establish_connection( adapter: 'sqlite3', database: ":memory:" )
20
+
21
+ ActiveRecord::Schema.define(version: 1) do
22
+ create_table :articles do |t|
23
+ t.string :title
24
+ t.date :published_at
25
+ t.timestamps
26
+ end
27
+ end
28
+
29
+ Kaminari::Hooks.init
30
+
31
+ class Article < ActiveRecord::Base
32
+ end
33
+
34
+ # Store data
35
+ #
36
+ Article.delete_all
37
+ Article.create title: 'Foo'
38
+ Article.create title: 'Bar'
39
+ Article.create title: 'Foo Foo'
40
+
41
+ # Index data
42
+ #
43
+ client = Elasticsearch::Client.new log:true
44
+
45
+ # client.indices.delete index: 'articles' rescue nil
46
+ # client.indices.create index: 'articles', body: { mappings: { article: { dynamic: 'strict' }, properties: {} } }
47
+
48
+ client.indices.delete index: 'articles' rescue nil
49
+ client.bulk index: 'articles',
50
+ type: 'article',
51
+ body: Article.all.as_json.map { |a| { index: { _id: a.delete('id'), data: a } } },
52
+ refresh: true
53
+
54
+ # Extend the model with Elasticsearch support
55
+ #
56
+ Article.__send__ :include, Elasticsearch::Model
57
+ # Article.__send__ :include, Elasticsearch::Model::Callbacks
58
+
59
+ # ActiveRecord::Base.logger.silence do
60
+ # 10_000.times do |i|
61
+ # Article.create title: "Foo #{i}"
62
+ # end
63
+ # end
64
+
65
+ puts '', '-'*Pry::Terminal.width!
66
+
67
+ Elasticsearch::Model.client = Elasticsearch::Client.new log: true
68
+
69
+ response = Article.search 'foo';
70
+
71
+ p response.size
72
+ p response.results.size
73
+ p response.records.size
74
+
75
+ Pry.start(binding, prompt: lambda { |obj, nest_level, _| '> ' },
76
+ input: StringIO.new('response.records.to_a'),
77
+ quiet: true)
@@ -0,0 +1,153 @@
1
+ # ActiveRecord associations and Elasticsearch
2
+ # ===========================================
3
+ #
4
+ # https://github.com/rails/rails/tree/master/activerecord
5
+ # http://guides.rubyonrails.org/association_basics.html
6
+
7
+ $LOAD_PATH.unshift File.expand_path('../../lib', __FILE__)
8
+
9
+ require 'pry'
10
+ Pry.config.history.file = File.expand_path('../../tmp/elasticsearch_development.pry', __FILE__)
11
+
12
+ require 'logger'
13
+ require 'ansi/core'
14
+ require 'active_record'
15
+
16
+ require 'elasticsearch/model'
17
+
18
+ ActiveRecord::Base.logger = ActiveSupport::Logger.new(STDOUT)
19
+ ActiveRecord::Base.establish_connection( adapter: 'sqlite3', database: ":memory:" )
20
+
21
+ # ----- Schema definition -------------------------------------------------------------------------
22
+
23
+ ActiveRecord::Schema.define(version: 1) do
24
+ create_table :categories do |t|
25
+ t.string :title
26
+ t.timestamps
27
+ end
28
+
29
+ create_table :authors do |t|
30
+ t.string :first_name, :last_name
31
+ t.timestamps
32
+ end
33
+
34
+ create_table :authorships do |t|
35
+ t.references :article
36
+ t.references :author
37
+ t.timestamps
38
+ end
39
+
40
+ create_table :articles do |t|
41
+ t.string :title
42
+ t.timestamps
43
+ end
44
+
45
+ create_table :articles_categories, id: false do |t|
46
+ t.references :article, :category
47
+ end
48
+
49
+ create_table :comments do |t|
50
+ t.string :text
51
+ t.references :article
52
+ t.timestamps
53
+ end
54
+ add_index(:comments, :article_id)
55
+ end
56
+
57
+ # ----- Model definitions -------------------------------------------------------------------------
58
+
59
+ class Category < ActiveRecord::Base
60
+ has_and_belongs_to_many :articles
61
+ end
62
+
63
+ class Author < ActiveRecord::Base
64
+ has_many :authorships
65
+
66
+ def full_name
67
+ [first_name, last_name].compact.join(' ')
68
+ end
69
+ end
70
+
71
+ class Authorship < ActiveRecord::Base
72
+ belongs_to :author
73
+ belongs_to :article, touch: true
74
+ end
75
+
76
+ class Article < ActiveRecord::Base
77
+ has_and_belongs_to_many :categories, after_add: [ lambda { |a,c| a.__elasticsearch__.index_document } ],
78
+ after_remove: [ lambda { |a,c| a.__elasticsearch__.index_document } ]
79
+ has_many :authorships
80
+ has_many :authors, through: :authorships
81
+ has_many :comments
82
+ end
83
+
84
+ class Comment < ActiveRecord::Base
85
+ belongs_to :article, touch: true
86
+ end
87
+
88
+ # ----- Search integration ------------------------------------------------------------------------
89
+
90
+ module Searchable
91
+ extend ActiveSupport::Concern
92
+
93
+ included do
94
+ include Elasticsearch::Model
95
+ include Elasticsearch::Model::Callbacks
96
+
97
+ __elasticsearch__.client = Elasticsearch::Client.new log: true
98
+ __elasticsearch__.client.transport.logger.formatter = proc { |s, d, p, m| "\e[32m#{m}\n\e[0m" }
99
+
100
+ include Indexing
101
+ after_touch() { __elasticsearch__.index_document }
102
+ end
103
+
104
+ module Indexing
105
+
106
+ # Customize the JSON serialization for Elasticsearch
107
+ def as_indexed_json(options={})
108
+ self.as_json(
109
+ include: { categories: { only: :title},
110
+ authors: { methods: [:full_name], only: [:full_name] },
111
+ comments: { only: :text }
112
+ })
113
+ end
114
+ end
115
+ end
116
+
117
+ Article.__send__ :include, Searchable
118
+
119
+ # ----- Insert data -------------------------------------------------------------------------------
120
+
121
+ # Create category
122
+ #
123
+ category = Category.create title: 'One'
124
+
125
+ # Create author
126
+ #
127
+ author = Author.create first_name: 'John', last_name: 'Smith'
128
+
129
+ # Create article
130
+
131
+ article = Article.create title: 'First Article'
132
+
133
+ # Assign category
134
+ #
135
+ article.categories << category
136
+
137
+ # Assign author
138
+ #
139
+ article.authors << author
140
+
141
+ # Add comment
142
+ #
143
+ article.comments.create text: 'First comment'
144
+
145
+ # Load
146
+ #
147
+ article = Article.all.includes(:categories, :authors, :comments).first
148
+
149
+ # ----- Pry ---------------------------------------------------------------------------------------
150
+
151
+ Pry.start(binding, prompt: lambda { |obj, nest_level, _| '> ' },
152
+ input: StringIO.new('puts "\n\narticle.as_indexed_json\n"; article.as_indexed_json'),
153
+ quiet: true)
@@ -0,0 +1,66 @@
1
+ # Couchbase and Elasticsearch
2
+ # ===========================
3
+ #
4
+ # https://github.com/couchbase/couchbase-ruby-model
5
+
6
+ $LOAD_PATH.unshift File.expand_path('../../lib', __FILE__)
7
+
8
+ require 'pry'
9
+ Pry.config.history.file = File.expand_path('../../tmp/elasticsearch_development.pry', __FILE__)
10
+
11
+ require 'logger'
12
+ require 'couchbase/model'
13
+
14
+ require 'elasticsearch/model'
15
+
16
+ # Documents are stored as JSON objects in Riak but have rich
17
+ # semantics, including validations and associations.
18
+ class Article < Couchbase::Model
19
+ attribute :title
20
+ attribute :published_at
21
+
22
+ # view :all, :limit => 10, :descending => true
23
+ # TODO: Implement view a la
24
+ # bucket.save_design_doc <<-JSON
25
+ # {
26
+ # "_id": "_design/article",
27
+ # "language": "javascript",
28
+ # "views": {
29
+ # "all": {
30
+ # "map": "function(doc, meta) { emit(doc.id, doc.title); }"
31
+ # }
32
+ # }
33
+ # }
34
+ # JSON
35
+
36
+ end
37
+
38
+ # Extend the model with Elasticsearch support
39
+ #
40
+ Article.__send__ :extend, Elasticsearch::Model::Client::ClassMethods
41
+ Article.__send__ :extend, Elasticsearch::Model::Searching::ClassMethods
42
+ Article.__send__ :extend, Elasticsearch::Model::Naming::ClassMethods
43
+
44
+ # Create documents in Riak
45
+ #
46
+ Article.create id: '1', title: 'Foo' rescue nil
47
+ Article.create id: '2', title: 'Bar' rescue nil
48
+ Article.create id: '3', title: 'Foo Foo' rescue nil
49
+
50
+ # Index data into Elasticsearch
51
+ #
52
+ client = Elasticsearch::Client.new log:true
53
+
54
+ client.indices.delete index: 'articles' rescue nil
55
+ client.bulk index: 'articles',
56
+ type: 'article',
57
+ body: Article.find(['1', '2', '3']).map { |a|
58
+ { index: { _id: a.id, data: a.attributes } }
59
+ },
60
+ refresh: true
61
+
62
+ response = Article.search 'foo', index: 'articles', type: 'article';
63
+
64
+ Pry.start(binding, prompt: lambda { |obj, nest_level, _| '> ' },
65
+ input: StringIO.new('response.records.to_a'),
66
+ quiet: true)
@@ -0,0 +1,71 @@
1
+ # DataMapper and Elasticsearch
2
+ # ============================
3
+ #
4
+ # https://github.com/datamapper/dm-core
5
+ # https://github.com/datamapper/dm-active_model
6
+
7
+
8
+ $LOAD_PATH.unshift File.expand_path('../../lib', __FILE__)
9
+
10
+ require 'pry'
11
+ Pry.config.history.file = File.expand_path('../../tmp/elasticsearch_development.pry', __FILE__)
12
+
13
+ require 'logger'
14
+ require 'ansi/core'
15
+
16
+ require 'data_mapper'
17
+ require 'dm-active_model'
18
+
19
+ require 'active_support/all'
20
+
21
+ require 'elasticsearch/model'
22
+
23
+ DataMapper::Logger.new(STDOUT, :debug)
24
+ DataMapper.setup(:default, 'sqlite::memory:')
25
+
26
+ class Article
27
+ include DataMapper::Resource
28
+
29
+ property :id, Serial
30
+ property :title, String
31
+ property :published_at, DateTime
32
+ end
33
+
34
+ DataMapper.auto_migrate!
35
+ DataMapper.finalize
36
+
37
+ Article.create title: 'Foo'
38
+ Article.create title: 'Bar'
39
+ Article.create title: 'Foo Foo'
40
+
41
+ # Extend the model with Elasticsearch support
42
+ #
43
+ Article.__send__ :include, Elasticsearch::Model
44
+
45
+ # The DataMapper adapter
46
+ #
47
+ module DataMapperAdapter
48
+
49
+ # Implement the interface for fetching records
50
+ #
51
+ module Records
52
+ def records
53
+ klass.all(id: @ids)
54
+ end
55
+
56
+ # ...
57
+ end
58
+ end
59
+
60
+ # Register the adapter
61
+ #
62
+ Elasticsearch::Model::Adapter.register(
63
+ DataMapperAdapter,
64
+ lambda { |klass| defined?(::DataMapper::Resource) and klass.ancestors.include?(::DataMapper::Resource) }
65
+ )
66
+
67
+ response = Article.search 'foo';
68
+
69
+ Pry.start(binding, prompt: lambda { |obj, nest_level, _| '> ' },
70
+ input: StringIO.new('response.records.to_a'),
71
+ quiet: true)