Native Extension¶
The native extension bridges zvec's C++ API to Ruby using Rice v4.11.
File Organization¶
Binding files are split by zvec domain. They are initialized in dependency order from zvec_ext.cpp:
| File | Purpose | Dependencies |
|---|---|---|
zvec_types.cpp |
Enum constants (DataType, MetricType, etc.) | None |
zvec_status.cpp |
Status class and exception hierarchy | Types |
zvec_params.cpp |
Index params, query params, CollectionOptions, VectorQuery | Types |
zvec_schema.cpp |
FieldSchema, CollectionSchema, CollectionStats | Params |
zvec_doc.cpp |
Doc with typed field get/set | Schema, Types |
zvec_collection.cpp |
Collection CRUD and query operations | All above |
zvec_config.cpp |
Global configuration | Status |
Error Handling Pattern¶
zvec_common.hpp defines two error-handling helpers used throughout the bindings:
throw_if_error(status)¶
Converts a C++ Status to the appropriate Ruby exception:
void throw_if_error(const zvec::Status& status) {
if (status.ok()) return;
// maps StatusCode to Zvec::*Error and calls rb_raise
}
unwrap_result(expected)¶
Unwraps a tl::expected<T, Status>, returning the value on success or raising on error:
template <typename T>
T unwrap_result(const tl::expected<T, zvec::Status>& result) {
if (result.has_value()) return result.value();
throw_if_error(result.error());
}
The SharedPtr Pattern¶
Rice wraps shared_ptr<Collection> as Std::SharedPtr<zvec::Collection>, a proxy class that delegates method calls to the underlying C++ object via method_missing.
Ruby convenience methods (like query_vector) cannot be defined on Zvec::Collection directly because Rice's proxy class isn't the same as Zvec::Collection. Instead, lib/zvec.rb uses ObjectSpace to discover the proxy class at load time and mix in the CollectionConvenience module:
ObjectSpace.each_object(Class) do |klass|
if klass.name&.start_with?("Std::SharedPtr") && klass.name&.include?("Collection")
klass.include(Zvec::CollectionConvenience)
end
end
This is the key architectural detail that enables the query_vector helper and the to_h doc conversion to work naturally.
Type Dispatch in Doc¶
zvec_doc.cpp implements doc_set_field and doc_get_field as large switch statements over DataType. This is necessary because C++ templates can't be dispatched at runtime — each type requires explicit instantiation:
case zvec::DataType::STRING:
doc.set<std::string>(name, ...);
break;
case zvec::DataType::INT32:
doc.set<int32_t>(name, ...);
break;
case zvec::DataType::VECTOR_FP32: {
// Convert Ruby Array → std::vector<float>
Rice::Array arr(value);
std::vector<float> vec(arr.size());
for (size_t i = 0; i < arr.size(); i++)
vec[i] = From_Ruby<float>().convert(arr[i].value());
doc.set<std::vector<float>>(name, std::move(vec));
break;
}
The Ruby-side Doc#to_h(schema) method leverages this by iterating over field names and calling get_field with the data type from the schema.
Sparse Vector Serialization¶
Sparse vectors are represented differently in Ruby (Hash) and C++ (pair of index/value vectors). The binding handles the conversion:
- Ruby → C++: Hash
{ 42 => 0.8, 99 => 0.3 }→pair<vector<uint32_t>, vector<float>> - C++ → Ruby:
pair<vector<uint32_t>, vector<float>>→ Hash{ 42 => 0.8, 99 => 0.3 }