mirror of
https://github.com/seaweedfs/seaweedfs.git
synced 2026-11-05 11:37:44 +01:00
seaweed-vector: dev plan for ANN search over S3 Vectors indexes (#12408)
* seaweed-vector: dev plan for ANN search over S3 Vectors indexes Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011UpvafvK8JpagF94MHWqwK * seaweed-vector: dev plan: one probe count per query, a budget that can shrink Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011UpvafvK8JpagF94MHWqwK * seaweed-vector: dev plan: smaller deltas are searched in full only up to the probe count Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011UpvafvK8JpagF94MHWqwK * seaweed-vector: dev plan: drop the private issue reference Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * seaweed-vector: dev plan: phase 1 probes use the Design count Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * seaweed-vector: dev plan: status after phases 0 to 3, and what the measurements changed Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011UpvafvK8JpagF94MHWqwK * seaweed-vector: dev plan: name the Enterprise repository, not the private one Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011UpvafvK8JpagF94MHWqwK * seaweed-vector: dev plan: the Enterprise repository by that name Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011UpvafvK8JpagF94MHWqwK * seaweed-vector: dev plan: a filtered query widens to four times the probes, not without bound Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011UpvafvK8JpagF94MHWqwK * seaweed-vector: dev plan: throughput through the index at 50k and 200k Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011UpvafvK8JpagF94MHWqwK * seaweed-vector: dev plan: union coverage over shard names, and an explicit merge at the delta limit Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011UpvafvK8JpagF94MHWqwK * seaweed-vector: dev plan: status table names branches, not pull requests Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * seaweed-vector: dev plan: maximum_nprobes is always four times the count Lance reads maximum_nprobes only under a prefilter, and the phase 1 policy sets it the same way for every query. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * seaweed-vector: dev plan: the retrain follows the partition count, not a recorded row count Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * seaweed-vector: dev plan: phase 3 is open like the other phases Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * seaweed-vector: dev plan: the retrain stops past 2,048 partitions Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * seaweed-vector: dev plan: the merge at the delta limit takes the name's delta count Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
1 parent
cd10b4a5c1
commit
8350f297af
1 file changed
+339
@@ -0,0 +1,339 @@
|
||||
# S3 Vectors ANN — Dev Plan
|
||||
|
||||
## Goal
|
||||
|
||||
Approximate nearest-neighbour search for S3 Vectors indexes, built on Lance
|
||||
vector indexes, so that query latency stops growing linearly with the index.
|
||||
Target: 90%+ recall@10 (what AWS states for S3 Vectors), exact results below
|
||||
a size threshold and for rows written since the last index build, and one
|
||||
code path that serves both the open-source single engine and the Enterprise
|
||||
distributed query layer. Writes stay unchanged.
|
||||
|
||||
## Status (2026-11-04)
|
||||
|
||||
| Phase | Branch | State |
|
||||
|---|---|---|
|
||||
| 0 Build and measure | `vector-ann-phase0` | open: `vector_index` builder, `weed-vector-cli index`, `PERF_INDEX`, `PERF_CLUSTERS` |
|
||||
| 1 Probe policy | `vector-ann-phase1` | open: policy in `query::scanner`, `--nprobes`, one Lance session per engine |
|
||||
| 2 Build job | `vector-ann-phase2` | open: `lance_vector_index` in `weed-worker`, admin-driven end-to-end test, worker CI legs |
|
||||
| 3 Keep it covering | `vector-ann-phase3` | open: relative budget, retrain rule, end-to-end steps |
|
||||
| 4 Docs and gates | — | after the above merge |
|
||||
| 5 Enterprise sharded builds | — | the Enterprise repository |
|
||||
|
||||
What the measurements changed:
|
||||
|
||||
- **IVF needs structure in the data.** On independent Gaussian vectors, the
|
||||
perf suite's default, recall through an IVF index tracks the fraction of
|
||||
partitions probed: at 50k × 384 with 224 partitions, 8 probes gave 0.13 and
|
||||
64 probes 0.56, and latency did not fall with fewer probes. On vectors drawn
|
||||
around 100 shared centres, 8 probes gave recall 1.0; with eight clients
|
||||
the index served 145 queries/s against 52 exact at 50k and 70 against 11
|
||||
at 200k, and the 10% filtered query at 200k fell from 445 to 265 ms,
|
||||
while one client's latency stayed at the request path's floor on the
|
||||
shared host. Embeddings cluster; the perf suite grows `PERF_CLUSTERS`
|
||||
and the end-to-end test draws its rows around centres. The probe rule
|
||||
stands as written.
|
||||
- **The engine opened every table in a fresh Lance session**, so the index
|
||||
cache died with each request. Phase 1 gives the engine one session, sized
|
||||
by `--index-cache-mb`.
|
||||
- **Compaction keeps the index covering** (Lance 10 remaps it), so the
|
||||
compaction risk below did not materialise.
|
||||
|
||||
## Where things stand (2026-11-04)
|
||||
|
||||
What already exists, with the files that hold it:
|
||||
|
||||
- **The engine is index-aware; nothing builds an index.** `QueryVectors` in
|
||||
`crates/server/src/service.rs` always calls `weed_vector_core::query::ann`,
|
||||
which runs a Lance scan with `use_index(true)`: ANN on fragments an index
|
||||
covers, a flat pass on the rest, decided per fragment by Lance
|
||||
(`crates/core/src/query.rs`). `nprobes` is `None`. `CreateIndex` writes an
|
||||
empty Lance dataset and no vector index, so today every fragment is "tail"
|
||||
and every query is an exact scan.
|
||||
- **The maintenance worker only extends indexes that exist.** The
|
||||
`lance_optimize_indices` job in `seaweed-worker/crates/lance/src/jobs/indices.rs`
|
||||
skips a table whose `load_indices()` is empty and otherwise calls
|
||||
`optimize_indices(&OptimizeOptions::default())` once `num_unindexed_rows`
|
||||
exceeds its `max_unindexed_rows` budget (default 100,000).
|
||||
- **Coverage and segments are modelled.** `crates/core/src/coverage.rs` reports,
|
||||
per index name, the fragments covered and the uncovered rows; it requires the
|
||||
fragment bitmap each Lance `IndexMetadata` delta carries.
|
||||
`crates/core/src/segments.rs` lists physical segments (one per delta) with
|
||||
their names and bitmaps, and partitions them across workers. `weed-vector-cli
|
||||
coverage` and `weed-vector-cli query --flat` expose both.
|
||||
- **Filters are applied before ranking.** `query.rs` sets `prefilter(true)`
|
||||
unless the request asks for post-filtering, which S3 Vectors never does.
|
||||
Lance's IVF search honours the prefilter, so this is already the behaviour
|
||||
AWS calls an `ENHANCED` index.
|
||||
- **Enterprise consumes exactly this model.** In the Enterprise repository,
|
||||
`enterprise/weed/pb/vector_coord.proto` defines `SegmentInfo{id,
|
||||
resident_bytes, rows, fragment_ids}` and `SegmentList{tail_fragment_ids,
|
||||
tail_bytes}`; `VectorQuery` already carries `nprobes`; a `Task` is a set of
|
||||
segment ids plus fragment ids. `sw-vector-distributed/src/plan.rs` assigns
|
||||
a fragment covered by several segments to the lowest segment and never mixes
|
||||
index names in one scan; `resident_bytes` is the index files' size.
|
||||
`datasets.rs` warms a slot with `prewarm_index_segments` per index name and
|
||||
warms the tail by scanning it. `sw-vector-distributed/src/index.rs` holds
|
||||
`plan_index` (shard the uncovered fragments across workers),
|
||||
`build_index_task` (one delta per shard, named
|
||||
`<column>-w<worker>-v<version>`, built with `create_index_builder(..)
|
||||
.fragments(..)`) and `optimize_policy` (append deltas until `MAX_DELTAS = 8`,
|
||||
then Lance's default options, which promise no merge; see phase 3).
|
||||
Nothing calls these three yet; `tests/index.rs`,
|
||||
`tests/plan_search.rs` and `tests/engine_query.rs` exercise them with
|
||||
`ivf_flat(4)` indexes.
|
||||
- **Lance 10.0.0 offers** `VectorIndexParams::ivf_flat`, `ivf_pq`,
|
||||
`with_ivf_sq_params`, `with_ivf_rq_params`, `ivf_hnsw` and HNSW variants;
|
||||
scanner knobs `nprobes`, `minimum_nprobes`, `maximum_nprobes`, `ef`,
|
||||
`refine`; `OptimizeOptions{num_indices_to_merge, index_names, retrain}`.
|
||||
- **Measured today** (384-d, cosine, top-10, one 16-core host, exact scans):
|
||||
one engine answers in 42 ms at 50k vectors and 82 ms at 200k; filtered
|
||||
queries take 81 and 259–336 ms. Four Enterprise engines split four ways
|
||||
bring 200k to 58 ms and the filtered query to 73 ms. Recall is 1.000.
|
||||
- **Tests assume exactness only on tiny tables.** `test/s3vectors/s3vectors_test.py`
|
||||
asserts exact result order on indexes of a handful of vectors; the perf
|
||||
suite asserts recall@10 ≥ 0.9 against a client-side exact search.
|
||||
- **AWS**: "90%+ average recall for most datasets"; `CLASSIC` and `ENHANCED`
|
||||
index modes, the latter pre-filtering; no algorithm named.
|
||||
|
||||
## Design
|
||||
|
||||
### The contract every consumer relies on
|
||||
|
||||
An S3 Vectors index gains a Lance vector index on its `vector` column, and
|
||||
everything downstream keeps working because it only ever sees:
|
||||
|
||||
1. **Segments**: Lance index deltas with fragment bitmaps. Every build and
|
||||
every merge must leave `coverage_known == true`; a delta without a bitmap
|
||||
turns the whole index into "rebuild" for the Enterprise planner.
|
||||
2. **Names**: one logical index name per shard, never two shards under one
|
||||
name within a build. Scans and warm-ups group by name.
|
||||
3. **Tail**: fragments no segment covers, searched exactly. Rows written
|
||||
after a build land here, so recall never drops to zero between builds.
|
||||
4. **Bytes**: `resident_bytes` of a segment is its index files' size. The
|
||||
controller's W sizing (`W_mem`, `W_scan`) and the engine's
|
||||
`--capacity-bytes` budget use it unchanged.
|
||||
5. **Versions**: a build or merge commits a new dataset version. Routers cache
|
||||
segment lists per version, plans pin versions with the lease tags in
|
||||
`crates/core/src/maintenance.rs`, and cleanup respects those leases. No
|
||||
new mechanism is needed, but the build job must go through the same
|
||||
commit path as any Lance write.
|
||||
|
||||
### Index type and parameters
|
||||
|
||||
- **IVF_FLAT first.** Distances inside probed partitions are exact, so recall
|
||||
is governed by `nprobes` alone; the only training is k-means over the
|
||||
centroids; resident bytes stay close to the raw vectors, which keeps the
|
||||
Enterprise W sizing in the same range as today. `IVF_PQ` becomes an
|
||||
operator option later for indexes whose raw size is the constraint. HNSW
|
||||
variants are out of scope: their build cost and memory do not fit the
|
||||
maintenance-worker model.
|
||||
- **Metric from the table.** The vector field's `distance_metric` metadata
|
||||
(`COSINE`, `DOT_PRODUCT`, else L2) is the metric the index is built with,
|
||||
exactly as `service.rs` resolves it for queries.
|
||||
- **Partitions**: start at `clamp(round(sqrt(rows)), 1, 4096)` for the whole
|
||||
table, and for a shard, `sqrt` of the shard's rows. Phase 0 measures
|
||||
recall and latency against this and adjusts before it becomes a default.
|
||||
- **Probes**: one count per query, `max(8, ceil(partitions / 10))` clamped to
|
||||
the partition count, taken from the largest delta of the index. A delta
|
||||
with no more partitions than that count is searched in full; a larger one
|
||||
at that count. Lance searches at least that many partitions. Under a
|
||||
filter it may widen to four times the count until it has k rows, which a
|
||||
selective filter needs; an open upper bound made Lance prepare every
|
||||
partition for that and cost as much as the exact scan (201 ms against
|
||||
104 ms for the 10% filter at 50k × 384). Without a filter it stops at the
|
||||
count.
|
||||
|
||||
### Who builds, and when
|
||||
|
||||
The Lance maintenance worker (`weed-worker`) gets a new job type,
|
||||
`lance_vector_index`, beside `lance_optimize_indices`:
|
||||
|
||||
- **Detection**: tables in a vector bucket's `vector` namespace (ids of the
|
||||
form `<bucket>$vector$<index>`) whose vector column has no index and at
|
||||
least `min_rows` live rows (default 50,000). Below the threshold the index
|
||||
stays exact, which also keeps the existing small-table tests exact.
|
||||
- **Execution**: one `create_index` call with IVF_FLAT, the table's metric,
|
||||
the partition rule above, and a fixed name (`vector_idx`). Progress and
|
||||
counters follow the pattern of `indices.rs`; a new `indexes_built_total`
|
||||
metric and an admin observation of index coverage sit beside the row and
|
||||
fragment counts the worker already reports.
|
||||
- **Why the worker and not the engine or the gateway.** `CreateIndex` has no
|
||||
rows to index. The engine serves queries and should not run minute-long
|
||||
k-means builds. The worker already has the catalog client, credentials,
|
||||
the admin UI settings form, metrics, retries and dedupe, and plugin jobs
|
||||
start disabled until an operator turns them on, which is the rollout
|
||||
switch we want.
|
||||
- **Enterprise sharded builds** (phase 5): the same job, when Enterprise
|
||||
engines are registered, splits the uncovered fragments with `plan_index`
|
||||
and has each engine run `build_index_task` for its shard, producing one
|
||||
named delta per shard. This is what `index.rs` was written for; the worker
|
||||
drives it, the engines build, and the shared `optimize_policy` keeps each
|
||||
name's delta count under eight.
|
||||
|
||||
### Keeping it covering
|
||||
|
||||
`lance_optimize_indices` already does the work once an index exists. Two
|
||||
changes:
|
||||
|
||||
- The budget becomes relative for vector tables: reindex when uncovered rows
|
||||
exceed `min(absolute budget, 10% of indexed rows)`, so a 60k-row index
|
||||
reindexes after 6k new rows instead of waiting for 100k.
|
||||
- A retrain once the table has grown to about 4× the rows the centroids were
|
||||
trained on, so partitions do not go stale. No row count is recorded: the
|
||||
retrain is due when the partition rule for the current rows gives twice
|
||||
the partitions the index has, and it rebuilds a single IVF_FLAT name
|
||||
through the builder at that count. Past 2,048 partitions (about 4.2M
|
||||
rows) the rule cannot double under the 4,096 cap, so those centroids are
|
||||
not retrained again: that would need the trained row count recorded and a
|
||||
full rebuild of a table that size, which is left for later.
|
||||
|
||||
### Deletes and upserts
|
||||
|
||||
A delete marks rows in Lance deletion files; an upsert is a delete plus an
|
||||
append. Deleted rows are excluded through the prefilter on indexed and
|
||||
unindexed fragments alike, and appended rows are tail until the next
|
||||
optimize. Phase 1 adds a test that proves both, since this is where ANN
|
||||
systems silently return stale hits.
|
||||
|
||||
### API surface
|
||||
|
||||
No new S3 Vectors API parameter is needed to reach AWS parity. Two optional
|
||||
items for later: accept and echo AWS's `indexMode`/`queryMode` (both behave
|
||||
as `ENHANCED` here, because filters are always applied first), and a
|
||||
non-AWS per-request `nprobes` for the perf suite and the CLI, carried in the
|
||||
engine's `QueryVectorsRequest` (the Enterprise `VectorQuery` already has the
|
||||
field).
|
||||
|
||||
### Enterprise compatibility requirements
|
||||
|
||||
These hold for every phase, and the mono s3vectors legs (go/rust ×
|
||||
single/distributed) are the gate:
|
||||
|
||||
- Builds produce deltas with fragment bitmaps; merges keep them. A test in
|
||||
`tests/index.rs` asserts `coverage_known` after build, append and merge.
|
||||
- Index names never mix within a shard set; the router and `plan.rs` group by
|
||||
name, and `with_index_segments` rejects mixed names.
|
||||
- Segment count per index name stays under `MAX_DELTAS`: at the limit
|
||||
`optimize_policy(deltas)` passes `OptimizeOptions::merge(deltas)`, where
|
||||
`deltas` is the name's delta count, so every delta of that name folds
|
||||
into one with the new rows; Lance's default options may append another
|
||||
delta instead of merging. A test runs repeated append-and-optimize cycles
|
||||
and checks the actual segment counts.
|
||||
- Coverage is measured as the union over an index's names. After a sharded
|
||||
build each name misses the other shards' fragments, so a per-name count
|
||||
would extend every shard over the whole table and duplicate the index.
|
||||
An optimize extends one name only, the one with the fewest deltas; a test
|
||||
checks that two disjoint shards stay disjoint through it.
|
||||
- `resident_bytes` is the index files' size and `tail_bytes` the uncovered
|
||||
fragments' size; the controller's `max_scan_mb` and memory sizing need no
|
||||
change for IVF_FLAT, and PQ only makes slots smaller.
|
||||
- A merge replaces segment UUIDs, which is a new placement epoch. The
|
||||
existing make-before-break (target groups warm with
|
||||
`prewarm_index_segments` while serving groups keep answering) covers it;
|
||||
phase 5 measures how long a merged segment takes to warm and sets
|
||||
expectations for `warmup_timeout`.
|
||||
- Workers read index files the way they read data files, directly from the
|
||||
filers and volume servers, and the files are write-once,
|
||||
so the read cache and the warm path apply unchanged.
|
||||
- Query semantics are identical in both modes: each engine applies the same
|
||||
probe policy to the segments it owns, or the count the router passes in
|
||||
`VectorQuery.nprobes`, plus a flat pass over its tail fragments; the
|
||||
gateway merges the partial top-k lists. Each delta is probed on its own
|
||||
centroids, so the merged answer is never worse than one engine's.
|
||||
- The open-source `weed-vector` stays the writer and the fallback engine, and
|
||||
it serves ANN through the same `query::ann` the workers call.
|
||||
|
||||
## Phases
|
||||
|
||||
### Phase 0 — Measure before deciding (small)
|
||||
|
||||
- `weed-vector-cli index build --partitions N --name ..` on a local table,
|
||||
reusing the code paths the tests already use, so builds can be tried
|
||||
without the worker.
|
||||
- Perf suite: `PERF_INDEX=1` builds the index after ingest and reports exact
|
||||
versus ANN latency and recall@10 at 50k, 200k and 1M × 384, unfiltered and
|
||||
with the 10% filter, for a grid of partitions and `nprobes`.
|
||||
- Verify Lance behaviour we depend on: deletions excluded under ANN;
|
||||
compaction (`lance_compact`) keeps bitmaps and coverage; `optimize_indices`
|
||||
with `append` versus merge versus `retrain`.
|
||||
- Exit: chosen defaults for `min_rows`, partitions and probes with numbers
|
||||
behind them, and a written answer to each verification item.
|
||||
|
||||
### Phase 1 — Engine query path (small)
|
||||
|
||||
- Probe policy when an index exists: `minimum_nprobes` is the per-query
|
||||
count from the Probes rule above, taken from the largest delta;
|
||||
`maximum_nprobes` is four times it, which Lance uses only under a
|
||||
prefilter.
|
||||
- `nprobes` in `QueryVectorsRequest` for tests and the CLI; zero means the
|
||||
policy.
|
||||
- Per-query log line and counters: segments searched, tail rows scanned,
|
||||
index hit or not. Without these, recall regressions are invisible.
|
||||
- Tests in `crates/core`: ANN versus flat recall on a 50k synthetic table;
|
||||
delete and upsert correctness under ANN; filtered query with a selective
|
||||
filter still returns k results.
|
||||
|
||||
### Phase 2 — Build job in the maintenance worker (medium)
|
||||
|
||||
- `lance_vector_index` job: detection, execution, settings form (`min_rows`,
|
||||
index type, partition rule), metrics, admin observation of coverage.
|
||||
- Unit tests after the pattern of the other jobs; an integration test in
|
||||
`test/s3vectors` that ingests above the threshold, runs the worker, and
|
||||
asserts coverage through the CLI and recall ≥ 0.9 through the API.
|
||||
- Exit: a fresh cluster with the job enabled indexes a 100k-vector table on
|
||||
its own, and the small-table tests stay exact.
|
||||
|
||||
### Phase 3 — Keep it covering (small)
|
||||
|
||||
- Relative budget and `retrain` policy in `lance_optimize_indices` for vector
|
||||
tables. The retrain rebuilds through the builder with partitions for the
|
||||
current row count; Lance's `OptimizeOptions::retrain()` does not size them.
|
||||
- Union-based coverage over an index's names, extension scoped to one name,
|
||||
and `optimize_policy` with an explicit merge of all deltas at
|
||||
`MAX_DELTAS`, shared with the Enterprise builds in the worker crate.
|
||||
- Tests: append and optimize nine times and check the delta counts climb to
|
||||
the limit and fold; two names on disjoint fragments stay disjoint through
|
||||
an optimize; the end-to-end test runs the job after a tenth of the index
|
||||
is appended and again after the table has grown fivefold.
|
||||
|
||||
### Phase 4 — Gateway, docs and gates (small)
|
||||
|
||||
- Optional `indexMode`/`queryMode` acceptance; `GetIndex` echoes the mode.
|
||||
- Wiki and site docs: "Queries are exact" becomes "exact below the threshold,
|
||||
ANN above it, with the tail exact", with the recall and latency measured
|
||||
in phase 0. The performance page gets an ANN column.
|
||||
- CI: a `test/s3vectors` leg with the worker and a low threshold, gated on
|
||||
recall.
|
||||
|
||||
### Phase 5 — Enterprise distributed builds and placement (medium)
|
||||
|
||||
- Drive `plan_index` / `build_index_task` from the worker job when engines
|
||||
are registered; replace the Enterprise copy of `optimize_policy` with the
|
||||
shared one, which merges at the limit.
|
||||
- Measure four engines on 1M × 384 with ANN: latency, warm time of a merged
|
||||
segment, placement epochs per merge.
|
||||
- mono s3vectors legs with the index enabled, go and rust volume servers,
|
||||
single and distributed, plus the hardened TLS legs.
|
||||
|
||||
## Risks
|
||||
|
||||
- **Filtered recall.** AWS's weak spot is selective filters. Pre-filtering
|
||||
keeps recall but costs probes; `minimum_nprobes`/`maximum_nprobes` is the
|
||||
lever, and phase 0 has to show the latency is acceptable at 1M rows.
|
||||
- **Memory.** IVF_FLAT resident bytes are about the raw vectors: 1M × 384 ×
|
||||
4 bytes is 1.5 GB per copy. Enterprise spreads it by `capacity_bytes`; the
|
||||
single open-source engine needs that much RAM or PQ.
|
||||
- **Compaction and coverage.** Settled in phase 0: Lance 10 keeps the
|
||||
bitmaps through `compact_files`, and coverage stays complete.
|
||||
- **Build cost on the worker.** k-means over 1M × 384 is CPU-bound; execution
|
||||
concurrency stays at one and the job reports progress so the admin UI shows
|
||||
it working rather than hung.
|
||||
- **Placement churn after merges.** Every merge is a new epoch; too frequent
|
||||
merging makes engines re-warm constantly. `MAX_DELTAS = 8` bounds it; the
|
||||
phase 5 measurement decides whether to raise it.
|
||||
|
||||
## Out of scope
|
||||
|
||||
HNSW indexes, GPU builds, per-request recall parameters in the AWS API
|
||||
surface, and building an index at `CreateIndex` time.
|
||||
Reference in new issue
Block a user