Typesense document destination for dlt (data load tool)
Project description
dlt-typesense
Typesense document destination for dlt. Load data into
Typesense collections with proper write dispositions (append, replace,
merge/upsert), not as a blind reverse-ETL sink.
Install dlt with Typesense
pip install dlt-typesense
Or with uv:
uv add dlt-typesense
For local development:
git clone https://github.com/marcopesani/dlt-typesense.git
cd dlt-typesense
uv sync --group dev
Destination capabilities
| Feature | Value |
|---|---|
| Preferred loader file format | jsonl |
| Supported loader file formats | jsonl (plus dlt's internal reference format for cleanup follow-up jobs) |
| Has case sensitive identifiers | True |
| Supported merge strategies | upsert (default), insert-only |
| Supported replace strategies | truncate-and-insert |
| Max identifier length | 255 |
delete-insert and scd2 merge strategies and staging replace strategies
(insert-from-staging, staging-optimized) are not supported — there is
no SQL layer or staging dataset.
Setup guide
- Install the package (see above).
- Add credentials to
.dlt/secrets.toml(or the matchingDESTINATION__TYPESENSE__CREDENTIALS__*env vars). Theapi_keyis required; without it the run fails before any load starts. The key is never written to logs, reprs, or error messages.
[destination.typesense.credentials]
host = "localhost" # default: localhost
port = 8108 # default: 8108
protocol = "http" # default: http
api_key = "local-dev-key"
- Define a resource and a pipeline. The destination resolves by short name
(
destination="typesense") or by importing the factory:
import dlt
from dlt_typesense import typesense
@dlt.resource(name="products", write_disposition="merge", primary_key="sku")
def products():
yield {"sku": "A1", "title": "Widget", "price": 9.99}
pipeline = dlt.pipeline(
pipeline_name="shop",
destination=typesense(), # or destination="typesense"
dataset_name="catalog",
)
pipeline.run(products())
- Start a local Typesense with
docker compose up -dand run the examples inexamples/.
typesense_adapter
By default collections use Typesense auto schema (a .* catch-all field).
typesense_adapter pins explicit typed fields for hinted columns and sets
collection-level options. Unhinted columns still fall through to .* auto.
A type hint is a contract about both the schema and the document value.
When you pin a dlt timestamp/date column to int64/int32, the load job
stores Unix epoch seconds (Typesense's recommended date representation —
range-filterable and sortable). When you pin a decimal/wei column to
float/int64/int32, the exact decimal string on the wire is parsed to that
numeric type. Apply the adapter to individual resources, not to a whole source.
from dlt_typesense import typesense, typesense_adapter
pipeline.run(
typesense_adapter(
products,
facet="category", # shorthand for {"facet": True}
sort=["price", "rating"], # shorthand for {"sort": True}
field_hints={
"description": {"locale": "de", "infix": True},
"last_update": {"type": "int64", "sort": True}, # timestamp → epoch
"gltv_eur": {"type": "float", "sort": True}, # decimal → float
"embedding": {"type": "float[]", "num_dim": 384},
"summary_vec": {
"type": "float[]",
"embed": {
"from": ["title", "description"],
"model_config": {"model_name": "ts/e5-small"},
},
},
},
collection_hints={
"default_sorting_field": "price",
"token_separators": ["-"],
"metadata": {"owner": "search-team"},
},
)
)
field_hintsaccepts, per column:type,facet,index,optional,sort,infix,locale,stem,stem_dictionary,num_dim,vec_dist,hnsw_params,reference,async_reference,cascade_delete,range_index,store,truncate_len,token_separators,symbols_to_index,embed.collection_hintsaccepts:default_sorting_field,token_separators,symbols_to_index,enable_nested_fields,metadata,synonym_sets,curation_sets.no_remove_orphans=Truedisables orphan cleanup of nested-table documents under merge (see Orphan cleanup).- Unknown parameter names raise
ValueErrorwhen the adapter is called; value errors (e.g. a badlocaleorvec_dist) surface as terminal errors from the server at load time.
Cleaning rows with add_map
The type contract never auto-parses text columns into arrays or numbers —
that is data cleaning. Decode JSON-encoded strings (e.g. Redshift SUPER) with
dlt's native add_map before loading:
import json as pyjson
def clean_row(row: dict) -> dict:
raw = row.get("categories")
row["categories"] = pyjson.loads(raw) if isinstance(raw, str) else (raw or [])
row.setdefault("receive_marketing", False)
return row
resource.add_map(clean_row)
pipeline.run(
typesense_adapter(
resource,
field_hints={"categories": {"type": "string[]", "facet": True}},
)
)
add_map sees whatever shape the source yields. With
sql_database(backend="pyarrow") items are Arrow tables, not dicts — either
use backend="sqlalchemy" for row-shaped items, or map the table
(table.to_pylist() inside an add_yield_map). See
examples/sql_incremental_sync_pipeline.py.
Write disposition
Replace
truncate-and-insert: drop and recreate the collection, then import. A
concurrent reader can observe an empty collection window mid-replace.
Merge
upsert(default): documentidis a deterministic uuid5 of theprimary_key(or useruniquecolumns). Re-runs update existing documents.insert-only: documentidfrom_dlt_id; existing documents are never modified.
Set merge (and the primary key) from the first run; otherwise later merges
cannot reconcile documents created under append semantics. A merge table with
neither a primary_key nor a unique column fails terminally rather than
silently loading duplicates.
Orphan cleanup for nested tables
Nested lists become child collections (orders → orders__items →
orders__items__parts). Under merge (upsert), a follow-up job runs after
every load and deletes orphaned child documents — children whose parent
row was re-loaded but which were not re-written, i.e. elements that
disappeared from the parent's nested list:
pipeline.run(orders()) # o1 has items [a, b]
pipeline.run(orders()) # o1 now has items [a] → the "b" document is deleted
Semantics:
- Scoped to the load. Only root rows present in the load are cleaned up; children of parents that were not re-synced are never touched, so incremental loads stay incremental.
- Emptied lists are handled. Re-loading a parent whose list became empty deletes all of its child documents.
- All nesting levels. Grandchild collections (and deeper) are cleaned the same way.
- Merge
upsertonly.insert-onlymerge,append, andreplacenever run cleanup (replacerecreates collections instead). - Opt out per resource with
typesense_adapter(resource, no_remove_orphans=True)— re-loaded parents then leave stale child documents behind, as in versions before cleanup existed.
How it works: after all files of a merge table chain finish loading, a
follow-up job collects the loaded root ids (_dlt_id) and, per child
collection, exports the current child ids for those roots
(_dlt_root_id filter), diffs them against the ids just loaded, and deletes
the stale ones with batched filter_by requests (200 ids per request). Cost
is proportional to the number of children of the re-loaded parents.
Cleanup keys on dlt's system columns only (_dlt_id, _dlt_root_id), which
requires root-key propagation — dlt enables it automatically for merge. If
your pipeline explicitly disables it, the cleanup job fails terminally; opt
out with no_remove_orphans=True instead.
Append
Bulk import with action=upsert; document id from _dlt_id.
skip writes nothing for that table.
Data loading
Tables map to Typesense collections (dataset-qualified, e.g. catalog_products);
rows become documents loaded via streamed JSONL
bulk import.
Data types
| dlt type | Typesense (unhinted) | With numeric type hint |
|---|---|---|
text |
string |
no auto-parse (use add_map) |
bigint |
int64 |
as-is |
double |
float |
as-is |
bool |
bool |
as-is |
timestamp / date |
ISO-8601 string |
int64/int32 → Unix epoch seconds |
time |
ISO-8601 string |
— |
decimal / wei |
exact string |
float/int64/int32 → parsed number |
binary |
base64 string |
— |
json |
JSON string |
native JSON when hinted float[]/string[]/object/… |
Pinning a timestamp to float is rejected (Typesense floats are 32-bit and
lose epoch-second precision). Use int64 for range filters / sort, or int32
when the column must be default_sorting_field (valid until 2038).
Dataset name
Collection names are {dataset_name}{dataset_separator}{table_name}
(separator defaults to _). Empty dataset_name yields the bare table name.
Names longer than 255 characters are truncated with a hash suffix.
Reserved id
Typesense reserves the top-level document id, which this destination manages
(from _dlt_id or the merge key). A source column named id is renamed to
__id by the naming convention, so its value is preserved and never silently
overwritten.
Schema hints lifecycle
Hints apply at collection creation only: first load, every replace run, and
dev-mode/full-refresh runs. Changing hints on an existing append/merge
collection has no effect until the collection is recreated (a log line notes
this). There is no schema PATCH/alter support — the safe path for production
schema changes is recreate under a new name and switch a collection alias.
Additional destination options
Passed to typesense(...) or resolved from config (destination.typesense.*):
| Knob | Default | Purpose |
|---|---|---|
dataset_separator |
"_" |
Separator between dataset and table in collection names |
client_batch_size |
1000 |
Documents per HTTP import request (files are streamed, never buffered whole) |
server_batch_size |
40 |
Typesense batch_size import query parameter |
import_action |
"upsert" |
Import action. emplace updates only provided fields. Do not use create — it breaks dlt's whole-file retry idempotency |
connection_timeout_seconds |
5.0 |
Connect timeout (on credentials) |
read_timeout_seconds |
180.0 |
Read timeout for long import requests |
max_parallel_load_jobs |
None |
Optional loader parallelism override |
Run Typesense locally
docker compose up -d # Typesense 30.2, API key local-dev-key
Production pattern: incremental SQL sync
Real syncs look like sql_database + incremental cursor + merge + hints +
optional add_map. A declarative multi-collection config keeps each sync
spec in one place:
COLLECTIONS = [
{
"collection": "users",
"source": {"table": "user_stats"},
"primary_key": "user_id",
"timestamp_field": "last_update",
"field_hints": {"last_update": {"type": "int64", "sort": True}},
"transform": decode_categories,
},
]
See examples/sql_incremental_sync_pipeline.py
for a runnable SQLite version with --target, --limit, --full-refresh,
and --dev-mode.
dbt support
The Typesense destination does not support dbt.
Syncing of dlt state
The Typesense destination supports syncing of the dlt state (incremental
cursors survive clean runners).
Development
uv sync --group dev
uv run ruff check .
uv run ruff format --check .
uv run basedpyright
uv run pytest # unit tests (no server)
docker compose up -d # start Typesense for integration tests
uv run pytest -m integration # integration tests (fail, never skip, if unreachable)
See docs/architecture.md for module seams and scale notes.
Contributing
See CONTRIBUTING.md.
By contributing you agree to the Developer Certificate of Origin
(sign off commits with Signed-off-by).
License
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file dlt_typesense-0.2.0.tar.gz.
File metadata
- Download URL: dlt_typesense-0.2.0.tar.gz
- Upload date:
- Size: 170.7 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
222b86fdea3aff66cb32d82f9b454c931ae56ff6449f6ecba36435faa208b386
|
|
| MD5 |
232e8de2f02847fb5e1af415a34995a6
|
|
| BLAKE2b-256 |
b63c13cda71349d848bc2ee7d77d31150a7744748c06b82d5ff3faf6f0a9fcc1
|
Provenance
The following attestation bundles were made for dlt_typesense-0.2.0.tar.gz:
Publisher:
publish.yml on marcopesani/dlt-typesense
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
dlt_typesense-0.2.0.tar.gz -
Subject digest:
222b86fdea3aff66cb32d82f9b454c931ae56ff6449f6ecba36435faa208b386 - Sigstore transparency entry: 2189596657
- Sigstore integration time:
-
Permalink:
marcopesani/dlt-typesense@9ed43a6991805796df3911030e05e8d7fa65e47f -
Branch / Tag:
refs/tags/v0.2.0 - Owner: https://github.com/marcopesani
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@9ed43a6991805796df3911030e05e8d7fa65e47f -
Trigger Event:
release
-
Statement type:
File details
Details for the file dlt_typesense-0.2.0-py3-none-any.whl.
File metadata
- Download URL: dlt_typesense-0.2.0-py3-none-any.whl
- Upload date:
- Size: 36.0 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
852316bb079f279cf9567fa098cc57c786d613e382f5f23e36ce5293ab25aeda
|
|
| MD5 |
d32d03639e376b43abcfead7c33f5928
|
|
| BLAKE2b-256 |
60ae89ff4178e095aa6358dadb9ad72d0e69da449e6f03a07d8a8f6771992480
|
Provenance
The following attestation bundles were made for dlt_typesense-0.2.0-py3-none-any.whl:
Publisher:
publish.yml on marcopesani/dlt-typesense
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
dlt_typesense-0.2.0-py3-none-any.whl -
Subject digest:
852316bb079f279cf9567fa098cc57c786d613e382f5f23e36ce5293ab25aeda - Sigstore transparency entry: 2189596685
- Sigstore integration time:
-
Permalink:
marcopesani/dlt-typesense@9ed43a6991805796df3911030e05e8d7fa65e47f -
Branch / Tag:
refs/tags/v0.2.0 - Owner: https://github.com/marcopesani
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@9ed43a6991805796df3911030e05e8d7fa65e47f -
Trigger Event:
release
-
Statement type: