首页
/ 文本嵌入实战:构建语义视频搜索应用——从向量索引到余弦相似度检索(generative-ai-for-beginners 第 08 课)

文本嵌入实战:构建语义视频搜索应用——从向量索引到余弦相似度检索(generative-ai-for-beginners 第 08 课)

2026-09-06 11:35:23作者:盛欣凯Ernestine

本篇指南基于 generative-ai-for-beginners 课程第 08 课《Building a Search Applications》,围绕"用文本嵌入(Text Embeddings)构建语义搜索应用"这一核心主题展开:你将理解语义搜索与关键词搜索的本质区别、掌握文本嵌入向量的生成方式,并学会利用仓库内置的 YouTube 字幕嵌入索引(embedding_index_3m.json)和余弦相似度(Cosine Similarity)完成一次可运行的语义检索实战——学生输入一句自然语言问题,应用即可返回相关视频以及指向视频中答案所在位置的时间戳链接。读完本文,你将能够完整复现该课的搜索应用,并理解其背后从字幕下载、分桶、摘要、嵌入到检索的全链路数据准备流程。

语义查询示例:输入"can you use rstudio with azure ml"后,应用返回带时间戳的视频链接列表

课程定位与学习目标

大语言模型(LLM)的能力远不止聊天机器人和文本生成——利用嵌入向量构建搜索应用,是 LLM 生态中极具实用价值的能力之一。本课以一个教育创业公司为场景:该非营利组织为学生提供免费的 AI 课程学习资源,拥有大量 YouTube 教学视频,希望学生输入一个问题(例如 "What are Jupyter Notebooks?" 或 "What is Azure ML")后,搜索应用能返回与问题相关的视频列表,并进一步给出链接,直接定位到视频中回答该问题的位置。

搜索应用运行界面:Notebook 中等待用户输入查询的输入框

课程原文明确列出了四个知识模块与三条学习目标(见 08-building-search-applications/README.md):

课程覆盖内容

  • 语义搜索(Semantic Search)与关键词搜索(Keyword Search)的区别
  • 什么是文本嵌入(Text Embeddings)
  • 如何创建文本嵌入索引(Embedding Index)
  • 如何搜索文本嵌入索引

学完本课你将能够

  • 区分语义搜索与关键词搜索
  • 解释文本嵌入是什么
  • 使用嵌入构建一个可搜索数据的应用

语义搜索 vs 关键词搜索

课程用了一个直观的比喻来说明两者的差异:假设你想买一辆车,你在搜索框里输入 "my dream car"。

  • 语义搜索理解你查询中词语的"语义"(semantics,即含义):它知道你并不是在寻找关于"做梦"的车,而是想找一辆"理想中"的车,理解你的真实意图并返回相关结果。
  • 关键词搜索则会字面匹配 "dream" 和 "car",搜索"关于做梦的车",经常返回不相关的结果。

这正是本课要构建的应用的基础——学生用自然语言提问,系统按"意思相近"而非"字面相同"来匹配视频内容。

什么是文本嵌入(Text Embeddings)

文本嵌入是自然语言处理(NLP)中一种文本表示技术:它将文本转换为语义化的数值表示。嵌入的用途是让机器能够"理解"数据——在向量空间中,语义相近的文本,其向量也相近,这是语义检索的数学基础。

构建文本嵌入的模型有很多,本课聚焦于使用 OpenAI Embedding 模型text-embedding-ada-002)生成嵌入。课程给出的例子是 AI Show 频道某集视频的转录文本:

Today we are going to learn about Azure Machine Learning.

将这段文本传入 OpenAI Embedding API,会返回一个由 1536 个数字组成的向量(即嵌入)。向量中的每个数字代表文本的某个语义方面。课程展示了向量前 10 个数值:

[-0.006655829958617687, 0.0026128944009542465, 0.008792596869170666, -0.02446001023054123, -0.008540431968867779, 0.022071078419685364, -0.010703742504119873, 0.003311325330287218, -0.011632772162556648, -0.02187200076878071, ...]

嵌入索引是如何构建的:五步流水线

本课的嵌入索引由一系列 Python 脚本生成,脚本与完整说明位于 scripts/README.md。课程本身不需要运行这些脚本——嵌入索引已随仓库提供(embedding_index_3m.json)——但理解构建流程对掌握整条数据链路至关重要。

课程原文描述的五个核心操作是:

  1. 下载 YouTube 播放列表(AI Show 频道)中每个视频的字幕(transcript);
  2. 使用 OpenAI Functions(函数调用),尝试从字幕前 3 分钟中提取说话人(speaker)姓名,存入索引文件 embedding_index_3m.json
  3. 将字幕文本切分为以 3 分钟为单位的文本段,每个段落包含与下一段约 20 个单词的重叠,确保嵌入不被"切断",并提供更好的搜索上下文;
  4. 将每个文本段传入 OpenAI Chat API,压缩为 60 词的摘要,摘要同样存入 embedding_index_3m.json
  5. 最后将文本段传入 OpenAI Embedding API,得到 1536 维的语义向量,与文本段一起存入嵌入索引。

源码级流程剖析:scripts 目录的六个脚本

仓库 08-building-search-applications/scripts 目录实现了上述流水线,统一入口是 prepare_transcripts_ai_show.sh(另有 .ps1.bat 版本对应 macOS/Linux 与 Windows),它按顺序执行六个步骤:

export TRANSCRIPT_FOLDER=transcripts_the_ai_show
export TRANSCRIPT_BUCKET_MINUTES=3

python3 transcript_download.py -f $TRANSCRIPT_FOLDER -p PLlrxD0HtieHi0mwteKBOfEeOYf0LJU4O1
python3 transcript_enrich_speaker.py -f $TRANSCRIPT_FOLDER
python3 transcript_enrich_bucket.py -f $TRANSCRIPT_FOLDER -m $TRANSCRIPT_BUCKET_MINUTES
python3 transcript_enrich_summaries.py -f $TRANSCRIPT_FOLDER
python3 transcript_enrich_embeddings.py -f $TRANSCRIPT_FOLDER
python3 transcript_enrich_lite.py -f $TRANSCRIPT_FOLDER

各脚本的关键实现细节如下:

1. 字幕下载 —— transcript_download.py

基于 youtube-transcript-api 与 Google API 客户端,从指定播放列表批量下载字幕并格式化为 WebVTT;需要环境变量 GOOGLE_DEVELOPER_API_KEY,使用 40 个线程并发拉取(MAX_RESULTS = 50)。

2. 说话人提取 —— transcript_enrich_speaker.py

这正是课程所述"用 OpenAI Functions 提取说话人"的落地实现。脚本定义了一个名为 get_speaker_name 的函数(返回逗号分隔的说话人姓名列表),将视频标题、描述与前 3 分钟字幕拼成输入,通过 tool_choice={"type": "function", "name": "get_speaker_name"} 强制模型调用该函数,并以 temperature=0.0 保证结果稳定:

get_speaker_name = {
    "name": "get_speaker_name",
    "description": "Get the speaker names for the session.",
    "parameters": {
        "type": "object",
        "properties": {
            "speakers": {
                "type": "string",
                "description": "The speaker names.",
            }
        },
        "required": ["speaker_name"],
    },
}

脚本用 10 个线程并发处理,并对每个视频的调用使用 tenacity 做指数退避重试(最多 4 次)。提取到的姓名写回每个视频的元数据 JSON,最终成为索引中的 speaker 字段。

3. 分桶切段 —— transcript_enrich_bucket.py

负责把长字幕切成适合嵌入的文本段。核心参数与课程描述一一对应(从源码看):

  • SEGMENT_LENGTH_MINUTES = 5(默认值,入口脚本以 -m 3 覆盖为 3 分钟,即索引文件名中 "3m" 的由来);
  • PERCENTAGE_OVERLAP = 0.05:切段时将新段前 5% 的单词追加到上一段末尾,实现课程所说的"约 20 词重叠",平滑上下文衔接、避免嵌入被切断;
  • MAX_TOKENS = 2048:用 tiktokengpt-4o-mini 编码器)计数,单段文本超过该 token 上限即强制切新段,为后续摘要步骤预留 token 空间。

