moonrag
Documents, the five contracts, the in-memory store, the retrievers, vector arithmetic and metadata filters.
suberrorRefused
A request the library will not carry out. Mismatch is two vectors of different length meeting where one length is needed, Empty a blank string where one has to be non-blank, Exceeded a number outside the range limit bounds, Malformed unreadable input at at, and Unsupported a request this build does not serve.
pub(all) suberror Refused {
Mismatch(got~ : Int32, want~ : Int32)
Empty(what~ : String)
Exceeded(limit~ : Int32, got~ : Int32)
Malformed(at~ : Int32, why~ : String)
Unsupported(what~ : String)
}
structCandidate
A document together with the vector it was embedded to. MMR reranks by comparing candidates with each other, and a score alone cannot do that: the vectors have to travel with the documents.
pub(all) struct Candidate {
document : Document
vector : Array[Double]
}
structDocument
A piece of text, with an optional identity and metadata. id is what a store keys a document by. Leave it None and the store assigns one; set it and the document keeps the same identity across re-indexing, which is what makes an update replace the old copy instead of piling up beside it. meta carries whatever the caller needs — a source path, a page number, a heading path — and is what a Filter reads.
pub(all) struct Document {
id : String?
text : String
meta : Map[String, @json.Json]
}
pub fn Document::derived(Self, Int32, String, start_index? : Int32) -> Self
A document cut out of parent: same metadata plus the chunk number, a derived id when the parent had one, and the offset it started at when the splitter knows it. Splitters are not the only producers of chunks — a caller streaming pages out of a PDF has the same needs — so the bookkeeping lives here rather than in each of them.
pub fn Document::from_json(@json.Json, @json.JsonPath) -> Self raise @json.JsonDecodeError
pub fn Document::new(String, id? : String, meta? : Map[String, @json.Json]) -> Self
A document with text, no identity, and no metadata.
pub fn Document::to_json(Self) -> @json.Json
pub fn Document::with_id(Self, String) -> Self
The same document under id, for a store that keys by caller-chosen identity.
enumFilter
A condition on a document's metadata. Equal matches a whole JSON value, OneOf any of a list, Exists only that the key is present, and Compare a number against an operand. All and Any nest, Not negates. A document whose metadata lacks the key matches nothing except Not of a filter it would otherwise fail.
pub(all) enum Filter {
Equal(String, @json.Json)
OneOf(String, Array[@json.Json])
Exists(String)
Compare(String, Order, Double)
All(Array[Filter])
Any(Array[Filter])
Not(Filter)
}
pub fn Filter::matches(Self, Document) -> Bool
Whether document satisfies the filter.
structHashingEmbedder
A deterministic embedder that needs neither a model nor a network: it hashes the words and the character trigrams of a text into a fixed-width vector. It is lexical, not semantic — "cat" and "feline" land far apart — so it suits tests, offline examples and keyword-ish retrieval, not production ranking. Being deterministic, it is also the vector source for differential tests: the same text yields the same vector on every target.
pub struct HashingEmbedder {
dimension : Int32
}
pub async fn HashingEmbedder::embed(Self, ArrayView[String]) -> Array[Array[Double]]
pub fn HashingEmbedder::new(dimension? : Int32) -> Self raise
A hashing embedder producing dimension-component vectors. 256 is wide enough that a handful of documents rarely collide and narrow enough to keep a fixture readable. Raises Empty when dimension is not positive.
structInMemoryVectorStore
A VectorStore held in memory, keyed by document id. The store embeds what it is given, so add takes documents while a search takes the query's vector. That split is deliberate: writing is the rare, batched, cacheable path, while a search often reuses one query vector across several stores.
pub struct InMemoryVectorStore {
embedder : &Embedder
mut entries : Array[(String, Document, Array[Double])]
mut counter : Int32
}
pub async fn InMemoryVectorStore::add(Self, Array[Document]) -> Array[String]
pub fn InMemoryVectorStore::add_vectors(Self, Array[Document], ArrayView[Array[Double]]) -> Array[String] raise
Store documents that already carry their vectors, returning their ids. An id already present is replaced in place, which is what makes a re-index an update rather than a duplicate. Raises Mismatch when the vector count differs from the document count or two vectors differ in length, and Empty for a document whose id is the empty string.
pub fn InMemoryVectorStore::clear(Self) -> Unit
Remove every stored document.
pub async fn InMemoryVectorStore::delete(Self, ArrayView[String]) -> Unit
pub fn InMemoryVectorStore::get(Self, String) -> Document?
The document stored under id, or None. The returned document carries the id it is stored under, so it can be deleted or refetched without keeping the id beside it.
pub fn InMemoryVectorStore::ids(Self) -> Array[String]
The ids of the stored documents, in the order they were added.
pub fn InMemoryVectorStore::len(Self) -> Int32
The number of stored documents.
pub fn InMemoryVectorStore::new(&Embedder) -> Self
An empty store that embeds with embedder.
pub async fn InMemoryVectorStore::search(Self, ArrayView[Double], limit~ : Int32, filter? : Filter) -> Array[Scored]
pub fn InMemoryVectorStore::vector_of(Self, String) -> Array[Double]?
The vector stored for id, or None — for a caller that reranks hits itself.
structMmrRetriever
Returns relevant documents that are also unlike each other, by maximal marginal relevance. A plain search on a corpus of near-duplicates returns the same paragraph five times. MMR trades some relevance for coverage: lambda near 1 behaves like a plain search, near 0 like "the most different thing that is still related".
pub struct MmrRetriever {
embedder : &Embedder
store : &VectorStore
filter : Filter?
lambda : Double
fetch_limit : Int32
}
pub fn MmrRetriever::new(&Embedder, &VectorStore, lambda? : Double, fetch_limit? : Int32, filter? : Filter) -> Self
An MMR retriever over store. fetch_limit candidates are ranked by similarity first and then reranked by MMR, so it must be at least the limit a caller asks for; lambda weights relevance against diversity.
pub async fn MmrRetriever::retrieve(Self, String, limit~ : Int32) -> Array[Scored]
enumOrder
How Compare orders a metadata number against its operand.
pub(all) enum Order {
Less
AtMost
AtLeast
Greater
}
structScored
A document and the score it earned: what one search returns.
pub(all) struct Scored {
document : Document
score : Double
}
pub fn Scored::documents(ArrayView[Self]) -> Array[Document]
The documents in hits, dropping the scores.
pub fn Scored::from_json(@json.Json, @json.JsonPath) -> Self raise @json.JsonDecodeError
pub fn Scored::to_json(Self) -> @json.Json
structSimilarityRetriever
Ranks documents by cosine similarity to the query, and returns the best limit.
pub struct SimilarityRetriever {
embedder : &Embedder
store : &VectorStore
filter : Filter?
}
pub fn SimilarityRetriever::new(&Embedder, &VectorStore, filter? : Filter) -> Self
A similarity retriever over store, embedding queries with embedder. filter narrows the candidates before ranking, in the store where the documents live.
pub async fn SimilarityRetriever::retrieve(Self, String, limit~ : Int32) -> Array[Scored]
traitEmbedder
Turns batches of text into vectors of equal length. One call embeds a batch, not a string: every model worth calling charges per request, and a batch is also what makes a re-index affordable. All vectors in a batch must have the same length, and the same text must embed to the same vector — ranking is keyed on that.
pub(open) trait Embedder {
async fn embed(Self, ArrayView[String]) -> Array[Array[Double]]
}
traitLoader
Produces documents from somewhere — a file, a directory, an API, a literal string. Failure is part of the contract because the source may be unreadable or malformed; a loader that quietly returns nothing looks identical to an empty document and hides the problem.
pub(open) trait Loader {
async fn load(Self) -> Array[Document]
}
traitRetriever
Answers a question in text: embeds it, searches, returns the best documents. A retriever is the whole read path behind one call; a reranker, a query rewrite, or MMR is an implementation of this trait and nothing above it changes.
pub(open) trait Retriever {
async fn retrieve(Self, String, limit~ : Int32) -> Array[Scored]
}
traitSplitter
Cuts one document into smaller documents. Splitting is not async: it is pure text work with no I/O, and keeping it synchronous lets it run inside a parser or a test without an event loop.
pub(open) trait Splitter {
fn split(Self, Document) -> Array[Document]
}
traitVectorStore
Where vectors live. add takes documents, not vectors: the store owns the embedder it was built with, so a caller never has to keep the two in step by hand. search takes the query's vector, because the query is often embedded once and searched in several stores, and because an embedding call is the expensive part a caller wants to place itself.
pub(open) trait VectorStore {
async fn add(Self, Array[Document]) -> Array[String]
async fn delete(Self, ArrayView[String]) -> Unit
async fn search(Self, ArrayView[Double], limit~ : Int32, filter? : Filter) -> Array[Scored]
}
Functions
pub fn cosine_similarity(ArrayView[Double], ArrayView[Double]) -> Double raise
The cosine similarity of two vectors, in [-1, 1]: 1 for the same direction, 0 for orthogonal vectors or when either is all zeros, -1 for opposite directions. Raises Mismatch when the lengths differ. Cosine ignores magnitude, so a long document and a short one about the same thing score alike — which is why it is the usual ranking metric for text.
pub fn dot_product(ArrayView[Double], ArrayView[Double]) -> Double raise
The dot product of two vectors of the same length. Raises Mismatch when the lengths differ.
pub fn euclidean_distance(ArrayView[Double], ArrayView[Double]) -> Double raise
The Euclidean distance between two vectors: 0 for identical vectors, larger for further ones. Raises Mismatch when the lengths differ. Use it when the embedding model was trained on it; ranking by distance ascending and by similarity descending agree only for normalised vectors, so the two are kept apart rather than folded into one.
pub fn magnitude(ArrayView[Double]) -> Double
The Euclidean norm of a vector.
pub fn mmr(ArrayView[Double], ArrayView[Candidate], limit~ : Int32, lambda? : Double) -> Array[Scored] raise
Pick up to limit candidates that are relevant to query and unlike each other. Each round scores every remaining candidate as lambda * similarity to the query - (1 - lambda) * highest similarity to an already picked one, and takes the best; lambda = 1 degenerates to plain similarity, lambda = 0 to "show me the most novel document". Ties go to the earlier candidate, so the result is deterministic. Raises Mismatch when a candidate vector's length differs from the query's.
pub fn normalized(ArrayView[Double]) -> Array[Double]
vector scaled to unit length; an all-zero vector comes back unchanged rather than as NaNs.
pub fn rank(Array[Scored], Int32) -> Array[Scored]
hits sorted by score descending — equal scores keep their arrival order — cut to limit. The sort is stable on purpose: with a small corpus, several documents tie at cosine 1.0, and a caller re-running a search expects the same three each time.
split
Cutting a document into chunks: recursive separators, a token budget, Markdown sections and HTML sections.
structHtmlSplitter
Cuts an HTML document into one chunk per heading-delimited section. Each chunk's metadata carries the headings above it as h1 … h6, so a retrieved chunk can say which section it belongs to. A section longer than chunk_size is cut again by the recursive splitter with the same heading path, so no chunk outgrows an embedding window.
pub struct HtmlSplitter {
chunk_size : Int32
inner : RecursiveSplitter
}
pub fn HtmlSplitter::new(chunk_size? : Int32, chunk_overlap? : Int32, strip_whitespace? : Bool) -> Self raise
A splitter cutting sections of at most chunk_size code points; a section shorter than that stays whole. Raises Empty when chunk_size is not positive, and Exceeded when chunk_overlap is negative or larger than chunk_size.
pub fn HtmlSplitter::split(Self, @moonrag.Document) -> Array[@moonrag.Document]
pub fn HtmlSplitter::split_html(Self, String, meta? : Map[String, @json.Json]) -> Array[@moonrag.Document]
The chunks html splits into: one per section, or several when a section is over budget.
enumKeepSeparator
Where a separator goes relative to the piece it separated.
pub(all) enum KeepSeparator {
Drop
Start
End
}
structMarkdownHeading
One heading level that starts a new section, and the metadata key its text is filed under. The key is the caller's: MarkdownHeading::new(2, "h2") files an ## Install heading as {"h2": "Install"}. Levels are unique within a splitter, so a section's path has one entry per level.
pub(all) struct MarkdownHeading {
level : Int32
key : String
}
pub fn MarkdownHeading::defaults() -> Array[Self]
The six ATX levels, filed as h1 … h6.
pub fn MarkdownHeading::new(Int32, String) -> Self raise
A heading level 1 to 6 filed under key. Raises Exceeded when level is outside that range and Empty when key is blank.
structMarkdownSplitter
Cuts a Markdown document at its headings. A chunk never crosses a heading, and carries the whole path down to it — h1, h2 and so on — so a retrieved passage can say which section of which page it came from. A section longer than chunk_size is cut again by the recursive splitter at block boundaries; a code block is emitted whole however long it is, because cutting it would leave a fragment that neither compiles nor reads.
pub struct MarkdownSplitter {
chunk_size : Int32
chunk_overlap : Int32
headings : Array[MarkdownHeading]
strip_headers : Bool
fallback : RecursiveSplitter
}
pub fn MarkdownSplitter::new(chunk_size? : Int32, chunk_overlap? : Int32, headings? : Array[MarkdownHeading], strip_headers? : Bool) -> Self raise
A splitter producing sections of at most chunk_size code points. headings is the set of levels that start a new chunk; a heading of a level not listed stays in the text of the section around it. strip_headers drops the heading line itself from the chunk — on by default, because the heading is in the metadata and repeating it wastes an embedding slot. Raises Empty when chunk_size is not positive, Exceeded when chunk_overlap is negative or larger than chunk_size or when two headings share a level.
pub fn MarkdownSplitter::split(Self, @moonrag.Document) -> Array[@moonrag.Document]
pub fn MarkdownSplitter::split_markdown(Self, String, meta? : Map[String, @json.Json]) -> Array[@moonrag.Document]
The documents markdown splits into, filed under meta as well as their heading path — the same result as split on a document, for a caller that has the text and nothing else.
pub fn MarkdownSplitter::split_text(Self, String) -> Array[String]
The text of the chunks text splits into.
structRecursiveSplitter
Cuts text by trying a list of separators from coarse to fine. Paragraphs first, then lines, then words, then characters: a piece that still does not fit is split again by the next separator, so the result respects natural boundaries for as long as it can and only cuts mid-word when the text leaves no choice.
pub struct RecursiveSplitter {
chunk_size : Int32
chunk_overlap : Int32
separators : Array[String]
keep_separator : KeepSeparator
strip_whitespace : Bool
add_start_index : Bool
measure : (StringView) -> Int32
}
pub fn RecursiveSplitter::new(chunk_size? : Int32, chunk_overlap? : Int32, separators? : Array[String], keep_separator? : KeepSeparator, strip_whitespace? : Bool, add_start_index? : Bool, measure? : (StringView) -> Int32) -> Self raise
A splitter producing pieces of at most chunk_size units, where a unit is one code point unless measure says otherwise. chunk_overlap carries that many units of the previous piece into the next one so a sentence cut across a boundary is still readable in one of them; the defaults (4000 and 200) are the ones most chunking pipelines assume. keep_separator decides whether the separator stays at the start of the following piece, at the end of the preceding one, or is dropped. Raises Empty when chunk_size is not positive, and Exceeded when chunk_overlap is negative or larger than chunk_size.
pub fn RecursiveSplitter::split(Self, @moonrag.Document) -> Array[@moonrag.Document]
pub fn RecursiveSplitter::split_text(Self, String) -> Array[String]
The chunks text splits into. Public because a caller sometimes wants the strings without the document bookkeeping.
pub fn RecursiveSplitter::split_with_offsets(Self, String) -> Array[(String, Int32)]
The chunks text splits into, paired with the offset each started at.
structTokenSplitter
Cuts text into chunks of at most chunk_size tokens, where measure counts tokens. The pieces the text is cut into are the units separator delimits (a space by default), and each chunk is measured with measure on the assembled text, so the promise holds for the caller's real tokenizer rather than for an estimate.
pub struct TokenSplitter {
chunk_size : Int32
chunk_overlap : Int32
separator : String
strip_whitespace : Bool
measure : (StringView) -> Int32
}
pub fn TokenSplitter::new((StringView) -> Int32, chunk_size? : Int32, chunk_overlap? : Int32, separator? : String, strip_whitespace? : Bool) -> Self raise
A splitter whose chunk_size and chunk_overlap are counted in measure's units. Defaults are 512 and 64 tokens, the window most embedding models accept. Raises Empty when chunk_size is not positive, and Exceeded when chunk_overlap is negative or larger than chunk_size.
pub fn TokenSplitter::split(Self, @moonrag.Document) -> Array[@moonrag.Document]
pub fn TokenSplitter::split_text(Self, String) -> Array[String]
The chunks text splits into, each within the token budget.
Functions
pub fn html_text(String) -> String
The readable text of a HTML page: block boundaries become line breaks, markup, scripts, styles and headings-of-nothing are dropped. A loader and a splitter both read a page through this, so they cannot disagree about what it says.
pub fn html_title(String) -> String?
The page title: <title>, or the first h1 when the page has none.
load
Text, CSV, JSON, Markdown and HTML into documents, from bytes already in memory.
structCsvLoader
A CSV table, one document per row. The document text is the row spelled out as column: value lines, which reads better to a model than a comma-separated line, and every column is also in the metadata under its header name.
pub struct CsvLoader {
source : String
text : String
has_header : Bool
delimiter : Char
}
pub async fn CsvLoader::load(Self) -> Array[@moonrag.Document]
pub fn CsvLoader::new(String, String, has_header? : Bool, delimiter? : Char) -> Self
A loader over text. With has_header the first record names the columns; without it the columns are column_0, column_1, … Raises Malformed when a quoted field is never closed.
structHtmlLoader
A HTML page as one document of readable text.
pub struct HtmlLoader {
source : String
text : String
}
pub async fn HtmlLoader::load(Self) -> Array[@moonrag.Document]
pub fn HtmlLoader::new(String, String) -> Self
A loader over a HTML text.
structJsonLoader
A JSON document, one document per element when the top level is an array. With text_key the named field becomes the text and the rest of the object becomes metadata — which is how a corpus exported as [{"title": ..., "body": ...}] becomes documents without a transformation step. Without it, an object's text field is used, and failing that the object is written out as JSON so nothing is silently lost.
pub struct JsonLoader {
source : String
text : String
text_key : String?
}
pub async fn JsonLoader::load(Self) -> Array[@moonrag.Document]
pub fn JsonLoader::new(String, String, text_key? : String) -> Self
A loader over a JSON text.
structMarkdownLoader
A Markdown file as one document, with its first heading as the title metadata.
pub struct MarkdownLoader {
source : String
text : String
}
pub async fn MarkdownLoader::load(Self) -> Array[@moonrag.Document]
pub fn MarkdownLoader::new(String, String) -> Self
A loader over a Markdown text.
structTextLoader
A whole text as a single document, with source recorded in its metadata. Use it for a note, a log line, a field already read from somewhere else: anything whose text is the document rather than a container for documents.
pub struct TextLoader {
source : String
text : String
}
pub async fn TextLoader::load(Self) -> Array[@moonrag.Document]
pub fn TextLoader::new(String, String) -> Self
A loader over text, labelling the document with source.