跳至主要内容

混合搜索:在 MongoDB 中结合向量与关键词查询

了解混合搜索,以及如何在 MongoDB 中使用它。
· 6分钟 阅读

使用 AI 探索

有时,仅靠简单的全文检索或单独的向量搜索并不足以在数据库中进行合适的查询并得到您想要的结果。两者结合在开发者处理大量多模态、非结构化数据且同时受益于两种搜索类型时表现极佳。这被称为混合搜索,它为开发者提供了一个解决棘手问题的绝佳方案。

什么是混合搜索?

要正确理解混合搜索,我们需要先了解什么是全文检索。

全文检索是一种将查询中的字面词语与文档进行匹配的搜索方式。这类传统搜索其实是许多开发者非常熟悉的。

例如,如果您搜索“有户外座位的可爱咖啡馆”,搜索引擎会在数据库中查找这些精确的词语。简单来说,全文检索非常精确高效,但当您希望在搜索同义词、改写表达,甚至查询里有拼写错误时得到同样结果,它的表现就不够理想。

另一方面,向量搜索会将所有数据转换为数字,或称为向量嵌入。因此,向量搜索并不是匹配精确词语,而是将您的查询语义与数据库中存储的文档进行比较。

搜索“有户外座位的可爱咖啡馆”可能会返回“在室外享用糕点和咖啡”,即使它们并未使用完全相同的词语。向量搜索不仅具备语义能力,也非常灵活,但有时可能会根据查询返回过于宽泛的结果。

那么,混合搜索如何发挥作用呢?它将全文检索与向量搜索结合起来。这意味着开发者既可以利用向量的语义智能,又能保留全文检索非常精确的过滤特性。可谓两全其美。这对于处理大型非结构化数据集的开发者极为有用。

为什么混合搜索很重要

混合搜索在许多实际应用中都有益处,包括但不限于电商、医疗,甚至招聘。

在电商场景中,设想您访问喜欢的网站并搜索“100 美元以下的舒适办公椅”。使用向量搜索,您可以看到语义相近的商品(如人体工学办公椅),而全文检索会聚焦“椅子”这个词,并帮助落实价格范围。

在招聘中,如果招聘人员在简历中搜索“有 NLP 经验的工程师”,混合搜索既能捕捉到“自然语言处理(natural language processing)”,也能匹配到精确的关键词“engineer”。

这意味着,与单独使用任一方法相比,您能获得更相关、更可靠的结果。

MongoDB 中的混合搜索

下面我们来介绍如何在 MongoDB Atlas 中进行混合搜索。要顺利完成本教程,您需要准备以下先决条件:

  1. 一个MongoDB Atlas 账户
  2. 一个免费层 MongoDB Atlas 集群
  3. mongosh 已在您的终端中下载

在本教程中,我们将聚焦 sample_mflix 数据库中的 embedded movies 集合,并参考此$rankFusion 混合搜索教程,但会有少量变通处理。

尽管新的$rankFusion 运算符已在 8.1+ 版本的集群中提供,并能让在 MongoDB 中进行混合搜索变得异常简单,但它仍处于公开预览阶段,且免费层集群会自动使用 8.0 版本。因此,我们将按照该教程进行,稍作调整,顺利完成。

配置您的集群

在配置集群时,请确保下载了 sample_mflix 数据集。整个教程都会使用该数据集。我们将关注 sample_mflix.embedded_movies 集合。

快速检查您是否已下载 mongosh。

mongosh --version

本教程中,我使用的是 2.4.2。

现在,连接到您的 MongoDB Atlas 集群:

mongosh "mongodb+srv://<yourclusterhere>.mongodb.net/" --apiVersion 1 --username <yourusername> 

系统会提示输入密码,连接成功后,界面如下所示:

现在我们可以连接到数据库。运行命令:

use sample_mflix

输出将是:

switched to db sample_mflix

创建索引

接下来我们创建本教程需要的两个索引:一个向量索引,一个全文检索索引。请注意,我们是在 embedded_movies 集合上创建这两个索引。

向量索引:

db.embedded_movies.createSearchIndex(
  "hybrid-vector-search",
  "vectorSearch",
  {
    fields: [
      { type: "vector", path: "plot_embedding_voyage_3_large", numDimensions: 2048, similarity: "dotProduct" }
    ]
  }
)

