基于 Embeddings 与余弦相似度构建语义搜索应用:generative-ai-for-beginners 第 08 课实战
本课属于本仓库「Generative AI for Beginners」21 课课程体系的第 08 课(英文原版位于 08-building-search-applications/README.md,本文对应的多语言版本见 translations/cs/08-building-search-applications/README.md)。课程演示了 LLM 不止能做聊天机器人与文本生成,还能借助 文本嵌入(Text Embeddings) 构建"答案定位到视频具体时间戳"的语义搜索引擎。完成本课,你将掌握语义搜索与关键词搜索的区别、文本嵌入的生成原理、嵌入索引的构建流水线,以及用 Python + Azure OpenAI 实现一套可运行的语义视频检索 Notebook。
课程核心脉络
本课按"概念 → 原理 → 数据准备 → 实战"层层递进:
- 语义搜索(Semantic Search)与关键词搜索(Keyword Search)的区别;
- 什么是文本嵌入(Text Embeddings);
- 如何构建文本嵌入索引(Text Embeddings Index);
- 如何检索文本嵌入索引,找到与用户问题最相关的视频片段。
配套的代码产物集中在本仓库的 08-building-search-applications/python(OpenAI 与 Azure OpenAI 两种实现),课程数据集 embedding_index_3m.json(约 48 MB)与全部数据准备脚本位于 08-building-search-applications/scripts。
为什么选择构建语义搜索应用
课程的设定背景是一个向发展中国家学生提供免费 AI 教育的非营利"教育创业公司":仓库维护了大量 YouTube 教学视频,希望学生输入一句自然语言问题就能搜到相关视频,并直接定位到视频中回答问题的那一段(返回带时间戳的 YouTube 链接)。例如学生输入 "What is Azure ML?",应用返回按相关度排序的视频列表,同时给出 https://youtu.be/{videoId}?t={seconds} 这样的精确跳转链接,真正做到"秒级定位答案"。
下图是该语义检索的真实运行示例——注意结果链接中携带的时间戳,点击即可跳到视频里给出答案的位置:
示例索引覆盖了微软 [AI Show](课程文档中说明其为讲解 AI 与机器学习的 YouTube 频道)截至 2023 年 10 月的全部视频转录稿,每个转录文本片段都对应一条向量记录。
语义搜索与关键词搜索
语义搜索(Semantic Search) 利用查询词在语句中的"语义/意图"来返回相关结果。文档给出的类比是:当你想买车时搜索 "my dream car",语义搜索理解你要找的是"理想之车"而非"梦见车",从而返回真正相关的商品。而关键词搜索(Keyword Search) 会按字面去匹配包含 "dream car" 字样的内容,往往返回一堆无关结果。
落到本仓库的实现上,这种"语义匹配"能力正是由每段转录文本对应的 Embedding 向量承载的——检索不再比对字面字符,而是比对两段文本在高维空间中的方向相近程度(见后文余弦相似度)。
文本嵌入(Text Embeddings)是什么
文本嵌入是自然语言处理(NLP)中的一种文本表示技术:把一段文本映射为一串有语义含义的数值向量,让机器能够"理解"文本间的语义关系。生成嵌入的模型很多,本课统一使用 OpenAI Embedding 模型。
文档给出一个直观例子:转录稿中有一句
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, ...]
仓库中 aoai-assignment.ipynb 进一步通过一组对照实验验证"语义相近→向量相近"这一核心特性:分别对 automobile、vehicle、dinosaur、stick 求嵌入并两两计算余弦相似度——automobile 与自身的相似度为 1.0,与 vehicle 很接近,与 dinosaur、stick 则明显更低,把"机器如何理解语义"具象化了。
嵌入索引(Embedding Index)是如何构建的
本课所用的 embedding_index_3m.json 索引由 08-building-search-applications/scripts 下的一组 Python 脚本离线生成。完成本课不需要重跑这些脚本(索引已随仓库提供),但从源码理解其构造逻辑,对自建类似系统至关重要。
根据 scripts/README.md 及 transcript_*.py 系列脚本,流水线大致经历 5 步:
- 下载转录稿:用 transcript_download.py 抓取 AI Show 播放列表里每支视频的 YouTube 字幕。
- 抽取发言人:通过 OpenAI 函数调用从每支视频前 3 分钟的转录稿中尝试识别说话人姓名,结果写入
embedding_index_3m.json。 - 按 3 分钟分块:把完整转录切分为 3 分钟文本片段(segment),相邻片段之间保留约 20 个词的窗口重叠,避免把句子拦腰截断,同时为检索提供更完整的上下文(对应 transcript_enrich_bucket.py)。
- 生成摘要:每个片段交给 OpenAI Chat API 压缩为约 60 词的摘要,一并存入索引,用于结果的快速展示(对应 transcript_enrich_summaries.py)。
- 生成嵌入向量:将片段文本送入 OpenAI Embedding API,得到 1536 维向量,与片段元数据一起写入索引文件(对应 transcript_enrich_embeddings.py,内部使用
tiktoken做 token 预算控制、6 线程并发 +tenacity指数退避重试)。
从 embedding_index_3m.json 的真实内容可以确认每一条索引记录的结构:
{
"speaker": "Seth Juarez, Josh Lovejoy, Sarah Bird",
"title": "You're Not Solving the Problem You Think You're Solving",
"videoId": "-tJQm4mSh1s",
"start": "00:00:00",
"seconds": 0,
"summary": "Join Seth Juarez as he discusses ethical concerns with AI ...",
"ada_v2": [0.0043573323637247086, -0.02840915322303772, "..."]
}
字段含义:speaker 发言人、title 视频标题、videoId 视频 ID、start 片段起始时间、seconds 起始秒数(用于拼接 youtu.be/{videoId}?t={seconds})、summary 约 60 词摘要、ada_v2 该片段的 1536 维嵌入向量(由 text-embedding-ada-002 生成)。
生产环境中的向量数据库
为降低学习门槛,索引以 JSON 文件存放并载入 Pandas DataFrame;但文档明确指出,生产环境应把嵌入索引放进专用向量数据库,例如 Azure Cognitive Search、Redis、Pinecone、Weaviate 等——它们原生提供向量存储与 ANN 近似最近邻检索能力,避免全量线性扫描。
用余弦相似度检索"最近邻"
拿到文本嵌入后,检索的本质就是最近邻搜索(nearest neighbor search),本课采用 余弦相似度(Cosine Similarity) 作为度量,检索分三步:
- 用 OpenAI Embedding API 把查询文本向量化,得到查询向量;
- 计算查询向量与索引中每一个片段向量的余弦相似度(记住:索引里每行就是一条 YouTube 转录片段);
- 按相似度降序排序,Top-N 即与问题最相关的视频片段。
从数学上讲,余弦相似度度量的是两个向量在多维空间中夹角的余弦值。它的优势在于:即使两个文档因篇幅长度差异在欧氏距离上相距很远,只要方向接近(夹角小),余弦相似度依然很高——这正是处理变长文本时的关键特性。
动手实战:构建你的第一个语义视频搜索应用
文档说明该解决方案已在 Windows 11、macOS 与 Ubuntu 22.04 上使用 Python 3.10 及以上完成构建与测试。整套实践按"先开通 Azure OpenAI 服务,再跑 Notebook"两条主线展开。
第一步:创建 Azure OpenAI 服务(Azure Cloud Shell)
实践中需要一个 Azure 订阅。课程在 Azure Cloud Shell(选择 Bash 环境)中依次完成资源创建,以下命令摘自 08-building-search-applications/README.md 与 translations/cs/08-building-search-applications/README.md。
创建资源组(示例命名为 semantic-video-search,位于 East US;若更换区域请先核对模型的区域可用性):
az group create --name semantic-video-search --location eastus
创建 Azure OpenAI 服务资源:
az cognitiveservices account create --name semantic-video-openai --resource-group semantic-video-search \
--location eastus --kind OpenAI --sku s0
获取后续应用要用的 Endpoint 与 API Key:
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
部署 OpenAI Embedding 模型 text-embedding-ada-002(version 2):
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"
若你还想自己重跑 08-building-search-applications/scripts 的数据准备脚本,scripts/README.md 要求再部署一个 gpt-4o-mini 对话模型,并在环境中配置 AZURE_OPENAI_API_KEY、AZURE_OPENAI_ENDPOINT、AZURE_OPENAI_MODEL_DEPLOYMENT_NAME、GOOGLE_DEVELOPER_API_KEY 等变量;Windows / Linux / macOS 的变量注入方式在该 README 中有逐条说明。
第二步:在 Notebook 中实现语义检索
课程在 GitHub Codespaces 中打开 解决方案 Notebook 并依次执行即可:
- Azure OpenAI 版本:python/aoai-solution.ipynb
- OpenAI 直接调用版本:python/oai-solution.ipynb
- 若想先自行练习,可基于练习版 python/aoai-assignment.ipynb 或
oai-assignment.ipynb动手实现同样的检索函数。
运行时 Notebook 会弹出查询输入框:
下面按源码拆解 aoai-solution.ipynb 的四个关键环节。
① 初始化客户端与全局参数。 使用新版 openai SDK 的 AzureOpenAI 客户端,从 .env 读取三个变量:API Key、Endpoint 与 Embedding 部署名。检索阈值 SIMILARITIES_RESULTS_THRESHOLD = 0.75,数据源指向仓库根部的索引文件:
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"
注意:代码读取的是仓库根目录下的索引,即 embedding_index_3m.json;08-building-search-applications/scripts/embedding_index_3m.json 是同名备份副本。
② 把索引载入 Pandas DataFrame。 出于内存与简洁考虑,load_dataset 用 pd.read_json 读入后丢弃原始 text 列并用空串填充缺失值——检索阶段只需要元数据与向量列:
def load_dataset(source: str) -> pd.core.frame.DataFrame:
pd_vectors = pd.read_json(source)
return pd_vectors.drop(columns=["text"], errors="ignore").fillna("")
③ 实现余弦相似度与 get_videos 主检索函数。 Azure 版本为避免维度不一致先对向量做零填充,再按公式 a·b / (|a|·|b|) 计算。检索流程为:复制索引 → 为查询文本实时调用 Embedding API → 为每一行新增 similarity 列(查询向量与该行 ada_v2 向量的余弦相似度)→ 用阈值 0.75 过滤 → 按相似度降序取前 N 条:
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))
def get_videos(query: str, dataset: pd.core.frame.DataFrame, rows: int):
video_vectors = dataset.copy()
query_embeddings = client.embeddings.create(input=query, model=model).data[0].embedding
video_vectors["similarity"] = video_vectors["ada_v2"].apply(
lambda x: cosine_similarity(np.array(query_embeddings), np.array(x))
)
mask = video_vectors["similarity"] >= SIMILARITIES_RESULTS_THRESHOLD
video_vectors = video_vectors[mask].copy()
video_vectors = video_vectors.sort_values(by="similarity", ascending=False).head(rows)
return video_vectors.head(rows)
④ 展示结果。 display_results 把 videoId 与 seconds 拼成带时间戳的 https://youtu.be/{video_id}?t={seconds},并打印标题、前 15 个词的摘要、相似度与发言人;主循环持续读取用户输入直到键入 exit:
def display_results(videos: pd.core.frame.DataFrame, query: str):
def _gen_yt_url(video_id: str, seconds: int) -> str:
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']}")
运行依赖的环境变量与运行方式在 Notebook 首行 Markdown 中给出说明:Azure 版本需要 .env 中的 AZURE_OPENAI_API_KEY、AZURE_OPENAI_ENDPOINT 与部署名配置(示例见仓库 00-course-setup 的环境变量指引);纯 OpenAI 版本则只要求 .env 中的 OPENAI_API_KEY,模型名直接写 text-embedding-ada-002。运行时所需的 Python 依赖(openai、pandas、numpy、python-dotenv 等)汇总于 python/requirements.txt。
建议试跑的查询
课程为验证效果给出了 5 条建议查询,分别覆盖概念解释、框架提问与工具组合等不同语义形态:
- 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?
每条查询都会触发对索引的实时向量化与全量余弦相似度排序,观察返回片段的相似度分数与摘要,可以直观体会"关键词重合度低但语义相关"的文本也能被召回。
关键要点与生产化方向
- 语义搜索的价值在"理解意图":区别于字面匹配,Embedding 把文本变成可比较的高维向量,使"我的梦想之车"这类口语化查询也能命中正确结果。
- 索引质量决定召回上限:3 分钟分块 + 20 词重叠、60 词摘要、发言人元数据这三层处理,同时保证了检索粒度的可用性与结果的展示友好度。
- 检索度量公式即核心代码:
cosine_similarity = a·b / (|a||b|)配合 0.75 相似度阈值 + Top-N 排序,构成了本课搜索应用的全部"智能"。 - 从小 JSON 到向量数据库:学习阶段用 JSON + Pandas 足以跑通链路,生产化时应迁移到支持向量索引的数据库以获得可扩展的检索性能。
课程其余代码(JavaScript / TypeScript / .NET 等价实现)位于 08-building-search-applications 各语言子目录,可作对照。下一课将进入图像生成应用主题(见 09-building-image-applications/README.md)。
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 StartedRust0626
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

