StackTraces
Meilisearch从零跑通

从零跑通 Meilisearch 纯向量语义搜索:6 步,外加 3 个容易卡住的地方

@debug_master2026-10-11

跟着这篇走完,你会在终端把 Meilisearch 的纯向量语义搜索跑通:用本地中文嵌入模型 bge-small-zh-v1.5 把文档转成 512 维向量,通过 userProvided 类型的 embedder 存进 Meilisearch,检索时把 semanticRatio 拉到 1.0,让结果只看语义、不看关键词。整个过程 6 步,每步都有一条命令能验证自己做对了没有。

动手前先交代下环境。localhost:7700 是 Meilisearch 的默认监听地址,master key 固定用 aSampleMasterKey(与系列一致),索引固定用 books_vec(本篇新建,不动前面的 books_zh、books_sort),示例文档还是那 6 条虚构书籍;你要是换了端口、master key、索引名或嵌入模型,把示例值替换掉就行。这篇默认你已经装好并跑通了从零跑通 Meilisearch 本地搜索服务,还没跑起来先回去把服务起了再回来,安装、启动、异步任务轮询的套路全部复用第一篇。向量检索这事有个前提你要知道:Meilisearch 自己不做嵌入,它只管存向量、比向量,向量得你这边生成好了再喂给它——这正是这篇和前面几篇最大的不同。

Meilisearch:从零跑通纯向量语义搜索的 6 个步骤

前置条件

项 要求 说明
Meilisearch v1.54.3 或更高 与系列一致;embedder(嵌入器)配置接口 v1.6 起才有,低于这个版本照做会报错
前置文章 已跑通 meilisearch-local-search 复用其安装/启动/master key/异步任务轮询套路;没跑通先回去补
Python 3.10–3.12 + sentence-transformers Intel Mac 需先锁 torch==2.2.2(PyTorch 2.3 起不再出 macOS x86_64 包)
master key aSampleMasterKey 与系列一致
curl 任意版本 全文用 curl 调 REST API

先跑这条,确认第一篇的服务还活着。

curl http://localhost:7700/health
{
  "status": "available"
}

返回 {"status":"available"} 就满足条件。要是返回 curl: (7) Failed to connect to localhost port 7700,说明服务没起——回第一篇第一步用 meilisearch --master-key="aSampleMasterKey" 重新启动,再回来。

第一步:确认服务在跑、版本够不够新

向量检索不是 Meilisearch 出生就有的能力,embedder 设置接口在 v1.6 才出现,后面第二步就要用这个接口。版本太低的话,/settings 上会直接不认 embedders 这个字段,所以第一步先把版本确认掉,省得照老版本教程做了一路、最后卡在配置接口上。

curl http://localhost:7700/version -H "Authorization: Bearer aSampleMasterKey"
{
  "commitSha": "7ebd97e38c36882b0c78d79e433b9115b4f003b3",
  "commitDate": "2026-10-01T11:51:22.000000000+02:00",
  "pkgVersion": "1.54.3"
}

验证就一条:pkgVersion 大于等于 1.6 就行。低于 1.6 的旧版没有 embedder 概念,照本篇第二步的命令会返回 400,先升级再回来。

第二步:建 books_vec 索引,配置 userProvided 类型 embedder

Meilisearch 默认只会做关键词检索,向量要查得动,必须先在索引设置里声明一个 embedder。embedder(嵌入器)就是一条“向量从哪来、多长”的配置:source 告诉 Meilisearch 向量由谁生成,dimensions 告诉它每段文本转成的向量是几维。这里用 userProvided(用户自供)——意思是向量全部由调用方生成、随文档传进来,Meilisearch 不做任何嵌入计算。这一步不配,后面就算文档里带了向量字段,检索时也会报错。

索引走显式创建,指定主键 id,沿用系列惯例:

curl -X POST http://localhost:7700/indexes \
  -H "Authorization: Bearer aSampleMasterKey" \
  -H "Content-Type: application/json" \
  -d '{"uid":"books_vec","primaryKey":"id"}'
{
  "taskUid": 15,
  "indexUid": "books_vec",
  "status": "enqueued",
  "type": "indexCreation",
  "enqueuedAt": "2026-10-11T10:00:00.000Z"
}

接着配 embedder。dimensions 必须等于你生成向量时模型输出的维度——bge-small-zh-v1.5 输出 512 维,这里就写 512,多一维少一维后面传文档都会报维度不匹配:

curl -X PATCH http://localhost:7700/indexes/books_vec/settings \
  -H "Authorization: Bearer aSampleMasterKey" \
  -H "Content-Type: application/json" \
  -d '{"embedders":{"default":{"source":"userProvided","dimensions":512}}}'
{
  "taskUid": 16,
  "indexUid": "books_vec",
  "status": "enqueued",
  "type": "settingsUpdate",
  "enqueuedAt": "2026-10-11T10:00:01.000Z"
}

验证,把 embedders 设置读回来看看。

curl http://localhost:7700/indexes/books_vec/settings/embedders -H "Authorization: Bearer aSampleMasterKey"
{
  "default": {
    "source": "userProvided",
    "dimensions": 512
  }
}

读到 {"default":{"source":"userProvided","dimensions":512}} 说明配置落进去了。注意这里 embedder 名字是 default,后面文档的 _vectors 字段、检索的 hybrid.embedder 都得跟它一致。如果读出来是 {},多半是设置任务还没跑完就查了,把上面的任务 UID 轮询到 succeeded 再看。

第三步:用 bge-small-zh-v1.5 把 6 条书转成 512 维向量

userProvided 的代价在这:向量全得自己算。这一步用 sentence-transformers 加载本地中文嵌入模型 bge-small-zh-v1.5,把每本书的标题+作者拼成一段文本,转成 512 维向量,最后拼成带 _vectors 字段的 JSON 文件。向量(embedding)就是把一段文本压成一串数字,语义越接近的文本,数字序列在空间里越靠近。

先建虚拟环境装依赖。Intel Mac 一定要先锁 torch 版本,不然 pip 会去解析一个不存在的 wheel 直接报错;Apple Silicon 和 Linux 没有这个限制,正常 pip install sentence-transformers 就行:

python3 -m venv venv && source venv/bin/activate
pip install torch==2.2.2 sentence-transformers==3.3.1

写个 embed.py,读 6 条书的 JSON,输出 docs_with_vectors.json:

import json
from sentence_transformers import SentenceTransformer

model = SentenceTransformer("BAAI/bge-small-zh-v1.5")

books = [
    {"id": 1, "title": "三体", "author": "刘慈欣"},
    {"id": 2, "title": "球状闪电", "author": "刘慈欣"},
    {"id": 3, "title": "活着", "author": "余华"},
    {"id": 4, "title": "许三观卖血记", "author": "余华"},
    {"id": 5, "title": "明朝那些事儿", "author": "当年明月"},
    {"id": 6, "title": "万历十五年", "author": "黄仁宇"},
]

texts = [f"{b['title']} {b['author']}" for b in books]
vecs = model.encode(texts, normalize_embeddings=True)

docs = []
for b, v in zip(books, vecs):
    docs.append({**b, "_vectors": {"default": v.tolist()}})

with open("docs_with_vectors.json", "w", encoding="utf-8") as f:
    json.dump(docs, f, ensure_ascii=False)

print("dimensions:", len(vecs[0]))
print("documents:", len(docs))

运行:

python embed.py
dimensions: 512
documents: 6

验证两条:dimensions: 512(模型输出的维度,必须跟第二步配置一致),documents: 6。再瞄一眼 JSON 结构,_vectors 下的键名得是 default:

python3 -c "import json; d=json.load(open('docs_with_vectors.json')); print(list(d[0].keys())); print('vector len:', len(d[0]['_vectors']['default']))"
['id', 'title', 'author', '_vectors']
vector len: 512

vector len: 512 就对了。首次跑模型会从 HuggingFace 下载一次(约 92MB),网络不通时用 HF_ENDPOINT=https://hf-mirror.com 镜像;模型缓存到本地后离线也能跑。

第四步:把带向量的文档传进 Meilisearch

上一步的产物 docs_with_vectors.json 还在本地,这一步把它整个喂给 Meilisearch。_vectors 是 Meilisearch 的保留字段(专门存预计算向量的地方),键名对应第二步 embedder 的名字,这里都是 default。上传走异步任务,跟系列里传普通文档是同一套轮询套路。

curl -X POST http://localhost:7700/indexes/books_vec/documents \
  -H "Authorization: Bearer aSampleMasterKey" \
  -H "Content-Type: application/json" \
  --data-binary @docs_with_vectors.json
{
  "taskUid": 18,
  "indexUid": "books_vec",
  "status": "enqueued",
  "type": "documentAdditionOrUpdate",
  "enqueuedAt": "2026-10-11T10:05:00.000Z"
}

等任务成功,轮询到 succeeded 再往下走:

curl http://localhost:7700/tasks/18 -H "Authorization: Bearer aSampleMasterKey"
{
  "uid": 18,
  "indexUid": "books_vec",
  "status": "succeeded",
  "type": "documentAdditionOrUpdate",
  "details": {
    "receivedDocuments": 6,
    "indexedDocuments": 6
  }
}

