Milvus
Zilliz
  • Home
  • Blog
  • 나노 바나나 2 + 밀버스 + 퀀 3.5로 이커머스용 베스트셀러-이미지 파이프라인 구축하기

나노 바나나 2 + 밀버스 + 퀀 3.5로 이커머스용 베스트셀러-이미지 파이프라인 구축하기

  • Tutorials
March 03, 2026
Lumina Wang

이커머스 판매자를 위한 AI 툴을 개발하는 사람이라면 이런 요청을 수천 번은 들어보셨을 것입니다: "새로운 제품이 있습니다. 베스트셀러 목록에 올릴 만한 홍보용 이미지를 만들어 주세요. 사진작가나 스튜디오 없이 저렴하게 만들어주세요."

이것이 바로 이 문장의 문제입니다. 판매자는 평면적인 사진과 이미 전환된 베스트셀러 카탈로그를 가지고 있습니다. 판매자는 이 두 가지를 AI를 통해 빠르고 대규모로 연결하고자 합니다.

Google은 2026년 2월 26일에 나노 바나나 2(Gemini 3.1 플래시 이미지)를 출시했을 때, 같은 날 테스트하여 기존 Milvus 기반 검색 파이프라인에 통합했습니다. 그 결과, 총 이미지 생성 비용은 이전의 약 3분의 1 수준으로 떨어졌고 처리량은 두 배로 증가했습니다. 이미지당 가격 인하(나노 바나나 프로보다 약 50% 저렴)가 그 중 일부를 차지하지만, 더 큰 절감 효과는 재작업 주기를 완전히 없앤 데서 비롯됩니다.

이 글에서는 이커머스에 적합한 나노 바나나 2의 장점과 아직 부족한 부분을 살펴보고, 전체 파이프라인에 대한 실습 튜토리얼을 안내합니다: 시각적으로 유사한 베스트셀러를 찾기 위한 Milvus 하이브리드 검색, 스타일 분석을 위한 Qwen 3.5, 최종 세대를 위한 Nano Banana 2에 대해 알아봅니다.

나노 바나나 2의 새로운 기능은 무엇인가요?

나노 바나나 2(제미니 3.1 플래시 이미지)는 2026년 2월 26일에 출시되었습니다. 나노 바나나 프로의 대부분의 기능을 플래시 아키텍처에 적용하여 더 낮은 가격대로 더 빠르게 생성할 수 있습니다. 주요 업그레이드 사항은 다음과 같습니다:

  • 플래시 속도에서 프로 수준의 품질. 나노 바나나 2는 이전에는 프로에서만 가능했던 세계 최고 수준의 지식, 추론, 시각적 충실도를 플래시의 지연 시간 및 처리량으로 제공합니다.
  • 512픽셀에서 4K 출력. 네 가지 해상도 티어(512px, 1K, 2K, 4K)가 기본적으로 지원됩니다. 512px 티어는 Nano Banana 2에 새롭게 추가된 기능입니다.
  • 14가지 화면비. 기존 세트(1:1, 2:3, 3:2, 3:4, 4:3, 4:5, 5:4, 9:16, 16:9, 21:9)에 4:1, 1:4, 8:1, 1:8이 추가되었습니다.
  • 최대 14개의 참조 이미지. 단일 워크플로우에서 최대 5개의 문자에 대해 문자 유사성을 유지하고 최대 14개의 개체에 대해 개체 충실도를 유지합니다.
  • 향상된 텍스트 렌더링. 단일 세대 내에서 번역 및 로컬라이제이션을 지원하여 여러 언어에 걸쳐 읽기 쉽고 정확한 이미지 내 텍스트를 생성합니다.
  • 이미지 검색 기반. Google 검색에서 실시간 웹 데이터와 이미지를 가져와 실제 피사체를 더욱 정확하게 묘사합니다.
  • ~이미지당 최대 50% 저렴. 1K 해상도 기준: 0.067대 Pro의0.0670 .134.

나노 바나노 2의 재미있는 사용 사례: 간단한 Google 지도 스크린샷을 기반으로 위치 인식 파노라마 생성하기

Google 지도 스크린샷과 스타일 프롬프트가 주어지면 모델은 지리적 컨텍스트를 인식하고 올바른 공간 관계를 유지하는 파노라마를 생성합니다. 스톡 사진을 사용하지 않고 지역 타겟 광고 크리에이티브(파리의 카페 배경, 도쿄 거리 풍경)를 제작하는 데 유용합니다.

전체 기능에 대한 자세한 내용은 Google의 발표 블로그와 개발자 문서를 참조하세요.

이 나노 바나나 업데이트는 전자상거래에 어떤 의미가 있나요?

전자상거래는 가장 이미지 집약적인 산업 중 하나입니다. 제품 목록, 마켓플레이스 광고, 소셜 크리에이티브, 배너 캠페인, 현지화된 상점 첫 화면 등 모든 채널에는 각각 고유한 사양을 갖춘 시각적 자산이 지속적으로 필요합니다.

이커머스에서 AI 이미지 생성을 위한 핵심 요구 사항은 다음과 같이 요약됩니다:

  • 낮은 비용 유지 - 이미지당 비용은 카탈로그 규모에서 작동해야 합니다.
  • 검증된 베스트셀러의 이미지와 일치 - 새로운 이미지는 이미 전환된 목록의 시각적 스타일과 일치해야 합니다.
  • 저작권침해 방지 - 경쟁사의 크리에이티브를 복사하거나 보호 대상 자산을 재사용하지 않습니다.

