Nano Banana 2 + Milvus + Qwen 3.5でEコマースのためのベストセラーから画像へのパイプラインを構築する
eコマース販売者向けのAIツールを構築しているのであれば、このような要望を何度も耳にしたことがあるだろう:「新製品があります。ベストセラーに載るようなプロモーション画像をください。カメラマンもスタジオも使わず、安く作ってくれ」。
一言で言えば、これが問題なのだ。売り手は、平積みの写真と、すでにコンバージョンしているベストセラーのカタログを持っている。この2つをAIで高速かつ大規模に橋渡ししたいのだ。
Googleが2026年2月26日にNano Banana 2(Gemini 3.1 Flash Image)をリリースしたとき、私たちは同じ日にそれをテストし、既存のmilvusベースの検索パイプラインに統合した。その結果、画像生成の総コストは以前のおよそ3分の1に下がり、スループットは2倍になった。画像単価の引き下げ(Nano Banana Proより約50%安い)もその一因ですが、それ以上に大きな節約は、手直しサイクルを完全になくしたことによるものです。
本記事では、Nano Banana 2がeコマースでうまくいっている点、まだ不十分な点、そしてパイプライン全体のハンズオンチュートリアルを紹介する:Milvusハイブリッド検索による視覚的に類似したベストセラーの検索、Qwen3.5によるスタイル分析、そしてNano Banana 2による最終生成です。
Nano Banana 2の新機能
ナノ・バナナ2(Gemini 3.1 Flash Image)は2026年2月26日に発売されました。ナノ・バナナ・プロのほとんどの機能がフラッシュ・アーキテクチャに搭載され、より低価格でより高速な生成が可能になりました。主なアップグレードは以下の通り:
- プロレベルの品質をFlashのスピードで。Nano Banana 2は、これまでPro専用だった世界クラスの知識、推論、ビジュアルの忠実性を、Flashのレイテンシーとスループットで実現します。
- 512pxから4K出力。4つの解像度ティア(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枚の参照画像。1つのワークフローで、最大5つのキャラクタのキャラクタ類似性と最大14のオブジェクトのオブジェクト忠実性を維持。
- テキストレンダリングの向上。1回の生成で翻訳とローカリゼーションをサポートし、複数の言語で読みやすく正確な画像内テキストを生成します。
- 画像検索の基盤。Google検索からリアルタイムのウェブデータと画像を取得し、実世界の被写体のより正確な描写を生成します。
- ~画像1枚あたり~50%低価格。1K解像度で:0.134。
ナノ・バナノの楽しい使用例2:シンプルなGoogleマップのスクリーンショットに基づく位置認識パノラマの生成
Googleマップのスクリーンショットとスタイルプロンプトが与えられると、モデルは地理的なコンテキストを認識し、正しい空間関係を保持したパノラマを生成します。ストックフォトを調達することなく、地域をターゲットにした広告クリエイティブ(パリのカフェの背景や東京の街並み)を制作するのに便利です。
全機能については、Googleの発表ブログと 開発者向けドキュメントを参照のこと。
ナノ・バナナのアップデートはEコマースに何をもたらすか?
Eコマースは、最も画像を多用する業界のひとつです。商品リスト、マーケットプレイス広告、ソーシャル・クリエイティブ、バナー・キャンペーン、ローカライズされた店頭......あらゆるチャネルで、それぞれ独自の仕様のビジュアル・アセットが絶え間なく要求される。
EコマースにおけるAI画像生成の主な要件は、以下のとおりです:
- コストを低く抑える- 画像1枚あたりのコストは、カタログ規模で機能するものでなければならない。
- 実績のあるベストセラーのルックに合わせる- 新しい画像は、すでにコンバージョンしているリストのビジュアルスタイルに合わせる。
- 侵害を避ける- 競合他社のクリエイティブをコピーしたり、保護された資産を再利用したりしない。
その上で、国境を越えたセラーには以下が必要です:
- マルチプラットフォームフォーマットのサポート- マーケットプレイス、広告、店頭で異なるアスペクト比と仕様。
- 多言語テキストレンダリング- 複数の言語に対応したクリーンで正確な画像内テキスト。
Nano Banana 2は、全ての項目に対応しています。以下のセクションでは、各アップグレードが実際にどのような意味を持つのか、つまり、Eコマースのペインポイントを直接解決する部分、不足する部分、実際のコストへの影響について説明します。
出力生成コストを最大60%削減
1K解像度の場合、Nano Banana 2のあたりのコストは、0..134となり、実に50%の削減となります。しかし、画像単価は話の半分に過ぎない。かつてユーザーの予算を奪っていたのは手直しだった。各マーケットプレイスは独自の画像仕様(Amazonでは1:1、Shopifyの店頭では3:4、バナー広告ではウルトラワイド)を強制しており、各バリアントを生産することは、独自の失敗モードを持つ個別の世代パスを意味した。
Nano Banana 2は、これらの余分なパスを1つにまとめました。
4つのネイティブ解像度
512ピクセル(0.045ドル)
1K ($0.067)
2K ($0.101)
4K ($0.151).
512pxの階層は新しく、Nano Banana 2独自のものです。ユーザーは、反復のために低コストの512pxドラフトを生成し、別のアップスケーリングステップなしで2Kまたは4Kで最終アセットを出力できるようになりました。
合計14のアスペクト比をサポート。以下はその例です:
4:1
1:4
8:1
1:8
これらの新しいウルトラワイドとウルトラタールの比率は、既存のセットに加わります。一世代のセッションで、次のようなさまざまなフォーマットを作成できます:アマゾンのメイン画像(1:1)、ストアフロントのヒーロー(3:4)、バナー広告(ウルトラワイドまたはその他の比率)。
これらの4つのアスペクト比では、トリミング、パディング、再プロンプトは必要ありません。残りの10種類のアスペクト比はフルセットに含まれており、異なるプラットフォーム間でより柔軟な処理が可能です。
画像1枚あたり~50%の節約だけで、請求額は半分にしかなりません。解像度とアスペクト比にまたがる手直しをなくしたことで、総コストは以前のおよそ3分の1になりました。
ベストセラースタイルで最大14枚のリファレンス画像をサポート
Nano Banana 2のアップデートの中で、マルチ・リファレンス・ブレンドはMilvusのパイプラインに最も大きな影響を与えました。Nano Banana 2は、1つのリクエストで最大14の参照画像を受け入れ、以下のことを維持します:
- 最大5文字までの文字類似性
- 最大14個のオブジェクトの忠実度
実際には、Milvusから複数のベストセラー画像を取得し、それらを参照画像として渡すと、生成された画像はそれらのシーン構成、照明、ポージング、小道具の配置を継承しました。これらのパターンを手作業で再構築するための迅速なエンジニアリングは必要なかった。
以前のモデルでは、1つか2つのリファレンスしかサポートしていなかったため、ユーザーは真似するベストセラーを1つ選ぶしかなかった。14のリファレンス・スロットがあれば、複数のトップ・パフォーマンスのリストからの特徴をブレンドし、モデルに複合的なスタイルを合成させることができる。これが、以下のチュートリアルの検索ベースのパイプラインを可能にする能力である。
従来の制作コストやロジスティックスなしで、プレミアムで商用に適したビジュアルを制作
一貫性のある信頼性の高い画像生成のためには、すべての要件を単一のプロンプトにダンプすることは避けてください。より信頼性の高いアプローチは、段階的に作業することです。まず背景を生成し、次にモデルを別々に生成し、最後にそれらを合成します。
ナノ・バナナの3つのモデルすべてで、同じプロンプトを使って背景生成のテストを行いました:東方明珠タワーが見える、窓から見た4:1の超広角の雨の日の上海のスカイラインです。このプロンプトでは、構図、建築のディテール、フォトリアリズムが一度にテストされます。
オリジナルNano Banana vs. Nano Banana Pro vs. Nano Banana 2
- オリジナルのナノバナナ。自然な雨のテクスチャで、水滴の分布もよく再現されているが、建物のディテールが滑らかになりすぎている。オリエンタルパールタワーはほとんど認識できず、解像度は制作要件に満たない。
- ナノ・バナナPro。映画のような雰囲気:暖かい室内照明が冷たい雨に説得力を与えている。しかし、窓枠が完全に省略され、画像の奥行き感が平坦になった。ヒーローではなく、脇役として使える。
- ナノバナナ2。シーン全体をレンダリング。手前の窓枠が奥行きを生み出している。東方明珠塔のディテールがはっきりした。黄浦江に浮かぶ船。レイヤーを重ねたライティングは、室内の暖かさと外部の曇りを区別した。雨と水垢のテクスチャは写真に近く、4:1の超広角比率は、左の窓の端にわずかな歪みがあるだけで、正しい遠近感を保っていた。
商品撮影におけるほとんどの背景生成タスクにおいて、ナノ・バナナ2の出力は後処理なしで使用できることが分かった。
言語を超えて画像内テキストをきれいにレンダリング
値札、宣伝バナー、多言語コピーはEコマース画像では避けられないもので、これまでAI生成の限界点でした。ナノ・バナナ2では、これらの処理が大幅に改善され、多言語にわたる画像内テキストのレンダリングをサポートし、翻訳とローカリゼーションを1つの世代で行うことができます。
標準的なテキストレンダリング。私たちのテストでは、価格ラベル、短いマーケティングタグライン、バイリンガルの商品説明など、私たちが試したすべてのeコマースフォーマットでテキスト出力にエラーはありませんでした。
手書きの継続。eコマースでは、値札や名入れカードのような手書きの要素が必要とされることが多いため、既存の手書きスタイルにマッチさせ、それを拡張することができるかどうかをテストしました。3つのモデルの結果
- オリジナルのナノ・バナナ。繰り返される連番、誤解された構造。
- ナノ・バナナ・プロ。レイアウトは正しいが、フォントスタイルの再現性が低い。
- ナノ・バナナ2。エラーゼロ。ストロークの太さと字形がソースと見分けがつかないほど忠実に一致。
ただし、Googleのドキュメントによると、Nano Banana 2は「正確なスペルや画像の細かいディテールに苦労することがある」とのこと。私たちがテストした結果は、どのフォーマットでもきれいでしたが、どのような制作ワークフローにも、公開前のテキスト検証ステップを含めるべきです。
ステップバイステップのチュートリアルMilvus、Qwen 3.5、Nano Banana 2を使ったベストセラーから画像へのパイプラインの構築
始める前にアーキテクチャとモデルのセットアップ
単一プロンプト生成のランダム性を避けるため、プロセスを制御可能な3つの段階に分けます:Milvusハイブリッド検索ですでに機能しているものを取得し、Qwen 3.5でそれが機能している理由を分析し、Nano Banana 2でそれらの制約を組み込んだ最終画像を生成します。
各ツールを使ったことがない方のために、各ツールについて簡単に説明します:
- Milvus:最も広く採用されているオープンソースのベクターデータベース。商品カタログをベクトルとして保存し、ハイブリッド検索(密+疎+スカラーフィルタ)を実行して、新商品に最も似たベストセラー画像を見つける。
- Qwen 3.5: 人気のマルチモーダルLLM。検索されたベストセラー画像を取得し、その背後にある視覚的パターン(シーンレイアウト、照明、ポーズ、ムード)を構造化されたスタイルプロンプトに抽出する。
- ナノバナナ2:Googleの画像生成モデル(Gemini 3.1 Flash Image)。新商品のフラットレイ、ベストセラーのリファレンス、Qwen 3.5のスタイルプロンプトの3つを入力。最終的なプロモーション写真を出力。
このアーキテクチャの背後にあるロジックは、1つの観察から始まります:あらゆるeコマースカタログで最も価値のあるビジュアル資産は、すでに変換されたベストセラー画像のライブラリです。これらの写真のポーズ、構図、ライティングは、実際の広告費を通して洗練されたものだ。これらのパターンを直接検索することは、プロンプトを書きながらリバースエンジニアリングするよりも桁違いに速く、その検索ステップこそがベクトルデータベースが扱うものなのだ。
これが完全なフローだ。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の3つの機能を利用して、検索ステージを機能させている:
- 密+疎ハイブリッド検索。画像埋め込みとテキストTF-IDFベクトルを並列クエリとして実行し、RRF(Reciprocal Rank Fusion)リランキングで2つの結果セットをマージする。
- スカラーフィールドフィルタリング。ベクトル比較の前にcategoryやsales_countのようなメタデータフィールドでフィルタリングすることで、関連性の高い、パフォーマンスの高い商品のみを結果に含めます。
- マルチフィールドスキーマ。密なベクトル、疎なベクトル、スカラーメタデータを1つのMilvusコレクションに格納することで、検索ロジック全体を複数のシステムに分散させることなく、1つのクエリに保持します。
データ準備
過去の製品カタログ
既存の商品写真のimages/フォルダとメタデータを含む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():image_to_uri():PIL画像をAPI転送用のbase64データURIに変換します。
- get_image_embeddings():OpenRouter Embedding API を使って画像を 2048 次元ベクトルに一括エンコードします。
- get_text_embedding():テキストを同じ 2048 次元ベクトル空間にエンコードします。
- sparse_to_dict():scipyの疎行列の行をMilvusが期待する疎ベクトル用の{index: value}フォーマットに変換します。
- extract_images():Nano Banana 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:エンベッディングの生成
ハイブリッド検索では、各商品に対して2種類のベクトルが必要です。
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コレクションを作成します。この統一されたスキーマにより、単一のクエリでハイブリッド検索が可能になります。
| フィールド | タイプ | 目的 |
|---|---|---|
| 密ベクトル | FLOAT_VECTOR (2048d) | 画像埋め込み,COSINE類似度 |
| 疎なベクトル | sparse_float_vector | TF-IDF 疎なベクトル,内積 |
| カテゴリ | VARCHAR | フィルタリング用のカテゴリーラベル |
| 売上数 | INT64 | フィルタリングのための過去の販売数 |
| カラー、スタイル、シーズン | 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:類似ベストセラーを見つけるハイブリッド検索
これが核となる検索ステップである。新しい商品ごとに、パイプラインは3つの処理を同時に実行する:
- 密検索:視覚的に類似した画像埋め込みを持つ商品を見つける。
- スパース検索:TF-IDFを介してテキストキーワードが一致する商品を見つける。
- スカラーフィルタリング: 同じカテゴリで、sales_count > 1500の商品に結果を制限する。
- RRF reranking: Reciprocal Rank Fusionを使用して、密と疎の結果リストをマージします。
新しい商品をロードする:
# 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) は、Reciprocal Rank Fusion アルゴリズムを使用して、2つのランク付けされた結果リストを融合します。
- 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を抽出するよう依頼する。その分析から、ナノ・バナナ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によるプロモーション画像の生成
ナノ・バナナ2に3つのインプットを渡します:新製品のフラットレイフォト、トップランクのベストセラー画像、そして前のステップで抽出したスタイルプロンプトです。モデルはこれらを合成し、新商品と実績のあるビジュアルスタイルを組み合わせたプロモーション写真を作成します。
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!”)
Nano Banana 2 APIコールの主なパラメータ:
- モダリティ:modalities: ["text", "image"]: レスポンスに画像を含めることを宣言する。
- image_config.aspect_ratio: 出力のアスペクト比をコントロールします(ポートレート/ファッションショットには3:4が効果的です)。
- image_config.image_size: 解像度を設定します。Nano Banana 2は512pxから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:すべての新商品のバッチ生成
パイプライン全体を1つの関数にまとめ、残りの新製品に対して実行する。バッチコードは簡潔にするためここでは省略する。
バッチ結果では、2つの点が際立っています。Qwen 3.5から得られるスタイル プロンプトは、商品ごとに有意義に調整されます。サマードレスとウィンターニットでは、季節、ユースケース、アクセサリーに合わせた純粋に異なるシーン説明が得られます。ナノ・バナナ2から得られる画像は、ライティング、テクスチャー、構図において、実際のスタジオ写真に匹敵する。
まとめ
この記事では、ナノ・バナナ2がEコマースの画像生成に何をもたらすかを取り上げ、実際の制作タスクにおいてオリジナルのナノ・バナナおよびProと比較し、Milvus、Qwen 3.5、ナノ・バナナ2を用いてベストセラーから画像へのパイプラインを構築する方法を説明した。
このパイプラインには4つの実用的な利点がある:
- コントロールされたコスト、予測可能な予算。エンベッディング・モデル(Llama Nemotron Embed VL 1B v2)は、OpenRouter上で無料。Nano Banana 2は、Proの約半分の画像単価で動作し、ネイティブのマルチフォーマット出力により、効果的な請求書を2倍または3倍にしていた手直しサイクルがなくなります。シーズンごとに何千ものSKUを管理するEコマースチームにとって、この予測可能性は、予算オーバーになることなく、カタログに合わせて画像制作の規模を拡大できることを意味します。
- エンド・ツー・エンドの自動化で、掲載までの時間を短縮。平置きされた商品写真から完成した販促用画像までのフローは、手作業なしで実行されます。新商品は、倉庫の写真から数日ではなく数分で市場に出せる画像に仕上げることができ、これはカタログの回転率が最も高くなる繁忙期に最も重要です。
- ローカルGPUが不要で、参入障壁が低い。すべてのモデルはOpenRouter APIを通じて実行されます。MLインフラを持たず、専任のエンジニアリング人員もいないチームでも、ラップトップからこのパイプラインを実行できる。プロビジョニングも、メンテナンスも、ハードウェアへの先行投資も必要ありません。
- より高い検索精度、より強いブランド一貫性。Milvusは1つのクエリでデンス、スパース、スカラーフィルタリングを組み合わせ、商品マッチングにおいてシングルベクターアプローチを常に凌駕します。つまり、生成された画像は、ブランドの確立されたビジュアル言語、つまり既存のベストセラーがすでに証明した照明、構図、スタイリングをより確実に継承しています。出力された画像は、一般的なAIのストックアートのようではなく、御社の店舗にあるかのように見えます。
また、制限もあります:
- 衣服と体のブレンド。シングルパス生成では、衣服が着用ではなく合成されたように見えることがあります。小さなアクセサリーのような細かいディテールがぼやけることがあります。回避策:段階的に生成します(最初に背景、次にモデルのポーズ、次に合成)。このマルチパスアプローチにより、各ステップの範囲が狭くなり、ブレンド品質が大幅に向上します。
- エッジケースのディテール忠実度。アクセサリー、パターン、テキストの多いレイアウトでは、シャープネスが失われることがあります。回避策:生成プロンプトに明示的な制約を追加します(「服が体に自然にフィットする、ラベルが露出していない、余分な要素がない、製品の詳細がシャープである」)。それでも特定の製品で品質が落ちる場合は、最終的にNano Banana Proに切り替えてください。
Milvusは、ハイブリッド検索ステップを駆動するオープンソースのベクターデータベースであり、もしあなたがあちこち調べたり、自分の商品写真を入れ替えたりしてみたいなら、、クイックスタートは10分ほどで完了する。DiscordとSlackでかなり活発なコミュニティがあるので、これでどんなものを作るかぜひ見てみたい。また、Nano Banana 2を異なる製品群やより大きなカタログに対して実行することになったら、その結果を共有してください!その結果をぜひお聞かせください。
続きを読む
Try Managed Milvus for Free
Zilliz Cloud is hassle-free, powered by Milvus and 10x faster.
Get StartedLike the article? Spread the word



