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
impl SteelDb
Sourcepub fn ingest<I, S>(docs: I) -> Result<Self, Error>
pub fn ingest<I, S>(docs: I) -> Result<Self, Error>
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?
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}Sourcepub fn ingest_with<I, S>(docs: I, opts: Options) -> Result<Self, Error>
pub fn ingest_with<I, S>(docs: I, opts: Options) -> Result<Self, Error>
As SteelDb::ingest, with explicit discovery settings.
Sourcepub fn ingest_curated<I, S>(
docs: I,
curated: &Proposal,
surfaces: &[String],
) -> Result<Self, Error>
pub fn ingest_curated<I, S>( docs: I, curated: &Proposal, surfaces: &[String], ) -> Result<Self, Error>
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.
Sourcepub fn ingest_using<I, S>(
docs: I,
artifact_dir: impl AsRef<Path>,
) -> Result<Self, Error>
pub fn ingest_using<I, S>( docs: I, artifact_dir: impl AsRef<Path>, ) -> Result<Self, Error>
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.
Sourcepub fn save(&self, artifact_dir: impl AsRef<Path>) -> Result<(), Error>
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.
Sourcepub fn save_with_training(
&self,
artifact_dir: impl AsRef<Path>,
) -> Result<usize, Error>
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.
Sourcepub fn open(dir: impl AsRef<Path>) -> Result<Self, Error>
pub fn open(dir: impl AsRef<Path>) -> Result<Self, Error>
Index a directory of documents, discovering the vocabulary from what it finds.
Sourcepub fn query(&self, ikl: &str) -> Result<Answer, Refused>
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?
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}Sourcepub fn check(&self, ikl: &str) -> Result<(), Refused>
pub fn check(&self, ikl: &str) -> Result<(), Refused>
Check a query without running it. Cheap, and the same check query performs.
Sourcepub fn belief(&self, tag: &str) -> Interval
pub fn belief(&self, tag: &str) -> Interval
The evidential bound on a tag across the whole corpus.
Examples found in repository?
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}Sourcepub fn s_path(&self, from: &str, to: &str, s: usize) -> Answer
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.
Sourcepub fn filtration(&self, max_s: usize) -> Vec<Level>
pub fn filtration(&self, max_s: usize) -> Vec<Level>
Both readings of the incidence matrix, swept over the overlap threshold.
Sourcepub fn categories(&self) -> Vec<Category<'_>>
pub fn categories(&self) -> Vec<Category<'_>>
The discovered categories and the words each claims.
Examples found in repository?
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}Sourcepub fn askable(&self) -> Vec<String>
pub fn askable(&self) -> Vec<String>
The wildcard for every discovered category — the set of things you can ask about.
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.
Sourcepub fn text(&self, situation: u32) -> Option<&str>
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.
Sourcepub fn resolve<'a>(
&'a self,
answer: &'a Answer,
) -> impl Iterator<Item = (u32, &'a str)> + 'a
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?
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}Sourcepub fn len(&self) -> usize
pub fn len(&self) -> usize
How many situations are indexed.
Examples found in repository?
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}pub fn is_empty(&self) -> bool
Source§impl SteelDb
impl SteelDb
Sourcepub fn adopt(&mut self, proposal: &Proposal) -> Vec<Verdict>
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§
impl !Freeze for SteelDb
impl RefUnwindSafe for SteelDb
impl Send for SteelDb
impl Sync for SteelDb
impl Unpin for SteelDb
impl UnsafeUnpin for SteelDb
impl UnwindSafe for SteelDb
Blanket Implementations§
Source§impl<T> BorrowMut<T> for Twhere
T: ?Sized,
impl<T> BorrowMut<T> for Twhere
T: ?Sized,
Source§fn borrow_mut(&mut self) -> &mut T
fn borrow_mut(&mut self) -> &mut T
impl<ST, DT> CastableFrom<ST, Initialized, Initialized> for DT
impl<ST, DT> CastableFrom<ST, Uninit, Uninit> for DT
impl<T> ErasedDestructor for Twhere
T: 'static,
Source§impl<T> Instrument for T
impl<T> Instrument for T
Source§fn instrument(self, span: Span) -> Instrumented<Self> ⓘ
fn instrument(self, span: Span) -> Instrumented<Self> ⓘ
Source§fn in_current_span(self) -> Instrumented<Self> ⓘ
fn in_current_span(self) -> Instrumented<Self> ⓘ
Source§impl<T> IntoEither for T
impl<T> IntoEither for T
Source§fn into_either(self, into_left: bool) -> Either<Self, Self> ⓘ
fn into_either(self, into_left: bool) -> Either<Self, Self> ⓘ
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 moreSource§fn into_either_with<F>(self, into_left: F) -> Either<Self, Self> ⓘ
fn into_either_with<F>(self, into_left: F) -> Either<Self, Self> ⓘ
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