当你已经不满足于单机原型,真正的差异往往出现在过滤表达和检索组合能力上

从“能搜相似向量”到“能服务真实业务”

到了生产前夜,团队关心的问题会迅速变化。大家不再只问“能不能搜到相似内容”,而开始追问“能不能只搜当前租户的数据”“能不能限定最近三个月的文档”“能不能同时兼顾关键词命中和语义相似”“能不能在已有服务里稳定扩展”。这时,向量数据库之间的差异才真正显现出来。

Qdrant 与 Weaviate 都属于这个阶段里非常常见的选择。两者都支持向量存储、近似检索、元数据过滤与服务化部署,但产品重心并不完全一样。简化地说,QdrantQdrantQdrantQdrant is a vector database known for strong payload filtering, practical APIs, and a focused retrieval-oriented design. It is often favored when teams want explicit control over vectors, filters, and hybrid retrieval behavior. 更偏向“检索内核与过滤能力做得扎实、接口清楚”;WeaviateWeaviateWeaviateWeaviate is a vector database with a broader platform flavor. It combines vector search with schema management, modules, and retrieval features that can fit teams wanting a more integrated application-facing stack. 则更有“平台型产品”的气质,围绕 schema、模块化扩展与应用接口做了更多封装。

这并不意味着谁更先进,而是说明它们在团队协作方式和项目约束上各有适配面。

Qdrant 的强项:payload 过滤清晰而实用

在真实业务里,单纯按相似度返回结果往往是不够的。你几乎一定会带上业务条件,例如租户 ID、文档类型、语言、时间范围、是否公开、是否已审核。Qdrant 把这部分能力做得非常明确,它把结构化字段称为payloadpayload在 Qdrant 里,payload 是附着在向量点上的结构化元数据。检索时既可以先用 payload 做过滤,再做向量搜索,也可以把过滤与相似度检索组合到同一次请求里。 ,并提供较完整的条件表达。

这使得 Qdrant 很适合那些“过滤不是边缘需求,而是核心前提”的场景。例如企业知识库,租户隔离和权限过滤必须在召回阶段就生效;再例如电商或内容平台,用户可见范围、发布时间和标签状态都直接影响检索结果。此时,向量相似度只是条件之一,不是全部。

Qdrant 的另一个优势,是接口相对直接。集合、点、payload、filter 这些概念都比较贴近检索工程师的思维,系统行为通常也容易预期。对于已经清楚自己在做什么的团队,这种明确性很重要。

Weaviate 的特点:更完整的应用层抽象

Weaviate 除了提供向量检索本身,还更强调上层对象模型与模块化扩展。它常常以类和属性组织数据,提供较强的 schema 管理感受,并能配合不同模块处理向量化、生成、关键词搜索等能力。这让它对一部分偏应用开发、希望快速组合完整能力的团队很有吸引力。

尤其当你希望数据库不仅存储向量,还承担一部分应用入口职责时,Weaviate 的产品风格会显得更顺手。它对混合检索hybrid searchhybrid searchHybrid search combines lexical matching and vector similarity in one retrieval pipeline. It is valuable when exact terms, names, or identifiers matter alongside semantic similarity. 的强调也比较鲜明。对于企业文档、商品检索、知识问答这类场景,关键词与语义往往都重要。仅靠向量相似度,专有名词、编号和罕见实体可能不稳定;仅靠关键词,又难以处理改写和意图相近表达。混合检索正是为了同时利用两种信号。

混合检索为什么越来越重要

向量检索解决的是“语义接近”,但业务里常常还存在另一类强信号:关键词精确命中。比如用户问“错误码 E11000 是什么”,如果系统完全依赖向量相似度,未必能把包含这个错误码的文档排到最前;反过来,如果只依赖关键词,又可能错过用自然语言解释该错误的说明文档。

因此,混合检索已经从“加分项”逐渐变成许多系统的默认配置。通常做法是把稀疏信号与稠密信号结合,再通过一定权重融合排序。Qdrant 和 Weaviate 都支持类似能力,只是入口设计与生态集成方式不同。你真正要考虑的,是自己的数据更偏哪一类信号,以及团队更习惯怎样配置和调试这套组合。

选 Qdrant 还是 Weaviate,关键看团队与问题

如果你的团队目标明确,已经知道自己需要怎样的 embedding、怎样的过滤字段、怎样的混合检索策略,而且希望对底层检索行为保持较强掌控,那么 Qdrant 往往是很稳妥的选择。它的优势在于能力聚焦、接口直接、过滤表现突出。

如果你的团队更希望获得一个“向量检索 + schema + 模块能力”一体化程度较高的平台,或者你更倾向于在产品层快速组合对象模型、向量化和搜索能力,那么 Weaviate 可能更顺手。它更像一个应用导向的平台,而不仅是检索引擎外壳。

