Build the `TextFields` payload for an `OpCode::VectorSearch` request. The native protocol reserves wire byte 68 for the optional `TextFields::filters: Option >` field. When the trait caller passes a non-`None` `MetadataFilter`, the predicate is serialized here so it travels alongside the SQL/DSL request rather than being dropped at the client. Wire-format note: the inline doc on `TextFiel
(
collection: &str,
query: &[f32],
k: usize,
filter: Option<&MetadataFilter>,
)
| 86 | /// both sides to a single agreed encoding; for now the bytes are |
| 87 | /// observable as non-empty, which is what the trait contract requires. |
| 88 | pub(super) fn build_vector_search_request( |
| 89 | collection: &str, |
| 90 | query: &[f32], |
| 91 | k: usize, |
| 92 | filter: Option<&MetadataFilter>, |
| 93 | ) -> NodeDbResult<TextFields> { |
| 94 | // Serialization failure here must surface to the caller. Dropping |
| 95 | // the filter and sending the request anyway would send the query |
| 96 | // to the server without the caller's predicate — exactly the |
| 97 | // silent-drop pattern this client guards against. |
| 98 | let filters_bytes = match filter { |
| 99 | Some(f) => Some(sonic_rs::to_vec(f).map_err(|e| { |
| 100 | NodeDbError::serialization("json", format!("vector_search metadata filter: {e}")) |
| 101 | })?), |
| 102 | None => None, |
| 103 | }; |
| 104 | Ok(TextFields { |
| 105 | collection: Some(collection.to_string()), |
| 106 | query_vector: Some(query.to_vec()), |
| 107 | top_k: Some(k as u32), |
| 108 | filters: filters_bytes, |
| 109 | ..Default::default() |
| 110 | }) |
| 111 | } |
| 112 | |
| 113 | /// Format a `&[f32]` as a SQL `ARRAY[...]` literal. |
| 114 | pub(super) fn format_f32_array(arr: &[f32]) -> String { |