agnostic_backend 0.9.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|