⚠️ 此页面为自动翻译,翻译可能不完美。
blog-post

想找袜子,结果搜出裤子:用同一份商品目录比较六种搜索方式

author image
View as markdown

“世界变了。我能从水中感觉到,从大地中感觉到,也能从空气中嗅到。”

——凯兰崔尔,《指环王:护戒使者》

搜索也变了。过去很长一段时间,给商品目录做搜索,首先考虑的都是文字:怎么处理关键词、怎么容忍拼写错误、怎么给结果排序。这些问题现在依然存在。只不过,除了全文搜索,我们又有了向量搜索、混合搜索、图片搜索,以及能直接回答问题、而不只是列出一堆商品的 AI。

这些方法听起来都不错,直到你拿真实的搜索词试一试。比如有人搜索 merino wool socks(美利奴羊毛袜),向量搜索倒是理解了“美利奴羊毛”,但给出的前四件商品却是裤子、夹克、另一条裤子和 T 恤。袜子直到第五名才出现。

所以,究竟哪种搜索更好?我们不想光靠嘴争论,干脆让它们面对同一份商品目录,看看各自会交出什么答卷。

于是,我们做了 Manticore Search Playground 。它有七个标签页:可以按关键词、语义、照片或地理位置搜索,可以并排比较结果,也可以直接向 AI 提问。每组搜索结果下面还会显示对应的 SQL,以及等效的 HTTP JSON API 请求。

Manticore Search Playground

这不只是一个摆着漂亮商品卡片的演示页面,而是一个开源的小应用 。你可以用 Docker Compose 在本地运行,看看里面到底是怎么实现的。全文、向量、混合和地理位置搜索共用一张商品表。搜索照片则另外用了一张表和一个向量化服务;AI 部分使用 CALL CHAT(Manticore 的一条 SQL 命令)。

下面我们会用具体的搜索词、一些出人意料的结果,以及相应的 SQL,逐个看看这些方法。文中的结果数量和耗时,来自 2026 年 9 月的公开版 Playground。商品目录、模型或应用更新后,数字可能会有所变化。

我们到底在比较什么?

我们从 Google 的 ConvApparel 数据集中取了 82,524 件商品。这个数据集围绕人们如何挑选服装展开,相关对话中的商品推荐包含名称、描述、属性和照片。

七个标签页使用的都是这些数据:Full-text、Vector、Hybrid、Image、Geo、Compare 和 Ask AI。每次搜索,Playground 都会同时展示 SQL 和 curl 命令,你可以拿到自己的 Manticore 实例上运行。

这有什么意义?因为我们经常看到有人用两个不同的数据集、两个不同的演示来比较搜索引擎。这里商品目录始终不变,改变的只有搜索方式,区别自然一目了然。

大多数搜索模式只需要下面这一张表:

CREATE TABLE convapparel_products (
    id bigint,
    item_id text,
    title text,
    description text,
    features text,
    image_url text,
    category string indexed attribute,
    source text,
    categories json,
    lat float,
    lon float,
    embedding_vector float_vector knn_type='hnsw' hnsw_similarity='COSINE'
        model_name='onnx-models/all-MiniLM-L12-v2-onnx' from='title,description,features'
) min_infix_len='2'

如果你也打算用 Manticore 开发应用,有两个地方值得注意:

  • min_infix_len='2' 允许从两个字符开始匹配单词的一部分,适合做搜索建议和自动补全。
  • model_name 配合 from='title,description,features',就能启用自动生成向量(embeddings) 。添加商品时,Manticore 根据文本字段生成向量;按语义搜索时,又用同一个模型把查询转成向量。这一步不需要另起一个服务。

照片的处理方式有点不一样,后面再说。

1. 全文搜索:两个拼写错误,零条结果

先从熟悉的全文搜索说起。假设顾客想输入 leather jacket(皮夹克),却不小心打成了 lether jaket 。两个单词,两个拼写错误。对于网店来说,再正常不过了。

关掉 Typo tolerance(拼写容错),Playground 一件商品也找不到。

关闭拼写容错后,lether jaket 搜不到任何商品

再把拼写容错打开,结果突然变成了 1,027 件商品。排在最前面的正是皮夹克,和我们预想的一样。

搜索 lether jaket 时找到皮夹克,并显示分类数量、SQL 和 curl 命令

启用拼写容错时,执行的是这条查询:

SELECT id, title, description, features, category, image_url, weight() AS text_score
FROM convapparel_products
WHERE MATCH('lether jaket')
LIMIT 12
OPTION fuzzy=1
FACET categories ORDER BY COUNT(*) DESC

