> ## Content Index
> Fetch the complete content index at: https://www.hubwiz.com/blog/llms.txt
> Use this file to discover other available public pages before exploring further.

# 基于嵌入量化的大规模文档搜索
- URL: https://www.hubwiz.com/blog/vector-quantization-based-large-scale-document-search/
- Published: 2026-01-10T03:38:31.000Z
- Updated: 2026-01-10T03:38:31.000Z
- Description: 使用二元嵌入和 int8 重评分，在 CPU 上以不到 200 毫秒的时间搜索 4000 万份文档。真实代码，真实数据，真实结果。
- Author: admin
- Tags: APPLICATION, #Import 2026-10-14 14:41

4000 万份文档。单 CPU。无需 GPU。

二元搜索加上 int8 重评分如何悄然改写大规模语义搜索的规则。

当你长时间使用嵌入技术时，你会遇到一种非常特殊的挫败感。

你终于实现了检索。它很准确。感觉很智能。你为此感到自豪。然后你看了看账单。

或者更糟，看看内存使用情况。

或者更糟——当你意识到你“简单”的语义搜索仅仅为了运行就需要 180GB 的内存时。

这时，一个悄悄的假设就会悄然出现：“好吧……我想这就是正确进行向量搜索的代价吧。”

但也许并非如此。

因为接下来这件事让我停下了滚动页面：

> 你只需 8GB 内存和 45GB 硬盘，就能在 CPU 上用大约 200 毫秒的时间搜索 4000 万条文本。

无需 GPU。无需特殊硬件。无需任何魔法。

只需一种精心设计的推理策略。

一旦你看到了这一点，就无法再视而不见。

## 1、关于 fp32 嵌入的令人不安的真相

让我们大声说出来。

我们大多数人默认使用 float32 嵌入，因为模型就是这么给出的。我们视它们为神圣不可侵犯。要么高精度，要么什么都不要。

但想想我们在检索过程中究竟在做什么。

我们不是在解物理方程，而是在对文档进行排序。

而且，排序，尤其是在漏斗顶端，容错率出奇地高。

这就是整个方法的漏洞所在。

## 2、诀窍

一旦揭开神秘面纱，你会发现这套设置几乎乏味：

> 二分查找用于召回率。Int8 重评分用于精确率。

就是这样。

你不会丢弃质量，而是对其进行优化。

你先让低成本的表示方法完成繁重的工作，然后在真正重要的地方再提升精确率。

以下是完整流程的工作原理。

## 3、推理策略，逐步详解

让我们慢慢来，就像一起调试一样。

### 步骤 1：嵌入查询

我们从你预期的地方开始。

一个密集嵌入模型。一个标准的 fp32 向量。

```
#1. Embed the query as float32
start_time = time.time
query_embedding = model.encode_query (query)
embed_time = time.time() start_time
```

目前还没有什么特别之处。这是你的参考信号，也是你之后会信赖的东西。

### 步骤 2：将查询量化为二进制

现在，思维方式发生了第一次转变。

我们将 float32 查询嵌入量化为二进制。语义方向相同，但表示形式缩小了 32 倍。

```
#2. Quantize the query to ubinary
start_time = time.time
query_embedding_ubinary = quantize_embeddings(
query_embedding.reshape(1, 1), "ubinary"
)
quantize_time = time.time() start_time
```

这一步非常快，快得几乎有点可疑。

而且它为后续所有步骤奠定了基础。

### 步骤 3：搜索二元索引

在这里，规模不再令人担忧。

二元索引体积小巧，速度快，而且内存占用低。

您可以选择精确搜索或近似搜索。

```
#3. Search the binary index (either exact or approximate)
index = binary_ivf_index if use_approx else binary_index
start_time = time.time()
_scores, binary_ids = index.search
query_embedding_ubinary, top_k rescore_multiplier
)
binary_ids = binary_ids[0]
search_time = time.time() start_time
```

与其问“4000 万个文档中哪个最好？”

不如问“哪 40 个文档看起来有希望？”

这种重新定义问题至关重要。

### 步骤 4：加载最佳候选结果的 int8 词嵌入

现在我们已经缩小了范围，接下来需要引入更多信号。

我们仅加载排名靠前的二元匹配结果的 int8 词嵌入。

```
#4. Load the corresponding int8 embeddings
start_time = time.time()
int8_embeddings = np.array(
title_text_int8_dataset [binary_ids]["embedding"], dtype=np.int8
)
load_int8_time = time.time() start_time
```

数据量仍然很小，成本仍然很低，但比二元匹配更具表达力。

### 步骤 5：使用原始 fp32 查询重新评分

此时精度得以恢复。

我们使用原始 fp32 查询词嵌入，并将其与 int8 文档词嵌入进行比较评分。

