全部产品
Search
文档中心

向量检索服务 Milvus 版:通过阿里云Milvus构建短剧素材生产与多模态检索流水线

更新时间:Aug 13, 2026

本文介绍如何用阿里云 Milvus 的四个 AI Function 串联一条「生成 → 检索」流水线:从分镜参考图生成关键帧、由关键帧生成运动镜头、为素材自动生成检索描述,再把图片与视频写入同一个多模态 Collection,实现以文搜图、以文搜视频与以图搜视频。

方案概述

AIGC 短剧、竖屏微短剧、品牌短视频的生产节奏很快:一个团队一周要产出几十条甚至上百条成片,每条成片背后又有大量关键帧、分镜与候选素材需要生成和筛选。典型生产链路可以拆成四段:

  1. 分镜文案:先写出一段段分镜脚本,例如「雨夜,女主撑伞站在便利店门口,暖色灯光打在脸上」。

  2. 关键帧生图:把分镜文案转成关键帧图片,作为镜头的视觉锚点,也可在已有素材上做背景替换或风格调整。

  3. 图生视频:把选定的关键帧作为首帧,生成一段运动镜头。

  4. 素材沉淀与检索复用:为每天产生的图片与视频自动生成描述并转成向量入库,之后就能以文搜图、以图搜视频、以文搜视频,让素材从一次性消耗品变成可复用资产。

如果不借助统一的向量数据库平台,自己从零对接各家生成模型再拼一套检索系统,通常会遇到以下问题:生图与生视频模型分散在不同服务、鉴权与返回格式各异;视频生成是长耗时异步任务,需要自己管理轮询、终态判定与超时兜底;图片、视频、描述文本与向量分散在四处,检索时要跨系统拼装;跨模态检索需要把文本、图片、视频映射到同一向量空间,缺少语义检索时只能靠文件名和记忆找素材。

阿里云 Milvus 用四个 AI Function 把这条链路串起来,分为「生成」与「检索」两个阶段、五个步骤:

Function

作用

本文中的用途

AI_IMAGE_EDIT

图像生成与编辑,支持单图编辑和多参考图融合。

把分镜参考图按文案转成关键帧图片。

AI_VIDEO_EDIT

图生视频与文生视频,异步任务。

以关键帧为首帧生成运动镜头。

AI_MULTI_MODAL_GENERATE

多模态内容理解,为图片与视频生成标题、描述、标签。

为素材生成用于检索的客观描述。

AI_EMBEDDING

写入即向量化,把文本、图片、视频映射到同一向量空间。

素材入库并支持跨模态检索。

这条链路的关键在三点:生成即入库,关键帧与运动镜头连同描述、标签一起落库,每个生成物都带着结构化信息和向量;同一向量空间,图片和视频共享同一个多模态模型与同一向量字段,才能做以文搜图、以图搜视频这类跨模态检索;写入即向量化、查询即推理,应用侧无需自己调用 embedding 模型。

前提条件

  • 已创建 Milvus 2.6 版本实例。AI Function 依赖 2.6 版本内核,创建后无需单独绑定模型服务。

  • 如需从公网访问实例,已在实例详情页的 安全配置 页签开启 公网访问 并将客户端出口 IP 加入公网访问白名单。

  • 已安装 pymilvus。本文示例基于 pymilvus 3.0.0 验证。

说明

RESTful 接口与 gRPC 共用 19530 端口,调用时必须在地址中显式带上端口,例如 http://c-xxx.milvus.aliyuncs.com:19530;若省略端口将默认访问 80 端口并导致连接超时。

操作步骤

准备公共代码

以下代码包含连接配置与 REST 调用工具,供步骤 1~3 共用。请将 MILVUS_ENDPOINT 与 MILVUS_TOKEN 替换为实际实例信息。

from __future__ import annotations

import json
import time
from typing import Any
from urllib.error import HTTPError
from urllib.request import Request, urlopen

from pymilvus import DataType, Function, FunctionType, MilvusClient

