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.

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: true still 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)
courses, err := search.OpenIndex[Course](store, "courses", search.Fields{"name": 1}, search.Layouts("ru", "en"))
result, err := courses.Search(ctx, "rhbgnj", search.Prefix())
let courses = store.index::<Course>("courses").field("name", 1.0).layouts(["ru", "en"]).open()?;
let found = courses.search("rhbgnj").prefix().run()?;
  • Only a word that finds nothing as typed is swapped, so a hit as typed always comes first.
  • ru and en are 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)]
result, err := courses.Search(ctx, "алгебре", search.Matches())
result.Hits[0].Matches // [{Field: "name", Start: 17, End: 31, Word: "алгебре"}]
let found = courses.search("алгебре").matches().run()?;
let algebra = &found.hits[0].value.name[found.hits[0].matches[0].at.clone()]; // "алгебра"
  • 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.
  • matches is 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, or false in 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 // 1000
most = bounds.SET_MANY_DOCUMENTS.bound  # 1000
most := limits.SetManyDocuments.Bound // 1000
let most = tinystore::limits::SET_MANY_DOCUMENTS.bound; // 1000

From 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:

sh
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-01

It 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: limits holds each bound as { name, bound }. Its names are now the core's, which the old ones did not match. For example, limits.decodedSamples is limits.querySamples, matchedSeries is querySeries, fetchedBytes is queryBytes, and outputSamples and outputBuckets are queryAnswer.
  • Metrics' limits are named for what they bound: one ingest is ingest samples or ingest bytes, and one text is text samples or text bytes.
  • Rust: search::Hit and the new search::Match are #[non_exhaustive].

Install

bun add @tinyshed/tinystore@0.5.0
npm install @tinyshed/tinystore@0.5.0
pip install tinyshed-tinystore==0.5.0
go get tinyshed.org/tinystore@v0.5.0
cargo add tinystore@0.5.0
cargo install tinystore-cli@0.5.0

Nothing 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.

Edit this page on GitHub Updated 1 November 2026

esc
↑↓ navigate ↵ open ⇥ filter 0 results