每个段落会携带 startHH:MM:SS 格式)与 seconds(秒数)两个时间戳字段——这正是最终搜索结果能生成"跳转到视频指定时刻"链接的数据来源。

4. 60 词摘要 —— transcript_enrich_summaries.py

将每个文本段送给 Chat API 生成摘要,系统提示词明确要求"写一段权威的 60 词摘要,且句子不要以 'This video' 开头",max_tokens=512,10 线程并发并带重试。摘要写入段落的 summary 字段,用于在搜索结果中直接展示。

5. 生成嵌入 —— transcript_enrich_embeddings.py

对每个段落调用 text-embedding-ada-002 部署(环境变量 AZURE_OPENAI_EMBEDDINGS_DEPLOYMENT,默认值即 text-embedding-ada-002),向量写入 ada_v2 字段。源码中的几个工程细节值得注意:

  • 使用 tiktokencl100k_base 编码,跳过超过 8191 token 的超长段(嵌入接口上限 8191);
  • 6 个线程并发处理队列,每次请求后 sleep(0.2) 做温和限速;
  • tenacity 重试策略:指数退避(6–30 秒随机等待)最多 20 次,但 BadRequestError 不重试;
  • 已含 ada_v2 字段的段落直接跳过(幂等,支持断点续跑);
  • 处理完成后按 (videoId, start) 排序输出,保证索引内段落的时间顺序。

6. 精简输出 —— transcript_enrich_lite.py

删除每个段落中的 textdescription 字段(检索只需要摘要与向量,不需要原文),得到体积更小的"lite"索引——即仓库提供的 embedding_index_3m.json。入口脚本最终将其重命名为 embedding_index_3m.json 输出。

索引数据结构:embedding_index_3m.json

仓库提供了两份内容相同的嵌入索引:08-building-search-applications/embedding_index_3m.jsonscripts/embedding_index_3m.json(各约 48MB,前者被搜索应用直接加载)。索引内容为 JSON 数组,实际规模约 1400 个段落,覆盖 AI Show 频道截至 2023 年 10 月的视频转录。每个段落对象的字段结构如下:

字段 含义 示例
speaker 说话人姓名(由函数调用提取) Seth Juarez, Josh Lovejoy, Sarah Bird
title 视频标题 You're Not Solving the Problem You Think You're Solving
videoId YouTube 视频 ID -tJQm4mSh1s
start 段落起始时间(HH:MM:SS 00:00:00
seconds 段落起始秒数(用于生成带时间戳的链接) 0
summary 该段落的 60 词摘要 Join Seth Juarez as he discusses ethical concerns...
ada_v2 1536 维嵌入向量(text-embedding-ada-002 生成) [0.004357, -0.028409, ...]

生产环境应该使用向量数据库

课程明确指出:为了教学简化,嵌入索引存放在 JSON 文件中并用 Pandas DataFrame 加载;而在生产环境中,嵌入索引应存放在向量数据库中,例如 Azure Cognitive Search、Redis、Pinecone、Weaviate 等。教学场景用 JSON + DataFrame 足以在本地完成检索演示,但面对大规模数据时,向量数据库提供的索引结构(如 HNSW、IVF)与近似最近邻(ANN)能力是必要的——这一点从本应用"全量计算 1400 个向量的余弦相似度"的做法即可看出(见下文)。

理解余弦相似度(Cosine Similarity)

学完文本嵌入后,下一步是学会用嵌入搜索数据:核心工具就是余弦相似度,也常被称作最近邻搜索(nearest neighbor search)。执行一次余弦相似度搜索的完整流程是:

  1. 用 OpenAI Embedding API 把查询文本向量化;
  2. 计算查询向量与嵌入索引中每个向量的余弦相似度;
  3. 按相似度排序,相似度最高的文本段就是与查询最相近的结果。

从数学角度看,余弦相似度衡量的是两个向量在高维空间中投影时夹角的余弦值。这一度量方式的优点在于:即使两个文档因长度不同而在欧氏距离上相距甚远,只要它们的"方向"接近(夹角小),仍能获得高余弦相似度——这正是文本检索需要"与长度无关的相似性"的原因。

在解决方案 Notebook 中,这个计算是这样实现的(见 python/aoai-solution.ipynb):

def cosine_similarity(a, b):
    if len(a) > len(b):
        b = np.pad(b, (0, len(a) - len(b)), 'constant')
    elif len(b) > len(a):
        a = np.pad(a, (0, len(b) - len(a)), 'constant')
    return np.dot(a, b) / (np.linalg.norm(a) * np.linalg.norm(b))

实现上先对长度不一致的向量做零填充再计算 dot(a, b) / (||a|| * ||b||),与数学定义完全一致。

构建你的第一个搜索应用

本应用已在 Windows 11、macOS 与 Ubuntu 22.04 上使用 Python 3.10 或更高版本构建并测试(Python 可从 python.org 下载)。应用的主流程是:加载嵌入索引 → 提示用户输入查询 → 调用检索函数 → 展示结果 → 循环直到用户输入 exit

解决方案 Notebook 的完整代码逻辑(摘自 python/aoai-solution.ipynb):

初始化客户端与关键参数

import os
import pandas as pd
import numpy as np
from openai import AzureOpenAI
from dotenv import load_dotenv

load_dotenv()

client = AzureOpenAI(
  api_key=os.environ['AZURE_OPENAI_API_KEY'],
  api_version = "2024-10-21",
  azure_endpoint = os.environ['AZURE_OPENAI_ENDPOINT']
)

model = os.environ['AZURE_OPENAI_EMBEDDINGS_DEPLOYMENT']

SIMILARITIES_RESULTS_THRESHOLD = 0.75   # 相似度阈值
DATASET_NAME = "../embedding_index_3m.json"

注意两个关键参数:相似度阈值 0.75 用于过滤掉语义上不够相关的段落;model 指向你在 Azure OpenAI 中部署的 text-embedding-ada-002 部署名。

加载索引

def load_dataset(source: str) -> pd.core.frame.DataFrame:
    # Load the video session index
    pd_vectors = pd.read_json(source)
    return pd_vectors.drop(columns=["text"], errors="ignore").fillna("")

errors="ignore" 表明该加载函数对"lite"版索引(无 text 列)与完整版索引都兼容。

核心检索函数 get_videos:对查询生成嵌入 → 为索引每一行计算余弦相似度 → 按阈值过滤 → 按相似度降序排序 → 返回 Top 5:

def get_videos(
    query: str, dataset: pd.core.frame.DataFrame, rows: int
) -> pd.core.frame.DataFrame:
    # create a copy of the dataset
    video_vectors = dataset.copy()

    # get the embeddings for the query
    query_embeddings = client.embeddings.create(input=query, model=model).data[0].embedding

    # create a new column with the calculated similarity for each row
    video_vectors["similarity"] = video_vectors["ada_v2"].apply(
        lambda x: cosine_similarity(np.array(query_embeddings), np.array(x))
    )

    # filter the videos by similarity
    mask = video_vectors["similarity"] >= SIMILARITIES_RESULTS_THRESHOLD
    video_vectors = video_vectors[mask].copy()

    # sort the videos by similarity
    video_vectors = video_vectors.sort_values(by="similarity", ascending=False).head(rows)

    return video_vectors.head(rows)

结果展示 display_results:将段落的 seconds 字段拼进 YouTube 链接的时间戳参数 ?t=,生成"直达视频答案位置"的链接:

def display_results(videos: pd.core.frame.DataFrame, query: str):
    def _gen_yt_url(video_id: str, seconds: int) -> str:
        """convert time in format 00:00:00 to seconds"""
        return f"https://youtu.be/{video_id}?t={seconds}"

    print(f"\nVideos similar to '{query}':")
    for _, row in videos.iterrows():
        youtube_url = _gen_yt_url(row["videoId"], row["seconds"])
        print(f" - {row['title']}")
        print(f"   Summary: {' '.join(row['summary'].split()[:15])}...")
        print(f"   YouTube: {youtube_url}")
        print(f"   Similarity: {row['similarity']}")
        print(f"   Speakers: {row['speaker']}")

交互式主循环

pd_vectors = load_dataset(DATASET_NAME)

while True:
    query = input("Enter a query: ")
    if query == "exit":
        break
    videos = get_videos(query, pd_vectors, 5)
    display_results(videos, query)

运行 Notebook 后会看到如下输入提示,输入查询即可得到"视频标题 + 摘要前 15 词 + 带时间戳的链接 + 相似度 + 说话人"的结果列表。课程建议尝试的查询包括:

  • What is Azure Machine Learning?
  • How do convolutional neural networks work?
  • What is a neural network?
  • Can I use Jupyter Notebooks with Azure Machine Learning?
  • What is ONNX?

动手练习:创建 Azure OpenAI 服务资源

运行解决方案需要 Azure 订阅。练习引导你在 Azure Cloud Shell 中依次完成以下操作(命令均出自课程原文):

1. 打开 Azure Cloud Shell:登录 Azure 门户,点击右上角的 Cloud Shell 图标,环境类型选择 Bash

2. 创建资源组(课程使用名为 semantic-video-search 的资源组,位于 East US;更换区域前请先核对模型可用区域表):

az group create --name semantic-video-search --location eastus

3. 创建 Azure OpenAI 服务资源

az cognitiveservices account create --name semantic-video-openai --resource-group semantic-video-search \
    --location eastus --kind OpenAI --sku s0

4. 获取应用的端点与密钥

az cognitiveservices account show --name semantic-video-openai \
   --resource-group  semantic-video-search | jq -r .properties.endpoint
az cognitiveservices account keys list --name semantic-video-openai \
   --resource-group semantic-video-search | jq -r .key1

5. 部署 OpenAI 嵌入模型 text-embedding-ada-002

az cognitiveservices account deployment create \
    --name semantic-video-openai \
    --resource-group  semantic-video-search \
    --deployment-name text-embedding-ada-002 \
    --model-name text-embedding-ada-002 \
    --model-version "2"  \
    --model-format OpenAI \
    --sku-capacity 100 --sku-name "Standard"

若你还要复现 scripts 目录的完整数据准备流水线(说话人提取与摘要步骤依赖 gpt-4o-mini),则需额外部署聊天模型,scripts/README.md 给出的部署命令为:

az cognitiveservices account deployment create \
    --name semantic-video-openai \
    --resource-group  semantic-video-search \
    --deployment-name gpt-4o-mini \
    --model-name gpt-4o-mini \
    --model-format OpenAI \
    --sku-capacity 100 \
    --sku-name "Standard"

依赖与环境变量

运行搜索应用(Notebook)所需的包(见 python/requirements.txt):

ipykernel>=6.25.2
pandas>=2.1.1,<3.0.0
plotly>=5.16.1,<5.17.0
matplotlib>=3.7.2,<4.0.0
scipy>=1.11.2,<2.0.0
scikit-learn>=1.3.0,<2.0.0

需要说明的是:从源码结构看,解决方案 Notebook 的导入 from openai import AzureOpenAI 以及 api_version 参数属于 OpenAI Python SDK v1.x 的用法,而该 requirements 文件中另有一条 openai>=0.28.0,<0.29.0 的旧版约束——实际配置环境时应以 Notebook 代码所需的 v1.x SDK 为准(openai>=1.0<2.0scripts 目录的 requirements.txt 即采用 openai>=1.54.0,<2.0.0),否则 AzureOpenAI 类无法导入。

运行数据准备脚本所需的环境变量(见 scripts/README.md,Linux/macOS 下写入 ~/.bashrc~/.zshrc):

export AZURE_OPENAI_API_KEY=<your Azure OpenAI Service API key>
export AZURE_OPENAI_ENDPOINT=<your Azure OpenAI Service endpoint>
export AZURE_OPENAI_MODEL_DEPLOYMENT_NAME=<your Azure OpenAI Service model deployment name>
export GOOGLE_DEVELOPER_API_KEY=<your Google developer API key>

而 Notebook 本身通过 .env 文件加载 AZURE_OPENAI_API_KEYAZURE_OPENAI_ENDPOINTAZURE_OPENAI_EMBEDDINGS_DEPLOYMENT(代码中用 load_dotenv() 读取,嵌入部署名建议命名为 AZURE_OPENAI_EMBEDDINGS_DEPLOYMENT 指向你的 text-embedding-ada-002 部署)。

关键要点回顾

  • 语义搜索依据查询的含义返回结果,区别于字面匹配的关键词搜索;其技术载体是文本嵌入向量与向量相似性度量;
  • 文本嵌入将文本映射为 1536 维数值向量(text-embedding-ada-002),语义相近的文本向量夹角更小;
  • 嵌入索引通过"下载字幕 → 函数调用提取说话人 → 3 分钟分桶(含词重叠)→ 60 词摘要 → 生成嵌入 → 精简输出"的流水线构建,索引中每个段落携带时间戳,这是"跳转到视频答案位置"能力的来源;
  • 余弦相似度是检索核心:查询向量化后与索引全量向量求夹角余弦,取 Top 5 且相似度 ≥ 0.75 的结果;
  • 教学实现用 JSON + Pandas 完成全量精确检索,生产环境应迁移到向量数据库以支撑规模化的近似最近邻搜索。

完成本课练习后,可以继续学习课程第 9 课,了解如何构建图像生成应用

登录后查看全文
热门项目推荐
相关项目推荐

项目优选

收起
kernelkernel
deepin linux kernel
C
33
18
ops-transformerops-transformer
本项目是CANN提供的transformer类大模型算子库,实现网络在NPU上加速计算。
C++
1.13 K
2.75 K
pytorchpytorch
作为 Ascend for PyTorch 社区的核心组件,TorchNPU 是昇腾专为 PyTorch 打造的深度学习适配插件,使 PyTorch 框架能够直接调用昇腾 NPU,为开发者提供昇腾 AI 处理器的超强算力。
Python
857
1.35 K
docsdocs
暂无描述
Markdown
897
5.8 K
kernelkernel
openEuler内核是openEuler操作系统的核心,既是系统性能与稳定性的基石,也是连接处理器、设备与服务的桥梁。
C
529
593
ops-nnops-nn
本项目是CANN提供的神经网络类计算算子库,实现网络在NPU上加速计算。
C++
915
1.83 K
jiuwenswarmjiuwenswarm
JiuwenSwarm 是一款基于openJiuwen开发的智能AI Agent,它能够将大语言模型的强大能力,通过你日常使用的各类通讯应用,直接延伸至你的指尖。
Python
3.58 K
1.01 K
ops-mathops-math
本项目是CANN提供的数学类基础计算算子库,实现网络在NPU上加速计算。
C++
1.35 K
1.46 K
cann-learning-hubcann-learning-hub
CANN 学习中心仓,支持在线互动运行、边学边练,提供教程、示例与优化方案,一站式助力昇腾开发者快速上手。
Jupyter Notebook
1.01 K
515
AscendNPU-IRAscendNPU-IR
AscendNPU-IR是基于MLIR(Multi-Level Intermediate Representation)构建的,面向昇腾亲和算子编译时使用的中间表示,提供昇腾完备表达能力,通过编译优化提升昇腾AI处理器计算效率,支持通过生态框架使能昇腾AI处理器与深度调优
C++
547
388