文本嵌入实战:构建语义视频搜索应用——从向量索引到余弦相似度检索(generative-ai-for-beginners 第 08 课)
本篇指南基于 generative-ai-for-beginners 课程第 08 课《Building a Search Applications》,围绕"用文本嵌入(Text Embeddings)构建语义搜索应用"这一核心主题展开:你将理解语义搜索与关键词搜索的本质区别、掌握文本嵌入向量的生成方式,并学会利用仓库内置的 YouTube 字幕嵌入索引(embedding_index_3m.json)和余弦相似度(Cosine Similarity)完成一次可运行的语义检索实战——学生输入一句自然语言问题,应用即可返回相关视频以及指向视频中答案所在位置的时间戳链接。读完本文,你将能够完整复现该课的搜索应用,并理解其背后从字幕下载、分桶、摘要、嵌入到检索的全链路数据准备流程。
课程定位与学习目标
大语言模型(LLM)的能力远不止聊天机器人和文本生成——利用嵌入向量构建搜索应用,是 LLM 生态中极具实用价值的能力之一。本课以一个教育创业公司为场景:该非营利组织为学生提供免费的 AI 课程学习资源,拥有大量 YouTube 教学视频,希望学生输入一个问题(例如 "What are Jupyter Notebooks?" 或 "What is Azure ML")后,搜索应用能返回与问题相关的视频列表,并进一步给出链接,直接定位到视频中回答该问题的位置。
课程原文明确列出了四个知识模块与三条学习目标(见 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)——但理解构建流程对掌握整条数据链路至关重要。
课程原文描述的五个核心操作是:
- 下载 YouTube 播放列表(AI Show 频道)中每个视频的字幕(transcript);
- 使用 OpenAI Functions(函数调用),尝试从字幕前 3 分钟中提取说话人(speaker)姓名,存入索引文件
embedding_index_3m.json; - 将字幕文本切分为以 3 分钟为单位的文本段,每个段落包含与下一段约 20 个单词的重叠,确保嵌入不被"切断",并提供更好的搜索上下文;
- 将每个文本段传入 OpenAI Chat API,压缩为 60 词的摘要,摘要同样存入
embedding_index_3m.json; - 最后将文本段传入 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:用tiktoken(gpt-4o-mini编码器)计数,单段文本超过该 token 上限即强制切新段,为后续摘要步骤预留 token 空间。
每个段落会携带 start(HH: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 字段。源码中的几个工程细节值得注意:
- 使用
tiktoken的cl100k_base编码,跳过超过 8191 token 的超长段(嵌入接口上限 8191); - 6 个线程并发处理队列,每次请求后
sleep(0.2)做温和限速; tenacity重试策略:指数退避(6–30 秒随机等待)最多 20 次,但BadRequestError不重试;- 已含
ada_v2字段的段落直接跳过(幂等,支持断点续跑); - 处理完成后按
(videoId, start)排序输出,保证索引内段落的时间顺序。
6. 精简输出 —— transcript_enrich_lite.py
删除每个段落中的 text 与 description 字段(检索只需要摘要与向量,不需要原文),得到体积更小的"lite"索引——即仓库提供的 embedding_index_3m.json。入口脚本最终将其重命名为 embedding_index_3m.json 输出。
索引数据结构:embedding_index_3m.json
仓库提供了两份内容相同的嵌入索引:08-building-search-applications/embedding_index_3m.json 与 scripts/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)。执行一次余弦相似度搜索的完整流程是:
- 用 OpenAI Embedding API 把查询文本向量化;
- 计算查询向量与嵌入索引中每个向量的余弦相似度;
- 按相似度排序,相似度最高的文本段就是与查询最相近的结果。
从数学角度看,余弦相似度衡量的是两个向量在高维空间中投影时夹角的余弦值。这一度量方式的优点在于:即使两个文档因长度不同而在欧氏距离上相距甚远,只要它们的"方向"接近(夹角小),仍能获得高余弦相似度——这正是文本检索需要"与长度无关的相似性"的原因。
在解决方案 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.0,scripts 目录的 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_KEY、AZURE_OPENAI_ENDPOINT 与 AZURE_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 课,了解如何构建图像生成应用。
atomcodeClaude Code 的开源替代方案。连接任意大模型,编辑代码,运行命令,自动验证 — 全自动执行。用 Rust 构建,极致性能。 | An open-source alternative to Claude Code. Connect any LLM, edit code, run commands, and verify changes — autonomously. Built in Rust for speed. Get StartedRust0627
Hy4-previewHy4 preview 是由腾讯混元团队研发的新一代混合专家(MoE)旗舰模型。模型总参数量 770B,每个 token 激活 49B,主干共包含78层,第一层采用标准 FFN,其余 77 层均为 MoE 结构,每层包含 256 个路由专家与 1 个共享专家,每个 token 激活 top-8 路由专家及共享专家。主干之外原生内置 1 层 MTP(总参数量 10B,激活 0.7B)以支持投机解码。Python00
GLM-5.3GLM-5.3 与 GLM-5.2 使用相同的基座模型——所有提升均来自后训练。与 GLM-5.2 相比,它在复杂编程和长程任务上的表现显著提升。Jinja00
GLM-5.3-FlashGLM-5.3-Flash (320B-A18B),是GLM-5系列的首个原生多模态模型。320B总参数,能力超过GLM-5.2Jinja00
Spark-X2.5-4BSpark-X2.5-4B 旨在让强大的 AI 更实用、更高效、更易获得。在广泛日常任务中表现强劲,涵盖对话、写作、翻译、推理、编码、工具调用以及智能体工作流,并在同等规模的开源模型中取得领先成绩。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00

