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.
- data/.gitignore +3 -0
- data/LICENSE.txt +1 -1
- data/README.md +669 -8
- data/Rakefile +52 -0
- data/elasticsearch-model.gemspec +48 -17
- data/examples/activerecord_article.rb +77 -0
- data/examples/activerecord_associations.rb +153 -0
- data/examples/couchbase_article.rb +66 -0
- data/examples/datamapper_article.rb +71 -0
- data/examples/mongoid_article.rb +68 -0
- data/examples/ohm_article.rb +70 -0
- data/examples/riak_article.rb +52 -0
- data/gemfiles/3.gemfile +11 -0
- data/gemfiles/4.gemfile +11 -0
- data/lib/elasticsearch/model.rb +151 -1
- data/lib/elasticsearch/model/adapter.rb +145 -0
- data/lib/elasticsearch/model/adapters/active_record.rb +97 -0
- data/lib/elasticsearch/model/adapters/default.rb +44 -0
- data/lib/elasticsearch/model/adapters/mongoid.rb +90 -0
- data/lib/elasticsearch/model/callbacks.rb +35 -0
- data/lib/elasticsearch/model/client.rb +61 -0
- data/lib/elasticsearch/model/importing.rb +94 -0
- data/lib/elasticsearch/model/indexing.rb +332 -0
- data/lib/elasticsearch/model/naming.rb +101 -0
- data/lib/elasticsearch/model/proxy.rb +127 -0
- data/lib/elasticsearch/model/response.rb +70 -0
- data/lib/elasticsearch/model/response/base.rb +44 -0
- data/lib/elasticsearch/model/response/pagination.rb +96 -0
- data/lib/elasticsearch/model/response/records.rb +71 -0
- data/lib/elasticsearch/model/response/result.rb +50 -0
- data/lib/elasticsearch/model/response/results.rb +32 -0
- data/lib/elasticsearch/model/searching.rb +107 -0
- data/lib/elasticsearch/model/serializing.rb +35 -0
- data/lib/elasticsearch/model/support/forwardable.rb +44 -0
- data/lib/elasticsearch/model/version.rb +1 -1
- data/test/integration/active_record_associations_parent_child.rb +138 -0
- data/test/integration/active_record_associations_test.rb +306 -0
- data/test/integration/active_record_basic_test.rb +139 -0
- data/test/integration/active_record_import_test.rb +74 -0
- data/test/integration/active_record_namespaced_model_test.rb +49 -0
- data/test/integration/active_record_pagination_test.rb +109 -0
- data/test/integration/mongoid_basic_test.rb +178 -0
- data/test/test_helper.rb +57 -0
- data/test/unit/adapter_active_record_test.rb +93 -0
- data/test/unit/adapter_default_test.rb +31 -0
- data/test/unit/adapter_mongoid_test.rb +87 -0
- data/test/unit/adapter_test.rb +69 -0
- data/test/unit/callbacks_test.rb +30 -0
- data/test/unit/client_test.rb +27 -0
- data/test/unit/importing_test.rb +97 -0
- data/test/unit/indexing_test.rb +364 -0
- data/test/unit/module_test.rb +46 -0
- data/test/unit/naming_test.rb +76 -0
- data/test/unit/proxy_test.rb +88 -0
- data/test/unit/response_base_test.rb +40 -0
- data/test/unit/response_pagination_test.rb +159 -0
- data/test/unit/response_records_test.rb +87 -0
- data/test/unit/response_result_test.rb +52 -0
- data/test/unit/response_results_test.rb +31 -0
- data/test/unit/response_test.rb +57 -0
- data/test/unit/searching_search_request_test.rb +73 -0
- data/test/unit/searching_test.rb +39 -0
- data/test/unit/serializing_test.rb +17 -0
- metadata +418 -11
@@ -0,0 +1,97 @@
|
|
1
|
+
module Elasticsearch
|
2
|
+
module Model
|
3
|
+
module Adapter
|
4
|
+
|
5
|
+
# An adapter for ActiveRecord-based models
|
6
|
+
#
|
7
|
+
module ActiveRecord
|
8
|
+
|
9
|
+
Adapter.register self,
|
10
|
+
lambda { |klass| !!defined?(::ActiveRecord::Base) && klass.ancestors.include?(::ActiveRecord::Base) }
|
11
|
+
|
12
|
+
module Records
|
13
|
+
# Returns an `ActiveRecord::Relation` instance
|
14
|
+
#
|
15
|
+
def records
|
16
|
+
sql_records = klass.where(id: ids)
|
17
|
+
|
18
|
+
# Re-order records based on the order from Elasticsearch hits
|
19
|
+
# by redefining `to_a`, unless the user has called `order()`
|
20
|
+
#
|
21
|
+
sql_records.instance_exec(response.response['hits']['hits']) do |hits|
|
22
|
+
define_singleton_method :to_a do
|
23
|
+
if defined?(::ActiveRecord) && ::ActiveRecord::VERSION::MAJOR >= 4
|
24
|
+
self.load
|
25
|
+
else
|
26
|
+
self.__send__(:exec_queries)
|
27
|
+
end
|
28
|
+
@records.sort_by { |record| hits.index { |hit| hit['_id'].to_s == record.id.to_s } }
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
sql_records
|
33
|
+
end
|
34
|
+
|
35
|
+
# Prevent clash with `ActiveSupport::Dependencies::Loadable`
|
36
|
+
#
|
37
|
+
def load
|
38
|
+
records.load
|
39
|
+
end
|
40
|
+
|
41
|
+
# Intercept call to the `order` method, so we can ignore the order from Elasticsearch
|
42
|
+
#
|
43
|
+
def order(*args)
|
44
|
+
sql_records = records.__send__ :order, *args
|
45
|
+
|
46
|
+
# Redefine the `to_a` method to the original one
|
47
|
+
#
|
48
|
+
sql_records.instance_exec do
|
49
|
+
define_singleton_method(:to_a) do
|
50
|
+
if defined?(::ActiveRecord) && ::ActiveRecord::VERSION::MAJOR >= 4
|
51
|
+
self.load
|
52
|
+
else
|
53
|
+
self.__send__(:exec_queries)
|
54
|
+
end
|
55
|
+
@records
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
sql_records
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
module Callbacks
|
64
|
+
|
65
|
+
# Handle index updates (creating, updating or deleting documents)
|
66
|
+
# when the model changes, by hooking into the lifecycle
|
67
|
+
#
|
68
|
+
# @see http://guides.rubyonrails.org/active_record_callbacks.html
|
69
|
+
#
|
70
|
+
def self.included(base)
|
71
|
+
base.class_eval do
|
72
|
+
after_commit lambda { __elasticsearch__.index_document }, on: :create
|
73
|
+
after_commit lambda { __elasticsearch__.update_document }, on: :update
|
74
|
+
after_commit lambda { __elasticsearch__.delete_document }, on: :destroy
|
75
|
+
end
|
76
|
+
end
|
77
|
+
end
|
78
|
+
|
79
|
+
module Importing
|
80
|
+
|
81
|
+
# Fetch batches of records from the database
|
82
|
+
#
|
83
|
+
# @see http://api.rubyonrails.org/classes/ActiveRecord/Batches.html ActiveRecord::Batches.find_in_batches
|
84
|
+
#
|
85
|
+
def __find_in_batches(options={}, &block)
|
86
|
+
find_in_batches(options) do |batch|
|
87
|
+
batch_for_bulk = batch.map { |a| { index: { _id: a.id, data: a.__elasticsearch__.as_indexed_json } } }
|
88
|
+
yield batch_for_bulk
|
89
|
+
end
|
90
|
+
end
|
91
|
+
end
|
92
|
+
|
93
|
+
end
|
94
|
+
|
95
|
+
end
|
96
|
+
end
|
97
|
+
end
|
@@ -0,0 +1,44 @@
|
|
1
|
+
module Elasticsearch
|
2
|
+
module Model
|
3
|
+
module Adapter
|
4
|
+
|
5
|
+
# The default adapter for models which haven't one registered
|
6
|
+
#
|
7
|
+
module Default
|
8
|
+
|
9
|
+
# Module for implementing methods and logic related to fetching records from the database
|
10
|
+
#
|
11
|
+
module Records
|
12
|
+
|
13
|
+
# Return the collection of records fetched from the database
|
14
|
+
#
|
15
|
+
# By default uses `MyModel#find[1, 2, 3]`
|
16
|
+
#
|
17
|
+
def records
|
18
|
+
klass.find(@ids)
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
# Module for implementing methods and logic related to hooking into model lifecycle
|
23
|
+
# (e.g. to perform automatic index updates)
|
24
|
+
#
|
25
|
+
# @see http://api.rubyonrails.org/classes/ActiveModel/Callbacks.html
|
26
|
+
module Callbacks
|
27
|
+
# noop
|
28
|
+
end
|
29
|
+
|
30
|
+
# Module for efficiently fetching records from the database to import them into the index
|
31
|
+
#
|
32
|
+
module Importing
|
33
|
+
|
34
|
+
# @abstract Implement this method in your adapter
|
35
|
+
#
|
36
|
+
def __find_in_batches(options={}, &block)
|
37
|
+
raise NotImplemented, "Method not implemented for default adapter"
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
@@ -0,0 +1,90 @@
|
|
1
|
+
module Elasticsearch
|
2
|
+
module Model
|
3
|
+
module Adapter
|
4
|
+
|
5
|
+
# An adapter for Mongoid-based models
|
6
|
+
#
|
7
|
+
# @see http://mongoid.org
|
8
|
+
#
|
9
|
+
module Mongoid
|
10
|
+
|
11
|
+
Adapter.register self,
|
12
|
+
lambda { |klass| !!defined?(::Mongoid::Document) && klass.ancestors.include?(::Mongoid::Document) }
|
13
|
+
|
14
|
+
module Records
|
15
|
+
|
16
|
+
# Return a `Mongoid::Criteria` instance
|
17
|
+
#
|
18
|
+
def records
|
19
|
+
criteria = klass.where(:id.in => ids)
|
20
|
+
|
21
|
+
criteria.instance_exec(response.response['hits']['hits']) do |hits|
|
22
|
+
define_singleton_method :to_a do
|
23
|
+
self.entries.sort_by { |e| hits.index { |hit| hit['_id'].to_s == e.id.to_s } }
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
criteria
|
28
|
+
end
|
29
|
+
|
30
|
+
# Intercept call to sorting methods, so we can ignore the order from Elasticsearch
|
31
|
+
#
|
32
|
+
%w| asc desc order_by |.each do |name|
|
33
|
+
define_method name do |*args|
|
34
|
+
criteria = records.__send__ name, *args
|
35
|
+
criteria.instance_exec do
|
36
|
+
define_singleton_method(:to_a) { self.entries }
|
37
|
+
end
|
38
|
+
|
39
|
+
criteria
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
module Callbacks
|
45
|
+
|
46
|
+
# Handle index updates (creating, updating or deleting documents)
|
47
|
+
# when the model changes, by hooking into the lifecycle
|
48
|
+
#
|
49
|
+
# @see http://mongoid.org/en/mongoid/docs/callbacks.html
|
50
|
+
#
|
51
|
+
def self.included(base)
|
52
|
+
base.after_create { |document| document.__elasticsearch__.index_document }
|
53
|
+
base.after_update { |document| document.__elasticsearch__.update_document }
|
54
|
+
base.after_destroy { |document| document.__elasticsearch__.delete_document }
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
module Importing
|
59
|
+
|
60
|
+
# Fetch batches of records from the database
|
61
|
+
#
|
62
|
+
# @see https://github.com/mongoid/mongoid/issues/1334
|
63
|
+
# @see https://github.com/karmi/retire/pull/724
|
64
|
+
#
|
65
|
+
def __find_in_batches(options={}, &block)
|
66
|
+
options[:batch_size] ||= 1_000
|
67
|
+
items = []
|
68
|
+
|
69
|
+
all.each do |item|
|
70
|
+
items << item
|
71
|
+
|
72
|
+
if items.length % options[:batch_size] == 0
|
73
|
+
batch_for_bulk = items.map { |a| { index: { _id: a.id.to_s, data: a.as_indexed_json } } }
|
74
|
+
yield batch_for_bulk
|
75
|
+
items = []
|
76
|
+
end
|
77
|
+
end
|
78
|
+
|
79
|
+
unless items.empty?
|
80
|
+
batch_for_bulk = items.map { |a| { index: { _id: a.id.to_s, data: a.as_indexed_json } } }
|
81
|
+
yield batch_for_bulk
|
82
|
+
end
|
83
|
+
end
|
84
|
+
end
|
85
|
+
|
86
|
+
end
|
87
|
+
|
88
|
+
end
|
89
|
+
end
|
90
|
+
end
|
@@ -0,0 +1,35 @@
|
|
1
|
+
module Elasticsearch
|
2
|
+
module Model
|
3
|
+
|
4
|
+
# Allows to automatically update index based on model changes,
|
5
|
+
# by hooking into the model lifecycle.
|
6
|
+
#
|
7
|
+
# @note A blocking HTTP request is done during the update process.
|
8
|
+
# If you need a more performant/resilient way of updating the index,
|
9
|
+
# consider adapting the callbacks behaviour, and use a background
|
10
|
+
# processing solution such as [Sidekiq](http://sidekiq.org)
|
11
|
+
# or [Resque](https://github.com/resque/resque).
|
12
|
+
#
|
13
|
+
module Callbacks
|
14
|
+
|
15
|
+
# When included in a model, automatically injects the callback subscribers (`after_save`, etc)
|
16
|
+
#
|
17
|
+
# @example Automatically update Elasticsearch index when the model changes
|
18
|
+
#
|
19
|
+
# class Article
|
20
|
+
# include Elasticsearch::Model
|
21
|
+
# include Elasticsearch::Model::Callbacks
|
22
|
+
# end
|
23
|
+
#
|
24
|
+
# Article.first.update_attribute :title, 'Updated'
|
25
|
+
# # SQL (0.3ms) UPDATE "articles" SET "title" = ...
|
26
|
+
# # 2013-11-20 15:08:52 +0100: POST http://localhost:9200/articles/article/1/_update ...
|
27
|
+
#
|
28
|
+
def self.included(base)
|
29
|
+
adapter = Adapter.from_class(base)
|
30
|
+
base.__send__ :include, adapter.callbacks_mixin
|
31
|
+
end
|
32
|
+
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
@@ -0,0 +1,61 @@
|
|
1
|
+
module Elasticsearch
|
2
|
+
module Model
|
3
|
+
|
4
|
+
# Contains an `Elasticsearch::Client` instance
|
5
|
+
#
|
6
|
+
module Client
|
7
|
+
|
8
|
+
module ClassMethods
|
9
|
+
|
10
|
+
# Get the client for a specific model class
|
11
|
+
#
|
12
|
+
# @example Get the client for `Article` and perform API request
|
13
|
+
#
|
14
|
+
# Article.client.cluster.health
|
15
|
+
# # => { "cluster_name" => "elasticsearch" ... }
|
16
|
+
#
|
17
|
+
def client client=nil
|
18
|
+
@client ||= Elasticsearch::Model.client
|
19
|
+
end
|
20
|
+
|
21
|
+
# Set the client for a specific model class
|
22
|
+
#
|
23
|
+
# @example Configure the client for the `Article` model
|
24
|
+
#
|
25
|
+
# Article.client = Elasticsearch::Client.new host: 'http://api.server:8080'
|
26
|
+
# Article.search ...
|
27
|
+
#
|
28
|
+
def client=(client)
|
29
|
+
@client = client
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
module InstanceMethods
|
34
|
+
|
35
|
+
# Get or set the client for a specific model instance
|
36
|
+
#
|
37
|
+
# @example Get the client for a specific record and perform API request
|
38
|
+
#
|
39
|
+
# @article = Article.first
|
40
|
+
# @article.client.info
|
41
|
+
# # => { "name" => "Node-1", ... }
|
42
|
+
#
|
43
|
+
def client
|
44
|
+
@client ||= self.class.client
|
45
|
+
end
|
46
|
+
|
47
|
+
# Set the client for a specific model instance
|
48
|
+
#
|
49
|
+
# @example Set the client for a specific record
|
50
|
+
#
|
51
|
+
# @article = Article.first
|
52
|
+
# @article.client = Elasticsearch::Client.new host: 'http://api.server:8080'
|
53
|
+
#
|
54
|
+
def client=(client)
|
55
|
+
@client = client
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
end
|
60
|
+
end
|
61
|
+
end
|
@@ -0,0 +1,94 @@
|
|
1
|
+
module Elasticsearch
|
2
|
+
module Model
|
3
|
+
|
4
|
+
# Provides support for easily and efficiently importing large amounts of
|
5
|
+
# records from the including class into the index.
|
6
|
+
#
|
7
|
+
# @see ClassMethods#import
|
8
|
+
#
|
9
|
+
module Importing
|
10
|
+
|
11
|
+
# When included in a model, adds the importing methods.
|
12
|
+
#
|
13
|
+
# @example Import all records from the `Article` model
|
14
|
+
#
|
15
|
+
# Article.import
|
16
|
+
#
|
17
|
+
# @see #import
|
18
|
+
#
|
19
|
+
def self.included(base)
|
20
|
+
base.__send__ :extend, ClassMethods
|
21
|
+
|
22
|
+
adapter = Adapter.from_class(base)
|
23
|
+
base.__send__ :include, adapter.importing_mixin
|
24
|
+
base.__send__ :extend, adapter.importing_mixin
|
25
|
+
end
|
26
|
+
|
27
|
+
module ClassMethods
|
28
|
+
|
29
|
+
# Import all model records into the index
|
30
|
+
#
|
31
|
+
# The method will pick up correct strategy based on the `Importing` module
|
32
|
+
# defined in the corresponding adapter.
|
33
|
+
#
|
34
|
+
# @param options [Hash] Options passed to the underlying `__find_in_batches`method
|
35
|
+
# @param block [Proc] Optional block to evaluate for each batch
|
36
|
+
#
|
37
|
+
# @yield [Hash] Gives the Hash with the Elasticsearch response to the block
|
38
|
+
#
|
39
|
+
# @return [Fixnum] Number of errors encountered during importing
|
40
|
+
#
|
41
|
+
# @example Import all records into the index
|
42
|
+
#
|
43
|
+
# Article.import
|
44
|
+
#
|
45
|
+
# @example Set the batch size to 100
|
46
|
+
#
|
47
|
+
# Article.import batch_size: 100
|
48
|
+
#
|
49
|
+
# @example Process the response from Elasticsearch
|
50
|
+
#
|
51
|
+
# Article.import do |response|
|
52
|
+
# puts "Got " + response['items'].select { |i| i['index']['error'] }.size.to_s + " errors"
|
53
|
+
# end
|
54
|
+
#
|
55
|
+
# @example Delete and create the index with appropriate settings and mappings
|
56
|
+
#
|
57
|
+
# Article.import force: true
|
58
|
+
#
|
59
|
+
# @example Refresh the index after importing all batches
|
60
|
+
#
|
61
|
+
# Article.import refresh: true
|
62
|
+
#
|
63
|
+
#
|
64
|
+
def import(options={}, &block)
|
65
|
+
errors = 0
|
66
|
+
|
67
|
+
if options.delete(:force)
|
68
|
+
self.create_index! force: true
|
69
|
+
end
|
70
|
+
|
71
|
+
refresh = options.delete(:refresh) || false
|
72
|
+
|
73
|
+
__find_in_batches(options) do |batch|
|
74
|
+
response = client.bulk \
|
75
|
+
index: index_name,
|
76
|
+
type: document_type,
|
77
|
+
body: batch
|
78
|
+
|
79
|
+
yield response if block_given?
|
80
|
+
|
81
|
+
errors += response['items'].map { |k, v| k.values.first['error'] }.compact.length
|
82
|
+
end
|
83
|
+
|
84
|
+
self.refresh_index! if refresh
|
85
|
+
|
86
|
+
return errors
|
87
|
+
end
|
88
|
+
|
89
|
+
end
|
90
|
+
|
91
|
+
end
|
92
|
+
|
93
|
+
end
|
94
|
+
end
|
@@ -0,0 +1,332 @@
|
|
1
|
+
module Elasticsearch
|
2
|
+
module Model
|
3
|
+
|
4
|
+
# Provides the necessary support to set up index options (mappings, settings)
|
5
|
+
# as well as instance methods to create, update or delete documents in the index.
|
6
|
+
#
|
7
|
+
# @see ClassMethods#settings
|
8
|
+
# @see ClassMethods#mapping
|
9
|
+
#
|
10
|
+
# @see InstanceMethods#index_document
|
11
|
+
# @see InstanceMethods#update_document
|
12
|
+
# @see InstanceMethods#delete_document
|
13
|
+
#
|
14
|
+
module Indexing
|
15
|
+
|
16
|
+
# Wraps the [index settings](http://www.elasticsearch.org/guide/en/elasticsearch/reference/current/setup-configuration.html#configuration-index-settings)
|
17
|
+
#
|
18
|
+
class Settings
|
19
|
+
attr_accessor :settings
|
20
|
+
|
21
|
+
def initialize(settings={})
|
22
|
+
@settings = settings
|
23
|
+
end
|
24
|
+
|
25
|
+
def to_hash
|
26
|
+
@settings
|
27
|
+
end
|
28
|
+
|
29
|
+
def as_json(options={})
|
30
|
+
to_hash
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
# Wraps the [index mappings](http://www.elasticsearch.org/guide/en/elasticsearch/reference/current/mapping.html)
|
35
|
+
#
|
36
|
+
class Mappings
|
37
|
+
attr_accessor :options
|
38
|
+
|
39
|
+
def initialize(type, options={})
|
40
|
+
@type = type
|
41
|
+
@options = options
|
42
|
+
@mapping = {}
|
43
|
+
end
|
44
|
+
|
45
|
+
def indexes(name, options = {}, &block)
|
46
|
+
@mapping[name] = options
|
47
|
+
|
48
|
+
if block_given?
|
49
|
+
@mapping[name][:type] ||= 'object'
|
50
|
+
properties = @mapping[name][:type] == 'multi_field' ? :fields : :properties
|
51
|
+
|
52
|
+
@mapping[name][properties] ||= {}
|
53
|
+
|
54
|
+
previous = @mapping
|
55
|
+
begin
|
56
|
+
@mapping = @mapping[name][properties]
|
57
|
+
self.instance_eval(&block)
|
58
|
+
ensure
|
59
|
+
@mapping = previous
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
# Set the type to `string` by default
|
64
|
+
#
|
65
|
+
@mapping[name][:type] ||= 'string'
|
66
|
+
|
67
|
+
self
|
68
|
+
end
|
69
|
+
|
70
|
+
def to_hash
|
71
|
+
{ @type.to_sym => @options.merge( properties: @mapping ) }
|
72
|
+
end
|
73
|
+
|
74
|
+
def as_json(options={})
|
75
|
+
to_hash
|
76
|
+
end
|
77
|
+
end
|
78
|
+
|
79
|
+
module ClassMethods
|
80
|
+
|
81
|
+
# Defines mappings for the index
|
82
|
+
#
|
83
|
+
# @example Define mapping for model
|
84
|
+
#
|
85
|
+
# class Article
|
86
|
+
# mapping dynamic: 'strict' do
|
87
|
+
# indexes :foo do
|
88
|
+
# indexes :bar
|
89
|
+
# end
|
90
|
+
# indexes :baz
|
91
|
+
# end
|
92
|
+
# end
|
93
|
+
#
|
94
|
+
# Article.mapping.to_hash
|
95
|
+
#
|
96
|
+
# # => { :article =>
|
97
|
+
# # { :dynamic => "strict",
|
98
|
+
# # :properties=>
|
99
|
+
# # { :foo => {
|
100
|
+
# # :type=>"object",
|
101
|
+
# # :properties => {
|
102
|
+
# # :bar => { :type => "string" }
|
103
|
+
# # }
|
104
|
+
# # }
|
105
|
+
# # },
|
106
|
+
# # :baz => { :type=> "string" }
|
107
|
+
# # }
|
108
|
+
# # }
|
109
|
+
#
|
110
|
+
# @example Define index settings and mappings
|
111
|
+
#
|
112
|
+
# class Article
|
113
|
+
# settings number_of_shards: 1 do
|
114
|
+
# mappings do
|
115
|
+
# indexes :foo
|
116
|
+
# end
|
117
|
+
# end
|
118
|
+
# end
|
119
|
+
#
|
120
|
+
# @example Call the mapping method directly
|
121
|
+
#
|
122
|
+
# Article.mapping(dynamic: 'strict') { indexes :foo, type: 'long' }
|
123
|
+
#
|
124
|
+
# Article.mapping.to_hash
|
125
|
+
#
|
126
|
+
# # => {:article=>{:dynamic=>"strict", :properties=>{:foo=>{:type=>"long"}}}}
|
127
|
+
#
|
128
|
+
# The `mappings` and `settings` methods are accessible directly on the model class,
|
129
|
+
# when it doesn't already defines them. Use the `__elasticsearch__` proxy otherwise.
|
130
|
+
#
|
131
|
+
def mapping(options={}, &block)
|
132
|
+
@mapping ||= Mappings.new(document_type, options)
|
133
|
+
|
134
|
+
if block_given?
|
135
|
+
@mapping.options.update(options)
|
136
|
+
|
137
|
+
@mapping.instance_eval(&block)
|
138
|
+
return self
|
139
|
+
else
|
140
|
+
@mapping
|
141
|
+
end
|
142
|
+
end; alias_method :mappings, :mapping
|
143
|
+
|
144
|
+
# Define settings for the index
|
145
|
+
#
|
146
|
+
# @example Define index settings
|
147
|
+
#
|
148
|
+
# Article.settings(index: { number_of_shards: 1 })
|
149
|
+
#
|
150
|
+
# Article.settings.to_hash
|
151
|
+
#
|
152
|
+
# # => {:index=>{:number_of_shards=>1}}
|
153
|
+
#
|
154
|
+
def settings(settings={}, &block)
|
155
|
+
@settings ||= Settings.new(settings)
|
156
|
+
|
157
|
+
@settings.settings.update(settings) unless settings.empty?
|
158
|
+
|
159
|
+
if block_given?
|
160
|
+
self.instance_eval(&block)
|
161
|
+
return self
|
162
|
+
else
|
163
|
+
@settings
|
164
|
+
end
|
165
|
+
end
|
166
|
+
|
167
|
+
# Creates an index with correct name, automatically passing
|
168
|
+
# `settings` and `mappings` defined in the model
|
169
|
+
#
|
170
|
+
# @example Create an index for the `Article` model
|
171
|
+
#
|
172
|
+
# Article.__elasticsearch__.create_index!
|
173
|
+
#
|
174
|
+
# @example Forcefully create (delete first) an index for the `Article` model
|
175
|
+
#
|
176
|
+
# Article.__elasticsearch__.create_index! force: true
|
177
|
+
#
|
178
|
+
def create_index!(options={})
|
179
|
+
delete_index!(options) if options[:force]
|
180
|
+
|
181
|
+
unless ( self.client.indices.exists(index: self.index_name) rescue false )
|
182
|
+
begin
|
183
|
+
self.client.indices.create index: self.index_name,
|
184
|
+
body: {
|
185
|
+
settings: self.settings.to_hash,
|
186
|
+
mappings: self.mappings.to_hash }
|
187
|
+
rescue Exception => e
|
188
|
+
unless e.class.to_s =~ /NotFound/ && options[:force]
|
189
|
+
STDERR.puts "[!!!] Error when creating the index: #{e.class}", "#{e.message}"
|
190
|
+
end
|
191
|
+
end
|
192
|
+
else
|
193
|
+
end
|
194
|
+
end
|
195
|
+
|
196
|
+
# Deletes the index with corresponding name
|
197
|
+
#
|
198
|
+
# @example Delete the index for the `Article` model
|
199
|
+
#
|
200
|
+
# Article.__elasticsearch__.delete_index!
|
201
|
+
#
|
202
|
+
def delete_index!(options={})
|
203
|
+
begin
|
204
|
+
self.client.indices.delete index: self.index_name
|
205
|
+
rescue Exception => e
|
206
|
+
unless e.class.to_s =~ /NotFound/ && options[:force]
|
207
|
+
STDERR.puts "[!!!] Error when deleting the index: #{e.class}", "#{e.message}"
|
208
|
+
end
|
209
|
+
end
|
210
|
+
end
|
211
|
+
|
212
|
+
# Performs the "refresh" operation for the index (useful e.g. in tests)
|
213
|
+
#
|
214
|
+
# @example Refresh the index for the `Article` model
|
215
|
+
#
|
216
|
+
# Article.__elasticsearch__.refresh_index!
|
217
|
+
#
|
218
|
+
# @see http://www.elasticsearch.org/guide/en/elasticsearch/reference/current/indices-refresh.html
|
219
|
+
#
|
220
|
+
def refresh_index!(options={})
|
221
|
+
begin
|
222
|
+
self.client.indices.refresh index: self.index_name
|
223
|
+
rescue Exception => e
|
224
|
+
unless e.class.to_s =~ /NotFound/ && options[:force]
|
225
|
+
STDERR.puts "[!!!] Error when refreshing the index: #{e.class}", "#{e.message}"
|
226
|
+
end
|
227
|
+
end
|
228
|
+
end
|
229
|
+
end
|
230
|
+
|
231
|
+
module InstanceMethods
|
232
|
+
|
233
|
+
def self.included(base)
|
234
|
+
# Register callback for storing changed attributes for models
|
235
|
+
# which implement `before_save` and `changed_attributes` methods
|
236
|
+
#
|
237
|
+
# @note This is typically triggered only when the module would be
|
238
|
+
# included in the model directly, not within the proxy.
|
239
|
+
#
|
240
|
+
# @see #update_document
|
241
|
+
#
|
242
|
+
base.before_save do |instance|
|
243
|
+
instance.instance_variable_set(:@__changed_attributes,
|
244
|
+
Hash[ instance.changes.map { |key, value| [key, value.last] } ])
|
245
|
+
end if base.respond_to?(:before_save) && base.instance_methods.include?(:changed_attributes)
|
246
|
+
end
|
247
|
+
|
248
|
+
# Serializes the model instance into JSON (by calling `as_indexed_json`),
|
249
|
+
# and saves the document into the Elasticsearch index.
|
250
|
+
#
|
251
|
+
# @param options [Hash] Optional arguments for passing to the client
|
252
|
+
#
|
253
|
+
# @example Index a record
|
254
|
+
#
|
255
|
+
# @article.__elasticsearch__.index_document
|
256
|
+
# 2013-11-20 16:25:57 +0100: PUT http://localhost:9200/articles/article/1 ...
|
257
|
+
#
|
258
|
+
# @return [Hash] The response from Elasticsearch
|
259
|
+
#
|
260
|
+
# @see http://rubydoc.info/gems/elasticsearch-api/Elasticsearch/API/Actions:index
|
261
|
+
#
|
262
|
+
def index_document(options={})
|
263
|
+
document = self.as_indexed_json
|
264
|
+
|
265
|
+
client.index(
|
266
|
+
{ index: index_name,
|
267
|
+
type: document_type,
|
268
|
+
id: self.id,
|
269
|
+
body: document }.merge(options)
|
270
|
+
)
|
271
|
+
end
|
272
|
+
|
273
|
+
# Deletes the model instance from the index
|
274
|
+
#
|
275
|
+
# @param options [Hash] Optional arguments for passing to the client
|
276
|
+
#
|
277
|
+
# @example Delete a record
|
278
|
+
#
|
279
|
+
# @article.__elasticsearch__.delete_document
|
280
|
+
# 2013-11-20 16:27:00 +0100: DELETE http://localhost:9200/articles/article/1
|
281
|
+
#
|
282
|
+
# @return [Hash] The response from Elasticsearch
|
283
|
+
#
|
284
|
+
# @see http://rubydoc.info/gems/elasticsearch-api/Elasticsearch/API/Actions:delete
|
285
|
+
#
|
286
|
+
def delete_document(options={})
|
287
|
+
client.delete(
|
288
|
+
{ index: index_name,
|
289
|
+
type: document_type,
|
290
|
+
id: self.id }.merge(options)
|
291
|
+
)
|
292
|
+
end
|
293
|
+
|
294
|
+
# Tries to gather the changed attributes of a model instance
|
295
|
+
# (via [ActiveModel::Dirty](http://api.rubyonrails.org/classes/ActiveModel/Dirty.html)),
|
296
|
+
# performing a _partial_ update of the document.
|
297
|
+
#
|
298
|
+
# When the changed attributes are not available, performs full re-index of the record.
|
299
|
+
#
|
300
|
+
# @param options [Hash] Optional arguments for passing to the client
|
301
|
+
#
|
302
|
+
# @example Update a document corresponding to the record
|
303
|
+
#
|
304
|
+
# @article = Article.first
|
305
|
+
# @article.update_attribute :title, 'Updated'
|
306
|
+
# # SQL (0.3ms) UPDATE "articles" SET "title" = ?...
|
307
|
+
#
|
308
|
+
# @article.__elasticsearch__.update_document
|
309
|
+
# # 2013-11-20 17:00:05 +0100: POST http://localhost:9200/articles/article/1/_update ...
|
310
|
+
# # 2013-11-20 17:00:05 +0100: > {"doc":{"title":"Updated"}}
|
311
|
+
#
|
312
|
+
# @return [Hash] The response from Elasticsearch
|
313
|
+
#
|
314
|
+
# @see http://rubydoc.info/gems/elasticsearch-api/Elasticsearch/API/Actions:update
|
315
|
+
#
|
316
|
+
def update_document(options={})
|
317
|
+
if changed_attributes = self.instance_variable_get(:@__changed_attributes)
|
318
|
+
client.update(
|
319
|
+
{ index: index_name,
|
320
|
+
type: document_type,
|
321
|
+
id: self.id,
|
322
|
+
body: { doc: changed_attributes } }.merge(options)
|
323
|
+
)
|
324
|
+
else
|
325
|
+
index_document(options)
|
326
|
+
end
|
327
|
+
end
|
328
|
+
end
|
329
|
+
|
330
|
+
end
|
331
|
+
end
|
332
|
+
end
|