当文档库规模扩张时向量数据库肯定会跟着膨胀。百万级甚至千万级的 embedding 存储,float32 格式下的内存开销相当可观。
好在有个经过生产环境验证的方案,在保证检索性能的前提下大幅削减内存占用,它就是Binary Quantization(二值化量化)
本文会逐步展示如何搭建一个能在 30ms 内查询 3600 万+向量的 RAG 系统,用的就是二值化 embedding。
二值化量化解决什么问题
常规 embedding 用 float32 存储:单个 embedding(1024 维)占 4 KB 左右,3600 万个 embedding 就是 144 GB
二值化量化把每个维度压缩成 1 bit:同样的 embedding 只需 128 bytes,3600 万个 embedding 降到 4.5 GB
内存直接减少约 32 倍,而且位运算做相似度搜索更快。
精度损失与应对策略
二值化量化确实会带来精度损失,这点不能回避。
从 float32 直接压缩到 1 bit信息丢失不可避免,根据测试数据显示纯二值检索的准确度会下降到 92.5% 左右。不过这个问题有成熟的解决方案。
Oversampling(过采样)
检索时多拿一些候选结果。比如本来只需要 top-5,可以先检索 top-20 或 top-50,用数量换精度抵消量化造成的分辨率损失。
Rescoring(重排序)
先用二值向量快速筛选候选集,然后用原始的 float32 向量重新计算相似度并排序。
具体做法是:把全精度向量存在磁盘、二值向量和索引放内存,检索时先用内存里的二值索引快速找到候选,再从磁盘加载原始向量做精确评分。
这两个技术组合使用,能把准确度拉回到 95%-96%,对大多数 RAG 应用来说够用了。
使用限制
维度小于 1024 的 embedding 不建议用二值化。维度太小时,1 bit 能保留的信息不足,准确度会掉得比较厉害。所以这个技术更适合高维向量(≥1024 维)和大规模数据集。
相比之下,float8 这类低位浮点格式在 4 倍压缩下性能损失不到 0.3%,但内存节省远不如二值化激进。32 倍的压缩率带来的精度代价,需要根据具体场景权衡。
数据加载
先用 LlamaIndex 的 directory reader 读取文档。
支持的格式挺全:PDF、Word、Markdown、PowerPoint、图片、音频、视频都行。
LLM 配置
from llama_index.llms.groq import Groq
from llama_index.core.base.llms.types import (
ChatMessage, MessageRole )
llm = Groq(
model="MiniMaxAI/MiniMax-M2.1",
api_key=groq_api_key,
temperature=0.5,
max_tokens=1000
)
Moonshot Al
prompt_template = (
"Context information is below.\n"
"-----\n"
"CONTEXT: {context}\n"
"Given the context information above think step by step
"to answer the user's query in a crisp and concise manner.
"In case you don't know the answer say 'I don't know!'.\n"
"QUERY: {query}\n"
"ANSWER:
)
= query "Provide concise breakdown of the document"
prompt = prompt_template.format(context=full_context, query=query)
user_msg = ChatMessage(role=MessageRole.USER, content=prompt)
# Stream response from LLM
streaming_response = llm.stream_complete(user_msg.content)
LLM 配置完成,下一步开始对文件进行索引
二值 Embedding 生成
我们先生成标准 float32 embedding,然后用简单阈值转成二值向量。
每个维度的转换规则:
- 值 > 0 →
1 - 否则 →
0
Query Embedding 和二值化
# Generate float32 query embedding
query_embedding = embed_model.get_query_embedding(query)
# Apply binary quantization to query
binary_query = binary_quantize(query_embedding)
# Perform similarity search using Milvus
search_results = client.search(
)
collection_name="fastest-rag",
data=[binary_query],
Similarity search
anns_field="binary_vector",
search_params={"metric_type": "HAMMING"},
output_fields=["context"],
limit=5 # Retrieve top 5 similar chunks
# Store retrieved context
full_context = []
for res in search_results:
context = res ["payload"]["context"]
full_context.append(context)
为什么用 Hamming distance? 它是二值向量的天然相似度度量,计算速度极快。
Milvus Schema 和索引设置
from pymilvus import MilvusClient, DataType
# Initialize client and schema
client = MilvusClient("milvus_binary_quantized.db")
schema = client.create_schema (auto_id=True, enable_dynamic_fields=True)
# Add fields to schema
schema.add_field(field_name="context", datatype=DataType. VARCHAR)
schema.add_field(field_name="binary_vector", datatype=DataType.BINARY_VECTOR)
# Create index parameters for binary vectors
index_params = client.prepare_index_params()
index_params.add_index(
Specify index params
field_name="binary_vector",
index_name="binary_vector_index",
index_type="BIN_FLAT",
# Exact search for binary vectors
metric_type="HAMMING" # Hamming distance for binary vectors
)
# Create collection with schema and index
client.create_collection(
collection_name="fastest-rag",
schema=schema,
)
index_params=index_params
Create collection
# Insert data to index
client.insert(
collection_name="fastest-rag",
Insert data
data=[
{"context": context, "binary_vector": binary_embedding}
for context, binary_embedding in zip(batch_context, binary_embeddings)
]
)
这套配置能让 Milvus 高效处理数千万级别的向量。
检索流程
检索时的数据流:
- 用户 query 转 embedding
- embedding 转二值向量
- 用 Hamming distance 做二值检索
- 返回 top-k 相关文本块
文档 Embedding 的二值化处理
import numpy as np
from llama_index.embeddings.huggingface import HuggingFaceEmbedding
embed_model = HuggingFaceEmbedding(
model_name="BAAI/bge-large-en-v1.5",
trust_remote_code=True,
cache_folder='./hf_cache'
)
for context in batch_iterate(documents, batch_size=512):
# Generate float32 vector embeddings
batch_embeds = embed_model.get_text_embedding_batch(context)
# Convert float32 vectors to binary vectors
embeds_array = np.array(batch_embeds)
binary_embeds = np.where(embeds_array > 0, 1, 0).astype(np.uint8)
# Convert to bytes array
packed_embeds = np.packbits(binary_embeds, axis=1)
byte_embeds = [vec.tobytes() for vec in packed_embeds]
binary_embeddings.extend(byte_embeds)
这个转换过程快、简单、效果好。
LLM 生成环节
检索到 top-k 文本块后,用结构化 prompt 喂给 LLM。
# Combine retrieved contexts
full_context = "\n\n".join(full_context)
# Format prompt with context and query
prompt = prompt_template.format(context=full_context, query=query)
# Create chat message
user_msg = ChatMessage(role=MessageRole.USER, content=prompt)
# Stream response from LLM
streaming_response = llm.stream_complete(user_msg.content)
# Display streaming response
for chunk in streaming_response:
print(chunk.delta, end="", flush=True)
这里把检索到的多个文本块拼接起来,填充到 prompt template 里。LLM 会基于这些上下文生成回答。如果检索内容里没有答案,LLM 会直接回复 "I don't know!"。
总结
二值化量化在大规模 RAG 系统中的价值已经得到验证。32 倍的内存压缩率配合 Hamming distance 的计算效率,使得在资源受限环境下部署千万级向量检索成为可能。
精度损失是这个方案的代价,但 oversampling + rescoring 的组合能将准确度维持在 95% 以上,这对多数应用场景足够。
Perplexity、Azure、HubSpot 的生产实践说明这套方案已经过大规模验证。不过具体部署时还是要根据数据特征做测试,尤其是 rescoring 的候选集大小(oversampling factor)需要根据实际召回率调整。
作者:Algo Insights