codecamp

Ollama 嵌入向量(Embeddings)

搜索有两种做法。一种是找字面匹配——搜"退款"就只出现含"退款"的段落;另一种是找意思相近——搜"钱能退回来吗",也能命中写着"申请返还货款"的文档。后者靠的就是嵌入向量(Embeddings)。

嵌入向量把一段文本压成一串数字(向量的每一维是一个浮点数)。语义相近的文本,向量在空间里的距离也近。有了这个性质,语义检索、去重、聚类、RAG 全都成立。

版本基线 v0.40.12,官方 docs.ollama.com/capabilities/embeddings。向量维度由模型决定,常见在 384–1024 维之间。

一、命令行先跑一次

## 拉一个嵌入模型
ollama pull embeddinggemma

## 直接给文本
ollama run embeddinggemma "Hello world"

也可以用管道把文本喂进去,适合脚本里批量处理:

echo "Hello world" | ollama run embeddinggemma

输出是一个 JSON 数组,那就是向量本体。肉眼看不出意义,但可以拿来算距离。

二、REST 接口:/api/embed

真正的集成入口是 /api/embed。单条文本:

## 单条输入:input 传字符串
curl -X POST http://localhost:11434/api/embed \
  -H "Content-Type: application/json" \
  -d '{
    "model": "embeddinggemma",
    "input": "The quick brown fox jumps over the lazy dog."
  }'

Python 和 JavaScript 拿到的是同一个结构,embeddings 是二维数组——即使只传了一条文本,也是最外层包一层的形态:

## pip install ollama
import ollama

single = ollama.embed(
    model='embeddinggemma',
    input='The quick brown fox jumps over the lazy dog.'
)

print(len(single['embeddings'][0]))   # 向量维度(第一层是数组,第二层是具体那条)

// npm install ollama
import ollama from 'ollama'

const single = await ollama.embed({
  model: 'embeddinggemma',
  input: 'The quick brown fox jumps over the lazy dog.',
})

console.log(single.embeddings[0].length)   // 向量维度

注意 single['embeddings'][0] 这个下标。新手常写成 single['embeddings'] 然后奇怪为什么长度是 1——因为它拿到的是"装着一条向量的数组",不是向量本身。

三、批量:把 input 换成数组

建索引时一条一条发请求太慢。input 直接传字符串数组,一次编码一批:

## 批量输入:input 传字符串数组,返回数组与输入一一对应
curl -X POST http://localhost:11434/api/embed \
  -H "Content-Type: application/json" \
  -d '{
    "model": "embeddinggemma",
    "input": [
      "First sentence",
      "Second sentence",
      "Third sentence"
    ]
  }'

import ollama

batch = ollama.embed(
    model='embeddinggemma',
    input=[
        'The quick brown fox jumps over the lazy dog.',
        'The five boxing wizards jump quickly.',
        'Jackdaws love my big sphinx of quartz.',
    ]
)

print(len(batch['embeddings']))   # 3 —— 向量条数 = 输入条数

import ollama from 'ollama'

const batch = await ollama.embed({
  model: 'embeddinggemma',
  input: [
    'The quick brown fox jumps over the lazy dog.',
    'The five boxing wizards jump quickly.',
    'Jackdaws love my big sphinx of quartz.',
  ],
})

console.log(batch.embeddings.length)   // 3

返回顺序与输入顺序严格一致,可以直接按下标对应回原文。这一点很关键:建索引时务必把向量和原文、以及原文的唯一 ID 一起存起来,否则后面检索到向量却找不到对应内容。

四、/api/embed 的向量已经归一化

这是官方文档里一句容易被跳过、但很影响实现的说明:/api/embed 返回的是 L2 归一化(单位长度)向量。

什么意思?每个向量的模长都是 1。带来的直接好处是:计算余弦相似度时,分母恒等于 1,你可以直接用点积代替余弦相似度,省掉两次开方和一次除法。向量库里的 dot_product 和 cosine 在这批数据上结果完全一致,选更快的那个就行。