全文检索索引:

db.embedded_movies.createSearchIndex(
  "hybrid-full-text-search",
  "search",
  { mappings: { dynamic: true } }
)

在 MongoDB Atlas 集群中再次确认索引已就绪:

查询我们的数据库

现在,我们希望在 sample_mflix.embedded_movies 集合中,针对 plot_embedding_voyage_3_large 字段查询“star wars”。

按照教程进行,由于我们不需要使用任何 API,可以将需要的所有嵌入向量保存到一个名为 query_embeddings.js 的单独文件中。

现在我们可以加载嵌入向量用于查询。运行以下命令:

load('/Users/<PATH NAME>/query_embeddings.js')
const queryVec = STAR_WARS_EMBEDDING

为确保嵌入向量加载成功,运行命令:

STAR_WARS_EMBEDDING.length

输出为 2048 即为正确。

聚合管道

现在我们可以为混合搜索创建一个聚合管道。请注意,这里存在一些限制与原因,决定了我们必须以这种方式来组织聚合管道。

如上所述,首先,虽然 MongoDB Atlas 中有 $rankFusion,但它目前处于公开预览阶段,且需要 8.0 或更高版本的集群。在免费层集群中,您目前无法使用 $rankFusion。这会带来一些复杂性,因为 MongoDB Atlas 不会为我们原生融合文本与向量的排序分数。这意味着我们必须自己进行融合,即对两份排名列表进行加权混合。

聚合管道中各阶段的位置也有严格要求:

  • $search 必须是管道的第一个阶段。
  • $vectorSearch 必须位于管道的起始位置,且不能处于 $facet 内。

一种变通方法?由于子管道被视为独立的管道,$vectorSearch 可以作为 $unionWith 中子管道的第一阶段。

因此,我们需要先从 $search 开始以保留全文检索的分数,然后使用 $unionWith 启动第二个以 $vectorSearch 开头的小型管道,从而同样保留其分数。

随后我们需要按 _id 分组,以避免重复,并从 $search 与 $vectorSearch 的结果中各取最优。

完成后,我们需要计算混合搜索分数,即取 $search 的最佳分数、$vectorSearch 的最佳分数,乘以我们选择的权重(我选择 0.35),然后据此排序。

权重决定了最终结果中,向量搜索相对于全文检索的影响力大小。

我们的混合公式是:hybrid_score = text_score + (vector_score x weight)。因此,更高的权重(如 0.7–1.0)意味着向量搜索占主导,从而优先考虑语义相似度。

相反,更低的权重(0.1–0.3)意味着全文检索占主导,优先考虑精确的关键词匹配。

将以下聚合管道复制并粘贴到您的终端窗口:

const WEIGHT = 0.35;

db.embedded_movies.aggregate([
// Stage 1: full-text search
  { $search: { index: "hybrid-full-text-search", text: { query: "star wars", path: ["title","plot"] } } },
  { $set: { t: { $meta: "searchScore" } } },
  { $project: { _id: 1, title: 1, plot: 1, t: 1 } },
// Stage 2: Union with vector search
  { $unionWith: {
      coll: "embedded_movies",
      pipeline: [
// Vector search subpipeline
        { $vectorSearch: {
            index: "hybrid-vector-search",
            path: "plot_embedding_voyage_3_large",
            queryVector: queryVec,
            numCandidates: 200,
            limit: 150
        }},
        { $project: { _id: 1, title: 1, plot: 1, v: { $meta: "vectorSearchScore" } } }
      ]
  }},
// Stage 3: Combine and rank results
  { $group: { _id: "$_id", title: { $first: "$title" }, plot: { $first: "$plot" }, t: { $max: "$t" }, v: { $max: "$v" } } },
  { $set: { h: { $add: [ { $ifNull: ["$t", 0] }, { $multiply: [ { $ifNull: ["$v", 0] }, WEIGHT ] } ] } } },
  { $sort: { h: -1 } },
  { $limit: 20 }
]).toArray()

