首页
/ 基于 Embeddings 与余弦相似度构建语义搜索应用:generative-ai-for-beginners 第 08 课实战

基于 Embeddings 与余弦相似度构建语义搜索应用:generative-ai-for-beginners 第 08 课实战

2026-09-07 15:11:17作者:范垣楠Rhoda

本课属于本仓库「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} 这样的精确跳转链接,真正做到"秒级定位答案"。

下图是该语义检索的真实运行示例——注意结果链接中携带的时间戳,点击即可跳到视频里给出答案的位置:

对 “can you use rstudio with Azure ML?” 进行语义查询的返回结果,URL 中包含答案所在位置的时间戳

示例索引覆盖了微软 [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 进一步通过一组对照实验验证"语义相近→向量相近"这一核心特性:分别对 automobilevehicledinosaurstick 求嵌入并两两计算余弦相似度——automobile 与自身的相似度为 1.0,与 vehicle 很接近,与 dinosaurstick 则明显更低,把"机器如何理解语义"具象化了。

嵌入索引(Embedding Index)是如何构建的

本课所用的 embedding_index_3m.json 索引由 08-building-search-applications/scripts 下的一组 Python 脚本离线生成。完成本课不需要重跑这些脚本(索引已随仓库提供),但从源码理解其构造逻辑,对自建类似系统至关重要。

根据 scripts/README.mdtranscript_*.py 系列脚本,流水线大致经历 5 步:

  1. 下载转录稿:用 transcript_download.py 抓取 AI Show 播放列表里每支视频的 YouTube 字幕。
  2. 抽取发言人:通过 OpenAI 函数调用从每支视频前 3 分钟的转录稿中尝试识别说话人姓名,结果写入 embedding_index_3m.json
  3. 按 3 分钟分块:把完整转录切分为 3 分钟文本片段(segment),相邻片段之间保留约 20 个词的窗口重叠,避免把句子拦腰截断,同时为检索提供更完整的上下文(对应 transcript_enrich_bucket.py)。
  4. 生成摘要:每个片段交给 OpenAI Chat API 压缩为约 60 词的摘要,一并存入索引,用于结果的快速展示(对应 transcript_enrich_summaries.py)。
  5. 生成嵌入向量:将片段文本送入 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) 作为度量,检索分三步:

  1. 用 OpenAI Embedding API 把查询文本向量化,得到查询向量;
  2. 计算查询向量与索引中每一个片段向量的余弦相似度(记住:索引里每行就是一条 YouTube 转录片段);
  3. 按相似度降序排序,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.mdtranslations/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_KEYAZURE_OPENAI_ENDPOINTAZURE_OPENAI_MODEL_DEPLOYMENT_NAMEGOOGLE_DEVELOPER_API_KEY 等变量;Windows / Linux / macOS 的变量注入方式在该 README 中有逐条说明。

第二步:在 Notebook 中实现语义检索

课程在 GitHub Codespaces 中打开 解决方案 Notebook 并依次执行即可:

运行时 Notebook 会弹出查询输入框:

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.json08-building-search-applications/scripts/embedding_index_3m.json 是同名备份副本。

② 把索引载入 Pandas DataFrame。 出于内存与简洁考虑,load_datasetpd.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_resultsvideoIdseconds 拼成带时间戳的 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_KEYAZURE_OPENAI_ENDPOINT 与部署名配置(示例见仓库 00-course-setup 的环境变量指引);纯 OpenAI 版本则只要求 .env 中的 OPENAI_API_KEY,模型名直接写 text-embedding-ada-002。运行时所需的 Python 依赖(openaipandasnumpypython-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)。

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