Documents, splitting, vector stores and retrieval for MoonBit. Embeddings come through a contract, so no model is needed.
async fn answer(embedder : &@moonrag.Embedder) -> Array[@moonrag.Scored] raise {
let store = @moonrag.InMemoryVectorStore::new(embedder)
let _ = store.add([
@moonrag.Document::new(
"the harbour at dawn, fishing boats leaving",
meta=Map([("topic", @json.Json::string("sea"))]),
),
@moonrag.Document::new("a mountain path in autumn, cold and clear"),
])
let query = embedder.embed(["boats in the harbour"])[0]
store.search(
query[:],
limit=1,
filter=@moonrag.Filter::Equal("topic", @json.Json::string("sea")),
)
}moon add moonbitstack/moonragimport {
"moonbitstack/moonrag", // documents, contracts, memory store, retrieval
"moonbitstack/moonrag/split", // recursive, token, Markdown and HTML splitters
"moonbitstack/moonrag/load", // text, CSV, JSON, Markdown, HTML loaders
"moonbitstack/moonrag/load/file", // the same loaders, reading from disk
"moonbitstack/moonrag/index", // incremental indexing
"moonbitstack/moonrag/sql", // a store in SQL, over any moondb driver
}let document = @moonrag.Document::new("some text", id="guide.md#2")
.set_meta("source", @json.Json::string("guide.md"))
.set_meta("page", @json.Json::number(3.0))let recent = @moonrag.Filter::All([
@moonrag.Filter::Equal("topic", @json.Json::string("sea")),
@moonrag.Filter::Compare("year", @moonrag.Order::AtLeast, 2025.0),
])pub(open) trait Embedder {
async fn embed(Self, texts : ArrayView[String]) -> Array[Array[Double]] raise
}let embedder : &@moonrag.Embedder = @moonrag.HashingEmbedder::new()
let store = @moonrag.InMemoryVectorStore::new(embedder)
let query_vector = embedder.embed(["boats in the harbour"])[0]
let ids = store.add([document])
let hits = store.search(query_vector, limit=5)
store.delete([ids[0]][:])pub async fn[T : @moondb.AsyncDriver] search_with(
driver : T,
embedder : &@moonrag.Embedder,
) -> Array[@moonrag.Scored] raise {
let store = @sql.SqlVectorStore::new(driver, embedder, dimension=256)
store.create()
let _ = store.add([@moonrag.Document::new("the harbour at dawn")])
let query = embedder.embed(["the harbour"])[0]
store.search(query[:], limit=3)
}let driver = @sql.BlockingDriver::new(@moonsqlite.SqliteDriver::open("app.db"))let plain : &@moonrag.Retriever = @moonrag.SimilarityRetriever::new(embedder, store)
let diverse : &@moonrag.Retriever = @moonrag.MmrRetriever::new(embedder, store, lambda=0.3)let splitter = @split.RecursiveSplitter::new(chunk_size=1000, chunk_overlap=200)
let chunks = splitter.split(document)fn count_tokens(text : StringView) -> Int32 {
let mut count = 0
for word in text.to_owned().split(" ") {
if word != "" {
count = count + 1
}
}
count
}let splitter = @split.TokenSplitter::new(count_tokens, chunk_size=512, chunk_overlap=64)let markdown = "# Install\n\nmoon add moonbitstack/moonrag.\n\n## Search\n\nBuild a store, then search.\n"
let splitter = @split.MarkdownSplitter::new(chunk_size=1000)
let chunks = splitter.split(@moonrag.Document::new(markdown))
assert_eq(chunks.length(), 2)
assert_eq(chunks[1].str_meta("h1"), Some("Install"))
assert_eq(chunks[1].str_meta("h2"), Some("Search"))let page_html = "<html><body><h1>Install</h1><p>moon add moonbitstack/moonrag.</p><h2>Search</h2><p>Build a store, then search.</p></body></html>"
let splitter = @split.HtmlSplitter::new(chunk_size=1000)
let sections = splitter.split(@moonrag.Document::new(page_html))
assert_eq(sections.length(), 2)
assert_eq(sections[0].str_meta("h1"), Some("Install"))
assert_eq(sections[1].str_meta("h2"), Some("Search"))| Format | What a document is | Metadata |
|---|---|---|
| TextLoader | the whole text | source |
| CsvLoader | one row, written as column: value lines | source, row, every column |
| JsonLoader | one element of an array, or the object | source, every field except the text key |
| MarkdownLoader | the whole file | source, title from the first heading |
| HtmlLoader | the readable text of the page | source, title |
let csv_text = "name,city\nalice,London\nbob,Paris\n"
let documents = @moonrag.Loader::load(@load.CsvLoader::new("people.csv", csv_text))let documents = @file.csv("people.csv")
let text = @file.markdown("notes.md")let indexer = @index.Indexer::new(store)
let stats = indexer.index(chunks) // added / updated / skipped / deleted
let cleaned = indexer.index(chunks, cleanup=@index.Cleanup::Full)| Document | id : String?, text : String, meta : Map[String, Json] |
| Scored | document : Document, score : Double |
| Filter | Equal, OneOf, Exists, Compare, All, Any, Not |
| Embedder | async embed(texts : ArrayView[String]) -> Array[Array[Double]] raise |
| VectorStore | async add(Array[Document]) -> Array[String] raise, async delete(ids) raise, async search(vector, limit~, filter?) -> Array[Scored] raise |
| Retriever | async retrieve(query : String, limit~ : Int32) -> Array[Scored] raise |
| Splitter | split(Document) -> Array[Document] |
| Loader | async load() -> Array[Document] raise |
| Refused | Mismatch, Empty, Exceeded, Malformed, Unsupported |
cd integration && moon test --target native| # | Example | What it shows |
|---|---|---|
| 01 | ingest | load, split, index, retrieve, then index again and embed nothing |
| 02 | store | add, filter, rank, rerank with MMR, delete |
| 03 | files | the file front end over a real temporary directory |
| 04 | markdown | one chunk per heading, the path in the metadata, a code block kept whole |
moon run src/examples/01-ingest --target nativeimpl Embedder for HashingEmbedderasync fn HashingEmbedder::embed(self : HashingEmbedder, texts : ArrayView[String]) -> Array[Array[Double]]impl VectorStore for InMemoryVectorStoreasync fn InMemoryVectorStore::add(self : InMemoryVectorStore, documents : Array[Document]) -> Array[String]fn InMemoryVectorStore::add_vectors(self : InMemoryVectorStore, documents : Array[Document], vectors : ArrayView[Array[Double]]) -> Array[String] raiseasync fn InMemoryVectorStore::search(self : InMemoryVectorStore, vector : ArrayView[Double], limit~ : Int, filter? : Filter) -> Array[Scored]pub struct MmrRetriever {
embedder : &Embedder
store : &VectorStore
filter : Filter?
lambda : Double
fetch_limit : Int
}impl Retriever for MmrRetrieverfn MmrRetriever::new(embedder : &Embedder, store : &VectorStore, lambda? : Double, fetch_limit? : Int, filter? : Filter) -> MmrRetrieverimpl Retriever for SimilarityRetrieverfn SimilarityRetriever::new(embedder : &Embedder, store : &VectorStore, filter? : Filter) -> SimilarityRetrieverasync fn SimilarityRetriever::retrieve(self : SimilarityRetriever, query : String, limit~ : Int) -> Array[Scored]Install
Download zipDocuments, splitting, vector stores and retrieval for MoonBit. Embeddings come through a contract, so no model is needed.