SiamCafe.net Blog
Technology

Payload CMS MLOps Workflow

payload cms mlops workflow
Payload CMS MLOps Workflow | SiamCafe Blog
2025-11-28· อ. บอม — SiamCafe.net· 10,778 คำ

Payload CMS คืออะไร

Payload CMS เป็น Headless CMS รุ่นใหม่ที่สร้างด้วย TypeScript มี Admin Panel ที่ปรับแต่งได้เต็มที่ รองรับทั้ง REST API และ GraphQL สามารถ Self-host ได้ ไม่ต้องพึ่งบริการ Cloud ของ Vendor มี Access Control ระดับ Field, Versioning, Localization และ Hooks สำหรับ Custom Logic

เมื่อนำ Payload CMS มาใช้ร่วมกับ MLOps Workflow จะได้ Content Platform ที่ชาญฉลาด สามารถจัดหมวดหมู่ Content อัตโนมัติ ทำนาย SEO Score, แนะนำ Content ที่เกี่ยวข้อง และตรวจสอบคุณภาพ Content ด้วย ML Models

Setup Payload CMS

# === Payload CMS Setup ===

# 1. สร้างโปรเจค
npx create-payload-app@latest ml-content-platform
# เลือก: blank template, TypeScript, MongoDB

# 2. โครงสร้างโปรเจค
# ml-content-platform/
# ├── src/
# │   ├── collections/
# │   │   ├── Articles.ts
# │   │   ├── Categories.ts
# │   │   ├── Media.ts
# │   │   └── MLPredictions.ts
# │   ├── hooks/
# │   │   ├── afterArticleChange.ts
# │   │   └── classifyContent.ts
# │   ├── endpoints/
# │   │   └── mlPredict.ts
# │   ├── payload.config.ts
# │   └── server.ts
# ├── ml-pipeline/
# │   ├── train.py
# │   ├── predict.py
# │   └── models/
# ├── package.json
# └── tsconfig.json

# 3. ติดตั้ง Dependencies เพิ่มเติม
npm install @payloadcms/richtext-lexical @payloadcms/db-mongodb
npm install axios node-cron

# --- payload.config.ts ---
# import { buildConfig } from 'payload/config'
# import { mongooseAdapter } from '@payloadcms/db-mongodb'
# import { lexicalEditor } from '@payloadcms/richtext-lexical'
# import { Articles } from './collections/Articles'
# import { Categories } from './collections/Categories'
# import { MLPredictions } from './collections/MLPredictions'
#
# export default buildConfig({
#   editor: lexicalEditor(),
#   collections: [Articles, Categories, MLPredictions],
#   db: mongooseAdapter({
#     url: process.env.MONGODB_URI || 'mongodb://localhost/ml-cms',
#   }),
#   typescript: { outputFile: 'src/payload-types.ts' },
# })

# 4. Articles Collection
# --- src/collections/Articles.ts ---
# import { CollectionConfig } from 'payload/types'
# import { classifyContent } from '../hooks/classifyContent'
#
# export const Articles: CollectionConfig = {
#   slug: 'articles',
#   admin: { useAsTitle: 'title' },
#   access: {
#     read: () => true,
#     create: ({ req: { user } }) => !!user,
#     update: ({ req: { user } }) => !!user,
#   },
#   hooks: {
#     afterChange: [classifyContent],
#   },
#   fields: [
#     { name: 'title', type: 'text', required: true },
#     { name: 'content', type: 'richText' },
#     { name: 'excerpt', type: 'textarea' },
#     { name: 'category', type: 'relationship', relationTo: 'categories' },
#     { name: 'tags', type: 'array', fields: [
#       { name: 'tag', type: 'text' },
#     ]},
#     { name: 'status', type: 'select', options: [
#       'draft', 'review', 'published',
#     ]},
#     // ML-generated fields
#     { name: 'mlCategory', type: 'text', admin: { readOnly: true }},
#     { name: 'mlSeoScore', type: 'number', admin: { readOnly: true }},
#     { name: 'mlQualityScore', type: 'number', admin: { readOnly: true }},
#     { name: 'mlSuggestedTags', type: 'json', admin: { readOnly: true }},
#   ],
# }

# 5. รันโปรเจค
npm run dev
# เข้า Admin: http://localhost:3000/admin

ML Pipeline สำหรับ Content Intelligence

# ml_pipeline.py — ML Pipeline สำหรับ Content Intelligence
# pip install scikit-learn transformers torch mlflow fastapi uvicorn

import mlflow
import mlflow.sklearn
from sklearn.feature_extraction.text import TfidfVectorizer
from sklearn.naive_bayes import MultinomialNB
from sklearn.pipeline import Pipeline
from sklearn.model_selection import train_test_split
from sklearn.metrics import classification_report
import numpy as np
import json
import re
from datetime import datetime

