OriginChainDB docs
examples · vector · 11 / 13

11. HNSW health, and rebuilding a damaged index

← Vector examples

what this does

GET /v1/tenants/:t/vector/:table/hnsw-health reports whether a table's HNSW graph can actually be walked: how many live vectors a search can reach, how many disconnected components the graph has, how many nodes nothing points at, and a one-line verdict.

POST /v1/tenants/:t/vector/:table/rebuild-hnsw rebuilds the graph from the vectors already in the store. No embedding is rewritten and nothing is re-ingested.

when to use it

  • Recall is worse than it should be and widening the search beam does not help - that pattern is a graph problem, not a tuning problem.
  • You inherited a table and do not know which distance metric its index was built for.
  • Before a fleet-wide reindex, to find which tables actually need one instead of rebuilding everything.

the request

GET /v1/tenants/:t/vector/:table/hnsw-health
curl -G "https://$OC_HOST/v1/tenants/$OC_TENANT/vector/shop.products/hnsw-health" \
  -H "Authorization: Bearer $OC_TOKEN" \
  --data-urlencode "self_recall=512"

what you get back

{
  "table":                "shop.products",
  "present":              true,
  "nodes":                41892,
  "live":                 41892,
  "tombstoned":           0,
  "reachable_live":       41892,
  "reachable_fraction":   1.0,
  "components":           1,
  "zero_indegree_live":   0,
  "indegree_percentiles": [1, 4, 9, 16, 27, 48, 91],  // min p1 p10 p50 p90 p99 max
  "mean_outdegree":       15.8,
  "shattered":            false,
  "damage":               [],
  "build_metric":         "cosine",
  "stored_vectors":       41892,
  "unindexed_records":    0,
  "verdict":              "healthy: 41892 of 41892 live vectors retrievable, ...",
  "self_recall": {
    "metric":      "cosine",
    "applicable":  true,
    "probes":      512,
    "hits":        512,
    "self_recall": 1.0,
    "misses":      [],
    "verdict":     "healthy: all 512 probed vectors retrieve themselves ..."
  }
  /* plus per-layer shape, norm deciles and advisories */
}

self_recall is present only when you ask for it. Everything else comes back on every call.

query parameters

FieldRequiredNotes
self_recallnoRun the self-retrieval probe over this many live vectors as well as the topology report. Absent or 0 skips it; the cap is 4096 because each sample is a real query inside this one request.
metricnoMetric to probe under. Defaults to the recorded build metric, then the schema's declared distance, then cosine.
dimnoVector width. Only needed when the probe runs and the table has no registered vector dimension.

how it works

The topology half walks the graph and counts. It catches the failure it was built for - a graph that reaches a small fraction of its own nodes, where no amount of beam widening recovers the rest - and it reports shattered: true with the reasons in damage.

The topology half is structurally blind to one thing: an index built for one distance metric and queried under another. That graph is perfectly connected and reports every reachability field as healthy. ?self_recall=N asks the other question, and needs no ground truth to do it - under cosine, L2 or Manhattan a vector is its own exact nearest neighbour, so any miss is a defect. Under dot product that is not true, and the probe says so with "applicable": false rather than returning a number that would read as a failure.

That makes the probe the tool for an unstamped index. Run it once per candidate metric; the one the graph was really built under is the one that comes back at 1.0. Then pass that metric to the rebuild, which records it beside the graph so nothing has to guess again.

rebuilding the graph

POST /v1/tenants/:t/vector/:table/rebuild-hnsw
curl -X POST "https://$OC_HOST/v1/tenants/$OC_TENANT/vector/shop.products/rebuild-hnsw" \
  -H "Authorization: Bearer $OC_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "metric":  "cosine",
    "dry_run": true
  }'

what the rebuild returns

{
  "tenant":                "",
  "table":                 "shop.products",
  "rebuilt":               false,           // false on a dry run
  "metric":                "cosine",
  "build_metric_before":   null,            // null on an index built before the stamp
  "dim":                   768,
  "vectors":               41892,
  "estimated_peak_bytes":  137070624,
  "estimated_build_secs":  55,
  "health_before":         { /* the same report as GET hnsw-health */ },
  "health_after":          null,            // null on a dry run
  "elapsed_ms":            412
}

Drop dry_run to run it for real: rebuilt becomes true and health_after carries the post-rebuild report, so one response shows you the before and the after.

rebuild fields

FieldRequiredNotes
metricconditionalRequired unless the table has a registered vector schema or a recorded build metric. The engine refuses rather than defaulting - a rebuild under the wrong metric produces a well-connected index for the wrong distance, which no health check can detect.
dry_runnoReport the plan - current health, vector count, memory estimate, time estimate - and build nothing. Defaults to false.
forcenoRebuild even when the diagnostic reads healthy. Defaults to false so a fleet sweep cannot spend hours reindexing tables that are fine.
max_bytesnoOverride the per-request memory budget for the build. The default ceiling is 2 GiB; a rebuild whose estimate exceeds it is refused with the estimate in the message.

common mistakes

  • Rebuilding a table under write load. The build holds no lock, but the final swap refuses with 409 if any vector was written or deleted while it ran - overwriting the graph would leave the new embedding on disk and unreachable by every query. Nothing is written on that refusal, so a retry is free; a continuously written table will not converge until you pause writes to it.
  • Rebuilding a healthy index. That is a 409, on purpose. Add "force": true if you mean it.
  • Guessing the metric. If the table has no schema and no recorded build metric, the rebuild refuses instead of assuming cosine. Use ?self_recall= to find out which metric the graph matches, then state it.
  • Reading self_recall under dot product. Self-retrieval is not a valid signal there, and the probe reports "applicable": false with the numeric fields zeroed.
  • Calling it on a table with no HNSW index. The rebuild answers 404 - there is no graph to rebuild. An IVF or IVF-PQ table is the usual reason.