Skip to content

Tenant-partitioned collections: multi-tenant scale in one collection (Phase 6 core) - #11

Closed
EdgarBabajanyan wants to merge 3 commits into
feat/warm-serverlessfrom
feat/tenant-partitions
Closed

Tenant-partitioned collections: multi-tenant scale in one collection (Phase 6 core)#11
EdgarBabajanyan wants to merge 3 commits into
feat/warm-serverlessfrom
feat/tenant-partitions

Conversation

@EdgarBabajanyan

Copy link
Copy Markdown
Contributor

Problem

One collection cannot hold a large multi-tenant corpus: every attached collection costs RAM/attach-time proportional to its FULL size, so a billion vectors across thousands of tenants in one namespace is unservable — the measured single-namespace envelope is ~10–50M chunks.

Design

A partition IS a full internal collection. config.partition_by = "tenant_id" at create marks the parent; every chunk routes to {parent}--part--{value} — its own LSM manifest/WAL/segments, Tantivy dir, vector files, and attach/evict lifecycle, reusing the existing engine wholesale. Phase 6 is a router at the manager entry points, not a new engine: the fundamentals (collections, local-first behavior, unpartitioned paths) are untouched, and partition_by unset means zero code-path change.

  • Ingest groups by metadata[partition_by], auto-creates partitions on first sight (writer role bootstraps bucket objects with create-only writes).
  • Search / delete-by-filter require a filter on the partition field: exact → one partition, {"in": [...]} fans out (≤16, merged by score). Empty tenant = empty results, not an error.
  • Ids are collection-unique: partitions claim CAS-leased blocks from the PARENT's allocator (local mode included — the allocator is a CAS'd file, no WAL objects locally). Bare-id deletes are rejected with guidance since ids don't name their partition.
  • Partitions attach on demand even in non-lazy cloud mode (writers mint them at any time), are hidden from listings, and cascade-delete with the parent from disk, registry, and bucket.
  • Fenced with clear errors until routed: relations, facets, TAMS lookup, vector-space CRUD, min_seq across multi-partition fan-out. Writer-role fences consult the bucket config so a writer can't durably append into the parent namespace that no serving node materializes.

Evidence

  • Suites: 105 local / 153 object-storage (11 new partition tests: isolation, id uniqueness under interleaved ingest, filter-routed deletes, cascade, fences, restart, writer-role cross-node visibility, cold rebuild from bucket). Clippy clean; all four build combinations verified.
  • Live stack (MinIO + full node + writer node): 53/53 E2E checks including 9 new partition checks — writer-minted tenant attaches on a serving node that predates it.
  • Multi-tenant scale proof (one collection, serving node capped at 8 attached partitions):
50 tenants × 2k chunks 200 tenants × 500 chunks
Ingest 3,613 chunks/s 4,673 chunks/s
Boot→healthy (all registered, 0 attached) 0.6s / 14.8MiB RSS 0.6s / 14.7MiB RSS
Hot-set search p50 1.4ms 1.3ms
Full-sweep attach-on-demand p50 302ms 102ms
RSS after sweeping every tenant 557MiB 289MiB
Tenants served / isolation 50/50, holds 200/200, holds

Boot and serving RAM are independent of tenant count — RAM tracks cap × per-tenant size. That moves the envelope from per-collection to per-tenant: 1B vectors across 10k × 100k-vector tenants serves on the same bounded footprint.

Notes / follow-ups

…on (Phase 6 core)

Create a collection with config.partition_by = "tenant_id" and every
chunk routes to an internal per-tenant namespace. A partition IS a full
collection under the hood — own LSM manifest/WAL/segments, Tantivy dir,
vector files, attach/evict lifecycle — so Phase 6 is a router at the
manager entry points over the existing engine, not new machinery. This
is what moves the scale envelope from per-COLLECTION to per-TENANT: a
query for tenant T attaches T's slice only, LRU eviction and refresh
scale with the hot-tenant set, and RAM is bounded regardless of how
many tenants (or how many billion vectors) the collection holds.

Mechanics:
- Partition namespace: {parent}--part--{value} (separator reserved at
  create; partition values validated kebab-case, len <= 64). Namespaces
  reuse every existing storage/path/attach rule unchanged.
- Ingest groups by metadata[partition_by] and delegates per group,
  auto-creating partitions on first sight (inheriting the parent's
  vector spaces + embed model). Writer role routes the same way,
  bootstrapping partition bucket objects with create-only writes.
- Chunk ids are COLLECTION-unique: partitions claim CAS-leased blocks
  from the PARENT's allocator — in local mode too (the allocator is a
  CAS-updated file; no WAL/manifest objects locally). Without this,
  delete-by-id and search results would be ambiguous across tenants.
- Search and delete-by-filter require a filter on the partition field:
  exact match routes to one partition, set membership fans out (<=16,
  merged by score). A tenant with no data is empty, not an error.
  Bare-id deletes are rejected with guidance (ids don't name their
  partition; probing every partition is unbounded).
- Partitions attach on demand even in non-lazy cloud mode (a writer can
  mint one at any time — 'all attached at boot' can never hold), are
  hidden from listings, and are cascade-deleted with the parent from
  disk, registry, and bucket.
- Fenced with clear errors until routed: relations, facets, TAMS
  lookup, vector-space CRUD, min_seq across a multi-partition fan-out.
  Writer-role fences consult the bucket config, so a writer can't
  durably append relations/tombstones into the parent namespace that no
  serving node ever materializes.

Tests (11 new): tenant isolation on shared query terms, id uniqueness
under interleaved multi-tenant ingest, filter-routed deletes, listing/
cascade behavior, fence + validation errors, restart persistence,
writer-role ingest visible on a serving node that predates the
partitions, and cold rebuild of a partitioned collection from the
bucket alone (ids keep minting without reuse). Suites: 105 local / 153
object-storage, clippy clean, all four build combinations verified.

Signed-off-by: Edgar Babajanyan <bedgar2005@gmail.com>
Signed-off-by: Edgar Babajanyan <bedgar2005@gmail.com>
Signed-off-by: Edgar Babajanyan <bedgar2005@gmail.com>
@EdgarBabajanyan

Copy link
Copy Markdown
Contributor Author

Shipped in v0.4.0 via the consolidated release PR #13 (all commits, authorship, and sign-offs from this branch landed there verbatim).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant