s3arch 0.0.9 → 0.1.1
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 +4 -4
- checksums.yaml.gz.sig +0 -0
- data/CHANGELOG.md +13 -0
- data/lambda/controllers/s3arch_controller.rb +4 -36
- data/lambda/models/search_version.rb +72 -0
- data/lib/s3arch/indexer.rb +8 -97
- data/lib/s3arch/searcher.rb +5 -13
- data/lib/s3arch/store.rb +111 -0
- data/lib/s3arch/version.rb +1 -1
- data/lib/s3arch.rb +1 -0
- data.tar.gz.sig +0 -0
- metadata +17 -1
- metadata.gz.sig +0 -0
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 89f4fce79a53db6b2890bf9034c5702e556ce27a1f399e8d4c605b8953fcd033
|
|
4
|
+
data.tar.gz: 13b9092d968a9aa1a44dcacbe9bd9ede1304e8a9e67623219245e0f601a9d467
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 1c2093cac0b0e12bc0a9fe679d218fa13aa6b1dfeb35b1bd69bef03dbf1699eaba32084931330e3322993981d541787966e270803e566716ea4b61de9edaddfe
|
|
7
|
+
data.tar.gz: c6d81a49d383c03c03638a0d6548acc7df697b3fa3f1dc6bc26f8347bf03bb6f29a63707af68baa8920f45dceea74f3fe886533a4914c3af281149271f0b00e1
|
checksums.yaml.gz.sig
CHANGED
|
Binary file
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,18 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [0.1.0] - 2026-06-18
|
|
4
|
+
|
|
5
|
+
### Added
|
|
6
|
+
|
|
7
|
+
- `S3arch::Store` — thin adapter layer encapsulating all DynamoDB/S3 operations (fetch records, upload/download indexes, version tracking). Indexer and Searcher accept an injected `store:` parameter for testability.
|
|
8
|
+
- `S3arch::Models::SearchVersion` — lightweight model for the version tracking table with `.all`, `.find`, `.increment!`, and `#to_h`
|
|
9
|
+
- Comprehensive unit specs (84 examples): Tokenizer, Indexer, Searcher, StreamParser, Handler, Store, SearchVersion
|
|
10
|
+
|
|
11
|
+
### Changed
|
|
12
|
+
|
|
13
|
+
- `Indexer` and `Searcher` no longer call AWS SDKs directly — delegated to `Store`
|
|
14
|
+
- `S3archController` simplified to use `SearchVersion.all` instead of inline DynamoDB scanning
|
|
15
|
+
|
|
3
16
|
## [0.0.5] - 2025-06-12
|
|
4
17
|
|
|
5
18
|
### Added
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
require_relative '../models/search_version'
|
|
4
4
|
|
|
5
5
|
class S3archController < BeltController::Base
|
|
6
6
|
def index
|
|
7
|
-
|
|
8
|
-
success_response(owners:
|
|
7
|
+
versions = S3arch::Models::SearchVersion.all
|
|
8
|
+
success_response(owners: versions.map(&:to_h))
|
|
9
9
|
rescue StandardError => e
|
|
10
10
|
error_response("Failed to load owners: #{e.message}", 500)
|
|
11
11
|
end
|
|
@@ -20,41 +20,9 @@ class S3archController < BeltController::Base
|
|
|
20
20
|
else
|
|
21
21
|
S3arch::Indexer.new.rebuild(owner_id)
|
|
22
22
|
end
|
|
23
|
+
|
|
23
24
|
success_response(status: 'ok', owner_id: owner_id)
|
|
24
25
|
rescue StandardError => e
|
|
25
26
|
error_response("Failed to rebuild index: #{e.message}", 500)
|
|
26
27
|
end
|
|
27
|
-
|
|
28
|
-
private
|
|
29
|
-
|
|
30
|
-
def fetch_owners
|
|
31
|
-
dynamodb = Aws::DynamoDB::Client.new
|
|
32
|
-
config = S3arch.configuration
|
|
33
|
-
items = []
|
|
34
|
-
scan_params = { table_name: config.version_table }
|
|
35
|
-
|
|
36
|
-
loop do
|
|
37
|
-
result = dynamodb.scan(scan_params)
|
|
38
|
-
items.concat(result.items)
|
|
39
|
-
break unless result.last_evaluated_key
|
|
40
|
-
|
|
41
|
-
scan_params[:exclusive_start_key] = result.last_evaluated_key
|
|
42
|
-
end
|
|
43
|
-
|
|
44
|
-
owners = items.map do |item|
|
|
45
|
-
{
|
|
46
|
-
owner_id: item[config.owner_key] || item.values.first,
|
|
47
|
-
version: format_version(item['version']),
|
|
48
|
-
record_count: item['record_count'],
|
|
49
|
-
updated_at: item['updated_at']
|
|
50
|
-
}
|
|
51
|
-
end
|
|
52
|
-
owners.sort_by { |o| o[:updated_at].to_s }.reverse
|
|
53
|
-
end
|
|
54
|
-
|
|
55
|
-
def format_version(version)
|
|
56
|
-
return version unless version
|
|
57
|
-
|
|
58
|
-
version.to_i.to_s
|
|
59
|
-
end
|
|
60
28
|
end
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module S3arch
|
|
4
|
+
module Models
|
|
5
|
+
# Lightweight model for the version tracking table.
|
|
6
|
+
# Each record represents one owner's index state.
|
|
7
|
+
class SearchVersion
|
|
8
|
+
attr_reader :owner_id, :version, :record_count, :updated_at
|
|
9
|
+
|
|
10
|
+
def initialize(owner_id:, version: 0, record_count: 0, updated_at: nil)
|
|
11
|
+
@owner_id = owner_id
|
|
12
|
+
@version = version.to_i
|
|
13
|
+
@record_count = record_count.to_i
|
|
14
|
+
@updated_at = updated_at
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def to_h
|
|
18
|
+
{ owner_id: owner_id, version: version.to_s, record_count: record_count, updated_at: updated_at }
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
class << self
|
|
22
|
+
def all(config: S3arch.configuration)
|
|
23
|
+
items = []
|
|
24
|
+
params = { table_name: config.version_table }
|
|
25
|
+
|
|
26
|
+
loop do
|
|
27
|
+
result = Aws::DynamoDB::Client.new.scan(params)
|
|
28
|
+
items.concat(result.items)
|
|
29
|
+
break unless result.last_evaluated_key
|
|
30
|
+
|
|
31
|
+
params[:exclusive_start_key] = result.last_evaluated_key
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
items.map { |item| from_dynamo(item, config) }
|
|
35
|
+
.sort_by { |v| v.updated_at.to_s }
|
|
36
|
+
.reverse
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def find(owner_id, config: S3arch.configuration)
|
|
40
|
+
result = Aws::DynamoDB::Client.new.get_item(table_name: config.version_table,
|
|
41
|
+
key: { config.owner_key => owner_id },
|
|
42
|
+
projection_expression: 'version')
|
|
43
|
+
return nil unless result.item
|
|
44
|
+
|
|
45
|
+
result.item['version']&.to_i
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def increment!(owner_id, record_count:, config: S3arch.configuration)
|
|
49
|
+
Aws::DynamoDB::Client.new.update_item(
|
|
50
|
+
table_name: config.version_table,
|
|
51
|
+
key: { config.owner_key => owner_id },
|
|
52
|
+
update_expression: 'SET version = if_not_exists(version, :zero) + :one, ' \
|
|
53
|
+
'updated_at = :now, record_count = :count',
|
|
54
|
+
expression_attribute_values: { ':zero' => 0, ':one' => 1, ':now' => Time.now.iso8601,
|
|
55
|
+
':count' => record_count }
|
|
56
|
+
)
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
private
|
|
60
|
+
|
|
61
|
+
def from_dynamo(item, config)
|
|
62
|
+
new(
|
|
63
|
+
owner_id: item[config.owner_key] || item.values.first,
|
|
64
|
+
version: item['version'],
|
|
65
|
+
record_count: item['record_count'],
|
|
66
|
+
updated_at: item['updated_at']
|
|
67
|
+
)
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
end
|
data/lib/s3arch/indexer.rb
CHANGED
|
@@ -1,7 +1,5 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
require 'aws-sdk-dynamodb'
|
|
4
|
-
require 'aws-sdk-s3'
|
|
5
3
|
require 'sqlite3'
|
|
6
4
|
require 'json'
|
|
7
5
|
require_relative 'indexer/stream_parser'
|
|
@@ -13,22 +11,20 @@ module S3arch
|
|
|
13
11
|
class Indexer
|
|
14
12
|
include StreamParser
|
|
15
13
|
|
|
16
|
-
def initialize(config: S3arch.configuration)
|
|
14
|
+
def initialize(config: S3arch.configuration, store: nil)
|
|
17
15
|
config.validate!
|
|
18
16
|
@config = config
|
|
19
|
-
@
|
|
20
|
-
@s3 = Aws::S3::Client.new
|
|
17
|
+
@store = store || Store.new(config: config)
|
|
21
18
|
end
|
|
22
19
|
|
|
23
20
|
# Full rebuild — pulls all tokens from DynamoDB for an owner.
|
|
24
|
-
# Used for initial backfill or when incremental isn't possible.
|
|
25
21
|
def rebuild(owner_id)
|
|
26
|
-
records = fetch_records(owner_id)
|
|
22
|
+
records = @store.fetch_records(owner_id)
|
|
27
23
|
db_path = "/tmp/s3arch_#{owner_id}.sqlite3"
|
|
28
24
|
|
|
29
25
|
build_database(db_path, records)
|
|
30
|
-
|
|
31
|
-
increment_version(owner_id, records.size)
|
|
26
|
+
@store.upload_index(owner_id, db_path)
|
|
27
|
+
@store.increment_version(owner_id, records.size)
|
|
32
28
|
|
|
33
29
|
log(:info, 'Index rebuilt', owner_id: owner_id, record_count: records.size)
|
|
34
30
|
ensure
|
|
@@ -36,12 +32,10 @@ module S3arch
|
|
|
36
32
|
end
|
|
37
33
|
|
|
38
34
|
# Incremental update — applies INSERT/DELETE/UPDATE to an existing index.
|
|
39
|
-
# Downloads current DB from S3, applies changes, re-uploads.
|
|
40
35
|
def apply_changes(owner_id, changes)
|
|
41
36
|
db_path = "/tmp/s3arch_#{owner_id}.sqlite3"
|
|
42
|
-
download_existing(owner_id, db_path)
|
|
43
37
|
|
|
44
|
-
unless
|
|
38
|
+
unless @store.download_index(owner_id, db_path)
|
|
45
39
|
log(:info, 'No existing index, doing full rebuild', owner_id: owner_id)
|
|
46
40
|
return rebuild(owner_id)
|
|
47
41
|
end
|
|
@@ -56,8 +50,8 @@ module S3arch
|
|
|
56
50
|
record_count = db.get_first_value('SELECT COUNT(*) FROM records_meta')
|
|
57
51
|
db.close
|
|
58
52
|
|
|
59
|
-
|
|
60
|
-
increment_version(owner_id, record_count)
|
|
53
|
+
@store.upload_index(owner_id, db_path)
|
|
54
|
+
@store.increment_version(owner_id, record_count)
|
|
61
55
|
|
|
62
56
|
log(:info, 'Index updated incrementally', owner_id: owner_id, changes: changes.size, record_count: record_count)
|
|
63
57
|
ensure
|
|
@@ -65,7 +59,6 @@ module S3arch
|
|
|
65
59
|
end
|
|
66
60
|
|
|
67
61
|
# Process SQS event containing DynamoDB stream records.
|
|
68
|
-
# Groups by owner and applies incremental changes.
|
|
69
62
|
def process_event(event)
|
|
70
63
|
sqs_records = event['Records'] || []
|
|
71
64
|
grouped = group_changes(sqs_records)
|
|
@@ -83,8 +76,6 @@ module S3arch
|
|
|
83
76
|
{ statusCode: 200, body: JSON.generate(rebuilt: grouped.size) }
|
|
84
77
|
end
|
|
85
78
|
|
|
86
|
-
RESERVED_WORDS = Set.new(%w[status name comment count size type]).freeze
|
|
87
|
-
|
|
88
79
|
private
|
|
89
80
|
|
|
90
81
|
def apply_change(db, change)
|
|
@@ -101,7 +92,6 @@ module S3arch
|
|
|
101
92
|
delete_row(db, rowid, change[:old_tokens])
|
|
102
93
|
insert_row(db, rowid, change[:record_id], change[:new_tokens], change[:meta])
|
|
103
94
|
else
|
|
104
|
-
# Record wasn't in index yet, just insert
|
|
105
95
|
new_rowid = next_rowid(db)
|
|
106
96
|
insert_row(db, new_rowid, change[:record_id], change[:new_tokens], change[:meta])
|
|
107
97
|
end
|
|
@@ -126,7 +116,6 @@ module S3arch
|
|
|
126
116
|
fts_cols = @config.searchable_fields
|
|
127
117
|
fts_values = fts_cols.map { |f| tokens[f] || '' }
|
|
128
118
|
placeholders = (['?'] * (fts_values.size + 1)).join(', ')
|
|
129
|
-
# FTS5 contentless delete: INSERT with special 'delete' command
|
|
130
119
|
fts_delete_sql = "INSERT INTO records_fts(records_fts, rowid, #{fts_cols.join(', ')}) " \
|
|
131
120
|
"VALUES ('delete', #{placeholders})"
|
|
132
121
|
db.execute(fts_delete_sql, [rowid] + fts_values)
|
|
@@ -142,70 +131,6 @@ module S3arch
|
|
|
142
131
|
(max || 0) + 1
|
|
143
132
|
end
|
|
144
133
|
|
|
145
|
-
def download_existing(owner_id, db_path)
|
|
146
|
-
@s3.get_object(bucket: @config.index_bucket, key: "#{owner_id}/index.sqlite3", response_target: db_path)
|
|
147
|
-
rescue Aws::S3::Errors::NoSuchKey
|
|
148
|
-
# No existing index — caller will fall back to rebuild
|
|
149
|
-
end
|
|
150
|
-
|
|
151
|
-
# Full rebuild: fetches token field from DynamoDB (never reads content)
|
|
152
|
-
def fetch_records(owner_id)
|
|
153
|
-
records = []
|
|
154
|
-
params = build_query_params(owner_id)
|
|
155
|
-
|
|
156
|
-
loop do
|
|
157
|
-
result = @dynamodb.query(params)
|
|
158
|
-
result.items.each do |item|
|
|
159
|
-
next unless @config.record_filter.call(item)
|
|
160
|
-
|
|
161
|
-
tokens = extract_tokens_from_item(item)
|
|
162
|
-
next unless tokens.is_a?(Hash) && tokens.any?
|
|
163
|
-
|
|
164
|
-
records << { 'id' => item['id'], 'tokens' => tokens, 'meta' => extract_meta_from_item(item) }
|
|
165
|
-
end
|
|
166
|
-
break unless result.last_evaluated_key
|
|
167
|
-
|
|
168
|
-
params[:exclusive_start_key] = result.last_evaluated_key
|
|
169
|
-
end
|
|
170
|
-
|
|
171
|
-
records
|
|
172
|
-
end
|
|
173
|
-
|
|
174
|
-
def extract_tokens_from_item(item)
|
|
175
|
-
# Try the dedicated token field first
|
|
176
|
-
tokens = item[@config.token_field]
|
|
177
|
-
return tokens if tokens.is_a?(Hash) && tokens.any?
|
|
178
|
-
|
|
179
|
-
# Fall back: synthesize tokens from searchable_fields directly
|
|
180
|
-
@config.searchable_fields.each_with_object({}) do |field, map|
|
|
181
|
-
val = item[field]
|
|
182
|
-
map[field] = val.to_s if val
|
|
183
|
-
end
|
|
184
|
-
end
|
|
185
|
-
|
|
186
|
-
def build_query_params(owner_id)
|
|
187
|
-
fields = (['id', @config.token_field, @config.owner_key] +
|
|
188
|
-
@config.searchable_fields + @config.metadata_fields + @config.filter_fields).compact.uniq
|
|
189
|
-
expression_names = {}
|
|
190
|
-
projected = fields.map { |f| reserved_word?(f) ? "##{f}".tap { |p| expression_names[p] = f } : f }
|
|
191
|
-
|
|
192
|
-
owner_placeholder = reserved_word?(@config.owner_key) ? "##{@config.owner_key}" : @config.owner_key
|
|
193
|
-
expression_names["##{@config.owner_key}"] = @config.owner_key if reserved_word?(@config.owner_key)
|
|
194
|
-
|
|
195
|
-
params = { table_name: @config.source_table, index_name: @config.source_index,
|
|
196
|
-
key_condition_expression: "#{owner_placeholder} = :owner",
|
|
197
|
-
expression_attribute_values: { ':owner' => owner_id },
|
|
198
|
-
projection_expression: projected.join(', ') }
|
|
199
|
-
params[:expression_attribute_names] = expression_names if expression_names.any?
|
|
200
|
-
params
|
|
201
|
-
end
|
|
202
|
-
|
|
203
|
-
def extract_meta_from_item(item)
|
|
204
|
-
@config.metadata_fields.to_h { |field| [field, item[field].to_s] }
|
|
205
|
-
end
|
|
206
|
-
|
|
207
|
-
def reserved_word?(field) = RESERVED_WORDS.include?(field.downcase)
|
|
208
|
-
|
|
209
134
|
def build_database(db_path, records)
|
|
210
135
|
FileUtils.rm_f(db_path)
|
|
211
136
|
db = SQLite3::Database.new(db_path)
|
|
@@ -237,20 +162,6 @@ module S3arch
|
|
|
237
162
|
db.close
|
|
238
163
|
end
|
|
239
164
|
|
|
240
|
-
def upload(owner_id, db_path)
|
|
241
|
-
@s3.put_object(bucket: @config.index_bucket, key: "#{owner_id}/index.sqlite3", body: File.open(db_path, 'rb'))
|
|
242
|
-
end
|
|
243
|
-
|
|
244
|
-
def increment_version(owner_id, record_count)
|
|
245
|
-
@dynamodb.update_item(
|
|
246
|
-
table_name: @config.version_table,
|
|
247
|
-
key: { @config.owner_key => owner_id },
|
|
248
|
-
update_expression: 'SET version = if_not_exists(version, :zero) + :one, ' \
|
|
249
|
-
'updated_at = :now, record_count = :count',
|
|
250
|
-
expression_attribute_values: { ':zero' => 0, ':one' => 1, ':now' => Time.now.iso8601, ':count' => record_count }
|
|
251
|
-
)
|
|
252
|
-
end
|
|
253
|
-
|
|
254
165
|
def log(level, message, **data)
|
|
255
166
|
return unless @config.logger
|
|
256
167
|
|
data/lib/s3arch/searcher.rb
CHANGED
|
@@ -1,7 +1,5 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
require 'aws-sdk-dynamodb'
|
|
4
|
-
require 'aws-sdk-s3'
|
|
5
3
|
require 'sqlite3'
|
|
6
4
|
|
|
7
5
|
module S3arch
|
|
@@ -18,11 +16,10 @@ module S3arch
|
|
|
18
16
|
end
|
|
19
17
|
end
|
|
20
18
|
|
|
21
|
-
def initialize(config: S3arch.configuration)
|
|
19
|
+
def initialize(config: S3arch.configuration, store: nil)
|
|
22
20
|
config.validate!
|
|
23
21
|
@config = config
|
|
24
|
-
@
|
|
25
|
-
@s3 = Aws::S3::Client.new
|
|
22
|
+
@store = store || Store.new(config: config)
|
|
26
23
|
end
|
|
27
24
|
|
|
28
25
|
def search(query:, owner_ids:, filters: {})
|
|
@@ -74,23 +71,18 @@ module S3arch
|
|
|
74
71
|
cached = self.class.version_cache[owner_id]
|
|
75
72
|
return cached[:version] if cached && (Time.now - cached[:checked_at]) < @config.version_ttl
|
|
76
73
|
|
|
77
|
-
|
|
78
|
-
key: { @config.owner_key => owner_id },
|
|
79
|
-
projection_expression: 'version')
|
|
80
|
-
version = result.item&.dig('version')
|
|
74
|
+
version = @store.fetch_version(owner_id)
|
|
81
75
|
self.class.version_cache[owner_id] = { version: version, checked_at: Time.now } if version
|
|
82
76
|
version
|
|
83
77
|
end
|
|
84
78
|
|
|
85
79
|
def download_database(owner_id)
|
|
86
80
|
db_path = "/tmp/s3arch_#{owner_id}.sqlite3"
|
|
87
|
-
@
|
|
88
|
-
|
|
81
|
+
return nil unless @store.download_index(owner_id, db_path)
|
|
82
|
+
|
|
89
83
|
db = SQLite3::Database.new(db_path)
|
|
90
84
|
db.results_as_hash = true
|
|
91
85
|
db
|
|
92
|
-
rescue Aws::S3::Errors::NoSuchKey
|
|
93
|
-
nil
|
|
94
86
|
end
|
|
95
87
|
|
|
96
88
|
def query_fts(db, query, filters: {})
|
data/lib/s3arch/store.rb
ADDED
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'aws-sdk-dynamodb'
|
|
4
|
+
require 'aws-sdk-s3'
|
|
5
|
+
|
|
6
|
+
module S3arch
|
|
7
|
+
# Thin adapter layer encapsulating all DynamoDB/S3 operations for index lifecycle.
|
|
8
|
+
# Indexer and Searcher delegate here instead of calling AWS SDKs directly.
|
|
9
|
+
class Store
|
|
10
|
+
def initialize(config: S3arch.configuration)
|
|
11
|
+
@config = config
|
|
12
|
+
@dynamodb = Aws::DynamoDB::Client.new
|
|
13
|
+
@s3 = Aws::S3::Client.new
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
# --- Source data (read tokens from host app's DynamoDB table) ---
|
|
17
|
+
|
|
18
|
+
def fetch_records(owner_id)
|
|
19
|
+
records = []
|
|
20
|
+
params = build_query_params(owner_id)
|
|
21
|
+
|
|
22
|
+
loop do
|
|
23
|
+
result = @dynamodb.query(params)
|
|
24
|
+
result.items.each do |item|
|
|
25
|
+
next unless @config.record_filter.call(item)
|
|
26
|
+
|
|
27
|
+
tokens = extract_tokens_from_item(item)
|
|
28
|
+
next unless tokens.is_a?(Hash) && tokens.any?
|
|
29
|
+
|
|
30
|
+
records << { 'id' => item['id'], 'tokens' => tokens, 'meta' => extract_meta(item) }
|
|
31
|
+
end
|
|
32
|
+
break unless result.last_evaluated_key
|
|
33
|
+
|
|
34
|
+
params[:exclusive_start_key] = result.last_evaluated_key
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
records
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
# --- Index files (SQLite databases on S3) ---
|
|
41
|
+
|
|
42
|
+
def upload_index(owner_id, db_path)
|
|
43
|
+
@s3.put_object(bucket: @config.index_bucket, key: index_key(owner_id), body: File.open(db_path, 'rb'))
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def download_index(owner_id, db_path)
|
|
47
|
+
@s3.get_object(bucket: @config.index_bucket, key: index_key(owner_id), response_target: db_path)
|
|
48
|
+
true
|
|
49
|
+
rescue Aws::S3::Errors::NoSuchKey
|
|
50
|
+
false
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
# --- Version tracking ---
|
|
54
|
+
|
|
55
|
+
def fetch_version(owner_id)
|
|
56
|
+
result = @dynamodb.get_item(table_name: @config.version_table,
|
|
57
|
+
key: { @config.owner_key => owner_id },
|
|
58
|
+
projection_expression: 'version')
|
|
59
|
+
result.item&.dig('version')&.to_i
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def increment_version(owner_id, record_count)
|
|
63
|
+
@dynamodb.update_item(
|
|
64
|
+
table_name: @config.version_table,
|
|
65
|
+
key: { @config.owner_key => owner_id },
|
|
66
|
+
update_expression: 'SET version = if_not_exists(version, :zero) + :one, ' \
|
|
67
|
+
'updated_at = :now, record_count = :count',
|
|
68
|
+
expression_attribute_values: { ':zero' => 0, ':one' => 1, ':now' => Time.now.iso8601, ':count' => record_count }
|
|
69
|
+
)
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
RESERVED_WORDS = Set.new(%w[status name comment count size type]).freeze
|
|
73
|
+
|
|
74
|
+
private
|
|
75
|
+
|
|
76
|
+
def index_key(owner_id) = "#{owner_id}/index.sqlite3"
|
|
77
|
+
|
|
78
|
+
def build_query_params(owner_id)
|
|
79
|
+
fields = (['id', @config.token_field, @config.owner_key] +
|
|
80
|
+
@config.searchable_fields + @config.metadata_fields + @config.filter_fields).compact.uniq
|
|
81
|
+
expression_names = {}
|
|
82
|
+
projected = fields.map { |f| reserved_word?(f) ? "##{f}".tap { |p| expression_names[p] = f } : f }
|
|
83
|
+
|
|
84
|
+
owner_placeholder = reserved_word?(@config.owner_key) ? "##{@config.owner_key}" : @config.owner_key
|
|
85
|
+
expression_names["##{@config.owner_key}"] = @config.owner_key if reserved_word?(@config.owner_key)
|
|
86
|
+
|
|
87
|
+
params = { table_name: @config.source_table, index_name: @config.source_index,
|
|
88
|
+
key_condition_expression: "#{owner_placeholder} = :owner",
|
|
89
|
+
expression_attribute_values: { ':owner' => owner_id },
|
|
90
|
+
projection_expression: projected.join(', ') }
|
|
91
|
+
params[:expression_attribute_names] = expression_names if expression_names.any?
|
|
92
|
+
params
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
def extract_tokens_from_item(item)
|
|
96
|
+
tokens = item[@config.token_field]
|
|
97
|
+
return tokens if tokens.is_a?(Hash) && tokens.any?
|
|
98
|
+
|
|
99
|
+
@config.searchable_fields.each_with_object({}) do |field, map|
|
|
100
|
+
val = item[field]
|
|
101
|
+
map[field] = val.to_s if val
|
|
102
|
+
end
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
def extract_meta(item)
|
|
106
|
+
@config.metadata_fields.to_h { |field| [field, item[field].to_s] }
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
def reserved_word?(field) = RESERVED_WORDS.include?(field.downcase)
|
|
110
|
+
end
|
|
111
|
+
end
|
data/lib/s3arch/version.rb
CHANGED
data/lib/s3arch.rb
CHANGED
data.tar.gz.sig
CHANGED
|
Binary file
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: s3arch
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.1.1
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Adam Dalton
|
|
@@ -64,6 +64,20 @@ dependencies:
|
|
|
64
64
|
- - "~>"
|
|
65
65
|
- !ruby/object:Gem::Version
|
|
66
66
|
version: '1.0'
|
|
67
|
+
- !ruby/object:Gem::Dependency
|
|
68
|
+
name: belt
|
|
69
|
+
requirement: !ruby/object:Gem::Requirement
|
|
70
|
+
requirements:
|
|
71
|
+
- - "~>"
|
|
72
|
+
- !ruby/object:Gem::Version
|
|
73
|
+
version: '0.0'
|
|
74
|
+
type: :runtime
|
|
75
|
+
prerelease: false
|
|
76
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
77
|
+
requirements:
|
|
78
|
+
- - "~>"
|
|
79
|
+
- !ruby/object:Gem::Version
|
|
80
|
+
version: '0.0'
|
|
67
81
|
- !ruby/object:Gem::Dependency
|
|
68
82
|
name: rack
|
|
69
83
|
requirement: !ruby/object:Gem::Requirement
|
|
@@ -107,6 +121,7 @@ files:
|
|
|
107
121
|
- infrastructure/routes.tf.rb
|
|
108
122
|
- infrastructure/schema.tf.rb
|
|
109
123
|
- lambda/controllers/s3arch_controller.rb
|
|
124
|
+
- lambda/models/search_version.rb
|
|
110
125
|
- lib/s3arch.rb
|
|
111
126
|
- lib/s3arch/configuration.rb
|
|
112
127
|
- lib/s3arch/handler.rb
|
|
@@ -114,6 +129,7 @@ files:
|
|
|
114
129
|
- lib/s3arch/indexer/stream_parser.rb
|
|
115
130
|
- lib/s3arch/routes.rb
|
|
116
131
|
- lib/s3arch/searcher.rb
|
|
132
|
+
- lib/s3arch/store.rb
|
|
117
133
|
- lib/s3arch/tokenizer.rb
|
|
118
134
|
- lib/s3arch/version.rb
|
|
119
135
|
- lib/s3arch/web.rb
|
metadata.gz.sig
CHANGED
|
Binary file
|