Skip to content

perf: batch convert docs to achieve acceleration - #647

Open
zzlin237 wants to merge 9 commits into
alibaba:mainfrom
zzlin237:reduce-overhead
Open

perf: batch convert docs to achieve acceleration#647
zzlin237 wants to merge 9 commits into
alibaba:mainfrom
zzlin237:reduce-overhead

Conversation

@zzlin237

@zzlin237 zzlin237 commented Aug 3, 2026

Copy link
Copy Markdown
Collaborator

Change the query result materialization from converting each document individually across the Python/C++ boundary into a PyDoc, to batch-materializing results as lightweight tuples according to the schema in a single C++ call and then constructing Doc objects via Doc._from_tuple on the Python side.
This aims to eliminate the per-document language binding conversion overhead on the query hot path, thereby reducing query latency.
Dataset: cohere-1m, M: 15

recall QPS
94.0% 15868.8129 -> 16825.6338
92.66% 17108.384 -> 18478.759
91.76% 18642.1531 -> 19924.5225

Copilot AI review requested due to automatic review settings August 3, 2026 09:22
@zzlin237
zzlin237 requested a review from Cuiyus as a code owner August 3, 2026 09:22

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Note

Copilot couldn't run its full agentic review because it didn't start before the timeout. Make sure your repository has a runner available, or add a copilot-code-review.yml file specifying one with the runs-on attribute. See the docs for more details.

This PR changes the Python query hot path to avoid per-document Python/C++ binding overhead by batch-materializing query results as lightweight tuples in C++, then constructing Doc objects from those tuples in Python.

Changes:

  • Refactors _Collection.Query bindings to return a batch of (id, score, fields, vectors) tuples, parameterized by schema.
  • Extracts per-doc materialization into a shared C++ helper (ZVecPyDoc::doc_to_tuple) and reuses it for both per-doc and batch paths.
  • Updates the Python QueryExecutor to consume tuples via Doc._from_tuple, and adds correctness tests for the new batch-materialized behavior.

Reviewed changes

Copilot reviewed 6 out of 6 changed files in this pull request and generated 3 comments.

Show a summary per file
File Description
src/binding/python/model/python_doc.cc Extracts get_all logic into doc_to_tuple and binds get_all to it.
src/binding/python/model/python_collection.cc Changes Query bindings to return batch-materialized tuples and adds docs_to_tuples helper.
src/binding/python/include/python_doc.h Declares doc_to_tuple and adds schema dependency for the new signature.
python/zvec/executor/query_executor.py Switches query execution to consume tuple batches and build Doc via Doc._from_tuple.
python/tests/test_query_executor.py Adjusts unit test to patch Doc._from_tuple instead of convert_to_py_doc.
python/tests/test_batch_materialize.py Adds integration-style correctness tests for batch-materialized query results.
Suppressed comments (1)

src/binding/python/include/python_doc.h:1

  • Including <zvec/db/schema.h> in this public binding header increases compile-time coupling for every translation unit that includes python_doc.h. Since doc_to_tuple only needs CollectionSchema by reference in the declaration, you can forward-declare class CollectionSchema; in namespace zvec here and move the schema include into the corresponding .cc file.
// Copyright 2025-present the zvec project

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread src/binding/python/model/python_collection.cc
Comment thread src/binding/python/model/python_collection.cc
Comment thread python/tests/test_query_executor.py Outdated
Copilot AI review requested due to automatic review settings August 4, 2026 03:00

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

