Files
lcbp3/specs/03-Data-and-Storage/03-04-legacy-data-migration.md
T
admin 11984bfa29
CI Pipeline / build (push) Failing after 12m41s
Build and Deploy / deploy (push) Failing after 2m44s
260322:1648 Correct Coresspondence / Doing RFA / Correct CI
2026-03-22 16:48:12 +07:00

23 KiB
Raw Blame History

03-04: Legacy Data Migration Plan (PDF 20k Docs)

description version
legacy PDF document migration to system v1.8.0 uses n8n and Ollama 1.8.0

Note: Category Enum system-driven, Idempotency Contract, Duplicate Handling Clarification, Storage Enforcement, Audit Log Enhancement, Review Queue Integration, Revision Drift Protection, Execution Time, Encoding Normalization, Security Hardening, Orchestrator on QNAP, AI Physical Isolation (Desktop Desk-5439), Folder Standard (/share/np-dms/n8n), AI Tag Extraction & Auto-Tagging


1. วัตถุประสงค์ (Objectives)

  • นำเข้าเอกสาร PDF 20,000 ฉบับ พร้อม Metadata จาก Excel (Legacy system export) เข้าสู่ระบบ LCBP3-DMS
  • ใช้ AI (Ollama Local Model) เพื่อตรวจสอบความถูกต้องของลักษณะข้อมูล (Data format, Title consistency) ก่อนการนำเข้า
  • AI Tag Extraction: ใช้ Ollama วิเคราะห์เอกสารและสกัด Tags ที่เกี่ยวข้อง (เช่น สาขางาน, ประเภทเอกสาร, องค์กร) อัตโนมัติ
  • รักษาโครงสร้างความสัมพันธ์ (Project / Contract / Ref No.) และระบบการทำ Revision ตาม Business Rules
  • Checkpoint Support: รองรับการหยุดและเริ่มงานต่อ (Resume) จากจุดที่ค้างอยู่ได้กรณีเกิดเหตุขัดข้อง

Note: เอกสารนี้ขยายความถึงวิธีปฏิบัติ (Implementation) จากการตัดสินใจทางสถาปัตยกรรมใน ADR-017: Ollama Data Migration Architecture


2. โครงสร้างพื้นฐาน (Migration Infrastructure)

  • Migration Orchestrator: n8n (รันจาก Docker Container บน QNAP NAS)
  • AI Validator: Ollama (รันใน Internal Network บน Desktop Desk-5439, RTX 2060 SUPER 8GB)
  • Target Database: MariaDB (correspondences table) บน QNAP NAS
  • Target Storage: QNAP File System — ผ่าน Backend StorageService API เท่านั้น (ห้าม move file โดยตรง)
  • Connection: 2.5G LAN + LACP / Internal VLAN

3. ขั้นตอนการดำเนินงาน (Implementation Steps)

Phase 1: การเตรียม Infrastructure และ Storage (สัปดาห์ที่ 1)

File Migration:

  • ย้ายไฟล์ PDF ทั้งหมดจากแหล่งเก็บไปยัง Folder ชั่วคราวบน NAS (QNAP)
  • Target Path: /share/np-dms/staging_ai/

Mount Folder:

  • Bind Mount /share/np-dms/staging_ai/ เข้ากับ n8n Container แบบ read-only
  • สร้าง /share/np-dms/n8n/migration_logs/ Volume แยกสำหรับเขียน Log แบบ read-write

Ollama Config:

  • ติดตั้ง Ollama บน Desktop (Desk-5439, RTX 2060 SUPER 8GB)
  • No DB credentials, Internal network only

🔍 เปรียบเทียบผลลัพธ์ที่คาดหวัง

งาน Typhoon2-4B Qwen2.5-7B OpenThaiGPT-7B
ความเร็ว (ток/วินาที) ~35-45 ~8-12 ~10-15
ความเข้าใจบริบทไทย ดีมาก ดี ดีมาก
การสร้างแท็กแม่นยำ
ความเสถียรบน 8GB สูง ⚠️ ปานกลาง ⚠️ ปานกลาง
# แนะนำ: llama3.2:3b (เร็ว, VRAM ~3GB, เหมาะ Classification) หรือ ollama run llama3.2:3b
ollama pull llama3.2:3b

# ทางเลือกที่ 1: เร็ว + ไทยดี (แนะนำ)
ollama pull scb10x/typhoon2.1-gemma3-4b
ollama run scb10x/typhoon2.1-gemma3-4b --system "คุณเป็นผู้ช่วยจัดหมวดหมู่เอกสารภาษาไทย โปรดตอบกลับในรูปแบบ JSON เท่านั้น" --option temperature=0.2 --option num_ctx=4096

# ทางเลือกที่ 2: คุณภาพสูง (โมเดลที่คุณใช้อยู่)
ollama pull qwen2.5:7b-instruct-q4_K_M
ollama run qwen2.5:7b-instruct-q4_K_M --system "คุณเป็นผู้ช่วยจัดหมวดหมู่เอกสารภาษาไทย โปรดตอบกลับในรูปแบบ JSON เท่านั้น" --option temperature=0.2 --option num_ctx=4096
# ถ้า Q4_K_M ยังหนักไป ลอง Q3_K_M (คุณภาพลดเล็กน้อย แต่ประหยัดแรม)
ollama pull qwen2.5:7b-instruct-q3_K_M

# ทางเลือกที่ 3: ไทยเฉพาะทาง
ollama pull promptnow/openthaigpt1.5-7b-instruct-q4_k_m
ollama run openthaigpt1.5-7b-instruct-q4_k_m --system "คุณเป็นผู้ช่วยจัดหมวดหมู่เอกสารภาษาไทย โปรดตอบกลับในรูปแบบ JSON เท่านั้น" --option temperature=0.2 --option num_ctx=4096

# เปิด terminal อีกหน้าต่างแล้วรัน
watch -n 1 nvidia-smi

# Fallback: mistral:7b-instruct-q4_K_M (แม่นกว่า, VRAM ~5GB)
# ollama pull mistral:7b-instruct-q4_K_M

ใช้ ทางเลือกที่ 1

ทดสอบ Ollama:

curl http://192.168.20.100:11434/api/generate \
  -d '{"model":"llama3.2:3b","prompt":"reply: ok","stream":false}'

Concurrency Configuration:

  • Sequential: Batch Size = 1, Delay ≥ 2 วินาที, ปิด Parallel Execution
  • เพิ่ม Health Check Node ก่อนเริ่ม Batch เพื่อป้องกัน Workflow ค้างหาก Desktop Sleep หรือ Overheat

Phase 2: การเตรียม Target Database และ API (สัปดาห์ที่ 1)

SQL Indexing:

ALTER TABLE correspondences ADD INDEX idx_doc_number (document_number);
ALTER TABLE correspondences ADD INDEX idx_deleted_at (deleted_at);
ALTER TABLE correspondences ADD INDEX idx_created_by (created_by);

Checkpoint Table:

CREATE TABLE IF NOT EXISTS migration_progress (
    batch_id             VARCHAR(50) PRIMARY KEY,
    last_processed_index INT DEFAULT 0,
    status               ENUM('RUNNING','COMPLETED','FAILED') DEFAULT 'RUNNING',
    updated_at           TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
);

Tags Table (สำหรับ AI Tag Extraction):

-- ตาราง Master เก็บ Tags (Global หรือ Project-specific)
CREATE TABLE tags (
  id INT PRIMARY KEY AUTO_INCREMENT,
  project_id INT NULL COMMENT 'NULL = Global Tag',
  tag_name VARCHAR(100) NOT NULL,
  color_code VARCHAR(30) DEFAULT 'default',
  description TEXT,
  created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
  updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
  created_by INT,
  deleted_at DATETIME NULL,
  UNIQUE KEY ux_tag_project (project_id, tag_name),
  INDEX idx_tags_deleted_at (deleted_at),
  FOREIGN KEY (project_id) REFERENCES projects (id) ON DELETE CASCADE,
  FOREIGN KEY (created_by) REFERENCES users (user_id) ON DELETE SET NULL
);