这背后其实对应着两种工程风格。一种风格偏“检索系统思维”,强调索引、过滤、召回链路的可控性;另一种风格偏“应用平台思维”,强调通过更高层抽象缩短开发路径。两种风格都合理,关键是与你的团队能力结构是否匹配。

一个常见误区:把向量数据库选型完全等同于索引算法选型
很多团队比较产品时,过度关注 HNSW、IVF 或压缩参数,却忽视了真实项目里更常卡住的是过滤表达、数据建模、权限隔离、回填更新和运维接口。只看索引算法,会把问题看得太窄。数据库选型更像是系统能力选型,而不是单一检索算法选型。

Qdrant 完整实战:从启动到带过滤的混合检索

理论说够了,上代码。先用 Docker 起一个 Qdrant:

docker run -p 6333:6333 -p 6334:6334 \
  -v $(pwd)/qdrant_storage:/qdrant/storage \
  qdrant/qdrant

Python 客户端:

pip install qdrant-client sentence-transformers

1. 创建 collection(一次性 schema 定义)

from qdrant_client import QdrantClient
from qdrant_client.http import models

client = QdrantClient(url="http://localhost:6333")

# 创建 collection(相当于关系库的"表")
client.create_collection(
    collection_name="kb",
    vectors_config=models.VectorParams(
        size=512,                              # 向量维度(要跟 embedding 模型对得上)
        distance=models.Distance.COSINE,       # 距离度量:COSINE / DOT / EUCLID
    ),
    # HNSW 参数 —— 上一篇讲过
    hnsw_config=models.HnswConfigDiff(
        m=16,
        ef_construct=200,
    ),
    # 可选:开启 quantization 进一步省内存
    quantization_config=models.ScalarQuantization(
        scalar=models.ScalarQuantizationConfig(
            type=models.ScalarType.INT8,
            quantile=0.99,
            always_ram=True,                  # 量化后的小向量常驻 RAM
        ),
    ),
)

2. 写入数据(向量 + payload)

from sentence_transformers import SentenceTransformer
from uuid import uuid4

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

# 一批文档
docs = [
    {"text": "Python 异常 TypeError 通常因为对错误类型做操作", 
     "tenant_id": "team-a", "category": "python", "date": "2026-05-01"},
    {"text": "JavaScript 中 undefined 和 null 的区别",
     "tenant_id": "team-a", "category": "frontend", "date": "2026-05-10"},
    {"text": "PostgreSQL 索引选型:B-tree、GIN、GiST 何时用",
     "tenant_id": "team-b", "category": "database", "date": "2026-04-15"},
    {"text": "错误码 E11000 在 MongoDB 里表示重复主键冲突",
     "tenant_id": "team-b", "category": "database", "date": "2026-05-20"},
]

# 把文本批量编码成向量
texts = [d["text"] for d in docs]
vectors = model.encode(texts, normalize_embeddings=True).tolist()

# 批量插入
client.upsert(
    collection_name="kb",
    points=[
        models.PointStruct(
            id=str(uuid4()),
            vector=vec,
            payload=d,         # payload 就是结构化元数据
        )
        for vec, d in zip(vectors, docs)
    ],
)

print(client.count(collection_name="kb"))   # CountResult(count=4)

3. 纯向量检索

q = "怎么解决重复主键的报错"
q_vec = model.encode([q], normalize_embeddings=True)[0].tolist()

results = client.search(
    collection_name="kb",
    query_vector=q_vec,
    limit=3,
)
for r in results:
    print(f"score={r.score:.3f}  {r.payload['text']}")

4. 向量检索 + payload 过滤(这是 Qdrant 的强项)

# 只在 team-b 这个租户里搜,且只要 database 分类的文档
results = client.search(
    collection_name="kb",
    query_vector=q_vec,
    limit=3,
    query_filter=models.Filter(
        must=[
            models.FieldCondition(
                key="tenant_id",
                match=models.MatchValue(value="team-b"),
            ),
            models.FieldCondition(
                key="category",
                match=models.MatchValue(value="database"),
            ),
        ],
    ),
)

更复杂的过滤——日期范围 + 排除某些标签 + 多分类 OR:

results = client.search(
    collection_name="kb",
    query_vector=q_vec,
    limit=10,
    query_filter=models.Filter(
        must=[
            models.FieldCondition(
                key="date",
                range=models.DatetimeRange(
                    gte="2026-05-01T00:00:00",
                    lte="2026-05-31T23:59:59",
                ),
            ),
        ],
        should=[      # OR 关系
            models.FieldCondition(key="category", match=models.MatchValue(value="database")),
            models.FieldCondition(key="category", match=models.MatchValue(value="backend")),
        ],
        must_not=[
            models.FieldCondition(key="status", match=models.MatchValue(value="draft")),
        ],
    ),
)

