Skip to main content

haematite_cli/serve/storage/
embed_job.rs

1//! V2 and V2b (ruled 5 Oct 08:15 and 08:27): one page of the embedding job's
2//! work, committed into the branch's companion namespace with the job's
3//! position, in one commit.
4//!
5//! A page names at most the page setting's records (default 64, several
6//! worker calls), so one commit is bounded by the setting, never by the
7//! store. The branch itself never moves, so no client's `expected` is ever
8//! refused because the job wrote (Q1).
9//!
10//! The job walks the difference from the position's `through` to a target
11//! version, `toward`, in key order, a page at a time. A page in the middle of
12//! the walk moves its `after`; the page that ends it makes `toward` the new
13//! `through`. A crash between pages resumes after the last committed `after`,
14//! so no record is embedded twice and none is lost. Whether a page ends its
15//! walk is the job's claim: storage cannot tell a last page from one with
16//! records still after it, so V3 proves the job claims the end only when the
17//! difference has no more rows. Within one walk `after` never goes back, and
18//! a new walk cannot begin while one is open.
19//!
20//! A page's text was read at its walk's `toward`. A vector is written only
21//! while its record's revision at the branch's head is still the one its
22//! text was read at. A record rewritten or removed after `toward` is skipped:
23//! the next walk lists it again, so nothing is written as current for text it
24//! was not made from. A revision at or before `toward` that is not the one
25//! given means the page was not read there, and the whole page is refused.
26//!
27//! The whole page is also refused, with nothing written, for a vector whose
28//! length differs from the declaration's `dimensions` or one made by a model
29//! other than the declared one.
30//!
31//! EMB-E2: a record longer than the model reads may come back as its
32//! windows. They are written in the same commit as the rest of the page,
33//! every one or none, under `vw\0` keys with their spans, and the record's
34//! `v\0` vector of any earlier text is deleted. Any record a page touches
35//! loses the windows of its earlier text past those it now has: window 0
36//! names how many there are, so that is one read, never a scan.
37//!
38//! Owed elsewhere: a discarded branch's companion is removed in the same
39//! finishing sequence as the discard (the discard box), and a caller's
40//! `vector.put` to a field declared embedded is refused (V6).
41
42use std::num::NonZeroUsize;
43
44use haematite::branch::{CommitDurability, CommitRequest};
45
46use super::embedded::{self, Declaration, Field, ModelName, Position, Provenance, Toward};
47use super::{
48    BranchKind, Captured, DocumentRevision, Failure, Storage, branch_name, create_branch_with_kind,
49    current_timestamp, identifier, open_branch, reads, revision_key, version,
50};
51use crate::serve::protocol::Version;
52
53/// Records one commit may name when no setting says otherwise.
54pub const DEFAULT_PAGE_RECORDS: usize = 64;
55
56/// One record's vector, and the revision its text was read at.
57#[derive(Debug, Clone, PartialEq)]
58pub struct Made {
59    pub id: String,
60    pub made_from: Version,
61    pub vector: Vec<f32>,
62}
63
64/// EMB-E2: one window of a record's text, its bytes `[start, end)`, and its
65/// vector.
66#[derive(Debug, Clone, PartialEq)]
67pub struct Window {
68    pub start: u64,
69    pub end: u64,
70    pub vector: Vec<f32>,
71}
72
73/// EMB-E2: a record's windows in text order, and the revision its text was
74/// read at.
75#[derive(Debug, Clone, PartialEq)]
76pub struct MadeWindows {
77    pub id: String,
78    pub made_from: Version,
79    pub windows: Vec<Window>,
80}
81
82/// One page of the job's work on one declared field of one branch.
83#[derive(Debug, Clone, PartialEq)]
84pub struct Page {
85    pub collection: String,
86    pub field: String,
87    /// The model the worker's Ready named for these vectors.
88    pub model: ModelName,
89    pub made: Vec<Made>,
90    /// EMB-E2: records longer than the model reads, each embedded whole as
91    /// its windows.
92    pub windowed: Vec<MadeWindows>,
93    /// Records removed from the branch between the walk's start and
94    /// `toward`.
95    pub removed: Vec<String>,
96    /// RED4: records still there whose text at `toward` makes no vector,
97    /// too long in bytes or for the model: a vector made from an earlier
98    /// text is deleted, so none is kept for text the record no longer has.
99    pub cleared: Vec<String>,
100    /// The walk's target: the branch version the page's text was read at.
101    pub toward: Version,
102    /// How far through the walk this page reaches.
103    pub reach: Reach,
104}
105
106/// How far a page reaches in its walk.
107#[derive(Debug, Clone, PartialEq, Eq)]
108pub enum Reach {
109    /// The walk goes on after this record id; every record up to and
110    /// including it is finished.
111    Upto(String),
112    /// The job claims the walk has no records after this page's.
113    End,
114}
115
116/// What one page's commit did.
117#[derive(Debug, Clone, PartialEq, Eq)]
118pub struct Committed {
119    pub written: usize,
120    pub removed: usize,
121    /// Changed after `toward`: no vector written, and listed again.
122    pub skipped: Vec<String>,
123    pub position: Position,
124}
125
126const fn order(at: Version) -> (u64, u64) {
127    (at.generation, at.sequence)
128}
129
130/// The branch key of a field's `w\0` value, as vectors.rs lays it out.
131#[must_use]
132pub fn field_key(collection: &str, field: &str) -> Vec<u8> {
133    format!("w\0{collection}\0{field}").into_bytes()
134}
135
136/// Whose versions the kept `through` is, that `through`, and the last record
137/// the open walk finished, after checking the page belongs where the
138/// position is.
139///
140/// A position may name another branch, as a fork's inherited one names its
141/// parent (F-1): its `through` is that branch's version, and stays labelled
142/// so until the walk ends (V2c). An open walk in this companion is always
143/// this branch's own, since a companion is inherited only with no walk open
144/// (F-2).
145fn walk(
146    previous: Option<&Position>,
147    branch: &str,
148    page: &Page,
149) -> Result<(String, Option<Version>, Option<String>), Failure> {
150    let Some(previous) = previous else {
151        return Ok((branch.to_owned(), None, None));
152    };
153    if let Some(open) = &previous.toward {
154        if open.version != page.toward {
155            return Err(Failure::new(
156                "embed_walk_open",
157                "a new walk cannot begin while one is open",
158            ));
159        }
160        if let Reach::Upto(after) = &page.reach
161            && after.as_bytes() < open.after.as_bytes()
162        {
163            return Err(Failure::new(
164                "embed_walk_backwards",
165                "a page cannot reach back past the walk's last record",
166            ));
167        }
168        return Ok((
169            previous.branch.clone(),
170            previous.through,
171            Some(open.after.clone()),
172        ));
173    }
174    if previous.branch == branch
175        && previous
176            .through
177            .is_some_and(|through| order(page.toward) <= order(through))
178    {
179        return Err(Failure::new(
180            "embed_position_backwards",
181            "a walk must reach past the job's position",
182        ));
183    }
184    Ok((previous.branch.clone(), previous.through, None))
185}
186
187/// One page's companion writes, as they are gathered.
188struct Writes<'page> {
189    /// The companion's head before this page, `None` before its first.
190    at: Option<Captured>,
191    page: &'page Page,
192    pending: Vec<(Vec<u8>, Option<Vec<u8>>)>,
193    dimensions: usize,
194}
195
196impl Writes<'_> {
197    /// # Errors
198    /// `embed_vector_length` for a vector of another length than declared.
199    fn length(&self, id: &str, vector: &[f32]) -> Result<(), Failure> {
200        if vector.len() == self.dimensions {
201            return Ok(());
202        }
203        Err(Failure::new(
204            "embed_vector_length",
205            format!(
206                "record {id} has {} members; the field is declared with {}",
207                vector.len(),
208                self.dimensions
209            ),
210        ))
211    }
212}
213
214fn stale(id: &str) -> Failure {
215    Failure::new(
216        "embed_page_stale_text",
217        format!("record {id} was not read at the page's version"),
218    )
219}
220
221fn page_size(branch: &str, page: &Page, page_records: NonZeroUsize) -> Result<usize, Failure> {
222    identifier(branch)?;
223    identifier(&page.collection)?;
224    identifier(&page.field)?;
225    let named = page
226        .made
227        .len()
228        .saturating_add(page.windowed.len())
229        .saturating_add(page.removed.len())
230        .saturating_add(page.cleared.len());
231    if named > page_records.get() {
232        return Err(Failure::new(
233            "embed_page_too_large",
234            format!("a page names {named} records; the page setting is {page_records}"),
235        ));
236    }
237    Ok(named)
238}
239
240fn page_position(branch: &str, page: &Page, owner: String, through: Option<Version>) -> Position {
241    match &page.reach {
242        Reach::Upto(after) => Position {
243            branch: owner,
244            through,
245            toward: Some(Toward {
246                version: page.toward,
247                after: after.clone(),
248            }),
249        },
250        Reach::End => Position {
251            branch: branch.to_owned(),
252            through: Some(page.toward),
253            toward: None,
254        },
255    }
256}
257
258fn validate_page_walk(page: &Page, done: Option<&str>) -> Result<(), Failure> {
259    let ids = page
260        .made
261        .iter()
262        .map(|made| &made.id)
263        .chain(page.windowed.iter().map(|made| &made.id))
264        .chain(&page.removed)
265        .chain(&page.cleared);
266    for id in ids {
267        let past = done.is_some_and(|done| id.as_bytes() <= done.as_bytes());
268        let beyond = match &page.reach {
269            Reach::Upto(after) => id.as_bytes() > after.as_bytes(),
270            Reach::End => false,
271        };
272        if past || beyond {
273            return Err(Failure::new(
274                "embed_page_outside_walk",
275                format!("record {id} is not between the walk's last record and this page's"),
276            ));
277        }
278    }
279    Ok(())
280}
281
282impl Storage {
283    /// Commits `page` into `branch`'s companion: its vectors, its removals
284    /// and the new position, in one commit. The companion is made on the
285    /// first page.
286    ///
287    /// # Errors
288    /// `embed_page_too_large` past `page_records`; `branch_missing`;
289    /// `embed_position_ahead` for `toward` past the head;
290    /// `embed_field_not_declared`; `embed_model_mismatch`;
291    /// `embed_walk_open`; `embed_position_backwards`; `embed_walk_backwards`;
292    /// `embed_page_outside_walk`; `embed_vector_length`;
293    /// `embed_page_stale_text`; whatever a read or the commit answers.
294    pub fn embed_commit(
295        &mut self,
296        branch: &str,
297        page: &Page,
298        page_records: NonZeroUsize,
299    ) -> Result<Committed, Failure> {
300        let named = page_size(branch, page, page_records)?;
301        let record = self
302            .refs
303            .get(&branch_name(branch))
304            .ok_or_else(|| Failure::new("branch_missing", branch))?;
305        let head = Captured {
306            branch: branch.to_owned(),
307            version: version(record),
308            shards: record.shards.clone(),
309        };
310        if order(page.toward) > order(head.version) {
311            return Err(Failure::new(
312                "embed_position_ahead",
313                "a page cannot complete a version the branch has not reached",
314            ));
315        }
316        let declaration = self.declared(&head, &page.collection, &page.field)?;
317        let tag = embedded::model_tag(&page.model)?;
318        if tag != embedded::model_tag(&declaration.model)? {
319            return Err(Failure::new(
320                "embed_model_mismatch",
321                format!(
322                    "the vectors are {}'s; the field is declared for {}",
323                    page.model.name, declaration.model.name
324                ),
325            ));
326        }
327        let companion = embedded::companion_name(branch);
328        let position_key = embedded::position_key(&page.collection, &page.field);
329        let previous = self.position(&companion, &position_key)?;
330        let (owner, through, done) = walk(previous.as_ref(), branch, page)?;
331        let position = page_position(branch, page, owner, through);
332        validate_page_walk(page, done.as_deref())?;
333        let mut writes = Writes {
334            at: self.companion_at(&companion),
335            page,
336            pending: Vec::with_capacity(named.saturating_add(1)),
337            dimensions: declaration.dimensions.get(),
338        };
339        let (mut written, mut skipped) = (0_usize, Vec::new());
340        for made in &page.made {
341            if self.write_made(&head, &mut writes, made, tag)? {
342                written = written.saturating_add(1);
343            } else {
344                skipped.push(made.id.clone());
345            }
346        }
347        for made in &page.windowed {
348            if self.write_windows(&head, &mut writes, made, tag)? {
349                written = written.saturating_add(1);
350            } else {
351                skipped.push(made.id.clone());
352            }
353        }
354        for id in &page.removed {
355            if let Some(now) = self.revision(&head, &page.collection, id)?
356                && order(now) <= order(page.toward)
357            {
358                return Err(stale(id));
359            }
360            let key = embedded::vector_key(&page.collection, &page.field, id);
361            writes.pending.push((key, None));
362            self.clear_windows(&mut writes, id, 0)?;
363        }
364        // RED4: whatever the record holds now, no vector of its text at
365        // `toward` exists: one changed since is listed by the next walk.
366        for id in &page.cleared {
367            let key = embedded::vector_key(&page.collection, &page.field, id);
368            writes.pending.push((key, None));
369            self.clear_windows(&mut writes, id, 0)?;
370        }
371        self.end_rewalk(&mut writes)?;
372        let mut writes = writes.pending;
373        writes.push((position_key, Some(embedded::encode_position(&position)?)));
374        if self.refs.get(&companion).is_none() {
375            create_branch_with_kind(
376                &companion,
377                self.writer.base_roots().map_err(Failure::database)?,
378                BranchKind::Namespace,
379                &mut self.refs,
380                &self.registry,
381                current_timestamp(),
382            )
383            .map_err(Failure::branch_commit)?;
384        }
385        self.commit_namespace(&companion, &writes)?;
386        Ok(Committed {
387            written,
388            removed: page.removed.len(),
389            skipped,
390            position,
391        })
392    }
393
394    /// Writes `made`'s one vector when its text is current, clearing any
395    /// windows an earlier walk wrote for it (EMB-E2); `false` when skipped.
396    ///
397    /// # Errors
398    /// `embed_vector_length`; `embed_page_stale_text`; whatever a read answers.
399    fn write_made(
400        &self,
401        head: &Captured,
402        writes: &mut Writes<'_>,
403        made: &Made,
404        tag: embedded::ModelTag,
405    ) -> Result<bool, Failure> {
406        writes.length(&made.id, &made.vector)?;
407        if !self.current(head, writes.page, &made.id, made.made_from)? {
408            return Ok(false);
409        }
410        let provenance = Provenance {
411            made_from: made.made_from,
412            tag,
413        };
414        let value = embedded::encode_embedded(&provenance, &made.vector);
415        let key = embedded::vector_key(&writes.page.collection, &writes.page.field, &made.id);
416        writes.pending.push((key, Some(value)));
417        self.clear_windows(writes, &made.id, 0)?;
418        Ok(true)
419    }
420
421    /// EMB-E2 (j): a page that ends its field's walk removes the rewalk
422    /// marker, if there is one, in the same commit.
423    ///
424    /// # Errors
425    /// Whatever a read answers.
426    fn end_rewalk(&self, writes: &mut Writes<'_>) -> Result<(), Failure> {
427        let (Reach::End, Some(at)) = (&writes.page.reach, &writes.at) else {
428            return Ok(());
429        };
430        let key = embedded::rewalk_key(&writes.page.collection, &writes.page.field);
431        if reads::get_bytes(&self.reader, at, &key)?.is_some() {
432            writes.pending.push((key, None));
433        }
434        Ok(())
435    }
436
437    /// Whether `id`'s text at `head` is still the one read at `made_from`:
438    /// `false` when it changed after `toward`, and the next walk lists it.
439    ///
440    /// # Errors
441    /// `embed_page_stale_text` for text not read at the page's version.
442    fn current(
443        &self,
444        head: &Captured,
445        page: &Page,
446        id: &str,
447        made_from: Version,
448    ) -> Result<bool, Failure> {
449        if order(made_from) > order(page.toward) {
450            return Err(stale(id));
451        }
452        match self.revision(head, &page.collection, id)? {
453            Some(now) if now == made_from => Ok(true),
454            Some(now) if order(now) <= order(page.toward) => Err(stale(id)),
455            _ => Ok(false),
456        }
457    }
458
459    /// EMB-E2: `made`'s windows, each with its span and the window count,
460    /// replacing the record's single vector and any windows of an earlier
461    /// text past these; `false` when its text changed after `toward`.
462    ///
463    /// # Errors
464    /// `embed_vector_length`; `embed_windows_invalid` for fewer than two
465    /// windows, more than [`embedded::WINDOWS_MOST`], or spans that do not
466    /// cover the text in order; as [`Self::current`].
467    fn write_windows(
468        &self,
469        head: &Captured,
470        writes: &mut Writes<'_>,
471        made: &MadeWindows,
472        tag: embedded::ModelTag,
473    ) -> Result<bool, Failure> {
474        let page = writes.page;
475        let invalid = || {
476            Failure::new(
477                "embed_windows_invalid",
478                format!(
479                    "record {}'s windows do not cover its text in order",
480                    made.id
481                ),
482            )
483        };
484        let count = u32::try_from(made.windows.len()).map_err(|_| invalid())?;
485        if !(2..=embedded::WINDOWS_MOST).contains(&count) {
486            return Err(invalid());
487        }
488        let mut previous: Option<&Window> = None;
489        for window in &made.windows {
490            writes.length(&made.id, &window.vector)?;
491            let follows = previous.map_or(window.start == 0, |before| {
492                (before.start..=before.end).contains(&window.start)
493                    && window.start != before.start
494                    && window.end > before.end
495            });
496            if !follows || window.end <= window.start {
497                return Err(invalid());
498            }
499            previous = Some(window);
500        }
501        if !self.current(head, page, &made.id, made.made_from)? {
502            return Ok(false);
503        }
504        let provenance = Provenance {
505            made_from: made.made_from,
506            tag,
507        };
508        let key = embedded::vector_key(&page.collection, &page.field, &made.id);
509        writes.pending.push((key, None));
510        for (index, window) in (0..count).zip(&made.windows) {
511            let span = embedded::Span {
512                count,
513                start: window.start,
514                end: window.end,
515            };
516            let key = embedded::window_key(&page.collection, &page.field, &made.id, index);
517            let value = embedded::encode_window(span, &provenance, &window.vector);
518            writes.pending.push((key, Some(value)));
519        }
520        self.clear_windows(writes, &made.id, count)?;
521        Ok(true)
522    }
523
524    /// EMB-E2: deletes `id`'s windows from `keep` on, as the companion holds
525    /// them before this page: window 0 names their count, so this is one
526    /// read, never a scan.
527    ///
528    /// # Errors
529    /// `vector_corrupt` for a stored window 0 that names no count; whatever
530    /// the read answers.
531    fn clear_windows(&self, writes: &mut Writes<'_>, id: &str, keep: u32) -> Result<(), Failure> {
532        let Some(at) = &writes.at else {
533            return Ok(());
534        };
535        let page = writes.page;
536        let first = embedded::window_key(&page.collection, &page.field, id, 0);
537        let Some(bytes) = reads::get_bytes(&self.reader, at, &first)? else {
538            return Ok(());
539        };
540        let count = embedded::window_span(&bytes)?.count;
541        for index in keep..count {
542            let key = embedded::window_key(&page.collection, &page.field, id, index);
543            writes.pending.push((key, None));
544        }
545        Ok(())
546    }
547
548    /// The companion's head, `None` before its first page.
549    fn companion_at(&self, companion: &str) -> Option<Captured> {
550        self.refs.get(companion).map(|record| Captured {
551            branch: companion.to_owned(),
552            version: version(record),
553            shards: record.shards.clone(),
554        })
555    }
556
557    /// The declaration of an embedded field at `head`.
558    pub(super) fn declared(
559        &self,
560        head: &Captured,
561        collection: &str,
562        field: &str,
563    ) -> Result<Declaration, Failure> {
564        let not_declared = || {
565            Failure::new(
566                "embed_field_not_declared",
567                format!("{collection}.{field} is not declared embedded"),
568            )
569        };
570        let bytes = reads::get_bytes(&self.reader, head, &field_key(collection, field))?
571            .ok_or_else(not_declared)?;
572        match embedded::read_field(&bytes)? {
573            Field::Embedded(declaration) => Ok(declaration),
574            Field::Supplied { .. } => Err(not_declared()),
575        }
576    }
577
578    /// A record's revision at `head`, or `None` when it does not exist.
579    fn revision(
580        &self,
581        head: &Captured,
582        collection: &str,
583        id: &str,
584    ) -> Result<Option<Version>, Failure> {
585        reads::get_bytes(&self.reader, head, &revision_key(collection, id))?
586            .map(|bytes| {
587                serde_json::from_slice::<DocumentRevision>(&bytes)
588                    .map(|row| row.written_at)
589                    .map_err(Failure::storage)
590            })
591            .transpose()
592    }
593
594    /// The job's position in `companion`, or `None` before its first page.
595    ///
596    /// # Errors
597    /// Propagates companion read failures or an invalid stored embedding position.
598    pub fn position(&self, companion: &str, key: &[u8]) -> Result<Option<Position>, Failure> {
599        let Some(record) = self.refs.get(companion) else {
600            return Ok(None);
601        };
602        let at = Captured {
603            branch: companion.to_owned(),
604            version: version(record),
605            shards: record.shards.clone(),
606        };
607        reads::get_bytes(&self.reader, &at, key)?
608            .map(|bytes| embedded::read_position(&bytes))
609            .transpose()
610    }
611
612    /// Writes `declaration` as `collection.field`'s `w\0` value on `branch`.
613    /// It stands in for the declaring change (V6) and is to be removed when
614    /// that change is on main (ruled 08:49).
615    ///
616    /// # Errors
617    /// Propagates declaration encoding or namespace commit failures.
618    #[cfg(test)]
619    pub fn declare_for_tests(
620        &mut self,
621        branch: &str,
622        collection: &str,
623        field: &str,
624        declaration: &Declaration,
625    ) -> Result<(), Failure> {
626        let bytes = embedded::encode_declaration(declaration)?;
627        let key = field_key(collection, field);
628        self.commit_namespace(&branch_name(branch), &[(key, Some(bytes))])
629    }
630
631    /// Writes `position` for `collection.field` into `owner`'s companion,
632    /// making the companion if it has none. It stands in for a fork's
633    /// inheriting, which does not exist yet (ruled 08:55, F2).
634    ///
635    /// # Errors
636    /// Propagates position encoding, companion creation or namespace commit failures.
637    #[cfg(test)]
638    pub fn position_for_tests(
639        &mut self,
640        owner: &str,
641        collection: &str,
642        field: &str,
643        position: &Position,
644    ) -> Result<(), Failure> {
645        let companion = embedded::companion_name(owner);
646        if self.refs.get(&companion).is_none() {
647            create_branch_with_kind(
648                &companion,
649                self.writer.base_roots().map_err(Failure::database)?,
650                BranchKind::Namespace,
651                &mut self.refs,
652                &self.registry,
653                current_timestamp(),
654            )
655            .map_err(Failure::branch_commit)?;
656        }
657        let key = embedded::position_key(collection, field);
658        let bytes = embedded::encode_position(position)?;
659        self.commit_namespace(&companion, &[(key, Some(bytes))])
660    }
661
662    /// Writes `writes` into the namespace or branch `name` in one durable
663    /// commit, and publishes its record.
664    pub(super) fn commit_namespace(
665        &mut self,
666        name: &str,
667        writes: &[(Vec<u8>, Option<Vec<u8>>)],
668    ) -> Result<(), Failure> {
669        let at = self
670            .refs
671            .get(name)
672            .map(version)
673            .ok_or_else(|| Failure::new("reference_missing", name))?;
674        let handle =
675            open_branch(name, &self.refs, &self.registry).map_err(Failure::branch_commit)?;
676        for (key, value) in writes {
677            let shard = self.writer.shard_for(key);
678            value
679                .as_ref()
680                .map_or_else(
681                    || handle.delete(shard, key),
682                    |value| handle.put(shard, key, value),
683                )
684                .map_err(Failure::branch)?;
685        }
686        self.writer
687            .commit(
688                &handle,
689                &self.registry,
690                CommitRequest {
691                    durability: CommitDurability::Durable {
692                        refs: &mut self.refs,
693                    },
694                    extra_parents: &[],
695                    timestamp: current_timestamp(),
696                },
697            )
698            .map_err(|error| {
699                let installed = self.refs.get(name).is_some_and(|now| version(now) != at);
700                Failure::new(
701                    if installed {
702                        "embed_durability_unknown"
703                    } else {
704                        "embed_write_refused"
705                    },
706                    error.to_string(),
707                )
708            })?;
709        self.publish_record(name)
710    }
711}
712
713#[cfg(test)]
714#[path = "embed_job_tests.rs"]
715mod tests;