这里发生了什么?MATCH() 负责搜索文本字段,weight() 给出相关性评分。默认排序算法是 proximity_bm25,既考虑 BM25,也考虑词语之间的距离。

不过,这个例子的关键是 OPTION fuzzy=1 。它能让搜索匹配拼写相近的单词。去掉它:零条结果。加回来:皮夹克就出现了。

如果你准备在自己的应用里加入拼写容错,有两个用户体验方面的细节值得注意。

  • 告诉用户,你修改了他的搜索词。 Playground 会调用 CALL QSUGGEST ,并显示 Showing results for leather jacket(显示 leather jacket 的搜索结果)。如果有一天搜索引擎猜错了,用户至少知道这些奇怪的结果从何而来。
  • 开启拼写容错时,别指望用户会写完整的 Manticore 全文搜索表达式。 大家输入的通常只是普通短语,不是精心构造的 MATCH() 表达式。在模糊搜索模式下,运算符字符会被转义(用于短语搜索的引号除外)。

明明找到 1,027 件商品,分类数量加起来却有 1,127 件。怎么回事?

注意 SQL 最后的 FACET categories。除了返回商品,它还会返回各分类的结果数量 。这次搜索得到的是:

Outerwear — 960、Footwear — 58、Tops — 55、Bottoms — 54。

把它们加起来是 1,127,可搜索结果明明只有 1,027 件。

各分类数量之和竟然超过搜索结果总数

别急,不是加法算错了。一件商品可以属于多个分类,所以它可能在分类统计中出现好几次,但在商品总数里只算一次。

我们是这样处理的:

  1. 把分类存成 JSON 数组。例如,一件商品可以同时属于 ["outerwear", "tops"]。
  2. 用 categories IN ('outerwear', 'tops') 筛选。只要属于所选分类中的任意一个,商品就符合条件。
  3. 用单独的 COUNT(*) 统计商品总数,而不是把各个 facet 的数量相加,避免重复计算。

为什么输入 int 时,自动补全不应该推荐 print

还有一种大家都很熟悉的功能:搜索框里的输入提示。配置得不好,它很快就会让人烦躁。

打开 Full-text 标签页,关闭 Search as you type(否则搜索结果会盖住提示),然后输入 linen sh。单词还没打完,就会出现 linen shirt、linen shoes 和 linen shorts。

输入 linen sh 后出现的自动补全建议

实现它只需要一条查询:

CALL AUTOCOMPLETE('linen sh', 'convapparel_products', 0 AS fuzziness, 1 AS append, 0 AS prepend)

但这些参数可不是随便填的。

  • 把完整输入交给 Manticore。 比如 linen sh,CALL AUTOCOMPLETE 会保留前面的 linen,只补全最后的 sh。返回的每一条结果都可以直接用作搜索建议。
  • 不匹配单词中间的部分。 设置 0 AS prepend,就不会在查询左边加 *。否则输入 int 时可能出现 print。从技术上说确实匹配,但一个刚开始打字的人,多半不是想找这个。
  • 不在补全建议里改正拼写错误。 0 AS fuzziness 表示只补全用户实际输入的内容。拼写纠错留到正式搜索时处理,而且会明确告诉用户改了什么。

CALL AUTOCOMPLETE 的全部参数都可以在文档中查到。

2. 向量搜索:不知道商品叫什么,也能找到想要的东西

接下来换一种问题,也换一种搜索方式。有人没输入具体商品名,而是描述了自己的用途:outfit for a job interview ,也就是想找一套面试穿的衣服。

全文搜索在这里就有点吃力。默认情况下,它用 AND 连接各个单词(当然可以调整,但这里用的是默认设置),而目录里没有商品同时包含 outfit、job 和 interview。所以结果是 零条。

可切到 Vector 标签页,就出现了商务套装和搭配西装外套的服装。明明用户根本没输入 business suit 或 blazer。

SELECT id, title, description, features, category, image_url, knn_dist() AS distance
FROM convapparel_products
WHERE knn(embedding_vector, 100, 'outfit for a job interview')
LIMIT 12

注意,SQL 里没有预先算好的向量,传进去的就是普通文本。Manticore 使用和处理商品时相同的模型,把查询转成向量,再通过 HNSW 索引 找出最近邻。模型能够把这个问题和 business suit、office、blazer 等商品描述联系起来。

有时候,它甚至能顺手应付拼写错误。例如搜索 warm waterprof jacket ,依然能找到防水夹克,虽然我们并没有专门开启拼写纠错。