Qdrant 过滤的关键设计:它的过滤直接嵌入到 HNSW 搜索过程里——遍历图时如果某个节点不满足 filter 就跳过,不是先全召回再后置过滤。这让过滤后召回率和延迟都更稳定,特别是过滤条件很苛刻(命中率 < 1%)时优势明显。

5. 给 payload 字段加索引(生产必做)

client.create_payload_index(
    collection_name="kb",
    field_name="tenant_id",
    field_schema=models.PayloadSchemaType.KEYWORD,
)
client.create_payload_index(
    collection_name="kb",
    field_name="category",
    field_schema=models.PayloadSchemaType.KEYWORD,
)
client.create_payload_index(
    collection_name="kb",
    field_name="date",
    field_schema=models.PayloadSchemaType.DATETIME,
)

经常被过滤的字段一定要加索引,否则大数据集上过滤会变慢。

6. 混合检索:稠密 + 稀疏(Qdrant 1.10+)

from qdrant_client.http import models

# 创建支持混合检索的 collection
client.create_collection(
    collection_name="kb_hybrid",
    vectors_config={
        "dense": models.VectorParams(size=512, distance=models.Distance.COSINE),
    },
    sparse_vectors_config={
        "sparse": models.SparseVectorParams(),    # BM25-style 稀疏向量
    },
)

# 写入时同时提供稠密 + 稀疏
# 稀疏向量可以是 BM25 提取出的 token weight,例如用 fastembed:
# from fastembed import SparseTextEmbedding
# sparse_model = SparseTextEmbedding("Qdrant/bm25")

# 混合检索 + RRF 融合
results = client.query_points(
    collection_name="kb_hybrid",
    prefetch=[
        models.Prefetch(query=q_dense, using="dense", limit=20),
        models.Prefetch(query=q_sparse, using="sparse", limit=20),
    ],
    query=models.FusionQuery(fusion=models.Fusion.RRF),    # Reciprocal Rank Fusion
    limit=10,
)

RRF(Reciprocal Rank Fusion)是混合检索最常用的融合算法:score = Σ 1/(60 + rank_i),简单稳定,不需要调超参。

Weaviate 完整实战:用 schema + 模块化思维

Weaviate 跟 Qdrant 的最大区别:更强调 schema 和模块。你定义类(Class),属性(Property),然后 Weaviate 把 vectorizer(向量化器)/ generative(生成器)/ reranker 等能力当成可插拔模块。

启动:

docker run -p 8080:8080 -p 50051:50051 \
  -e QUERY_DEFAULTS_LIMIT=25 \
  -e AUTHENTICATION_ANONYMOUS_ACCESS_ENABLED=true \
  -e PERSISTENCE_DATA_PATH=/var/lib/weaviate \
  -e DEFAULT_VECTORIZER_MODULE=none \
  -e ENABLE_MODULES=text2vec-openai,generative-openai \
  -e CLUSTER_HOSTNAME=node1 \
  cr.weaviate.io/semitechnologies/weaviate:1.27.0

Python 客户端:

pip install weaviate-client

1. 定义 schema(Class)

import weaviate
from weaviate.classes.config import Configure, Property, DataType

client = weaviate.connect_to_local()

client.collections.create(
    name="Article",
    properties=[
        Property(name="title", data_type=DataType.TEXT),
        Property(name="content", data_type=DataType.TEXT),
        Property(name="tenant_id", data_type=DataType.TEXT),
        Property(name="category", data_type=DataType.TEXT),
        Property(name="published_at", data_type=DataType.DATE),
    ],
    # 不让 Weaviate 自己 vectorize,我们外部提供向量
    vectorizer_config=Configure.Vectorizer.none(),
    # HNSW 参数
    vector_index_config=Configure.VectorIndex.hnsw(
        ef_construction=200,
        max_connections=16,
        distance_metric=weaviate.classes.config.VectorDistances.COSINE,
    ),
)

2. 写入数据(带向量)

articles = client.collections.get("Article")

with articles.batch.dynamic() as batch:
    for d in docs:
        batch.add_object(
            properties={
                "title": d["text"][:30],
                "content": d["text"],
                "tenant_id": d["tenant_id"],
                "category": d["category"],
                "published_at": d["date"] + "T00:00:00Z",
            },
            vector=model.encode(d["text"], normalize_embeddings=True).tolist(),
        )

3. 向量检索 + 过滤

from weaviate.classes.query import Filter

q_vec = model.encode("怎么解决重复主键的报错", normalize_embeddings=True).tolist()