class ContentMLPipeline:
    """ML Pipeline สำหรับ Content Intelligence"""

    def __init__(self, mlflow_uri="http://localhost:5000"):
        mlflow.set_tracking_uri(mlflow_uri)
        self.models = {}

    def train_classifier(self, texts, labels, experiment_name="content_classifier"):
        """Train Content Classifier"""
        mlflow.set_experiment(experiment_name)

        X_train, X_test, y_train, y_test = train_test_split(
            texts, labels, test_size=0.2, random_state=42
        )

        with mlflow.start_run(run_name=f"classifier_{datetime.now():%Y%m%d}"):
            pipeline = Pipeline([
                ("tfidf", TfidfVectorizer(max_features=5000, ngram_range=(1, 2))),
                ("clf", MultinomialNB(alpha=0.1)),
            ])

            pipeline.fit(X_train, y_train)
            y_pred = pipeline.predict(X_test)

            # Metrics
            report = classification_report(y_test, y_pred, output_dict=True)
            mlflow.log_metric("accuracy", report["accuracy"])
            mlflow.log_metric("macro_f1", report["macro avg"]["f1-score"])

            # Log Model
            mlflow.sklearn.log_model(pipeline, "content_classifier",
                                      registered_model_name="content_classifier")

            self.models["classifier"] = pipeline
            print(f"Classifier trained: accuracy={report['accuracy']:.4f}")
            return report

    def predict_category(self, text):
        """ทำนายหมวดหมู่ของ Content"""
        if "classifier" not in self.models:
            # Load from MLflow
            self.models["classifier"] = mlflow.sklearn.load_model(
                "models:/content_classifier/Production"
            )

        prediction = self.models["classifier"].predict([text])[0]
        probabilities = self.models["classifier"].predict_proba([text])[0]
        confidence = float(max(probabilities))

        return {"category": prediction, "confidence": confidence}

    def calculate_seo_score(self, title, content, meta_description=""):
        """คำนวณ SEO Score"""
        score = 0
        factors = {}

        # Title Length (50-60 chars optimal)
        title_len = len(title)
        if 50 <= title_len <= 60:
            factors["title_length"] = 15
        elif 30 <= title_len <= 70:
            factors["title_length"] = 10
        else:
            factors["title_length"] = 5
        score += factors["title_length"]

        # Content Length
        word_count = len(content.split())
        if word_count >= 2000:
            factors["content_length"] = 20
        elif word_count >= 1000:
            factors["content_length"] = 15
        elif word_count >= 500:
            factors["content_length"] = 10
        else:
            factors["content_length"] = 5
        score += factors["content_length"]

        # Headings
        h2_count = content.lower().count("= 3 and h3_count >= 2:
            factors["headings"] = 15
        elif h2_count >= 2:
            factors["headings"] = 10
        else:
            factors["headings"] = 5
        score += factors["headings"]

        # Meta Description
        if meta_description:
            meta_len = len(meta_description)
            if 150 <= meta_len <= 160:
                factors["meta_description"] = 15
            elif 120 <= meta_len <= 170:
                factors["meta_description"] = 10
            else:
                factors["meta_description"] = 5
        else:
            factors["meta_description"] = 0
        score += factors["meta_description"]

        # Internal Links
        link_count = content.lower().count("= 3:
            factors["internal_links"] = 10
        elif link_count >= 1:
            factors["internal_links"] = 5
        else:
            factors["internal_links"] = 0
        score += factors["internal_links"]

        # Images with Alt
        img_count = content.lower().count(" 0 and alt_count >= img_count:
            factors["images"] = 10
        elif img_count > 0:
            factors["images"] = 5
        else:
            factors["images"] = 0
        score += factors["images"]

        # Code blocks (technical content)
        code_count = content.lower().count("
") + content.lower().count("")
        if code_count >= 3:
            factors["code_blocks"] = 10
        elif code_count >= 1:
            factors["code_blocks"] = 5
        else:
            factors["code_blocks"] = 0
        score += factors["code_blocks"]

        # FAQ Section
        if "faq" in content.lower():
            factors["faq"] = 5
            score += 5

        return {"score": min(score, 100), "factors": factors,
                "word_count": word_count}

    def suggest_tags(self, text, top_n=5):
        """แนะนำ Tags จาก Content"""
        # Simple TF-IDF based tag extraction
        from sklearn.feature_extraction.text import TfidfVectorizer

        vectorizer = TfidfVectorizer(max_features=100, stop_words="english",
                                      ngram_range=(1, 2))
        tfidf = vectorizer.fit_transform([text])
        feature_names = vectorizer.get_feature_names_out()
        scores = tfidf.toarray()[0]

        # Top N tags
        top_indices = scores.argsort()[-top_n:][::-1]
        tags = [{"tag": feature_names[i], "score": float(scores[i])}
                for i in top_indices if scores[i] > 0]

        return tags

# ตัวอย่าง
# pipeline = ContentMLPipeline()
# cat = pipeline.predict_category("How to deploy Kubernetes clusters")
# seo = pipeline.calculate_seo_score("Title", "

Content

...

") # tags = pipeline.suggest_tags("machine learning deployment kubernetes")

FastAPI — ML Prediction Service

# ml_service.py — FastAPI Service สำหรับ ML Predictions
from fastapi import FastAPI, HTTPException
from pydantic import BaseModel
from typing import Optional
import logging

app = FastAPI(title="Content ML Service", version="1.0")
logger = logging.getLogger(__name__)

# Initialize Pipeline
pipeline = None

@app.on_event("startup")
async def startup():
    global pipeline
    pipeline = ContentMLPipeline()
    logger.info("ML Pipeline initialized")

class PredictRequest(BaseModel):
    title: str
    content: str
    meta_description: Optional[str] = ""

class PredictResponse(BaseModel):
    category: dict
    seo_score: dict
    suggested_tags: list
    quality_score: float

@app.post("/predict", response_model=PredictResponse)
async def predict(req: PredictRequest):
    """ทำนาย Category, SEO Score, Tags"""
    if pipeline is None:
        raise HTTPException(500, "Pipeline not ready")

    full_text = f"{req.title} {req.content}"

    category = pipeline.predict_category(full_text)
    seo = pipeline.calculate_seo_score(req.title, req.content,
                                        req.meta_description)
    tags = pipeline.suggest_tags(full_text)

    # Quality Score (simple heuristic)
    quality = min(100, seo["score"] * 0.5 + category["confidence"] * 50)

    return PredictResponse(
        category=category,
        seo_score=seo,
        suggested_tags=tags,
        quality_score=round(quality, 1),
    )

@app.get("/health")
async def health():
    return {"status": "healthy", "pipeline_ready": pipeline is not None}

# Payload CMS Hook (TypeScript)
# --- src/hooks/classifyContent.ts ---
# import axios from 'axios'
# import { AfterChangeHook } from 'payload/types'
#
# export const classifyContent: AfterChangeHook = async ({
#   doc, req, operation,
# }) => {
#   if (operation === 'create' || operation === 'update') {
#     try {
#       const response = await axios.post(
#         'http://localhost:8000/predict',
#         {
#           title: doc.title,
#           content: JSON.stringify(doc.content),
#           meta_description: doc.excerpt || '',
#         }
#       )
#       const { category, seo_score, suggested_tags, quality_score } =
#         response.data
#
#       await req.payload.update({
#         collection: 'articles',
#         id: doc.id,
#         data: {
#           mlCategory: category.category,
#           mlSeoScore: seo_score.score,
#           mlQualityScore: quality_score,
#           mlSuggestedTags: suggested_tags,
#         },
#       })
#     } catch (error) {
#       console.error('ML prediction failed:', error.message)
#     }
#   }
# }

# รัน: uvicorn ml_service:app --host 0.0.0.0 --port 8000

Best Practices

  • Async Hooks: ใช้ Payload Hooks แบบ Async เรียก ML Service ไม่ Block User
  • Caching: Cache ML Predictions สำหรับ Content ที่ไม่เปลี่ยน ลด Latency
  • Fallback: ถ้า ML Service ล่ม ให้ CMS ทำงานปกติ ML Fields เป็น Optional
  • Model Versioning: ใช้ MLflow Registry จัดการ Model Versions แยก Staging/Production
  • Batch Processing: สำหรับ Content เยอะ ใช้ Batch Prediction แทน Real-time
  • Monitoring: ติดตาม Prediction Accuracy, Latency, Error Rate ตั้ง Alert

Payload CMS คืออะไร

Headless CMS แบบ Open-source สร้างด้วย TypeScript และ React มี Admin Panel ปรับแต่งได้ รองรับ REST API, GraphQL, Access Control, Versioning, Localization และ Hooks รันบน Node.js ใช้ MongoDB หรือ PostgreSQL

MLOps Workflow คืออะไร

กระบวนการจัดการ ML ตั้งแต่ Data Collection, Feature Engineering, Training, Evaluation, Deployment ถึง Monitoring แบบอัตโนมัติ ใช้เครื่องมือ MLflow, Kubeflow, Airflow, DVC

Payload CMS เกี่ยวกับ MLOps อย่างไร

Payload CMS เป็นแหล่ง Content ที่เชื่อมกับ ML Pipeline สำหรับ Content Classification อัตโนมัติ, SEO Score Prediction, Image Tagging, Content Recommendation และ Quality Check

Payload CMS ติดตั้งอย่างไร

npx create-payload-app เลือก Template และ Database ตั้งค่า Collections ใน payload.config.ts กำหนด Fields, Access Control, Hooks รัน npm run dev เข้า Admin ที่ localhost:3000/admin

สรุป

Payload CMS ร่วมกับ MLOps Workflow สร้าง Content Platform ที่ชาญฉลาด ใช้ Hooks เชื่อมกับ ML Service สำหรับ Content Classification, SEO Scoring, Tag Suggestion และ Quality Assessment ใช้ MLflow จัดการ Model Versions FastAPI เป็น Prediction Service สิ่งสำคัญคือ Async Processing, Caching, Fallback Strategy และ Model Monitoring

📖 บทความที่เกี่ยวข้อง

DALL-E API MLOps Workflowอ่านบทความ → Tailwind CSS v4 MLOps Workflowอ่านบทความ → gRPC Protobuf MLOps Workflowอ่านบทความ → Payload CMS CQRS Event Sourcingอ่านบทความ → Payload CMS Disaster Recovery Planอ่านบทความ →

📚 ดูบทความทั้งหมด →