> Fetch the index of the TinyStore docs, every page as markdown, at https://tinyshed.org/tinystore/llms.txt

# 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](#every-word-as-a-prefix): `лин алг` finds «Линейная алгебра».
  - [The other keyboard layout](#the-other-keyboard-layout): `rhbgnj` is read as `крипто`.
  - [Show what matched](#show-what-matched): ranges to show in bold, in your language's string units.
- **KV**
  - [Update without losing a write](#update-without-losing-a-write): `update` tries again when another write came first.
- **Errors**
  - [Limits by name](#limits-by-name): every limit error says which bound, what the call wanted and the most it takes.
- **Command line**
  - [What each name holds](#what-each-name-holds): `status --names` counts and weighs every bucket, stream and index.

> \[!WARNING]
> **Two changes can break your build**
> In Bun and Node, `limits` holds each bound as `{ name, bound }`, named as
> the core names it, instead of a name alone. In Rust, `search::Hit` has a
> new field, `matches`, and is `#[non_exhaustive]`:
>
> ```diff for=bun
> - if (error instanceof LimitError && error.limit === limits.decodedSamples) {
> + if (error instanceof LimitError && error.limit === limits.querySamples.name) {
> ```
>
> ```diff for=rust
> - let Hit { id, value, score } = hit;
> + let Hit { id, value, score, .. } = hit;
> ```
>
> [Changed names](#changed-names) lists the rest. A store's files don't
> change, so 0.4 and 0.5.0 open each other's stores.

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

```ts
const { hits } = await courses.search({ text: 'лин алг', prefix: 'every' })
```

```python
result = await courses.search("лин алг", prefix="every")
```

```go
result, err := courses.Search(ctx, "лин алг", search.PrefixEvery())
```

```rust
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](https://tinyshed.org/tinystore/docs/guides/search/prefix.md#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:

```ts
const courses = store.index<{ name: string }>('courses', { fields: { name: 1 }, layouts: ['ru', 'en'] })
const { hits, corrected } = await courses.search({ text: 'rhbgnj', prefix: true })
corrected // 'крипто'
```

```python
courses = store.index("courses", Course, fields={"name": 1}, layouts=["ru", "en"])
result = await courses.search("rhbgnj", prefix=True)
```

```go
courses, err := search.OpenIndex[Course](store, "courses", search.Fields{"name": 1}, search.Layouts("ru", "en"))
result, err := courses.Search(ctx, "rhbgnj", search.Prefix())
```

```rust
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](https://tinyshed.org/tinystore/docs/guides/search/layouts.md) 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:

```ts
const { hits } = await courses.search({ text: 'алгебре', matches: true })
hits[0].matches // [{ field: 'name', start: 9, end: 16, word: 'алгебре' }]
```

```python
result = await courses.search("алгебре", matches=True)
result.hits[0].matches  # [Match(field='name', start=9, end=16, word='алгебре', item=None)]
```

```go
result, err := courses.Search(ctx, "алгебре", search.Matches())
result.Hits[0].Matches // [{Field: "name", Start: 17, End: 31, Word: "алгебре"}]
```

```rust
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](https://tinyshed.org/tinystore/docs/guides/search/matches.md) 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:

```ts
const next = await settings.update('home', current => ({ ...(current ?? defaults), theme: 'dark' }))
```

```python
next = await settings.update("home", lambda current: replace(current or defaults, theme="dark"))
```

```go
next, _, err := settings.Update(ctx, "home", func(current Settings, found bool) (Settings, bool) {
	if !found {
		current = defaults
	}
	current.Theme = "dark"
	return current, true
})
```

```rust
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](https://tinyshed.org/tinystore/docs/guides/kv/versions.md#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:

```ts
const most = limits.setManyDocuments.bound // 1000
```

```python
most = bounds.SET_MANY_DOCUMENTS.bound  # 1000
```

```go
most := limits.SetManyDocuments.Bound // 1000
```

```rust
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](https://tinyshed.org/tinystore/docs/handbook/production/limits.md#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](https://tinyshed.org/tinystore/docs/guides/cli/status-and-logs.md#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

```sh for=bun
bun add @tinyshed/tinystore@0.5.0
npm install @tinyshed/tinystore@0.5.0
```

```sh for=python
pip install tinyshed-tinystore==0.5.0
```

```sh for=go
go get tinyshed.org/tinystore@v0.5.0
```

```sh for=rust
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](https://github.com/tinyshed/tinystore/releases/tag/v0.5.0).
