yams/tests/unit/search/search_engine_topology_catch2_test.cpp
Trevon 6ea11353ec
perf(search): build the topology route index off the query path (#346)
TopologyRoutingSnapshotCache built its route index lazily, under its
lock, on the first routed lookup. The first routed query after daemon
start, and after every topology rebuild (a new epoch misses the cache),
built it inline: 2.9-3.0 s on SciFact, 7.0 s on NFCorpus, while
concurrent queries waited on the lock.

- getRoutingSnapshot(cache, epoch, options): the lookup a routed query
  makes, shared by queries and warm-ups so a warmed entry is the one
  the next query reads.
- SearchEngine::prewarmTopologyRouting(): load the latest snapshot
  with the options a routed query uses. False when routing is
  disabled or nothing is published.
- SearchEngineManager posts a warm-up after an engine build, as a
  separate task so readiness does not wait for it.
- ServiceManager warms after an async topology rebuild stores a batch,
  on the rebuild task.

Retrieval A/B on grim (gcc Profiling, search_product_clean_baseline,
3 timed reps + 1 profiled pass per arm; base is #344):

                         SciFact            NFCorpus
  first query, profiled  3251 -> 28.9 ms    7481 -> 33.6 ms
  max query, profiled    3251 -> 91.2 ms    7481 -> 90.0 ms
  buildIndex             query thread ->    query thread ->
                         background         background
  cold first, timed      84.3 -> 69.3 ms    51.0 -> 36.7 ms
  p95, timed             55.3 -> 53.3 ms    56.7 -> 52.3 ms
  MAP / MRR / nDCG       identical          within base run-to-run
                                            range (base MAP stdev
                                            0.0020, change 0.0004)
2026-10-25 14:58:26 -06:00

2396 lines
112 KiB
C++

#include <nlohmann/json.hpp>
#include <catch2/catch_approx.hpp>
#include <catch2/catch_test_macros.hpp>
#include <algorithm>
#include <atomic>
#include <chrono>
#include <cstdlib>
#include <filesystem>
#include <memory>
#include <optional>
#include <span>
#include <string>
#include <tuple>
#include <unordered_map>
#include <unordered_set>
#include <vector>
#include <yams/metadata/connection_pool.h>
#include <yams/metadata/knowledge_graph_store.h>
#include <yams/metadata/metadata_repository.h>
#include <yams/search/search_engine.h>
#include <yams/search/search_execution_context.h>
#include <yams/search/search_topology_stage.h>
#include <yams/search/topology_routing_session.h>
#include <yams/topology/topology_baseline.h>
#include <yams/topology/topology_metadata_store.h>
#include <yams/vector/embedding_generator.h>
#include <yams/vector/vector_database.h>
#include "src/search/search_vector_pipeline_internal.h"
#include "tests/common/test_helpers_catch2.h"
#include "tests/unit/search/topology_search_test_support.h"
using namespace yams;
using namespace yams::metadata;
using namespace yams::search;
using namespace yams::search::topology_test_support;
// Catch2 assertion macros expand through do/while and large generated test bodies.
// NOLINTBEGIN(cppcoreguidelines-avoid-do-while, readability-function-cognitive-complexity)
namespace {
class HashLookupCountingRepository : public MetadataRepository {
public:
explicit HashLookupCountingRepository(ConnectionPool& pool) : MetadataRepository(pool) {}
Result<std::optional<DocumentInfo>> getDocumentByHash(const std::string& hash) override {
++singleLookups;
return MetadataRepository::getDocumentByHash(hash);
}
Result<std::unordered_map<std::string, DocumentInfo>>
batchGetDocumentsByHash(const std::vector<std::string>& hashes) override {
const auto call = ++batchLookups;
if (failBatchLookups || (failBatchAt > 0 && call == failBatchAt)) {
return Error{ErrorCode::InternalError, "injected metadata lookup failure"};
}
return MetadataRepository::batchGetDocumentsByHash(hashes);
}
std::atomic<int> singleLookups{0};
std::atomic<int> batchLookups{0};
bool failBatchLookups{false};
int failBatchAt{0};
};
yams::topology::TopologyArtifactBatch buildMultiscaleOverlapTopologyBatch() {
auto batch = buildTwoClusterTopologyBatch();
auto fineIt = std::ranges::find_if(batch.clusters, [](const auto& cluster) {
return std::ranges::find(cluster.memberDocumentHashes, "y1") !=
cluster.memberDocumentHashes.end();
});
REQUIRE(fineIt != batch.clusters.end());
yams::topology::ClusterArtifact coarse = *fineIt;
coarse.clusterId += ":coarse";
coarse.level = 1;
coarse.persistenceScore = 0.70;
coarse.cohesionScore = 0.80;
coarse.bridgeMass = 0.30;
coarse.densityScore = 0.60;
coarse.parentClusterId.reset();
coarse.overlapClusterIds = {fineIt->clusterId};
if (coarse.medoid.has_value()) {
coarse.medoid->clusterId = coarse.clusterId;
}
fineIt->parentClusterId = coarse.clusterId;
fineIt->overlapClusterIds = {coarse.clusterId};
for (auto& membership : batch.memberships) {
if (membership.documentHash != "y1" && membership.documentHash != "y2") {
continue;
}
membership.parentClusterId = coarse.clusterId;
membership.overlapClusterIds = {coarse.clusterId};
membership.bridgeScore = membership.documentHash == "y1" ? 0.60 : 0.20;
}
batch.clusters.push_back(std::move(coarse));
return batch;
}
void seedTopologyWithStaleClusterMember(TopologySearchFixture& fix) {
auto topologyBatch = buildTwoClusterTopologyBatch();
REQUIRE(!topologyBatch.clusters.empty());
topologyBatch.clusters.front().memberDocumentHashes.push_back("missing-hash");
topologyBatch.clusters.front().memberCount =
topologyBatch.clusters.front().memberDocumentHashes.size();
yams::topology::MetadataKgTopologyArtifactStore topologyStore(fix.repo, fix.kgStore);
REQUIRE(topologyStore.storeBatch(topologyBatch).has_value());
}
} // namespace
TEST_CASE("SearchEngine topology refuses uncertified weak-query narrowing",
"[search][topology][admission][catch2]") {
TopologySearchFixture fix{vector::VectorSearchEngine::Vec0L2};
fix.addDocument("x1", "alpha one", {1.0F, 0.0F});
fix.addDocument("x2", "alpha two", {0.9F, 0.1F});
fix.addDocument("y1", "omega one", {0.0F, 1.0F});
fix.addDocument("y2", "omega two", {0.1F, 0.9F});
yams::topology::ConnectedComponentTopologyEngine topologyEngine;
yams::topology::TopologyBuildConfig topologyConfig;
topologyConfig.reciprocalOnly = true;
topologyConfig.minEdgeScore = 0.5;
std::vector<yams::topology::TopologyDocumentInput> topologyDocs{
{.documentHash = "x1",
.filePath = "/tmp/x1.txt",
.embedding = {1.0F, 0.0F},
.neighbors = {{.documentHash = "x2", .score = 0.9F, .reciprocal = true}}},
{.documentHash = "x2",
.filePath = "/tmp/x2.txt",
.embedding = {0.9F, 0.1F},
.neighbors = {{.documentHash = "x1", .score = 0.9F, .reciprocal = true}}},
{.documentHash = "y1",
.filePath = "/tmp/y1.txt",
.embedding = {0.0F, 1.0F},
.neighbors = {{.documentHash = "y2", .score = 0.9F, .reciprocal = true}}},
{.documentHash = "y2",
.filePath = "/tmp/y2.txt",
.embedding = {0.1F, 0.9F},
.neighbors = {{.documentHash = "y1", .score = 0.9F, .reciprocal = true}}},
};
auto topologyBatch = topologyEngine.buildArtifacts(topologyDocs, topologyConfig);
REQUIRE(topologyBatch.has_value());
yams::topology::MetadataKgTopologyArtifactStore topologyStore(fix.repo, fix.kgStore);
REQUIRE(topologyStore.storeBatch(topologyBatch.value()).has_value());
vector::EmbeddingConfig embeddingConfig;
embeddingConfig.embedding_dim = 2;
auto generator = std::make_shared<vector::EmbeddingGenerator>(
std::make_unique<FixedEmbeddingBackend>(std::vector<float>{0.0F, 1.0F}), embeddingConfig);
REQUIRE(generator->initialize());
SearchEngineConfig config;
config.includeDebugInfo = true;
config.enableParallelExecution = true;
config.textWeight = 1.0F;
config.simeonTextWeight = 0.0F;
config.pathTreeWeight = 0.0F;
config.kgWeight = 0.0F;
config.vectorWeight = 1.0F;
config.entityVectorWeight = 0.0F;
config.tagWeight = 0.0F;
config.metadataWeight = 0.0F;
config.vectorOnlyThreshold = 0.0F;
config.vectorOnlyPenalty = 1.0F;
config.vectorMaxResults = 4;
config.topologyVectorPolicy = SearchEngineConfig::TopologyVectorPolicy::Narrow;
config.topologyMaxClusters = 1;
config.topologyMaxDocs = 2;
SearchExecutionContext context = defaultSearchExecutionContext();
context.freshness.lexicalReady = true;
context.freshness.vectorReady = true;
context.freshness.kgReady = true;
context.freshness.topologyReady = true;
SearchExecutionContextGuard contextGuard(context);
SearchEngine engine(fix.repo, fix.vectorDb, generator, fix.kgStore, config);
SearchParams params;
params.limit = 4;
auto response = engine.searchWithResponse("unmatched query", params);
REQUIRE(response.has_value());
const auto& debug = response.value().debugStats;
CHECK((debug.at("search_pipeline_name") == "topology_evidence"));
REQUIRE(debug.contains("topology_weak_query_applied"));
CHECK((debug.at("topology_weak_query_load_attempted") == "1"));
CHECK((debug.at("topology_weak_query_load_succeeded") == "1"));
CHECK(debug.at("topology_weak_query_skip_reason").empty());
CHECK((debug.at("topology_weak_query_applied") == "1"));
CHECK((debug.at("topology_route_narrow_proposed") == "1"));
CHECK((debug.at("topology_route_admission_eligible") == "0"));
CHECK((debug.at("topology_route_admission_denial_reason") ==
"protected_fibers_representation_unavailable"));
CHECK((debug.at("topology_weak_query_narrow_applied") == "0"));
CHECK((debug.at("topology_weak_query_allowed_candidates") == "2"));
CHECK((debug.at("topology_vector_filter_applied") == "0"));
CHECK((debug.at("topology_vector_filter_fallback") == "0"));
CHECK((debug.at("topology_vector_allowed_set_ann_applied") == "0"));
CHECK((debug.at("topology_vector_allowed_set_ann_fallback") == "0"));
CHECK_FALSE(debug.contains("topology_vector_partition_ann_applied"));
CHECK((debug.at("topology_vector_filter_matched") == "0"));
CHECK((debug.at("topology_vector_filter_removed") == "0"));
CHECK_FALSE(debug.contains("topology_vector_scores_reused"));
CHECK_FALSE(debug.contains("topology_vector_scores_reused_count"));
CHECK_FALSE(debug.contains("topology_member_rerank_rows_visited_actual"));
CHECK_FALSE(debug.contains("topology_member_rerank_distance_evaluations_actual"));
CHECK(debug.at("vector_search_rows_visited_status") == "unavailable");
CHECK(debug.at("vector_search_exact_distance_evaluations_status") == "unavailable");
CHECK(debug.at("vector_search_ann_candidate_budget_status") == "unavailable");
CHECK_FALSE(debug.contains("vector_search_rows_visited_actual"));
CHECK_FALSE(debug.contains("vector_search_exact_distance_evaluations_actual"));
CHECK_FALSE(debug.contains("vector_search_ann_candidate_budget_actual"));
CHECK((debug.at("topology_weak_query_routed_clusters") == "1"));
CHECK((debug.at("topology_weak_query_added_candidates") == "0"));
CHECK((debug.at("topology_weak_query_total_candidates") == "0"));
CHECK((debug.at("topology_snapshot_cache_hit") == "0"));
auto cachedResponse = engine.searchWithResponse("unmatched query", params);
REQUIRE(cachedResponse.has_value());
CHECK((cachedResponse.value().debugStats.at("topology_snapshot_cache_hit") == "1"));
}
TEST_CASE("SearchEngine keeps Simeon PQ global while route admission is unavailable",
"[search][topology][spq][admission][catch2]") {
TopologySearchFixture fix{vector::VectorSearchEngine::SimeonPqAdc};
seedTopologyDocuments(fix);
seedTwoClusterTopology(fix);
REQUIRE(fix.vectorDb->buildIndex());
auto generator = makeFixedGenerator({0.1F, 0.9F});
auto config = topologyRoutingTestConfig(true);
config.topologyRoutingMode = SearchEngineConfig::TopologyRoutingMode::HybridAssist;
config.topologyNarrowMinBoundaryMargin = 0.0F;
config.topologyMaxDocs = 1;
auto response = runTopologySearch(fix, generator, config, 4);
REQUIRE(response.has_value());
const auto& debug = response.value().debugStats;
CHECK(debug.at("topology_weak_query_allowed_candidates") == "2");
CHECK(debug.at("topology_route_narrow_proposed") == "1");
CHECK(debug.at("topology_route_admission_eligible") == "0");
CHECK(debug.at("topology_route_admission_denial_reason") ==
"protected_fibers_representation_unavailable");
CHECK(debug.at("topology_vector_allowed_set_ann_applied") == "0");
CHECK(debug.at("topology_vector_allowed_set_ann_fallback") == "0");
CHECK(debug.at("topology_weak_query_narrow_applied") == "0");
CHECK(debug.at("vector_search_rows_visited_status") == "observed");
CHECK(debug.at("vector_search_exact_distance_evaluations_status") == "observed");
CHECK(debug.at("vector_search_ann_candidate_budget_status") == "observed");
CHECK(debug.at("vector_search_ann_candidate_budget_actual") == "4");
CHECK(debug.at("vector_search_rows_visited_actual") == "4");
CHECK(debug.at("vector_search_exact_distance_evaluations_actual") == "4");
REQUIRE_FALSE(response.value().results.empty());
}
TEST_CASE("SearchEngine commits hard narrowing when every theorem certificate passes",
"[search][topology][admission][work][catch2]") {
TopologySearchFixture fix{vector::VectorSearchEngine::SimeonPqAdc};
seedTopologyDocuments(fix);
auto batch = buildTwoClusterTopologyBatch();
yams::topology::MetadataKgTopologyArtifactStore topologyStore(fix.repo, fix.kgStore);
REQUIRE(topologyStore.storeBatch(batch).has_value());
auto generator = makeFixedGenerator({0.0F, 1.0F});
auto config = topologyRoutingTestConfig(true);
configureCertifiedTopologyRoute(config, batch);
auto response = runTopologySearch(fix, generator, config, 4, "omega");
REQUIRE(response.has_value());
const auto& debug = response.value().debugStats;
CHECK(debug.at("topology_route_coordinate_space_identity") == "test-space-v1");
CHECK(debug.at("topology_route_coordinate_space_alignment_status") == "satisfied");
CHECK(debug.at("topology_route_protected_relation_coverage_status") == "satisfied");
CHECK(debug.at("topology_route_protected_fibers_represented_status") == "satisfied");
CHECK(debug.at("topology_route_certificate_saturates_protected_fibers_status") == "satisfied");
CHECK(debug.at("topology_route_risk_status") == "satisfied");
CHECK(debug.at("topology_route_work_status") == "satisfied");
CHECK(debug.at("topology_route_cover_materialization_status") == "satisfied");
CHECK(debug.at("topology_route_admission_eligible") == "1");
CHECK(debug.at("topology_weak_query_narrow_applied") == "1");
CHECK(debug.at("topology_vector_allowed_set_ann_applied") == "1");
CHECK(debug.at("topology_vector_global_fill_count") == "0");
REQUIRE(response.value().results.size() == 2U);
CHECK(std::ranges::all_of(response.value().results, [](const auto& result) {
return result.document.sha256Hash == "y1" || result.document.sha256Hash == "y2";
}));
}
TEST_CASE("SearchEngine discards a trial route whose observed vector work exceeds its budget",
"[search][topology][admission][work][fallback][catch2]") {
TopologySearchFixture fix{vector::VectorSearchEngine::SimeonPqAdc};
seedTopologyDocuments(fix);
auto batch = buildTwoClusterTopologyBatch();
yams::topology::MetadataKgTopologyArtifactStore topologyStore(fix.repo, fix.kgStore);
REQUIRE(topologyStore.storeBatch(batch).has_value());
auto generator = makeFixedGenerator({0.0F, 1.0F});
auto config = topologyRoutingTestConfig(true);
configureCertifiedTopologyRoute(config, batch, 1);
auto response = runTopologySearch(fix, generator, config, 4, "omega");
REQUIRE(response.has_value());
const auto& debug = response.value().debugStats;
CHECK(debug.at("topology_route_work_status") == "violated");
CHECK(debug.at("topology_route_admission_eligible") == "0");
CHECK(debug.at("topology_route_admission_denial_reason") == "work_budget_violated");
CHECK(debug.at("topology_weak_query_narrow_applied") == "0");
CHECK(debug.at("topology_vector_filter_fallback") == "1");
CHECK(response.value().results.size() == 4U);
}
TEST_CASE("SearchEngine rejects a relation cover that cuts an observed protected pair",
"[search][topology][admission][pairs][fallback][catch2]") {
TopologySearchFixture fix{vector::VectorSearchEngine::SimeonPqAdc};
seedTopologyDocuments(fix);
auto batch = buildTwoClusterTopologyBatch();
auto selectedChart = std::ranges::find_if(batch.clusters, [](const auto& cluster) {
return std::ranges::find(cluster.memberDocumentHashes, "y1") !=
cluster.memberDocumentHashes.end();
});
REQUIRE(selectedChart != batch.clusters.end());
REQUIRE(selectedChart->protectedPairCount > 0);
selectedChart->preservedProtectedPairCount = 0;
yams::topology::MetadataKgTopologyArtifactStore topologyStore(fix.repo, fix.kgStore);
REQUIRE(topologyStore.storeBatch(batch).has_value());
auto generator = makeFixedGenerator({0.0F, 1.0F});
auto config = topologyRoutingTestConfig(true);
configureCertifiedTopologyRoute(config, batch);
auto response = runTopologySearch(fix, generator, config, 4, "omega");
REQUIRE(response.has_value());
const auto& debug = response.value().debugStats;
CHECK(debug.at("topology_route_protected_relation_coverage_status") == "violated");
CHECK(debug.at("topology_route_admission_denial_reason") ==
"protected_relation_coverage_violated");
CHECK(debug.at("topology_route_work_status") == "unavailable");
CHECK(debug.at("topology_weak_query_narrow_applied") == "0");
CHECK(response.value().results.size() == 4U);
}
TEST_CASE("SearchEngine rejects route calibration from a different topology construction",
"[search][topology][admission][risk][fallback][catch2]") {
TopologySearchFixture fix{vector::VectorSearchEngine::SimeonPqAdc};
seedTopologyDocuments(fix);
auto batch = buildTwoClusterTopologyBatch();
yams::topology::MetadataKgTopologyArtifactStore topologyStore(fix.repo, fix.kgStore);
REQUIRE(topologyStore.storeBatch(batch).has_value());
auto generator = makeFixedGenerator({0.0F, 1.0F});
auto config = topologyRoutingTestConfig(true);
configureCertifiedTopologyRoute(config, batch);
config.topologyRouteRiskCalibration.constructionFingerprint = "different-construction";
auto response = runTopologySearch(fix, generator, config, 4, "omega");
REQUIRE(response.has_value());
const auto& debug = response.value().debugStats;
CHECK(debug.at("topology_route_protected_relation_coverage_status") == "satisfied");
CHECK(debug.at("topology_route_risk_status") == "unavailable");
CHECK(debug.at("topology_route_admission_denial_reason") ==
"protected_fibers_representation_unavailable");
CHECK(debug.at("topology_weak_query_narrow_applied") == "0");
CHECK(response.value().results.size() == 4U);
}
TEST_CASE("SearchEngine does not infer protected fiber representation from bounded miss risk",
"[search][topology][admission][fiber][fallback][catch2]") {
TopologySearchFixture fix{vector::VectorSearchEngine::SimeonPqAdc};
seedTopologyDocuments(fix);
auto batch = buildTwoClusterTopologyBatch();
yams::topology::MetadataKgTopologyArtifactStore topologyStore(fix.repo, fix.kgStore);
REQUIRE(topologyStore.storeBatch(batch).has_value());
auto generator = makeFixedGenerator({0.0F, 1.0F});
auto config = topologyRoutingTestConfig(true);
configureCertifiedTopologyRoute(config, batch);
config.topologyRouteRiskCalibration.missedProtectedCandidates = 1;
auto response = runTopologySearch(fix, generator, config, 4, "omega");
REQUIRE(response.has_value());
const auto& debug = response.value().debugStats;
CHECK(debug.at("topology_route_risk_status") == "satisfied");
CHECK(debug.at("topology_route_protected_fibers_represented_status") == "violated");
CHECK(debug.at("topology_route_admission_denial_reason") == "protected_fibers_unrepresented");
CHECK(debug.at("topology_weak_query_narrow_applied") == "0");
}
TEST_CASE("SearchEngine keeps multi-route proposals global until admitted",
"[search][topology][spq][balance][admission][catch2]") {
TopologySearchFixture fix{vector::VectorSearchEngine::SimeonPqAdc};
seedTopologyDocuments(fix);
seedTwoClusterTopology(fix);
REQUIRE(fix.vectorDb->buildIndex());
auto generator = makeFixedGenerator({0.0F, 1.0F});
auto config = topologyRoutingTestConfig(true);
config.topologyRoutingMode = SearchEngineConfig::TopologyRoutingMode::HybridAssist;
config.topologyNarrowMinBoundaryMargin = 0.0F;
config.topologyMinClusters = 2;
config.topologyMaxClusters = 2;
config.topologyMaxDocs = 2;
config.similarityThreshold = -1.0F;
auto response = runTopologySearch(fix, generator, config, 2);
REQUIRE(response.has_value());
REQUIRE(response.value().results.size() == 2U);
std::unordered_set<std::string> resultHashes;
for (const auto& result : response.value().results) {
resultHashes.insert(result.document.sha256Hash);
}
CHECK_FALSE((resultHashes.contains("x1") || resultHashes.contains("x2")));
CHECK((resultHashes.contains("y1") || resultHashes.contains("y2")));
CHECK(response.value().debugStats.at("topology_route_narrow_proposed") == "1");
CHECK(response.value().debugStats.at("topology_route_admission_eligible") == "0");
}
TEST_CASE("SearchEngine does not bound Simeon to an uncertified route",
"[search][topology][spq][fill][admission][catch2]") {
TopologySearchFixture fix{vector::VectorSearchEngine::SimeonPqAdc};
seedTopologyDocuments(fix);
seedTwoClusterTopology(fix);
REQUIRE(fix.vectorDb->buildIndex());
auto generator = makeFixedGenerator({0.0F, 1.0F});
auto config = topologyRoutingTestConfig(true);
config.topologyRoutingMode = SearchEngineConfig::TopologyRoutingMode::HybridAssist;
config.topologyNarrowMinBoundaryMargin = 0.0F;
config.topologyMinClusters = 1;
config.topologyMaxClusters = 1;
config.topologyMaxDocs = 4;
config.similarityThreshold = -1.0F;
auto response = runTopologySearch(fix, generator, config, 4);
REQUIRE(response.has_value());
CHECK(response.value().results.size() == 4U);
CHECK(response.value().debugStats.at("topology_vector_global_fill_count") == "0");
CHECK(response.value().debugStats.at("vector_search_ann_candidate_budget_actual") == "4");
CHECK(response.value().debugStats.at("topology_route_admission_eligible") == "0");
}
TEST_CASE("SearchEngine uses global Vec0 directly for an uncertified route",
"[search][topology][vec0][fill][admission][catch2]") {
TopologySearchFixture fix{vector::VectorSearchEngine::Vec0L2};
seedTopologyDocuments(fix);
seedTwoClusterTopology(fix);
auto generator = makeFixedGenerator({0.0F, 1.0F});
auto config = topologyRoutingTestConfig(true);
config.topologyRoutingMode = SearchEngineConfig::TopologyRoutingMode::HybridAssist;
config.topologyNarrowMinBoundaryMargin = 0.0F;
config.topologyMinClusters = 1;
config.topologyMaxClusters = 1;
config.topologyMaxDocs = 4;
config.similarityThreshold = -1.0F;
auto response = runTopologySearch(fix, generator, config, 4);
REQUIRE(response.has_value());
CHECK(response.value().results.size() == 4U);
CHECK(response.value().debugStats.at("topology_vector_global_fill_count") == "0");
CHECK(response.value().debugStats.at("topology_route_admission_eligible") == "0");
}
TEST_CASE("SearchEngine topology abstains from narrowing at an ambiguous route boundary",
"[search][topology][spq][confidence][catch2]") {
TopologySearchFixture fix{vector::VectorSearchEngine::SimeonPqAdc};
seedTopologyDocuments(fix);
seedTwoClusterTopology(fix);
REQUIRE(fix.vectorDb->buildIndex());
auto generator = makeFixedGenerator({1.0F, 1.0F});
auto config = topologyRoutingTestConfig(true);
config.topologyRoutingMode = SearchEngineConfig::TopologyRoutingMode::HybridAssist;
config.topologySparseDenseAlpha = 0.0F;
config.topologyMinClusters = 1;
config.topologyMaxClusters = 1;
config.topologyNarrowMinBoundaryMargin = 0.20F;
config.topologyMaxDocs = 2;
config.similarityThreshold = -1.0F;
auto response = runTopologySearch(fix, generator, config, 4);
REQUIRE(response.has_value());
CHECK(response.value().results.size() == 4U);
const auto& debug = response.value().debugStats;
CHECK(debug.at("topology_route_confidence_abstained") == "1");
CHECK(debug.at("topology_weak_query_skip_reason") == "route_low_confidence");
CHECK(debug.at("topology_weak_query_narrow_applied") == "0");
CHECK(debug.at("topology_vector_allowed_set_ann_applied") == "0");
CHECK(debug.at("vector_search_ann_candidate_budget_actual") == "4");
}
TEST_CASE("SearchEngine topology shadow leaves global ranking and scores unchanged",
"[search][topology][shadow][catch2]") {
yams::test::ScopedEnvVar stageTrace("YAMS_SEARCH_STAGE_TRACE", std::optional<std::string>("1"));
TopologySearchFixture fix{vector::VectorSearchEngine::SimeonPqAdc};
seedTopologyDocuments(fix);
seedTwoClusterTopology(fix);
REQUIRE(fix.vectorDb->buildIndex());
auto generator = makeFixedGenerator({0.0F, 1.0F});
SearchExecutionContext context = defaultSearchExecutionContext();
context.freshness.lexicalReady = true;
context.freshness.vectorReady = true;
context.freshness.kgReady = true;
context.freshness.topologyReady = true;
SearchExecutionContextGuard contextGuard(context);
auto globalConfig = topologyRoutingTestConfig(false);
SearchEngine globalEngine(fix.repo, fix.vectorDb, generator, fix.kgStore, globalConfig);
auto shadowConfig = topologyRoutingTestConfig(true);
shadowConfig.topologyRoutingMode = SearchEngineConfig::TopologyRoutingMode::HybridAssist;
shadowConfig.topologyVectorPolicy = SearchEngineConfig::TopologyVectorPolicy::Shadow;
shadowConfig.topologySparseDenseAlpha = 0.0F;
shadowConfig.topologyNarrowMinBoundaryMargin = 0.0F;
shadowConfig.topologyRouteWorkBudget.maxRowsVisited = 4;
shadowConfig.topologyRouteWorkBudget.maxExactDistanceEvaluations = 4;
shadowConfig.topologyRouteWorkBudget.maxAnnCandidates = 4;
SearchEngine shadowEngine(fix.repo, fix.vectorDb, generator, fix.kgStore, shadowConfig);
SearchParams params;
params.limit = 4;
auto global = globalEngine.searchWithResponse("unmatched query", params);
auto shadow = shadowEngine.searchWithResponse("unmatched query", params);
REQUIRE(global.has_value());
REQUIRE(shadow.has_value());
REQUIRE(shadow.value().results.size() == global.value().results.size());
bool scoreChanged = false;
for (std::size_t index = 0; index < global.value().results.size(); ++index) {
CHECK(shadow.value().results[index].document.sha256Hash ==
global.value().results[index].document.sha256Hash);
scoreChanged = scoreChanged ||
shadow.value().results[index].score != global.value().results[index].score;
}
CHECK_FALSE(scoreChanged);
const auto& debug = shadow.value().debugStats;
CHECK(debug.at("topology_vector_policy") == "shadow");
CHECK(debug.at("topology_shadow_evaluated") == "1");
CHECK(debug.at("topology_shadow_proposed_action") == "narrow");
CHECK(debug.at("topology_shadow_retained_candidates") == "2");
CHECK(debug.at("topology_shadow_removed_candidates") == "2");
CHECK(debug.at("topology_shadow_retained_candidate_doc_ids") == "y1\ty2");
CHECK(debug.at("topology_shadow_removed_candidate_doc_ids") == "x1\tx2");
CHECK(debug.at("topology_route_work_status") == "satisfied");
CHECK(debug.at("topology_route_work_observed") == "1");
CHECK(debug.at("topology_route_work_rows_visited_actual") == "2");
CHECK(debug.at("topology_route_work_exact_distance_evaluations_actual") == "2");
CHECK(debug.at("topology_route_work_ann_candidate_budget_actual") == "2");
CHECK(debug.at("topology_weak_query_applied") == "0");
CHECK(debug.at("topology_weak_query_narrow_applied") == "0");
CHECK(debug.at("topology_weak_query_added_candidates") == "0");
CHECK(debug.at("topology_vector_filter_applied") == "0");
CHECK(debug.at("topology_vector_allowed_set_ann_applied") == "0");
}
TEST_CASE("SearchEngine topology augmentation materializes query-scored rescue candidates",
"[search][topology][candidate-rescue][catch2]") {
yams::test::ScopedEnvVar stageTrace("YAMS_SEARCH_STAGE_TRACE", std::optional<std::string>("1"));
for (const auto searchEngine :
{vector::VectorSearchEngine::SimeonPqAdc, vector::VectorSearchEngine::ExactScan}) {
CAPTURE(searchEngine);
TopologySearchFixture fix{searchEngine};
seedTopologyDocuments(fix);
seedTwoClusterTopology(fix);
REQUIRE(fix.vectorDb->buildIndex());
auto generator = makeFixedGenerator({0.0F, 1.0F});
auto config = topologyRoutingTestConfig(true);
config.topologyRoutingMode = SearchEngineConfig::TopologyRoutingMode::HybridAssist;
config.topologyVectorPolicy = SearchEngineConfig::TopologyVectorPolicy::Augment;
config.topologySparseDenseAlpha = 1.0F;
config.topologyNarrowMinBoundaryMargin = 0.0F;
config.textMaxResults = 1;
config.vectorMaxResults = 1;
config.enableWeakQueryFanoutBoost = false;
config.topologyMaxDocs = 2;
config.topologyExpansionOutputLimit = 2;
config.similarityThreshold = -1.0F;
auto response = runTopologySearch(fix, generator, config, 4, "alpha one");
REQUIRE(response.has_value());
std::vector<std::string> resultHashes;
for (const auto& result : response.value().results) {
resultHashes.push_back(result.document.sha256Hash);
}
CHECK(std::ranges::find(resultHashes, "x2") != resultHashes.end());
const auto& debug = response.value().debugStats;
CHECK(debug.at("topology_vector_policy") == "augment");
CHECK(debug.at("topology_expansion_output_budget") == "2");
CHECK(debug.at("vector_search_result_budget") == "1");
CHECK(debug.at("topology_candidate_rescue_attempted") == "1");
CHECK(debug.at("topology_candidate_rescue_applied") == "1");
CHECK(debug.at("topology_candidate_rescue_added_candidates") == "2");
CHECK(debug.at("topology_candidate_rescue_novel_candidates") == "1");
CHECK(debug.at("topology_candidate_rescue_evidence_rescues") == "1");
CHECK(debug.at("topology_candidate_rescue_added_candidate_hashes") == "x2\tx1");
CHECK(debug.at("topology_candidate_rescue_added_candidate_doc_ids") == "x2\tx1");
CHECK(debug.at("topology_candidate_rescue_novel_candidate_doc_ids") == "x2");
CHECK(debug.at("topology_candidate_rescue_evidence_rescue_doc_ids") == "x1");
}
}
TEST_CASE("Topology selector resolves candidate hashes to metadata IDs, not trace IDs",
"[search][topology][candidate-rescue][simeon][catch2]") {
TopologySearchFixture fix{vector::VectorSearchEngine::SimeonPqAdc};
fix.addDocument("31715818", "alpha one", {1.0F, 0.0F});
fix.addDocument("30655442", "beta two", {0.0F, 1.0F});
const std::vector<std::string> hashes = {"30655442", "missing", "31715818"};
auto resolved = yams::search::detail::resolveMetadataDocumentIdsByHash(fix.repo, hashes);
REQUIRE(resolved.has_value());
REQUIRE(resolved.value().size() == 2U);
auto first = fix.repo->getDocumentByHash("30655442");
auto second = fix.repo->getDocumentByHash("31715818");
REQUIRE(first.has_value());
REQUIRE(first.value().has_value());
REQUIRE(second.has_value());
REQUIRE(second.value().has_value());
CHECK(resolved.value().at(0).first == "30655442");
CHECK(resolved.value().at(0).second == first.value()->id);
CHECK(resolved.value().at(1).first == "31715818");
CHECK(resolved.value().at(1).second == second.value()->id);
}
TEST_CASE("SearchEngine shadow policy measures candidate rescue without changing results",
"[search][topology][candidate-rescue][shadow][catch2]") {
yams::test::ScopedEnvVar stageTrace("YAMS_SEARCH_STAGE_TRACE", std::optional<std::string>("1"));
TopologySearchFixture fix{vector::VectorSearchEngine::SimeonPqAdc};
seedTopologyDocuments(fix);
seedTwoClusterTopology(fix);
REQUIRE(fix.vectorDb->buildIndex());
auto generator = makeFixedGenerator({0.0F, 1.0F});
auto config = topologyRoutingTestConfig(true);
config.topologyRoutingMode = SearchEngineConfig::TopologyRoutingMode::HybridAssist;
config.topologyVectorPolicy = SearchEngineConfig::TopologyVectorPolicy::Shadow;
config.topologySparseDenseAlpha = 1.0F;
config.topologyNarrowMinBoundaryMargin = 0.0F;
config.textMaxResults = 1;
config.vectorMaxResults = 1;
config.topologyMaxDocs = 2;
config.topologyExpansionOutputLimit = 2;
config.similarityThreshold = -1.0F;
auto response = runTopologySearch(fix, generator, config, 4, "alpha one");
REQUIRE(response.has_value());
std::vector<std::string> resultHashes;
for (const auto& result : response.value().results) {
resultHashes.push_back(result.document.sha256Hash);
}
CHECK(std::ranges::find(resultHashes, "x2") == resultHashes.end());
const auto& debug = response.value().debugStats;
CHECK(debug.at("topology_vector_policy") == "shadow");
CHECK(debug.at("topology_candidate_rescue_attempted") == "1");
CHECK(debug.at("topology_candidate_rescue_applied") == "0");
CHECK(debug.at("topology_candidate_rescue_added_candidates") == "2");
CHECK(debug.at("topology_candidate_rescue_novel_candidates") == "1");
CHECK(debug.at("topology_candidate_rescue_evidence_rescues") == "1");
CHECK(debug.at("topology_candidate_rescue_added_candidate_hashes") == "x2\tx1");
CHECK(debug.at("topology_candidate_rescue_added_candidate_doc_ids") == "x2\tx1");
CHECK(debug.at("topology_candidate_rescue_novel_candidate_doc_ids") == "x2");
CHECK(debug.at("topology_candidate_rescue_evidence_rescue_doc_ids") == "x1");
CHECK(debug.at("topology_candidate_rescue_exact_scored_candidate_doc_ids") == "x1\tx2");
const auto trace =
nlohmann::json::parse(debug.at("topology_candidate_rescue_exact_scored_candidate_rows"));
REQUIRE(trace.contains("vector"));
const auto& hits = trace.at("vector").at("top_hits");
REQUIRE(hits.size() == 2U);
CHECK(hits.at(0).at("doc_id") == "x2");
CHECK(hits.at(0).at("rank") == 1U);
CHECK(hits.at(0).at("score").get<double>() < 0.5);
CHECK(hits.at(1).at("doc_id") == "x1");
CHECK(debug.at("topology_candidate_rescue_exact_candidate_hashes_requested") == "2");
CHECK(debug.at("topology_candidate_rescue_exact_rows_visited_status") == "observed");
CHECK(debug.at("topology_candidate_rescue_exact_rows_visited") == "2");
CHECK(debug.at("topology_candidate_rescue_exact_distance_evaluations_status") == "observed");
CHECK(debug.at("topology_candidate_rescue_exact_distance_evaluations") == "2");
CHECK(debug.at("topology_candidate_rescue_exact_returned_rows") == "2");
CHECK(debug.at("topology_candidate_rescue_exact_matched_documents") == "2");
CHECK(debug.at("topology_candidate_rescue_exact_matched_document_hashes") == "x1\tx2");
CHECK(debug.at("topology_candidate_rescue_exact_matched_document_doc_ids") == "x1\tx2");
}
TEST_CASE("SearchEngine topology final-window rescue promotes a topology-scored candidate",
"[search][topology][candidate-rescue][final-rescue][catch2]") {
yams::test::ScopedEnvVar stageTrace("YAMS_SEARCH_STAGE_TRACE", std::optional<std::string>("1"));
TopologySearchFixture fix{vector::VectorSearchEngine::SimeonPqAdc};
fix.addDocument("x1", "alpha one", {1.0F, 0.0F});
// x2 is topology-reachable from x1 (see seedTwoClusterTopology) but its real vector embedding
// is deliberately dissimilar to the query, mirroring the q0/q37 boundary: relation-reachable
// and document-scored, but losing on raw fused score alone.
fix.addDocument("x2", "alpha two", {0.1F, 0.9F});
// x3 is a strong organic match (like x1) but has no topology relation at all, so it competes
// for the same final-window slot as x2 without ever receiving topology evidence.
fix.addDocument("x3", "alpha one", {0.95F, 0.05F});
fix.addDocument("y1", "omega one", {0.0F, 1.0F});
fix.addDocument("y2", "omega two", {0.1F, 0.9F});
seedTwoClusterTopology(fix);
REQUIRE(fix.vectorDb->buildIndex());
auto generator = makeFixedGenerator({1.0F, 0.0F});
auto config = topologyRoutingTestConfig(true);
config.topologyRoutingMode = SearchEngineConfig::TopologyRoutingMode::HybridAssist;
config.topologyVectorPolicy = SearchEngineConfig::TopologyVectorPolicy::Augment;
config.topologySparseDenseAlpha = 1.0F;
config.topologyNarrowMinBoundaryMargin = 0.0F;
config.textMaxResults = 2;
config.vectorMaxResults = 2;
config.enableWeakQueryFanoutBoost = false;
config.topologyMaxDocs = 2;
config.topologyExpansionOutputLimit = 2;
config.similarityThreshold = -1.0F;
// Default: no final-window rescue. The window's 2 slots go to the strong organic matches
// (x1, x3); the weak, topology-only x2 is excluded even though it was admitted and scored as
// a candidate.
auto baseline = runTopologySearch(fix, generator, config, 2, "alpha one");
REQUIRE(baseline.has_value());
REQUIRE(baseline.value().results.size() == 2);
std::vector<std::string> baselineHashes;
for (const auto& r : baseline.value().results) {
baselineHashes.push_back(r.document.sha256Hash);
}
CHECK(std::ranges::find(baselineHashes, "x1") != baselineHashes.end());
CHECK(std::ranges::find(baselineHashes, "x3") != baselineHashes.end());
CHECK(std::ranges::find(baselineHashes, "x2") == baselineHashes.end());
CHECK(baseline.value().debugStats.at("topology_final_rescue_enabled") == "0");
// Enabling the topology final-window rescue seam and requiring both window slots to carry
// topology evidence promotes x2 (which has it) over x3 (which does not), independent of the
// generic evidence-rescue family (fusionEvidenceRescueSlots), which stays disabled here.
config.topologyFinalRescueSlots = 2;
auto rescued = runTopologySearch(fix, generator, config, 2, "alpha one");
REQUIRE(rescued.has_value());
REQUIRE(rescued.value().results.size() == 2);
std::vector<std::string> rescuedHashes;
for (const auto& r : rescued.value().results) {
rescuedHashes.push_back(r.document.sha256Hash);
}
CHECK(std::ranges::find(rescuedHashes, "x1") != rescuedHashes.end());
CHECK(std::ranges::find(rescuedHashes, "x2") != rescuedHashes.end());
CHECK(std::ranges::find(rescuedHashes, "x3") == rescuedHashes.end());
const auto& debug = rescued.value().debugStats;
CHECK(debug.at("topology_final_rescue_enabled") == "1");
CHECK(debug.at("topology_final_rescue_slots") == "2");
CHECK(debug.at("topology_final_rescue_promoted_doc_ids") == "x2");
CHECK(debug.at("topology_final_rescue_displaced_doc_ids") == "x3");
CHECK(debug.at("topology_final_rescue_eligible_window_doc_ids").empty());
CHECK(debug.at("topology_final_rescue_eligible_tail_doc_ids") == "x2");
// x1 is organically found (not a novel rescue) but still shares the topology route with x2,
// so it also receives nonzero topologyScore -- a magnitude-only gate would have considered it
// just as eligible as x2. It must never appear as promoted/displaced: the eligibility set is
// scoped to novel candidates only, not every topology-touched one.
const auto x1Result = std::ranges::find_if(
rescued.value().results, [](const auto& r) { return r.document.sha256Hash == "x1"; });
REQUIRE(x1Result != rescued.value().results.end());
CHECK(x1Result->topologyScore.has_value());
CHECK(debug.at("topology_final_rescue_promoted_doc_ids").find("x1") == std::string::npos);
CHECK(debug.at("topology_final_rescue_displaced_doc_ids").find("x1") == std::string::npos);
}
namespace {
// x2 shares a topology route with the lexical seed x1, but lexical Tier-1 keeps only x1
// (textMaxResults=1) and the global vector top-1 is y1 (query embedding {0,1}). x2 is therefore
// reachable only through topology routing.
SearchEngineConfig routedOnlyCandidateConfig(SearchEngineConfig::TopologyVectorPolicy policy) {
auto config = topologyRoutingTestConfig(true);
config.topologyRoutingMode = SearchEngineConfig::TopologyRoutingMode::HybridAssist;
config.topologyVectorPolicy = policy;
config.topologySparseDenseAlpha = 1.0F;
config.topologyNarrowMinBoundaryMargin = 0.0F;
config.textMaxResults = 1;
config.vectorMaxResults = 1;
config.enableWeakQueryFanoutBoost = false;
config.topologyMaxDocs = 2;
config.topologyExpansionOutputLimit = 2;
config.similarityThreshold = -1.0F;
return config;
}
TopologyAssistStageResult
runRoutedOnlyAssistStage(TopologySearchFixture& fix,
SearchEngineConfig::TopologyVectorPolicy policy) {
TopologyAssistStageRequest request;
request.query = "alpha one";
request.config = routedOnlyCandidateConfig(policy);
request.routingMode = SearchEngineConfig::TopologyRoutingMode::HybridAssist;
request.weakTier1Query = true;
request.tier1SeedHashes = {"x1"};
request.tier1SeedEvidence = {{.documentHash = "x1", .weight = 1.0F}};
request.existingCandidateHashes = {"x1"};
request.queryEmbedding = std::vector<float>{0.0F, 1.0F};
request.queryEmbeddingSpaceIdentity = "test-space-v1";
request.metadataRepo = fix.repo;
request.kgStore = fix.kgStore;
return runTopologyAssistStage(request);
}
} // namespace
TEST_CASE("Topology assist stage reports routed members added under augment only",
"[search][topology][augment][catch2]") {
const TopologyVectorEnvPin vectorEnv;
TopologySearchFixture fix{vector::VectorSearchEngine::SimeonPqAdc};
seedTopologyDocuments(fix);
seedTwoClusterTopology(fix);
SECTION("augment counts allowed members that are new to the candidate pool") {
const auto stage =
runRoutedOnlyAssistStage(fix, SearchEngineConfig::TopologyVectorPolicy::Augment);
const auto& session = stage.session;
REQUIRE(session.applied);
CHECK(stage.skipReason.empty());
CHECK(session.certificate.allowedDocumentHashes ==
std::unordered_set<std::string>{"x1", "x2"});
CHECK(session.routedDocs == 2U);
CHECK(session.addedCandidates == 1U);
CHECK(session.duplicateCandidates == 1U);
CHECK(session.addedCandidateHashes == std::vector<std::string>{"x2"});
}
for (const auto policy : {SearchEngineConfig::TopologyVectorPolicy::Narrow,
SearchEngineConfig::TopologyVectorPolicy::Shadow}) {
DYNAMIC_SECTION("policy " << SearchEngineConfig::topologyVectorPolicyToString(policy)
<< " keeps the allowed set without claiming additions") {
const auto stage = runRoutedOnlyAssistStage(fix, policy);
const auto& session = stage.session;
REQUIRE(session.applied);
CHECK(session.certificate.allowedDocumentHashes ==
std::unordered_set<std::string>{"x1", "x2"});
CHECK(session.routedDocs == 2U);
CHECK(session.addedCandidates == 0U);
CHECK(session.duplicateCandidates == 0U);
CHECK(session.addedCandidateHashes.empty());
}
}
}
TEST_CASE("SearchEngine topology augment adds a routed-only document to the fused results",
"[search][topology][augment][catch2]") {
const TopologyVectorEnvPin vectorEnv;
TopologySearchFixture fix{vector::VectorSearchEngine::SimeonPqAdc};
seedTopologyDocuments(fix);
seedTwoClusterTopology(fix);
REQUIRE(fix.vectorDb->buildIndex());
auto generator = makeFixedGenerator({0.0F, 1.0F});
auto disabledConfig =
routedOnlyCandidateConfig(SearchEngineConfig::TopologyVectorPolicy::Shadow);
disabledConfig.topologyRoutingMode = SearchEngineConfig::TopologyRoutingMode::Disabled;
auto disabled = runTopologySearch(fix, generator, disabledConfig, 4, "alpha one");
REQUIRE(disabled.has_value());
const auto disabledHashes = resultHashes(disabled.value());
// Precondition: without topology the routed-only document is unreachable.
REQUIRE(std::ranges::find(disabledHashes, "x2") == disabledHashes.end());
SECTION("augment") {
auto response = runTopologySearch(
fix, generator,
routedOnlyCandidateConfig(SearchEngineConfig::TopologyVectorPolicy::Augment), 4,
"alpha one");
REQUIRE(response.has_value());
const auto routed = std::ranges::find_if(
response.value().results, [](const auto& r) { return r.document.sha256Hash == "x2"; });
REQUIRE(routed != response.value().results.end());
CHECK(routed->score > 0.0);
const auto& debug = response.value().debugStats;
CHECK(debug.at("topology_vector_policy") == "augment");
CHECK(debug.at("topology_weak_query_applied") == "1");
CHECK(debug.at("topology_weak_query_skip_reason").empty());
CHECK(debug.at("topology_weak_query_added_candidates") == "1");
CHECK(debug.at("topology_weak_query_duplicate_candidates") == "1");
CHECK(debug.at("topology_weak_query_total_candidates") == "2");
CHECK(debug.at("topology_candidate_rescue_applied") == "1");
CHECK(debug.at("topology_weak_query_narrow_applied") == "0");
}
SECTION("shadow matches topology-disabled results") {
auto response = runTopologySearch(
fix, generator,
routedOnlyCandidateConfig(SearchEngineConfig::TopologyVectorPolicy::Shadow), 4,
"alpha one");
REQUIRE(response.has_value());
REQUIRE(response.value().results.size() == disabled.value().results.size());
for (std::size_t i = 0; i < response.value().results.size(); ++i) {
CHECK(response.value().results[i].document.sha256Hash ==
disabled.value().results[i].document.sha256Hash);
CHECK(response.value().results[i].score == disabled.value().results[i].score);
}
const auto& debug = response.value().debugStats;
CHECK(debug.at("topology_shadow_evaluated") == "1");
CHECK(debug.at("topology_weak_query_applied") == "0");
CHECK(debug.at("topology_weak_query_added_candidates") == "0");
CHECK(debug.at("topology_weak_query_duplicate_candidates") == "0");
CHECK(debug.at("topology_weak_query_total_candidates") == "1");
}
SECTION("narrow without a certificate stays global") {
auto response = runTopologySearch(
fix, generator,
routedOnlyCandidateConfig(SearchEngineConfig::TopologyVectorPolicy::Narrow), 4,
"alpha one");
REQUIRE(response.has_value());
const auto hashes = resultHashes(response.value());
CHECK(std::ranges::find(hashes, "x2") == hashes.end());
const auto& debug = response.value().debugStats;
CHECK(debug.at("topology_vector_policy") == "narrow");
CHECK(debug.at("topology_weak_query_applied") == "1");
CHECK(debug.at("topology_route_admission_eligible") == "0");
CHECK(debug.at("topology_weak_query_narrow_applied") == "0");
CHECK(debug.at("topology_weak_query_added_candidates") == "0");
CHECK(debug.at("topology_weak_query_duplicate_candidates") == "0");
CHECK(debug.at("topology_weak_query_total_candidates") == "1");
CHECK(debug.at("topology_candidate_rescue_attempted") == "0");
}
}
TEST_CASE("Evidence pipeline receives no topology adjustments under a shadow policy",
"[search][topology][evidence_pipeline][catch2]") {
TopologySearchFixture fix{vector::VectorSearchEngine::Vec0L2};
seedTopologyDocuments(fix);
seedTwoClusterTopology(fix);
auto generator = makeFixedGenerator({0.0F, 1.0F});
SearchExecutionContext context = defaultSearchExecutionContext();
context.freshness.lexicalReady = true;
context.freshness.vectorReady = true;
context.freshness.kgReady = true;
context.freshness.topologyReady = true;
SearchExecutionContextGuard contextGuard(context);
auto globalConfig = topologyRoutingTestConfig(false);
SearchEngine globalEngine(fix.repo, fix.vectorDb, generator, fix.kgStore, globalConfig);
auto evidenceConfig = topologyRoutingTestConfig(true);
evidenceConfig.topologyRoutingMode = SearchEngineConfig::TopologyRoutingMode::HybridAssist;
evidenceConfig.topologyVectorPolicy = SearchEngineConfig::TopologyVectorPolicy::Shadow;
evidenceConfig.topologySparseDenseAlpha = 0.0F;
evidenceConfig.topologyNarrowMinBoundaryMargin = 0.0F;
SearchEngine evidenceEngine(fix.repo, fix.vectorDb, generator, fix.kgStore, evidenceConfig);
SearchParams params;
params.limit = 4;
auto global = globalEngine.searchWithResponse("unmatched query", params);
auto evidence = evidenceEngine.searchWithResponse("unmatched query", params);
REQUIRE(global.has_value());
REQUIRE(evidence.has_value());
std::vector<std::string> globalHashes;
std::vector<std::string> evidenceHashes;
for (const auto& result : global.value().results) {
globalHashes.push_back(result.document.sha256Hash);
}
for (const auto& result : evidence.value().results) {
evidenceHashes.push_back(result.document.sha256Hash);
}
std::ranges::sort(globalHashes);
std::ranges::sort(evidenceHashes);
CHECK(evidenceHashes == globalHashes);
const auto& debug = evidence.value().debugStats;
CHECK(debug.at("topology_vector_policy") == "shadow");
CHECK(debug.at("topology_weak_query_applied") == "0");
CHECK(debug.at("topology_weak_query_narrow_applied") == "0");
CHECK(debug.at("topology_vector_filter_applied") == "0");
CHECK(debug.at("topology_vector_allowed_set_ann_applied") == "0");
CHECK_FALSE(debug.contains("topology_shadow_retained_candidate_doc_ids"));
CHECK_FALSE(debug.contains("topology_shadow_removed_candidate_doc_ids"));
CHECK_FALSE(debug.contains("trace_stage_summary_json"));
CHECK_FALSE(debug.contains("trace_fusion_source_summary_json"));
CHECK(debug.at("candidate_pipeline_topology_annotated_candidates") == "0");
}
TEST_CASE("Hybrid topology keeps lexical hits while uncertified narrowing falls back",
"[search][topology][narrowing][admission][catch2]") {
TopologySearchFixture fix;
seedTopologyDocuments(fix);
fix.addDocument("lexical-only", "unique needle", {1.0F, 0.0F});
seedTwoClusterTopology(fix);
auto config = topologyRoutingTestConfig(true);
config.topologyRoutingMode = SearchEngineConfig::TopologyRoutingMode::HybridAssist;
config.topologySparseDenseAlpha = 0.0F;
auto generator = makeFixedGenerator({0.0F, 1.0F});
SearchExecutionContext context = defaultSearchExecutionContext();
context.freshness.lexicalReady = true;
context.freshness.vectorReady = true;
context.freshness.kgReady = true;
context.freshness.topologyReady = true;
SearchExecutionContextGuard contextGuard(context);
SearchEngine engine(fix.repo, fix.vectorDb, generator, fix.kgStore, config);
SearchParams params;
params.limit = 4;
auto response = engine.searchWithResponse("unique needle", params);
REQUIRE(response.has_value());
const auto& debug = response.value().debugStats;
REQUIRE(debug.at("topology_weak_query_narrow_applied") == "0");
CHECK(debug.at("topology_route_narrow_proposed") == "1");
CHECK(debug.at("topology_route_admission_eligible") == "0");
CHECK(debug.at("topology_weak_query_allowed_candidates") == "2");
CHECK(debug.at("topology_vector_filter_applied") == "0");
CHECK_FALSE(debug.contains("topology_vector_scores_reused"));
CHECK_FALSE(debug.contains("topology_member_rerank_distance_evaluations_actual"));
const auto lexical = std::find_if(
response.value().results.begin(), response.value().results.end(),
[](const auto& result) { return result.document.sha256Hash == "lexical-only"; });
REQUIRE(lexical != response.value().results.end());
}
TEST_CASE("SearchEngine topology routing loads stored artifacts without freshness readiness",
"[search][topology][catch2]") {
TopologySearchFixture fix;
seedTopologyDocuments(fix);
seedTwoClusterTopology(fix);
auto generator = makeFixedGenerator({0.0F, 1.0F});
SearchEngineConfig config = topologyRoutingTestConfig(true);
SearchExecutionContext context = defaultSearchExecutionContext();
context.freshness.lexicalReady = true;
context.freshness.vectorReady = true;
context.freshness.kgReady = true;
context.freshness.topologyReady = false;
context.freshness.topologyArtifactsFresh = false;
context.freshness.topologyEpoch = 0;
SearchExecutionContextGuard contextGuard(context);
SearchEngine engine(fix.repo, fix.vectorDb, generator, fix.kgStore, config);
SearchParams params;
params.limit = 4;
auto response = engine.searchWithResponse("zz", params);
REQUIRE(response.has_value());
const auto& debug = response.value().debugStats;
CHECK(debug.at("topology_routing_freshness_gate") == "1");
CHECK((debug.at("topology_routing_mode") == "weak_query_only"));
CHECK((debug.at("topology_weak_query_enabled") == "1"));
CHECK((debug.at("topology_weak_query_load_attempted") == "1"));
CHECK((debug.at("topology_weak_query_load_succeeded") == "1"));
CHECK((debug.at("topology_ready") == "1"));
CHECK((debug.at("topology_artifacts_fresh") == "1"));
CHECK(debug.contains("topology_epoch"));
CHECK(debug.at("topology_weak_query_skip_reason").empty());
CHECK((debug.at("topology_weak_query_applied") == "1"));
CHECK((debug.at("topology_weak_query_added_candidates") == "0"));
}
TEST_CASE("SearchEngine abstains from topology routing when daemon artifacts are stale",
"[search][topology][freshness][catch2]") {
TopologySearchFixture fix;
seedTopologyDocuments(fix);
seedTwoClusterTopology(fix);
auto generator = makeFixedGenerator({0.0F, 1.0F});
SearchEngineConfig config = topologyRoutingTestConfig(true);
SearchExecutionContext context = defaultSearchExecutionContext();
context.freshness.lexicalReady = true;
context.freshness.vectorReady = true;
context.freshness.kgReady = true;
context.freshness.topologyStatusKnown = true;
context.freshness.topologyEpoch = 7;
context.freshness.topologyReady = false;
context.freshness.topologyArtifactsFresh = false;
context.freshness.topologyDirtyDocuments = 1;
SearchExecutionContextGuard contextGuard(context);
SearchEngine engine(fix.repo, fix.vectorDb, generator, fix.kgStore, config);
SearchParams params;
params.limit = 4;
auto response = engine.searchWithResponse("zz", params);
REQUIRE(response.has_value());
const auto& debug = response.value().debugStats;
CHECK(debug.at("topology_routing_freshness_gate") == "0");
CHECK(debug.at("topology_routing_mode") == "disabled");
CHECK(debug.at("topology_weak_query_load_attempted") == "0");
CHECK(debug.at("topology_weak_query_load_succeeded") == "0");
CHECK(debug.at("topology_weak_query_skip_reason") == "artifacts_not_fresh");
CHECK(debug.at("topology_weak_query_applied") == "0");
CHECK(debug.at("topology_weak_query_narrow_applied") == "0");
}
TEST_CASE("SearchEngine rejects inconsistent topology artifacts before routing",
"[search][topology][catch2]") {
TopologySearchFixture fix;
seedTopologyDocuments(fix);
seedTopologyWithStaleClusterMember(fix);
auto generator = makeFixedGenerator({0.0F, 1.0F});
SearchEngineConfig config = topologyRoutingTestConfig(true);
SearchExecutionContext context = defaultSearchExecutionContext();
context.freshness.lexicalReady = true;
context.freshness.vectorReady = true;
context.freshness.kgReady = true;
context.freshness.topologyReady = true;
context.freshness.topologyArtifactsFresh = true;
SearchExecutionContextGuard contextGuard(context);
SearchEngine engine(fix.repo, fix.vectorDb, generator, fix.kgStore, config);
SearchParams params;
params.limit = 4;
auto response = engine.searchWithResponse("zz", params);
REQUIRE(response.has_value());
const auto& debug = response.value().debugStats;
CHECK((debug.at("topology_weak_query_load_attempted") == "1"));
CHECK((debug.at("topology_weak_query_load_succeeded") == "1"));
CHECK((debug.at("topology_artifact_admitted") == "0"));
CHECK((debug.at("topology_weak_query_skip_reason").rfind("invalid_artifact:", 0) == 0));
CHECK((debug.at("topology_weak_query_applied") == "0"));
CHECK((debug.at("topology_weak_query_added_candidates") == "0"));
}
TEST_CASE("SearchEngine disabled topology routing does not load or add candidates",
"[search][topology][catch2]") {
TopologySearchFixture fix;
seedTopologyDocuments(fix);
seedTwoClusterTopology(fix);
auto generator = makeFixedGenerator({0.0F, 1.0F});
SearchEngineConfig config = topologyRoutingTestConfig(false);
SearchExecutionContext context = defaultSearchExecutionContext();
context.freshness.lexicalReady = true;
context.freshness.vectorReady = true;
context.freshness.kgReady = true;
SearchExecutionContextGuard contextGuard(context);
SearchEngine engine(fix.repo, fix.vectorDb, generator, fix.kgStore, config);
SearchParams params;
params.limit = 4;
auto response = engine.searchWithResponse("zz", params);
REQUIRE(response.has_value());
const auto& debug = response.value().debugStats;
CHECK((debug.at("topology_routing_mode") == "disabled"));
CHECK((debug.at("topology_weak_query_enabled") == "0"));
CHECK((debug.at("topology_weak_query_load_attempted") == "0"));
CHECK((debug.at("topology_weak_query_load_succeeded") == "0"));
CHECK((debug.at("topology_weak_query_skip_reason") == "disabled"));
CHECK((debug.at("topology_weak_query_applied") == "0"));
CHECK((debug.at("topology_weak_query_narrow_applied") == "0"));
CHECK((debug.at("topology_weak_query_added_candidates") == "0"));
}
TEST_CASE("SearchEngine weak-only topology routing skips strong tier-1 queries",
"[search][topology][catch2]") {
TopologySearchFixture fix;
seedTopologyDocuments(fix);
seedTwoClusterTopology(fix);
auto generator = makeFixedGenerator({0.0F, 1.0F});
SearchEngineConfig config = topologyRoutingTestConfig(true);
config.weakQueryMinTextHits = 1;
config.weakQueryMinTopTextScore = 0.0F;
SearchExecutionContext context = defaultSearchExecutionContext();
context.freshness.lexicalReady = true;
context.freshness.vectorReady = true;
context.freshness.kgReady = true;
context.freshness.topologyReady = true;
context.freshness.topologyArtifactsFresh = true;
context.freshness.topologyEpoch = 7;
SearchExecutionContextGuard contextGuard(context);
SearchEngine engine(fix.repo, fix.vectorDb, generator, fix.kgStore, config);
SearchParams params;
params.limit = 4;
auto response = engine.searchWithResponse("alpha", params);
REQUIRE(response.has_value());
const auto& debug = response.value().debugStats;
CHECK((debug.at("topology_routing_mode") == "weak_query_only"));
CHECK((debug.at("topology_weak_query_enabled") == "1"));
CHECK((debug.at("topology_weak_query_load_attempted") == "0"));
CHECK((debug.at("topology_weak_query_load_succeeded") == "0"));
CHECK((debug.at("topology_weak_query_skip_reason") == "strong_tier1_query"));
CHECK((debug.at("topology_weak_query_applied") == "0"));
CHECK((debug.at("topology_weak_query_added_candidates") == "0"));
}
TEST_CASE("SearchEngine global fallback vector-scores outside-route lexical hits",
"[search][topology][admission][catch2]") {
TopologySearchFixture fix;
seedTopologyDocuments(fix);
fix.addDocument("z1", "alpha outside route", {0.0F, 1.0F});
seedTwoClusterTopology(fix);
auto generator = makeFixedGenerator({0.0F, 1.0F});
SearchEngineConfig config = topologyRoutingTestConfig(true);
config.topologyRoutingMode = SearchEngineConfig::TopologyRoutingMode::HybridAssist;
config.weakQueryMinTextHits = 1;
config.weakQueryMinTopTextScore = 0.0F;
SearchExecutionContext context = defaultSearchExecutionContext();
context.freshness.lexicalReady = true;
context.freshness.vectorReady = true;
context.freshness.kgReady = true;
SearchExecutionContextGuard contextGuard(context);
SearchEngine engine(fix.repo, fix.vectorDb, generator, fix.kgStore, config);
SearchParams params;
params.limit = 4;
auto response = engine.searchWithResponse("alpha", params);
REQUIRE(response.has_value());
const auto& debug = response.value().debugStats;
CHECK((debug.at("topology_routing_mode") == "hybrid_assist"));
CHECK((debug.at("topology_weak_query_load_attempted") == "1"));
CHECK((debug.at("topology_weak_query_load_succeeded") == "1"));
CHECK(debug.at("topology_weak_query_skip_reason").empty());
CHECK((debug.at("topology_weak_query_applied") == "1"));
CHECK((debug.at("topology_weak_query_narrow_applied") == "0"));
CHECK((debug.at("topology_route_narrow_proposed") == "1"));
CHECK((debug.at("topology_route_admission_eligible") == "0"));
CHECK((debug.at("topology_weak_query_allowed_candidates") == "2"));
const auto outsideRoute =
std::find_if(response.value().results.begin(), response.value().results.end(),
[](const auto& result) { return result.document.sha256Hash == "z1"; });
REQUIRE((outsideRoute != response.value().results.end()));
CHECK(outsideRoute->vectorScore.has_value());
const bool hasTopologyVectorScore = std::any_of(
response.value().results.begin(), response.value().results.end(), [](const auto& result) {
return result.graphVectorScore.has_value() && result.graphVectorScore.value() > 0.0;
});
CHECK_FALSE(hasTopologyVectorScore);
}
TEST_CASE("SearchEngine stage trace reports reranker flags as booleans",
"[search][trace][rerank][catch2]") {
yams::test::ScopedEnvVar stageTrace("YAMS_SEARCH_STAGE_TRACE", std::optional<std::string>("1"));
yams::test::ScopedEnvVar traceTopN("YAMS_SEARCH_STAGE_TRACE_TOP_N",
std::optional<std::string>("4"));
TopologySearchFixture fix;
fix.addDocument("x1", "alpha one", {1.0F, 0.0F});
fix.addDocument("x2", "alpha two", {0.9F, 0.1F});
SearchEngineConfig config;
config.includeDebugInfo = true;
config.enableParallelExecution = false;
config.textWeight = 1.0F;
config.simeonTextWeight = 0.0F;
config.pathTreeWeight = 0.0F;
config.kgWeight = 0.0F;
config.vectorWeight = 0.0F;
config.entityVectorWeight = 0.0F;
config.tagWeight = 0.0F;
config.metadataWeight = 0.0F;
config.enableGraphRerank = false;
config.enableReranking = true;
config.rerankTopK = 5;
SearchExecutionContext context = defaultSearchExecutionContext();
context.freshness.lexicalReady = true;
SearchExecutionContextGuard contextGuard(context);
SearchEngine engine(fix.repo, fix.vectorDb, nullptr, fix.kgStore, config);
SearchParams params;
params.limit = 2;
auto response = engine.searchWithResponse("alpha", params);
REQUIRE(response.has_value());
const auto& debug = response.value().debugStats;
REQUIRE(debug.contains("trace_enabled"));
CHECK((debug.at("trace_enabled") == "1"));
REQUIRE(debug.contains("trace_cross_rerank_applied"));
CHECK((debug.at("trace_cross_rerank_applied") == "0"));
REQUIRE(debug.contains("trace_rerank_guard_score_gap"));
REQUIRE(debug.contains("trace_stage_summary_json"));
REQUIRE(debug.contains("trace_fusion_source_summary_json"));
CHECK((debug.at("trace_rerank_guard_score_gap") == "0.000000"));
}
TEST_CASE("SearchEngine exposes the topology evidence pipeline as its only frontend",
"[search][evidence_pipeline][catch2]") {
TopologySearchFixture fix;
fix.addDocument("x1", "alpha one", {1.0F, 0.0F});
fix.addDocument("x2", "alpha two", {0.9F, 0.1F});
SearchEngineConfig config;
config.includeDebugInfo = true;
config.enableParallelExecution = false;
config.textWeight = 1.0F;
config.simeonTextWeight = 0.0F;
config.pathTreeWeight = 0.0F;
config.kgWeight = 0.0F;
config.vectorWeight = 0.0F;
config.entityVectorWeight = 0.0F;
config.tagWeight = 0.0F;
config.metadataWeight = 0.0F;
config.enableGraphRerank = false;
config.enableReranking = false;
SearchExecutionContext context = defaultSearchExecutionContext();
context.freshness.lexicalReady = true;
SearchExecutionContextGuard contextGuard(context);
SearchEngine engine(fix.repo, fix.vectorDb, nullptr, fix.kgStore, config);
SearchParams params;
params.limit = 2;
auto response = engine.searchWithResponse("alpha", params);
REQUIRE(response);
REQUIRE(response.value().results.size() == 2);
const auto& debug = response.value().debugStats;
CHECK(debug.at("search_pipeline_name") == "topology_evidence");
CHECK(std::stoull(debug.at("candidate_pipeline_input_components")) > 0);
CHECK(std::stoull(debug.at("candidate_pipeline_aggregated_candidates")) >=
response.value().results.size());
CHECK(debug.at("candidate_pipeline_topology_annotated_candidates") == "0");
}
TEST_CASE("SearchEngine cross reranker promotes lower fused candidate",
"[search][rerank][catch2]") {
TopologySearchFixture fix;
fix.addDocument("x1", "alpha one", {1.0F, 0.0F});
fix.addDocument("x2", "alpha two", {0.9F, 0.1F});
SearchEngineConfig config;
config.includeDebugInfo = true;
config.enableParallelExecution = false;
config.textWeight = 1.0F;
config.simeonTextWeight = 0.0F;
config.pathTreeWeight = 0.0F;
config.kgWeight = 0.0F;
config.vectorWeight = 0.0F;
config.entityVectorWeight = 0.0F;
config.tagWeight = 0.0F;
config.metadataWeight = 0.0F;
config.enableGraphRerank = false;
config.enableReranking = true;
config.rerankTopK = 2;
config.rerankReplaceScores = true;
SearchExecutionContext context = defaultSearchExecutionContext();
context.freshness.lexicalReady = true;
SearchExecutionContextGuard contextGuard(context);
SearchEngine engine(fix.repo, fix.vectorDb, nullptr, fix.kgStore, config);
engine.setCrossReranker(
[](const std::string&,
const std::vector<std::string>& documents) -> Result<std::vector<float>> {
std::vector<float> scores;
scores.reserve(documents.size());
for (const auto& doc : documents) {
scores.push_back(doc.find("x2") != std::string::npos ? 1.0F : 0.1F);
}
return scores;
});
SearchParams params;
params.limit = 2;
auto response = engine.searchWithResponse("alpha", params);
REQUIRE(response.has_value());
REQUIRE((response.value().results.size() == 2));
CHECK((response.value().results.front().document.sha256Hash == "x2"));
const auto& debug = response.value().debugStats;
CHECK((debug.at("cross_rerank_available") == "1"));
CHECK(debug.at("cross_rerank_skip_reason").empty());
CHECK((debug.at("cross_rerank_replace_scores") == "1"));
}
TEST_CASE("SearchEngine reports the scoring mode of the wired cross reranker",
"[search][rerank][catch2]") {
TopologySearchFixture fix;
fix.addDocument("x1", "alpha one", {1.0F, 0.0F});
fix.addDocument("x2", "alpha two", {0.9F, 0.1F});
SearchEngineConfig config;
config.includeDebugInfo = true;
config.enableParallelExecution = false;
config.enableGraphRerank = false;
config.enableReranking = true;
config.rerankTopK = 2;
// The daemon resolves the scoring mode from reranker policy, which can differ from the
// engine config; the debug stat must describe the scorer that actually ran.
config.simeonRerankOuterMaxSim = false;
SearchExecutionContext context = defaultSearchExecutionContext();
context.freshness.lexicalReady = true;
SearchExecutionContextGuard contextGuard(context);
SearchEngine engine(fix.repo, fix.vectorDb, nullptr, fix.kgStore, config);
auto scorer = [](const std::string&,
const std::vector<std::string>& documents) -> Result<std::vector<float>> {
return std::vector<float>(documents.size(), 0.5F);
};
SearchParams params;
params.limit = 2;
SECTION("mode supplied by the wiring wins over the engine config") {
engine.setCrossReranker(scorer, "simeon_outer_maxsim");
auto response = engine.searchWithResponse("alpha", params);
REQUIRE(response.has_value());
const auto& debug = response.value().debugStats;
CHECK((debug.at("cross_rerank_scoring_mode") == "simeon_outer_maxsim"));
CHECK((debug.at("cross_rerank_simeon_outer_maxsim") == "1"));
}
SECTION("an unlabelled scorer reports an unknown mode") {
engine.setCrossReranker(scorer);
auto response = engine.searchWithResponse("alpha", params);
REQUIRE(response.has_value());
const auto& debug = response.value().debugStats;
CHECK((debug.at("cross_rerank_scoring_mode") == "unknown"));
CHECK((debug.at("cross_rerank_simeon_outer_maxsim") == "0"));
}
}
// NOLINTEND(cppcoreguidelines-avoid-do-while, readability-function-cognitive-complexity)
TEST_CASE("Topology routing options preserve the typed product configuration",
"[search][topology][options][catch2]") {
SearchEngineConfig config;
config.topologyRouteScoringMode = SearchEngineConfig::TopologyRouteScoringMode::SeedCoverage;
config.topologyExpansionSource = SearchEngineConfig::TopologyExpansionSource::GraphNeighbors;
config.topologyMinClusters = 2;
config.topologyMaxClusters = 5;
config.topologyRoutingRepresentativeLimit = 3;
config.topologyRoutingAnnCandidateLimit = 19;
config.topologyAdaptiveProbeScoreGap = 0.07F;
config.topologyNarrowMinBoundaryMargin = 0.11F;
config.topologyMaxDocs = 17;
config.topologySparseDenseAlpha = 0.65F;
config.topologyMinRouteScore = 0.22F;
config.topologyRouteRiskCalibration.constructionFingerprint = "atlas-fingerprint";
config.topologyRouteWorkBudget.maxRowsVisited = 23;
config.topologyGraphNeighborMinScore = 0.31F;
config.topologyGraphNeighborReciprocalOnly = false;
config.topologyGraphWeightedSeedRanking = true;
const auto options =
makeTopologyRoutingOptions(config, SearchEngineConfig::TopologyRoutingMode::WeakQueryOnly,
/*weakTier1Query=*/true, /*collectRouteMembership=*/true);
CHECK(options.routingMode == SearchEngineConfig::TopologyRoutingMode::WeakQueryOnly);
CHECK(options.routeScoringMode == SearchEngineConfig::TopologyRouteScoringMode::SeedCoverage);
CHECK(options.expansionSource == SearchEngineConfig::TopologyExpansionSource::GraphNeighbors);
CHECK(options.weakTier1Query);
CHECK(options.minClusters == 2U);
CHECK(options.maxClusters == 5U);
CHECK(options.representativeLimit == 3U);
CHECK(options.denseAnnCandidateLimit == 19U);
CHECK(options.adaptiveProbeScoreGap == Catch::Approx(0.07F));
CHECK(options.narrowMinBoundaryMargin == Catch::Approx(0.11F));
CHECK(options.maxDocs == 17U);
CHECK(options.sparseDenseAlpha == Catch::Approx(0.65F));
CHECK(options.minRouteScore == Catch::Approx(0.22F));
CHECK(options.routeRiskCalibration.constructionFingerprint == "atlas-fingerprint");
CHECK(options.routeWorkBudget.maxRowsVisited == 23U);
CHECK(options.collectRouteMembership);
CHECK(options.graphNeighborMinScore == Catch::Approx(0.31F));
CHECK_FALSE(options.graphNeighborReciprocalOnly);
CHECK(options.graphWeightedSeedRanking);
// Default (shadow) policy reports no additions; only augment counts route members.
CHECK_FALSE(options.countRouteMemberAdditions);
config.topologyVectorPolicy = SearchEngineConfig::TopologyVectorPolicy::Narrow;
CHECK_FALSE(makeTopologyRoutingOptions(config, options.routingMode, true, true)
.countRouteMemberAdditions);
config.topologyVectorPolicy = SearchEngineConfig::TopologyVectorPolicy::Augment;
const auto augment = makeTopologyRoutingOptions(config, options.routingMode, true, true);
CHECK(augment.countRouteMemberAdditions);
CHECK_FALSE(makeTopologyRoutingOptions(config, options.routingMode, true, false)
.countRouteMemberAdditions);
// Accounting does not change the allowed-set policy identity used by route calibration.
CHECK(topologyRoutingPolicyFingerprint("representation", augment) ==
topologyRoutingPolicyFingerprint("representation", options));
}
TEST_CASE("Topology route admission requires the protected-relation proof obligations",
"[search][topology][admission][catch2]") {
TopologyRouteAdmission admission;
CHECK_FALSE(admission.eligibleForNarrowing());
CHECK(admission.denialReason() == "protected_relation_coverage_unavailable");
admission.protectedRelationCoverage = TopologyProofObligationStatus::Satisfied;
admission.coordinateSpaceAlignment = TopologyProofObligationStatus::Satisfied;
admission.protectedFibersRepresented = TopologyProofObligationStatus::Satisfied;
admission.certificateSaturatesProtectedFibers = TopologyProofObligationStatus::Satisfied;
admission.routeRisk = TopologyProofObligationStatus::Satisfied;
admission.work = TopologyProofObligationStatus::Satisfied;
admission.coverMaterialization = TopologyProofObligationStatus::Satisfied;
admission.selectedCoverDocuments = 2;
admission.materializedCoverDocuments = 2;
CHECK(admission.eligibleForTrialNarrowing());
CHECK(admission.eligibleForNarrowing());
CHECK(admission.denialReason().empty());
admission.protectedRelationCoverage = TopologyProofObligationStatus::Violated;
CHECK_FALSE(admission.eligibleForTrialNarrowing());
CHECK_FALSE(admission.eligibleForNarrowing());
CHECK(admission.denialReason() == "protected_relation_coverage_violated");
admission.protectedRelationCoverage = TopologyProofObligationStatus::Satisfied;
admission.protectedFibersRepresented = TopologyProofObligationStatus::Unavailable;
CHECK(admission.denialReason() == "protected_fibers_representation_unavailable");
admission.protectedFibersRepresented = TopologyProofObligationStatus::Satisfied;
admission.certificateSaturatesProtectedFibers = TopologyProofObligationStatus::Violated;
CHECK(admission.denialReason() == "protected_fiber_saturation_violated");
}
TEST_CASE("Topology route certificate owns the theorem action boundary",
"[search][topology][certificate][catch2]") {
TopologyRouteCertificate certificate;
CHECK(certificate.action() == TopologyRouteAction::Global);
CHECK_FALSE(certificate.eligibleForTrialNarrowing());
certificate.allowedDocumentHashes.insert("document");
CHECK(certificate.action() == TopologyRouteAction::Augment);
auto& admission = certificate.admission;
admission.coordinateSpaceAlignment = TopologyProofObligationStatus::Satisfied;
admission.protectedRelationCoverage = TopologyProofObligationStatus::Satisfied;
admission.protectedFibersRepresented = TopologyProofObligationStatus::Satisfied;
admission.certificateSaturatesProtectedFibers = TopologyProofObligationStatus::Satisfied;
admission.routeRisk = TopologyProofObligationStatus::Satisfied;
admission.coverMaterialization = TopologyProofObligationStatus::Satisfied;
admission.selectedCoverDocuments = 1;
admission.materializedCoverDocuments = 1;
CHECK_FALSE(certificate.isConstructionBound());
CHECK_FALSE(certificate.eligibleForTrialNarrowing());
certificate.constructionFingerprint = "construction";
certificate.selectedCoverIds = {"cover"};
certificate.selectedProtectedRelationFiberIds = {"fiber"};
CHECK(certificate.isConstructionBound());
CHECK(certificate.eligibleForTrialNarrowing());
CHECK(certificate.action() == TopologyRouteAction::Augment);
admission.work = TopologyProofObligationStatus::Satisfied;
CHECK(certificate.action() == TopologyRouteAction::Narrow);
}
TEST_CASE("Topology routing session reports route confidence and seed coverage",
"[search][topology][catch2]") {
TopologySearchFixture fix;
seedTopologyDocuments(fix);
seedTwoClusterTopology(fix);
TopologyRoutingSessionRequest request;
request.query = "omega";
request.seedDocumentHashes = {"y1", "not-in-any-cluster"};
request.queryEmbedding = std::vector<float>{0.0F, 1.0F};
request.options.routingMode = SearchEngineConfig::TopologyRoutingMode::HybridAssist;
request.options.weakTier1Query = true;
request.options.maxClusters = 2;
request.options.maxDocs = 8;
request.options.minRouteScore = 0.0F;
const auto session = runTopologyRoutingSession(request, fix.repo, fix.kgStore);
REQUIRE(session.artifactAdmitted);
REQUIRE((session.acceptedRoutes >= 1));
CHECK((session.bestRouteScore > 0.0F));
CHECK((session.meanAcceptedRouteScore > 0.0F));
CHECK((session.meanAcceptedRouteScore <= session.bestRouteScore));
CHECK((session.seedCount == 2));
CHECK((session.seedsInRoutedClusters == 1));
}
TEST_CASE("Topology routing session excludes min-score-rejected routes from confidence aggregates",
"[search][topology][catch2]") {
TopologySearchFixture fix;
seedTopologyDocuments(fix);
seedTwoClusterTopology(fix);
TopologyRoutingSessionRequest request;
request.query = "omega";
request.seedDocumentHashes = {"y1"};
request.queryEmbedding = std::vector<float>{0.0F, 1.0F};
request.options.routingMode = SearchEngineConfig::TopologyRoutingMode::HybridAssist;
request.options.weakTier1Query = true;
request.options.maxClusters = 2;
request.options.maxDocs = 8;
request.options.minRouteScore = 1000000.0F;
const auto session = runTopologyRoutingSession(request, fix.repo, fix.kgStore);
CHECK((session.routesRejected >= 1));
CHECK((session.acceptedRoutes == 0));
CHECK((session.bestRouteScore == 0.0F));
CHECK((session.meanAcceptedRouteScore == 0.0F));
CHECK((session.seedsInRoutedClusters == 0));
CHECK((session.addedCandidates == 0));
}
TEST_CASE("Topology routing expands through query-independent medoids",
"[search][topology][catch2]") {
TopologySearchFixture fix;
seedTopologyDocuments(fix);
seedTwoClusterTopology(fix);
TopologyRoutingSessionRequest request;
request.query = "omega";
request.seedDocumentHashes = {"y1"};
request.queryEmbedding = std::vector<float>{0.0F, 1.0F};
request.options.routingMode = SearchEngineConfig::TopologyRoutingMode::HybridAssist;
request.options.weakTier1Query = true;
request.options.maxClusters = 2;
request.options.maxDocs = 16;
const auto session = runTopologyRoutingSession(request, fix.repo, fix.kgStore);
REQUIRE(session.artifactAdmitted);
CHECK((session.addedCandidates <= session.acceptedRoutes));
CHECK((session.routedCandidateHashes.size() <= session.acceptedRoutes));
}
TEST_CASE("Topology routing refuses hard narrowing without complete theorem evidence",
"[search][topology][narrowing][admission][catch2]") {
TopologySearchFixture fix;
seedTopologyDocuments(fix);
seedTwoClusterTopology(fix);
TopologyRoutingSessionRequest request;
request.query = "omega";
request.seedDocumentHashes = {"y2"};
request.queryEmbedding = std::vector<float>{0.0F, 1.0F};
request.options.routingMode = SearchEngineConfig::TopologyRoutingMode::HybridAssist;
request.options.weakTier1Query = true;
request.options.maxClusters = 1;
request.options.maxDocs = 1;
request.options.collectRouteMembership = true;
const auto session = runTopologyRoutingSession(request, fix.repo, fix.kgStore);
REQUIRE(session.artifactAdmitted);
REQUIRE(session.certificate.hasUsefulRoute());
CHECK_FALSE(session.certificate.admission.eligibleForNarrowing());
CHECK(session.certificate.admission.coverMaterialization ==
TopologyProofObligationStatus::Violated);
CHECK(session.certificate.admission.certificateSaturatesProtectedFibers ==
TopologyProofObligationStatus::Violated);
CHECK(session.certificate.admission.selectedCoverDocuments == 2U);
CHECK(session.certificate.admission.materializedCoverDocuments == 1U);
CHECK(session.certificate.admission.denialReason() == "cover_materialization_incomplete");
CHECK(session.routedCandidateHashes.empty());
CHECK(session.routedDocs == 1U);
CHECK(session.certificate.allowedDocumentHashes.size() == 1U);
CHECK(session.addedCandidates == 0U);
CHECK(session.addedCandidateHashes.empty());
CHECK(session.certificate.allowedDocumentHashes.contains("y2"));
CHECK_FALSE(session.certificate.allowedDocumentHashes.contains("y1"));
CHECK(session.candidateStructureEvidence.size() == 1U);
REQUIRE(session.candidateStructureEvidence.contains("y2"));
CHECK(session.candidateStructureEvidence.at("y2").scaleAgreement == Catch::Approx(0.0F));
request.options.maxDocs = 0;
const auto unbounded = runTopologyRoutingSession(request, fix.repo, fix.kgStore);
REQUIRE(unbounded.certificate.hasUsefulRoute());
CHECK_FALSE(unbounded.certificate.admission.eligibleForNarrowing());
CHECK(unbounded.certificate.admission.coverMaterialization ==
TopologyProofObligationStatus::Satisfied);
CHECK(unbounded.certificate.admission.certificateSaturatesProtectedFibers ==
TopologyProofObligationStatus::Satisfied);
CHECK(unbounded.certificate.admission.selectedCoverDocuments == 2U);
CHECK(unbounded.certificate.admission.materializedCoverDocuments == 2U);
CHECK(unbounded.certificate.admission.denialReason() ==
"protected_fibers_representation_unavailable");
CHECK(unbounded.certificate.allowedDocumentHashes.size() == 2U);
CHECK(unbounded.certificate.allowedDocumentHashes.contains("y1"));
CHECK(unbounded.certificate.allowedDocumentHashes.contains("y2"));
CHECK_FALSE(unbounded.certificate.constructionFingerprint.empty());
REQUIRE(unbounded.certificate.selectedCoverIds.size() == 1U);
REQUIRE(unbounded.certificate.selectedProtectedRelationFiberIds.size() == 1U);
CHECK(unbounded.routedCandidateHashes.contains("y1"));
}
TEST_CASE("Topology routing exposes selected multiscale structural evidence from the snapshot",
"[search][topology][evidence_pipeline][multiscale][catch2]") {
TopologySearchFixture fix;
seedTopologyDocuments(fix);
auto batch = buildMultiscaleOverlapTopologyBatch();
TopologyRoutingSessionRequest request;
request.query = "omega";
request.seedDocumentHashes = {"y1"};
request.queryEmbedding = std::vector<float>{0.0F, 1.0F};
request.options.routingMode = SearchEngineConfig::TopologyRoutingMode::HybridAssist;
request.options.weakTier1Query = true;
request.options.maxClusters = 2;
request.options.maxDocs = 1;
request.options.collectRouteMembership = true;
request.snapshotCache = std::make_shared<TopologyRoutingSnapshotCache>([batch]() mutable {
return Result<std::optional<yams::topology::TopologyArtifactBatch>>{
std::optional<yams::topology::TopologyArtifactBatch>{std::move(batch)}};
});
const auto session = runTopologyRoutingSession(request, fix.repo, fix.kgStore);
REQUIRE(session.artifactAdmitted);
REQUIRE(session.certificate.allowedDocumentHashGroups.size() == 2U);
CHECK(session.certificate.allowedDocumentHashGroups[0].contains("y1"));
CHECK(session.certificate.allowedDocumentHashGroups[1].contains("y1"));
REQUIRE(session.candidateStructureEvidence.contains("y1"));
const auto& evidence = session.candidateStructureEvidence.at("y1");
CHECK(evidence.scaleAgreement == Catch::Approx(1.0F));
CHECK(evidence.overlapSupport > 0.0F);
CHECK(evidence.persistenceSupport > 0.0F);
CHECK(evidence.cohesionSupport > 0.0F);
CHECK(evidence.bridgeSupport > 0.0F);
CHECK(evidence.densitySupport > 0.0F);
}
TEST_CASE("Topology snapshot cache validates once and reuses the admitted epoch",
"[search][topology][cache][catch2]") {
auto batch = buildTwoClusterTopologyBatch();
int loadCalls = 0;
TopologyRoutingSnapshotCache cache([&]() {
++loadCalls;
return Result<std::optional<yams::topology::TopologyArtifactBatch>>{
std::optional<yams::topology::TopologyArtifactBatch>{batch}};
});
auto first = cache.get(batch.topologyEpoch);
REQUIRE(first.has_value());
REQUIRE(first.value().snapshot);
CHECK_FALSE(first.value().cacheHit);
CHECK((first.value().snapshot->clustersById.size() == batch.clusters.size()));
CHECK((first.value().snapshot->membershipsByDocumentHash.size() == batch.memberships.size()));
auto second = cache.get(batch.topologyEpoch);
REQUIRE(second.has_value());
REQUIRE(second.value().snapshot);
CHECK(second.value().cacheHit);
CHECK((loadCalls == 1));
}
TEST_CASE("Topology snapshot cache upgrades an exact index when ANN is later required",
"[search][topology][cache][ann][catch2]") {
auto batch = buildTwoClusterTopologyBatch();
int loadCalls = 0;
TopologyRoutingSnapshotCache cache([&]() {
++loadCalls;
return Result<std::optional<yams::topology::TopologyArtifactBatch>>{
std::optional<yams::topology::TopologyArtifactBatch>{batch}};
});
const auto exact = cache.get(batch.topologyEpoch, false);
REQUIRE(exact.has_value());
REQUIRE(exact.value().snapshot);
CHECK_FALSE(exact.value().snapshot->denseAnnBuildAttempted);
CHECK_FALSE(exact.value().snapshot->sparseRouteIndex.centroidAnnIndex);
const auto upgraded = cache.get(batch.topologyEpoch, true);
REQUIRE(upgraded.has_value());
REQUIRE(upgraded.value().snapshot);
CHECK(upgraded.value().cacheHit);
CHECK(upgraded.value().snapshot->denseAnnBuildAttempted);
CHECK(upgraded.value().snapshot->sparseRouteIndex.centroidAnnIndex);
CHECK(loadCalls == 1);
}
TEST_CASE("Topology construction fingerprint excludes publication and routing representatives",
"[search][topology][cache][identity][catch2]") {
auto baseline = buildTwoClusterTopologyBatch();
auto experimental = baseline;
experimental.snapshotId = "different-publication";
experimental.generatedAtUnixSeconds += 100;
experimental.topologyEpoch += 1;
experimental.clusters.front().routingRepresentatives.push_back(
yams::topology::ClusterRoutingRepresentative{.documentHash = "x1",
.embedding = {1.0F, 0.0F}});
const auto baselineIdentity = topologyRoutingConstructionFingerprint(baseline);
const auto experimentalIdentity = topologyRoutingConstructionFingerprint(experimental);
REQUIRE(baselineIdentity.size() == 16);
CHECK(experimentalIdentity == baselineIdentity);
const auto baselineRepresentations = topologyRoutingRepresentationFingerprint(baseline);
CHECK(topologyRoutingRepresentationFingerprint(experimental) != baselineRepresentations);
auto options = TopologyRoutingOptions{};
const auto policyIdentity = topologyRoutingPolicyFingerprint(baselineRepresentations, options);
options.collectGraphDiagnostics = true;
CHECK(topologyRoutingPolicyFingerprint(baselineRepresentations, options) == policyIdentity);
options.sparseDenseAlpha = 0.25F;
CHECK(topologyRoutingPolicyFingerprint(baselineRepresentations, options) != policyIdentity);
options = TopologyRoutingOptions{};
options.graphNeighborReciprocalOnly = false;
CHECK(topologyRoutingPolicyFingerprint(baselineRepresentations, options) != policyIdentity);
experimental.embeddingSpaceIdentity = "test-space-v2";
CHECK(topologyRoutingConstructionFingerprint(experimental) != baselineIdentity);
experimental.embeddingSpaceIdentity = baseline.embeddingSpaceIdentity;
experimental.protectedRelationIdentity = "rewired-relation";
CHECK(topologyRoutingConstructionFingerprint(experimental) != baselineIdentity);
experimental.protectedRelationIdentity = baseline.protectedRelationIdentity;
experimental.clusters.front().memberDocumentHashes.push_back("new-member");
CHECK(topologyRoutingConstructionFingerprint(experimental) != baselineIdentity);
}
TEST_CASE("Topology routing refuses theorem admission across coordinate spaces",
"[search][topology][identity][admission][catch2]") {
TopologySearchFixture fix;
seedTopologyDocuments(fix);
auto batch = buildTwoClusterTopologyBatch();
const auto constructionFingerprint = topologyRoutingConstructionFingerprint(batch);
TopologyRoutingSessionRequest request;
request.query = "omega";
request.seedDocumentHashes = {"y2"};
request.queryEmbedding = std::vector<float>{0.0F, 1.0F};
request.queryEmbeddingSpaceIdentity = "different-space-v1";
request.options.routingMode = SearchEngineConfig::TopologyRoutingMode::HybridAssist;
request.options.weakTier1Query = true;
request.options.maxClusters = 1;
request.options.maxDocs = 0;
request.options.collectRouteMembership = true;
request.options.routeRiskCalibration.constructionFingerprint = constructionFingerprint;
request.options.routeRiskCalibration.routingPolicyFingerprint =
topologyRoutingPolicyFingerprint(topologyRoutingRepresentationFingerprint(batch),
request.options);
request.options.routeRiskCalibration.datasetIdentity = "fixture-held-out-v1";
request.options.routeRiskCalibration.calibrationQueries = 50;
request.options.routeRiskCalibration.protectedCandidates = 100;
request.options.routeRiskCalibration.missedProtectedCandidates = 0;
request.options.routeRiskCalibration.minCalibrationQueries = 30;
request.options.routeRiskCalibration.maxMissesPerThousand = 10;
request.options.routeRiskCalibration.minBoundaryMargin = 0.0F;
request.options.routeRiskCalibration.minSeedHits = 1;
request.snapshotCache = std::make_shared<TopologyRoutingSnapshotCache>([batch]() mutable {
return Result<std::optional<yams::topology::TopologyArtifactBatch>>{
std::optional<yams::topology::TopologyArtifactBatch>{std::move(batch)}};
});
const auto mismatched = runTopologyRoutingSession(request, fix.repo, fix.kgStore);
REQUIRE(mismatched.certificate.hasUsefulRoute());
CHECK(mismatched.certificate.admission.coordinateSpaceAlignment ==
TopologyProofObligationStatus::Violated);
CHECK(mismatched.certificate.admission.denialReason() == "coordinate_space_mismatch");
CHECK(mismatched.certificate.action() == TopologyRouteAction::Augment);
request.queryEmbeddingSpaceIdentity = "test-space-v1";
const auto aligned = runTopologyRoutingSession(request, fix.repo, fix.kgStore);
CHECK(aligned.certificate.admission.coordinateSpaceAlignment ==
TopologyProofObligationStatus::Satisfied);
CHECK(aligned.certificate.admission.routeRisk == TopologyProofObligationStatus::Satisfied);
// Counts from an older routing policy cannot certify a changed representative budget.
request.options.representativeLimit = 1;
const auto changedPolicy = runTopologyRoutingSession(request, fix.repo, fix.kgStore);
CHECK(changedPolicy.certificate.admission.routeRisk ==
TopologyProofObligationStatus::Unavailable);
}
TEST_CASE("Topology snapshot cache rejects duplicate cluster identifiers",
"[search][topology][cache][validation][catch2]") {
auto batch = buildTwoClusterTopologyBatch();
REQUIRE((batch.clusters.size() >= 2));
batch.clusters[1].clusterId = batch.clusters[0].clusterId;
TopologyRoutingSnapshotCache cache([batch]() mutable {
return Result<std::optional<yams::topology::TopologyArtifactBatch>>{
std::optional<yams::topology::TopologyArtifactBatch>{std::move(batch)}};
});
const auto result = cache.get();
REQUIRE_FALSE(result.has_value());
CHECK((result.error().message == "invalid_artifact:duplicate_cluster_id"));
}
TEST_CASE("Topology snapshot cache admits a materialized boundary spill",
"[search][topology][cache][validation][overlap][catch2]") {
auto batch = buildTwoClusterTopologyBatch();
REQUIRE(batch.clusters.size() == 2);
const auto primaryId = batch.memberships.front().clusterId;
auto secondary = std::ranges::find_if(
batch.clusters, [&](const auto& cluster) { return cluster.clusterId != primaryId; });
REQUIRE(secondary != batch.clusters.end());
secondary->memberDocumentHashes.push_back(batch.memberships.front().documentHash);
std::ranges::sort(secondary->memberDocumentHashes);
secondary->memberCount = secondary->memberDocumentHashes.size();
batch.memberships.front().overlapClusterIds = {secondary->clusterId};
TopologyRoutingSnapshotCache cache([batch]() mutable {
return Result<std::optional<yams::topology::TopologyArtifactBatch>>{
std::optional<yams::topology::TopologyArtifactBatch>{std::move(batch)}};
});
const auto result = cache.get();
REQUIRE(result.has_value());
REQUIRE(result.value().snapshot);
CHECK(result.value()
.snapshot->sparseRouteIndex.clustersByDocumentHash
.at(batch.memberships.front().documentHash)
.size() == 2);
}
TEST_CASE("mergeTopologySeedHashes keeps Tier-1 first and caps vector seeds",
"[unit][search][topology][graph_neighbors]") {
const auto merged =
mergeTopologySeedHashes({"t1", "t2", "t1"}, {"v1", "t2", "v2", "v3"}, /*maxVectorSeeds=*/2);
REQUIRE((merged.size() == 4));
CHECK((merged[0] == "t1"));
CHECK((merged[1] == "t2"));
CHECK((merged[2] == "v1"));
CHECK((merged[3] == "v2"));
const auto none = mergeTopologySeedHashes({"t1", "t2"}, {"v1", "v2"}, /*maxVectorSeeds=*/0);
REQUIRE((none.size() == 2));
CHECK((none[0] == "t1"));
CHECK((none[1] == "t2"));
const auto emptyVec = mergeTopologySeedHashes({"t1"}, {}, /*maxVectorSeeds=*/16);
REQUIRE((emptyVec.size() == 1));
CHECK((emptyVec[0] == "t1"));
}
TEST_CASE("rankTopologySeedEvidence keeps ranked lexical evidence and caps fanout",
"[unit][search][topology][weighted-seeds]") {
std::vector<ComponentResult> components{
ComponentResult{.documentHash = "rank-zero",
.score = 0.80F,
.source = ComponentResult::Source::Text,
.rank = 0},
ComponentResult{.documentHash = "late-high",
.score = 0.95F,
.source = ComponentResult::Source::Text,
.rank = 10},
ComponentResult{.documentHash = "early",
.score = 0.80F,
.source = ComponentResult::Source::Text,
.rank = 1},
ComponentResult{.documentHash = "late-high",
.score = 0.90F,
.source = ComponentResult::Source::SimeonText,
.rank = 2},
ComponentResult{.documentHash = "vector-only",
.score = 1.0F,
.source = ComponentResult::Source::Vector,
.rank = 1},
};
const auto evidence = rankTopologySeedEvidence(components, 3);
REQUIRE((evidence.size() == 3));
CHECK((evidence[0].documentHash == "rank-zero"));
CHECK((evidence[1].documentHash == "early"));
CHECK((evidence[2].documentHash == "late-high"));
CHECK((evidence[0].weight > evidence[1].weight));
}
TEST_CASE("rankTopologyVectorSeedEvidence preserves vector score and rank",
"[unit][search][topology][weighted-seeds]") {
std::vector<ComponentResult> components{
ComponentResult{.documentHash = "near",
.score = 0.90F,
.source = ComponentResult::Source::Vector,
.rank = 0},
ComponentResult{.documentHash = "far",
.score = 0.95F,
.source = ComponentResult::Source::Vector,
.rank = 10},
ComponentResult{.documentHash = "near",
.score = 0.80F,
.source = ComponentResult::Source::Vector,
.rank = 2},
};
const auto evidence = rankTopologyVectorSeedEvidence(components, 2);
REQUIRE(evidence.size() == 2);
CHECK(evidence[0].documentHash == "near");
CHECK(evidence[1].documentHash == "far");
CHECK(evidence[0].weight > evidence[1].weight);
}
TEST_CASE("selectTopologyRoutesForNarrowing adapts probes and abstains at an unsafe boundary",
"[unit][search][topology][adaptive-probe]") {
std::vector<yams::topology::ClusterRoute> routes{
yams::topology::ClusterRoute{.clusterId = "a", .routeScore = 1.00},
yams::topology::ClusterRoute{.clusterId = "b", .routeScore = 0.97},
yams::topology::ClusterRoute{.clusterId = "c", .routeScore = 0.80},
};
const auto safe = selectTopologyRoutesForNarrowing(routes, /*minClusters=*/1, /*maxClusters=*/3,
/*adaptiveScoreGap=*/0.05F,
/*minBoundaryMargin=*/0.10F);
REQUIRE((safe.routes.size() == 2));
CHECK((safe.routes[0].clusterId == "a"));
CHECK((safe.routes[1].clusterId == "b"));
CHECK((safe.boundaryScoreMargin == 0.17F));
CHECK_FALSE(safe.abstained);
const auto unsafe = selectTopologyRoutesForNarrowing(
routes, /*minClusters=*/1, /*maxClusters=*/2, /*adaptiveScoreGap=*/0.05F,
/*minBoundaryMargin=*/0.20F);
REQUIRE((unsafe.routes.size() == 2));
CHECK(unsafe.abstained);
}
TEST_CASE("legacy topology false preserves the hybrid product default",
"[unit][search][topology][config]") {
SearchEngineConfig config;
REQUIRE(config.topologyRoutingMode == SearchEngineConfig::TopologyRoutingMode::HybridAssist);
CHECK(resolveTopologyRoutingMode(config, false) ==
SearchEngineConfig::TopologyRoutingMode::HybridAssist);
CHECK(resolveTopologyRoutingMode(config, true) ==
SearchEngineConfig::TopologyRoutingMode::WeakQueryOnly);
config.topologyRoutingMode = SearchEngineConfig::TopologyRoutingMode::Disabled;
CHECK(resolveTopologyRoutingMode(config) == SearchEngineConfig::TopologyRoutingMode::Disabled);
}
TEST_CASE("fillTopologySkipReason preserves session reason and fills product defaults",
"[unit][search][topology]") {
using Mode = SearchEngineConfig::TopologyRoutingMode;
TopologyRoutingOptions options;
TopologyRoutingSessionResult session;
options.routingMode = Mode::HybridAssist;
std::string reason = "graph_medoid_neighbors";
fillTopologySkipReason(reason, options, /*hasStores=*/true, session);
CHECK((reason == "graph_medoid_neighbors"));
options.routingMode = Mode::Disabled;
reason.clear();
fillTopologySkipReason(reason, options, /*hasStores=*/true, session);
CHECK((reason == "disabled"));
options.routingMode = Mode::HybridAssist;
session.loadSucceeded = true;
session.routedClusters = 2;
reason.clear();
fillTopologySkipReason(reason, options, /*hasStores=*/true, session);
CHECK((reason == "no_added_candidates"));
}
TEST_CASE("rankGraphNeighborCandidates ranks multi-seed hits and filters score/reciprocal",
"[unit][search][topology][graph_neighbors]") {
std::unordered_map<std::string, std::vector<std::tuple<std::string, float, bool>>> adj;
adj["s1"] = {
{"n_shared", 0.9F, true},
{"n_weak", 0.1F, true},
{"n_oneway", 0.95F, false},
};
adj["s2"] = {
{"n_shared", 0.85F, true},
// single-seed only — loses to multi-seed n_shared even with higher edge score
{"n_strong", 0.99F, true},
};
auto ranked = rankGraphNeighborCandidates(adj, {"s1", "s2"}, /*maxDocs=*/10,
/*minScore=*/0.25F, /*reciprocalOnly=*/true);
REQUIRE_FALSE(ranked.empty());
// Multi-seed hits beat single-seed even when the single edge score is higher.
CHECK((ranked.front() == "n_shared"));
// Weak edge below minScore and one-way reciprocal-only drop out.
CHECK((std::find(ranked.begin(), ranked.end(), "n_weak") == ranked.end()));
CHECK((std::find(ranked.begin(), ranked.end(), "n_oneway") == ranked.end()));
CHECK((std::find(ranked.begin(), ranked.end(), "n_strong") != ranked.end()));
auto limited = rankGraphNeighborCandidates(adj, {"s1", "s2"}, /*maxDocs=*/1, 0.25F, true);
REQUIRE((limited.size() == 1));
CHECK((limited.front() == "n_shared"));
// Reverse-only / one-way edges still rank when reciprocalOnly=false.
std::unordered_map<std::string, std::vector<std::tuple<std::string, float, bool>>> oneWay;
oneWay["s1"] = {{"n_oneway", 0.9F, false}};
auto oneWayRanked =
rankGraphNeighborCandidates(oneWay, {"s1"}, 5, 0.25F, /*reciprocalOnly=*/false);
REQUIRE((oneWayRanked.size() == 1));
CHECK((oneWayRanked.front() == "n_oneway"));
auto oneWayStrict =
rankGraphNeighborCandidates(oneWay, {"s1"}, 5, 0.25F, /*reciprocalOnly=*/true);
CHECK(oneWayStrict.empty());
}
TEST_CASE("rankGraphNeighborCandidates can weight support by query seed evidence",
"[unit][search][topology][graph_neighbors][weighted-seeds]") {
std::unordered_map<std::string, std::vector<std::tuple<std::string, float, bool>>> adjacency;
adjacency["low-1"] = {{"popular", 0.9F, true}};
adjacency["low-2"] = {{"popular", 0.9F, true}};
adjacency["high"] = {{"focused", 0.9F, true}};
const std::vector<std::string> hashes{"low-1", "low-2", "high"};
const std::vector<yams::topology::WeightedDocumentSeed> evidence{
{.documentHash = "low-1", .weight = 0.1F},
{.documentHash = "low-2", .weight = 0.1F},
{.documentHash = "high", .weight = 1.0F},
};
const auto unweighted = rankGraphNeighborCandidates(adjacency, hashes, /*maxDocs=*/2,
/*minScore=*/0.0F, /*reciprocalOnly=*/true);
REQUIRE(unweighted.size() == 2);
CHECK(unweighted.front() == "popular");
const auto weighted =
rankGraphNeighborCandidates(adjacency, hashes, /*maxDocs=*/2, /*minScore=*/0.0F,
/*reciprocalOnly=*/true, evidence);
REQUIRE(weighted.size() == 2);
CHECK(weighted.front() == "focused");
}
TEST_CASE("SearchEngine topology merge keeps graph-vector candidates behind baseline anchors",
"[search][topology][graph-vector][characterization][catch2]") {
using yams::search::detail::AuxiliaryVectorCandidateBatch;
SearchEngineConfig config;
config.vectorMaxResults = 2;
config.graphExpansionVectorPenalty = 0.5F;
config.graphVectorRequireCorroboration = true;
config.graphVectorRequireTextAnchoring = true;
config.graphVectorRequireBaselineTextAnchoring = true;
std::vector<ComponentResult> components{
{.documentHash = "baseline-anchor", .score = 0.9F, .source = ComponentResult::Source::Text},
{.documentHash = "graph-anchor",
.score = 0.8F,
.source = ComponentResult::Source::GraphText},
};
std::vector<AuxiliaryVectorCandidateBatch> graphTerms{
{.query = "related",
.weight = 1.0F,
.candidates = {{.documentHash = "baseline-anchor", .score = 1.0F},
{.documentHash = "graph-anchor", .score = 1.0F},
{.documentHash = "unanchored", .score = 1.0F}}},
};
const auto merged = yams::search::detail::mergeAuxiliaryVectorCandidates(
std::move(components), {}, std::move(graphTerms), config);
CHECK(merged.stats.graphVectorRawHitCount == 3U);
CHECK(merged.stats.graphVectorAddedNewCount == 1U);
CHECK(merged.stats.graphVectorBlockedUncorroboratedCount == 1U);
CHECK(merged.stats.graphVectorBlockedMissingBaselineTextAnchorCount == 1U);
REQUIRE(merged.components.size() == 3U);
CHECK(merged.components.back().documentHash == "baseline-anchor");
CHECK(merged.components.back().source == ComponentResult::Source::GraphVector);
CHECK(merged.components.back().score == Catch::Approx(0.5F));
}
TEST_CASE("Graph-neighbor trace separates stored relation from the selected cap",
"[unit][search][topology][graph_neighbors][trace]") {
TopologySearchFixture fix;
fix.addDocument("seed", "seed", {1.0F, 0.0F});
fix.addDocument("near", "near", {0.9F, 0.1F});
fix.addDocument("far", "far", {0.8F, 0.2F});
const auto nodeIds = fix.kgStore->upsertNodes({
KGNode{.nodeKey = "doc:seed", .type = "document"},
KGNode{.nodeKey = "doc:near", .type = "document"},
KGNode{.nodeKey = "doc:far", .type = "document"},
});
REQUIRE(nodeIds.has_value());
REQUIRE(nodeIds.value().size() == 3);
REQUIRE(fix.kgStore
->addEdge(KGEdge{.srcNodeId = nodeIds.value()[0],
.dstNodeId = nodeIds.value()[1],
.relation = "semantic_neighbor",
.weight = 0.9F})
.has_value());
REQUIRE(fix.kgStore
->addEdge(KGEdge{.srcNodeId = nodeIds.value()[0],
.dstNodeId = nodeIds.value()[2],
.relation = "semantic_neighbor",
.weight = 0.8F})
.has_value());
TopologyRoutingSessionRequest request;
request.seedDocumentHashes = {"seed"};
request.options.routingMode = SearchEngineConfig::TopologyRoutingMode::HybridAssist;
request.options.expansionSource = SearchEngineConfig::TopologyExpansionSource::GraphNeighbors;
request.options.maxDocs = 1;
request.options.collectRouteMembership = true;
request.options.collectGraphDiagnostics = true;
request.options.graphNeighborMinScore = 0.0F;
request.options.graphNeighborReciprocalOnly = false;
const auto result = runTopologyRoutingSession(request, fix.repo, fix.kgStore);
REQUIRE(result.graphNeighborTrace.collected);
CHECK(result.graphNeighborTrace.edgeFetchLimit == 64);
CHECK(result.graphNeighborTrace.fetchTruncatedSeedCount == 0);
CHECK(result.graphNeighborTrace.seedDocumentIds == std::vector<std::string>{"seed"});
CHECK(result.graphNeighborTrace.relationCandidateCount == 2);
CHECK(result.graphNeighborTrace.relationCandidateDocumentIds ==
std::vector<std::string>{"near", "far"});
CHECK(result.graphNeighborTrace.fetchedCandidateCount == 2);
CHECK(result.graphNeighborTrace.fetchedCandidateDocumentIds ==
std::vector<std::string>{"near", "far"});
CHECK(result.graphNeighborTrace.eligibleCandidateCount == 2);
CHECK(result.graphNeighborTrace.eligibleCandidateDocumentIds ==
std::vector<std::string>{"near", "far"});
REQUIRE(result.routedCandidateDocIds.size() == 1);
CHECK(result.routedCandidateDocIds.front() == "near");
}
TEST_CASE("Topology cluster hydration failure discards partial admission",
"[unit][search][topology][hydration]") {
TopologySearchFixture fix;
auto counting = std::make_shared<HashLookupCountingRepository>(*fix.pool);
fix.repo = counting;
seedTopologyDocuments(fix);
const auto batch = buildTwoClusterTopologyBatch();
TopologyRoutingSessionRequest request;
request.seedDocumentHashes = {"x1", "y1"};
request.options.routingMode = SearchEngineConfig::TopologyRoutingMode::HybridAssist;
request.options.expansionSource = SearchEngineConfig::TopologyExpansionSource::Clusters;
request.options.maxClusters = 2;
request.options.maxDocs = 8;
request.options.minRouteScore = 0.0F;
request.snapshotCache = std::make_shared<TopologyRoutingSnapshotCache>(
[batch] { return Result<std::optional<yams::topology::TopologyArtifactBatch>>{batch}; });
counting->batchLookups = 0;
SECTION("all cluster lookups succeed") {
const auto result = runTopologyRoutingSession(request, fix.repo, fix.kgStore);
CHECK(result.applied);
// Non-membership expansion materializes one medoid per selected cluster.
CHECK(result.addedCandidateHashes.size() == 2);
CHECK(counting->batchLookups.load() == 2);
}
SECTION("second cluster lookup fails after the first was admitted") {
counting->failBatchAt = 2;
const auto result = runTopologyRoutingSession(request, fix.repo, fix.kgStore);
CHECK(counting->batchLookups.load() == 2);
CHECK_FALSE(result.applied);
CHECK(result.staleCandidates == 0);
CHECK(result.skipReason.starts_with("metadata_lookup_failed"));
CHECK(result.certificate.allowedDocumentHashes.empty());
CHECK(result.routedCandidateHashes.empty());
CHECK(result.addedCandidateHashes.empty());
CHECK(result.addedCandidates == 0);
}
}
TEST_CASE("Topology routing hydrates ranked candidates and trace stages in batches",
"[unit][search][topology][graph_neighbors][hydration]") {
TopologySearchFixture fix;
auto counting = std::make_shared<HashLookupCountingRepository>(*fix.pool);
fix.repo = counting;
fix.addDocument("seed", "seed", {1.0F, 0.0F});
std::vector<KGNode> nodes{KGNode{.nodeKey = "doc:seed", .type = "document"}};
for (int i = 0; i < 8; ++i) {
const auto hash = "near" + std::to_string(i);
fix.addDocument(hash, hash, {0.9F, 0.1F});
nodes.push_back(KGNode{.nodeKey = "doc:" + hash, .type = "document"});
}
const auto nodeIds = fix.kgStore->upsertNodes(nodes);
REQUIRE(nodeIds.has_value());
REQUIRE(nodeIds.value().size() == 9);
for (std::size_t i = 1; i < nodeIds.value().size(); ++i) {
REQUIRE(fix.kgStore
->addEdge(KGEdge{.srcNodeId = nodeIds.value()[0],
.dstNodeId = nodeIds.value()[i],
.relation = "semantic_neighbor",
.weight = 0.9F})
.has_value());
}
TopologyRoutingSessionRequest request;
request.seedDocumentHashes = {"seed"};
request.options.routingMode = SearchEngineConfig::TopologyRoutingMode::HybridAssist;
request.options.expansionSource = SearchEngineConfig::TopologyExpansionSource::GraphNeighbors;
request.options.maxDocs = 8;
request.options.collectRouteMembership = true;
request.options.collectGraphDiagnostics = true;
request.options.graphNeighborMinScore = 0.0F;
request.options.graphNeighborReciprocalOnly = false;
counting->singleLookups = 0;
counting->batchLookups = 0;
SECTION("successful hydration remains batched") {
const auto result = runTopologyRoutingSession(request, fix.repo, fix.kgStore);
REQUIRE(result.graphNeighborTrace.collected);
CHECK(result.graphNeighborTrace.relationCandidateCount == 8);
REQUIRE(result.routedCandidateDocIds.size() == 8);
CHECK(counting->batchLookups.load() >= 1);
CHECK(counting->singleLookups.load() == 0);
}
SECTION("a diagnostic batch failure retains per-document fallback") {
counting->failBatchAt = 1;
const auto result = runTopologyRoutingSession(request, fix.repo, fix.kgStore);
CHECK(result.applied);
CHECK(result.staleCandidates == 0);
CHECK(result.graphNeighborTrace.eligibleUnresolvedCount == 0);
CHECK(result.graphNeighborTrace.eligibleCandidateDocumentIds.size() == 8);
CHECK(counting->singleLookups.load() > 0);
}
SECTION("metadata failure is not evidence of stale documents") {
counting->failBatchLookups = true;
const auto result = runTopologyRoutingSession(request, fix.repo, fix.kgStore);
CHECK_FALSE(result.applied);
CHECK(result.staleCandidates == 0);
CHECK(result.skipReason.starts_with("metadata_lookup_failed"));
CHECK(result.certificate.allowedDocumentHashes.empty());
CHECK(result.addedCandidateHashes.empty());
}
}
namespace {
std::vector<std::string> rankedHashes(const SearchResponse& response) {
std::vector<std::string> hashes;
for (const auto& result : response.results) {
hashes.push_back(result.document.sha256Hash);
}
return hashes;
}
} // namespace
TEST_CASE("SearchEngine topology warm-up serves the first routed query from the cache",
"[search][topology][prewarm][catch2]") {
TopologySearchFixture fix;
seedTopologyDocuments(fix);
seedTwoClusterTopology(fix);
auto generator = makeFixedGenerator({0.0F, 1.0F});
const SearchEngineConfig config = topologyRoutingTestConfig(true);
SearchExecutionContext context = defaultSearchExecutionContext();
context.freshness.lexicalReady = true;
context.freshness.vectorReady = true;
context.freshness.kgReady = true;
SearchExecutionContextGuard contextGuard(context);
SearchParams params;
params.limit = 4;
SearchEngine cold(fix.repo, fix.vectorDb, generator, fix.kgStore, config);
auto coldResponse = cold.searchWithResponse("zz", params);
REQUIRE(coldResponse.has_value());
REQUIRE((coldResponse.value().debugStats.at("topology_weak_query_load_succeeded") == "1"));
CHECK((coldResponse.value().debugStats.at("topology_snapshot_cache_hit") == "0"));
SearchEngine warmed(fix.repo, fix.vectorDb, generator, fix.kgStore, config);
auto prewarm = warmed.prewarmTopologyRouting();
REQUIRE(prewarm.has_value());
CHECK(prewarm.value());
auto warmResponse = warmed.searchWithResponse("zz", params);
REQUIRE(warmResponse.has_value());
// The first routed query reads the warmed snapshot, and warming changes no result.
const auto& debug = warmResponse.value().debugStats;
CHECK((debug.at("topology_weak_query_load_succeeded") == "1"));
CHECK((debug.at("topology_snapshot_cache_hit") == "1"));
CHECK(rankedHashes(warmResponse.value()) == rankedHashes(coldResponse.value()));
}
TEST_CASE("SearchEngine topology warm-up does nothing when routing is disabled",
"[search][topology][prewarm][catch2]") {
TopologySearchFixture fix;
seedTopologyDocuments(fix);
seedTwoClusterTopology(fix);
auto generator = makeFixedGenerator({0.0F, 1.0F});
SearchEngine engine(fix.repo, fix.vectorDb, generator, fix.kgStore,
topologyRoutingTestConfig(false));
auto prewarm = engine.prewarmTopologyRouting();
REQUIRE(prewarm.has_value());
CHECK_FALSE(prewarm.value());
}
TEST_CASE("SearchEngine topology warm-up reports no snapshot before one is published",
"[search][topology][prewarm][catch2]") {
TopologySearchFixture fix;
seedTopologyDocuments(fix);
auto generator = makeFixedGenerator({0.0F, 1.0F});
SearchEngine engine(fix.repo, fix.vectorDb, generator, fix.kgStore,
topologyRoutingTestConfig(true));
auto prewarm = engine.prewarmTopologyRouting();
REQUIRE(prewarm.has_value());
CHECK_FALSE(prewarm.value());
}