用ClickHouse实现极速向量搜索,性能爆炸提升的秘密!
很多人对 ClickHouse 的印象还停留在“实时 OLAP 引擎”上,觉得它跟向量数据库这种时髦概念没什么关系。其实不然——ClickHouse 内置了一套完整的 SQL 函数和数据结构,专门用于向量之间的距离计算。换句话说,它不仅能做标准分析,还能承担向量数据库的角色。
真正让 ClickHouse 在向量搜索领域站住脚的,是其高度并行化的查询管道。当你需要对所有行做线性扫描、进行精确匹配时,ClickHouse 的处理速度完全可以叫板那些专用的向量数据库。这一点在实际测试中反复得到验证,并非纸面数字。
更值得一提的是压缩能力。ClickHouse 依靠自定义压缩编解码器,可以轻松应对多 TB 级别的嵌入数据集,查询时几乎不受内存限制。这意味着你不必为了存储海量向量而去调整昂贵的硬件配置。
距离计算被作为 SQL 函数实现,这意味着它可以和传统 SQL 的过滤、聚合操作无缝混用。向量数据完全可以和元数据、富文本放在同一张表里查询,这种灵活性在很多场景下非常实用——比如电商搜索、内容推荐、语义匹配,都能一套系统搞定。
准备工作:嵌入生成与表结构设计
无论你的数据是文档、图像还是结构化记录,第一步都是转化为嵌入向量。推荐使用 OpenAI Embeddings API 或 Python 库 SentenceTransformers 来生成。这一步不在 ClickHouse 内部完成,但生成的嵌入可以非常自然地被 ClickHouse 接收。
嵌入生成后,需要把它们逐行存进 ClickHouse。每行可以附带若干元数据字段,用于后续的筛选、聚合或分析。下面是一个存储图片标题和嵌入向量的建表示例:
CREATE TABLE images(
`_file` LowCardinality(String),
`caption` String,
`image_embedding` Array(Float32)
) ENGINE = MergeTree;
假设你想在数据集中搜一张“狗”的图片。你可以用狗图像的嵌入向量,配合 cosineDistance 函数进行搜索:
SELECT
_file,
caption,
cosineDistance(
-- 你的“输入”狗图像的嵌入
[0.5736801028251648, 0.2516217529773712, ..., -0.6825592517852783],
image_embedding
) AS score
FROM images
ORDER BY score ASC
LIMIT 10
下面是一个更通用的最近邻搜索示例,使用 L2Distance 函数:
SELECT
Name,
embedding,
L2Distance(embedding, [0.1, 0.2, 0.3, 0.4, 0.5]) AS score
FROM ann_index_example
ORDER BY score ASC
LIMIT 5
Query id: 9de95e7f-79e1-4d02-81ff-43f7ace14072 ┌─Name──┬─embedding────────────────────────────────────────────────┬───────────────score─┐ │ anabd │ [0.109578826,0.17093645,0.1863009,0.4694911,0.56862974]│ 0.1529803320145931 │ │ ewzos │ [0.24622843,0.28853804,0.42360082,0.5199647,0.5086616] │ 0.24282803026289437 │ │ a vajo │ [0.24545254,0.36559477,0.26052764,0.31315225,0.53613853] │ 0.24286758135453868 │ │ wyayw │ [0.032240078,0.18597832,0.31064722,0.21862713,0.6575932] │ 0.2502660809473027 │ │ adxir │ [0.1420014,0.11847292,0.41707036,0.41038868,0.71311146]│ 0.2600782018429016 │ └───────┴──────────────────────────────────────────────────────────┴─────────────────────┘ 5 rows in set. Elapsed: 0.006 sec. Processed 1.01 thousand rows, 42.42 KB (178.20 thousand rows/s., 7.48 MB/s.) Peak memory usage: 121.19 KiB.
对接 LangChain:集成实践

ClickHouse 的向量能力可以非常方便地通过 LangChain 框架调用。首先安装必要的依赖:
pip install -qU langchain_community clickhouse-connect
关键初始化参数中,embedding 用于指定向量生成函数,config 是可选的 ClickhouseSettings 配置项,用于设定客户端参数。
初始化实例的代码比较简单:
from langchain_community.vectorstores import Clickhouse, ClickhouseSettings
from langchain_openai import OpenAIEmbeddings
# 配置 ClickHouse 设置
settings = ClickhouseSettings(table="clickhouse_example")
# 实例化 ClickHouse 向量存储
vector_store = Clickhouse(embedding=OpenAIEmbeddings(), config=settings)
添加文档的操作也很直观:
from langchain_core.documents import Document
# 定义文档
document_1 = Document(page_content="foo", metadata={"baz": "bar"})
document_2 = Document(page_content="thud", metadata={"bar": "baz"})
document_3 = Document(page_content="i will be deleted :(")
# 添加文档到向量存储
documents = [document_1, document_2, document_3]
ids = ["1", "2", "3"]
vector_store.add_documents(documents=documents, ids=ids)
删除文档:
# 删除指定 ID 的文档 vector_store.delete(ids=["3"])
向量搜索:
# 进行向量搜索
results = vector_store.similarity_search(query="thud", k=1)
# 输出搜索结果
for doc in results:
print(f"* {doc.page_content} [{doc.metadata}]")
带过滤条件的向量搜索:
# 使用过滤条件进行向量搜索
results = vector_store.similarity_search(query="thud", k=1, filter="metadata.baz='bar'")
# 输出搜索结果
for doc in results:
print(f"* {doc.page_content} [{doc.metadata}]")
带分数的向量搜索:
# 进行带分数的向量搜索
results = vector_store.similarity_search_with_score(query="qux", k=1)
# 输出搜索结果和相似度得分
for doc, score in results:
print(f"* [SIM={score:3f}] {doc.page_content} [{doc.metadata}]")
异步操作的支持也没落下:
# 异步添加文档
# await vector_store.aadd_documents(documents=documents, ids=ids)
# 异步删除文档
# await vector_store.adelete(ids=["3"])
# 异步向量搜索
# results = await vector_store.asimilarity_search(query="thud", k=1)
# 异步带分数的向量搜索
results = await vector_store.asimilarity_search_with_score(query="qux", k=1)
# 输出搜索结果和相似度得分
for doc, score in results:
print(f"* [SIM={score:3f}] {doc.page_content} [{doc.metadata}]")
从实际使用体验来说,ClickHouse 不仅仅能跑简单的向量索引,它还支持多条复杂的查询条件——子查询、多字段过滤都可以混在一起用。这对于需要同时做语义检索和结构化过滤的场景来说,是一个非常实用的加分项。