-- ตารางเชื่อมระหว่าง correspondences และ tags (M:N)
CREATE TABLE correspondence_tags (
  correspondence_id INT,
  tag_id INT,
  PRIMARY KEY (correspondence_id, tag_id),
  FOREIGN KEY (correspondence_id) REFERENCES correspondences (id) ON DELETE CASCADE,
  FOREIGN KEY (tag_id) REFERENCES tags (id) ON DELETE CASCADE,
  INDEX idx_tag_lookup (tag_id)
);

Idempotency Table :

CREATE TABLE IF NOT EXISTS import_transactions (
    id               INT AUTO_INCREMENT PRIMARY KEY,
    idempotency_key  VARCHAR(255) UNIQUE NOT NULL,
    document_number  VARCHAR(100),
    batch_id         VARCHAR(100),
    status_code      INT DEFAULT 201,
    created_at       TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    INDEX idx_idem_key (idempotency_key)
);

Idempotency Logic: ถ้า idempotency_key ซ้ำ → Backend คืน HTTP 200 ทันที (ไม่สร้าง Revision ซ้ำ) ถ้าไม่ซ้ำ → ประมวลผลปกติ

API Authentication — Migration Token:

INSERT INTO users (username, email, role, is_active)
VALUES ('migration_bot', 'migration@system.internal', 'SYSTEM_ADMIN', true);

Scope ของ Migration Token (Patch — คำนิยามชัดเจน):

สิทธิ์ ปกติ Migration Token หมายเหตุ
Bypass File Virus Scan ไฟล์ผ่าน Scan มาแล้วก่อน Import
Bypass Duplicate Validation Error Revision Logic ยัง enforce ปกติ
Bypass Created-by User validation
Overwrite existing revision ห้ามโดยเด็ดขาด
Delete previous revision ห้ามโดยเด็ดขาด
ลบ / แก้ไข Record อื่น ห้ามโดยเด็ดขาด

⚠️ Patch Clarification: "Bypass Duplicate Number Check" ถูกแทนด้วย "Bypass Duplicate Validation Error" — Revision increment logic ยังทำงานตามปกติทุกกรณี

  • Token Expiry: ไม่เกิน 7 วัน ต้อง Revoke ทันทีหลัง Migration เสร็จ
  • IP Whitelist: ใช้ได้เฉพาะจาก <NAS_IP> เท่านั้น
  • Audit: ทุก Request บันทึก created_by = 'SYSTEM_IMPORT'

Phase 3: การออกแบบ n8n Workflow (The Migration Logic)

Node 0: Pre-flight Health Check + Fetch System Categories

ตรวจสอบทุก dependency ก่อน Batch:

  1. HTTP GET Ollama /api/tags → ต้อง HTTP 200
  2. MariaDB SELECT 1 → ต้องเชื่อมได้
  3. HTTP GET Backend /health → ต้อง HTTP 200
  4. File Mount Check → staging_ai มีไฟล์, migration_logs เขียนได้

Fetch System Categories (Patch — ห้าม hardcode):

GET /api/meta/categories
Authorization: Bearer <MIGRATION_TOKEN>

Response:

{ "categories": ["Correspondence", "RFA", "Drawing", "Transmittal", "Report", "Other"] }

n8n ต้องเก็บ categories นี้ไว้ใน Workflow Variable (system_categories) และ inject เข้า AI Prompt ทุก Request

Node 1: Data Reader & Checkpoint

