s3arch 0.0.6 → 0.0.8
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/lambda/controllers/s3arch_controller.rb +7 -1
- data/lib/s3arch/indexer/stream_parser.rb +108 -0
- data/lib/s3arch/indexer.rb +18 -100
- data/lib/s3arch/version.rb +1 -1
- data.tar.gz.sig +0 -0
- metadata +2 -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: 538d32a585dde80df0138c153bd7b715d7ae762a42c122ce793ab037440c6940
|
|
4
|
+
data.tar.gz: f7d53b318b898243fb27ff2e0f9a653c4749cd2e573a74130e93347aa429e9ed
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 8219d1600287fe1ccffc5405cb89019edec9975814c409a03eed6236f15092e4a0621b452aae867d4cb6942c901c928c8e80afb4c601444659d4d8078e2c2d7e
|
|
7
|
+
data.tar.gz: 0fa20c2f54a2c27aa1f5b86fbb4c48809e792045ea5edeb8896d99d2cbde7bc0f5a21bbc7aa022c111391db427358a9ad2b8f8791af26318b82c280a4d80dc77
|
checksums.yaml.gz.sig
CHANGED
|
Binary file
|
|
@@ -44,11 +44,17 @@ class S3archController < BeltController::Base
|
|
|
44
44
|
owners = items.map do |item|
|
|
45
45
|
{
|
|
46
46
|
owner_id: item[config.owner_key] || item.values.first,
|
|
47
|
-
version: item['version'],
|
|
47
|
+
version: format_version(item['version']),
|
|
48
48
|
record_count: item['record_count'],
|
|
49
49
|
updated_at: item['updated_at']
|
|
50
50
|
}
|
|
51
51
|
end
|
|
52
52
|
owners.sort_by { |o| o[:updated_at].to_s }.reverse
|
|
53
53
|
end
|
|
54
|
+
|
|
55
|
+
def format_version(version)
|
|
56
|
+
return version unless version
|
|
57
|
+
|
|
58
|
+
version.to_i.to_s
|
|
59
|
+
end
|
|
54
60
|
end
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module S3arch
|
|
4
|
+
class Indexer
|
|
5
|
+
# Parses DynamoDB Stream events from SQS records into change operations.
|
|
6
|
+
module StreamParser
|
|
7
|
+
private
|
|
8
|
+
|
|
9
|
+
def group_changes(sqs_records)
|
|
10
|
+
grouped = Hash.new { |h, k| h[k] = [] }
|
|
11
|
+
|
|
12
|
+
sqs_records.each do |sqs_record|
|
|
13
|
+
stream_record = JSON.parse(sqs_record['body'])
|
|
14
|
+
event_name = stream_record['eventName']
|
|
15
|
+
new_image = stream_record.dig('dynamodb', 'NewImage')
|
|
16
|
+
old_image = stream_record.dig('dynamodb', 'OldImage')
|
|
17
|
+
|
|
18
|
+
owner_id = extract_owner(new_image || old_image)
|
|
19
|
+
next unless owner_id
|
|
20
|
+
|
|
21
|
+
change = build_change(event_name, new_image, old_image)
|
|
22
|
+
grouped[owner_id] << change if change
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
grouped
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def build_change(event_name, new_image, old_image)
|
|
29
|
+
case event_name
|
|
30
|
+
when 'INSERT'
|
|
31
|
+
tokens = extract_tokens(new_image)
|
|
32
|
+
record_id = extract_record_id(new_image)
|
|
33
|
+
return nil unless tokens && record_id && passes_filter?(new_image)
|
|
34
|
+
|
|
35
|
+
{ action: :insert, record_id: record_id, tokens: tokens, meta: extract_meta(new_image) }
|
|
36
|
+
when 'REMOVE'
|
|
37
|
+
tokens = extract_tokens(old_image)
|
|
38
|
+
record_id = extract_record_id(old_image)
|
|
39
|
+
return nil unless tokens && record_id
|
|
40
|
+
|
|
41
|
+
{ action: :delete, record_id: record_id, tokens: tokens }
|
|
42
|
+
when 'MODIFY'
|
|
43
|
+
build_modify_change(new_image, old_image)
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def build_modify_change(new_image, old_image)
|
|
48
|
+
old_tokens = extract_tokens(old_image)
|
|
49
|
+
new_tokens = extract_tokens(new_image)
|
|
50
|
+
record_id = extract_record_id(new_image)
|
|
51
|
+
return nil unless record_id
|
|
52
|
+
|
|
53
|
+
unless passes_filter?(new_image)
|
|
54
|
+
return old_tokens ? { action: :delete, record_id: record_id, tokens: old_tokens } : nil
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
unless old_tokens
|
|
58
|
+
return new_tokens ? { action: :insert, record_id: record_id, tokens: new_tokens,
|
|
59
|
+
meta: extract_meta(new_image) } : nil
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
return nil unless new_tokens
|
|
63
|
+
|
|
64
|
+
{ action: :update, record_id: record_id, old_tokens: old_tokens, new_tokens: new_tokens,
|
|
65
|
+
meta: extract_meta(new_image) }
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def extract_owner(image)
|
|
69
|
+
return nil unless image
|
|
70
|
+
|
|
71
|
+
val = image[@config.owner_key]
|
|
72
|
+
val.is_a?(Hash) ? val['S'] : val
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
def extract_record_id(image)
|
|
76
|
+
return nil unless image
|
|
77
|
+
|
|
78
|
+
val = image['id']
|
|
79
|
+
val.is_a?(Hash) ? val['S'] : val
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
def extract_tokens(image)
|
|
83
|
+
return nil unless image
|
|
84
|
+
|
|
85
|
+
val = image[@config.token_field]
|
|
86
|
+
return nil unless val
|
|
87
|
+
|
|
88
|
+
if val.is_a?(Hash) && val.key?('M')
|
|
89
|
+
val['M'].transform_values { |v| v.is_a?(Hash) ? (v['S'] || '') : v.to_s }
|
|
90
|
+
elsif val.is_a?(Hash) && !val.key?('S')
|
|
91
|
+
val.transform_values { |v| v.is_a?(Hash) ? (v['S'] || '') : v.to_s }
|
|
92
|
+
end
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
def extract_meta(image)
|
|
96
|
+
@config.metadata_fields.each_with_object({}) do |field, meta|
|
|
97
|
+
val = image[field]
|
|
98
|
+
meta[field] = val.is_a?(Hash) ? (val['S'] || val['N'] || '') : val.to_s
|
|
99
|
+
end
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
def passes_filter?(image)
|
|
103
|
+
plain = image.transform_values { |v| v.is_a?(Hash) ? (v['S'] || v['N'] || v['BOOL']&.to_s || '') : v }
|
|
104
|
+
@config.record_filter.call(plain)
|
|
105
|
+
end
|
|
106
|
+
end
|
|
107
|
+
end
|
|
108
|
+
end
|
data/lib/s3arch/indexer.rb
CHANGED
|
@@ -4,11 +4,15 @@ require 'aws-sdk-dynamodb'
|
|
|
4
4
|
require 'aws-sdk-s3'
|
|
5
5
|
require 'sqlite3'
|
|
6
6
|
require 'json'
|
|
7
|
+
require_relative 'indexer/stream_parser'
|
|
8
|
+
|
|
7
9
|
module S3arch
|
|
8
10
|
# Builds SQLite FTS5 databases per owner from pre-computed tokens stored in DynamoDB.
|
|
9
11
|
# The indexer never sees raw content — only tokens. Supports incremental updates via
|
|
10
12
|
# DynamoDB Stream events (INSERT/MODIFY/REMOVE).
|
|
11
13
|
class Indexer
|
|
14
|
+
include StreamParser
|
|
15
|
+
|
|
12
16
|
def initialize(config: S3arch.configuration)
|
|
13
17
|
config.validate!
|
|
14
18
|
@config = config
|
|
@@ -83,104 +87,6 @@ module S3arch
|
|
|
83
87
|
|
|
84
88
|
private
|
|
85
89
|
|
|
86
|
-
def group_changes(sqs_records)
|
|
87
|
-
grouped = Hash.new { |h, k| h[k] = [] }
|
|
88
|
-
|
|
89
|
-
sqs_records.each do |sqs_record|
|
|
90
|
-
stream_record = JSON.parse(sqs_record['body'])
|
|
91
|
-
event_name = stream_record['eventName']
|
|
92
|
-
new_image = stream_record.dig('dynamodb', 'NewImage')
|
|
93
|
-
old_image = stream_record.dig('dynamodb', 'OldImage')
|
|
94
|
-
|
|
95
|
-
owner_id = extract_owner(new_image || old_image)
|
|
96
|
-
next unless owner_id
|
|
97
|
-
|
|
98
|
-
change = build_change(event_name, new_image, old_image)
|
|
99
|
-
grouped[owner_id] << change if change
|
|
100
|
-
end
|
|
101
|
-
|
|
102
|
-
grouped
|
|
103
|
-
end
|
|
104
|
-
|
|
105
|
-
def build_change(event_name, new_image, old_image)
|
|
106
|
-
case event_name
|
|
107
|
-
when 'INSERT'
|
|
108
|
-
tokens = extract_tokens(new_image)
|
|
109
|
-
record_id = extract_record_id(new_image)
|
|
110
|
-
return nil unless tokens && record_id && passes_filter?(new_image)
|
|
111
|
-
|
|
112
|
-
{ action: :insert, record_id: record_id, tokens: tokens, meta: extract_meta(new_image) }
|
|
113
|
-
when 'REMOVE'
|
|
114
|
-
tokens = extract_tokens(old_image)
|
|
115
|
-
record_id = extract_record_id(old_image)
|
|
116
|
-
return nil unless tokens && record_id
|
|
117
|
-
|
|
118
|
-
{ action: :delete, record_id: record_id, tokens: tokens }
|
|
119
|
-
when 'MODIFY'
|
|
120
|
-
old_tokens = extract_tokens(old_image)
|
|
121
|
-
new_tokens = extract_tokens(new_image)
|
|
122
|
-
record_id = extract_record_id(new_image)
|
|
123
|
-
return nil unless record_id
|
|
124
|
-
|
|
125
|
-
# If item no longer passes filter, treat as delete
|
|
126
|
-
unless passes_filter?(new_image)
|
|
127
|
-
return old_tokens ? { action: :delete, record_id: record_id, tokens: old_tokens } : nil
|
|
128
|
-
end
|
|
129
|
-
|
|
130
|
-
# If item previously didn't pass filter (no old tokens), treat as insert
|
|
131
|
-
unless old_tokens
|
|
132
|
-
return new_tokens ? { action: :insert, record_id: record_id, tokens: new_tokens,
|
|
133
|
-
meta: extract_meta(new_image) } : nil
|
|
134
|
-
end
|
|
135
|
-
|
|
136
|
-
return nil unless new_tokens
|
|
137
|
-
|
|
138
|
-
{ action: :update, record_id: record_id, old_tokens: old_tokens, new_tokens: new_tokens,
|
|
139
|
-
meta: extract_meta(new_image) }
|
|
140
|
-
end
|
|
141
|
-
end
|
|
142
|
-
|
|
143
|
-
def extract_owner(image)
|
|
144
|
-
return nil unless image
|
|
145
|
-
|
|
146
|
-
val = image[@config.owner_key]
|
|
147
|
-
val.is_a?(Hash) ? val['S'] : val
|
|
148
|
-
end
|
|
149
|
-
|
|
150
|
-
def extract_record_id(image)
|
|
151
|
-
return nil unless image
|
|
152
|
-
|
|
153
|
-
val = image['id']
|
|
154
|
-
val.is_a?(Hash) ? val['S'] : val
|
|
155
|
-
end
|
|
156
|
-
|
|
157
|
-
def extract_tokens(image)
|
|
158
|
-
return nil unless image
|
|
159
|
-
|
|
160
|
-
val = image[@config.token_field]
|
|
161
|
-
return nil unless val
|
|
162
|
-
|
|
163
|
-
# Token field is a DynamoDB Map: { "M": { "name": { "S": "..." }, "description": { "S": "..." } } }
|
|
164
|
-
if val.is_a?(Hash) && val.key?('M')
|
|
165
|
-
val['M'].transform_values { |v| v.is_a?(Hash) ? (v['S'] || '') : v.to_s }
|
|
166
|
-
elsif val.is_a?(Hash) && !val.key?('S')
|
|
167
|
-
val.transform_values { |v| v.is_a?(Hash) ? (v['S'] || '') : v.to_s }
|
|
168
|
-
end
|
|
169
|
-
end
|
|
170
|
-
|
|
171
|
-
def extract_meta(image)
|
|
172
|
-
@config.metadata_fields.each_with_object({}) do |field, meta|
|
|
173
|
-
val = image[field]
|
|
174
|
-
meta[field] = val.is_a?(Hash) ? (val['S'] || val['N'] || '') : val.to_s
|
|
175
|
-
end
|
|
176
|
-
end
|
|
177
|
-
|
|
178
|
-
def passes_filter?(image)
|
|
179
|
-
# Convert DynamoDB image to plain hash for filter
|
|
180
|
-
plain = image.transform_values { |v| v.is_a?(Hash) ? (v['S'] || v['N'] || v['BOOL']&.to_s || '') : v }
|
|
181
|
-
@config.record_filter.call(plain)
|
|
182
|
-
end
|
|
183
|
-
|
|
184
90
|
def apply_change(db, change)
|
|
185
91
|
case change[:action]
|
|
186
92
|
when :insert
|
|
@@ -252,7 +158,7 @@ module S3arch
|
|
|
252
158
|
result.items.each do |item|
|
|
253
159
|
next unless @config.record_filter.call(item)
|
|
254
160
|
|
|
255
|
-
tokens = item
|
|
161
|
+
tokens = extract_tokens_from_item(item)
|
|
256
162
|
next unless tokens.is_a?(Hash) && tokens.any?
|
|
257
163
|
|
|
258
164
|
records << { 'id' => item['id'], 'tokens' => tokens, 'meta' => extract_meta_from_item(item) }
|
|
@@ -265,9 +171,21 @@ module S3arch
|
|
|
265
171
|
records
|
|
266
172
|
end
|
|
267
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
|
+
|
|
268
186
|
def build_query_params(owner_id)
|
|
269
187
|
fields = (['id', @config.token_field, @config.owner_key] +
|
|
270
|
-
@config.metadata_fields + @config.filter_fields).uniq
|
|
188
|
+
@config.searchable_fields + @config.metadata_fields + @config.filter_fields).compact.uniq
|
|
271
189
|
expression_names = {}
|
|
272
190
|
projected = fields.map { |f| reserved_word?(f) ? "##{f}".tap { |p| expression_names[p] = f } : f }
|
|
273
191
|
|
data/lib/s3arch/version.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.0.
|
|
4
|
+
version: 0.0.8
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Adam Dalton
|
|
@@ -111,6 +111,7 @@ files:
|
|
|
111
111
|
- lib/s3arch/configuration.rb
|
|
112
112
|
- lib/s3arch/handler.rb
|
|
113
113
|
- lib/s3arch/indexer.rb
|
|
114
|
+
- lib/s3arch/indexer/stream_parser.rb
|
|
114
115
|
- lib/s3arch/routes.rb
|
|
115
116
|
- lib/s3arch/searcher.rb
|
|
116
117
|
- lib/s3arch/tokenizer.rb
|
metadata.gz.sig
CHANGED
|
Binary file
|