# ==== 全局配置(REST 与 gRPC 同在 19530 端口)====
MILVUS_ENDPOINT = "c-xxx.milvus.aliyuncs.com:19530"
MILVUS_TOKEN = "root:xxx"
MILVUS_REST_BASE_URL = f"http://{MILVUS_ENDPOINT}"
MILVUS_URI = MILVUS_REST_BASE_URL


def post_json(path: str, body: dict[str, Any], timeout: int = 200) -> tuple[int, dict[str, Any]]:
    """步骤 1~3 共用的 REST 调用工具。"""
    request = Request(
        f"{MILVUS_REST_BASE_URL.rstrip('/')}{path}",
        data=json.dumps(body, ensure_ascii=False).encode("utf-8"),
        headers={"Authorization": f"Bearer {MILVUS_TOKEN}", "Content-Type": "application/json"},
        method="POST",
    )
    try:
        with urlopen(request, timeout=timeout) as response:
            return response.status, json.loads(response.read().decode("utf-8"))
    except HTTPError as exc:
        return exc.code, json.loads(exc.read().decode("utf-8"))

步骤一:关键帧生图

拿一张分镜参考图,按分镜文案做背景替换或风格调整,产出一张关键帧图片,作为后续图生视频的首帧。

项

内容

Function / 模型

AI_IMAGE_EDIT / wan2.7-image

输入

参考图 URL + 分镜文案

输出

关键帧图片 URL

调用方式

同步返回

# ========== 步骤 1:关键帧生图 AI_IMAGE_EDIT (wan2.7-image) ==========
IMAGE_URL = "https://dashscope.oss-cn-beijing.aliyuncs.com/images/dog_and_girl.jpeg"
PROMPT = (
    "雨夜,一位女子撑着伞站在便利店门口,暖色的店内灯光打在她脸上,"
    "电影级构图,保留画面主体。"
)
status, data = post_json(
    "/v2/vectordb/ai/image_edit",
    {
        "model_name": "wan2.7-image",
        "texts": [IMAGE_URL],
        "params": {"prompt": PROMPT, "n": 1, "size": "1024*1024",
                   "watermark": False, "timeout_sec": 180},
    },
)
assert status == 200 and data.get("code") == 0, data
keyframe_url = data["data"]["output"]["outputs"][0]
print(f"关键帧图片 URL:{keyframe_url}")
说明

单图输入用 texts 传参考图 URL,编辑指令写在 params.prompt 中。n 表示生成数量,本例为 1。

步骤二:图生视频

把关键帧作为首帧生成一段运动镜头。视频生成是长耗时异步任务,需要先创建任务拿到 task_id,再轮询查询任务状态。

项

内容

Function / 模型

AI_VIDEO_EDIT / happyhorse-1.1-i2v

输入

首帧图 + 运镜文案

输出

运动镜头 video_url

调用方式

异步:/v2/vectordb/ai/video_edit 创建,/v2/vectordb/ai/tasks/describe 轮询

# ========== 步骤 2:图生视频 AI_VIDEO_EDIT (happyhorse-1.1-i2v),异步任务 ==========
VIDEO_MODEL = "happyhorse-1.1-i2v"  # 创建与查询必须使用同一模型名

# 用步骤 1 的关键帧作首帧(media.type=first_frame)
status, data = post_json(
    "/v2/vectordb/ai/video_edit",
    {
        "model_name": VIDEO_MODEL,
        "prompt": "镜头缓缓推进,雨丝飘落,暖光在伞面上轻轻闪动,保留首帧图中的主体。",
        "media": [{"type": "first_frame", "url": keyframe_url}],
        "params": {"resolution": "720P", "audio_setting": "none",
                   "watermark": False, "timeout_sec": 180},
    },
)
assert status == 200 and data.get("code") == 0, data
task_id = data["data"]["output"]["task_id"]
print(f"任务已创建  task_id = {task_id}")

# 轮询查询:tasks/describe 一次只查一个 task_id,带 provider 与相同 model_name
video_url = None
for attempt in range(60):  # 本地轮询上限,达到上限只记录 task_id,不判定服务端失败
    status, data = post_json(
        "/v2/vectordb/ai/tasks/describe",
        {"provider": "aliyun_milvus", "model_name": VIDEO_MODEL, "task_id": task_id},
    )
    task_status = data.get("data", {}).get("output", {}).get("task_status")
    print(f"  轮询 #{attempt:<2} ->  {task_status}")
    if task_status == "SUCCEEDED":
        video_url = data["data"]["output"]["video_url"]
        break
    if task_status in ("FAILED", "CANCELED"):
        raise RuntimeError(f"task ended in {task_status}: {json.dumps(data, ensure_ascii=False)}")
    time.sleep(10)
