想象你管理着一座二十四小时不打烦的社区图书馆,馆里藏书早已超过千万册。普通图书馆只会按书名、作者做“关键词匹配”,而你聘了一位特殊的管理员“小维”。小维给每本书都贴上一张“语义名片”,上面用一串数字记录这本书“到底在讲什么”:是讲“机器人有了感情”,还是讲“深夜厨房里的治愈”。当有人问“有没有一本讲机器生命但结尾很温暖的书”时,小维不需要精确命中某个词,它拿起一张全新的“查询名片”,在数千万张名片里快速找到最相近的一批,再根据你的额外要求(只看某年出版、只选红色封面)精筛结果。这个故事里,小维就是 Weaviate,语义名片就是嵌入向量,找名片的规则就是相似度检索,而拿出来的“咨询单”则是 GraphQL 查询接口。这篇文章会带你从零看懂这套机制的每一块拼图。
对 AI 从业者而言,这套知识是构建语义搜索、RAG 知识库和多模态检索的底层能力,也是向量数据库选型与技术评审时绕不开的高频考点。
可视化演示
通过向量空间ANN检索、对象与向量双存储模型、GraphQL查询管道三个模式,展示Weaviate语义搜索核心机制。
本节配有交互动效(进入视口后自动加载,约 3KB)。
向量空间ANN检索 查询向量Q在768维空间中寻找k近邻 certainty=1/(1+d)衡量语义相似度 sim(q,v)=cos(q,v)=q·v/(|q|·|v|) HNSW索引实现亚线性时间近似最近邻检索。
语义嵌入基础
在小维管理的图书馆里,每一本书都被编目员认真读过一遍,随后写出一张“语义名片”。这张名片不是一句话简介,而是一串冷冰冰的数字,比如“温暖度 0.82、机器感 0.73、牺牲感 0.65”。如果两本书的名片数字很接近,读者就会觉得它们是“同一类的故事”。真实世界中的文本向量化正是如此:大语言模型或编码器把文字转换成长度固定的数字列表,这个列表被称为嵌入向量。
从比喻回到技术,Weaviate 本身并不“理解”文字,它依赖向量化模型将文本变成高维坐标。常见模型会把一句话映射为 384 维、768 维甚至更高维的向量。每个维度并没有明确含义,但组合在一起能捕捉词义、句法、上下文和常识关系。比如“狗”和“宠物”的向量距离很近,而“狗”和“汽车”的距离很远。这个空间就是那座图书馆的“语义大厅”,每一张名片都是大厅里的一个坐标点。
有了坐标点,我们就能用数学方式衡量两段文字的相似程度。最常用的是余弦相似度:它只看两个向量方向是否一致,而不关心文本长度,因此适合比较不同长度的句子。
你看,小维能回答“温暖又悲伤的机器人故事”,靠的正是这套坐标计算:查询变成一张临时名片,再与馆内千万张名片逐一比夹角。这正是 Weaviate 所有语义搜索能力的起点。
- 分子点积把查询向量 \(q\) 和文档向量 \(d\) 的每一维相乘后累加,得到两个向量“同方向强度”的加总。
- 分母模长分别计算两个向量的长度,用来消除文本长度带来的偏差,避免长文本天然得分更高。
- 余弦夹角点积除以模长乘积后,结果落在 \(-1\) 到 \(1\) 之间,越接近 \(1\) 表示语义方向越一致。
核心架构
传统图书馆会把“书的正文”和“编目卡片”分开放:书在书库,卡片在目录柜。小维的图书馆却不一样,它把书和名片订在一起,放进同一个智能书架。Weaviate 采用“对象与向量一体化存储”,每个业务对象既包含原始字段,也包含语义向量,二者在一次写入中共同落盘,避免了两套系统之间的同步难题。
这种设计在技术上的体现是:每个对象有一个 class 表示所属分区,相当于图书馆中的“楼层主题牌”;有一个全局唯一 id,相当于书架上的二维码;有一个向量字段 vector,就是内页里的语义名片;最后还有若干业务属性 properties,比如电影名、年份、导演。整体可以用一个集合形式表达:
正因为业务字段和向量被放进同一条记录,写入时只要一次事务就能保证两边都成功。如果分开存储,可能出现“书已入库但名片还没生成”的尴尬状态。小维把书和名片钉在一起,就保证了任何时候都能同时找到内容和它的语义位置。
- 分区类别
class是逻辑上的数据分区,例如Movie或Article,所有同类型对象共享同一个集合,方便按主题检索。 - 唯一标识
id是对象的全局唯一编号,可以是 UUID,所有引用和更新都依赖它。 - 语义向量\(\mathbf{v}\) 是对象内容被向量化后得到的高维坐标,通常由外部模块生成后存入对象。
- 业务属性
properties是你能看到的原始字段,比如年份、标题、价格,它们承担描述与过滤的功能。
相似度检索
当读者问小维“有没有关于 AI 和灵魂的电影”时,小维不会把这句话拿去逐字匹配标题,而是先为问题生成一张新名片,然后走进“近邻柜”。所谓近邻柜,就是一个专门按语义距离组织的索引结构:它不按字母排序,而是按向量远近把对象连成一张网,HNSW(分层可导航小世界图)就是这张网的实现。有了它,小维不用遍历千万张名片,只需沿着“朋友介绍朋友”的路径快速逼近目标区域。
在 Weaviate 中,面向自然语言的查询入口是 nearText,它会自动把用户文本向量化;面向已有向量的入口是 nearVector,可以接收外部模型生成的向量。为了让结果兼顾“意思相近”和“关键词命中”,Weaviate 还提供了混合检索:把向量相似度与经典的 BM25 文本得分按比例融合。
小维之所以能“秒回”问题,并不是记忆力超群,而是近邻柜把检索范围从千万级压缩到几十个候选点。HNSW 会建立多层网络:上层跳得远,下层走得细,查询从顶层快速落到目标区域,再逐层精修。这套机制让“从千万张名片里找最像的几张”成为毫秒级操作。
- 语义得分\(\text{sim}_{\text{vec}}\) 是查询向量与候选对象向量的余弦相似度,负责捕捉“意思相近但字面不同”的结果。
- 关键词得分\(\text{norm}_{\text{BM25}}\) 是对 BM25 分值做归一化后的结果,负责捕捉专有名词和精确术语的命中。
- 加权融合参数 \(\alpha\) 控制两种信号的权重,\(\alpha=1\) 时只做纯语义搜索,\(\alpha=0\) 时退化为纯 BM25。
GraphQL查询接口
在小维的图书馆里,读者不能直接冲到书架里乱翻,而是要在服务台填一张“咨询单”。这张咨询单必须写明:想进哪个分区、语义条件是什么、只要哪几个字段、最多返回几条。小维提供的咨询单格式就是 GraphQL。GraphQL 是一种 API 查询语言,它让客户端精确声明要什么数据结构,不多不少。Weaviate 用它把“语义搜索 + 标量过滤 + 字段裁剪”组合在同一个请求中。
下面是一次典型的 Query:
这段咨询单告诉小维:在电影分区里,找出和“AI and soul”语义最接近、且 2000 年后上映的 5 部电影,只返回标题、上映年份和与查询的距离。从抽象视角看,一次 GraphQL 查询可以被拆成几个运算的组合:
GraphQL 的灵活性就像一张“可勾选”的咨询单:你可以只勾“标题”和“距离”,不勾“导演”和“剧情”。这种按需取数的方式,让检索服务既能满足复杂语义需求,又不会把所有数据一股脑倒给用户。小维也因此能高效地服务成千上万个并发读者。
- 近邻运算\(\rho_{\text{nearText}}(\text{Class})\) 表示对某个分区执行语义近邻召回,候选结果在这里产生。
- 选择条件\(\sigma_{\text{where}}\) 表示用条件表达式对候选集进行选择,匹配条件的对象才被保留。
- 投影字段\(\Pi_{\text{fields}}\) 表示从结果对象中只挑出客户端需要的属性,减小网络传输量。
{ Get { Movie( nearText: { concepts: ["AI and soul"] } where: { path: ["release_year"], operator: GreaterThan, valueInt: 2000 } limit: 5 ) { title release_year _additional { distance } } }}
过滤机制
语义名片能找到“意思相近”,但读者往往还有硬性要求:“我只要 2018 年以后出版的推理小说,不要言情。”这时候,小维会把语义名片和书架隔板一起使用。Weaviate 的过滤机制通过 GraphQL 中的 where 条件实现,它可以基于对象的标量属性做精确筛选,例如文本相等、数值大于、时间范围、是否存在等。
一个 where 条件由三部分构成:path 指明要过滤哪个属性,operator 指明操作符,valueText 或 valueInt 等字段给出目标值。比如筛选“类型=推理 且 年份>2018”:
从形式逻辑上看,所有过滤规则都可以看作原子条件的“与”和“或”组合。And 表示必须同时满足,Or 表示满足其一即可,它们的嵌套结构能表达非常复杂的业务规则:
这里的关键是:过滤不是检索完再“事后倒垃圾”。小维在近邻柜里走动时,就已经带着“只看 2018 年后的推理小说”这张纸条,不符合条件的候选会被直接跳过。这样既节省计算,也让最终结果更精准。明白了过滤机制,你就能把“语义相似”与“业务条件是硬约束”两件事自然地装进同一次查询。
- 原子条件\(\text{cond}_{ij}\) 是最小过滤单元,例如“年份大于 2018”或“类型等于推理小说”。
- 与运算\(\bigwedge_j \text{cond}_{ij}\) 表示同一组内所有条件同时成立,对应
operator: And。 - 或运算\(\bigvee_i\) 表示多个条件组之间至少组成立,对应
operator: Or。
where: { operator: And, operands: [ { path: ["genre"], operator: Equal, valueText: "mystery" }, { path: ["year"], operator: GreaterThan, valueInt: 2018 } ]}
模块生态
小维并不是一个人完成所有编目工作,他背后有很多“外聘编目专家”。不同的专家擅长不同领域:有的擅长英文文本,有的擅长中文小说,有的擅长把图片和文字同时映射到同一张名片空间。Weaviate 的模块化设计正是为了干这件事:向量化模型被封装成插件,按需接入,而不是写死在数据库内核里。
常见的模块包括 text2vec-openai、text2vec-transformers、text2vec-cohere,以及支持图文的多模态模块 multi2vec-clip。当你在某个 Class 上配置了 text2vec-openai,写入对象时 Weaviate 会自动调用该模块把文本字段变成向量,查询时也会自动把自然语言变成向量。整个过程对调用方透明,像雇佣了一位全天候在线的“编目员”。
从数学视角,一次向量化就是一个函数映射:
模块化的好处是“按需换人”:今天团队更擅长 OpenAI 模型,明天想换成开源的 BERT,只需要调整配置并重新写入一次向量,不需要替换整个数据库。小维因此总能保持最专业的编目水平,而图书馆的核心检索架构完全不受影响。
- 模块参数\(M_\theta\) 代表一个神经网络模型,\(\theta\) 是它的训练权重,不同模块擅长不同数据形态。
- 原始数据\(\text{data}\) 可以是句子、段落、图片或表格内容,是编目员需要理解的原始对象。
- 语义输出输出 \(\mathbf{v}\) 是一个归一化的高维向量,供近邻柜使用。
应用优化
故事讲到最后,图书馆终于要正式开业。规模变大后,小维需要关注三件事:读者的问题能不能快速得到答案;新书能不能及时上架;海量请求下系统会不会崩溃。这些对应到 Weaviate 就是检索性能、数据一致性和可观测性。
在真实业务中,一条语义搜索链路通常包含多个阶段:先把用户问题向量化,然后到向量索引中做近邻召回,再叠加标量过滤和排序,最后做后处理(如调用大模型生成回答)。每个阶段都可能成为瓶颈,所以优化前必须先拆解延迟:
除了延迟,数据一致性同样重要。Weaviate 把对象和向量放在同一次写事务中,因此不会出现“有向量但没对象”的中间态;在分布式部署时,副本同步与版本管理则保证了小维在任意服务节点上都能看到同样的图书目录。掌握了这套结构,你就能像经营图书馆一样设计自己的语义检索平台。
- 向量化耗时\(T_{\text{embed}}\) 是调用模型把查询变成向量的时间,通常可以通过模型服务缓存和批处理压缩。
- 近邻搜索耗时\(T_{\text{search}}\) 是 HNSW 等索引查找候选向量的时间,受索引层数、连接数和候选返回量影响。
- 过滤耗时\(T_{\text{filter}}\) 是执行
where条件的时间,复杂嵌套条件会带来额外开销。 - 后处理耗时\(T_{\text{post}}\) 包括结果排序、字段补全、生成模块问答等,通常在毫秒到秒级波动。
AI 应用场景
RAG 智能知识库
阶段:知识库索引构建 + 检索增强生成(RAG)
解决的问题:传统企业与客服通过关键词搜索 FAQ,用户问法一变就找不到答案。Weaviate 先对文档分块并生成语义向量,再通过 nearText 召回最相关内容,配合 where 限定部门或版本,最后把召回片段交给大模型生成回答,显著降低“答非所问”概率。
具体案例:某 SaaS 公司把产品手册、工单记录和版本更新日志写入 Weaviate,客服输入“如何导出账单但不要包含已退款订单”,系统自动召回高相似文档,并通过过滤条件排除旧版本内容,最终给出规范化答复。
电商多模态商品搜索
阶段:离线向量化 + 在线多模态召回
解决的问题:传统电商搜索只能匹配商品标题关键词,难以理解“适合下雨天穿的轻便鞋”这类描述性查询。通过 multi2vec-clip 模块,图片和文本共享同一语义向量空间,再结合价格、库存等 where 过滤,实现跨模态语义检索。
具体案例:一个服饰平台允许用户上传一张运动鞋照片,同时在搜索框中输入“复古风”,Weaviate 用 CLIP 模块把图片和文字统一编码,在近邻柜中召回同时满足“图像相似”和“复古标签”的商品,并按价格区间过滤。
企业级知识图谱问答
阶段:对象存储 + 语义检索 + 生成式回答
解决的问题:大量企业数据以结构化和非结构化混合形态存在,传统关系型数据库难以处理“和某供应商合作过、且涉及专利纠纷的项目有哪些”这类语义型问题。Weaviate 利用 class 分区、向量近邻和 GraphQL where 组合条件,把复杂问题拆解为一次可解释检索。
具体案例:某咨询公司把项目文档、合同摘要和专家档案都存入 Weaviate,分析师输入“找和新能源汽车电池相关的欧洲项目”,系统先语义召回候选项目,再按地区与业务线过滤,最后使用生成模块自动汇总背景摘要。
Python 演示
import mathimport re class HashingVectorizer: def __init__(self, dim=64): self.dim = dim def _token_hash(self, token): return sum(ord(ch) for ch in token) % self.dim def vectorize(self, text): vec = [0.0] * self.dim tokens = re.findall(r"[a-z0-9]+", text.lower()) for token in tokens: vec[self._token_hash(token)] += 1.0 norm = math.sqrt(sum(v * v for v in vec)) if norm > 0: vec = [v / norm for v in vec] return vec class MiniWeaviate: def __init__(self, vectorizer): self.vectorizer = vectorizer self.row_list = [] def add_object(self, obj_id, text, genre, year): vector = self.vectorizer.vectorize(text) self.row_list.append({ "id": obj_id, "text": text, "genre": genre, "year": year, "vector": vector, }) def _cosine(self, left, right): if len(left) != len(right): raise ValueError("vector dim mismatch") return sum(a * b for a, b in zip(left, right)) def _matches(self, row, where): if not where: return True return row[where["key"]] == where["value"] def search(self, query, where=None, limit=3): qv = self.vectorizer.vectorize(query) scored = [] for row in self.row_list: if self._matches(row, where): scored.append((self._cosine(qv, row["vector"]), row)) scored.sort(key=lambda item: item[0], reverse=True) return scored[:limit] def graphql_like_query(self, query, where=None, limit=3): return self.search(query, where=where, limit=limit) if __name__ == "__main__": vectorizer = HashingVectorizer(dim=64) db = MiniWeaviate(vectorizer) db.add_object(1, "a robot learns to love and sacrifices itself", "sci-fi", 2015) db.add_object(2, "a detective hunts a killer in a rainy city", "mystery", 2019) db.add_object(3, "a machine dreams about electric sheep", "sci-fi", 1968) db.add_object(4, "a chef cooks noodles at midnight", "food", 2021) results = db.graphql_like_query( "artificial intelligence with feelings", where={"key": "genre", "value": "sci-fi"}, limit=2, ) for score, row in results: print(row["id"], row["text"], round(score, 3))
vec = [0.0] * self.dim初始化一个长度为dim的向量,所有位置先填零,代表一张空白语义名片。tokens = re.findall(r"[a-z0-9]+", text.lower())把文本拆成词元,模拟向量化模型接收原始文本的第一步。vec[self._token_hash(token)] += 1.0把每个词元映射到某个维度并累加,形成简单的词袋信号。self.row_list.append({...})对象字段、文本标签和语义向量被放进同一条记录,对应 Weaviate 的对象一体化存储。return sum(a * b for a, b in zip(left, right))因为向量已归一化,余弦相似度退化为点积,这是最常用的语义距离计算。scored.sort(key=lambda item: item[0], reverse=True)按相似度从高到低排序,模拟近邻检索的最终排序过程。db.add_object(1, ..., "sci-fi", 2015)写入一个完整对象,业务字段和向量同时落地,对应开启向量化模块后的写入行为。
常见误区
误区我一直以为 `nearText` 只是搜索引擎里的模糊匹配,只要把拼写错误容忍度调高就能实现,语义搜索就是“高级版 LIKE”。
正解模糊搜索通常基于编辑距离或字符重合,改写一个词可能就失效;语义搜索则基于向量空间中的位置。例如搜“会哭的机器人”可以命中外星人 + 亲情主题的影片,即使标题里完全没有“哭”和“机器人”。Weaviate 的混合搜索里,BM25 负责字面命中,nearText 负责意思命中,二者是互补关系。
为何常见:传统搜索的匹配思维根深蒂固,容易把“容错”和“理解语义”混为一谈。
误区既然 Weaviate 用 GraphQL 查询,那 GraphQL 就是向量数据库的“新 SQL”,应该能用它建表、删表、改索引。
正解GraphQL 是一种面向客户端的 API 查询语言,重点是声明“我要什么字段、条件和结构”,并不负责存储引擎的内部管理。在 Weaviate 中,建类、配置向量化模块、设置 HNSW 参数通常通过 Schema 或配置文件完成,GraphQL 只负责查询与少量变更操作。
为何常见:因为第一次接触 Weaviate 就是通过 GraphQL playground,容易把接口语言误认为存储语言。
误区我认为查询过程一定是先用 `nearText` 召回几千条,再逐条检查 `where` 条件,最后输出过滤后的结果。
正解Weaviate 会把结构化过滤条件和向量搜索放到同一个检索计划中,而不是简单粗暴的“事后 filter”。符合条件的候选会作为近邻搜索的边界,过滤条件参与剪枝,因此能避免“召回结果被大量过滤掉”的性能浪费。当然不同实现细节有差异,但整体设计远比“先检索后过滤”精细。
为何常见:很多向量数据库产品的早期实现是内存后过滤,容易让人形成惯性认知。
误区我以为 Weaviate 的向量化能力是写死的,数据库核心自带一个模型,所以所有数据都用同一个向量空间。
正解Weaviate 是模块化架构,可以通过 text2vec-openai、text2vec-transformers、multi2vec-clip 等模块接入不同模型。甚至可以在不同 Class 上配置不同向量器,让电影检索用 A 模型、商品检索用 B 模型。
为何常见:默认体验中不需要配置模型,容易让人以为向量化能力被内置固定住了。
面试题
Q1. 在 Weaviate 中,对象的语义向量和业务字段的关系是?
- A.向量单独存 PostgreSQL,字段存对象库
- B.向量与字段一起封装为对象存储← 正确答案
- C.只保存向量,不保存原始字段
- D.字段来自 Redis,向量来自文件系统
答案:B。Weaviate 的核心设计是对象与向量一体化存储,同一个对象既包含业务属性也包含语义向量,这样写事务才能保证两边一致。A、D 把数据拆到多套系统,C 忽略业务字段,都不符合架构。
Q2. 用户想用一段自然语言找相似对象,应优先使用哪种查询?
- A.nearText 查询← 正确答案
- B.nearVector 查询
- C.BM25 关键词查询
- D.where 条件查询
答案:A。nearText 会自动把自然语言变向量并执行语义近邻检索,是“用一句话找意思相近对象”的入口。nearVector 需要调用方自己提供向量,BM25 和 where 都不负责语义理解。
Q3. text2vec 模块在写入对象时主要负责?
- A.把文本转换为向量← 正确答案
- B.把向量压缩成标量
- C.执行 GraphQL 语法分析
- D.创建数据库索引文件
答案:A。text2vec 模块的核心职责是文本转向量,也就是给对象生成“语义名片”。压缩标量、GraphQL 分析、索引创建都不是向量化模块的分内事。
Q4. 在 GraphQL 中查看结果与查询向量的距离,应使用哪个字段?
- A.
title { distance } - B.
_additional { distance }← 正确答案 - C.
where { distance } - D.
sort { distance }
答案:B。Weaviate 把 distance 这类查询附加信息放在 _additional 中,用于获取相似度、id 或向量等元数据。title、where、sort 都不是距离信息的正确位置。
Q5. HNSW 索引主要解决什么问题?
- A.在大规模向量上加速近似近邻搜索← 正确答案
- B.把向量变成可排序的字符串
- C.替代向量化模型生成向量
- D.将对象元数据导出到外部仓库
答案:A。HNSW 是分层小世界图索引,专门解决“千万级向量中快速找近邻”的问题。它不能生成向量,也不负责导出元数据,更不是字符串排序工具。
Q6. where 条件 operator: And 通常表示?
- A.至少一个条件成立
- B.所有子条件同时成立← 正确答案
- C.所有子条件都不成立
- D.只对文本字段生效
答案:B。And 是逻辑与,要求 operands 中所有子条件都成立,就像“既要推理类型,又要 2018 年以后”。Or 才表示至少一个成立。
Q7. hybrid search 中 alpha 接近 1 的含义是?
- A.完全依赖关键词 BM25 得分
- B.完全依赖向量语义相似度← 正确答案
- C.表示索引使用 HNSW 的层数
- D.表示过滤条件必须满足
答案:B。混合检索得分是 alpha * 向量分 + (1-alpha) * BM25分,所以 alpha 大时向量语义占主导,alpha 小则更依赖字面关键词。它与 HNSW 层数和过滤强度无关。
Q8. 下列哪些属于 Weaviate 可接入的向量化模块?
- A.text2vec-openai← 正确答案
- B.text2vec-transformers← 正确答案
- C.multi2vec-clip← 正确答案
- D.mysql-vectorizer
答案:ABC。Weaviate 模块生态包括 OpenAI、Transformers、CLIP 等多类向量器,满足文本与多模态场景。mysql-vectorizer 不是真实模块,数据库产品也不会在 Weaviate 里负责向量化。
Q9. 一次 GraphQL 语义查询中,可以同时带上哪些约束?
- A.nearText 语义条件← 正确答案
- B.where 标量过滤← 正确答案
- C.sort 结果排序← 正确答案
- D.insert 数据写入
答案:ABC。GraphQL 查询可以把语义近邻、where 过滤、limit 限制、sort 排序组合在同一个请求中。insert 属于写入操作,不属于查询约束,也不会写在查询请求里。
Q10. 关于 Weaviate 架构与优化,下列说法正确的是?
- A.对象与向量一体化存储,写操作更易保持一致性← 正确答案
- B.不同 Class 可以配置不同的向量化模块← 正确答案
- C.检索延迟受嵌入、索引、过滤、后处理共同影响← 正确答案
- D.使用向量索引后,标量过滤条件会被忽略
答案:ABC。一体化存储让对象与向量同事务写入,模块化允许按 Class 换模型,延迟可以拆成嵌入、近邻搜索、过滤和后处理四段。D 是误区:过滤条件始终参与查询执行,并不是有了向量索引就会被忽略。