Skip to main content

SteelDb

Struct SteelDb 

Source
pub struct SteelDb { /* private fields */ }
Expand description

An indexed corpus you can ask questions of.

Read-only after construction and Send + Sync, so one instance can serve many threads.

Implementations§

Source§

impl SteelDb

Source

pub fn ingest<I, S>(docs: I) -> Result<Self, Error>
where I: IntoIterator<Item = S>, S: AsRef<str>,

Ingest. Discover a vocabulary from documents and index them.

Offline and deterministic: no credentials, no network, no model files. The same documents always give the same vocabulary.

Examples found in repository?
examples/quickstart.rs (line 18)
6fn main() -> Result<(), Box<dyn std::error::Error>> {
7    let docs = [
8        "Morty Shade defeated Wallace Gale at Ecruteak City during the Indigo Invitational in 2025.",
9        "Bea Strike defeated Iris Draco at Ecruteak City during the Indigo Invitational in 2025.",
10        "Juan Tide defeated Cynthia Ward at Ecruteak City during the Indigo Invitational in 2026.",
11        "A habitat survey recorded Aggron near Sootopolis City at an elevation of 1082 m.",
12        "A habitat survey recorded Salamence near Sootopolis City at an elevation of 2369 m.",
13        "A habitat survey recorded Milotic near Sootopolis City at an elevation of 340 m.",
14        "Milotic is not permitted in Series 1 play for the 2025 season.",
15        "Metagross is permitted in Series 4 play for the 2026 season.",
16    ];
17
18    let db = SteelDb::ingest(docs)?;
19    println!("indexed {} situations", db.len());
20
21    println!("\nyou can ask about:");
22    for c in db.categories() {
23        println!("  {:<14} {}", c.wildcard(), c.words.iter().take(4).cloned().collect::<Vec<_>>().join(", "));
24    }
25
26    // use a category the corpus actually produced — see the list printed above
27    let q = "(and elevation/* (not state/negated))";
28    match db.query(q) {
29        Ok(a) => {
30            println!("\n{q}  ->  {} situations", a.len());
31            for (id, text) in db.resolve(&a).take(2) {
32                // chars(), not a byte slice: `&text[..n]` panics mid-character
33                println!("  [{id}] {}", text.chars().take(56).collect::<String>());
34            }
35        }
36        Err(r) => println!("\n{r}"),
37    }
38
39    // a category this corpus does not have — refused, not answered with an empty list
40    match db.query("survey/*") {
41        Ok(_) => println!("\nunexpected: survey is not a category here"),
42        Err(r) => println!("\n{r}"),
43    }
44
45    match db.query("gene/brca1") {
46        Ok(_) => println!("\nunexpected: that should not exist"),
47        Err(r) => println!("\n{r}"),
48    }
49
50    println!("\nbelief in a denied claim: {}", db.belief("state/negated"));
51    Ok(())
52}
Source

pub fn ingest_with<I, S>(docs: I, opts: Options) -> Result<Self, Error>
where I: IntoIterator<Item = S>, S: AsRef<str>,

As SteelDb::ingest, with explicit discovery settings.

Source

pub fn ingest_curated<I, S>( docs: I, curated: &Proposal, surfaces: &[String], ) -> Result<Self, Error>
where I: IntoIterator<Item = S>, S: AsRef<str>,

Index a corpus against a curated ontology. The third stage of neural discovery.

The full pipeline is three explicit steps, because each is a different kind of work:

use steeldb::{SteelDb, learn::Teacher, tagger_discover};

// 1. mechanical: the tagger reads typed spans, transport groups them into RAW clusters
let sample: Vec<String> = documents.iter().take(100).cloned().collect();
let raw = tagger_discover::discover(&sample, 12, 8)?;

// 2. judgment: merge synonyms, drop noise, name each surviving facet as a KIND
let curated = Teacher::ollama("qwen3:1.7b")?.curate(&raw).await?;

// 3. deterministic: gate the facets and index the FULL corpus, model-free
let db = SteelDb::ingest_curated(documents, &curated, &raw.surfaces())?;

Step 2 cannot be skipped. A raw cluster’s label is a value, not a facet: the reference’s own committed spec has clusters called ph and ge, and only curation turns those into weapon-platform and control-system with the raw terms demoted to examples. Indexing raw clusters directly puts ph in the index as a retrieval dimension.

