1use super::REPRODUCIBLE_CREATED;
7use super::SectionEncoding;
8use super::UrnaFileBuilder;
9use super::payload::{encode_embeddings_payload, maybe_zstd};
10use super::text_codec;
11use crate::chunk::{chunk_id, validate_chunk};
12use crate::error::UrnaError;
13use crate::layout::{
14 REQUIRED_SECTIONS, SECTION_ALIGNMENT, SECTION_BLOB_DATA, SECTION_BLOB_REFS,
15 SECTION_BLOB_SPAN_OVERLAY, SECTION_BM25_INDEX, SECTION_CHUNK_IDS, SECTION_CHUNKS_CANONICAL,
16 SECTION_CHUNKS_ORIGINAL_SPANS, SECTION_EMBEDDINGS, SECTION_ENCODING_INTPACK,
17 SECTION_ENCODING_RAW, SECTION_GRAPH_ADJACENCY, SECTION_HNSW_INDEX, SECTION_PROVENANCE,
18 SECTION_SEARCH_CONTRACT, SECTION_SPACE_TABLE, SectionEntry, URNA_FOOTER_SIZE, URNA_HEADER_SIZE,
19 URNA_SECTION_ENTRY_SIZE, UrnaFooter, UrnaHeader, align_up,
20};
21use crate::sections::{
22 OriginalSpan, SearchContract, encode_chunk_ids, encode_chunk_ids_intpack,
23 encode_chunks_canonical, encode_chunks_original_spans, encode_chunks_original_spans_intpack,
24 encode_provenance, encode_search_contract,
25};
26use sha2::{Digest, Sha256};
27
28impl UrnaFileBuilder {
29 pub fn build_bytes(mut self) -> crate::Result<Vec<u8>> {
31 if self.reproducible {
32 self.manifest.created = Some(REPRODUCIBLE_CREATED.into());
33 }
34 self.manifest.validate()?;
36
37 let embedding_dim = self.manifest.embedding_dim as usize;
39 if self.chunks.len() as u64 != self.manifest.n_chunks {
40 return Err(UrnaError::ManifestInvalid(format!(
41 "n_chunks={} but builder has {} chunks",
42 self.manifest.n_chunks,
43 self.chunks.len()
44 )));
45 }
46 for c in &self.chunks {
47 validate_chunk(c, embedding_dim)?;
48 }
49
50 let chunk_ids: Vec<String> = self
52 .chunks
53 .iter()
54 .map(|c| {
55 chunk_id(
56 &c.canonical_text,
57 &c.source_uri,
58 c.byte_start,
59 c.byte_end,
60 &self.manifest.chunker_version,
61 )
62 })
63 .collect();
64
65 let canonical_texts: Vec<String> = self
66 .chunks
67 .iter()
68 .map(|c| c.canonical_text.clone())
69 .collect();
70 let original_spans: Vec<OriginalSpan> = self
71 .chunks
72 .iter()
73 .map(|c| OriginalSpan {
74 source_uri: c.source_uri.clone(),
75 byte_start: c.byte_start,
76 byte_end: c.byte_end,
77 })
78 .collect();
79
80 let embeddings_bytes = encode_embeddings_payload(self.dtype, &self.chunks, embedding_dim)?;
81
82 let contract = SearchContract {
83 metric: self.manifest.metric.clone(),
84 score_type: self.manifest.score_type.clone(),
85 normalize: self.manifest.normalize.clone(),
86 index_type: self.manifest.index_type.clone(),
87 rerank_policy: self.manifest.rerank_policy.clone(),
88 };
89
90 let text_enc = self.text_encoding;
101 let compressed = matches!(text_enc, SectionEncoding::Zstd);
102 let mut sections: Vec<(u32, u32, Vec<u8>)> = Vec::with_capacity(8);
103
104 let chunk_ids_section = match compressed.then(|| encode_chunk_ids_intpack(&chunk_ids)) {
105 Some(Some(packed)) => (SECTION_CHUNK_IDS, SECTION_ENCODING_INTPACK, packed),
106 _ => (
107 SECTION_CHUNK_IDS,
108 SECTION_ENCODING_RAW,
109 encode_chunk_ids(&chunk_ids)?,
110 ),
111 };
112 sections.push(chunk_ids_section);
113
114 if compressed {
123 let choice = text_codec::choose(&canonical_texts)?;
124 sections.push(choice.canonical);
125 sections.extend(choice.aux);
126 } else {
127 let canonical_raw = encode_chunks_canonical(&canonical_texts)?;
128 sections.push(maybe_zstd(
129 SECTION_CHUNKS_CANONICAL,
130 text_enc,
131 canonical_raw,
132 )?);
133 }
134
135 let spans_raw = encode_chunks_original_spans(&original_spans)?;
136 let spans_section = if compressed {
137 let zst = maybe_zstd(SECTION_CHUNKS_ORIGINAL_SPANS, text_enc, spans_raw)?;
138 let packed = encode_chunks_original_spans_intpack(&original_spans);
139 if packed.len() < zst.2.len() {
140 (
141 SECTION_CHUNKS_ORIGINAL_SPANS,
142 SECTION_ENCODING_INTPACK,
143 packed,
144 )
145 } else {
146 zst
147 }
148 } else {
149 maybe_zstd(SECTION_CHUNKS_ORIGINAL_SPANS, text_enc, spans_raw)?
150 };
151 sections.push(spans_section);
152 sections.push((SECTION_EMBEDDINGS, self.dtype.encoding(), embeddings_bytes));
153 sections.push(maybe_zstd(
154 SECTION_PROVENANCE,
155 text_enc,
156 encode_provenance(&self.provenance)?,
157 )?);
158 sections.push(maybe_zstd(
159 SECTION_SEARCH_CONTRACT,
160 text_enc,
161 encode_search_contract(&contract)?,
162 )?);
163
164 if let Some(payload) = self.hnsw_index.take() {
165 sections.push((SECTION_HNSW_INDEX, SECTION_ENCODING_RAW, payload));
168 }
169 if let Some(payload) = self.bm25_index.take() {
170 sections.push(maybe_zstd(SECTION_BM25_INDEX, text_enc, payload)?);
173 }
174 if let Some(payload) = self.graph_adjacency.take() {
175 sections.push((SECTION_GRAPH_ADJACENCY, SECTION_ENCODING_RAW, payload));
180 }
181 if let Some(payload) = self.blob_refs.take() {
182 sections.push((SECTION_BLOB_REFS, SECTION_ENCODING_RAW, payload));
186 }
187 if let Some(payload) = self.blob_data.take() {
188 sections.push((SECTION_BLOB_DATA, SECTION_ENCODING_RAW, payload));
193 }
194 if let Some(payload) = self.blob_span_overlay.take() {
195 sections.push((SECTION_BLOB_SPAN_OVERLAY, SECTION_ENCODING_RAW, payload));
199 }
200 if let Some(payload) = self.space_table.take() {
201 sections.push((SECTION_SPACE_TABLE, SECTION_ENCODING_RAW, payload));
205 }
206 for (band_id, encoding, payload) in std::mem::take(&mut self.space_bands) {
207 sections.push((band_id, encoding, payload));
212 }
213
214 debug_assert!(
216 REQUIRED_SECTIONS
217 .iter()
218 .all(|(id, _)| sections.iter().any(|s| s.0 == *id))
219 );
220
221 let manifest_json = self.manifest.to_canonical_json()?;
223
224 sections.sort_by_key(|s| s.0);
226 let section_table_count = sections.len() as u64;
227 let section_table_size = section_table_count * URNA_SECTION_ENTRY_SIZE as u64;
228 let header_size = URNA_HEADER_SIZE as u64;
229 let manifest_offset = header_size + section_table_size;
230 let manifest_size = manifest_json.len() as u64;
231
232 let mut section_entries: Vec<SectionEntry> = Vec::with_capacity(sections.len());
233 let mut current_offset = align_up(manifest_offset + manifest_size, SECTION_ALIGNMENT);
234 for (id, encoding, data) in §ions {
235 let mut entry = SectionEntry::new(*id, current_offset, data.len() as u64);
236 entry.encoding = *encoding;
237 section_entries.push(entry);
238 let after = current_offset + data.len() as u64;
239 current_offset = align_up(after, SECTION_ALIGNMENT);
240 }
241
242 let last_section_end = match (section_entries.last(), sections.last()) {
245 (Some(entry), Some((_, _, data))) => entry.offset + data.len() as u64,
246 _ => manifest_offset + manifest_size,
247 };
248 let data_end = last_section_end as usize;
249 let file_size = data_end + URNA_FOOTER_SIZE;
250 let mut buf = vec![0u8; file_size];
251
252 let mut header = UrnaHeader::new(
254 self.manifest.embedding_dim,
255 self.manifest.n_chunks,
256 self.chunks.len() as u64,
257 file_size as u64,
258 header_size,
259 section_table_count,
260 manifest_offset,
261 manifest_size,
262 );
263 buf[..URNA_HEADER_SIZE].copy_from_slice(header.as_bytes());
264
265 for (i, entry) in section_entries.iter().enumerate() {
267 let off = URNA_HEADER_SIZE + i * URNA_SECTION_ENTRY_SIZE;
268 buf[off..off + URNA_SECTION_ENTRY_SIZE].copy_from_slice(entry.as_bytes());
269 }
270
271 let manifest_off = manifest_offset as usize;
273 buf[manifest_off..manifest_off + manifest_json.len()].copy_from_slice(&manifest_json);
274
275 for (i, (_, _, data)) in sections.iter().enumerate() {
280 let entry_off = URNA_HEADER_SIZE + i * URNA_SECTION_ENTRY_SIZE;
281 let data_off = section_entries[i].offset as usize;
282 buf[data_off..data_off + data.len()].copy_from_slice(data);
283 let hash = Sha256::digest(data);
284 buf[entry_off + 24..entry_off + 32].copy_from_slice(&hash[..8]);
285 }
286
287 let footer_hash = UrnaFooter::compute_file_hash(&buf[..data_end]);
289 buf[data_end..data_end + 8].copy_from_slice(&URNA_FOOTER_SIZE.to_le_bytes());
290 buf[data_end + 8..file_size].copy_from_slice(&footer_hash);
291
292 header.compute_checksum();
294 buf[..URNA_HEADER_SIZE].copy_from_slice(header.as_bytes());
295
296 Ok(buf)
297 }
298}