또한 해외 판매자에게는 다음이 필요합니다:

  • 멀티플랫폼 형식 지원 - 마켓플레이스, 광고, 상점의 다양한 화면 비율과 사양을 지원합니다.
  • 다국어 텍스트 렌더링 - 여러 언어에 걸쳐 깔끔하고 정확한 이미지 내 텍스트를 제공합니다.

나노 바나나 2는 모든 상자를 거의 모두 충족합니다. 아래 섹션에서는 각 업그레이드가 실제로 어떤 의미를 갖는지, 즉 이커머스의 문제점을 직접적으로 해결하는 부분과 부족한 부분, 실제 비용에 미치는 영향에 대해 자세히 설명합니다.

출력 생성 비용 최대 60% 절감

1K 해상도에서 나노 바나나 2의 이미지당비용은 0.067달러로프로의0 0. 비해 약 50% 절감됩니다. 하지만 이미지당 가격은 절반에 불과합니다. 사용자 예산을 죽이던 것은 재작업이었습니다. 모든 마켓플레이스는 자체적인 이미지 사양(아마존의 경우 1:1, Shopify 스토어 프론트의 경우 3:4, 배너 광고의 경우 울트라와이드)을 적용하고 있으며, 각 변형을 제작하려면 실패 모드가 있는 별도의 세대별 패스를 사용해야 했습니다.

나노 바나나 2는 이러한 추가 패스를 하나로 통합합니다.

  • 4가지 기본 해상도 계층.

  • 512픽셀 ($0.045)

  • 1K ($0.067)

  • 2K ($0.101)

  • 4K ($0.151).

512px 티어는 나노 바나나 2에 새롭게 추가된 기능입니다. 이제 사용자는 반복 작업을 위해 저비용 512px 초안을 생성하고 별도의 업스케일링 단계 없이 최종 에셋을 2K 또는 4K로 출력할 수 있습니다.

  • 지원되는 화면비는14가지입니다. 다음은 몇 가지 예시입니다:

  • 4:1

  • 1:4

  • 8:1

  • 1:8

이 새로운 울트라 와이드 및 울트라 톨 비율은 기존 세트에 추가됩니다. 한 세대의 세션으로 다음과 같은 다양한 형식을 제작할 수 있습니다: 아마존 메인 이미지 (1:1), 스토어 첫 화면 히어로 (3:4), 배너 광고 (울트라 와이드 또는 기타 비율).

이 4가지 비율에는 자르기, 패딩, 재 프롬프트가 필요하지 않습니다. 나머지 10가지 종횡비는 전체 세트에 포함되어 있어 다양한 플랫폼에서 더욱 유연하게 사용할 수 있습니다.

이미지당 최대 50%의 비용 절감 효과만으로도 비용은 절반으로 줄어듭니다. 해상도와 종횡비에 따른 재작업이 없어지면서 총 비용이 기존 대비 약 1/3 수준으로 낮아졌습니다.

베스트셀러 스타일로 최대 14개의 참조 이미지 지원

나노 바나나 2의 모든 업데이트 중 멀티 레퍼런스 블렌딩은 Milvus 파이프라인에 가장 큰 영향을 미쳤습니다. 나노 바나나 2는 한 번의 요청으로 최대 14개의 레퍼런스 이미지를 사용할 수 있습니다:

  • 최대 5개의 문자에 대한 문자 유사성
  • 최대 14개의 오브젝트에 대한 오브젝트 충실도

실제로 Milvus에서 여러 베스트셀러 이미지를 검색하여 레퍼런스로 전달하면 생성된 이미지가 해당 이미지의 장면 구성, 조명, 포즈, 소품 배치를 그대로 계승했습니다. 이러한 패턴을 수작업으로 재구성하는 데 즉각적인 엔지니어링이 필요하지 않았습니다.

이전 모델은 한두 개의 레퍼런스만 지원했기 때문에 사용자는 모방할 베스트셀러를 하나만 선택해야 했습니다. 14개의 레퍼런스 슬롯을 통해 여러 베스트셀러 목록의 특성을 혼합하여 모델이 복합적인 스타일을 합성할 수 있게 되었습니다. 이것이 바로 아래 튜토리얼의 검색 기반 파이프라인을 가능하게 하는 기능입니다.

기존 제작 비용이나 물류 비용 없이 상업적으로 적합한 프리미엄 비주얼 제작하기

일관되고 안정적인 이미지 생성을 위해서는 모든 요구 사항을 하나의 프롬프트에 몰아넣지 마세요. 배경을 먼저 생성한 다음 모델을 개별적으로 생성하고 마지막으로 합성하는 등 단계적으로 작업하는 것이 더 신뢰할 수 있는 접근 방식입니다.

세 가지 나노 바나나 모델 모두에서 동일한 프롬프트를 사용하여 배경 생성을 테스트했습니다. 창문을 통해 보이는 4:1 비율의 비오는 날 상하이 스카이라인과 동양의 진주탑이 보입니다. 이 프롬프트는 한 번의 패스로 구도, 건축 디테일, 사실감을 테스트합니다.

