OriginChainDB
Vector

Check and repair the search graph

← Vector examples

The language tabs use earlier client interfaces. Check SDK versions and connection setup before running them.

HNSW links nearby vectors in a graph. Health checks show whether the graph can reach the stored entries.

Before you run it: Concurrent writes can conflict with rebuilding. Memory limits and supported diagnostic metrics are listed below.

Use a table with a populated HNSW index, such as the single-vector setup expanded with representative data. HNSW is an index that links nearby vectors. A component is a disconnected group; an unreachable entry cannot be reached from the index's starting point. Start with the health request, not the rebuild.

Inspect the search graph before rebuildingA rebuild needs the correct metric, sufficient memory and a plan for concurrent writes.
  1. Run the health checkInspect disconnected components and unreachable vectors.
  2. Review a dry runCheck the proposed metric, memory and rebuild plan.
  3. Rebuild when justifiedPause conflicting writes and follow the documented limits.
  4. Compare health afterwardsInspect the returned before-and-after reports.

Follow the example

  1. Run the read-only health request.
  2. Inspect unreachable entries and components.
  3. Try a dry run before rebuilding.
  4. Plan memory use and pause writes if rebuilding.

Run the request

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

Check the result

A rebuild reconstructs the index from stored vectors. The health report also offers optional recall checks.

{
  "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 options

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.

Understand the behavior

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.

Rebuild the index

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
  }'

Check the rebuild result

{
  "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 options

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.

Important limits and checks

  • 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.

Continue

Continue to remove vectors from an index.

© 2026 Silicoyn Technologies Pvt Ltd

On this page