agnostic_backend 0.9.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 (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