听起来挺完美,对吧?可别忘了开头那双袜子。

搜索 merino wool socks 时,前四个结果是美利奴羊毛裤、夹克、另一条裤子和 T 恤。袜子排第五。

向量搜索抓住了 merino wool,却没给 socks 足够的权重。颜色、尺码或者具体商品类型也可能遭遇同样的问题:语义上挺接近,找到的却不是你想买的东西。

3. 混合搜索:关键词和语义都能用,为什么非要二选一?

看完袜子的例子,一个很自然的问题就来了:为什么不把两种方法结合起来?

既然有全文搜索和向量搜索,为什么不两个一起用?

这就是混合搜索做的事。打开 Hybrid 标签页里的 merino wool socks 。这次袜子排在第一位,后面才是其他美利奴羊毛商品。

对应的 SQL:

SELECT id, title, description, features, category, image_url,
    weight() AS text_score, knn_dist() AS distance
FROM convapparel_products
WHERE MATCH('merino wool socks')
  AND knn(embedding_vector, 100, 'merino wool socks')
LIMIT 12
OPTION fuzzy=1, fusion_method='rrf'

Manticore 分别执行全文搜索和向量搜索,再通过 Reciprocal Rank Fusion(RRF,倒数排名融合) 合并结果。简单说,就是看商品在两个列表中分别排第几。如果两边都排得靠前,它在最终结果中的位置通常也会更高。

界面上也能看到这一点:商品卡片会标注 Words、Meaning 或 Words + meaning,告诉你是哪种搜索方式找到了它。

如果两部分用的是同一段查询文本,还可以写得更短:hybrid_match() 。它会完成全文搜索、将查询转换成向量、执行 KNN 搜索,再用 RRF 合并结果。

再看一个例子:bicycle shoes (骑行鞋)。MTB 骑行鞋和 Unisex Bike Shoes,全文搜索和向量搜索都能找到;向量搜索还会找出根本没提到 bicycle 的 Cycling Shoes。但关键词有时也会把结果带偏:排在最前面的竟然是 Paul Parkman 的经典牛津皮鞋。它们的鞋头款式叫 bicycle toe,因此在全文搜索里排名靠前,向量搜索也找到了它们。

至于 warm waterprof jacket ,全文搜索负责纠正拼写,向量搜索则帮忙找到保暖又防水的夹克。两种方法都有用,只是各有所长。

Compare:把三组搜索结果并排看

为了不用在标签页之间来回切换,我们做了 Compare。它会分别运行 Full-text、Vector 和 Hybrid,把三组结果摆在相邻的列里。

如果同一件商品出现在多组结果中,Playground 会做标记。鼠标悬停在一张商品卡片上,其他列中相同的商品也会高亮。有时候,全文搜索和向量搜索还真能认出同一件东西:

全文搜索和向量搜索找到了同一件商品

但它们意见不一致的时候更有意思。

试试 outfit for a job interview :左边的全文搜索一片空白,向量搜索和混合搜索却给出了商务套装与西装外套搭配。

对比面试穿搭搜索:全文搜索没有结果,向量和混合搜索找到了商务服装

再试试 merino wool socks :全文搜索找到了袜子;向量搜索却被裤子和夹克吸引走了;混合搜索则把袜子放回第一位。

对比美利奴羊毛袜搜索:全文搜索找到袜子,向量搜索把其他羊毛服装排在前面,混合搜索把袜子排第一

要真正比较这些方法,最好拿你自己用户输入过的搜索词来试,而不是只用我们编的例子。想先随便体验一下,可以从 bicycle shoes 、lether jaket 或 steel toe boots 开始。

相似商品:连搜索词都不用输入

假设有人正在看一件皮夹克,想再找几件类似的。总不能让他重新去搜索框里打字吧。

因此,Playground 的商品卡片里有两个区域:Similar by description 和 Similar by photo。

一件皮夹克,以及按描述找到的八件相似商品

按描述找相似商品,执行的是这条 SQL:

SELECT id, title, description, features, category, image_url, knn_dist() AS distance
FROM convapparel_products
WHERE knn(embedding_vector, 8, 2388085963546244483)
LIMIT 8

这里 knn() 里的数字是商品 ID ,不是向量。Manticore 会找到这件商品已保存的向量,再搜索它的最近邻。应用不必先从数据库取出向量,然后又原封不动地发回去。

查找相似照片也是同一个原理,只是图片向量放在另一张表里。

4. 以图搜图