assert video_url, f"轮询达到本地上限,请稍后凭 task_id={task_id} 继续查询"
print(f"运动镜头 video_url:{video_url}")

轮询返回的 task_status 取值说明:

task_status

含义

处理方式

UNKNOWN

任务进行中,尚未完成。这是任务执行期间的正常返回值。

继续轮询,不要判定为异常。

SUCCEEDED

任务成功,可从 video_url 取结果。

结束轮询。

FAILED

任务失败。

终态,结束轮询并排查。

CANCELED

任务被取消。

终态,结束轮询。

警告

实测一次 720P、5 秒的图生视频任务约需 2~3 分钟,期间 task_status 会持续返回 UNKNOWN。tasks/describe 的返回体包含 task_id、task_status、video_url 三个字段,且必须携带与创建时相同的 model_name 和 provider。

说明

params.resolution 指定的是像素量档位,不是固定的宽高。实际输出尺寸由首帧图的宽高比决定:本例首帧为 1024×1024 正方形,设置 720P 后实际输出为 960×960(像素总量与 1280×720 相当),时长约 5 秒。如需 16:9 横屏输出,请使用对应宽高比的首帧图。

步骤三:素材理解,生成描述与标签

在素材入库前,用多模态大模型给图片和视频各生成一句客观的检索描述,作为标题、简介或标签来源,随素材一起在步骤四入库。

项

内容

Function / 模型

AI_MULTI_MODAL_GENERATE / qwen3.7-plus

输入

图片或视频 URL

输出

一句中文描述,可作为标题、描述或标签来源

# ========== 步骤 3:素材理解 AI_MULTI_MODAL_GENERATE (qwen3.7-plus) ==========
def describe_media(url: str, media_type: str, prompt: str) -> str:
    status, data = post_json(
        "/v2/vectordb/ai/multi_modal_generate",
        {
            "model_name": "qwen3.7-plus",
            "texts": [url],
            "params": {"media_type": media_type, "prompt": prompt, "temperature": 0},
        },
    )
    assert status == 200 and data.get("code") == 0, data
    return data["data"]["output"]["outputs"][0]


image_caption = describe_media(
    keyframe_url, "image",
    "用一句中文客观描述这张关键帧图片的主体、场景和色调,便于以文搜图。",
)
video_caption = describe_media(
    video_url, "video",
    "用一句中文客观描述这段视频的主体、动作和画面氛围,便于按镜头检索。",
)
print(f"[图片描述] {image_caption}")
print(f"[视频描述] {video_caption}")

图片用 media_type=image,视频用 media_type=video,prompt 为必填项。实测生成的描述示例:

[图片描述] 雨夜街头,一位穿格子衬衫的女子撑着伞站在便利店门口,身旁坐着一只金毛犬,
          整体色调偏冷蓝,灯光温暖映衬湿漉地面,营造静谧而略带忧郁的氛围。
[视频描述] 这段视频呈现了雨夜街头,一名身穿格子衬衫的女子撑着透明雨伞与一只黄狗站在亮灯的
          店铺外,镜头由全景逐渐推近至女子面部的特写,整体画面色调偏冷且带有湿润的反光。
说明

描述文案的质量直接决定后续以文搜图的召回效果,建议在 prompt 中明确要求「客观描述主体、场景、色调、动作」等检索时会用到的维度,避免生成带主观评价的文案。

步骤四:多模态向量入库

建一个「写入即向量化」的多模态素材 Collection,把图片 URL、视频 URL 连同描述、标签写入,Milvus 会自动为素材生成向量并落库。

项

内容

Function / 模型

AI_EMBEDDING / qwen3-vl-embedding

关键参数

is_multimodal 必须为 true,dim 为 2560

输入