Comment thread src/binding/python/model/python_collection.cc
Result<CollectionSchema> schema_result;
{
py::gil_scoped_release release;
result = self.Query(query);

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

self.Query(query) & self.Schema() 不是同一把锁吧? 并发情况下,如果drop_column,会不会有解析丢字段的问题?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

确实有这个问题,Query() 和Schema()是两次独立的加锁,两者之间存在无锁窗口。新增 QueryWithSchema 原子接口,在同一把 schema 读锁内返回 docs 与查询所用的 schema 快照(QuerySnapshot),绑定层改为单次调用,消除了原 Query() + Schema() 两次加锁之间可被 DropColumn 插入的窗口

@Cuiyus Cuiyus Aug 20, 2026

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

我觉得当前的解决方案不太优雅。为了解决pybind 层带来的开销,我们要去维护一个query_with_schema的接口。(或者至少这个接口应该是internal的)。

能不能将这个batch的操作在python & pybind层实现。(在python 有一个比较恶心的问题,就是需要在python 层加锁,去保证 query & get_schema 的原子操作。) @egolearner @feihongxu0824 @zhourrr 看下有啥好的法子没

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

不增加公共api的一个思路,供参考

建议核心模板叫 query_result_snapshot_impl,避免 DB 层出现 Python 语义;execute_for_python 留在 binding 层。实现分三层。

1. 内部结果类型

新增非公开头文件 src/db/collection_query_internal.h

#pragma once

#include <memory>
#include <zvec/db/collection.h>

namespace zvec::internal {

struct QueryResultSnapshot {
  DocPtrList docs;
  std::shared_ptr<const CollectionSchema> schema;
};

Result<QueryResultSnapshot> query_result_snapshot(
    const Collection &collection, const SearchQuery &query);

}  // namespace zvec::internal

如果当前确实需要支持 MultiQuery,再增加一个同名 overload;否则不要提前泛化。

2. CollectionImpl 内部模板

collection.ccCollectionImpl 中声明:

template <typename Query>
Result<internal::QueryResultSnapshot> query_result_snapshot_impl(
    const Query &query) const;

模板只在 collection.cc 使用,因此定义也留在 .cc,不暴露 CollectionImpl

template <typename Query>
Result<internal::QueryResultSnapshot>
CollectionImpl::query_result_snapshot_impl(const Query &query) const {
  std::shared_lock lock(schema_handle_mtx_);

  CHECK_DESTROY_RETURN_STATUS_EXPECTED(destroyed_, false);
  CHECK_CLOSED_RETURN_STATUS_EXPECTED(closed_, false);

  auto docs = query_unsafe(query);
  if (!docs) {
    return tl::make_unexpected(docs.error());
  }

  // schema_ 与 query_unsafe 使用的 schema 属于同一个读锁区间。
  // shared_ptr 只增加引用计数,不复制 CollectionSchema。
  std::shared_ptr<const CollectionSchema> schema_snapshot = schema_;

  return internal::QueryResultSnapshot{
      std::move(docs.value()),
      std::move(schema_snapshot),
  };
}

这里的关键点:

  • query_unsafe() 不再加锁,由调用者保证锁。
  • 查询完成和捕获 schema_ 都发生在同一个共享锁内。
  • DDL 需要独占锁,因此不能在两者之间替换 schema。
  • DDL 采用 clone-and-swap;解锁后即使发生 DropColumn,旧 schema 仍由 shared_ptr 保活。
  • 不进行 CollectionSchema 深拷贝。
  • Python 对象转换不在锁内执行,避免长时间阻塞 DDL。

3. 内部 free function

binding 只有 Collection&,看不到 CollectionImpl,因此在 collection.cc 提供一个内部转发:

namespace zvec::internal {

Result<QueryResultSnapshot> query_result_snapshot(
    const Collection &collection, const SearchQuery &query) {
  const auto *impl = dynamic_cast<const CollectionImpl *>(&collection);
  if (impl == nullptr) {
    return tl::make_unexpected(
        Status::NotSupported("Unsupported Collection implementation"));
  }

  return impl->query_result_snapshot_impl(query);
}

}  // namespace zvec::internal

dynamic_cast 的成本相对查询可以忽略,并且比假设具体实现后直接 static_cast 更安全。

该函数不进入公开 collection.h,也不增加 Collection virtual,所以不改变 C++ vtable。

4. Python binding

binding 层才叫 execute_for_python

template <typename Query>
py::list execute_for_python(const Collection &collection,
                            const Query &query) {
  Result<internal::QueryResultSnapshot> result;

  {
    py::gil_scoped_release release;
    result = internal::query_result_snapshot(collection, query);
  }

  // 到这里 GIL 已恢复,schema 读锁已经释放。
  auto snapshot = unwrap_expected(std::move(result));

  return docs_to_tuples(snapshot.docs, *snapshot.schema);
}

绑定保持简单:

col.def(
    "Query",
    [](const Collection &collection, const SearchQuery &query) {
      return execute_for_python(collection, query);
    },
    py::arg("query"));

完整时序是:

释放 GIL
  → 获取 schema 读锁
  → 执行一个 query
  → O(1) 保留同版本 schema 指针
  → 释放 schema 读锁
恢复 GIL
  → 将整批 Doc 转成 list[tuple]
  → 返回 Python

建议额外让 docs_to_tuples 每批只调用一次 schema.forward_fields()schema.vector_fields(),不要在每个 doc_to_tuple 中重复构造字段列表。

这样既保持“一次 query、一批结果统一转换”,又把公开 Interface、锁、schema 生命周期和 Python 物化职责分得很清楚。

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

已采用该方案

Comment thread src/binding/python/model/python_doc.cc
@zzlin237 zzlin237 changed the title Batch convert docs to achieve acceleration perf: Batch convert docs to achieve acceleration Aug 17, 2026
@zzlin237 zzlin237 changed the title perf: Batch convert docs to achieve acceleration perf: batch convert docs to achieve acceleration Aug 17, 2026
# Conflicts:
#	src/binding/python/model/python_collection.cc
#	src/db/collection.cc
#	src/include/zvec/db/collection.h
# Conflicts:
#	tests/db/collection_test.cc
@zzlin237
zzlin237 requested a review from iaojnh as a code owner August 19, 2026 02:40
Comment thread src/include/zvec/db/collection.h Outdated
*/
struct ZVEC_API QuerySnapshot {
DocPtrList docs;
CollectionSchema schema;

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

schema可以用shared_ptr<CollectionSchema>,并且检查下schema变更必须先创建副本再修改,这样多数场景都只需要引用计数+1

struct名称改为QueryResultSnapshot? 或者更直白的DocsAndSchema

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

已采用

# Conflicts:
#	src/binding/python/model/python_collection.cc
#	src/db/collection.cc
upstream alibaba#688 made kTaskCount static constexpr, which already avoids the
MSVC capture requirement. The explicit capture left over from the merge
is invalid (static storage duration cannot be captured) and broke
clang-tidy. Align the file with upstream.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants