# Show HN: MorletDB is a Postgres extension for waveforms

> Source: <https://github.com/merkhet-io/morletdb>
> Published: 2026-11-14 20:34:17+00:00

*ἁρμονίη ἀφανὴς φανερῆς κρείττων.*

<sub>— Heraclitus, fragment B54</sub>

**MorletDB is a PostgreSQL extension for waveform archives.**

Select an interval and ask *“Have we seen a signal like this before?”* — every answer is a recording

and a sample interval you can read back, for an engineer or for an AI agent.

[AI agents](#for-ai-agents) · [Install](#install) · [Use](#use) · [Research](#research) · [Explore](#explore) · [Evidence](#what-has-been-measured) · [Website](https://merkhet-io.github.io/morletdb/)

**Recordings, not rows.** A recording is one channel with a sample rate, a unit, a
calibration and its acquisition metadata. MorletDB keeps that unit intact inside
PostgreSQL, instead of scattering samples over rows or vectors.

- **Lossless and compact.** Raw samples are coded bit-exactly, often several times
smaller than general-purpose time-series compression on the same data. The codec,[Grizzbit](https://github.com/merkhet-io/morletdb/blob/main/docs/grizzbit.md) , stores each chunk on the value grid it detects, with no
precision parameter, and is weaker on floats without a grid.
- **Search that tolerates time shifts.** Two occurrences of the same pattern rarely
line up with a window grid. MorletDB scores similarity over a bounded range of
shifts, exactly or through a standard vector index.
- **Every hit traces back.** Results are intervals of recordings, with their
metadata, that read back the original samples.
- **Safe to keep ingesting.** Appends, search representations and index rows commit
in one transaction; retries are verified, gaps and overlaps are rejected.
- **Plain PostgreSQL.** SQL functions, your roles and row-level security, your
backups. pgvector is optional.

**Give your agents signal evidence to reason with.** An investigation agent can search
your recording archive before it answers: *have we seen a signal like this before?*
MorletDB supplies the matching intervals and their original samples; your application
connects them to analysis tools, maintenance notes or experiment records.

The tool is plain SQL, so it runs with the agent's database role, row-level security and limits:

```
-- a bounded tool an agent can call: similar intervals for a source interval and an asset
CREATE FUNCTION agent_similar_intervals(recording text, from_sample bigint, to_sample bigint,
                                        asset text, k integer DEFAULT 5)
RETURNS TABLE (recording_id text, start_sample bigint, end_sample bigint, distance float8)
LANGUAGE sql STABLE AS $$
  SELECT recording_id, start_sample, end_sample, distance
  FROM morlet_search_envelope_probes('signals', recording, from_sample, to_sample,
                                     jsonb_build_object('asset', asset), least(k, 20))
$$;
-- and to bring evidence into analysis: morlet_read_interval('signals', recording_id, start, end)
```

| Use case | What the agent does | 
|---|---|
| **Anomaly investigation** | an external detector flags an interval; the agent checks whether it resembles earlier events and under which operating conditions | 
| **Autonomous factory workflows** | gathers signal evidence across machine runs before proposing maintenance, a quality check or an escalation; your application decides which actions need approval | 
| **Engineering investigation** | retrieves earlier test runs to explain a changed transient, with the experiment metadata | 
| **Research and labelling assistants** | collects similar intervals for human review, annotation or model evaluation | 

**Try it with Claude:** the [agent demo](https://github.com/merkhet-io/morletdb/blob/main/examples/agent-demo/README.md) loads four days of public seismic
recordings with Docker and lets Claude Code or Claude Desktop investigate an earthquake through the
read-only [MCP server](https://github.com/merkhet-io/morletdb/blob/main/docs/mcp.md). Four of the five closest matches it finds are other catalogued earthquakes.
The [machine demo](https://github.com/merkhet-io/morletdb/blob/main/examples/machine-demo/README.md) does the same for a bearing run to failure (NASA/IMS):
Claude finds when the failing bearing first changed and whether the others show it.

Important

The SQL function above is an integration pattern; the bundled MCP server only reads. Detection models,
agent orchestration and machine controls come from your application. On seismic data, this search
returns catalogued earthquakes about twice as often as zero-lag similarity
([E5](https://github.com/merkhet-io/morletdb/blob/main/evaluation/task_relevance/RESULTS.md), [E5-H](https://github.com/merkhet-io/morletdb/blob/main/evaluation/task_relevance/RESULTS_HELDOUT.md));
anomaly workflows have not been evaluated, and similarity alone does not establish normality, a fault
or its cause.

1. **Append** chunks of a recording at their global sample offset.
2. **Build** search representations, such as amplitude envelopes of 4096-sample
windows, in the same transaction, so new windows are searchable at commit.
3. **Search** with a query interval: exact scoring of every eligible window, or
approximate candidates from a vector index followed by exact re-ranking.
4. **Read back** each match as the original samples, with its recording metadata.

**Grizzbit** is MorletDB's lossless chunk codec: what goes in comes back **bit for bit**. Each chunk of up to
32,768 float64 samples is stored in the smallest of three exact forms: integer steps on the value grid the
encoder detects (predicted and Rice-coded as in FLAC, with last-place corrections where needed), the float64 bit
patterns coded the same way, or plain float64. Every chunk carries a CRC-32C checksum.

- **No precision parameter.** Nothing can be configured to drop detail. Finite values up to ±10⁹ are stored
exactly; anything else is refused at write time, never altered.
- **Checked when written.** With`morlet.verify_appends` on (the default), every append decodes each chunk again
and compares it bit for bit before storing it.
- **Strong on gridded signals, weaker without a grid.** On ADC counts, fixed decimals and calibrated waveforms it
is among the smallest codecs measured (B4); on floats without a clean grid it is 1.6–4× larger than the best.
Grizzbit 2, an attempt to close that gap, failed study B5 and was not shipped (#326).
- **Float32 codings, on by default** (chunk format version 6): samples that are float32 values, such as PLC and
CNC controller signals, are coded in float32 terms where that is smaller. It passed study B8: on IU4 force and
controller channels 3.02 and 2.29 bytes per sample instead of 6.60 and 6.52, never a larger chunk
([results](https://github.com/merkhet-io/morletdb/blob/main/evaluation/storage_b8/RESULTS.md) ;`SET morlet.float32_chunks = off` turns it off).
- **Grizzbit 2.1, opt-in** (`morlet.chunk_format = grizzbit21` ): a per-chunk linear predictor and corrections per
grid index, kept only where smaller. It passed study B6: waveforms 12.6% smaller in total, never larger
([how to use it](https://github.com/merkhet-io/morletdb/blob/main/docs/grizzbit.md#using-grizzbit-21) ).
- **Nothing here is novel** , and the docs say so: Grizzbit is a named, specified format built from established
lossless techniques (FLAC's fixed predictors and Rice coding, SEED and EDF style gain and offset).

More: [docs/grizzbit.md](https://github.com/merkhet-io/morletdb/blob/main/docs/grizzbit.md) · [codecs at a glance](https://github.com/merkhet-io/morletdb/blob/main/docs/grizzbit.md#codecs-at-a-glance) · [format specification](https://github.com/merkhet-io/morletdb/blob/main/docs/chunk-format.md) · [crates/grizzbit](https://github.com/merkhet-io/morletdb/blob/main/crates/grizzbit)

Full setup and query examples: [installation and query guide](https://github.com/merkhet-io/morletdb/blob/main/docs/installation-and-queries.md).
Source builds use Rust 1.99.0 and pgrx 0.19.3.

**Run the release image** (prebuilt: PostgreSQL 17 with pg_morlet, Linux amd64 and arm64, with SBOM and
build provenance; no pgvector, so no probe search; choose your own password):

```
docker run -d --name morletdb -e POSTGRES_USER=morlet -e POSTGRES_PASSWORD=change-me \
  -e POSTGRES_DB=morlet -p 127.0.0.1:54329:5432 -v morletdb-data:/var/lib/postgresql/data \
  ghcr.io/merkhet-io/morletdb:0.3.14-alpha.1
docker exec morletdb psql -U morlet -d morlet -c "CREATE EXTENSION pg_morlet"
```

**Try the walkthrough** (builds a PostgreSQL 17 preview for your machine, amd64 or arm64; imports
synthetic data, finds a known interval and reconstructs its matches):

```
docker compose run --rm walkthrough
```

**Build from source** with Rust and [pgrx](https://github.com/pgcentralfoundation/pgrx)
(PostgreSQL 17, 18 or 19; on Debian/Ubuntu install `build-essential bison flex libreadline-dev zlib1g-dev libclang-dev clang pkg-config libssl-dev` first):

```
cargo install cargo-pgrx --version 0.19.3 --locked
cargo pgrx init --pg17 download          # or --pg18, --pg19 /path/to/pg_config, or --pg17 /path/to/pg_config
cd extensions/pg_morlet
cargo pgrx install --release --pg-config "$(sed -n 's/^pg17 = "\(.*\)"/\1/p' ~/.pgrx/config.toml)"
```

Then, in your database:

```
CREATE EXTENSION pg_morlet;
CREATE EXTENSION vector;   -- optional: only for probe search (pgvector 0.8 or later)
```

**Use it from Python**: ingest a NumPy waveform, read exact intervals and search similar ones with the
[Python SDK](https://github.com/merkhet-io/morletdb/blob/main/docs/python-sdk.md) (`sdk/python`, alpha), starting with a [runnable quickstart](https://github.com/merkhet-io/morletdb/blob/main/examples/python/quickstart.py).

**Let an AI agent investigate your recordings** through a read-only MCP server (Claude Code,
Claude Desktop and other MCP clients): [MorletDB for AI agents](https://github.com/merkhet-io/morletdb/blob/main/docs/mcp.md).

**What MorletDB works with** (PostgreSQL, pgvector, Docker, MCP, Grafana, Python and Jupyter, file formats),
with how each is verified: [integrations](https://github.com/merkhet-io/morletdb/blob/main/docs/integrations.md).

**Import your own files** (WAV, EDF, miniSEED, CSV) and get a first search query:
[from files to a first search](https://github.com/merkhet-io/morletdb/blob/main/docs/import.md).

Supported versions, CI coverage and local test commands:
[PostgreSQL compatibility](https://github.com/merkhet-io/morletdb/blob/main/docs/compatibility.md). Upgrading an existing
installation: [existing installations](https://github.com/merkhet-io/morletdb/blob/main/extensions/pg_morlet/README.md#existing-installations).

```
-- a collection holds recordings, raw chunks and search representations
SELECT morlet_create_stream_collection('public', 'signals');
SELECT morlet_enable_envelopes('signals');

-- one recording = one channel: id, sample rate, channel, unit, metadata
SELECT morlet_start_recording('signals', 'pump-7', 25600, 'accel_x', 'g', '{"asset": "P7"}');

-- append samples at their global offset; repeating an identical append is a no-op
SELECT morlet_append_chunk('signals', 'pump-7', 0,
  ARRAY(SELECT sin(i / 40.0) * (1 + (i / 3000) % 3) FROM generate_series(0, 32767) i));

-- build envelopes (4096-sample windows, stride 2048); call it in the append
-- transaction to make new windows searchable at commit
SELECT morlet_build_envelopes('signals', 'pump-7');

-- where else does samples 8192..12288 occur? shift-tolerant search
SELECT recording_id, start_sample, end_sample, distance
FROM morlet_search_similar('signals', 'pump-7', 8192, 12288, '{}', 5);

-- read any match back as original samples
SELECT samples FROM morlet_read_interval('signals', 'pump-7', 26624, 30720);

-- progress and backlog per window size
SELECT * FROM morlet_collection_status('signals');
```

For large archives, enable **probe search** (needs pgvector): it finds candidates
through an HNSW index and re-ranks them with the same exact score. It is
approximate in which windows it considers, never in the distances it returns.

```
SELECT morlet_enable_envelope_probes('signals', 4096);
SELECT * FROM morlet_search_similar('signals', 'pump-7', 8192, 12288);   -- now uses probes
```

`morlet_search_similar` uses probe search when it is enabled for the window size and exact search
otherwise; `morlet_search_envelope_interval` and `morlet_search_envelope_probes` choose a path
explicitly.

The fifth argument of the interval searches is a metadata filter (`'{"asset": "P7"}'`).
Small filtered galleries are routed to exact search automatically. Searches run with
the caller's permissions, including row-level security.

More: [SQL reference](https://github.com/merkhet-io/morletdb/blob/main/extensions/pg_morlet/README.md) · [search contract](https://github.com/merkhet-io/morletdb/blob/main/docs/search-contract.md) ·
[recording workflow](https://github.com/merkhet-io/morletdb/blob/main/docs/recording-workflow.md) · [operations](https://github.com/merkhet-io/morletdb/blob/main/docs/operations.md) ·
[access control](https://github.com/merkhet-io/morletdb/blob/main/docs/access-control.md) · [Parquet import](https://github.com/merkhet-io/morletdb/blob/main/tools/ingest/README.md)

| Task | Main functions | 
|---|---|
| Create a collection and recording | `morlet_create_stream_collection` ,`morlet_start_recording` | 
| Append and inspect progress | `morlet_append_chunk` ,`morlet_stream_status` ,`morlet_collection_status` | 
| Read original samples | `morlet_read_interval` | 
| Search Morlet/statistical features | `morlet_search_streams` ,`morlet_search_interval` | 
| Materialize envelopes | `morlet_enable_envelopes` ,`morlet_build_envelopes` | 
| Search similar intervals (probes if enabled, else exact) | `morlet_search_similar` | 
| Search bounded-lag envelopes exactly | `morlet_search_envelopes` ,`morlet_search_envelope_interval` | 
| Enable and query optional indexed probes | `morlet_enable_envelope_probes` ,`morlet_search_envelope_probes` | 
| Inspect storage | `morlet_recording_storage` ,`morlet_collection_storage` | 

The [SQL guide](https://github.com/merkhet-io/morletdb/blob/main/docs/installation-and-queries.md#store-search-read-back) walks
through storing two synthetic recordings, building envelopes, filtering a query,
and reading its matches. Intervals are zero-based and half-open; feature and
envelope search use different scorers. Envelopes require an explicit builder;
raw ingestion alone does not make a recording searchable through that path.
Probe candidate selection is approximate above the exact-routing threshold.
See the [search contract](https://github.com/merkhet-io/morletdb/blob/main/docs/search-contract.md) for budgets, permissions,
source exclusion, overlap suppression and snapshot semantics.

| [MorletDB: Recordings, Not Rows — Lag-Tolerant Waveform Retrieval in PostgreSQL](https://github.com/merkhet-io/morletdb/blob/main/research/paper/morletdb.pdf) Version 0.25 Bounded-lag cross-correlation is the right score when matches may be shifted in time, but standard vector indexes rank by an unshifted metric. Because each lag term is an inner product with a shifted copy of the query, a sparse grid of **shifted probes** into an ordinary pgvector HNSW index, re-ranked with the exact score, recovers the shift-tolerant top ten. | On held-out seismic data (234,622 windows):  | 

What it costs and where it is not shown: a probe table and index of about the size
of the raw data, slower search under filters that exclude a query's neighbours, and
extra write-ahead logging for index maintenance. The paper's retrieval results are seismic. Study
[N1](https://github.com/merkhet-io/morletdb/blob/main/evaluation/nilm_n1/RESULTS.md) added appliance identification from current transients (PLAID): exact
envelope search named the appliance about as well as a current-feature 1-NN (H1 passed), but its distances did
not flag unknown appliances (H2 failed). Other signals, diagnostic relevance and long-running operation are not
shown.
[Supplement](https://github.com/merkhet-io/morletdb/blob/main/research/paper/supplement.pdf) · [study overview](https://github.com/merkhet-io/morletdb/blob/main/research/paper/STUDIES.md) ·
[reproduce the numbers](https://github.com/merkhet-io/morletdb/blob/main/research/paper/ARTIFACT.md) · [main results](https://github.com/merkhet-io/morletdb/blob/main/research/paper/README.md#main-numbers)

| **Recording viewer.** A local, read-only page for a recording, its context lanes (program, block, tool, part), segments and labels; compare a segment with the same context on other parts, or find similar windows.[tools/viewer](https://github.com/merkhet-io/morletdb/blob/main/tools/viewer/README.md) 

```
python -m pip install -r tools/viewer/requirements.txt
DATABASE_URL=postgresql://... python tools/viewer/demo.py
DATABASE_URL=postgresql://... python tools/viewer/viewer.py
```

 |  | 
| **Sleep staging by example.** 153 Sleep-EDF nights stored as raw recordings; click an epoch of a held-out night and see the 40 most similar expert-scored epochs from other people, each read back from its interval. A research demo, not a medical device.[examples/sleep](https://github.com/merkhet-io/morletdb/blob/main/examples/sleep/README.md) |  | 

Also: [compare a machine with its own history](https://github.com/merkhet-io/morletdb/blob/main/docs/context-workflow.md) under the
same operating context · [visual notebook](https://github.com/merkhet-io/morletdb/blob/main/examples/notebooks/README.md) ·
[Parquet importer](https://github.com/merkhet-io/morletdb/blob/main/tools/ingest/README.md)

Every evaluation fixes its protocol before reading the data, runs once, and reports the result, including negative ones. In short:

- **Held up:** lossless storage (1.2–6× smaller than TimescaleDB compression on the
same samples), exact and shift-tolerant search, probe search on standard indexes,
retrieval of labelled examples as explanation, comparison under the same operating
context, access control.
- **Did not hold up:** Morlet features have not beaten simple FFT band powers on
bearings, ECG or sleep EEG; no wavelet or filter type has beaten a plain band-pass
filter for bearing faults; own-history comparison has not warned earlier than a plain
RMS alarm on run-to-failure bearings.

Every evaluation with its data, baselines, confidence intervals and verdict:
**[docs/evaluations.md](https://github.com/merkhet-io/morletdb/blob/main/docs/evaluations.md)**.

Append-only recordings; 1–32,768 samples per chunk; explicit channel and unit; no
resampling, gap interpolation, background extraction worker or cross-channel search.
Exact search scans eligible windows; probe search is approximate and opt-in. Alpha:
no stable API or schema guarantee yet. Details: [support boundaries](https://github.com/merkhet-io/morletdb/blob/main/SUPPORT.md) ·
[compatibility](https://github.com/merkhet-io/morletdb/blob/main/docs/compatibility.md) · [numerical contract](https://github.com/merkhet-io/morletdb/blob/main/docs/morlet-extractor.md).

**The name.** MorletDB honours Jean Morlet and the project's starting hypothesis that
Morlet wavelets would suit search in technical signal archives. The system is not tied
to that transform, and our evaluations have not shown a general advantage for it.

| Path | What | 
|---|---|
| [extensions/pg_morlet](https://github.com/merkhet-io/morletdb/blob/main/extensions/pg_morlet) | the PostgreSQL extension: storage, ingestion, interval reads, search | 
| [crates/grizzbit](https://github.com/merkhet-io/morletdb/blob/main/crates/grizzbit) | [Grizzbit](https://github.com/merkhet-io/morletdb/blob/main/docs/grizzbit.md) , the lossless chunk codec ([specification](https://github.com/merkhet-io/morletdb/blob/main/docs/chunk-format.md) ) | 
| [crates/morlet-core](https://github.com/merkhet-io/morletdb/blob/main/crates/morlet-core) | portable Rust core: envelopes, features, checkpoints; re-exports the codec | 
| [tools](https://github.com/merkhet-io/morletdb/blob/main/tools) | Parquet import, Python client, recording viewer, independent numerical reference | 
| [examples](https://github.com/merkhet-io/morletdb/blob/main/examples) | sleep staging, notebooks and other worked examples | 
| [evaluation](https://github.com/merkhet-io/morletdb/blob/main/evaluation) ·[benchmarks](https://github.com/merkhet-io/morletdb/blob/main/benchmarks) | frozen evaluations and workload benchmarks | 
| [research/paper](https://github.com/merkhet-io/morletdb/blob/main/research/paper) | the paper, supplement and reproduction package | 

```
make test       # Rust core and formatting
make pg-test    # needs a pgrx-managed PostgreSQL
```

[Contributing](https://github.com/merkhet-io/morletdb/blob/main/CONTRIBUTING.md) · [Development](https://github.com/merkhet-io/morletdb/blob/main/docs/development.md) · [Support](https://github.com/merkhet-io/morletdb/blob/main/SUPPORT.md) ·
[Security](https://github.com/merkhet-io/morletdb/blob/main/SECURITY.md) · [Roadmap](https://github.com/merkhet-io/morletdb/blob/main/docs/product-roadmap.md) · [Brand assets](https://github.com/merkhet-io/morletdb/blob/main/assets/brand/README.md)

Copyright 2026 MorletDB contributors. Code, documentation and original brand artwork
are licensed under the [Apache License, Version 2.0](https://github.com/merkhet-io/morletdb/blob/main/LICENSE). Every part of MorletDB, integrations included, is open source under
that licence; there is no enterprise edition ([licensing](https://github.com/merkhet-io/morletdb/blob/main/docs/licensing.md)). The technical report's text and
figures are licensed under [CC BY 4.0](https://github.com/merkhet-io/morletdb/blob/main/research/paper/LICENSE.md). A
[Merkhet](https://github.com/merkhet-io) project.