```
#5. Rescore the top_k rescore_multiplier using the float32 query embedding and the int8 document embeddings
start_time = time.time()
scores = query_embedding @ int8_embeddings.T
rescore_time = time.time() start time
```

这一步几乎不耗费任何资源。而且它能帮你找回之前“丢失”的大部分质量。

### 步骤 6：排序并选择最佳结果

```
#6. Sort the scores and return the top k
start_time = time.time()
indices = scores.argsort() [::-1][:top_k]
top_k_indices = binary_ids[indices]
top_k_scores = scores [indices]
sort_time = time.time() start_time
```

至此，排名基本完成。

### 步骤 7：加载实际文本

现在才需要处理标题、URL 和内容。

```
#7. Load titles and texts for the top k results
start_time = time.time()
top_k_titles = title_text_int8_dataset [top_k_indices] ["title"]
top_k_urls = title_text_int8_dataset [top_k_indices] ["url"]
top_k_texts = title_text_int8_dataset [top_k_indices] ["text"]
top_k_titles = [f" [{title}] ({url})" for title, url in zip(top_k_titles, top_k_urls)]
load_text_time = time.time() start_time
```

这是加载文本的准确时间。

不能提前加载。

## 4、真正重要的数字

以下是对超过 4000 万条维基百科文本的实际运行结果：

```
Embed Time: 0.0833 s
Quantize Time: 0.0000 s
Search Time: 0.0464 s
Load int8 Time: 0.0263 s
Rescore Time: 0.0006 s
Sort Time: 0.0000 s
Load Text Time: 0.0192 s
Total Retrieval Time: 0.0925 s
```

再读一遍。

> 全程耗时不到 100 毫秒。基于 CPU。

你可以在[这里](https://huggingface.co/spaces/sentence-transformers/quantized-retrieval?ref=hubwiz.com)立即亲自尝试，无需登录。

## 5、为什么这种方法有效

感觉不对劲是因为我们被教导要不惜一切代价维护精确性。

但检索并非分类，也非回归，甚至不是生成。

它是筛选。

二进制嵌入在排除某些内容方面表现出色。Int8 嵌入擅长对候选对象进行排序。FP32 最适合真正需要细微差别的情况——但当文档数量减少到 40 个时，这种情况就很少见了。

因此，与其处处消耗 FP32 的成本，不如像聚光灯一样，逐步提升精度。

先用宽光束，后用窄光束。

## 6、存储和内存

让我们将其与标准的 FP32 设置进行比较。

Float32 数据检索（基准）

- 内存：约 180GB
- 磁盘：约 180GB
- 速度：慢
- 成本：高

二进制 + int8 数据检索

- 二进制索引：缩小 32 倍
- int8 嵌入：缩小 4 倍
- 内存使用量：约 6GB
- 磁盘使用量：约 45GB

关键在于：您只需将二进制索引保存在内存中。

这才是真正节省成本的地方。

## 7、性能权衡

我们不能假装没有损失。

以下是实验结果：

| 表示           | 索引大小      | 速度          | 性能      |
| ------------ | --------- | ----------- | ------- |
| float32      | 1 倍       | 1 倍         | 100%    |
| int8 / uint8 | 缩小 4 倍    | 速度提升高达 4 倍  | 约 99.3% |
| 二分查找 / 超二分查找 | 体积缩小 32 倍 | 速度提升高达 45 倍 | 约 96%   |

现在来说说重点。

通过二分查找检索更多候选结果，并使用 int8 重新评分，您可以恢复约 99% 的 fp32 质量。

这最后的 1% 几乎不需要任何成本。

## 8、这在实际应用中会带来哪些改变？

说实话？改变很大。

这意味着：

- 您不再需要 GPU 来进行大规模语义搜索
- 您可以在普通硬件上实现强大的检索功能
- 您无需再为 fp32 支付“奢侈税”
- 您可以扩展数据集而无需重写所有代码

也许更大的转变在于思维方式。

我们一直把嵌入向量当作易碎的玻璃。

它们不是。

它们是工具。而工具只有在您不再用同一把锤子敲所有钉子时才能发挥最佳作用。

如果您只能记住一点：

> 分阶段思考。

不要问“最精确的表示是什么？”，而要问“在流程的这个阶段，什么精度才足够？”

二进制优先，Int8其次，精度最后。

一旦你理解了这种检索方式，再去到处用暴力破解的FP32格式就显得……有点不负责任了。

也许这才是真正值得我们深思的教训。

并非说量化有多么高明。

但即便在巨型模型时代，这种工程判断仍然胜过暴力破解。

---

原文链接：[Search 40M documents in under 200ms on a CPU using binary embeddings and int8 rescoring.](https://civillearning.medium.com/search-40m-documents-in-under-200ms-on-a-cpu-using-binary-embeddings-and-int8-rescoring-4f5d34ad11ab?ref=hubwiz.com)

汇智网翻译整理，转载请标明出处