你试过在商品页面里描述一件具体的格子衬衫吗?名称和参数也许能告诉你面料、尺码,却不一定能说清格子的样式和颜色深浅。有时候,直接看照片更省事。

在 Image 标签页,你可以输入 red plaid flannel shirt (红色格纹法兰绒衬衫),也可以上传、拖入或粘贴一张图片,让系统帮你找类似的东西。

搜索 red plaid flannel shirt,返回红色格纹衬衫的照片

在这个演示中,我们用专门的模型把文字和照片转换成可以相互比较的向量。这样,“红色格子衬衫”的文字描述和一张相应的照片,在向量搜索所使用的多维空间里就会彼此靠近。

多维空间示意图

与文本向量搜索不同,图片搜索用的是独立的向量化服务。首先,服务把查询转换成向量。在公开版 Playground 中,这一步通常需要 50–250 毫秒,页面上会显示 turned into a vector in … ms。随后,Manticore 找到最近的图片向量,再去主商品表取出对应的商品。

SELECT id, knn_dist() AS distance
FROM convapparel_product_images
WHERE knn(image_vector, 100, (-0.0021, -0.01, 0.0782, …))
LIMIT 100;

SELECT id, title, description, features, category, image_url
FROM convapparel_products
WHERE id IN (3698494869106650404, 3557146672156430291, 1902002584948931003, …)
LIMIT 100

为了方便阅读,这里省略了部分 SQL。真正执行时,查询会包含向量的全部 512 个数值,以及 100 个商品 ID。

实现过程中,我们做了两个选择,或许也能给你的应用一些参考:

  • 图片搜索与主商品目录分开管理。 文本使用 MiniLM,照片使用 Fashion CLIP。我们把图片向量存放在单独的表中,这样以后换模型或者重新计算全部图片向量,都不必改动商品本身,也不会影响文本搜索。
  • 不索引反复出现的占位图片。 如果你抓取过商品目录,大概见过那张到处出现的 No image available。它出现一千次,也没法告诉你商品长什么样,索引它没有意义。所以在我们的演示中,82,524 件商品里约有 0.3% 没有图片向量。

5. 地理位置搜索:把商品放到地图上

做地理位置搜索时,我们遇到一个小问题:ConvApparel 没有坐标。为了展示地理搜索是如何工作的,我们给商品安排了纽约周边的模拟位置。

打开 Geo 标签页,就可以在地图上拖动位置、调整搜索半径,并立即看到 SQL 如何变化。

SELECT id, title, description, features, category, image_url, lat, lon,
    GEODIST(40.712800, -74.006000, lat, lon, {in=degrees, out=km}) AS distance_km
FROM convapparel_products
WHERE distance_km <= 10
ORDER BY id ASC
LIMIT 100

GEODIST() 用来计算距离。你可以在 SELECT 中返回这个值,用它筛选结果,也可以按距离排序。

在我们的演示里,结果按 id 排序,以便在地图选定的区域内展示分散的点。如果是真正的“给我看看最近的门店”,那多半应该改用 ORDER BY distance_km ASC,让距离最近的地点排在前面。

6. AI 搜索:良言无价

如果顾客连筛选条件和商品列表都不想研究,只想直接得到建议呢?比如他问:What should I wear to a summer wedding on the beach? (夏天去海边参加婚礼,应该穿什么?)

在 Ask AI 标签页,他可以直接提问,得到一个基于商品目录内容的回答。

Ask AI 根据商品目录回答海边夏日婚礼穿搭问题,并链接到具体商品

Manticore 为此提供了 CALL CHAT:

CALL CHAT('What should I wear to a summer wedding on the beach?',
    'convapparel_products', 'shopping_assistant', '', 'embedding_vector')

CALL CHAT 可以结合商品表和向量索引工作。收到新问题后,系统可以先搜索相关商品,再把商品信息交给语言模型。这里用到的是 title、description 和 features 字段。Playground 还会在回答里附上对应商品卡片的链接。

而且可以接着聊。比如先问 I need warm boots for walking the dog in snow(下雪天遛狗,需要一双保暖的靴子),再问 Which of those are waterproof?(这些里面哪些防水?)。前文的对话上下文会保留下来,你不用再解释一遍自己说的是哪双靴子。

不过,有两点值得记住:

  • 把 AI 推荐的商品展示出来。 如果 AI 信心满满地推荐一条“透气的连衣裙”,至少应该让用户点开商品页面,确认描述里是不是真的这么写的。
  • 别指望语言模型能弥补所有搜索失误。 合适的商品得先被搜索出来,进入上下文。如果前一步没找到它,AI 就很难在推荐中可靠地考虑这件商品。因此,模型的质量和全文搜索的配置也很重要。