오리지널 나노 바나나 대 나노 바나나 프로 대 나노 바나나 2

  • 오리지널 나노 바나나. 사실적인 빗방울 분포로 자연스러운 빗방울 텍스처를 구현했지만 건물 디테일은 지나치게 부드럽게 처리했습니다. 오리엔탈 펄 타워는 거의 알아볼 수 없었고 해상도가 제작 요구 사항에 미치지 못했습니다.
  • 나노 바나나 프로. 영화 같은 분위기: 차가운 비와 대비되는 따뜻한 실내 조명이 설득력 있게 표현되었습니다. 하지만 창틀이 완전히 생략되어 이미지의 깊이감이 밋밋해졌습니다. 주인공이 아닌 보조 이미지로 사용할 수 있습니다.
  • 나노 바나나 2. 전체 장면을 렌더링했습니다. 전경의 창틀이 깊이감을 만들어냈습니다. 동양의 진주탑이 선명하게 표현되었습니다. 황푸강에 배가 나타났습니다. 레이어드 라이팅으로 내부의 따뜻한 느낌과 흐린 외부를 구분했습니다. 비와 물 얼룩 텍스처는 사진에 가깝게 표현되었고, 4:1 울트라 와이드 비율은 왼쪽 창 가장자리에서 약간의 왜곡만 있을 뿐 정확한 원근감을 유지했습니다.

제품 사진에서 대부분의 배경 생성 작업에서 Nano Banana 2의 출력물은 후처리 없이도 사용할 수 있었습니다.

여러 언어에 걸쳐 이미지 내 텍스트를 깔끔하게 렌더링하기

가격표, 홍보 배너, 다국어 카피는 이커머스 이미지에서 피할 수 없는 요소이며, 그동안 AI 생성의 한계점이었습니다. 나노 바나나 2는 한 세대 만에 번역 및 로컬라이제이션을 통해 여러 언어에 걸친 이미지 내 텍스트 렌더링을 지원하여 이러한 문제를 훨씬 더 잘 처리합니다.

표준 텍스트 렌더링. 테스트 결과, 가격 라벨, 짧은 마케팅 태그 라인, 이중 언어 제품 설명 등 모든 이커머스 형식에서 오류 없이 텍스트가 출력되었습니다.

손글씨 연속. 전자상거래에는 가격표나 개인화된 카드와 같은 필기 요소가 필요한 경우가 많기 때문에 모델이 기존 필기 스타일과 일치하고 이를 확장할 수 있는지 테스트했습니다(특히 필기 할 일 목록과 일치하고 동일한 스타일로 5개의 새 항목을 추가하는 등). 세 가지 모델에 대한 결과입니다:

  • 오리지널 나노 바나나. 반복되는 시퀀스 번호, 잘못된 구조.
  • 나노 바나나 프로. 레이아웃은 올바르지만 글꼴 스타일이 제대로 재현되지 않음.
  • 나노 바나나 2. 오류 없음. 원본과 구별할 수 없을 정도로 획 굵기와 글자꼴 스타일이 일치함.

하지만 구글의 자체 문서에 따르면 나노 바나나 2는 "정확한 철자와 이미지의 미세한 디테일에 대해서는 여전히 어려움을 겪을 수 있다"고 언급하고 있습니다. 테스트한 모든 형식에서 결과가 깨끗했지만 모든 프로덕션 워크플로에는 게시하기 전에 텍스트 확인 단계가 포함되어야 합니다.

단계별 튜토리얼: Milvus, Qwen 3.5 및 Nano Banana 2를 사용하여 베스트셀러-이미지 파이프라인 구축하기

시작하기 전에 아키텍처 및 모델 설정

단일 프롬프트 생성의 무작위성을 피하기 위해 Milvus 하이브리드 검색으로 이미 작동하는 것을 검색하고, Qwen 3.5로 작동하는 이유를 분석한 다음, Nano Banana 2로 이러한 제약 조건을 구운 최종 이미지를 생성하는 제어 가능한 세 단계로 프로세스를 분할했습니다.

각 도구를 사용해 본 적이 없다면 각 도구에 대한 간단한 입문서를 참고하세요:

  • Milvus: 가장 널리 채택된 오픈 소스 벡터 데이터베이스입니다. 제품 카탈로그를 벡터로 저장하고 하이브리드 검색(밀도 + 스파스 + 스칼라 필터)을 실행하여 신제품과 가장 유사한 베스트셀러 이미지를 찾습니다.
  • Qwen 3.5: 널리 사용되는 멀티모달 LLM. 검색된 베스트셀러 이미지를 가져와 그 뒤에 있는 시각적 패턴(장면 레이아웃, 조명, 포즈, 분위기)을 구조화된 스타일 프롬프트로 추출합니다.
  • 나노 바나나 2: Google의 이미지 생성 모델(Gemini 3.1 플래시 이미지). 신제품 평면 레이아웃, 베스트셀러 참조, Qwen 3.5의 스타일 프롬프트 등 세 가지 입력을 받습니다. 최종 홍보용 사진을 출력합니다.

이 아키텍처의 논리는 한 가지 관찰에서 시작됩니다. 모든 전자상거래 카탈로그에서 가장 가치 있는 시각적 자산은 이미 변환된 베스트셀러 이미지 라이브러리입니다. 이러한 사진의 포즈, 구도, 조명은 실제 광고 지출을 통해 개선되었습니다. 이러한 패턴을 직접 검색하는 것은 프롬프트 작성을 통해 리버스 엔지니어링하는 것보다 훨씬 빠르며, 이러한 검색 단계는 벡터 데이터베이스가 처리하는 것과 정확히 일치합니다.

전체 흐름은 다음과 같습니다. 우리는 OpenRouter API를 통해 모든 모델을 호출하므로 로컬 GPU가 필요하지 않고 다운로드할 모델 가중치도 없습니다.

New product flat-lay
│
│── Embed → Llama Nemotron Embed VL 1B v2
│
│── Search → Milvus hybrid search
│   ├── Dense vectors (visual similarity)
│   ├── Sparse vectors (keyword matching)
│   └── Scalar filters (category + sales volume)
│
│── Analyze → Qwen 3.5 extracts style from retrieved bestsellers
│   └── scene, lighting, pose, mood → style prompt
│
└── Generate → Nano Banana 2
    ├── Inputs: new product + bestseller reference + style prompt
    └── Output: promotional photo

