用 Embeddings 构建语义搜索应用:generative-ai-for-beginners 第 8 课视频搜索实战
本课面向生成式 AI 初学者,以本仓库 generative-ai-for-beginners 第 8 课(源码对应目录 08-building-search-applications)为核心,讲解如何用**文本 Embeddings(向量)**替代关键词匹配,构建真正理解用户意图的语义搜索应用。文中会带你理解语义搜索与关键词搜索的本质差异、学会把视频转录稿切分并向量化、基于余弦相似度实现检索,最终基于 OpenAI Embedding 索引打造一个“输入问题 → 返回相关视频 + 精确到秒的定位链接”的实战应用。
本课配套索引 embedding_index_3m.json 覆盖了 Microsoft AI Show YouTube 频道截至 2023 年 10 月的全部视频转录稿,属于开源仓库提供的真实数据资产,因此你无需联网下载转录稿,也能完整跑通全文所述的搜索链路。读完你将掌握三类能力:区分语义检索与关键词检索、解释文本 Embeddings 的含义、独立构建基于 Embeddings 的数据检索程序。
为什么要构建搜索应用
大语言模型(LLM)的价值远不止聊天机器人和文本生成。在本课的叙事背景下,一家面向发展中国家学生提供免费 AI 教育的公益机构,拥有大量 YouTube 教学视频。学生希望通过输入一个自然语言问题(例如 “What are Jupyter Notebooks?” 或 “What is Azure ML?”)找到相关视频——更进一步,最好能直接给出答案出现在视频中的精确位置链接。
构建这样一个搜索应用,是掌握 Embeddings 检索范式的最佳练兵场:它不要求你训练模型,只要求你懂得如何把文本“翻译”成机器可计算的数值向量,再做相似度比对。这也是后续构建 RAG 应用的基础能力。
语义搜索 vs 关键词搜索
语义搜索(semantic search)利用查询词中词语的语义/含义来返回相关结果。文档给出的例子非常直观:假如你想买车,搜索 “my dream car”,语义搜索会理解你并不是在“梦见”一辆车,而是想购买一辆“理想之车”,从而返回真正相关的购车内容;而**关键词搜索(keyword search)**只会字面匹配含有 “dream” 和 “car” 的文本,往往返回不相关内容。
这正是本应用选择 Embeddings 的原因:学生的问题和视频转录稿几乎不可能字面一致,只有理解语义,才能跨表达方式完成匹配。
什么是文本 Embeddings
文本 Embeddings 是自然语言处理中的一种文本数值化表示技术,它把文本编码为机器易于理解、且携带语义信息的数字向量。可以用多种模型生成 Embeddings,本课聚焦 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, ...]
语义上相近的句子,其向量在多维空间中夹角更小、距离更近——这正是后面用余弦相似度做检索的物理基础。
索引是怎么构建的:转录稿 → 分片 → 摘要 → 向量
本课使用的 Embedding 索引 embedding_index_3m.json(位于 08-building-search-applications/embedding_index_3m.json,scripts 子目录也有一份副本)由一组 Python 脚本流水线生成。脚本与完整使用说明在 scripts/README.md。完成本课任务无需自行运行这些脚本(索引已随仓库提供),但理解流水线能让你透彻掌握向量索引的数据结构。整条流水线分为五步:
- 下载转录稿:通过 YouTube API 下载 AI Show 播放列表中每个视频的字幕转录。对应实现 transcript_download.py,依赖
GOOGLE_DEVELOPER_API_KEY环境变量与youtube-transcript-api库。 - 提取演讲者姓名:用 OpenAI Function Calling,从前约 3 分钟转录中提取演讲者名单,写入索引的
speaker字段。对应实现 transcript_enrich_speaker.py,内部通过get_speaker_name函数定义约束模型输出结构化结果。 - 按 3 分钟切分文本段:转录文本被切分为约 3 分钟一段的文本片段,相邻片段间保留约 20 个词的重叠区域,既保证 Embedding 不因边界截断而语义残缺,又为检索提供更好的上下文衔接。通用切分逻辑在 transcript_enrich_bucket.py:该脚本默认
SEGMENT_LENGTH_MINUTES、PERCENTAGE_OVERLAP等参数可调,并预留 token 预算给后续摘要请求;切分完成后,每个片段还带有start(形如00:00:00的时间戳)与seconds(绝对秒数)字段。 - 生成 60 词摘要:把每段文本送入 OpenAI Chat API,压缩为约 60 词的摘要,存入
summary字段。对应实现 transcript_enrich_summaries.py。 - 向量化:把每段文本送入 OpenAI Embedding API,得到 1536 维向量,连同片段元数据写入
embedding_index_3m.json。对应实现 transcript_enrich_embeddings.py:内部用tiktoken.get_encoding("cl100k_base")统计 token,超过上限(约 8191 token,见脚本中len(tokenizer.encode(text)) > 8191的判断)的片段会被跳过,并借助tenacity指数退避策略应对限流。
因此,索引中每条记录的核心结构是(以仓库内实际 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 ...",
"ada_v2": [0.0043573323637247086, -0.02840915322303772, "...1536 个浮点数..."]
}
字段含义分别是:videoId(用于拼 YouTube 链接)、start/seconds(定位到视频中的精确时刻)、summary(60 词摘要,用于结果展示)、ada_v2(文本段的 1536 维 Embedding 向量)、speaker/title(展示用元数据)。
从 JSON 索引到向量数据库
为降低教学门槛,索引以 JSON 文件存储、用 Pandas DataFrame 载入内存。生产环境中应改用真正的向量数据库承载,例如 Azure Cognitive Search、Redis、Pinecone、Weaviate 等。这类系统专职解决大规模向量的近邻检索、持久化与高并发问题,本课的 JSON+Pandas 方案则是理解其核心检索逻辑的最小可运行形态。
理解余弦相似度
有了文本向量,下一步就是“如何找与查询最相似的向量”。这里使用的度量是余弦相似度(cosine similarity),也常被称为近邻检索(nearest neighbor search)。检索流程分四步:
- 用 OpenAI Embedding API 把用户查询文本向量化;
- 逐条计算查询向量与索引中每个文本段向量的余弦相似度;
- 按相似度从高到低排序;
- 相似度最高的文本段即与查询最相关。
从数学上看,余弦相似度衡量的是多维空间中两个向量的夹角余弦值。它的好处在于:即使两个文档因长度不同而在欧氏距离上相距很远,只要方向(语义)接近,夹角依然很小、余弦相似度依然很高。向量点积除以模长即为余弦相似度,即 cos(a,b) = a·b / (|a|·|b|)。
前置准备:创建 Azure OpenAI 服务并部署 Embedding 模型
应用依赖 Azure OpenAI 服务,需要 Azure 订阅。以下是任务要求的资源创建过程,均在 **Azure Cloud Shell(选择 Bash 环境)**中完成:
1. 创建资源组(文档按 East US 区域的 semantic-video-search 命名;若修改地域,请对照模型可用性表确认支持):
az group create --name semantic-video-search --location eastus
2. 创建 Azure OpenAI 服务资源:
az cognitiveservices account create --name semantic-video-openai --resource-group semantic-video-search \
--location eastus --kind OpenAI --sku s0
3. 获取 Endpoint 与密钥(应用配置与运行都需要):
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
4. 部署 Embedding 模型 text-embedding-ada-002(版本 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"
注意:文档与仓库中 scripts/README.md 同时要求部署 chat 模型 gpt-4o-mini(供摘要/函数调用流水线使用);本课搜索 notebook 只需 Embedding 模型。实际运行时需通过环境变量注入密钥、Endpoint 与部署名(notebook 读取 AZURE_OPENAI_API_KEY、AZURE_OPENAI_ENDPOINT、AZURE_OPENAI_EMBEDDINGS_DEPLOYMENT,并在 .env 中配置,供 aoai-solution.ipynb 加载)。若你没有 Azure 环境,仓库同时也提供了基于纯 OpenAI 的练习:oai-assignment.ipynb、aoai-assignment.ipynb。
核心实现:从向量化查询到结果展示
搜索应用主逻辑全部集中在方案 notebook中,以下逐段拆解其核心代码,这也是本文的可直接运行的核心示例。
① 初始化客户端并加载索引。通过环境变量连接 Azure OpenAI,将 embedding_index_3m.json 载入 DataFrame,并丢弃体积较大、检索用不到的 text 原始文本列(errors="ignore" 保证兼容):
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"
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 内部按“复制索引 → 计算查询向量 → 逐行计算相似度 → 按 0.75 阈值过滤 → 排序取前 N 条”的顺序执行,返回 Top 5 结果:
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, dataset, rows):
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)
代码细节值得注意:cosine_similarity 先用 np.pad 对齐两个向量长度再计算 a·b/(|a||b|),是标准余弦相似度的向量化实现;0.75 的相似度阈值用于剔除明显不相关片段,属于可按数据集质量调节的超参数。
③ 展示结果并生成带时间戳的 YouTube 链接。这是应用价值的体现——把命中片段的 videoId 与 seconds 拼接成 https://youtu.be/{videoId}?t={seconds} 链接,并同时打印标题、摘要前 15 词、相似度与演讲者:
def display_results(videos, query):
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']}")
④ 主循环:加载索引,反复接收用户查询,直到输入 exit 退出:
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 后会出现查询输入框:
建议尝试的查询
文档和 notebook 提供了若干验证性查询,可直接复制体验效果:
- 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?
输入 “What is Azure Machine Learning?” 这类问题后,程序会返回相似度最高的若干视频段,并给出如 https://youtu.be/{videoId}?t={秒数} 的定位链接,点击即跳转到答案出现的位置。
源码印证:仓库中的工程化参考
除 Python notebook 外,本课还提供了多种工程形态的实现,可作为将该 demo 落地的参考:
- 前端 Web 应用:JavaScript 版本 js-githubmodels/app.js,TypeScript 版本 search-app/src/main.ts,封装了相同的“向量化查询 → 余弦相似度排序”逻辑并对外提供界面;
- 转录稿流水线脚本及依赖清单见 scripts/requirements.txt(含
openai、tiktoken、tenacity、youtube-transcript-api等); - 搜索 notebook 的依赖与版本约束见 python/requirements.txt。
小结与下一步
本课完整覆盖了 Embeddings 语义搜索的“为什么、是什么、怎么做”:从语义 vs 关键词检索的动机,到 1536 维向量与余弦相似度的原理,再到 JSON 索引构建流水线和可运行的检索 notebook,最后落到真实应用上——学生输入问题即可获得带时间戳定位的视频答案链接。整套代码可直接在 GitHub Codespaces 中打开 aoai-solution.ipynb 按提示运行。
本课验证的是“文本向量化 + 相似度检索”这条基线范式;进入第 9 课,我们将探讨如何构建图像生成应用,把生成式 AI 从理解文本扩展到生成视觉内容。
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 StartedRust0629
MiniCPM5-2BMiniCPM5-2B 是一款面向端侧、本地部署和资源受限场景的 2B 稠密 Transformer,能够达到同尺寸开源模型 SOTA 水平。Markdown00
GLM-5.3GLM-5.3 与 GLM-5.2 使用相同的基座模型——所有提升均来自后训练。与 GLM-5.2 相比,它在复杂编程和长程任务上的表现显著提升。Jinja00
HivisionIDPhotos⚡️HivisionIDPhotos: a lightweight and efficient AI ID photos tools. 一个轻量级的AI证件照制作算法。Python07
DragonOSDragonOS is an operating system developed from scratch using Rust, with Linux compatibility. It is designed for **Serverless** scenarios. 使用Rust从0自研内核,具有Linux兼容性的操作系统,面向云计算Serverless场景而设计。Rust00
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00