反过来说,如果你拿到的是自己另算的向量(或者换了别的模型来源),就不能默认它已归一化,该按余弦算还得按余弦算。

五、两条官方建议,值得当规则用

规则一:语义检索优先用余弦相似度。 官方明确推荐大多数场景用 cosine。欧氏距离在归一化向量上也等价(单调对应),但余弦的取值范围更好解释——1 表示完全同向,0 表示无关。

规则二:索引和查询必须用同一个模型。 这条比第一条更重要。不同模型的向量空间完全不通用,embeddinggemma 生成的向量拿去和另一个模型的向量比距离,结果毫无意义(哪怕维度碰巧一样)。所以:

  • 换嵌入模型 = 整个索引重建,不能只换查询侧
  • 模型版本升级也要留意,有些模型的版本迭代会改动向量空间
  • 项目里把"嵌入模型名 + 维度"写进索引元数据,加载时校验,能提前发现不匹配

六、一个最小可用的语义检索骨架

把上面几点串起来,一个不依赖任何向量库的最小实现长这样:

import ollama

MODEL = 'embeddinggemma'

## ① 建索引:文本 -> 向量,同时保留原文
docs = [
    '申请退还货款需要三个工作日到账。',
    '如何修改账户绑定的手机号。',
    '订单发货后不支持修改收货地址。',
]

index = ollama.embed(model=MODEL, input=docs)['embeddings']

## ② 查询:用同一个模型编码问题
question = '钱什么时候能退回来?'
q_vec = ollama.embed(model=MODEL, input=question)['embeddings'][0]

## ③ 向量已归一化,点积即余弦相似度
def dot(a, b):
    return sum(x * y for x, y in zip(a, b))

scored = sorted(
    ((dot(q_vec, vec), text) for vec, text in zip(index, docs)),
    reverse=True,
)

for score, text in scored:
    print(f'{score:.4f}  {text}')

跑一遍会发现,语义最接近"钱什么时候能退"的那条"申请退还货款…"排在第一位,尽管两者没有一个字面词重合。这就是嵌入向量相对于关键词搜索的价值。

生产环境当然要用真正的向量数据库(存向量、建近似最近邻索引、支持过滤条件),但上面的骨架解释了全部原理:编码、存储、用同模型编码查询、算余弦、排序。剩下的都是工程优化。

想把检索结果直接喂给模型生成答案(也就是 RAG 的"G"),下一步看 3.9 Ollama 网络搜索(Web search) 了解联网补充信息,或看 3.8 Ollama 工具调用(Tool calling) 把检索做成模型可自主调用的工具。

Ollama 视觉(Vision)
Ollama 语音(Speech)
温馨提示
下载编程狮App,免费阅读超1000+编程语言教程
取消
确定
目录

Ollama Cloud

Ollama CLI 命令行参考

Ollama Modelfile 参考

Ollama 上下文长度

Ollama 导入自定义模型

Ollama 常见问题

Ollama 硬件支持

Ollama 故障排查

关闭

MIP.setData({ 'pageTheme' : getCookie('pageTheme') || {'day':true, 'night':false}, 'pageFontSize' : getCookie('pageFontSize') || 20 }); MIP.watch('pageTheme', function(newValue){ setCookie('pageTheme', JSON.stringify(newValue)) }); MIP.watch('pageFontSize', function(newValue){ setCookie('pageFontSize', newValue) }); function setCookie(name, value){ var days = 1; var exp = new Date(); exp.setTime(exp.getTime() + days*24*60*60*1000); document.cookie = name + '=' + value + ';expires=' + exp.toUTCString(); } function getCookie(name){ var reg = new RegExp('(^| )' + name + '=([^;]*)(;|$)'); return document.cookie.match(reg) ? JSON.parse(document.cookie.match(reg)[2]) : null; }