还有速度问题。公开版 Playground 通常需要 5–15 秒 才能给出 AI 回答。影响速度的因素不少,尤其是所选 LLM 模型、模型服务商,或者你本地运行时使用的硬件(Manticore 也支持这种部署方式)。关于如何配置聊天功能、如何评估搜索质量,我们在另一篇文章 里详细介绍过。

如果我想用 HTTP API,而不是 SQL 呢?

Manticore 会说两种“语言”:SQL 和 JSON。上面我们已经看了很多 SQL 示例;如果你更喜欢通过 HTTP 发送 JSON,演示里每条 SQL 旁边也有对应的 curl 命令。它通过 HTTP JSON API 执行同样的搜索。

例如,前面那个带拼写错误的查询,对应的请求是:

curl -X POST localhost:9308/search \
  -H 'Content-Type: application/json' \
  -d '{
  "table": "convapparel_products",
  "query": {"query_string": "lether jaket"},
  "options": {"fuzzy": true},
  "limit": 12,
  "_source": ["title", "description", "features", "category", "image_url"],
  "aggs": {"categories": {"terms": {"field": "categories"}, "sort": [{"count(*)": {"order": "desc"}}]}}
}'

只要 Manticore 里已经加载了这张表,它就会返回相同的商品和分类统计。

说到全文搜索默认怎么连接单词,这里还有个容易忽略的区别:query_string 和 SQL MATCH() 一样,默认用 AND 连接单词;但 JSON 的 match 查询默认用的是 OR。把查询从一种格式改写成另一种时,别忘了这点。

整套 Playground 都可以在本地运行

Playground 的代码在 GitHub 上 。要在本地运行,只需要 Docker Compose:

git clone https://github.com/manticoresoftware/demo-search-playground
cd demo-search-playground
cp .env.example .env
./scripts/init_manticore.sh
docker compose up --build app embed

启动后打开 http://127.0.0.1:8000 。

仓库已经包含提前计算好的商品向量,因此部署时不必重新生成。想试试 Ask AI,启动前需要在 .env 文件的 OPENROUTER_API_KEY 中填入 OpenRouter 密钥。其他标签页不需要密钥。

首次启动时,图片向量化服务会下载一个约 600 MB 的 Fashion CLIP 模型。下载期间,其他搜索功能照样能用。想自己执行页面上的 SQL,也可以用 docker compose exec manticore mysql。

Playground 速查表:从哪里开始试?

功能Manticore 中对应的功能试一试
按相关性排序的全文搜索MATCH()、weight()levis 501
拼写容错OPTION fuzzy=1、CALL QSUGGESTlether jaket
按分类统计FACETlether jaket
自动补全CALL AUTOCOMPLETE在 Full-text 标签页输入 linen sh
自动生成向量的向量搜索使用文本查询的 knn()outfit for a job interview
混合搜索MATCH() + knn()、fusion_method='rrf'merino wool socks
相似商品使用文档 ID 的 knn()打开任意一件商品
图片搜索对照片向量执行 knn()red plaid flannel shirt 或上传照片
地理位置搜索GEODIST()Geo 标签页
AI 搜索CALL CHATsummer wedding on the beach
JSON API/search 接口任意一组结果下方的 curl 命令

公开版 Playground 运行在一台配置不高的服务器上。除 AI 聊天之外,大多数查询耗时约 5–100 毫秒。

试过这些例子就会发现,没必要指望一种搜索方式解决所有问题。精确关键词很重要时,全文搜索表现不错;用户只会描述需求时,向量搜索派得上用场;混合搜索则把两者结合起来。照片和地理位置需要各自的搜索方式,而当用户想要的是直接回答、不是一长串商品时,AI 就能派上用场。

当然,这些例子还远远没展示出搜索可能犯的所有错误。肯定存在某个查询,能让全文搜索、向量搜索和混合搜索一起翻车。

你能想出这样的查询吗?

接受挑战:找出能让三种搜索方式全部失手的查询

打开 Playground 试试看。如果发现特别糟糕的搜索结果,欢迎在 GitHub 提 issue ,或者到 Slack 告诉我们。如果你也想在自己的应用里实现类似的搜索,可以直接拿 Playground 的开源代码 作为起点。

安装Manticore Search

在 Linux 或 macOS 上用一条命令安装 Manticore Search:

curl https://manticoresearch.com | sh

有关高级安装选项,请参阅完整安装指南 和手册 。

安装Manticore Search