agnostic_backend 0.9.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (55) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +1 -0
  3. data/.rspec +1 -0
  4. data/.ruby-gemset +1 -0
  5. data/.ruby-version +1 -0
  6. data/.travis.yml +7 -0
  7. data/CODE_OF_CONDUCT.md +13 -0
  8. data/Gemfile +4 -0
  9. data/Gemfile.lock +52 -0
  10. data/LICENSE.txt +21 -0
  11. data/README.md +345 -0
  12. data/Rakefile +6 -0
  13. data/agnostic_backend.gemspec +34 -0
  14. data/bin/console +14 -0
  15. data/bin/setup +7 -0
  16. data/doc/indexable.md +292 -0
  17. data/doc/queryable.md +0 -0
  18. data/lib/agnostic_backend/cloudsearch/index.rb +99 -0
  19. data/lib/agnostic_backend/cloudsearch/index_field.rb +103 -0
  20. data/lib/agnostic_backend/cloudsearch/indexer.rb +84 -0
  21. data/lib/agnostic_backend/cloudsearch/remote_index_field.rb +32 -0
  22. data/lib/agnostic_backend/index.rb +24 -0
  23. data/lib/agnostic_backend/indexable/config.rb +24 -0
  24. data/lib/agnostic_backend/indexable/content_manager.rb +51 -0
  25. data/lib/agnostic_backend/indexable/field.rb +26 -0
  26. data/lib/agnostic_backend/indexable/field_type.rb +48 -0
  27. data/lib/agnostic_backend/indexable/indexable.rb +125 -0
  28. data/lib/agnostic_backend/indexer.rb +48 -0
  29. data/lib/agnostic_backend/queryable/attribute.rb +22 -0
  30. data/lib/agnostic_backend/queryable/cloudsearch/executor.rb +100 -0
  31. data/lib/agnostic_backend/queryable/cloudsearch/query.rb +29 -0
  32. data/lib/agnostic_backend/queryable/cloudsearch/query_builder.rb +13 -0
  33. data/lib/agnostic_backend/queryable/cloudsearch/result_set.rb +27 -0
  34. data/lib/agnostic_backend/queryable/cloudsearch/visitor.rb +127 -0
  35. data/lib/agnostic_backend/queryable/criteria/binary.rb +47 -0
  36. data/lib/agnostic_backend/queryable/criteria/criterion.rb +13 -0
  37. data/lib/agnostic_backend/queryable/criteria/ternary.rb +36 -0
  38. data/lib/agnostic_backend/queryable/criteria_builder.rb +83 -0
  39. data/lib/agnostic_backend/queryable/executor.rb +43 -0
  40. data/lib/agnostic_backend/queryable/expressions/expression.rb +65 -0
  41. data/lib/agnostic_backend/queryable/operations/n_ary.rb +18 -0
  42. data/lib/agnostic_backend/queryable/operations/operation.rb +13 -0
  43. data/lib/agnostic_backend/queryable/operations/unary.rb +35 -0
  44. data/lib/agnostic_backend/queryable/query.rb +27 -0
  45. data/lib/agnostic_backend/queryable/query_builder.rb +98 -0
  46. data/lib/agnostic_backend/queryable/result_set.rb +42 -0
  47. data/lib/agnostic_backend/queryable/tree_node.rb +48 -0
  48. data/lib/agnostic_backend/queryable/validator.rb +174 -0
  49. data/lib/agnostic_backend/queryable/value.rb +26 -0
  50. data/lib/agnostic_backend/queryable/visitor.rb +124 -0
  51. data/lib/agnostic_backend/rspec/matchers.rb +69 -0
  52. data/lib/agnostic_backend/utilities.rb +207 -0
  53. data/lib/agnostic_backend/version.rb +3 -0
  54. data/lib/agnostic_backend.rb +49 -0
  55. metadata +199 -0
