Skip to main content

urna_format/sections/
contract.rs

1//! `search_contract` section (`SECTION_SEARCH_CONTRACT = 0x06`). A
2//! versioned JSON document mirroring the manifest's search-related
3//! fields (metric, score_type, normalize, index_type, rerank_policy).
4//! The reader cross-checks it against the manifest at open time; any
5//! disagreement is rejected.
6
7use super::codec::Cursor;
8use crate::error::UrnaError;
9use crate::layout::{
10    SECTION_PAYLOAD_PREFIX_SIZE, SECTION_PAYLOAD_VERSION, SECTION_SEARCH_CONTRACT,
11};
12
13#[derive(Clone, Debug, PartialEq, serde::Serialize, serde::Deserialize)]
14pub struct SearchContract {
15    pub metric: String,
16    pub score_type: String,
17    pub normalize: String,
18    pub index_type: String,
19    pub rerank_policy: String,
20}
21
22pub fn encode_search_contract(contract: &SearchContract) -> crate::Result<Vec<u8>> {
23    let json = serde_json::to_vec(contract)?;
24    let json_len = u32::try_from(json.len())
25        .map_err(|_| UrnaError::InvalidInput("search_contract JSON too large".into()))?;
26    let mut buf = Vec::with_capacity(SECTION_PAYLOAD_PREFIX_SIZE + json.len());
27    buf.extend_from_slice(&SECTION_PAYLOAD_VERSION.to_le_bytes());
28    buf.extend_from_slice(&(json_len as u64).to_le_bytes());
29    buf.extend_from_slice(&json);
30    Ok(buf)
31}
32
33pub fn decode_search_contract(data: &[u8]) -> crate::Result<SearchContract> {
34    let mut c = Cursor::new(data, SECTION_SEARCH_CONTRACT);
35    if c.data.len() < SECTION_PAYLOAD_PREFIX_SIZE {
36        return Err(c.malformed("payload shorter than prefix"));
37    }
38    let version = c.read_u32()?;
39    if version != SECTION_PAYLOAD_VERSION {
40        return Err(UrnaError::UnsupportedSectionVersion {
41            section_id: SECTION_SEARCH_CONTRACT,
42            version,
43        });
44    }
45    let json_len = c.read_u64()? as usize;
46    let json = c.read_bytes(json_len)?;
47    let contract: SearchContract = serde_json::from_slice(json).map_err(UrnaError::Json)?;
48    c.finish()?;
49    Ok(contract)
50}
51
52#[cfg(test)]
53mod tests {
54    use super::*;
55
56    #[test]
57    fn roundtrip() {
58        let c = SearchContract {
59            metric: "ip".into(),
60            score_type: "cosine".into(),
61            normalize: "l2".into(),
62            index_type: "exact".into(),
63            rerank_policy: "none".into(),
64        };
65        let bytes = encode_search_contract(&c).unwrap();
66        let back = decode_search_contract(&bytes).unwrap();
67        assert_eq!(c, back);
68    }
69}