Node 1: Data Reader & Checkpoint

  • อ่าน Checkpoint จาก MariaDB Node แยก
  • Batch ทีละ 50100 แถว ตาม $env.MIGRATION_BATCH_SIZE (ควรจำกัด Batch Size ป้องกัน DB Connection Overload)
  • ติด original_index ทุก Item และ Normalize Encoding (UTF-8 NFC) สำหรับ ชื่อไฟล์ และ เลขเอกสารเก่า

Node 2: DB Lookup & Data Augmentation

  • Task: ให้ n8n นำข้อมูลจาก Excel (เช่น รหัสโปรเจ็กต์, รหัสผู้ส่ง) ยิงคำสั่ง Query ไปยัง MariaDB เพื่อแปลงเป็น id
  • Queries:
    1. แปลง project_code -> project_id
    2. แปลง sender_code -> sender_organization_id
    3. แปลง receiver_code -> receiver_organization_id
    4. หา Tags ที่มีอยู่ในโปรเจ็กต์: SELECT * FROM tags WHERE project_id = {{project_id}}
  • Output: n8n เก็บ project_id, organization_ids และ existing_tags_json ไว้ในแต่ละ item
  • ถ้าหารหัสโปรเจ็กต์ไม่เจอ ให้ส่งเข้า Error Log ไม่ทำต่อ

Node 3: File Processor (Extract PDF Text & Temp Upload)

  • ตรวจสอบไฟล์ PDF มีอยู่จริงบน NAS /share/np-dms/staging_ai
  • Extract PDF Text: ใช้ Apache Tika สกัดข้อความจากเอกสาร
  • Two-Phase Storage (Upload):
    • n8n ยิง POST /api/storage/upload ส่งไฟล์ PDF เข้า Backend
    • Backend อัพโหลดไฟล์, กำหนด is_temporary = TRUE
    • Backend ส่งคืน attachment_id ให้ n8n (จะเรียกว่า temp_attachment_id)

Node 4: AI Analysis (Sequential เท่านั้น)

System Prompt:

You are a Document Controller for a large construction project.
Your task is to validate document metadata, summarize content, and suggest relevant tags.
You MUST respond ONLY with valid JSON. No explanation, no markdown.

User Prompt:

Validate and summarize this document. Respond in JSON.
Document Number: {{$json.document_number}}
Title: {{$json.title}}
Extracted Text: {{$json.extracted_text}}

Existing Project Tags: {{$json.existing_tags_json}}

Analyze the content to provide:
1. Validation of Subject/Dates with PDF text.
2. A 4-5 sentence summary.
3. Suggest tags. Select from Existing Project Tags if applicable. If no existing tag fits, suggest a NEW one (set is_new: true).

Respond ONLY with this exact JSON structure:
{
  "is_valid": true | false,
  "confidence": 0.0 to 1.0,
  "category": "Correspondence",
  "summary": "<4-5 sentence summary>",
  "suggested_tags": [
    {"name": "Structural", "description": "...", "is_new": false}
  ],
  "detected_issues": []
}

Node 5: Staging Ingestion (Insert to Review Queue)

ข้อมูลทั้งหมดที่ผ่าน n8n และ AI Model จะต้องไม่ถูกอัพเดทเข้าตารางหลักอัตโนมัติ แต่จะถูกบังคับนำเข้าตาราง Staging migration_review_queue แทน เพื่อรอมนุษย์จัดการผ่าน Frontend UI

Status Routing Policy:

  • confidence >= 0.85 และ is_valid = true -> Status PENDING (พร้อมรับ Batch Import)
  • confidence >= 0.60 และ < 0.85 -> Status PENDING (ติด Flag ให้ระวัง)
  • confidence < 0.60 หรือ is_valid = false -> Status REJECTED
  • Parse Error / AI ไม่ตอบ -> Error Log (Node ถัดไป)

Insert into staging:

INSERT INTO migration_review_queue (
  document_number, title, project_id, sender_organization_id, receiver_organization_id,
  received_date, issued_date, remarks, ai_suggested_category, ai_confidence,
  ai_issues, ai_summary, extracted_tags, temp_attachment_id, status
) VALUES ( ... )
ON DUPLICATE KEY UPDATE status = VALUES(status), ai_summary = VALUES(ai_summary);

Node 6: Error Log & Reject Log

  • Parse Error → เขียนลงไฟล์ /share/np-dms/n8n/migration_logs/error_log.csv
  • ทุก 10-50 ราบการอัพเดท MariaDB migration_progress เพื่อเป็น Checkpoint.

Phase 4: Frontend Management & Final Commit (UI -> Backend API)

  1. หน้าจอ Frontend Management UI ดึงข้อมูลจาก migration_review_queue
  2. Admin สามารถ Browse & Edit ข้อมูล
  3. Tag Review: Admin สามารถพิจารณา Tags ที่เป็น is_new: true ว่าควรตีตก หรือเปลี่ยนไปแมตช์ของเดิม
  4. Admin กดปุ่ม Execute Import ส่งให้ Backend รัน Final Commit.
  5. Backend ยิงคำสั่งสร้าง Correspondence, นำ temp_attachment_id ไปผูกกับ Revision, ปรับเป็น is_temporary = FALSE และสร้าง/เชื่อม Tags จริง.

Phase 4: แผนการทดสอบ (Testing & QA)

Dry Run Policy (Mandatory):

  • All migrations MUST run with --dry-run
  • No DB commit until validation approved

Dry Run Validation (2050 แถว):

  • JSON Parse Success Rate > 95%
  • Category ที่ AI ตอบตรงกับ System Enum ทุกรายการ
  • รัน Batch เดิมซ้ำ 2 รอบ → ต้องไม่สร้าง Duplicate หรือ Revision ซ้ำ (Idempotency Test)
  • Storage Path ตรงตาม Core Storage Spec v1.8.0
  • Revision Drift ถูก route ไป Review Queue

Integrity Check:

-- ตรวจยอด
SELECT COUNT(*) FROM correspondences WHERE created_by = 'SYSTEM_IMPORT';

-- ตรวจ Revision ซ้ำ
SELECT document_number, COUNT(*) as cnt
FROM correspondences WHERE created_by = 'SYSTEM_IMPORT'
GROUP BY document_number HAVING cnt > 1;

-- ตรวจ Idempotency Key ไม่ซ้ำ
SELECT idempotency_key, COUNT(*) as cnt
FROM import_transactions GROUP BY idempotency_key HAVING cnt > 1;

-- ตรวจ Audit Log ครบ
SELECT COUNT(*) FROM audit_logs
WHERE created_by = 'SYSTEM_IMPORT' AND action = 'IMPORT';

Phase 5: การรันงานจริง (Execution & Monitoring)

  • Scheduling: รันอัตโนมัติ 22:00–06:00
  • Expected Runtime: ~3 วินาที/record (2 sec delay + ~1 sec inference) → 20,000 records ≈ 60,000 วินาที (~16.6 ชั่วโมง) → ใช้เวลาประมาณ 34 คืน
  • Daily Check: Admin ตรวจ Review Queue และ Reject Log ทุกเช้าจาก Night Summary Email
  • Progress Tracking: อัปเดต migration_progress ทุก 10 Records

4. Rollback Plan

Step 1: หยุด n8n และ Disable Token

UPDATE users SET is_active = false WHERE username = 'migration_bot';

Step 2: ลบ Records (Transaction)

START TRANSACTION;
DELETE FROM correspondence_files
WHERE correspondence_id IN (SELECT id FROM correspondences WHERE created_by = 'SYSTEM_IMPORT');
DELETE FROM correspondences WHERE created_by = 'SYSTEM_IMPORT';
DELETE FROM import_transactions WHERE batch_id = 'migration_20260226';
SELECT ROW_COUNT();
COMMIT;

Step 3: ย้ายไฟล์กลับ /share/np-dms/staging_ai/ ผ่าน Script แยก

Step 4: Reset State

UPDATE migration_progress
SET status = 'FAILED', last_processed_index = 0
WHERE batch_id = 'migration_20260226';

UPDATE migration_fallback_state
SET recent_error_count = 0, is_fallback_active = FALSE
WHERE batch_id = 'migration_20260226';

5. แผนรับมือความเสี่ยง (Risk Management)

ลำดับที่ ความเสี่ยง การจัดการ (Mitigation)
1 AI Node หรือ GPU ค้าง Timeout 30 วินาที, Retry 3 รอบ, Delay 60 วินาที
2 Ollama ตอบไม่ใช่ JSON JSON Pre-processor + ส่ง Human Review Queue
3 Category ไม่ตรง System Enum Fetch /api/meta/categories ก่อน Batch ทุกครั้ง
4 Idempotency ซ้ำ import_transactions table + Backend คืน HTTP 200
5 Revision Drift ตรวจ Excel revision column → Route ไป Review Queue
6 Storage bypass ห้าม move file โดยตรง — ผ่าน Backend API เท่านั้น
7 GPU VRAM Overflow ใช้เฉพาะ Quantized Model (q4_K_M)
8 ดิสก์ NAS เต็ม ปิด "Save Successful Executions" ใน n8n
9 Migration Token ถูกขโมย Token 7 วัน, IP Whitelist <NAS_IP> เท่านั้น
11 AI Tag Extraction ผิดพลาด Tag confidence < 0.6 → ส่งไป Review Queue / บันทึกใน metadata
12 Tag ซ้ำ/คล้ายกัน Normalization ก่อนบันทึก (lowercase, trim, deduplicate)

6. Post-Migration Verification

-- 1. ตรวจยอดครบ 20,000
SELECT COUNT(*) FROM correspondences WHERE created_by = 'SYSTEM_IMPORT';

-- 2. ตรวจ Revision ผิดปกติ
SELECT document_number, COUNT(*) FROM correspondences
WHERE created_by = 'SYSTEM_IMPORT'
GROUP BY document_number HAVING COUNT(*) > 5;

-- 3. ตรวจ Audit Log ครบ
SELECT COUNT(*) FROM audit_logs
WHERE created_by = 'SYSTEM_IMPORT' AND action = 'IMPORT';

-- 4. ตรวจ Idempotency ไม่มีซ้ำ
SELECT idempotency_key, COUNT(*) FROM import_transactions
GROUP BY idempotency_key HAVING COUNT(*) > 1;

-- 5. ตรวจ Tags ที่สร้างจาก Migration
SELECT COUNT(*) as total_tags FROM tags WHERE created_by = (SELECT user_id FROM users WHERE username = 'migration_bot');

-- 6. ตรวจเอกสารที่มี Tag ผูกอยู่
SELECT COUNT(DISTINCT correspondence_id) as docs_with_tags
FROM correspondence_tags ct
JOIN correspondences c ON ct.correspondence_id = c.id
WHERE c.created_by = (SELECT user_id FROM users WHERE username = 'migration_bot');

-- 7. ตรวจ Tag Distribution
SELECT t.tag_name, COUNT(ct.correspondence_id) as doc_count
FROM tags t
JOIN correspondence_tags ct ON t.id = ct.tag_id
JOIN correspondences c ON ct.correspondence_id = c.id
WHERE c.created_by = (SELECT user_id FROM users WHERE username = 'migration_bot')
GROUP BY t.id, t.tag_name
ORDER BY doc_count DESC
LIMIT 20;

ข้อแนะนำด้าน Physical Storage: ไฟล์ PDF ทั้ง 20,000 ไฟล์จะถูก move โดย Backend StorageService ไปยัง path ที่ถูกต้องโดยอัตโนมัติ ไม่ปล่อยค้างไว้ที่ /share/np-dms/staging_ai/