The models run once, over the sample. What this produces is the ordinary artefact vocabulary, and the whole corpus is then projected by it with the model-free matcher — so SteelDb::query stays model-free, and after SteelDb::save later runs need no model at all.

surfaces is the observed entity spans from the raw spec, which becomes the gazetteer. It is kept separate from curation deliberately: the surfaces are what the tagger actually saw, so they survive whatever the curator chooses to merge or drop.

Source

pub fn ingest_using<I, S>( docs: I, artifact_dir: impl AsRef<Path>, ) -> Result<Self, Error>
where I: IntoIterator<Item = S>, S: AsRef<str>,

Ingest, following an existing artefact set.

Reads the vocabulary from artifact_dir instead of rediscovering it, so the result is reproducible and no model is involved even if a model produced the vocabulary originally. This is the pairing that makes learn worth running: the expensive, non-deterministic step happens once, and every run afterwards is offline and identical.

Source

pub fn save(&self, artifact_dir: impl AsRef<Path>) -> Result<(), Error>

Write this database’s vocabulary to an artefact directory.

Only derived vocabulary is written — categories, mention surfaces, relation verbs. No document text, so the directory is safe to commit alongside code.

Source

pub fn save_with_training( &self, artifact_dir: impl AsRef<Path>, ) -> Result<usize, Error>

As SteelDb::save, and additionally write a finetuning set derived from the corpus.

The set is weak supervision: every span the discovered vocabulary can locate, labelled with the category that claims it. It is what you would hand to a tagger finetune so the model learns to find these spans in text it has not seen.

Unlike the vocabulary files, this one contains document text — a span label is meaningless without the words it points at. It is written to a training/ subdirectory which crate::artifact::Artifacts::save excludes with a .gitignore, and the manifest records that the subdirectory is unsafe to publish.

Source

pub fn open(dir: impl AsRef<Path>) -> Result<Self, Error>

Index a directory of documents, discovering the vocabulary from what it finds.

Source

pub fn query(&self, ikl: &str) -> Result<Answer, Refused>

Run a query, or refuse it.

Every tag is checked against the vocabulary before anything executes, so an unsupported query costs nothing and comes back with alternatives.

Examples found in repository?
examples/quickstart.rs (line 28)
6fn main() -> Result<(), Box<dyn std::error::Error>> {
7    let docs = [
8        "Morty Shade defeated Wallace Gale at Ecruteak City during the Indigo Invitational in 2025.",
9        "Bea Strike defeated Iris Draco at Ecruteak City during the Indigo Invitational in 2025.",
10        "Juan Tide defeated Cynthia Ward at Ecruteak City during the Indigo Invitational in 2026.",
11        "A habitat survey recorded Aggron near Sootopolis City at an elevation of 1082 m.",
12        "A habitat survey recorded Salamence near Sootopolis City at an elevation of 2369 m.",
13        "A habitat survey recorded Milotic near Sootopolis City at an elevation of 340 m.",
14        "Milotic is not permitted in Series 1 play for the 2025 season.",
15        "Metagross is permitted in Series 4 play for the 2026 season.",
16    ];
17
18    let db = SteelDb::ingest(docs)?;
19    println!("indexed {} situations", db.len());
20
21    println!("\nyou can ask about:");
22    for c in db.categories() {
23        println!("  {:<14} {}", c.wildcard(), c.words.iter().take(4).cloned().collect::<Vec<_>>().join(", "));
24    }
25
26    // use a category the corpus actually produced — see the list printed above
27    let q = "(and elevation/* (not state/negated))";
28    match db.query(q) {
29        Ok(a) => {
30            println!("\n{q}  ->  {} situations", a.len());
31            for (id, text) in db.resolve(&a).take(2) {
32                // chars(), not a byte slice: `&text[..n]` panics mid-character
33                println!("  [{id}] {}", text.chars().take(56).collect::<String>());
34            }
35        }
36        Err(r) => println!("\n{r}"),
37    }
38
39    // a category this corpus does not have — refused, not answered with an empty list
40    match db.query("survey/*") {
41        Ok(_) => println!("\nunexpected: survey is not a category here"),
42        Err(r) => println!("\n{r}"),
43    }
44
45    match db.query("gene/brca1") {
46        Ok(_) => println!("\nunexpected: that should not exist"),
47        Err(r) => println!("\n{r}"),
48    }
49
50    println!("\nbelief in a denied claim: {}", db.belief("state/negated"));
51    Ok(())
52}
Source

pub fn check(&self, ikl: &str) -> Result<(), Refused>

Check a query without running it. Cheap, and the same check query performs.

Source

pub fn belief(&self, tag: &str) -> Interval

The evidential bound on a tag across the whole corpus.

Examples found in repository?
examples/quickstart.rs (line 50)
6fn main() -> Result<(), Box<dyn std::error::Error>> {
7    let docs = [
8        "Morty Shade defeated Wallace Gale at Ecruteak City during the Indigo Invitational in 2025.",
9        "Bea Strike defeated Iris Draco at Ecruteak City during the Indigo Invitational in 2025.",
10        "Juan Tide defeated Cynthia Ward at Ecruteak City during the Indigo Invitational in 2026.",
11        "A habitat survey recorded Aggron near Sootopolis City at an elevation of 1082 m.",
12        "A habitat survey recorded Salamence near Sootopolis City at an elevation of 2369 m.",
13        "A habitat survey recorded Milotic near Sootopolis City at an elevation of 340 m.",
14        "Milotic is not permitted in Series 1 play for the 2025 season.",
15        "Metagross is permitted in Series 4 play for the 2026 season.",
16    ];
17
18    let db = SteelDb::ingest(docs)?;
19    println!("indexed {} situations", db.len());
20
21    println!("\nyou can ask about:");
22    for c in db.categories() {
23        println!("  {:<14} {}", c.wildcard(), c.words.iter().take(4).cloned().collect::<Vec<_>>().join(", "));
24    }
25
26    // use a category the corpus actually produced — see the list printed above
27    let q = "(and elevation/* (not state/negated))";
28    match db.query(q) {
29        Ok(a) => {
30            println!("\n{q}  ->  {} situations", a.len());
31            for (id, text) in db.resolve(&a).take(2) {
32                // chars(), not a byte slice: `&text[..n]` panics mid-character
33                println!("  [{id}] {}", text.chars().take(56).collect::<String>());
34            }
35        }
36        Err(r) => println!("\n{r}"),
37    }
38
39    // a category this corpus does not have — refused, not answered with an empty list
40    match db.query("survey/*") {
41        Ok(_) => println!("\nunexpected: survey is not a category here"),
42        Err(r) => println!("\n{r}"),
43    }
44
45    match db.query("gene/brca1") {
46        Ok(_) => println!("\nunexpected: that should not exist"),
47        Err(r) => println!("\n{r}"),
48    }
49
50    println!("\nbelief in a denied claim: {}", db.belief("state/negated"));
51    Ok(())
52}
Source

pub fn s_path(&self, from: &str, to: &str, s: usize) -> Answer

Situations on a chain from from to to where each step shares at least s tags.

Raising s demands more agreement per step, which is what stops a walk drifting somewhere unrelated.

Source

pub fn filtration(&self, max_s: usize) -> Vec<Level>

Both readings of the incidence matrix, swept over the overlap threshold.

Source

pub fn categories(&self) -> Vec<Category<'_>>

The discovered categories and the words each claims.