검색 단계를 수행하기 위해 세 가지 Milvus 기능을 활용합니다:

  1. 밀도 + 스파스 하이브리드 검색. 이미지 임베딩과 텍스트 TF-IDF 벡터를 병렬 쿼리로 실행한 다음, 두 결과 집합을 RRF(상호 순위 융합) 재순위를 통해 병합합니다.
  2. 스칼라 필드 필터링. 벡터 비교 전에 카테고리 및 sales_count와 같은 메타데이터 필드를 기준으로 필터링하므로 관련성이 높고 실적이 우수한 제품만 결과에 포함됩니다.
  3. 다중 필드 스키마. 밀집 벡터, 스파스 벡터, 스칼라 메타데이터를 단일 Milvus 컬렉션에 저장하여 전체 검색 로직을 여러 시스템에 흩어져 있지 않고 하나의 쿼리에 유지합니다.

데이터 준비

과거 제품 카탈로그

기존 제품 사진의 이미지/폴더와 해당 메타데이터가 포함된 products.csv 파일이라는 두 가지 자산으로 시작합니다.

images/
├── SKU001.jpg
├── SKU002.jpg
├── ...
└── SKU040.jpg

products.csv fields: product_id, image_path, category, color, style, season, sales_count, description, price

새 제품 데이터

프로모션 이미지를 생성하려는 제품의 경우, new_products/ 폴더와 new_products.csv라는 병렬 구조를 준비합니다.

new_products/
├── NEW001.jpg    # Blue knit cardigan + grey tulle skirt set
├── NEW002.jpg    # Light green floral ruffle maxi dress
├── NEW003.jpg    # Camel turtleneck knit dress
└── NEW004.jpg    # Dark grey ethnic-style cowl neck top dress

new_products.csv fields: new_id, image_path, category, style, season, prompt_hint

1단계: 종속성 설치

!pip install pymilvus openai requests pillow scikit-learn tqdm

2단계: 모듈 및 구성 가져오기

import os, io, base64, csv, time
import requests as req
import numpy as np
from PIL import Image
from tqdm.notebook import tqdm
from sklearn.feature_extraction.text import TfidfVectorizer
from IPython.display import display

from openai import OpenAI from pymilvus import MilvusClient, DataType, AnnSearchRequest, RRFRanker

모든 모델과 경로를 구성합니다:

# -- Config --
OPENROUTER_API_KEY = os.environ.get(
    "OPENROUTER_API_KEY",
    "<YOUR_OPENROUTER_API_KEY>",
)

# Models (all via OpenRouter, no local download needed) EMBED_MODEL = “nvidia/llama-nemotron-embed-vl-1b-v2” # free, image+text → 2048d EMBED_DIM = 2048 LLM_MODEL = “qwen/qwen3.5-397b-a17b” # style analysis IMAGE_GEN_MODEL = “google/gemini-3.1-flash-image-preview” # Nano Banana 2

# Milvus MILVUS_URI = “./milvus_fashion.db” COLLECTION = “fashion_products” TOP_K = 3

# Paths IMAGE_DIR = “./images” NEW_PRODUCT_DIR = “./new_products” PRODUCT_CSV = “./products.csv” NEW_PRODUCT_CSV = “./new_products.csv”

# OpenRouter client (shared for LLM + image gen) llm = OpenAI(api_key=OPENROUTER_API_KEY, base_url=“https://openrouter.ai/api/v1”)

print(“Config loaded. All models via OpenRouter API.”)

유틸리티 함수

이러한 헬퍼 함수는 이미지 인코딩, API 호출 및 응답 구문 분석을 처리합니다:

  • image_to_uri(): API 전송을 위해 PIL 이미지를 base64 데이터 URI로 변환합니다.
  • get_image_embeddings(): OpenRouter 임베딩 API를 통해 이미지를 2048차원 벡터로 일괄 인코딩합니다.
  • GET_TEXT_EMBEDDING(): 텍스트를 동일한 2048차원 벡터 공간으로 인코딩합니다.
  • sparse_to_dict(): 스키피 스파스 행렬 행을 밀버스가 스파스 벡터에 기대하는 {index: value} 형식으로 변환합니다.
  • extract_images(): 나노 바나나 2 API 응답에서 생성된 이미지를 추출합니다.
# -- Utility functions --

def image_to_uri(img, max_size=1024): “""Convert PIL Image to base64 data URI.""” img = img.copy() w, h = img.size if max(w, h) > max_size: r = max_size / max(w, h) img = img.resize((int(w * r), int(h * r)), Image.LANCZOS) buf = io.BytesIO() img.save(buf, format=“JPEG”, quality=85) return f"data:image/jpeg;base64,{base64.b64encode(buf.getvalue()).decode()}"