步骤 1~3 产出的 URL + 描述 + 标签

输出

落库的 2560 维向量

# ========== 步骤 4:建多模态 Collection 并向量入库 AI_EMBEDDING (qwen3-vl-embedding) ==========
EMBED_MODEL = "qwen3-vl-embedding"
VECTOR_DIM = 2560  # qwen3-vl-embedding 多模态维度,向量字段维度必须与 dim 一致

client = MilvusClient(uri=MILVUS_URI, token=MILVUS_TOKEN)

collection_name = "aigc_assets_multimodal"
if client.has_collection(collection_name):
    client.drop_collection(collection_name)

schema = MilvusClient.create_schema(auto_id=True, enable_dynamic_field=False)
schema.add_field("id", DataType.INT64, is_primary=True)
schema.add_field("media_type", DataType.VARCHAR, max_length=16)     # image / video
schema.add_field("media_ref", DataType.VARCHAR, max_length=4096)    # 用于向量化的图片/视频 URL
schema.add_field("caption", DataType.VARCHAR, max_length=2048)      # 步骤 3 生成的描述
schema.add_field("tags", DataType.VARCHAR, max_length=512)
schema.add_field("embedding", DataType.FLOAT_VECTOR, dim=VECTOR_DIM)

# 写入 media_ref 时自动调用 qwen3-vl-embedding 生成 2560 维向量
schema.add_function(
    Function(
        name="embed_media",
        function_type=FunctionType.TEXTEMBEDDING,
        input_field_names=["media_ref"],
        output_field_names=["embedding"],
        params={
            "provider": "aliyun_milvus",
            "model_name": EMBED_MODEL,
            "dim": VECTOR_DIM,
            "is_multimodal": "true",
        },
    )
)

index_params = client.prepare_index_params()
index_params.add_index(field_name="embedding", index_type="AUTOINDEX", metric_type="COSINE")
client.create_collection(collection_name=collection_name, schema=schema,
                        index_params=index_params)

# 把步骤 1~3 的产出(url + caption + tags)向量化入库
client.insert(
    collection_name,
    [
        {"media_type": "image", "media_ref": keyframe_url,
         "caption": image_caption, "tags": "关键帧,生成图"},
        {"media_type": "video", "media_ref": video_url,
         "caption": video_caption, "tags": "运动镜头,生成视频"},
    ],
)
client.flush(collection_name)
client.load_collection(collection_name)
print(f"图片与视频已向量化入库,Collection:{collection_name}")
警告

多模态 Function 必须带 "is_multimodal": "true",否则在 create_collection 阶段即报 multimodal=false ... 400 InvalidParameter url error。向量字段的 dim 必须与 Function 参数中的 dim 一致(2560)。

说明

写入后必须调用 flush(),否则紧接着执行检索可能返回空结果。图片和视频要落在同一个向量字段、使用同一个模型,才能在同一向量空间里做跨模态检索。

生成物 URL 是临时签名地址,入库前建议转存。生图与生视频返回的 URL 均带 OSS 签名,实测有效期约 24 小时。这会带来两类影响:

场景

影响

库中 media_ref 的 URL 过期后做文本检索

仍能正常召回(向量在写入时已生成并落库),但返回的 URL 已无法访问,业务侧展示素材会失败。

用已失效的 URL 作为查询做以图搜图

整个 search 请求报错 code 65535 ... download form url error,不会降级或跳过该条查询。

警告

因此生产环境中应在入库前把生成物转存到自己的 OSS,并把长期有效的地址写入 media_ref;同时保证查询侧传入的媒体 URL 可访问。

步骤五:内容搜索

素材入库后,用同一个多模态模型把查询映射到同一向量空间,检索最相似的素材实现复用。由于 Collection 已绑定 qwen3-vl-embedding 的 Function,search 的 data 里直接传原始文本或媒体 URL 即可,Milvus 会自动向量化查询。