我们的输出如下:

  {
    _id: ObjectId('573a139af29313caabcf124d'),
    title: 'Star Wars: Episode III - Revenge of the Sith',
    plot: 'As the Clone Wars near an end, the Sith Lord Darth Sidious steps out of the shadows, at which time Anakin succumbs to his emotions, becoming Darth Vader and putting his relationships with Obi-Wan and Padme at risk.',
    t: 4.957413673400879,
    v: 0.756283700466156,
    h: 5.2221129685640335
  },
  {
    _id: ObjectId('573a13a6f29313caabd17d08'),
    title: 'Star',
    plot: 'The Driver now carries an arrogant rock star who is visiting a major city (not Pittsburgh as earlier believed). Played by Madonna, this title character wants to get away from her bodyguards...',
    t: 5.151784420013428,
    v: null,
    h: 5.151784420013428
  },
  {
    _id: ObjectId('573a1397f29313caabce8cdb'),
    title: 'Star Wars: Episode VI - Return of the Jedi',
    plot: 'After rescuing Han Solo from the palace of Jabba the Hutt, the rebels attempt to destroy the second Death Star, while Luke struggles to make Vader return from the dark side of the Force.',
    t: 4.726564407348633,
    v: 0.7782549858093262,
    h: 4.998953652381897
  },
  {
    _id: ObjectId('573a13aef29313caabd2da15'),
    title: 'Star Runner',
    plot: 'Get ready for the ultimate martial arts competition, where anything goes and lives are bought and sold. Tank is the celebrated Champion Star Runner and is deemed invincible among the ...',
    t: 4.702453136444092,
    v: 0.696668267250061,
    h: 4.9462870299816135
  },
  {
    _id: ObjectId('573a13c0f29313caabd62f62'),
    title: 'Star Wars: The Clone Wars',
    plot: 'Anakin Skywalker and Ahsoka Tano must rescue the kidnapped son of Jabba the Hutt, but political intrigue complicates their mission.',
    t: 4.537533760070801,
    v: 0.7561323642730713,
    h: 4.802180087566375
  },
  {
    _id: ObjectId('573a1397f29313caabce6f53'),
    title: 'Message from Space',
    plot: 'In this Star Wars take-off, the peaceful planet of Jillucia has been nearly wiped out by the Gavanas, whose leader takes orders from his mother (played a comic actor in drag) rather than ...',
    t: 4.401333808898926,
    v: 0.7913960218429565,
    h: 4.678322416543961
  },
  {
    _id: ObjectId('573a139df29313caabcfa90b'),
    title: 'Message from Space',
    plot: 'In this Star Wars take-off, the peaceful planet of Jillucia has been nearly wiped out by the Gavanas, whose leader takes orders from his mother (played a comic actor in drag) rather than ...',
    t: 4.401333808898926,
    v: 0.7913960218429565,
    h: 4.678322416543961
  },
  {
    _id: ObjectId('573a1398f29313caabce9851'),
    title: 'Gymkata',
    plot: 'Johnathan Cabot is a champion gymnast. In the tiny, yet savage, country of Parmistan, there is a perfect spot for a "star wars" site. For the US to get this site, they must compete in the ...',
    t: 4.284864902496338,
    v: 0.7193170189857483,
    h: 4.53662585914135
  },
  {
    _id: ObjectId('573a1397f29313caabce68f6'),
    title: 'Star Wars: Episode IV - A New Hope',
    plot: "Luke Skywalker joins forces with a Jedi Knight, a cocky pilot, a wookiee and two droids to save the universe from the Empire's world-destroying battle-station, while also attempting to rescue Princess Leia from the evil Darth Vader.",
    t: 2.9629626274108887,
    v: 0.7987099885940552,
    h: 3.242511123418808
  },
  {
    _id: ObjectId('573a139af29313caabcf0f5f'),
    title: 'Star Wars: Episode I - The Phantom Menace',
    plot: 'Two Jedi Knights escape a hostile blockade to find allies and come across a young boy who may bring balance to the Force, but the long dormant Sith resurface to reclaim their old glory.',
    t: 2.9629626274108887,
    v: 0.7771433591842651,
    h: 3.2349628031253816
  },
  {
    _id: ObjectId('573a1397f29313caabce77d9'),
    title: 'Star Wars: Episode V - The Empire Strikes Back',
    plot: 'After the rebels have been brutally overpowered by the Empire on their newly established base, Luke Skywalker takes advanced Jedi training with Master Yoda, while his friends are pursued by Darth Vader as part of his plan to capture Luke.',
    t: 2.7174296379089355,
    v: 0.7810181975364685,
    h: 2.9907860070466996
  },
  {
    _id: ObjectId('573a139af29313caabcf1258'),
    title: 'Star Wars: Episode II - Attack of the Clones',
    plot: 'Ten years after initially meeting, Anakin Skywalker shares a forbidden romance with Padmè, while Obi-Wan investigates an assassination attempt on the Senator and discovers a secret clone army crafted for the Jedi.',
    t: 2.7174296379089355,
    v: 0.7400004267692566,
    h: 2.9764297872781755
  },
  {
    _id: ObjectId('573a1394f29313caabcdf65b'),
    title: 'Ugetsu',
    plot: 'A fantastic tale of war, love, family and ambition set in the midst of the Japanese Civil Wars of the sixteenth century.',
    t: 2.858368396759033,
    v: null,
    h: 2.858368396759033
  },
  {
    _id: ObjectId('573a13a4f29313caabd1137f'),
    title: 'S1m0ne',
    plot: "A producer's film is endangered when his star walks off, so he decides to digitally create an actress to substitute for the star, becoming an overnight sensation that everyone thinks is a real person.",
    t: 2.842216730117798,
    v: null,
    h: 2.842216730117798
  },
  {
    _id: ObjectId('573a13b8f29313caabd4c3c3'),
    title: 'Star Trek',
    plot: "The brash James T. Kirk tries to live up to his father's legacy with Mr. Spock keeping him in check as a vengeful, time-traveling Romulan creates black holes to destroy the Federation one planet at a time.",
    t: 2.577816963195801,
    v: 0.7295459508895874,
    h: 2.8331580460071564
  },
  {
    _id: ObjectId('573a13b5f29313caabd42e99'),
    title: 'Sars Wars',
    plot: "The fourth generation of the virus SARS is found in Africa! It's more dangerous and causes the patients to transform into bloodthirsty zombies. The virus quickly lands to Thailand, Dr. ...",
    t: 2.826810598373413,
    v: null,
    h: 2.826810598373413
  },
  {
    _id: ObjectId('573a13b5f29313caabd42e1b'),
    title: 'Sars Wars',
    plot: "The fourth generation of the virus SARS is found in Africa! It's more dangerous and causes the patients to transform into bloodthirsty zombies. The virus quickly lands to Thailand, Dr. ...",
    t: 2.826810598373413,
    v: null,
    h: 2.826810598373413
  },