data/doc/indexable.md ADDED
@@ -0,0 +1,292 @@
1
+ # Indexable module Guide
2
+
3
+ Broadly speaking, the `Indexable` module provides classes with
4
+ functionality related to the following:
5
+
6
+ - define what should be indexed (attributes names/values, nested
7
+ fields, field types etc.)
8
+ - define who should be notified when an object (or its owner) needs to
9
+ be indexed (when the object changes in some way)
10
+ - when should the above notifications occur
11
+
12
+ In our examples below, we are working with `ActiveRecord` models, but
13
+ `AgnosticBackend` can be used with any object. Also, whenever we
14
+ mention the word "document" below, we take this to be a Ruby Hash.
15
+
16
+ ## Document contents
17
+
18
+ Say we need to represent a workflow comprising a sequence of tasks
19
+ within a case, using a `Workflow` AR model and a `Task` AR model
20
+ connected by a one-to-many relationship, as follows:
21
+
22
+ ```ruby
23
+ class Task < ActiveRecord::Base
24
+ belongs_to :workflow, class_name: 'Workflow'
25
+ end
26
+
27
+ class Workflow < ActiveRecord::Base
28
+ has_many :tasks, class_name: 'Task'
29
+ end
30
+ ```
31
+
32
+ We can specify what to index in the `Task` model by including
33
+ `AgnosticBackend::Indexable` and using the `define_index_fields`
34
+ method as follows:
35
+
36
+ ```ruby
37
+ class Task < ActiveRecord::Base
38
+ include AgnosticBackend::Indexable
39
+ belongs_to :workflow, class_name: 'Workflow'
40
+
41
+ define_index_fields do
42
+ integer :id
43
+ date :last_assigned_at, value: :assigned_at
44
+ string :type, value: proc { task_category.name }
45
+ end
46
+ end
47
+ ```
48
+
49
+ In this case, we specify that the document to be generated when the
50
+ time comes (more about that below) will include 3 fields: `id`,
51
+ `last_assigned_at` and `type`. Let's look at each one of them in more
52
+ detail. The document will contain a field with the key `id` and the
53
+ value that will be generated when the object receives the message
54
+ `:id` at document generation time. This means that in the simplest
55
+ possible case, the field's key is a method to which we expect the
56
+ object to respond. The document will also contain the
57
+ `last_assigned_at` key whose value will be determined by sending the
58
+ message `assigned_at` to the object. Finally, the document will also
59
+ contain the field `type` which in this case is a computed value that
60
+ will be determined at runtime at executing the specified `proc` in the
61
+ context of `self` (i.e. in the context of the class's instance).
62
+
63
+ ## Nested documents
64
+
65
+ `Indexable` supports the specification of nested documents by using
66
+ the `struct` type as follows:
67
+
68
+ ```ruby
69
+ class Task < ActiveRecord::Base
70
+ include AgnosticBackend::Indexable
71
+ belongs_to :workflow, class_name: 'Workflow'
72
+
73
+ define_index_fields do
74
+ integer :id
75
+ date :last_assigned_at, value: :assigned_at
76
+ string :type, value: proc { task_category.name }
77
+ struct :workflow, from: Workflow
78
+ end
79
+ end
80
+ ```
81
+
82
+ As a result, the document will also contain a `workflow` field whose
83
+ value will be derived by requesting a document from the object's
84
+ `workflow` reference (which is a `Workflow` instance). In order to get
85
+ this to work, we need a corresponding definition in the `Workflow`
86
+ class as follows:
87
+
88
+ ```
89
+ class Workflow < ActiveRecord::Base
90
+ include AgnosticBackend::Indexable
91
+ has_many :tasks, class_name: 'Task'
92
+
93
+ define_index_fields(owner: Task) do
94
+ integer :id
95
+ date :created_at
96
+ text_array :notes, value: proc { notes.map(&:body) }
97
+ end
98
+ end
99
+ ```
100
+
101
+ Notice the use of the `owner: Task` argument in
102
+ `define_index_fields`. This means that the document specified within
103
+ the block is to be used only when requested by the `Task`'s document
104
+ generation process. It also implies that we can specify multiple
105
+ document definitions in the same class for different owners. When the
106
+ owner is not specified, it is taken to be the class in which the
107
+ definition is written.
108
+
109
+ ## Document Generation
110
+
111
+ Use the `Indexable#generate_document` method in order to obtain a hash
112
+ with the document's contents. For example, given a `Task` instance:
113
+
114
+ ```ruby
115
+ > task.generate_document
116
+ {:id=>5, :last_assigned_at=>2016-02-09 19:45:00 UTC, ...,
117
+ :workflow=>{:id=>6, ...}}
118
+ ```
119
+
120
+ The document includes all fields specified in `Task` including the
121
+ nested hash retrieved from `Workflow`.
122
+
123
+ ## When should a document be indexed
124
+
125
+ `Indexable` does not specify when and in what way a document should be
126
+ indexed. Instead, this decision is up to the client. The objective is
127
+ to achieve the maximum flexibility with regard to different
128
+ requirements, some of which are summarized below:
129
+
130
+ - when the class is an AR model, the client would like to use AR
131
+ callbacks (such as `after_save` or `after_commit`) to index the
132
+ model.
133
+ - the client may wish to implement document indexing in an
134
+ asynchronous manner for performance reasons.
135
+ - the client may wish to decide whether to index the document only if
136
+ certain conditions are met.
137
+
138
+ The entry point to indexing a document is
139
+ `Indexable::InstanceMethods#trigger_index_notification`, which is
140
+ responsible for notifying whoever has been registered in one or many
141
+ `define_index_notifiers` blocks within the class. The message
142
+ `:index_object` is sent to all objects that need to be notified. By
143
+ default, `Indexable::InstanceMethods#index_object` will delegate to
144
+ `Indexable::InstanceMethods#put_in_index` which will index the
145
+ document synchronously.
146
+
147
+ `Indexable::InstanceMethods#index_object` can be overriden in order to
148
+ implement custom behaviour (such as asynchronous indexing through
149
+ queueing). Any call to `put_in_index` will index
150
+
151
+ - `Indexable::InstanceMethods#trigger_index_notification` must be used
152
+ in order to notify all registered objects
153
+ - `Indexable::InstanceMethods#index_object`, by default, will index
154
+ the document synchronously (by calling
155
+ `Indexable::InstanceMethods#put_in_index`). `index_object` needs to
156
+ be overriden in order to implement custom behaviour (such as
157
+ asynchronous indexing)
158
+ - `Indexable::InstanceMethods#put_in_index` is the method that
159
+ implements the actual indexing. Any custom implementation of
160
+ `index_object` must ultimately call `put_in_index` for indexing to
161
+ occur.
162
+
163
+
164
+ ## Field Types
165
+
166
+ `Indexable` supports the following generic types:
167
+
168
+ - `:integer`
169
+ - `:double`
170
+ - `:string`: this is a literal string (i.e. should be matched exactly)
171
+ - `:string_array`: an array of literal strings
172
+ - `:text`: text that can be interpreted as free text by a specific backend
173
+ - `:text_array`: an array of text fields
174
+ - `:date`: datetime field
175
+ - `:boolean`
176
+ - `:struct`: used to specify a nested structure
177
+
178
+ ## Document Schemas
179
+
180
+ The specification of types in the definition of index fields implies
181
+ that we can derive the document schema using the `Indexable#schema`
182
+ method. E.g. given the `Task` class:
183
+
184
+ ```ruby
185
+ > Task.schema
186
+ {
187
+ "id" => :integer,
188
+ "last_assigned_at" => :date,
189
+ "type" => :string,
190
+ "workflow" => {
191
+ "id" => :integer,
192
+ "created_at" => :date,
193
+ "notes" => :text_array
194
+ }
195
+ }
196
+ ```
197
+
198
+ ## Custom Field Attributes
199
+
200
+ The definition of index fields within a class allows for the
201
+ specification of custom attributes, for example:
202
+
203
+ ```ruby
204
+ class Task < ActiveRecord::Base
205
+ include AgnosticBackend::Indexable
206
+ belongs_to :workflow, class_name: 'Workflow'
207
+
208
+ define_index_fields do
209
+ integer :id
210
+ date :last_assigned_at, value: :assigned_at,
211
+ is_column: true, label: 'Last Assigned At'
212
+ string :type, value: proc { task_category.name }
213
+ is_column: true, label: 'Task Type'
214
+ struct :workflow, from: Workflow
215
+ end
216
+ end
217
+ ```
218
+
219
+ In this example, we have specified two custom attributes for fields
220
+ `label` and `is_column`, for use in UI elements.
221
+
222
+ We can get these options back (say `:is_column`) by passing a block to
223
+ `Indexable#schema` (that yields a `FieldType` instance) as follows:
224
+
225
+ ```ruby
226
+ > task.schema {|field_type| field_type.get_option('is_column') }
227
+ {:id=>nil, :last_assigned_at=>true, :type=>true, ...}
228
+ ```
229
+
230
+ Custom attributes can be very useful in a variety of situations; for
231
+ example, they can be used in the context of web views in order to
232
+ control the visual/behavioural aspects of the document's fields.
233
+
234
+ ## Polymorphic relationships (for ActiveRecord classes)
235
+
236
+ `Indexable` also supports AR polymorphic relationships as nested
237
+ fields. Suppose we have the following model:
238
+
239
+ ```ruby
240
+ class Task < ActiveRecord::Base
241
+ include AgnosticBackend::Indexable
242
+ has_one :concrete_task, polymorphic: true
243
+
244
+ define_index_fields do
245
+ struct :concrete_task
246
+ end
247
+ end
248
+ ```
249
+
250
+ that has a polymorphic relationship with a concrete task, which can be
251
+ one of various classes, say `ConcreteTaskA` and `ConcreteTaskB`. When
252
+ requesting the `Task`'s schema using `Task.schema` the algorithm can
253
+ not figure out which class needs to be queried about its schema when
254
+ it encounters the `struct` field. As a result, the schema is
255
+ incomplete.
256
+
257
+ This can be overcome by specifying the possible classes that can
258
+ constitute a concrete task using the `from` attribute as:
259
+
260
+ ```ruby
261
+ class Task < ActiveRecord::Base
262
+ include AgnosticBackend::Indexable
263
+ has_one :concrete_task, polymorphic: true
264
+
265
+ define_index_fields do
266
+ struct :concrete_task, from: [ConcreteTaskA, ConcreteTaskB]
267
+ end
268
+ end
269
+ ```
270
+
271
+ As a result, the schema will include a `concrete_task` field whose
272
+ value will be the result of a merge between the schemas of all the
273
+ classes specified in the `from` attribute.
274
+
275
+ ## RSpec Matchers
276
+
277
+ `AgnosticBackends` also supplies `RSpec` matchers for verifying that a
278
+ given class is `Indexable` and that it indexes the expected fields.
279
+
280
+ In your `spec_helper.rb` use the following:
281
+
282
+ ```ruby
283
+ require 'agnostic_backend/rspec/matchers'
284
+
285
+ RSpec.configure do |config|
286
+ config.include AgnosticBackend::RSpec::Matchers
287
+ end
288
+ ```
289
+
290
+ This gives access to the matchers `be_indexable` and
291
+ `define_index_field`. For usage examples, check the
292
+ [corresponding test file](matchers_spec.rb).
data/doc/queryable.md ADDED
File without changes
@@ -0,0 +1,99 @@
1
+ module AgnosticBackend
2
+ module Cloudsearch
3
+ class Index < AgnosticBackend::Index
4
+
5
+ attr_reader :region,
6
+ :domain_name,
7
+ :document_endpoint,
8
+ :search_endpoint,
9
+ :access_key_id,
10
+ :secret_access_key
11
+
12
+ def initialize(indexable_klass, **options)
13
+ super(indexable_klass)
14
+ @region = parse_option(options, :region)
15
+ @domain_name = parse_option(options, :domain_name)
16
+ @document_endpoint = parse_option(options, :document_endpoint)
17
+ @search_endpoint = parse_option(options, :search_endpoint)
18
+ @access_key_id = parse_option(options, :access_key_id)
19
+ @secret_access_key = parse_option(options, :secret_access_key)
20
+ end
21
+
22
+ def indexer
23
+ AgnosticBackend::Cloudsearch::Indexer.new(self)
24
+ end
25
+
26
+ def query_builder
27
+ AgnosticBackend::Queryable::Cloudsearch::QueryBuilder.new(self)
28
+ end
29
+
30
+ def schema
31
+ @schema ||= @indexable_klass.schema{|ftype| ftype}
32
+ end
33
+
34
+ def configure
35
+ define_fields_in_domain(indexer.flatten(schema))
36
+ end
37
+
38
+ def cloudsearch_client
39
+ @cloudsearch_client ||= Aws::CloudSearch::Client.new(region: region, access_key_id: access_key_id, secret_access_key: secret_access_key)
40
+ end
41
+
42
+ def cloudsearch_domain_client
43
+ @cloudsearch_domain_client ||= Aws::CloudSearchDomain::Client.new(endpoint: search_endpoint, access_key_id: access_key_id, secret_access_key: secret_access_key)
44
+ end
45
+
46
+ private
47
+
48
+ def remove_fields_from_domain(remote_fields, verbose: true)
49
+ remote_fields.map(&:index_field_name).each do |field_name|
50
+ puts "#{domain_name} > Removing obsolete field: #{field_name}" if verbose
51
+ cloudsearch_client.delete_index_field(domain_name: domain_name,
52
+ index_field_name: field_name)
53
+ end
54
+ end
55
+
56
+ def define_fields_in_domain(flat_schema, verbose: true)
57
+ remote_fields = cloudsearch_client.describe_index_fields(domain_name: domain_name).
58
+ index_fields.map{|field| RemoteIndexField.new(field)}
59
+ puts "Found #{remote_fields.size} remote fields in #{domain_name}" if verbose
60
+ local_fields = index_fields(flat_schema)
61
+ puts "Found #{local_fields.size} local fields for that domain" if verbose
62
+
63
+ valid_remote_fields, obsolete_remote_fields =
64
+ RemoteIndexField.partition(local_fields, remote_fields)
65
+ puts "Found #{valid_remote_fields.size} valid remote fields" if verbose
66
+ puts "Found #{obsolete_remote_fields.size} obsolete remote fields" if verbose
67
+
68
+ remove_fields_from_domain(obsolete_remote_fields) unless obsolete_remote_fields.empty?
69
+
70
+ local_fields.each do |index_field|
71
+ # find the corresponding remote field
72
+ remote_field = valid_remote_fields.find do |remote_field|
73
+ remote_field.index_field_name == index_field.name
74
+ end
75
+ if remote_field.nil? ||
76
+ (remote_field.present? && !index_field.equal_to_remote_field?(remote_field))
77
+ puts "#{domain_name} > Defining new field: #{index_field.name}" if verbose
78
+ index_field.define_in_domain(index: self)
79
+ end
80
+ end
81
+ nil
82
+ end
83
+
84
+ def index_fields(flat_schema)
85
+ flat_schema.map do |field_name, field_type|
86
+ AgnosticBackend::Cloudsearch::IndexField.new(field_name, field_type)
87
+ end
88
+ end
89
+
90
+ def parse_option(options, option_name)
91
+ if options.has_key?(option_name)
92
+ options[option_name]
93
+ else
94
+ raise "#{option_name} must be specified"
95
+ end
96
+ end
97
+ end
98
+ end
99
+ end
@@ -0,0 +1,103 @@
1
+ require "aws-sdk"
2
+
3
+ module AgnosticBackend
4
+ module Cloudsearch
5
+ class IndexField
6
+ include AgnosticBackend::Utilities
7
+
8
+ TYPE_MAPPINGS = {
9
+ AgnosticBackend::Indexable::FieldType::STRING => "literal",
10
+ AgnosticBackend::Indexable::FieldType::STRING_ARRAY => "literal-array",
11
+ AgnosticBackend::Indexable::FieldType::DATE => "date",
12
+ AgnosticBackend::Indexable::FieldType::INTEGER => "int",
13
+ AgnosticBackend::Indexable::FieldType::DOUBLE => "double",
14
+ AgnosticBackend::Indexable::FieldType::BOOLEAN => "literal",
15
+ AgnosticBackend::Indexable::FieldType::TEXT => "text",
16
+ AgnosticBackend::Indexable::FieldType::TEXT_ARRAY => "text-array",
17
+ }.freeze
18
+
19
+ attr_reader :name, :type
20
+
21
+ def initialize(name, type)
22
+ @name = name
23
+ @type = type
24
+ end
25
+
26
+ def define_in_domain(index: )
27
+ with_exponential_backoff Aws::CloudSearch::Errors::Throttling do
28
+ index.cloudsearch_client.define_index_field(
29
+ :domain_name => index.domain_name,
30
+ :index_field => definition
31
+ )
32
+ end
33
+ end
34
+
35
+ def equal_to_remote_field?(remote_field)
36
+ remote_options = remote_field.send(options_name.to_sym)
37
+ local_options = options
38
+
39
+ remote_field.index_field_name == name.to_s &&
40
+ remote_field.index_field_type == cloudsearch_type &&
41
+ local_options.all?{|k, v| v == remote_options.send(k) }
42
+ end
43
+
44
+ def sortable?
45
+ type.has_option(:sortable) ? !!type.get_option(:sortable) : true
46
+ end
47
+
48
+ def searchable?
49
+ type.has_option(:searchable) ? !!type.get_option(:searchable) : true
50
+ end
51
+
52
+ def returnable?
53
+ type.has_option(:returnable) ? !!type.get_option(:returnable) : true
54
+ end
55
+
56
+ def facetable?
57
+ type.has_option(:facetable) ? !!type.get_option(:facetable) : false
58
+ end
59
+
60
+ private
61
+
62
+ def cloudsearch_type
63
+ @cloudsearch_type ||= TYPE_MAPPINGS[type.type]
64
+ end
65
+
66
+ def definition
67
+ {
68
+ :index_field_name => name.to_s,
69
+ :index_field_type => cloudsearch_type,
70
+ options_name.to_sym => options
71
+ }
72
+ end
73
+
74
+ def options_name
75
+ "#{cloudsearch_type.gsub('-', '_')}_options"
76
+ end
77
+
78
+ def options
79
+ opts = {
80
+ :sort_enabled => sortable?,
81
+ :search_enabled => searchable?,
82
+ :return_enabled => returnable?,
83
+ :facet_enabled => facetable?
84
+ }
85
+ # certain parameters are not included acc. to cloudsearch type
86
+ # we filter them out here
87
+ case cloudsearch_type
88
+ when 'text-array'
89
+ opts.delete(:sort_enabled)
90
+ opts.delete(:search_enabled)
91
+ opts.delete(:facet_enabled)
92
+ when 'text'
93
+ opts.delete(:search_enabled)
94
+ opts.delete(:facet_enabled)
95
+ when 'literal-array'
96
+ opts.delete(:sort_enabled)
97
+ end
98
+ opts
99
+ end
100
+
101
+ end
102
+ end
103
+ end
@@ -0,0 +1,84 @@
1
+ require 'aws-sdk'
2
+
3
+ module AgnosticBackend
4
+ module Cloudsearch
5
+ class Indexer < AgnosticBackend::Indexer
6
+ include AgnosticBackend::Utilities
7
+
8
+ def initialize(index)
9
+ @index = index
10
+ end
11
+
12
+ def publish(document)
13
+ with_exponential_backoff Aws::CloudSearch::Errors::Throttling do
14
+ client.upload_documents(
15
+ documents: document,
16
+ content_type:'application/json'
17
+ )
18
+ end
19
+ end
20
+
21
+ def delete(*document_ids)
22
+ documents = document_ids.map do |document_id|
23
+ {"type" => 'delete',
24
+ "id" => document_id}
25
+ end
26
+
27
+ with_exponential_backoff Aws::CloudSearch::Errors::Throttling do
28
+ client.upload_documents(
29
+ documents: convert_to_json(documents),
30
+ content_type:'application/json'
31
+
32
+ )
33
+ end
34
+ end
35
+
36
+ private
37
+
38
+ def client
39
+ index.cloudsearch_domain_client
40
+ end
41
+
42
+ def prepare(document)
43
+ document
44
+ end
45
+
46
+ def transform(document)
47
+ return {} if document.empty?
48
+
49
+ document = flatten document
50
+ document = reject_blank_values_from document
51
+ document = convert_bool_values_to_string_in document
52
+ document = date_format document
53
+ document = add_metadata_to document
54
+ document = convert_document_into_array(document)
55
+ convert_to_json document
56
+
57
+ end
58
+
59
+ def date_format(document)
60
+ document.each do |k, v|
61
+ if v.is_a?(Time)
62
+ document[k] = v.utc.strftime("%Y-%m-%dT%H:%M:%SZ")
63
+ end
64
+ end
65
+ end
66
+
67
+ def add_metadata_to(document)
68
+ {
69
+ "type" => "add",
70
+ "id" => document["id"].to_s,
71
+ "fields" => document,
72
+ }
73
+ end
74
+
75
+ def convert_to_json(transformed_document)
76
+ ActiveSupport::JSON.encode(transformed_document)
77
+ end
78
+
79
+ def convert_document_into_array(document)
80
+ [document]
81
+ end
82
+ end
83
+ end
84
+ end
@@ -0,0 +1,32 @@
1
+ module AgnosticBackend
2
+ module Cloudsearch
3
+ class RemoteIndexField
4
+
5
+ attr_reader :field, :status
6
+
7
+ # returns an array with two elements:
8
+ # the first is an array with the remote fields that correspond to local fields
9
+ # the second is an array with the remote that do not have corresponding local fields
10
+ def self.partition(local_fields, remote_fields)
11
+ local_field_names = local_fields.map(&:name)
12
+ remote_fields.partition do |remote_field|
13
+ local_field_names.include? remote_field.index_field_name
14
+ end
15
+ end
16
+
17
+ def initialize(remote_field_struct)
18
+ @field = remote_field_struct.options
19
+ @status = remote_field_struct.status
20
+ end
21
+
22
+ def method_missing(method_name)
23
+ if field.respond_to?(method_name)
24
+ field.send(method_name)
25
+ else
26
+ super
27
+ end
28
+ end
29
+
30
+ end
31
+ end
32
+ end
@@ -0,0 +1,24 @@
1
+ module AgnosticBackend
2
+ class Index
3
+
4
+ def initialize(indexable_klass)
5
+ @indexable_klass = indexable_klass
6
+ end
7
+
8
+ def name
9
+ @indexable_klass.index_name
10
+ end
11
+
12
+ def schema
13
+ @indexable_klass.schema
14
+ end
15
+
16
+ def indexer
17
+ raise NotImplementedError
18
+ end
19
+
20
+ def configure(new_schema = nil)
21
+ raise NotImplementedError
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,24 @@
1
+ module AgnosticBackend
2
+ module Indexable
3
+
4
+ class Config
5
+
6
+ class ConfigEntry < Struct.new(:index_class, :options);
7
+ end
8
+
9
+ def self.indices
10
+ @indices ||= {}
11
+ end
12
+
13
+ def self.configure_index(indexable_class, index_class, **options)
14
+ indices[indexable_class.name] = ConfigEntry.new index_class, options
15
+ end
16
+
17
+ def self.create_index_for(indexable_class)
18
+ entry = indices[indexable_class.name]
19
+ entry.index_class.try(:new, indexable_class, entry.options)
20
+ end
21
+
22
+ end
23
+ end
24
+ end