# ========== 步骤 5:内容搜索 以文搜图 / 以文搜视频 / 以图搜视频 ==========
def search_assets(query: str, top_k: int = 5, media_type: str | None = None,
                  min_score: float = 0.0):
    """query 可以是一段中文/英文文案,也可以是一张查询图片的 URL。
    min_score:相似度阈值,低于该值的结果视为不相关,直接丢弃。"""
    filter_expr = f'media_type == "{media_type}"' if media_type else ""
    results = client.search(
        collection_name=collection_name,
        data=[query],                 # 文本或图片 URL,均由 qwen3-vl-embedding 向量化
        anns_field="embedding",
        limit=top_k,
        filter=filter_expr,
        output_fields=["media_type", "media_ref", "caption", "tags"],
    )
    for rank, hit in enumerate(results[0], 1):
        if hit["distance"] < min_score:
            continue
        e = hit["entity"]
        url = e["media_ref"].split("?")[0]   # 去掉 OSS 签名参数,展示更干净
        print(f"  {rank}. [相似度 {hit['distance']:.4f}] [{e['media_type']}] {e['caption'][:42]}")
        print(f"       {url}")


# 以文搜图:限定只搜图片
print("[5.1] 以文搜图:")
search_assets("雨夜撑伞的女子站在便利店门口", media_type="image")

# 以文搜视频:限定只搜视频
print("[5.2] 以文搜视频:")
search_assets("雨夜街头镜头缓慢推进的运动镜头", media_type="video")

# 以图搜视频:用一张关键帧图片 URL 去召回风格相近的视频素材
print("[5.3] 以图搜视频(用关键帧图召回同源视频):")
search_assets(keyframe_url, media_type="video")

# 跨模态混合检索:去掉 filter,在图片与视频混合库中检索
print("[5.4] 跨模态混合检索:")
search_assets("雨夜便利店暖光镜头")

实测三路检索的召回结果与相似度:

检索方式

查询

命中

相似度

以文搜图

雨夜撑伞的女子站在便利店门口

image

0.4834

以文搜视频

雨夜街头镜头缓慢推进的运动镜头

video

0.4967

以图搜视频

关键帧图片 URL

video

0.8715

跨模态混合(无 filter)

雨夜便利店暖光镜头

video / image

0.4967 / 0.3816

其中以图搜视频达到 0.8715,是四种方式中最高的——该视频正是由这张关键帧生成的,同源素材在同一向量空间下相似度极高。这也直观体现了「图片和视频共享同一向量空间」的价值:把查询文案换成一张图片 URL,同一个 search 调用即可完成以图搜图与以图搜视频;去掉 filter 则在图片与视频混合库中跨模态检索。

查询文案要与素材主题相关,并配合相似度阈值。多模态检索按相似度排序返回 Top-K,即使查询与素材完全无关也会返回结果。实测对比:

查询文案

与素材的关系

相似度

雨夜撑伞的女子站在便利店门口

主题匹配

0.4834

室内暖光下的条纹毛衣造型

与素材无关

0.0715

角色近景,镜头缓慢推进的概念片

与素材无关

0.0694

说明

相差近 7 倍。因此应用侧应结合相似度阈值过滤(本例 search_assets 的 min_score 参数),把明显不相关的长尾结果丢弃,避免把 0.07 这类结果当作有效召回展示给用户。具体阈值需按业务素材分布调参。

方案价值

与自己从零对接各家生成模型、再拼一套检索系统相比:

维度

传统自建方案

阿里云 Milvus

生成模型接入

文生图、图生图、文生视频各家 SDK、鉴权与返回格式分散对接

AI Function 统一封装生图、生视频与内容理解

异步任务管理

自建任务队列、轮询退避、终态判定与超时兜底

tasks/describe 标准轮询接口,终态判定清晰

素材理解

自建或外接标题、描述、标签服务

AI_MULTI_MODAL_GENERATE 内置多模态理解

向量化链路

应用侧先调 embedding 再写入

写入即向量化,由 Collection Function 自动生成

检索入口

图片、视频、文本、向量四份数据割裂,跨系统拼装

单库单次 search 完成跨模态检索

这套方案的价值不只是生成快,更在于把每一次生成都沉淀为可检索、可复用的素材资产,让内容团队从一次性消耗走向资产化经营。新剧本出现相似镜头需求时,先在历史素材库里语义检索一遍,能复用的直接复用,不能复用的再走生成,边际成本持续下降。