跳转至

混合引擎#

查询与后端解耦#

一条 NLQL 查询在语义层面是确定的:取哪些候选、按什么过滤、如何排序。但它交给哪个存储后端执行,是另一件事——NLQL 把这两层分开。同一句查询可以跑在内置存储、Faiss、Qdrant、Chroma、PgVector 上,结果逐条一致,区别只在于性能。

from nlql import Engine, FakeEmbedder
from nlql.store.qdrant_store import QdrantStore

engine = Engine(FakeEmbedder(dim=64), store=QdrantStore(location=":memory:"))

后端切换不改查询代码。Engine 通过一个统一的 Store 接口与具体存储对话;每个后端用自己的原生能力实现这个接口。

交给后端,还是在内存里算#

查询里的 WHERE 条件分两种命运:

  • 能交给后端原生能力处理的,引擎直接翻译过去。例如元数据过滤在 Qdrant 里变成 Qdrant 的 Filter,在 Postgres 里变成 SQL WHERE,在内置存储里变成 numpy 掩码。后端在自己的查询引擎里就完成过滤,返回的候选已经满足条件。
  • 后端表达不了的,引擎在返回的候选上再过一遍内存。典型情形是自定义 Python 函数谓词、复杂正则、需要业务逻辑的判断。

这个拆分由 Planner 根据后端的能力声明自动决定,调用方不感知。当整条查询都能由后端处理时,就是一次纯粹的委托,没有内存环节。

用 explain 看拆分#

engine.explain(query) 返回查询计划,其中 filter 部分明确列出哪些条件交给了后端(pushed),哪些留作内存后置(residual)。

from nlql import Engine, FakeEmbedder
from nlql.store import LocalStore
from nlql.store.faiss_store import FaissStore

query = """
    SELECT SENTENCE
    LET rel = SIMILARITY(content, "deep learning networks")
    WHERE meta.status == "published" AND meta.year >= 2024
    ORDER BY rel DESC
    LIMIT 3
"""

for name, store in [("LocalStore", LocalStore()), ("FaissStore", FaissStore())]:
    engine = Engine(FakeEmbedder(dim=64), store=store)
    engine.add_text("Neural networks power deep learning.", metadata={"status": "published", "year": 2025})
    plan = engine.explain(query)
    print(name, "pushed=", plan["filter"]["pushed"] is not None,
          "residual=", plan["filter"]["residual"] is not None)

内置存储把元数据过滤编成 numpy 掩码,pushed 非空;Faiss 自身没有元数据过滤能力,meta.statusmeta.year 都进入 residual,在内存里完成。两个后端的最终结果相同。

传给 Store 的是数据,不是闭包

交给后端的过滤条件是一段 IR(查询的 WHERE 子表达式),而不是一个编译好的 Python 函数。这样每个适配器都能把它翻译成自己后端的原生查询语法——Qdrant Filter、Chroma where 子句、SQL WHERE。这也是为什么拆分能由 Planner 在执行前就规划好。

结果一致,性能不同#

跨后端的一致性是契约。同一句查询在内置存储、Faiss、HnswLib、Qdrant、Chroma 上返回的候选与分数逐条相同(由跨后端测试保证)。性能差异来自三处:

  • 召回方式:内置存储用 numpy 做精确点积;HnswLib 用近似最近邻(适合百万级以上);Qdrant/Chroma 用各自的 ANN 实现。
  • 过滤位置:能交给后端的过滤在召回时就生效,候选更少;走内存的过滤要先把过取的候选拉回来再筛。
  • 是否过取:当过滤留在内存时,引擎会向后端多取一些候选(scan + 内存过滤),保证不漏命中。

所以"让后端多做"不只是更快,在大数据量下也更省传输。把过滤尽量交给后端是提升查询吞吐的主要杠杆之一。

自定义后端怎么办#

实现一个 Store,返回诚实的 StoreCaps(声明你的后端支持向量检索、是否原生支持元数据过滤、是否原生支持全文)。引擎据此决定哪些条件交给你、哪些自己留。能整条处理就整条处理,处理不了的引擎自然接手,无需你写回退逻辑。详见 Store 接口

下一步#