Examples found in repository?
examples/quickstart.rs (line 22)
6fn main() -> Result<(), Box<dyn std::error::Error>> {
7    let docs = [
8        "Morty Shade defeated Wallace Gale at Ecruteak City during the Indigo Invitational in 2025.",
9        "Bea Strike defeated Iris Draco at Ecruteak City during the Indigo Invitational in 2025.",
10        "Juan Tide defeated Cynthia Ward at Ecruteak City during the Indigo Invitational in 2026.",
11        "A habitat survey recorded Aggron near Sootopolis City at an elevation of 1082 m.",
12        "A habitat survey recorded Salamence near Sootopolis City at an elevation of 2369 m.",
13        "A habitat survey recorded Milotic near Sootopolis City at an elevation of 340 m.",
14        "Milotic is not permitted in Series 1 play for the 2025 season.",
15        "Metagross is permitted in Series 4 play for the 2026 season.",
16    ];
17
18    let db = SteelDb::ingest(docs)?;
19    println!("indexed {} situations", db.len());
20
21    println!("\nyou can ask about:");
22    for c in db.categories() {
23        println!("  {:<14} {}", c.wildcard(), c.words.iter().take(4).cloned().collect::<Vec<_>>().join(", "));
24    }
25
26    // use a category the corpus actually produced — see the list printed above
27    let q = "(and elevation/* (not state/negated))";
28    match db.query(q) {
29        Ok(a) => {
30            println!("\n{q}  ->  {} situations", a.len());
31            for (id, text) in db.resolve(&a).take(2) {
32                // chars(), not a byte slice: `&text[..n]` panics mid-character
33                println!("  [{id}] {}", text.chars().take(56).collect::<String>());
34            }
35        }
36        Err(r) => println!("\n{r}"),
37    }
38
39    // a category this corpus does not have — refused, not answered with an empty list
40    match db.query("survey/*") {
41        Ok(_) => println!("\nunexpected: survey is not a category here"),
42        Err(r) => println!("\n{r}"),
43    }
44
45    match db.query("gene/brca1") {
46        Ok(_) => println!("\nunexpected: that should not exist"),
47        Err(r) => println!("\n{r}"),
48    }
49
50    println!("\nbelief in a denied claim: {}", db.belief("state/negated"));
51    Ok(())
52}
Source

pub fn askable(&self) -> Vec<String>

The wildcard for every discovered category — the set of things you can ask about.

Source

pub fn tags(&self) -> BTreeMap<String, Vec<String>>

Every tag in the index, grouped by its category and sorted within each group.

Sorted because the engine is otherwise deterministic and a caller should not have to defend against index iteration order — two runs over the same documents return byte-identical output.

Source

pub fn text(&self, situation: u32) -> Option<&str>

The document behind a situation id.

Without this an Answer is a list of integers. Every result is traceable back to the text that produced it, which is what makes an answer checkable rather than merely plausible.

Source

pub fn resolve<'a>( &'a self, answer: &'a Answer, ) -> impl Iterator<Item = (u32, &'a str)> + 'a

The documents an answer refers to, in id order.

Examples found in repository?
examples/quickstart.rs (line 31)
6fn main() -> Result<(), Box<dyn std::error::Error>> {
7    let docs = [
8        "Morty Shade defeated Wallace Gale at Ecruteak City during the Indigo Invitational in 2025.",
9        "Bea Strike defeated Iris Draco at Ecruteak City during the Indigo Invitational in 2025.",
10        "Juan Tide defeated Cynthia Ward at Ecruteak City during the Indigo Invitational in 2026.",
11        "A habitat survey recorded Aggron near Sootopolis City at an elevation of 1082 m.",
12        "A habitat survey recorded Salamence near Sootopolis City at an elevation of 2369 m.",
13        "A habitat survey recorded Milotic near Sootopolis City at an elevation of 340 m.",
14        "Milotic is not permitted in Series 1 play for the 2025 season.",
15        "Metagross is permitted in Series 4 play for the 2026 season.",
16    ];
17
18    let db = SteelDb::ingest(docs)?;
19    println!("indexed {} situations", db.len());
20
21    println!("\nyou can ask about:");
22    for c in db.categories() {
23        println!("  {:<14} {}", c.wildcard(), c.words.iter().take(4).cloned().collect::<Vec<_>>().join(", "));
24    }
25
26    // use a category the corpus actually produced — see the list printed above
27    let q = "(and elevation/* (not state/negated))";
28    match db.query(q) {
29        Ok(a) => {
30            println!("\n{q}  ->  {} situations", a.len());
31            for (id, text) in db.resolve(&a).take(2) {
32                // chars(), not a byte slice: `&text[..n]` panics mid-character
33                println!("  [{id}] {}", text.chars().take(56).collect::<String>());
34            }
35        }
36        Err(r) => println!("\n{r}"),
37    }
38
39    // a category this corpus does not have — refused, not answered with an empty list
40    match db.query("survey/*") {
41        Ok(_) => println!("\nunexpected: survey is not a category here"),
42        Err(r) => println!("\n{r}"),
43    }
44
45    match db.query("gene/brca1") {
46        Ok(_) => println!("\nunexpected: that should not exist"),
47        Err(r) => println!("\n{r}"),
48    }
49
50    println!("\nbelief in a denied claim: {}", db.belief("state/negated"));
51    Ok(())
52}
Source

