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.
- checksums.yaml +7 -0
- data/.gitignore +1 -0
- data/.rspec +1 -0
- data/.ruby-gemset +1 -0
- data/.ruby-version +1 -0
- data/.travis.yml +7 -0
- data/CODE_OF_CONDUCT.md +13 -0
- data/Gemfile +4 -0
- data/Gemfile.lock +52 -0
- data/LICENSE.txt +21 -0
- data/README.md +345 -0
- data/Rakefile +6 -0
- data/agnostic_backend.gemspec +34 -0
- data/bin/console +14 -0
- data/bin/setup +7 -0
- data/doc/indexable.md +292 -0
- data/doc/queryable.md +0 -0
- data/lib/agnostic_backend/cloudsearch/index.rb +99 -0
- data/lib/agnostic_backend/cloudsearch/index_field.rb +103 -0
- data/lib/agnostic_backend/cloudsearch/indexer.rb +84 -0
- data/lib/agnostic_backend/cloudsearch/remote_index_field.rb +32 -0
- data/lib/agnostic_backend/index.rb +24 -0
- data/lib/agnostic_backend/indexable/config.rb +24 -0
- data/lib/agnostic_backend/indexable/content_manager.rb +51 -0
- data/lib/agnostic_backend/indexable/field.rb +26 -0
- data/lib/agnostic_backend/indexable/field_type.rb +48 -0
- data/lib/agnostic_backend/indexable/indexable.rb +125 -0
- data/lib/agnostic_backend/indexer.rb +48 -0
- data/lib/agnostic_backend/queryable/attribute.rb +22 -0
- data/lib/agnostic_backend/queryable/cloudsearch/executor.rb +100 -0
- data/lib/agnostic_backend/queryable/cloudsearch/query.rb +29 -0
- data/lib/agnostic_backend/queryable/cloudsearch/query_builder.rb +13 -0
- data/lib/agnostic_backend/queryable/cloudsearch/result_set.rb +27 -0
- data/lib/agnostic_backend/queryable/cloudsearch/visitor.rb +127 -0
- data/lib/agnostic_backend/queryable/criteria/binary.rb +47 -0
- data/lib/agnostic_backend/queryable/criteria/criterion.rb +13 -0
- data/lib/agnostic_backend/queryable/criteria/ternary.rb +36 -0
- data/lib/agnostic_backend/queryable/criteria_builder.rb +83 -0
- data/lib/agnostic_backend/queryable/executor.rb +43 -0
- data/lib/agnostic_backend/queryable/expressions/expression.rb +65 -0
- data/lib/agnostic_backend/queryable/operations/n_ary.rb +18 -0
- data/lib/agnostic_backend/queryable/operations/operation.rb +13 -0
- data/lib/agnostic_backend/queryable/operations/unary.rb +35 -0
- data/lib/agnostic_backend/queryable/query.rb +27 -0
- data/lib/agnostic_backend/queryable/query_builder.rb +98 -0
- data/lib/agnostic_backend/queryable/result_set.rb +42 -0
- data/lib/agnostic_backend/queryable/tree_node.rb +48 -0
- data/lib/agnostic_backend/queryable/validator.rb +174 -0
- data/lib/agnostic_backend/queryable/value.rb +26 -0
- data/lib/agnostic_backend/queryable/visitor.rb +124 -0
- data/lib/agnostic_backend/rspec/matchers.rb +69 -0
- data/lib/agnostic_backend/utilities.rb +207 -0
- data/lib/agnostic_backend/version.rb +3 -0
- data/lib/agnostic_backend.rb +49 -0
- 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
|