Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Search Reasoning (Recall Diagnostics)

Search Reasoning is a per-query diagnostic mechanism that explains why each expected label was or was not recalled. Enable it by populating SearchRequest::expected_labels_ and calling SearchWithRequest; the result dataset carries a JSON report accessible through Dataset::GetReasoning().

Quick Start

For a built SINDI_V2 index and a single-query sparse query_dataset:

// Populate expected_labels_ with the labels of interest
vsag::SearchRequest request;
request.expected_labels_ = {100, 200, 300};
request.query_ = query_dataset;
request.topk_ = 10;
request.params_str_ = R"({"sindi_v2": {"n_candidate": 100}})";

auto results = index->SearchWithRequest(request);
if (!results.has_value()) {
    std::cerr << "SearchWithRequest failed: " << results.error().message << std::endl;
} else {
    std::cout << results.value()->GetReasoning() << std::endl;
}

Report Schema (v1)

A full report is a JSON object with expected_analysis and meta sections. Status-only reports produced by MakeStatusReport contain only meta, with exactly schema_version, status, and index_type; they do not include expected_analysis or the remaining metadata fields. Consumers must check meta.status and treat those additional fields as optional rather than requiring a full report.

For example, a status-only report for an empty IVF index is:

{"meta":{"schema_version":1,"status":"empty_index","index_type":"IVF"}}

expected_analysis

FieldTypeDescription
summarystring“M/N expected labels found, P missed”
missed_targetsarrayOne entry per missed label

Each missed_targets entry:

FieldTypeDescription
labelintOriginal label
inner_idintInternal ID
diagnosisstringOne of the diagnoses below
true_distancefloatGround-truth distance
quantized_distancefloatApproximate distance seen during search
was_visitedboolWhether the id was visited
visited_at_hopintHop when first visited (-1 if never)
was_evictedboolWhether it was evicted from the candidate set
filter_rejectedboolWhether the filter rejected it
reorder_evictedboolWhether reorder evicted it

meta

FieldTypeDescription
schema_versionintAlways 1
statusstringCurrently emitted: ok, empty_index. Reserved, not currently emitted: skipped_range_search, unsupported_by_index.
index_typestringHGraph, IVF, SINDI, etc.

The following metadata fields belong to full reports and are absent from MakeStatusReport output; consumers must tolerate their absence:

FieldTypeDescription
search_modestringknn or range
topkintRequested top k
use_reorderboolWhether reorder was active
filter_activeboolWhether a filter was active
termination_reasonstringnone, lower_bound_reached, hops_limit_reached, timeout
total_hopsintTotal search hops
total_distance_computationsintTotal distance evaluations
available_diagnoses[string]All diagnoses the engine can emit
available_events[string]Events this index supported for this search
supports_rangeboolWhether this index supports range-reasoning

Diagnoses

ValueMeaning
successLabel was in the result set
not_reachableExpected target was never visited
filter_rejectedFilter blocked the entry
quantization_errorApproximate distance was too far from true distance
ef_too_smallVisited but evicted (try larger ef)
reorder_evictedReorder evicted the entry
unknownNo clear reason determined

For missed targets, rules use first-match priority: not visited, filter rejected, quantization error, candidate eviction, reorder eviction, then unknown. In particular, quantization_error takes priority over ef_too_small when both conditions hold. The quantization check is the legacy heuristic approximate > 1.5 * true, applied only when the true distance is positive. It is not a metric-independent error bound or proof that quantization caused the miss; signed IP distances need particular care.

When present in a full report, supports_range is a boolean describing registered range-reasoning support, not availability of the index’s RangeSearch API. Unregistered index types report false; absence from a status-only report does not mean false.

For range requests, HGraph ignores expected_labels_ for reasoning and leaves GetReasoning() as {}. Pyramid instead rejects range requests with non-empty expected_labels_ with an invalid-argument error. Neither behavior produces a skipped_range_search or unsupported_by_index status report; those statuses are reserved and not currently emitted.

Per-Index Event Support

IndexKNNRangeVisitEvictionFilter RejectReorderReorder EvictionBucket Selection
HGraph✓—✓✓✓✓✓—
IVF✓✓✓✓✓✓✓✓
SINDI✓✓✓—✓✓✓✓
SINDI_V2✓✓✓✓✓✓✓✓
BruteForce✓✓✓—✓———
WARP✓✓✓—✓———
Pyramid✓—✓✓✓✓✓—

Advanced

To extend the framework:

  1. Add a new ReasoningDiagnosis value in reasoning_types.h, map it in ToString, add a diagnosis rule in reasoning_context.cpp, add a test, and document it.
  2. Add a new ReasoningEvent value in reasoning_types.h, implement a RecordXxx method on ReasoningContext, tag the instrumentation site with // [reasoning], update the capability matrix, and document it.
  3. For a new index: add a capability entry in reasoning_capability.cpp, implement SearchWithRequest, and insert recording hooks.