pub fn len(&self) -> usize

How many situations are indexed.

Examples found in repository?
examples/quickstart.rs (line 19)
6fn main() -> Result<(), Box<dyn std::error::Error>> {
7    let docs = [
8        "Morty Shade defeated Wallace Gale at Ecruteak City during the Indigo Invitational in 2025.",
9        "Bea Strike defeated Iris Draco at Ecruteak City during the Indigo Invitational in 2025.",
10        "Juan Tide defeated Cynthia Ward at Ecruteak City during the Indigo Invitational in 2026.",
11        "A habitat survey recorded Aggron near Sootopolis City at an elevation of 1082 m.",
12        "A habitat survey recorded Salamence near Sootopolis City at an elevation of 2369 m.",
13        "A habitat survey recorded Milotic near Sootopolis City at an elevation of 340 m.",
14        "Milotic is not permitted in Series 1 play for the 2025 season.",
15        "Metagross is permitted in Series 4 play for the 2026 season.",
16    ];
17
18    let db = SteelDb::ingest(docs)?;
19    println!("indexed {} situations", db.len());
20
21    println!("\nyou can ask about:");
22    for c in db.categories() {
23        println!("  {:<14} {}", c.wildcard(), c.words.iter().take(4).cloned().collect::<Vec<_>>().join(", "));
24    }
25
26    // use a category the corpus actually produced — see the list printed above
27    let q = "(and elevation/* (not state/negated))";
28    match db.query(q) {
29        Ok(a) => {
30            println!("\n{q}  ->  {} situations", a.len());
31            for (id, text) in db.resolve(&a).take(2) {
32                // chars(), not a byte slice: `&text[..n]` panics mid-character
33                println!("  [{id}] {}", text.chars().take(56).collect::<String>());
34            }
35        }
36        Err(r) => println!("\n{r}"),
37    }
38
39    // a category this corpus does not have — refused, not answered with an empty list
40    match db.query("survey/*") {
41        Ok(_) => println!("\nunexpected: survey is not a category here"),
42        Err(r) => println!("\n{r}"),
43    }
44
45    match db.query("gene/brca1") {
46        Ok(_) => println!("\nunexpected: that should not exist"),
47        Err(r) => println!("\n{r}"),
48    }
49
50    println!("\nbelief in a denied claim: {}", db.belief("state/negated"));
51    Ok(())
52}
Source

pub fn is_empty(&self) -> bool

Source

pub fn documents(&self) -> &[String]

The documents as given, for tracing a result back to its source.

Source§

impl SteelDb

Source

pub fn adopt(&mut self, proposal: &Proposal) -> Vec<Verdict>

Apply a proposal, keeping only what the MECE gate accepts.

The gate is the same one SteelDb::ingest uses, so a model-proposed category has to earn its place on the same terms as a locally-discovered one: enough coverage, and not a near-duplicate of something already present. Candidates are judged in order, so the second of two similar suggestions is rejected against the first.

Returns a verdict per candidate; the kept ones are queryable immediately.

Auto Trait Implementations§

Blanket Implementations§

Source§

impl<T> Any for T
where T: 'static + ?Sized,

Source§

fn type_id(&self) -> TypeId

Gets the TypeId of self. Read more
Source§

impl<T> Borrow<T> for T
where T: ?Sized,

Source§

fn borrow(&self) -> &T

Immutably borrows from an owned value. Read more
Source§

impl<T> BorrowMut<T> for T
where T: ?Sized,

Source§

fn borrow_mut(&mut self) -> &mut T

Mutably borrows from an owned value. Read more
Source§

impl<ST, DT> CastableFrom<ST, Initialized, Initialized> for DT
where ST: ?Sized, DT: ?Sized,

Source§

impl<ST, DT> CastableFrom<ST, Uninit, Uninit> for DT
where ST: ?Sized, DT: ?Sized,

Source§

impl<T> ErasedDestructor for T
where T: 'static,

Source§

impl<T> From<T> for T

Source§

fn from(t: T) -> T

Returns the argument unchanged.

Source§

impl<T> Instrument for T

Source§

fn instrument(self, span: Span) -> Instrumented<Self> ⓘ