可以看到,既有与查询“star wars”完全一致的结果,也有明显只是语义相关的结果。

结语

恭喜您!您已成功在 MongoDB 中完成混合搜索。虽然 $rankFusion 将简化这一过程,但在该运算符仍处于公开预览阶段时,此方法提供了一种可行的变通方案。通过本教程,我们已将全文检索与向量搜索合并进同一管道,从而为给定查询检索到最优结果。关于 MongoDB 中混合搜索的更多信息,请参阅MongoDB 文档。如果您仍在熟悉 MongoDB,推荐学习Python 中的 MongoDB 入门课程。

MongoDB 混合搜索常见问题

进行混合搜索是否需要 MongoDB 8.1 和 `$rankFusion`

不需要。在 8.0 上,您实际上可以自行融合分数。方法是分别在两个管道中运行 $search 与 $vectorSearch,并使用 $unionWith + $group 再配合加权公式进行合并。在 8.1+ 上,$rankFusion 会替您完成这一步。

什么是混合搜索?

它是将全文检索(精确词语)与向量搜索(语义含义)结合起来,从给定查询中获得尽可能最优的结果。

我应该在何时使用混合搜索?

当您的文本存在多样表达(同义词、改写、拼写错误),但仍希望保留对精确术语的匹配时,应使用混合搜索。

我该如何选择要使用的权重?

最佳实践是从 0.2–0.5 左右开始。较低的权重更偏向全文检索的影响,较高的权重更侧重语义影响。重要的是在测试并查看结果后对权重进行调优。

混合搜索能用于图像和音频数据吗?

可以!只要您的数据可以转换为向量嵌入,并且您可以与任何特定措辞约束相结合,就能在数据集上执行混合搜索。

主题