results = articles.query.near_vector(
    near_vector=q_vec,
    limit=5,
    filters=(
        Filter.by_property("tenant_id").equal("team-b") &
        Filter.by_property("category").equal("database")
    ),
    return_metadata=weaviate.classes.query.MetadataQuery(distance=True),
)

for o in results.objects:
    print(f"distance={o.metadata.distance:.3f}  {o.properties['content']}")

4. Weaviate 的原生混合检索(一行 API)

results = articles.query.hybrid(
    query="重复主键报错怎么解决",
    vector=q_vec,
    alpha=0.5,         # 0 = 纯关键词(BM25),1 = 纯向量,0.5 = 一半一半
    limit=5,
)

alpha 参数控制稠密 vs 稀疏的权重。这是 Weaviate 比 Qdrant 接口更简洁的地方——一个参数就能完成混合检索。Qdrant 要自己组装 prefetch + RRF。

5. Weaviate 的 generative 模块(边检索边生成)

# 如果配置了 generative-openai 模块
results = articles.generate.near_vector(
    near_vector=q_vec,
    limit=3,
    # grouped_task: 把检索到的 3 条作为 context,让 LLM 回答用户问题
    grouped_task="根据上面的文档,回答:如何修复 MongoDB 的重复主键报错?",
)
print(results.generated)

这就是 Weaviate 的"平台型"风格——把 RAG 中的"检索 + 生成"压成一次 API 调用。Qdrant 不提供这个,要自己在应用层用 OpenAI SDK 拼。

Qdrant 和 Weaviate 关键差异对比

维度QdrantWeaviate
核心理念检索引擎,控制力强平台,封装多,开发快
向量化外部提供外部提供,或用内置 module
混合检索自己组 prefetch + RRF一行 hybrid(alpha=0.5)
过滤性能HNSW 内嵌过滤,苛刻条件下也快后置过滤为主,复杂条件稍慢
schema 严格度灵活(payload 任意 JSON)严格(Class + Property)
生成集成无,自己拼有 generative 模块
典型客户工程团队,明确知道自己要什么应用团队,要快速出活
写入吞吐较高中等
运维复杂度低(单二进制 + 数据目录)中等(模块管理稍多)

什么时候应该优先考虑过滤能力

有些业务把过滤放在很后面,结果上线前才发现系统逻辑不成立。比如多租户知识库,如果不能先限制在当前租户内,召回出来的内容再相似也不能用;比如内部文档系统,如果检索阶段不处理访问权限,后面再裁掉结果,会让真正应该返回的相关文档数量不够。此时,过滤不是优化项,而是正确性的一部分。

在这种场景下,Qdrant 往往更容易成为优先候选,因为它的 payload 过滤设计长期围绕这类需求展开。Weaviate 也能做过滤,但团队通常会更关注其整体平台能力是否正好契合项目节奏。

什么时候应该优先考虑混合检索

如果你的语料里充满术语、产品型号、错误码、人名、药名、法规编号或字段名,那么纯向量检索通常不够稳。因为这些对象经常要求字面命中,而不是仅靠语义接近。此时,混合检索的重要性会迅速上升。

这类场景里,Weaviate 往往能吸引那些想快速把多种信号整合进一个应用接口的团队;而 Qdrant 则更适合已经有明确检索流水线设计,想把稀疏与稠密组合逻辑掌握在自己手里的团队。差异不在“能不能做”,而在“默认工作方式是否顺手”。

本篇要点

  • 进入真实业务后,向量数据库的差异往往首先体现在过滤与混合检索能力上。
  • Qdrant 的优势在于 payload 过滤清晰、接口直接,适合对检索行为有明确掌控需求的团队。
  • Weaviate 更有平台化特征,适合希望结合 schema、模块与应用层抽象一起使用的团队。
  • 混合检索越来越重要,因为很多场景同时需要关键词精确命中和语义相似召回。
  • 选型不应只盯索引算法,还要综合考虑数据建模、权限、接口习惯和团队工程风格。

下一篇

如果说 Qdrant 和 Weaviate 代表的是更成熟的服务化向量产品,那么下一篇将进一步走向重型生产架构:Milvus 如何把向量检索做成分布式系统,以及 Collection、Partition 与 Helm 部署分别承担什么角色。

参考资料

版权声明: 如无特别声明,本文版权归 sshipanoo 所有,转载请注明本文链接。

(采用 CC BY-NC-SA 4.0 许可协议进行授权)

本文标题:Qdrant与Weaviate

本文链接:https://www.sshipanoo.com/blog/ai/vector-db/06-Qdrant与Weaviate/

本文最后一次更新为 天前,文章中的某些内容可能已过时!