Instruments this type with the provided Span, returning an Instrumented wrapper. Read more
Source§

fn in_current_span(self) -> Instrumented<Self> ⓘ

Instruments this type with the current Span, returning an Instrumented wrapper. Read more
Source§

impl<T, U> Into<U> for T
where U: From<T>,

Source§

fn into(self) -> U

Calls U::from(self).

That is, this conversion is whatever the implementation of From<T> for U chooses to do.

Source§

impl<T> IntoEither for T

Source§

fn into_either(self, into_left: bool) -> Either<Self, Self> ⓘ

Converts self into a Left variant of Either<Self, Self> if into_left is true. Converts self into a Right variant of Either<Self, Self> otherwise. Read more
Source§

fn into_either_with<F>(self, into_left: F) -> Either<Self, Self> ⓘ
where F: FnOnce(&Self) -> bool,

Converts self into a Left variant of Either<Self, Self> if into_left(&self) returns true. Converts self into a Right variant of Either<Self, Self> otherwise. Read more
Source§

impl<Unshared, Shared> IntoShared<Shared> for Unshared
where Shared: FromUnshared<Unshared>,

Source§

fn into_shared(self) -> Shared

Creates a shared type from an unshared type.
Source§

impl<T> Pointable for T

Source§

const ALIGN: usize

The alignment of pointer.
Source§

type Init = T

The type for initializers.
Source§

unsafe fn init(init: <T as Pointable>::Init) -> usize

Initializes a with the given initializer. Read more
Source§

unsafe fn deref<'a>(ptr: usize) -> &'a T

Dereferences the given pointer. Read more
Source§

unsafe fn deref_mut<'a>(ptr: usize) -> &'a mut T

Mutably dereferences the given pointer. Read more
Source§

unsafe fn drop(ptr: usize)

Drops the object pointed to by the given pointer. Read more
Source§

impl<T> PolicyExt for T
where T: ?Sized,

Source§

fn and<P, B, E>(self, other: P) -> And<T, P>
where T: Sized + Policy<B, E>, P: Policy<B, E>,

Create a new Policy that returns Action::Follow only if self and other return Action::Follow. Read more
Source§

fn or<P, B, E>(self, other: P) -> Or<T, P>
where T: Sized + Policy<B, E>, P: Policy<B, E>,

Create a new Policy that returns Action::Follow if either self or other returns Action::Follow. Read more
Source§

impl<T> Read<Exclusive, BecauseExclusive> for T
where T: ?Sized,

Source§

impl<T> Same for T

Source§

type Output = T

Should always be Self
Source§

impl<T, U> TryFrom<U> for T
where U: Into<T>,

Source§

type Error = !

The type returned in the event of a conversion error.
Source§

fn try_from(value: U) -> Result<T, !>

Performs the conversion.
Source§

impl<T, U> TryInto<U> for T
where U: TryFrom<T>,

Source§

type Error = <U as TryFrom<T>>::Error

The type returned in the event of a conversion error.
Source§

fn try_into(self) -> Result<U, <U as TryFrom<T>>::Error>

Performs the conversion.
Source§

impl<S, T> Upcast<T> for S
where T: UpcastFrom<S> + ?Sized, S: ?Sized,

Source§

fn upcast(&self) -> &T
where Self: ErasableGeneric, T: Sized + ErasableGeneric<Repr = Self::Repr>,

Perform a zero-cost type-safe upcast to a wider ref type within the Wasm bindgen generics type system. Read more
Source§

fn upcast_into(self) -> T
where Self: Sized + ErasableGeneric, T: Sized + ErasableGeneric<Repr = Self::Repr>,

Perform a zero-cost type-safe upcast to a wider type within the Wasm bindgen generics type system. Read more
Source§

impl<V, T> VZip<V> for T
where V: MultiLane<T>,

Source§

fn vzip(self) -> V

Source§

impl<T> WithSubscriber for T

Source§

fn with_subscriber<S>(self, subscriber: S) -> WithDispatch<Self> ⓘ
where S: Into<Dispatch>,

Attaches the provided Subscriber to this type, returning a WithDispatch wrapper. Read more
Source§

fn with_current_subscriber(self) -> WithDispatch<Self> ⓘ

Attaches the current default Subscriber to this type, returning a WithDispatch wrapper. Read more