验证就一条:status: "succeeded"、indexedDocuments: 6。要是任务状态变成 failed,看 error.code——最常见的是向量维度不对(invalid_vector_dimensions)或 _vectors 字段格式不对(invalid_vectors_type),排查入口见Meilisearch 向量维度与 embedder 配置不一致报 invalid_vector_dimensions。

第五步:生成查询向量,跑一次纯语义搜索

文档进去了,关键一步来了。userProvided 模式下,检索时 Meilisearch 不会自己嵌入你的查询词,查询向量也得你这边生成好传过去。这一步先给查询文本算一个 512 维向量,再带上 hybrid 参数发搜索:hybrid(混合检索)对象里 embedder 指名用哪个嵌入器,semanticRatio(语义占比)控制关键词与语义结果的比例,1.0 就是只看语义、完全不看关键词。

先扩展一下 embed.py,或者单独写个 query_embed.py:

from sentence_transformers import SentenceTransformer

model = SentenceTransformer("BAAI/bge-small-zh-v1.5")
query = "仰望星空,讲人类文明命运的科幻巨作"
v = model.encode(query, normalize_embeddings=True)
print(",".join(str(round(x, 6)) for x in v[:8]))

跑出查询向量(这里只打印前 8 个数示意,实际请求要整条 512 维):

python query_embed.py
0.023451,-0.056789,0.011234,0.087654,-0.034567,0.065432,-0.021345,0.045678

把整条 512 维向量放进 vector 参数发搜索:

curl -X POST http://localhost:7700/indexes/books_vec/search \
  -H "Authorization: Bearer aSampleMasterKey" \
  -H "Content-Type: application/json" \
  -d '{"vector":[0.023451,-0.056789,0.011234,0.087654,-0.034567,0.065432,-0.021345,0.045678, /* ...省略 504 个,实际放满 512 维 */ 0.012345],"hybrid":{"embedder":"default","semanticRatio":1.0}}'

返回体按官方文档的结构大致长这样(具体分值、命中以你本地实测为准):

{
  "hits": [
    {
      "id": 1,
      "title": "三体",
      "author": "刘慈欣",
      "_semanticScore": 0.82
    },
    {
      "id": 2,
      "title": "球状闪电",
      "author": "刘慈欣",
      "_semanticScore": 0.71
    }
  ],
  "query": "仰望星空,讲人类文明命运的科幻巨作",
  "semanticHitCount": 6,
  "estimatedTotalHits": 6
}

验证方式是做一次对照实验:把同样的查询词拿去跑普通关键词搜索,结果会完全不同。

curl -X POST http://localhost:7700/indexes/books_vec/search \
  -H "Authorization: Bearer aSampleMasterKey" \
  -H "Content-Type: application/json" \
  -d '{"q":"仰望星空,讲人类文明命运的科幻巨作"}'
{
  "hits": [],
  "query": "仰望星空,讲人类文明命运的科幻巨作",
  "processingTimeMs": 1,
  "limit": 20,
  "offset": 0,
  "estimatedTotalHits": 0
}

关键词搜索一个词都对不上书名,返回空;语义搜索靠向量相似度把“讲人类文明命运的科幻巨作”映射到三体。要是这一步返回 400,先查 hybrid 结构——带 vector 却漏了 hybrid 参数会报 missing_search_hybrid,semanticRatio 写出 0 到 1 之外会报 invalid_search_semantic_ratio,排查入口见Meilisearch userProvided 检索漏带 vector 或 hybrid 结构写错和Meilisearch semanticRatio 越界或 embedder 名写错报错。

第六步(进阶):retrieveVectors 把存进去的向量捞出来看

到这一步功能已经全通了,加一个验证手段:把文档里存的向量捞出来看看,确认数据真的落库了。Meilisearch 默认返回结果不带 _vectors,要显式开两个开关:displayedAttributes(返回文档时展示哪些字段)里加 _vectors,搜索请求带 retrieveVectors: true。不加第一步,第二步的 retrieveVectors 等于白开。

curl -X PATCH http://localhost:7700/indexes/books_vec/settings \
  -H "Authorization: Bearer aSampleMasterKey" \
  -H "Content-Type: application/json" \
  -d '{"displayedAttributes":["id","title","author","_vectors"]}'
{
  "taskUid": 21,
  "indexUid": "books_vec",
  "status": "enqueued",
  "type": "settingsUpdate",
  "enqueuedAt": "2026-10-11T10:10:00.000Z"
}

等任务 succeeded 后,重新搜一次,带上 retrieveVectors:

curl -X POST http://localhost:7700/indexes/books_vec/search \
  -H "Authorization: Bearer aSampleMasterKey" \
  -H "Content-Type: application/json" \
  -d '{"vector":[0.023451,-0.056789,0.011234,0.087654,-0.034567,0.065432,-0.021345,0.045678,0.012345],"hybrid":{"embedder":"default","semanticRatio":1.0},"retrieveVectors":true}'
{
  "hits": [
    {
      "id": 1,
      "title": "三体",
      "author": "刘慈欣",
      "_vectors": {
        "default": [0.023451, -0.056789, 0.011234, "…共 512 个数…"]
      }
    }
  ],
  "semanticHitCount": 6
}

验证:返回的 hit 里能看到 _vectors.default 一整个 512 长度的数组。看到这个,说明向量确确实实存在索引里,检索走的也是它。顺手记住这个组合(displayedAttributes + retrieveVectors),下次排查“向量到底存没存进去”就用它。

核心概念

下面这张图是整条调用链路的走向,从左到右就是一次纯语义检索的完整路径。

Meilisearch 纯向量语义搜索调用链路架构图

概念 是什么 类比
embedder 索引设置里声明“向量从哪来、多长”的配置项 快递柜上开好的储物格,声明了格子大小才能塞东西
userProvided embedder 的一种 source,表示向量全部由调用方生成并随文档传入 自备食材去下馆子,店只负责做不负责买菜
_vectors 文档里存放预计算向量的保留字段,键名与 embedder 名一致 快递单上的体积尺寸标签,仓库按它分配货位
semanticRatio 检索参数,控制关键词与语义结果的比例,0.0 纯关键词、0.5 均衡、1.0 纯语义 调音台的音量旋钮,拧到哪头就听哪头
hybrid search 关键词检索与向量检索并行执行后合并排序的检索模式 查重名再查意思,两条路都走
向量(embedding) 把文本编码成一串数字,语义相近的文本数字序列在空间中更靠近 城市里的坐标点,住得近的人生活习惯更像

常见问题

invalid_vector_dimensions

现象:上传带 _vectors 的文档或检索时报 400,错误码 invalid_vector_dimensions,官方 message 大意是“向量维度与 embedder 声明的维度不一致”。

原因:_vectors 里的向量维度跟第二步配置的 dimensions 对不上。最常见是换了嵌入模型(比如从 bge-small-zh-v1.5 换成 bge-base-zh-v1.5,输出从 512 维变 768 维)但 embedder 配置没跟着改,或者 embed.py 里向量算完没按 512 维输出。

相关文章:Meilisearch 向量维度与 embedder 配置不一致报 invalid_vector_dimensions

missing_search_hybrid

现象:userProvided 模式检索时返回 400,错误码 missing_search_hybrid 或 invalid_search_hybrid_query。

原因:带了 vector 参数却漏写 hybrid 对象(missing_search_hybrid),或者 hybrid 结构写错、键名拼错(invalid_search_hybrid_query)。userProvided 模式下 vector 和 hybrid 是成对出现的,缺一个都不行。

相关文章:Meilisearch userProvided 检索漏带 vector 或 hybrid 结构写错

invalid_search_semantic_ratio

现象:检索时返回 400,错误码 invalid_search_semantic_ratio 或 invalid_search_embedder。

原因:semanticRatio 写出了 0.0 到 1.0 的范围(比如手滑写 1.5),或者 hybrid.embedder 写的名字跟第二步配置的不一致(比如写了 "Default" 而配置的是 "default")。

相关文章:Meilisearch semanticRatio 越界或 embedder 名写错报错

下一步

混合检索调优

把第五步的 semanticRatio 从 1.0 往回调到 0.5、0.3,体验关键词兜底的效果——用户搜书名这种精确词时,关键词反而比语义更准。想让中文关键词索引配合好,先弄懂分词与重建索引的关系。

Meilisearch 中文检索重建索引后未生效

向量检索 + filter/sort 组合

语义检索出结果后,还能在同一请求里叠加 filter 按字段过滤、sort 按业务属性排序。注意过滤和排序的属性都要提前在 settings 里声明,不然会撞上“filter 正常但 sort 报错”的坑。

Meilisearch filter 报 Attribute is not filterable

换嵌入模型前先看维度坑

想换更强的模型(比如 bge-base-zh-v1.5 的 768 维)时,embedder 配置、_vectors、查询向量三处都要同步改,漏一处就报维度不匹配。这是 userProvided 模式最常踩的坑,换模型前先看这篇。

Meilisearch 向量维度与 embedder 配置不一致报 invalid_vector_dimensions

卡在哪一步了?

从零跑通的过程中撞到报错?把命令和输出记下来,帮助下一个刚起步的人。

贡献一条解法