def get_image_embeddings(images, batch_size=5): “""Encode images via OpenRouter embedding API.""” all_embs = [] for i in tqdm(range(0, len(images), batch_size), desc=“Encoding images”): batch = images[i : i + batch_size] inputs = [ {“content”: [{“type”: “image_url”, “image_url”: {“url”: image_to_uri(img, max_size=512)}}]} for img in batch ] resp = req.post( “https://openrouter.ai/api/v1/embeddings”, headers={“Authorization”: f"Bearer {OPENROUTER_API_KEY}"}, json={“model”: EMBED_MODEL, “input”: inputs}, timeout=120, ) data = resp.json() if “data” not in data: print(f"API error: {data}") continue for item in sorted(data[“data”], key=lambda x: x[“index”]): all_embs.append(item[“embedding”]) time.sleep(0.5) # rate limit friendly return np.array(all_embs, dtype=np.float32)

def get_text_embedding(text): “""Encode text via OpenRouter embedding API.""” resp = req.post( “https://openrouter.ai/api/v1/embeddings”, headers={“Authorization”: f"Bearer {OPENROUTER_API_KEY}"}, json={“model”: EMBED_MODEL, “input”: text}, timeout=60, ) return np.array(resp.json()[“data”][0][“embedding”], dtype=np.float32)

def sparse_to_dict(sparse_row): “""Convert scipy sparse row to Milvus sparse vector format {index: value}.""” coo = sparse_row.tocoo() return {int(i): float(v) for i, v in zip(coo.col, coo.data)}

def extract_images(response): “""Extract generated images from OpenRouter response.""” images = [] raw = response.model_dump() msg = raw[“choices”][0][“message”] # Method 1: images field (OpenRouter extension) if “images” in msg and msg[“images”]: for img_data in msg[“images”]: url = img_data[“image_url”][“url”] b64 = url.split(“,”, 1)[1] images.append(Image.open(io.BytesIO(base64.b64decode(b64)))) # Method 2: inline base64 in content parts if not images and isinstance(msg.get(“content”), list): for part in msg[“content”]: if isinstance(part, dict) and part.get(“type”) == “image_url”: url = part[“image_url”][“url”] if url.startswith(“data:image”): b64 = url.split(“,”, 1)[1] images.append(Image.open(io.BytesIO(base64.b64decode(b64)))) return images

print(“Utility functions ready.”)

3단계: 제품 카탈로그 로드

products.csv를 읽고 해당 제품 이미지를 로드합니다:

with open(PRODUCT_CSV, newline="", encoding="utf-8") as f:
    products = list(csv.DictReader(f))

product_images = [] for p in products: img = Image.open(os.path.join(IMAGE_DIR, p[“image_path”])).convert(“RGB”) product_images.append(img)

print(f"Loaded {len(products)} products.") for i in range(3): p = products[i] print(f"{p[‘product_id’]} | {p[‘category’]} | {p[‘color’]} | {p[‘style’]} | sales: {p[‘sales_count’]}") display(product_images[i].resize((180, int(180 * product_images[i].height / product_images[i].width))))

샘플 출력:

4단계: 임베딩 생성

하이브리드 검색에는 각 제품에 대해 두 가지 유형의 벡터가 필요합니다.

4.1 고밀도 벡터: 이미지 임베딩

nvidia/llama-nemotron-embed-vl-1b-v2 모델은 각 제품 이미지를 2048차원의 고밀도 벡터로 인코딩합니다. 이 모델은 공유 벡터 공간에서 이미지와 텍스트 입력을 모두 지원하므로 이미지 대 이미지 및 텍스트 대 이미지 검색에 동일한 임베딩이 작동합니다.

# Dense embeddings: image → 2048-dim vector via OpenRouter API
dense_vectors = get_image_embeddings(product_images, batch_size=5)
print(f"Dense vectors: {dense_vectors.shape}  (products x {EMBED_DIM}d)")

출력:

Dense vectors: (40, 2048)  (products x 2048d)

4.2 스파스 벡터: TF-IDF 텍스트 임베딩

제품 텍스트 설명은 scikit-learn의 TF-IDF 벡터라이저를 사용해 스파스 벡터로 인코딩됩니다. 이는 밀도가 높은 벡터가 놓칠 수 있는 키워드 수준의 매칭을 포착합니다.

# Sparse embeddings: TF-IDF on product descriptions
descriptions = [p["description"] for p in products]
tfidf = TfidfVectorizer(stop_words="english", max_features=500)
tfidf_matrix = tfidf.fit_transform(descriptions)

sparse_vectors = [sparse_to_dict(tfidf_matrix[i]) for i in range(len(products))] print(f"Sparse vectors: {len(sparse_vectors)} products, vocab size: {len(tfidf.vocabulary_)}") print(f"Sample sparse vector (SKU001): {len(sparse_vectors[0])} non-zero terms")

출력:

Sparse vectors: 40 products, vocab size: 179
Sample sparse vector (SKU001): 11 non-zero terms

왜 두 가지 벡터 유형일까요? 고밀도 벡터와 희소 벡터는 서로를 보완합니다. 고밀도 벡터는 색상 팔레트, 의상 실루엣, 전체적인 스타일 등 시각적 유사성을 포착합니다. 스파스 벡터는 '꽃무늬', '미디', '쉬폰'과 같이 제품 속성을 나타내는 키워드 의미를 포착합니다. 두 가지 방법을 결합하면 어느 한 방법만 사용하는 것보다 훨씬 더 나은 검색 품질을 얻을 수 있습니다.

5단계: 하이브리드 스키마를 사용하여 Milvus 컬렉션 만들기

이 단계에서는 밀도 벡터, 스파스 벡터, 스칼라 메타데이터 필드를 함께 저장하는 단일 Milvus 컬렉션을 만듭니다. 이 통합 스키마는 단일 쿼리에서 하이브리드 검색을 가능하게 합니다.

필드유형목적
dense_vector플로트_벡터(2048d)이미지 임베딩, 코사인 유사도
스파스_벡터SPARSE_FLOAT_VECTORTF-IDF 스파스 벡터, 내적 곱
카테고리VARCHAR필터링용 카테고리 레이블
sales_countINT64필터링할 과거 판매량
색상, 스타일, 시즌VARCHAR추가 메타데이터 레이블
가격FLOAT제품 가격
milvus_client = MilvusClient(uri=MILVUS_URI)

if milvus_client.has_collection(COLLECTION): milvus_client.drop_collection(COLLECTION)

schema = milvus_client.create_schema(auto_id=True, enable_dynamic_field=True) schema.add_field(“id”, DataType.INT64, is_primary=True) schema.add_field(“product_id”, DataType.VARCHAR, max_length=20) schema.add_field(“category”, DataType.VARCHAR, max_length=50) schema.add_field(“color”, DataType.VARCHAR, max_length=50) schema.add_field(“style”, DataType.VARCHAR, max_length=50) schema.add_field(“season”, DataType.VARCHAR, max_length=50) schema.add_field(“sales_count”, DataType.INT64) schema.add_field(“description”, DataType.VARCHAR, max_length=500) schema.add_field(“price”, DataType.FLOAT) schema.add_field(“dense_vector”, DataType.FLOAT_VECTOR, dim=EMBED_DIM) schema.add_field(“sparse_vector”, DataType.SPARSE_FLOAT_VECTOR)

index_params = milvus_client.prepare_index_params() index_params.add_index(field_name=“dense_vector”, index_type=“FLAT”, metric_type=“COSINE”) index_params.add_index(field_name=“sparse_vector”, index_type=“SPARSE_INVERTED_INDEX”, metric_type=“IP”)

milvus_client.create_collection(COLLECTION, schema=schema, index_params=index_params) print(f"Milvus collection '{COLLECTION}' created with hybrid schema.")

제품 데이터를 입력합니다:

# Insert all products
rows = []
for i, p in enumerate(products):
    rows.append({
        "product_id": p["product_id"],
        "category": p["category"],
        "color": p["color"],
        "style": p["style"],
        "season": p["season"],
        "sales_count": int(p["sales_count"]),
        "description": p["description"],
        "price": float(p["price"]),
        "dense_vector": dense_vectors[i].tolist(),
        "sparse_vector": sparse_vectors[i],
    })

milvus_client.insert(COLLECTION, rows) stats = milvus_client.get_collection_stats(COLLECTION) print(f"Inserted {stats[‘row_count’]} products into Milvus.")

출력:

Inserted 40 products into Milvus.

6단계: 하이브리드 검색으로 유사 베스트셀러 찾기

이 단계가 핵심 검색 단계입니다. 각 새 제품에 대해 파이프라인은 세 가지 작업을 동시에 실행합니다:

  1. 밀도 검색: 시각적으로 유사한 이미지가 포함된 제품을 찾습니다.
  2. 희소 검색: TF-IDF를 통해 일치하는 텍스트 키워드를 가진 제품을 찾습니다.
  3. 스칼라 필터링: 동일한 카테고리와 판매 수가 1500을 초과하는 제품으로 결과를 제한합니다.
  4. RRF 재랭크: 상호 순위 융합을 사용하여 밀집 및 희소 결과 목록을 병합합니다.

새 제품을 로드합니다:

# Load new products
with open(NEW_PRODUCT_CSV, newline="", encoding="utf-8") as f:
    new_products = list(csv.DictReader(f))

# Pick the first new product for demo new_prod = new_products[0] new_img = Image.open(os.path.join(NEW_PRODUCT_DIR, new_prod[“image_path”])).convert(“RGB”)

print(f"New product: {new_prod[‘new_id’]}") print(f"Category: {new_prod[‘category’]} | Style: {new_prod[‘style’]} | Season: {new_prod[‘season’]}") print(f"Prompt hint: {new_prod[‘prompt_hint’]}") display(new_img.resize((300, int(300 * new_img.height / new_img.width))))

출력:

새 제품을 인코딩합니다:

# Encode new product
# Dense: image embedding via API
query_dense = get_image_embeddings([new_img], batch_size=1)[0]

# Sparse: TF-IDF from text query query_text = f"{new_prod[‘category’]} {new_prod[‘style’]} {new_prod[‘season’]} {new_prod[‘prompt_hint’]}" query_sparse = sparse_to_dict(tfidf.transform([query_text])[0])

# Scalar filter filter_expr = f’category == "{new_prod[“category”]}" and sales_count > 1500’

print(f"Dense query: {query_dense.shape}") print(f"Sparse query: {len(query_sparse)} non-zero terms") print(f"Filter: {filter_expr}")

출력:

Dense query: (2048,)
Sparse query: 6 non-zero terms
Filter: category == "midi_dress" and sales_count > 1500

하이브리드 검색 실행

핵심 API 호출은 다음과 같습니다:

  • AnnSearchRequest는 고밀도 및 희소 벡터 필드에 대한 별도의 검색 요청을 생성합니다.
  • expr=filter_expr은 각 검색 요청 내에서 스칼라 필터링을 적용합니다.
  • RRFRanker(k=60)는 상호 순위 융합 알고리즘을 사용하여 두 개의 순위가 매겨진 결과 목록을 융합합니다.
  • hybrid_search는 두 요청을 모두 실행하여 병합되고 순위가 재조정된 결과를 반환합니다.
# Hybrid search: dense + sparse + scalar filter + RRF reranking
dense_req = AnnSearchRequest(
    data=[query_dense.tolist()],
    anns_field="dense_vector",
    param={"metric_type": "COSINE"},
    limit=20,
    expr=filter_expr,
)
sparse_req = AnnSearchRequest(
    data=[query_sparse],
    anns_field="sparse_vector",
    param={"metric_type": "IP"},
    limit=20,
    expr=filter_expr,
)

results = milvus_client.hybrid_search( collection_name=COLLECTION, reqs=[dense_req, sparse_req], ranker=RRFRanker(k=60), limit=TOP_K, output_fields=[“product_id”, “category”, “color”, “style”, “season”, “sales_count”, “description”, “price”], )

# Display retrieved bestsellers retrieved_products = [] retrieved_images = [] print(f"Top-{TOP_K} similar bestsellers:\n") for hit in results[0]: entity = hit[“entity”] pid = entity[“product_id”] img = Image.open(os.path.join(IMAGE_DIR, f"{pid}.jpg")).convert(“RGB”) retrieved_products.append(entity) retrieved_images.append(img) print(f"{pid} | {entity[‘category’]} | {entity[‘color’]} | {entity[‘style’]} " f"| sales: {entity[‘sales_count’]} | ${entity[‘price’]:.1f} | score: {hit[‘distance’]:.4f}") print(f" {entity[‘description’]}") display(img.resize((250, int(250 * img.height / img.width)))) print()

결과: 가장 유사한 베스트셀러 상위 3개가 융합된 점수에 따라 순위가 매겨집니다.

7단계: Qwen 3.5로 베스트셀러 스타일 분석하기

검색된 베스트셀러 이미지를 Qwen 3.5에 입력하고 장면 구성, 조명 설정, 모델 포즈, 전반적인 분위기 등 공유된 시각적 DNA를 추출하도록 요청합니다. 이 분석을 통해 Nano Banana 2에 전달할 준비가 된 단일 세대 프롬프트를 다시 얻습니다.

content = [
    {"type": "image_url", "image_url": {"url": image_to_uri(img)}}
    for img in retrieved_images
]
content.append({
    "type": "text",
    "text": (
        "These are our top-selling fashion product photos.\n\n"
        "Analyze their common visual style in these dimensions:\n"
        "1. Scene / background setting\n"
        "2. Lighting and color tone\n"
        "3. Model pose and framing\n"
        "4. Overall mood and aesthetic\n\n"
        "Then, based on this analysis, write ONE concise image generation prompt "
        "(under 100 words) that captures this style. The prompt should describe "
        "a scene for a model wearing a new clothing item. "
        "Output ONLY the prompt, nothing else."
    ),
})

response = llm.chat.completions.create( model=LLM_MODEL, messages=[{“role”: “user”, “content”: content}], max_tokens=512, temperature=0.7, ) style_prompt = response.choices[0].message.content.strip() print(“Style prompt from Qwen3.5:\n”) print(style_prompt)

샘플 출력:

Style prompt from Qwen3.5:

Professional full-body fashion photograph of a model wearing a stylish new dress. Bright, soft high-key lighting that illuminates the subject evenly. Clean, uncluttered background, either stark white or a softly blurred bright outdoor setting. The model stands in a relaxed, natural pose to showcase the garment’s silhouette and drape. Sharp focus, vibrant colors, fresh and elegant commercial aesthetic.

8단계: 나노 바나나 2로 프로모션 이미지 생성하기

신제품의 평면 사진, 베스트셀러 상위권 이미지, 이전 단계에서 추출한 스타일 프롬프트의 세 가지 입력을 Nano Banana 2에 전달합니다. 모델은 이를 합성하여 새로운 의류와 검증된 시각적 스타일을 결합한 홍보용 사진을 만듭니다.

gen_prompt = (
    f"I have a new clothing product (Image 1: flat-lay photo) and a reference "
    f"promotional photo from our bestselling catalog (Image 2).\n\n"
    f"Generate a professional e-commerce promotional photograph of a female model "
    f"wearing the clothing from Image 1.\n\n"
    f"Style guidance: {style_prompt}\n\n"
    f"Scene hint: {new_prod['prompt_hint']}\n\n"
    f"Requirements:\n"
    f"- Full body shot, photorealistic, high quality\n"
    f"- The clothing should match Image 1 exactly\n"
    f"- The photo style and mood should match Image 2"
)

gen_content = [ {“type”: “image_url”, “image_url”: {“url”: image_to_uri(new_img)}}, {“type”: “image_url”, “image_url”: {“url”: image_to_uri(retrieved_images[0])}}, {“type”: “text”, “text”: gen_prompt}, ]

print(“Generating promotional photo with Nano Banana 2…”) gen_response = llm.chat.completions.create( model=IMAGE_GEN_MODEL, messages=[{“role”: “user”, “content”: gen_content}], extra_body={ “modalities”: [“text”, “image”], “image_config”: {“aspect_ratio”: “3:4”, “image_size”: “2K”}, }, ) print(“Done!”)

나노 바나나 2 API 호출의 주요 매개변수입니다:

  • 모달리티 ["text", "image"]: 응답에 이미지가 포함되어야 함을 선언합니다.
  • image_config.aspect_ratio: 출력 종횡비를 제어합니다(인물/패션 사진에는 3:4가 적합).
  • image_config.image_size: 해상도를 설정합니다. 나노 바나나 2는 512픽셀부터 4K까지 지원합니다.

생성된 이미지를 추출합니다:

generated_images = extract_images(gen_response)

text_content = gen_response.choices[0].message.content if text_content: print(f"Model response: {text_content[:300]}\n")

if generated_images: for i, img in enumerate(generated_images): print(f"— Generated promo photo {i+1} —") display(img) img.save(f"promo_{new_prod[‘new_id’]}{i+1}.png") print(f"Saved: promo{new_prod[‘new_id’]}_{i+1}.png") else: print(“No image generated. Raw response:”) print(gen_response.model_dump())

출력:

9단계: 나란히 비교

조명이 부드럽고 균일하며 모델의 포즈가 자연스러워 보이고 분위기가 베스트셀러 레퍼런스와 일치하는 등 전반적으로 만족스러운 결과물을 얻을 수 있습니다.

부족한 부분은 의상 블렌딩입니다. 카디건은 모델이 입은 것이 아니라 붙인 것처럼 보이고, 흰색 네크라인 라벨이 번져 보입니다. 싱글 패스 세대는 이런 종류의 세밀한 의류와 신체 통합에 어려움을 겪기 때문에 요약에서 해결 방법을 다룹니다.

10단계: 모든 신제품에 대한 배치 생성

전체 파이프라인을 단일 함수로 묶어 나머지 신제품에 대해 실행합니다. 여기서는 간결성을 위해 배치 코드를 생략하므로 전체 구현이 필요한 경우 문의하세요.

배치 결과에서 두 가지가 눈에 띕니다. 여름용 원피스와 겨울용 니트는 계절, 사용 사례, 액세서리에 따라 각기 다른 장면 설명을 받는 등 Qwen 3.5의 스타일 프롬프트가 제품별로 의미 있게 조정된다는 점입니다. 나노 바나나 2에서 얻은 이미지는 조명, 질감, 구도 면에서 실제 스튜디오 사진에 뒤지지 않습니다.

결론

이 글에서는 나노 바나나 2가 이커머스 이미지 생성에 제공하는 기능을 살펴보고, 실제 제작 작업에서 기존 나노 바나나 및 프로와 비교했으며, Milvus, Qwen 3.5 및 나노 바나나 2로 베스트셀러 이미지 파이프라인을 구축하는 방법을 살펴봤습니다.

이 파이프라인에는 네 가지 실질적인 이점이 있습니다:

  • 통제된 비용, 예측 가능한 예산. 임베딩 모델(Llama Nemotron Embed VL 1B v2)은 OpenRouter에서 무료로 제공됩니다. 나노 바나나 2는 이미지당 비용이 프로의 약 절반 수준이며, 기본 멀티포맷 출력을 통해 유효 비용을 두 배 또는 세 배로 늘리곤 했던 재작업 주기를 없앴습니다. 시즌당 수천 개의 SKU를 관리하는 이커머스 팀의 경우, 이러한 예측 가능성은 예산 초과 없이 카탈로그에 맞춰 이미지 제작을 확장할 수 있다는 것을 의미합니다.
  • 엔드투엔드 자동화, 더 빠른 리스팅 시간. 평면 제품 사진에서 완성된 홍보 이미지로 이어지는 흐름은 수동 개입 없이 실행됩니다. 카탈로그 회전율이 가장 높은 성수기에는 신제품을 창고 사진에서 마켓플레이스에 등록할 수 있는 리스팅 이미지로 만드는 데 며칠이 걸리지 않고 몇 분이면 충분합니다.
  • 로컬 GPU가 필요하지 않아 진입 장벽이 낮습니다. 모든 모델은 OpenRouter API를 통해 실행됩니다. ML 인프라나 전담 엔지니어링 인력이 없는 팀도 노트북으로 이 파이프라인을 실행할 수 있습니다. 프로비저닝할 것도, 유지 관리할 것도, 하드웨어에 대한 초기 투자도 없습니다.
  • 더 높은 검색 정확도, 더 강력한 브랜드 일관성. Milvus는 단일 쿼리에서 밀도, 스파스 및 스칼라 필터링을 결합하여 제품 매칭에 대한 단일 벡터 접근 방식보다 일관되게 뛰어난 성능을 발휘합니다. 이는 실제로 생성된 이미지가 기존 베스트셀러에서 이미 전환율이 입증된 조명, 구도, 스타일링 등 브랜드의 확립된 시각적 언어를 보다 안정적으로 계승한다는 것을 의미합니다. 결과물은 일반적인 AI 스톡 아트와 달리 스토어에 어울리는 것처럼 보입니다.

미리 알아두어야 할 제한 사항도 있습니다:

  • 의복과 바디 블렌딩. 단일 패스 생성은 의류를 착용한 것이 아니라 합성한 것처럼 보이게 만들 수 있습니다. 작은 액세서리와 같은 미세한 디테일이 흐릿하게 표현되기도 합니다. 해결 방법: 단계적으로 생성(배경 먼저, 모델 포즈, 합성)합니다. 이 멀티패스 방식은 각 단계의 범위를 좁히고 블렌딩 품질을 크게 향상시킵니다.
  • 가장자리 케이스의 디테일 충실도. 액세서리, 패턴 및 텍스트가 많은 레이아웃은 선명도가 떨어질 수 있습니다. 해결 방법: 생성 프롬프트에 명시적인 제약 조건을 추가합니다("옷이 몸에 자연스럽게 맞음, 라벨이 노출되지 않음, 추가 요소 없음, 제품 디테일이 선명함"). 특정 제품의 품질이 여전히 부족하다면 최종적으로 나노 바나나 프로로 전환하세요.

Milvus는 하이브리드 검색 단계를 지원하는 오픈 소스 벡터 데이터베이스로, 직접 제품 사진을 찾아보거나 교체해보고 싶다면 퀵스타트에서 10분 정도 소요됩니다. Discord와 Slack에 꽤 활발한 커뮤니티가 있으며, 이를 통해 사람들이 무엇을 만들어내는지 보고 싶습니다. 그리고 다른 제품군이나 더 큰 카탈로그에 대해 나노 바나나 2를 실행하게 되면 그 결과를 공유해 주세요! 여러분의 의견을 듣고 싶습니다.

계속 읽기

    Try Managed Milvus for Free

    Zilliz Cloud is hassle-free, powered by Milvus and 10x faster.

    Get Started

    Like the article? Spread the word

    계속 읽기