smart_rag 0.1.0 → 0.2.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 +4 -4
- data/.env.example +252 -0
- data/.rspec +2 -0
- data/AGENTS.md +33 -0
- data/API_DOCUMENTATION.md +828 -0
- data/CHANGELOG.md +11 -1
- data/ER-diagram.mmd +144 -0
- data/Gemfile +50 -0
- data/Gemfile.lock +381 -0
- data/Hybrid_Reranking.md +171 -0
- data/README.en.md +420 -28
- data/README.md +534 -63
- data/Rakefile +268 -0
- data/SETUP_GUIDE.md +650 -0
- data/SmartChunking.md +180 -0
- data/USAGE_EXAMPLES.md +1002 -0
- data/config/llm_config.yml +4 -2
- data/config/smart_rag.yml +45 -1
- data/config.ru +15 -0
- data/db/migrations/006_create_text_search_configs.rb +3 -2
- data/db/migrations/008_create_embeddings.rb +5 -4
- data/db/migrations/012_add_metadata_to_source_sections.rb +11 -0
- data/db/migrations/013_create_media_jobs.rb +25 -0
- data/db/migrations/014_add_media_job_operations_indexes.rb +11 -0
- data/db/migrations/015_add_media_leases_and_objects.rb +80 -0
- data/db/migrations/016_add_document_principals_and_staging_references.rb +38 -0
- data/db/migrations/017_add_media_job_request_fingerprint.rb +48 -0
- data/db/seeds/text_search_configs.sql +3 -3
- data/design.md +1057 -0
- data/docs/API_DOCUMENTATION.md +838 -0
- data/docs/DOCUMENTATION_INDEX.en.md +60 -0
- data/docs/DOCUMENTATION_INDEX.md +65 -0
- data/docs/FIX_SUMMARY.md +256 -0
- data/docs/FIX_SUMMARY_COMPLETE.md +273 -0
- data/docs/Hybrid_Reranking.md +171 -0
- data/docs/MIGRATION_GUIDE.md +151 -0
- data/docs/PERFORMANCE_GUIDE.md +58 -0
- data/docs/SETUP_GUIDE.md +659 -0
- data/docs/SmartChunking.md +180 -0
- data/docs/USAGE_EXAMPLES.md +1008 -0
- data/docs/design.md +1057 -0
- data/docs/evidence_pack.md +211 -0
- data/docs/requirements.md +376 -0
- data/docs/retrieval_plan.md +251 -0
- data/docs/smartrag_improvement_plan.md +201 -0
- data/docs/smartrag_refactor.md +216 -0
- data/docs/todo.md +931 -0
- data/examples/common.rb +1 -1
- data/exe/smart-rag-db +163 -0
- data/exe/smart-rag-media-worker +34 -0
- data/lib/smart_rag/config.rb +12 -0
- data/lib/smart_rag/core/document_processor.rb +80 -16
- data/lib/smart_rag/core/local_content_store.rb +51 -0
- data/lib/smart_rag/core/media_extractors.rb +140 -0
- data/lib/smart_rag/core/media_job_queue.rb +353 -0
- data/lib/smart_rag/core/media_metadata_extractor.rb +188 -0
- data/lib/smart_rag/core/media_object_registry.rb +79 -0
- data/lib/smart_rag/core/media_processor.rb +228 -0
- data/lib/smart_rag/core/media_safety_policy.rb +61 -0
- data/lib/smart_rag/core/s3_content_store.rb +78 -0
- data/lib/smart_rag/core/transcript_normalizer.rb +44 -0
- data/lib/smart_rag/core/video_semantic_extractor.rb +130 -0
- data/lib/smart_rag/http_access_policy.rb +86 -0
- data/lib/smart_rag/http_app.rb +188 -0
- data/lib/smart_rag/models/embedding.rb +1 -1
- data/lib/smart_rag/models/research_topic.rb +1 -1
- data/lib/smart_rag/models/research_topic_section.rb +5 -0
- data/lib/smart_rag/models/research_topic_tag.rb +5 -0
- data/lib/smart_rag/models/search_log.rb +1 -1
- data/lib/smart_rag/models/section_fts.rb +5 -0
- data/lib/smart_rag/models/section_tag.rb +5 -0
- data/lib/smart_rag/models/source_document.rb +1 -1
- data/lib/smart_rag/models/source_section.rb +1 -1
- data/lib/smart_rag/models/tag.rb +1 -1
- data/lib/smart_rag/models/text_search_config.rb +5 -0
- data/lib/smart_rag/retrieve.rb +72 -1
- data/lib/smart_rag/services/embedding_service.rb +1 -1
- data/lib/smart_rag/services/fulltext_search_service.rb +11 -13
- data/lib/smart_rag/services/hybrid_search_service.rb +15 -11
- data/lib/smart_rag/services/summarization_service.rb +1 -1
- data/lib/smart_rag/services/tag_service.rb +1 -1
- data/lib/smart_rag/version.rb +1 -1
- data/lib/smart_rag.rb +264 -30
- data/patch_language.rb +27 -0
- data/requirements.md +376 -0
- data/source_documents_export.json +11072 -0
- data/todo.md +931 -0
- data/workers/analyze_content.rb +6 -2
- data/workers/get_embedding.rb +1 -1
- metadata +151 -38
data/config/llm_config.yml
CHANGED
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
adapters:
|
|
2
2
|
openai: OpenAIAdapter
|
|
3
|
-
logger_file: ./log/prompt.log
|
|
4
3
|
llms:
|
|
5
4
|
OllamaEmbedding:
|
|
6
5
|
adapter: openai
|
|
@@ -10,6 +9,9 @@ llms:
|
|
|
10
9
|
SiliconFlow:
|
|
11
10
|
adapter: openai
|
|
12
11
|
url: https://api.siliconflow.cn/v1/
|
|
13
|
-
api_key: ""
|
|
12
|
+
api_key: ENV["SILICONFLOW_API_KEY"]
|
|
13
|
+
# Worker files are loaded by SmartRAG services directly from the gem's
|
|
14
|
+
# workers/ directory. These paths are only fallbacks for standalone
|
|
15
|
+
# SmartPrompt usage and must stay non-empty.
|
|
14
16
|
worker_path: "./workers"
|
|
15
17
|
template_path: "./templates"
|
data/config/smart_rag.yml
CHANGED
|
@@ -29,7 +29,7 @@ embedding:
|
|
|
29
29
|
api_key: <%= ENV['EMBEDDING_API_KEY'] || 'ollama-local' %>
|
|
30
30
|
endpoint: <%= ENV['EMBEDDING_ENDPOINT'] || 'http://localhost:11434/v1/embeddings' %>
|
|
31
31
|
model: <%= ENV['EMBEDDING_MODEL'] || 'qwen3-embedding' %>
|
|
32
|
-
dimensions: <%= ENV['EMBEDDING_DIMENSIONS'] ||
|
|
32
|
+
dimensions: <%= ENV['EMBEDDING_DIMENSIONS'] || 4096 %>
|
|
33
33
|
# Request timeout in seconds
|
|
34
34
|
timeout: 30
|
|
35
35
|
# Batch size for embedding generation
|
|
@@ -130,6 +130,50 @@ document:
|
|
|
130
130
|
# Download timeout for URLs
|
|
131
131
|
download_timeout: <%= ENV['DOWNLOAD_TIMEOUT'] || 30 %>
|
|
132
132
|
|
|
133
|
+
# Media ingestion, extraction, storage, and asynchronous jobs
|
|
134
|
+
media:
|
|
135
|
+
max_file_size_mb: <%= ENV['MEDIA_MAX_FILE_SIZE_MB'] || 50 %>
|
|
136
|
+
max_duration_ms: <%= ENV['MEDIA_MAX_DURATION_MS'] || 7200000 %>
|
|
137
|
+
command_timeout_seconds: <%= ENV['MEDIA_COMMAND_TIMEOUT'] || 120 %>
|
|
138
|
+
allow_private_urls: <%= ENV['MEDIA_ALLOW_PRIVATE_URLS'] == 'true' %>
|
|
139
|
+
allowed_hosts: <%= ENV.fetch('MEDIA_ALLOWED_HOSTS', '').split(',').map(&:strip).reject(&:empty?) %>
|
|
140
|
+
content_store:
|
|
141
|
+
enabled: <%= ENV['MEDIA_CONTENT_STORE_ENABLED'] == 'true' %>
|
|
142
|
+
provider: <%= ENV['MEDIA_CONTENT_STORE_PROVIDER'] || 'local' %>
|
|
143
|
+
root: <%= ENV['MEDIA_CONTENT_STORE_ROOT'] || File.expand_path('../storage/media', __dir__) %>
|
|
144
|
+
bucket: <%= ENV['MEDIA_S3_BUCKET'] %>
|
|
145
|
+
region: <%= ENV['MEDIA_S3_REGION'] || 'us-east-1' %>
|
|
146
|
+
prefix: <%= ENV['MEDIA_S3_PREFIX'] || 'smart-rag/media' %>
|
|
147
|
+
endpoint: <%= ENV['MEDIA_S3_ENDPOINT'] %>
|
|
148
|
+
force_path_style: <%= ENV['MEDIA_S3_FORCE_PATH_STYLE'] == 'true' %>
|
|
149
|
+
access_key_id: <%= ENV['MEDIA_S3_ACCESS_KEY_ID'] %>
|
|
150
|
+
secret_access_key: <%= ENV['MEDIA_S3_SECRET_ACCESS_KEY'] %>
|
|
151
|
+
openai:
|
|
152
|
+
base_url: <%= ENV['MEDIA_OPENAI_BASE_URL'] || 'https://api.openai.com' %>
|
|
153
|
+
api_key: <%= ENV['MEDIA_OPENAI_API_KEY'] || ENV['OPENAI_API_KEY'] %>
|
|
154
|
+
vision_model: <%= ENV['MEDIA_VISION_MODEL'] %>
|
|
155
|
+
transcription_model: <%= ENV['MEDIA_TRANSCRIPTION_MODEL'] %>
|
|
156
|
+
timeout_seconds: <%= ENV['MEDIA_MODEL_TIMEOUT'] || 120 %>
|
|
157
|
+
language: <%= ENV['MEDIA_TRANSCRIPTION_LANGUAGE'] %>
|
|
158
|
+
ocr:
|
|
159
|
+
provider: <%= ENV['MEDIA_OCR_PROVIDER'] %>
|
|
160
|
+
language: <%= ENV['MEDIA_OCR_LANGUAGE'] %>
|
|
161
|
+
timeout_seconds: <%= ENV['MEDIA_OCR_TIMEOUT'] || 60 %>
|
|
162
|
+
async:
|
|
163
|
+
max_attempts: <%= ENV['MEDIA_JOB_MAX_ATTEMPTS'] || 3 %>
|
|
164
|
+
stale_after_seconds: <%= ENV['MEDIA_JOB_STALE_AFTER_SECONDS'] || 900 %>
|
|
165
|
+
retention_seconds: <%= ENV['MEDIA_JOB_RETENTION_SECONDS'] || 604800 %>
|
|
166
|
+
heartbeat_interval_seconds: <%= ENV['MEDIA_JOB_HEARTBEAT_INTERVAL_SECONDS'] || 30 %>
|
|
167
|
+
|
|
168
|
+
# Optional HTTP Bearer authentication and shared PostgreSQL quotas.
|
|
169
|
+
http_auth:
|
|
170
|
+
enabled: <%= ENV['SMARTRAG_HTTP_AUTH_ENABLED'] == 'true' %>
|
|
171
|
+
principal: <%= ENV['SMARTRAG_HTTP_PRINCIPAL'] || 'default' %>
|
|
172
|
+
token: <%= ENV['SMARTRAG_HTTP_TOKEN'] %>
|
|
173
|
+
quota:
|
|
174
|
+
requests_per_minute: <%= ENV['SMARTRAG_REQUESTS_PER_MINUTE'] || 0 %>
|
|
175
|
+
upload_bytes_per_day: <%= ENV['SMARTRAG_UPLOAD_BYTES_PER_DAY'] || 0 %>
|
|
176
|
+
|
|
133
177
|
# Logging Configuration
|
|
134
178
|
logging:
|
|
135
179
|
# Log level: debug, info, warn, error, fatal
|
data/config.ru
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
$LOAD_PATH.unshift(File.expand_path('lib', __dir__))
|
|
4
|
+
|
|
5
|
+
require 'smart_rag'
|
|
6
|
+
require 'smart_rag/http_app'
|
|
7
|
+
require 'smart_rag/http_access_policy'
|
|
8
|
+
|
|
9
|
+
config_path = ENV['SMARTRAG_CONFIG_PATH']
|
|
10
|
+
rag = SmartRAG::SmartRAG.new(SmartRAG::Config.load(config_path))
|
|
11
|
+
access_policy = SmartRAG::HttpAccessPolicy.new(db: SmartRAG.db, config: rag.config[:http_auth] || {})
|
|
12
|
+
|
|
13
|
+
# Applications can replace this file and inject server-side OCR, vision, and
|
|
14
|
+
# transcription adapters through HttpApp's extractors: option.
|
|
15
|
+
run SmartRAG::HttpApp.new(rag: rag, access_policy: access_policy)
|
|
@@ -10,11 +10,12 @@ Sequel.migration do
|
|
|
10
10
|
add_index :text_search_configs, :config_name
|
|
11
11
|
|
|
12
12
|
# Seed initial language configurations
|
|
13
|
-
# Note:
|
|
13
|
+
# Note: Chinese uses pg_catalog.simple when pg_jieba is not installed.
|
|
14
|
+
# Set 'zh' to 'jiebacfg' after installing the pg_jieba extension for real segmentation.
|
|
14
15
|
from(:text_search_configs).multi_insert(
|
|
15
16
|
[
|
|
16
17
|
{ language_code: 'en', config_name: 'pg_catalog.english' },
|
|
17
|
-
{ language_code: 'zh', config_name: '
|
|
18
|
+
{ language_code: 'zh', config_name: 'pg_catalog.simple' },
|
|
18
19
|
{ language_code: 'ja', config_name: 'pg_catalog.simple' },
|
|
19
20
|
{ language_code: 'ko', config_name: 'pg_catalog.simple' },
|
|
20
21
|
{ language_code: 'default', config_name: 'pg_catalog.simple' }
|
|
@@ -7,13 +7,14 @@ Sequel.migration do
|
|
|
7
7
|
primary_key :id
|
|
8
8
|
foreign_key :source_id, :source_sections, null: false, on_delete: :cascade
|
|
9
9
|
# Vector dimension size (adjust based on embedding model)
|
|
10
|
-
column :vector, 'vector(
|
|
10
|
+
column :vector, 'vector(4096)', null: false
|
|
11
11
|
DateTime :created_at, default: Sequel::CURRENT_TIMESTAMP
|
|
12
12
|
end
|
|
13
13
|
|
|
14
|
-
#
|
|
15
|
-
#
|
|
16
|
-
|
|
14
|
+
# NOTE: no vector index is created here because pgvector's ivfflat/hnsw
|
|
15
|
+
# indexes only support up to 2000 dimensions, while qwen3-embedding
|
|
16
|
+
# produces 4096-dimension vectors. Similarity search falls back to exact
|
|
17
|
+
# (sequential) scan, which is fine for small to medium datasets.
|
|
17
18
|
|
|
18
19
|
# Composite index for source_id lookups
|
|
19
20
|
add_index :embeddings, :source_id
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
Sequel.migration do
|
|
2
|
+
up do
|
|
3
|
+
add_column :source_sections, :metadata, :jsonb, default: '{}', null: false
|
|
4
|
+
add_index :source_sections, :metadata, type: :gin
|
|
5
|
+
end
|
|
6
|
+
|
|
7
|
+
down do
|
|
8
|
+
drop_index :source_sections, :metadata
|
|
9
|
+
drop_column :source_sections, :metadata
|
|
10
|
+
end
|
|
11
|
+
end
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
Sequel.migration do
|
|
2
|
+
up do
|
|
3
|
+
create_table :media_jobs do
|
|
4
|
+
primary_key :id
|
|
5
|
+
String :operation, null: false, size: 32
|
|
6
|
+
String :source, text: true, null: false
|
|
7
|
+
column :options, :jsonb, default: '{}', null: false
|
|
8
|
+
String :status, null: false, size: 20, default: 'queued'
|
|
9
|
+
Integer :attempts, null: false, default: 0
|
|
10
|
+
Integer :max_attempts, null: false, default: 3
|
|
11
|
+
column :result, :jsonb
|
|
12
|
+
String :error, text: true
|
|
13
|
+
DateTime :available_at, null: false, default: Sequel::CURRENT_TIMESTAMP
|
|
14
|
+
DateTime :started_at
|
|
15
|
+
DateTime :finished_at
|
|
16
|
+
DateTime :created_at, null: false, default: Sequel::CURRENT_TIMESTAMP
|
|
17
|
+
DateTime :updated_at, null: false, default: Sequel::CURRENT_TIMESTAMP
|
|
18
|
+
index [:status, :available_at]
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
down do
|
|
23
|
+
drop_table :media_jobs
|
|
24
|
+
end
|
|
25
|
+
end
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
Sequel.migration do
|
|
2
|
+
up do
|
|
3
|
+
alter_table :media_jobs do
|
|
4
|
+
add_column :idempotency_key, String, size: 128
|
|
5
|
+
add_column :principal, String, size: 128, null: false, default: 'system'
|
|
6
|
+
add_column :lease_token, String, size: 64
|
|
7
|
+
add_column :heartbeat_at, DateTime
|
|
8
|
+
add_index [:principal, :idempotency_key], unique: true
|
|
9
|
+
add_index [:status, :heartbeat_at]
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
create_table :media_objects do
|
|
13
|
+
primary_key :id
|
|
14
|
+
String :content_hash, null: false, size: 128, unique: true
|
|
15
|
+
String :storage_uri, text: true, null: false
|
|
16
|
+
Integer :byte_size, null: false
|
|
17
|
+
Integer :reference_count, null: false, default: 0
|
|
18
|
+
DateTime :created_at, null: false, default: Sequel::CURRENT_TIMESTAMP
|
|
19
|
+
DateTime :updated_at, null: false, default: Sequel::CURRENT_TIMESTAMP
|
|
20
|
+
index :reference_count
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
create_table :media_object_references do
|
|
24
|
+
primary_key :id
|
|
25
|
+
foreign_key :media_object_id, :media_objects, null: false, on_delete: :cascade
|
|
26
|
+
foreign_key :document_id, :source_documents, null: false, on_delete: :cascade
|
|
27
|
+
DateTime :created_at, null: false, default: Sequel::CURRENT_TIMESTAMP
|
|
28
|
+
index [:media_object_id, :document_id], unique: true, name: :media_object_document_unique
|
|
29
|
+
index :document_id
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
create_table :api_rate_limits do
|
|
34
|
+
primary_key :id
|
|
35
|
+
String :principal, null: false, size: 128
|
|
36
|
+
String :period, null: false, size: 16
|
|
37
|
+
DateTime :window_start, null: false
|
|
38
|
+
Integer :request_count, null: false, default: 0
|
|
39
|
+
Bignum :byte_count, null: false, default: 0
|
|
40
|
+
DateTime :updated_at, null: false, default: Sequel::CURRENT_TIMESTAMP
|
|
41
|
+
index [:principal, :period, :window_start], unique: true, name: :api_rate_limit_window_unique
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
run <<~SQL
|
|
45
|
+
INSERT INTO media_objects (content_hash, storage_uri, byte_size, reference_count)
|
|
46
|
+
SELECT metadata->>'content_hash', metadata->>'storage_uri',
|
|
47
|
+
COALESCE((metadata->>'file_size')::bigint, 0), 0
|
|
48
|
+
FROM source_documents
|
|
49
|
+
WHERE metadata ? 'storage_uri' AND metadata ? 'content_hash'
|
|
50
|
+
ON CONFLICT (content_hash) DO NOTHING
|
|
51
|
+
SQL
|
|
52
|
+
run <<~SQL
|
|
53
|
+
INSERT INTO media_object_references (media_object_id, document_id)
|
|
54
|
+
SELECT mo.id, sd.id
|
|
55
|
+
FROM source_documents sd
|
|
56
|
+
JOIN media_objects mo ON mo.content_hash = sd.metadata->>'content_hash'
|
|
57
|
+
WHERE sd.metadata ? 'storage_uri'
|
|
58
|
+
ON CONFLICT DO NOTHING
|
|
59
|
+
SQL
|
|
60
|
+
run <<~SQL
|
|
61
|
+
UPDATE media_objects mo SET reference_count = refs.count
|
|
62
|
+
FROM (SELECT media_object_id, COUNT(*) AS count FROM media_object_references GROUP BY media_object_id) refs
|
|
63
|
+
WHERE refs.media_object_id = mo.id
|
|
64
|
+
SQL
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
down do
|
|
68
|
+
drop_table :api_rate_limits
|
|
69
|
+
drop_table :media_object_references
|
|
70
|
+
drop_table :media_objects
|
|
71
|
+
alter_table :media_jobs do
|
|
72
|
+
drop_index [:status, :heartbeat_at]
|
|
73
|
+
drop_index [:principal, :idempotency_key]
|
|
74
|
+
drop_column :heartbeat_at
|
|
75
|
+
drop_column :lease_token
|
|
76
|
+
drop_column :idempotency_key
|
|
77
|
+
drop_column :principal
|
|
78
|
+
end
|
|
79
|
+
end
|
|
80
|
+
end
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
Sequel.migration do
|
|
2
|
+
up do
|
|
3
|
+
alter_table :source_documents do
|
|
4
|
+
add_column :principal, String, size: 128, null: false, default: 'system'
|
|
5
|
+
add_index :principal
|
|
6
|
+
add_index [:principal, :created_at]
|
|
7
|
+
end
|
|
8
|
+
|
|
9
|
+
alter_table :media_jobs do
|
|
10
|
+
add_foreign_key :staging_media_object_id, :media_objects, on_delete: :set_null
|
|
11
|
+
add_index :staging_media_object_id
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
run <<~SQL
|
|
15
|
+
UPDATE source_documents
|
|
16
|
+
SET principal = COALESCE(NULLIF(metadata->>'principal', ''), 'system')
|
|
17
|
+
SQL
|
|
18
|
+
run <<~SQL
|
|
19
|
+
UPDATE media_jobs mj
|
|
20
|
+
SET staging_media_object_id = mo.id
|
|
21
|
+
FROM media_objects mo
|
|
22
|
+
WHERE mj.source = mo.storage_uri
|
|
23
|
+
AND mj.staging_media_object_id IS NULL
|
|
24
|
+
SQL
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
down do
|
|
28
|
+
alter_table :media_jobs do
|
|
29
|
+
drop_index :staging_media_object_id
|
|
30
|
+
drop_column :staging_media_object_id
|
|
31
|
+
end
|
|
32
|
+
alter_table :source_documents do
|
|
33
|
+
drop_index [:principal, :created_at]
|
|
34
|
+
drop_index :principal
|
|
35
|
+
drop_column :principal
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
end
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'digest'
|
|
4
|
+
require 'json'
|
|
5
|
+
|
|
6
|
+
Sequel.migration do
|
|
7
|
+
up do
|
|
8
|
+
alter_table :media_jobs do
|
|
9
|
+
add_column :request_fingerprint, String, size: 64
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
self[:media_jobs].select(:id, :operation, :source, :options).each do |job|
|
|
13
|
+
options = begin
|
|
14
|
+
JSON.parse(job[:options].to_s)
|
|
15
|
+
rescue JSON::ParserError
|
|
16
|
+
job[:options].to_s
|
|
17
|
+
end
|
|
18
|
+
canonicalize = lambda do |value|
|
|
19
|
+
case value
|
|
20
|
+
when Hash
|
|
21
|
+
value.keys.map(&:to_s).sort.to_h do |key|
|
|
22
|
+
original_key = value.key?(key) ? key : value.keys.find { |candidate| candidate.to_s == key }
|
|
23
|
+
[key, canonicalize.call(value[original_key])]
|
|
24
|
+
end
|
|
25
|
+
when Array
|
|
26
|
+
value.map { |item| canonicalize.call(item) }
|
|
27
|
+
else
|
|
28
|
+
value
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
payload = { 'operation' => job[:operation].to_s, 'source' => job[:source].to_s,
|
|
32
|
+
'options' => canonicalize.call(options) }
|
|
33
|
+
self[:media_jobs].where(id: job[:id]).update(
|
|
34
|
+
request_fingerprint: Digest::SHA256.hexdigest(JSON.generate(payload))
|
|
35
|
+
)
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
alter_table :media_jobs do
|
|
39
|
+
set_column_not_null :request_fingerprint
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
down do
|
|
44
|
+
alter_table :media_jobs do
|
|
45
|
+
drop_column :request_fingerprint
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
end
|
|
@@ -9,9 +9,9 @@ INSERT INTO text_search_configs (language_code, config_name, is_installed) VALUE
|
|
|
9
9
|
('en', 'pg_catalog.english', true),
|
|
10
10
|
('en_us', 'pg_catalog.english', true),
|
|
11
11
|
('en_gb', 'pg_catalog.english', true),
|
|
12
|
-
('zh', '
|
|
13
|
-
('zh_cn', '
|
|
14
|
-
('zh_tw', '
|
|
12
|
+
('zh', 'pg_catalog.simple', true),
|
|
13
|
+
('zh_cn', 'pg_catalog.simple', true),
|
|
14
|
+
('zh_tw', 'pg_catalog.simple', true),
|
|
15
15
|
('ja', 'pg_catalog.simple', true),
|
|
16
16
|
('ja_jp', 'pg_catalog.simple', true),
|
|
17
17
|
('ko', 'pg_catalog.simple', true),
|