Releases / TinyStore 0.5.0
TinyStore 0.5.0
TinyStore 0.5.0 came out on 1 November 2026. Search learns what a search box
needs: it reads every word of a query as the start of a word, reads a word
typed with the other keyboard layout on, and says where each hit matched. A
bucket's update changes a value without losing another write. Every limit
error names its bound, and status --names shows what each bucket, stream
and index holds.
- Search
- Every word as a prefix:
лин алгfinds «Линейная алгебра». - The other keyboard layout:
rhbgnjis read asкрипто. - Show what matched: ranges to show in bold, in your language's string units.
- Every word as a prefix:
- KV
- Update without losing a write:
updatetries again when another write came first.
- Update without losing a write:
- Errors
- Limits by name: every limit error says which bound, what the call wanted and the most it takes.
- Command line
- What each name holds:
status --namescounts and weighs every bucket, stream and index.
- What each name holds:
Every word as a prefix
People shorten every word of a name they search for: лин алг for Линейная
алгебра, теор вер for Теория вероятностей. With prefix: 'every', Search
reads each word of the text as the start of longer words, and a document that
holds a word for each word typed comes first:
const { hits } = await courses.search({ text: 'лин алг', prefix: 'every' })result = await courses.search("лин алг", prefix="every")result, err := courses.Search(ctx, "лин алг", search.PrefixEvery())let found = courses.search("лин алг").prefix_every().run()?;prefix: truestill reads the last word alone, as in 0.4.1.- Each word of two letters or more is read as a prefix, the first eight, even when a space follows it.
- A prefix reads the postings of up to 64 words it begins, so it costs more than a whole word.
Every word has the details.
The other keyboard layout
People who type in two alphabets often type a word with the wrong layout on:
rhbgnj for крипто. Name the layouts an index's readers use, and a word
that finds nothing as typed is read as the same keys type in the other
layout. corrected says what was read:
const courses = store.index<{ name: string }>('courses', { fields: { name: 1 }, layouts: ['ru', 'en'] })
const { hits, corrected } = await courses.search({ text: 'rhbgnj', prefix: true })
corrected // 'крипто'courses = store.index("courses", Course, fields={"name": 1}, layouts=["ru", "en"])
result = await courses.search("rhbgnj", prefix=True)- Only a word that finds nothing as typed is swapped, so a hit as typed always comes first.
ruandenare the layouts so far.
Keyboard layouts has the details.
Show what matched
A search box shows the matched words of each result in bold. With
matches, each hit says where the search's words matched it, including a
word found by its stem, by a prefix or after a typo fix:
const { hits } = await courses.search({ text: 'алгебре', matches: true })
hits[0].matches // [{ field: 'name', start: 9, end: 16, word: 'алгебре' }]result = await courses.search("алгебре", matches=True)
result.hits[0].matches # [Match(field='name', start=9, end=16, word='алгебре', item=None)]- The ranges count in the units your language slices strings by: UTF-16 units in Bun and Node, code points in Python, bytes in Go and Rust.
- A word a prefix began is marked by the letters typed.
matchesis off unless the search asks, so a plain search costs the same.
Show what matched has the details.
Update without losing a write
When two requests change the same key, get and then set loses one of the
changes. update reads the key, calls your function with its value, and
writes the result at the version it read. If another write came first, it
reads again and calls your function again:
const next = await settings.update('home', current => ({ ...(current ?? defaults), theme: 'dark' }))next = await settings.update("home", lambda current: replace(current or defaults, theme="dark"))next, _, err := settings.Update(ctx, "home", func(current Settings, found bool) (Settings, bool) {
if !found {
current = defaults
}
current.Theme = "dark"
return current, true
})let next = settings.update("home", |current| {
Some(Settings { theme: "dark".into(), ..current.unwrap_or_default() })
})?;- Your function may run more than once, so it must not send an email or call another service.
- Return nothing (
undefined,None, orfalsein Go) to write nothing. - After 8 tries that another write beat, it fails with a conflict error.
Change a value without losing another write has the details.
Limits by name
Every limit error now carries the bound's name, what the call wanted and the most the bound takes. Each SDK exports the same bounds, so a program can size its batches before it calls:
const most = limits.setManyDocuments.bound // 1000most = bounds.SET_MANY_DOCUMENTS.bound # 1000most := limits.SetManyDocuments.Bound // 1000let most = tinystore::limits::SET_MANY_DOCUMENTS.bound; // 1000From an SDK, a setMany meets the 2 MiB of one request first, and that
error says so: search.setMany: a request of 2203997 bytes, past the 2097152 a body holds. The 16 MiB bound applies only to Rust calling the engine
directly.
Bounds by name lists every bound.
What each name holds
status --names shows each bucket, queue, set of files, stream, metric and
index with how many items it holds and their size:
tinystore status ./data --names kv 2.0 MiB
sessions 500 keys 1.9 MiB
pins 50 keys 790 B
records 38 KiB
audit 2000 events 1.7 KiB keep 400d 2026-11-01 … 2026-11-01It reads every bucket and index, so it takes longer on a big store than
status alone. --json gives the same numbers.
What each name holds has the details.
Changed names
- Bun and Node:
limitsholds each bound as{ name, bound }. Its names are now the core's, which the old ones did not match. For example,limits.decodedSamplesislimits.querySamples,matchedSeriesisquerySeries,fetchedBytesisqueryBytes, andoutputSamplesandoutputBucketsarequeryAnswer. - Metrics' limits are named for what they bound:
one ingestisingest samplesoringest bytes, andone textistext samplesortext bytes. - Rust:
search::Hitand the newsearch::Matchare#[non_exhaustive].
Install
bun add @tinyshed/tinystore@0.5.0
npm install @tinyshed/tinystore@0.5.0pip install tinyshed-tinystore==0.5.0go get tinyshed.org/tinystore@v0.5.0cargo add tinystore@0.5.0
cargo install tinystore-cli@0.5.0Nothing in a store's files changes, so 0.4 and 0.5.0 open each other's
stores. If your program reaches a server, update the server with the SDK: a
0.4 server refuses prefix: 'every', matches and layouts with an
unimplemented error that names the field. The server image is
ghcr.io/tinyshed/tinystore:0.5.0.
Every commit since 0.4.1 is in the GitHub release.