Action 3/3

Building the CPU Inference Infrastructure

Building on an architecture designed by a senior engineer on the team, I owned implementing the batch job that indexes product vectors and the embedding API. Working through the implementation, I came to understand the reasoning behind each component's design, which I've laid out below.

Product Vector Indexing Batch

sequenceDiagram
    autonumber
    participant Dev as Developer<br/>(CI/CD)
    participant ECR
    participant Airflow as Airflow<br/>(MWAA)
    participant SM as SageMaker
    participant RT as SageMaker<br/>Training Job
    participant PDB as Product DB
    participant VDB as Vector DB

    rect rgb(245, 245, 245)
        Note over Dev,ECR: Once, on batch code changes
        Dev->>ECR: Build & push batch image
    end

    rect rgb(245, 245, 245)
        Note over Airflow,VDB: Repeats hourly
        Airflow->>SM: CreateTrainingJob<br/>(image URI, job code)
        SM->>ECR: Pull image
        ECR-->>SM: Image
        SM->>RT: Run container
        activate RT
        RT->>PDB: Fetch recommendable products
        PDB-->>RT: Product list
        RT->>RT: Convert products to vectors
        RT->>VDB: Upsert & delete vectors
        deactivate RT
    end
          

Most products registered in Bungaejangter's product DB sit unclicked and untouched for a long time — items with real demand sell quickly. So we built internal logic that treats the sellable-products DB as effectively defining the catalog of products that can actually be recommended, and built a batch task that converts newly listed or modified products within it into product vectors and indexes them in the vector DB. We defined an Airflow DAG that triggers this hourly as a SageMaker Training Job. Even though this is a batch job, not model training, the team used Training Jobs for it because they were already a familiar, conventional way to run a container with a given image, parameters, and instance spec.

To choose a vector DB service to store this catalog and support real-time similarity search, the team compared three options: Aurora PostgreSQL (pgvector), Milvus, and S3 Vectors. Since our filtered, recommendable-product catalog was on the order of millions of items, all three options performed essentially the same. With performance a non-factor, the comparison came down to operational burden and cost — and the team decided on Aurora.

Aurora vs. Milvus — When Performance Is Equal, Pick the One That's Easier to Manage

Running Milvus in production would require managing several different node types and storage layers on top of a Kubernetes cluster. Adopting it would have handed the DevOps team a set of new management touchpoints — deployment, monitoring, version upgrades — for each component. Adding the pgvector extension to our already-standard RDS cluster, by contrast, gave us vector search with no new learning curve, and that convenience was the basis for choosing Aurora. Granted, if the catalog keeps growing large enough that the vector index no longer fits in cluster memory, we'll need to revisit alternatives like Milvus — but for any foreseeable future, we judged Aurora was enough.

Aurora vs. S3 Vectors — When Management Difficulty Is Equal, Pick the Cheaper One

Both options are fully managed, so neither required standing up any new infrastructure — management difficulty was equally low. But this recommendation system is called from both the home and product-detail recommendation surfaces, so request volume was high. S3 Vectors' pricing model (request count plus scanned data size per request) risked scaling up quickly under high-QPS workloads compared to Aurora's fixed hourly instance pricing. To validate this risk, we ran a two-week PoC after S3 Vectors launched: building the same vector catalog in S3 Vectors and routing 10% of the recommendation system's traffic to it. Search latency and recommendation CTR were similar between the two services during the PoC, but extrapolating the measured cost at 10% traffic to 100% put S3 Vectors' projected monthly cost at roughly 3× Aurora's. On that basis, we confirmed the original plan of staying on Aurora.

Product Vector Embedding API

sequenceDiagram
    autonumber
    participant Client
    participant RecAPI as Recommendation API
    participant EmbAPI as Embedding API
    participant VectorDB as Vector DB

    Client->>RecAPI: Clicked product info<br/>(e.g. product name, category)
    RecAPI->>EmbAPI: Request embedding
    EmbAPI-->>RecAPI: Return embedding
    RecAPI->>VectorDB: Search for similar vectors
    VectorDB-->>RecAPI: Return search results
    RecAPI-->>Client: Return recommendation results
          

Our internal backend standard stack is Kotlin-based Spring Boot, but the embedding API needed a Python ML stack — PyTorch, transformers — to convert product features into vectors, so we built it as a separate service. We packaged the embedding API, built with FastAPI, running as a single uvicorn worker process managed by gunicorn, into a standard-format image and uploaded it to ECR; the DevOps team that manages our internal EKS cluster then tuned an HPA scaling policy based on this image and the expected peak RPS from the recommendation surfaces. As we developed and improved the lightweight encoder, throughput on the same CPU node pool increased, which in turn reduced the replica count and instance footprint needed for production serving — letting us translate the algorithm improvement directly into lower production operating cost.