Semantic-neighbor maintenance aborted the daemon during startup: std::clamp<float> asserted '!(__hi < __lo)' because the adaptive cosine threshold could exceed 1.0 for a legal pair of identical vectors. The same work was also admitted while the daemon was still initializing, so the 300-second starvation guard forced a pass before readiness. + semanticInverseNorm/semanticCosineSimilarity/semanticExplicitThreshold in the existing embed policy header, shared by the streamed and corpus branches; non-finite vectors or scores produce no neighbor and legal scores are bounded before ranking and threshold selection + startupMaintenanceEligible() requires ServiceManagerState::Ready, not stopping and idle, and is evaluated before ResourceGovernor::admitDeferrable(), so an elapsed deferral deadline can no longer manufacture missing readiness + StartupPhaseTracker (two atomics, no mutex) plus a warn-level init-waiter line reporting state, phase and phase_elapsed_ms instead of a debug-level state number; a 53-minute startup previously logged nothing actionable + regression through the public backfill and corpus-rebuild APIs using a 64-D vector whose old float inverse norm rounds its self-cosine above 1 - removed the duplicated inverse-norm and cosine lambdas from EmbeddingService - renamed __test_setModelProvider/__test_setKgStore to testing_* because they used reserved identifiers, across 11 files - re-anchored 13 pre-existing raw-environment allowlist entries to their new line numbers; no new exception was added Startup is still blocked by corpus-sized memory-sync scans; this change stops the abort and names the blocking phase, it does not make startup fast. Signed-off-by: trvon <git@trevon.dev>
9549 lines
468 KiB
C++
9549 lines
468 KiB
C++
/*
|
|
retrieval_quality_bench.cpp - RAG Retrieval Quality Benchmark
|
|
|
|
Uses daemon-based ingestion to properly test retrieval quality with IR metrics.
|
|
|
|
Usage:
|
|
YAMS_TEST_SAFE_SINGLE_INSTANCE=1 ./builddir/tests/benchmarks/retrieval_quality_bench
|
|
|
|
Environment variables:
|
|
YAMS_TEST_SAFE_SINGLE_INSTANCE=1 - Required to prevent GlobalIOContext reset
|
|
YAMS_BENCH_CORPUS_SIZE=N - Number of documents to generate (default: 50)
|
|
YAMS_BENCH_NUM_QUERIES=N - Number of test queries (default: 10)
|
|
YAMS_BENCH_TOPK=N - Retrieve top K results (default: 10)
|
|
YAMS_BENCH_DATASET=<name> - Use synthetic, BEIR, or local-manifest dataset
|
|
YAMS_BENCH_DATASET_PATH=... - Path to BEIR directory or benchmark_manifest.json root
|
|
YAMS_BENCH_EMBED_BACKEND=simeon|onnx
|
|
Embedding backend (default: simeon)
|
|
YAMS_BENCH_ENABLE_GLINT=1 - Opt into Glint/GLiNER entity extraction on simeon runs
|
|
YAMS_BENCH_ENABLE_ONNX_RERANK=1 - Opt into ONNX reranker on simeon runs
|
|
YAMS_BENCH_TOPOLOGY_MODE=disabled|weak_query_only|hybrid_assist
|
|
YAMS_BENCH_TOPOLOGY_EXPANSION=clusters|graph_neighbors
|
|
Cluster partition vs pure semantic_neighbor expansion
|
|
YAMS_BENCH_TOPOLOGY_ENGINE=connected|louvain|kmeans
|
|
Write topology engine into isolated config
|
|
YAMS_BENCH_TOPOLOGY_REPRESENTATIVE_RULE=degree|medoid
|
|
Write topology.representative_rule into isolated config
|
|
YAMS_BENCH_TOPOLOGY_BQ_ROTATION=none|fwht
|
|
Write search.topology.bq_rotation into isolated config
|
|
YAMS_BENCH_TOPOLOGY_ROUTE_SCORING=current|size_weighted|seed_coverage
|
|
Topology route scoring policy
|
|
YAMS_BENCH_TOPOLOGY_SPARSE_DENSE_ALPHA=0..1
|
|
Sparse seed overlap vs dense centroid blend
|
|
YAMS_BENCH_TOPOLOGY_MIN_ROUTE_SCORE=N.N
|
|
Reject routed clusters below score
|
|
YAMS_BENCH_TOPOLOGY_MEDOID_ONLY_EXPANSION=1
|
|
Expand only route medoids, not full clusters
|
|
YAMS_BENCH_TOPOLOGY_MIN_CLUSTERS=N Minimum adaptive routed-cluster prefix
|
|
YAMS_BENCH_TOPOLOGY_MAX_SEED_DOCUMENTS=N
|
|
Cap ranked lexical routing evidence
|
|
YAMS_BENCH_TOPOLOGY_ROAD_HOPS=N Road beam hops for route_only/augment (0 = off)
|
|
YAMS_BENCH_TOPOLOGY_ROAD_BEAM_WIDTH=N Clusters kept per road hop
|
|
YAMS_BENCH_TOPOLOGY_ROAD_WALK_WINDOW=N Positions walked around each routed hit along the
|
|
cluster walk order (route_only/augment; 0 = off)
|
|
YAMS_BENCH_TOPOLOGY_ROAD_MAX_PER_CLUSTER=N
|
|
Roads kept per cluster at topology build (0 = none)
|
|
YAMS_BENCH_TOPOLOGY_ROAD_ORDER=none|time|session|similarity_chain
|
|
Cluster walk order stored at topology build
|
|
YAMS_BENCH_TOPOLOGY_ADAPTIVE_PROBE_SCORE_GAP=N.N
|
|
Grow route prefix while scores remain near the best
|
|
YAMS_BENCH_TOPOLOGY_NARROW_MIN_BOUNDARY_MARGIN=N.N
|
|
Abstain when selected/excluded route margin is smaller
|
|
YAMS_BENCH_TOPOLOGY_CLUSTER_OUTPUT=... Export evaluation-only cluster memberships as JSON
|
|
YAMS_BENCH_TOPOLOGY_SOURCE=vector|fts5|keyphrase|segment_keyphrase|kg|gliner|header|theme
|
|
Benchmark semantic_neighbor seeding source for topology
|
|
YAMS_SEARCH_ENABLE_TOPOLOGY_WEAK_ROUTING=1 - Legacy opt-in topology weak-query routing
|
|
YAMS_SEARCH_TOPOLOGY_MAX_CLUSTERS=N - Cap routed topology clusters (default: 2)
|
|
YAMS_SEARCH_TOPOLOGY_MAX_DOCS=N - Cap routed topology docs (default: 64)
|
|
YAMS_SEARCH_TOPOLOGY_GRAPH_VECTOR_SEED_PROBE=N
|
|
GraphNeighbors seed ANN k (0=off/product default; opt-in
|
|
e.g. 16)
|
|
|
|
Tuning for faster ingestion (recommended for large corpora):
|
|
YAMS_POST_EMBED_CONCURRENT=12 - Parallel embedding workers (default: 4-8)
|
|
YAMS_POST_EXTRACTION_CONCURRENT=12- Parallel content extraction (default: 4)
|
|
YAMS_POST_KG_CONCURRENT=12 - Parallel KG extraction (default: 4)
|
|
YAMS_POST_INGEST_BATCH_SIZE=24 - Documents per batch (default: 8, adaptive)
|
|
YAMS_EMBED_CHANNEL_CAPACITY=16384 - Embedding queue capacity (default: 8192)
|
|
YAMS_DB_POOL_MAX=48 - Database connection pool (default: 20)
|
|
|
|
Supported BEIR datasets (download from
|
|
https://public.ukp.informatik.tu-darmstadt.de/thakur/BEIR/datasets/): scifact - Scientific
|
|
fact verification (300 queries, 5K docs) cqadupstack - Community Q&A including programming (13K
|
|
queries, 457K docs) nfcorpus - NutritionFacts corpus (323 queries, 3.6K docs) arguana -
|
|
Argument retrieval (1.4K queries, 8.7K docs) scidocs - Scientific document similarity (1K
|
|
queries, 25K docs) fiqa - Financial question answering (648 queries, 57K docs) quora -
|
|
Duplicate question retrieval (10K queries, 523K docs) hotpotqa - Multi-hop question answering
|
|
(7.4K queries, 5.2M docs) fever - Fact verification (6.7K queries, 5.4M docs)
|
|
climate-fever - Climate claim verification (1.5K queries, 5.4M docs)
|
|
dbpedia-entity- Entity retrieval (400 queries, 4.6M docs)
|
|
trec-covid - COVID-19 retrieval (50 queries, 171K docs)
|
|
touche-2020 - Argument retrieval (49 queries, 382K docs)
|
|
|
|
Example (code-related benchmark):
|
|
mkdir -p ~/.cache/yams/benchmarks && cd ~/.cache/yams/benchmarks
|
|
curl -L -o cqadupstack.zip
|
|
https://public.ukp.informatik.tu-darmstadt.de/thakur/BEIR/datasets/cqadupstack.zip unzip
|
|
cqadupstack.zip YAMS_TEST_SAFE_SINGLE_INSTANCE=1 YAMS_BENCH_DATASET=cqadupstack
|
|
./builddir/tests/benchmarks/retrieval_quality_bench
|
|
|
|
Example (repo fixture / local manifest):
|
|
YAMS_TEST_SAFE_SINGLE_INSTANCE=1 \
|
|
YAMS_BENCH_DATASET=local-manifest \
|
|
YAMS_BENCH_DATASET_PATH=tests/benchmarks/data/retrieval_manifest_mini \
|
|
./builddir/tests/benchmarks/retrieval_quality_bench --benchmark_filter=BM_RetrievalQuality
|
|
*/
|
|
|
|
#include <sqlite3.h>
|
|
#include <nlohmann/json.hpp>
|
|
#include <spdlog/spdlog.h>
|
|
#include <benchmark/benchmark.h>
|
|
|
|
#include "retrieval_benchmark_support.h"
|
|
#include "tests/common/test_helpers_catch2.h"
|
|
#include "tests/integration/daemon/test_daemon_harness.h"
|
|
#include <yams/cli/cli_sync.h>
|
|
#include <yams/cli/search_runner.h>
|
|
#include <yams/common/fs_utils.h>
|
|
#include <yams/crypto/hasher.h>
|
|
#include <yams/daemon/client/asio_connection_pool.h>
|
|
#include <yams/daemon/client/daemon_client.h>
|
|
#include <yams/daemon/components/ServiceManager.h>
|
|
#include <yams/daemon/resource/model_provider.h>
|
|
#include <yams/daemon/resource/plugin_trust.h>
|
|
#include <yams/search/internal_benchmark.h>
|
|
#include <yams/search/topology_routing_session.h>
|
|
#include <yams/topology/topology_metadata_store.h>
|
|
#include <yams/vector/vector_database.h>
|
|
|
|
#include <algorithm>
|
|
#include <cctype>
|
|
#include <chrono>
|
|
#include <cmath>
|
|
#include <cstdlib>
|
|
#include <cstring>
|
|
#include <filesystem>
|
|
#include <fstream>
|
|
#include <iomanip>
|
|
#include <iostream>
|
|
#include <map>
|
|
#include <memory>
|
|
#include <numeric>
|
|
#include <optional>
|
|
#include <random>
|
|
#include <set>
|
|
#include <sstream>
|
|
#include <string>
|
|
#include <string_view>
|
|
#include <thread>
|
|
#include <unordered_map>
|
|
#include <unordered_set>
|
|
#include <vector>
|
|
#include <yams/compat/unistd.h>
|
|
|
|
namespace fs = std::filesystem;
|
|
using namespace std::chrono_literals;
|
|
using namespace yams;
|
|
using json = nlohmann::json;
|
|
|
|
class BenchmarkMockModelProvider final : public yams::daemon::IModelProvider {
|
|
public:
|
|
explicit BenchmarkMockModelProvider(std::size_t dim) : dim_(dim) {}
|
|
|
|
void setProgressCallback(std::function<void(const yams::daemon::ModelLoadEvent&)> cb) override {
|
|
progress_ = std::move(cb);
|
|
}
|
|
|
|
Result<std::vector<float>> generateEmbedding(const std::string& text) override {
|
|
return generateEmbeddingFor(defaultModelName_, text);
|
|
}
|
|
|
|
Result<std::vector<std::vector<float>>>
|
|
generateBatchEmbeddings(const std::vector<std::string>& texts) override {
|
|
return generateBatchEmbeddingsFor(defaultModelName_, texts);
|
|
}
|
|
|
|
Result<std::vector<float>> generateEmbeddingFor(const std::string& modelName,
|
|
const std::string& text) override {
|
|
if (!isModelLoaded(modelName)) {
|
|
return Error{ErrorCode::NotFound, "model not loaded"};
|
|
}
|
|
std::vector<float> embedding(dim_, 0.0f);
|
|
std::hash<std::string> hasher;
|
|
const auto seed = hasher(text);
|
|
for (std::size_t i = 0; i < embedding.size(); ++i) {
|
|
embedding[i] = static_cast<float>((seed ^ (i * 131)) % 1000) / 1000.0f;
|
|
}
|
|
return embedding;
|
|
}
|
|
|
|
Result<std::vector<std::vector<float>>>
|
|
generateBatchEmbeddingsFor(const std::string& modelName,
|
|
const std::vector<std::string>& texts) override {
|
|
if (!isModelLoaded(modelName)) {
|
|
return Error{ErrorCode::NotFound, "model not loaded"};
|
|
}
|
|
std::vector<std::vector<float>> batch;
|
|
batch.reserve(texts.size());
|
|
for (const auto& text : texts) {
|
|
auto embedding = generateEmbeddingFor(modelName, text);
|
|
if (!embedding) {
|
|
return embedding.error();
|
|
}
|
|
batch.push_back(std::move(embedding.value()));
|
|
}
|
|
return batch;
|
|
}
|
|
|
|
Result<void> loadModel(const std::string& modelName) override {
|
|
if (progress_) {
|
|
yams::daemon::ModelLoadEvent ev;
|
|
ev.modelName = modelName;
|
|
ev.phase = "loading";
|
|
ev.message = "benchmark mock loading";
|
|
progress_(ev);
|
|
}
|
|
loadedModels_.insert(modelName);
|
|
defaultModelName_ = modelName;
|
|
if (progress_) {
|
|
yams::daemon::ModelLoadEvent ev;
|
|
ev.modelName = modelName;
|
|
ev.phase = "completed";
|
|
ev.message = "benchmark mock ready";
|
|
progress_(ev);
|
|
}
|
|
return Result<void>();
|
|
}
|
|
|
|
Result<void> unloadModel(const std::string& modelName) override {
|
|
loadedModels_.erase(modelName);
|
|
return Result<void>();
|
|
}
|
|
|
|
bool isModelLoaded(const std::string& modelName) const override {
|
|
return loadedModels_.contains(modelName);
|
|
}
|
|
|
|
std::vector<std::string> getLoadedModels() const override {
|
|
return std::vector<std::string>(loadedModels_.begin(), loadedModels_.end());
|
|
}
|
|
|
|
size_t getLoadedModelCount() const override { return loadedModels_.size(); }
|
|
|
|
Result<yams::daemon::ModelInfo> getModelInfo(const std::string& modelName) const override {
|
|
if (!isModelLoaded(modelName)) {
|
|
return Error{ErrorCode::NotFound, "model not loaded"};
|
|
}
|
|
yams::daemon::ModelInfo info;
|
|
info.name = modelName;
|
|
info.embeddingDim = dim_;
|
|
return info;
|
|
}
|
|
|
|
size_t getEmbeddingDim(const std::string&) const override { return dim_; }
|
|
std::shared_ptr<yams::vector::EmbeddingGenerator>
|
|
getEmbeddingGenerator(const std::string& = "") override {
|
|
return nullptr;
|
|
}
|
|
std::string getProviderName() const override { return "BenchmarkMockProvider"; }
|
|
std::string getProviderVersion() const override { return "1.0"; }
|
|
bool isAvailable() const override { return true; }
|
|
size_t getMemoryUsage() const override { return 0; }
|
|
void releaseUnusedResources() override {}
|
|
void shutdown() override { loadedModels_.clear(); }
|
|
|
|
private:
|
|
std::size_t dim_;
|
|
std::string defaultModelName_;
|
|
std::unordered_set<std::string> loadedModels_;
|
|
std::function<void(const yams::daemon::ModelLoadEvent&)> progress_;
|
|
};
|
|
|
|
template <typename T, typename Rep, typename Period>
|
|
static Result<T> benchRunSync(boost::asio::awaitable<Result<T>> aw,
|
|
const std::chrono::duration<Rep, Period>& timeout) {
|
|
return yams::cli::run_sync<T, Rep, Period>(std::move(aw), timeout);
|
|
}
|
|
|
|
static std::uint64_t parseAdditionalStat(const std::map<std::string, std::string>& stats,
|
|
std::string_view key) {
|
|
const auto it = stats.find(std::string(key));
|
|
if (it == stats.end() || it->second.empty()) {
|
|
return 0;
|
|
}
|
|
try {
|
|
return std::stoull(it->second);
|
|
} catch (...) {
|
|
return 0;
|
|
}
|
|
}
|
|
|
|
static Result<yams::search::BenchmarkDaemonGraphState>
|
|
observeDaemonGraphState(yams::daemon::DaemonClient& client) {
|
|
yams::daemon::GetStatsRequest request;
|
|
request.includeHealth = true;
|
|
auto statsResult = benchRunSync(client.getStats(request), 30s);
|
|
if (!statsResult) {
|
|
return statsResult.error();
|
|
}
|
|
|
|
const auto& stats = statsResult.value().additionalStats;
|
|
yams::search::BenchmarkDaemonGraphState state;
|
|
state.semanticDocumentsProcessed = parseAdditionalStat(stats, "embed_semantic_docs_processed");
|
|
state.semanticEdgesCreated = parseAdditionalStat(stats, "embed_semantic_edges_created");
|
|
state.semanticUpdateErrors = parseAdditionalStat(stats, "embed_semantic_update_errors");
|
|
state.topologyDocumentNodes = parseAdditionalStat(stats, "kg_topology_document_nodes");
|
|
state.topologySemanticEdges = parseAdditionalStat(stats, "kg_topology_semantic_edges");
|
|
state.topologyDocumentsWithNeighbors =
|
|
parseAdditionalStat(stats, "kg_topology_documents_with_neighbors");
|
|
state.topologyIsolatedDocuments = parseAdditionalStat(stats, "kg_topology_isolated_documents");
|
|
state.postIngestQueued = parseAdditionalStat(stats, "post_ingest_queued");
|
|
state.postIngestInFlight = parseAdditionalStat(stats, "post_ingest_inflight");
|
|
state.embeddingQueued = parseAdditionalStat(stats, "embed_svc_queued");
|
|
state.embeddingInFlight = parseAdditionalStat(stats, "embed_in_flight");
|
|
state.writeQueued = parseAdditionalStat(stats, "write_queue_depth");
|
|
state.writeInFlight = parseAdditionalStat(stats, "write_in_flight");
|
|
return state;
|
|
}
|
|
|
|
static std::string describeDaemonGraphState(const yams::search::BenchmarkDaemonGraphState& state) {
|
|
std::ostringstream message;
|
|
message << "semantic_docs_processed=" << state.semanticDocumentsProcessed
|
|
<< ", semantic_edges_created=" << state.semanticEdgesCreated
|
|
<< ", topology_document_nodes=" << state.topologyDocumentNodes
|
|
<< ", topology_semantic_edges=" << state.topologySemanticEdges
|
|
<< ", topology_documents_with_neighbors=" << state.topologyDocumentsWithNeighbors
|
|
<< ", topology_isolated_documents=" << state.topologyIsolatedDocuments
|
|
<< ", semantic_update_errors=" << state.semanticUpdateErrors
|
|
<< ", post_ingest_queued=" << state.postIngestQueued
|
|
<< ", post_ingest_in_flight=" << state.postIngestInFlight
|
|
<< ", embed_queued=" << state.embeddingQueued
|
|
<< ", embed_in_flight=" << state.embeddingInFlight
|
|
<< ", write_queued=" << state.writeQueued
|
|
<< ", write_in_flight=" << state.writeInFlight;
|
|
return message.str();
|
|
}
|
|
|
|
static Result<yams::search::BenchmarkDaemonGraphState>
|
|
waitForDaemonNativeGraph(yams::daemon::DaemonClient& client,
|
|
std::uint64_t minimumSemanticDocuments) {
|
|
spdlog::info("Waiting for daemon-native semantic graph production before query evaluation...");
|
|
const auto deadline = std::chrono::steady_clock::now() + 300s;
|
|
|
|
// Semantic-neighbor backfill is intentionally idle-only. A tight getStats loop keeps a
|
|
// non-health IPC connection active and prevents TuningManager from publishing an idle
|
|
// snapshot, so observing readiness too frequently can stop the work itself.
|
|
constexpr auto kObservationInterval = 15s;
|
|
auto observationAt = std::chrono::steady_clock::now() + kObservationInterval;
|
|
yams::search::BenchmarkDaemonGraphState state;
|
|
std::optional<yams::search::BenchmarkDaemonGraphState> previousReadyState;
|
|
|
|
while (std::chrono::steady_clock::now() < deadline) {
|
|
const auto now = std::chrono::steady_clock::now();
|
|
if (now < observationAt) {
|
|
std::this_thread::sleep_for(std::min(
|
|
500ms, std::chrono::duration_cast<std::chrono::milliseconds>(observationAt - now)));
|
|
continue;
|
|
}
|
|
observationAt = now + kObservationInterval;
|
|
|
|
auto observation = observeDaemonGraphState(client);
|
|
if (!observation) {
|
|
previousReadyState.reset();
|
|
spdlog::warn("Daemon graph observation failed: {}", observation.error().message);
|
|
continue;
|
|
}
|
|
state = std::move(observation.value());
|
|
spdlog::info("Daemon graph wait: {}", describeDaemonGraphState(state));
|
|
|
|
if (!yams::search::benchmarkDaemonGraphReady(state, minimumSemanticDocuments)) {
|
|
previousReadyState.reset();
|
|
continue;
|
|
}
|
|
if (!previousReadyState.has_value() ||
|
|
!yams::search::benchmarkDaemonGraphProductionStable(*previousReadyState, state)) {
|
|
previousReadyState = state;
|
|
continue;
|
|
}
|
|
|
|
spdlog::info("Daemon-native semantic graph ready: {}", describeDaemonGraphState(state));
|
|
return state;
|
|
}
|
|
|
|
return Error{ErrorCode::Timeout,
|
|
"Daemon-native semantic graph not ready before benchmark queries (" +
|
|
describeDaemonGraphState(state) + ")."};
|
|
}
|
|
|
|
static Result<void> exportTopologyClusters(yams::daemon::ServiceManager& serviceManager,
|
|
std::string_view snapshotId) {
|
|
const char* rawOutput = std::getenv("YAMS_BENCH_TOPOLOGY_CLUSTER_OUTPUT");
|
|
if (rawOutput == nullptr || *rawOutput == '\0') {
|
|
return {};
|
|
}
|
|
|
|
yams::topology::MetadataKgTopologyArtifactStore store(serviceManager.getMetadataRepo(),
|
|
serviceManager.getKgStore());
|
|
auto loaded = store.loadLatest(snapshotId);
|
|
if (!loaded) {
|
|
return loaded.error();
|
|
}
|
|
if (!loaded.value().has_value()) {
|
|
return Error{ErrorCode::NotFound, "topology snapshot unavailable for cluster export"};
|
|
}
|
|
|
|
const auto& snapshot = *loaded.value();
|
|
json memberships = json::array();
|
|
for (const auto& membership : snapshot.memberships) {
|
|
memberships.push_back({{"document_hash", membership.documentHash},
|
|
{"cluster_id", membership.clusterId},
|
|
{"overlap_cluster_ids", membership.overlapClusterIds}});
|
|
}
|
|
|
|
json clusters = json::array();
|
|
for (const auto& cluster : snapshot.clusters) {
|
|
clusters.push_back({{"cluster_id", cluster.clusterId},
|
|
{"member_count", cluster.memberCount},
|
|
{"overlap_cluster_ids", cluster.overlapClusterIds}});
|
|
}
|
|
|
|
const fs::path outputPath(rawOutput);
|
|
std::error_code ec;
|
|
if (!outputPath.parent_path().empty()) {
|
|
fs::create_directories(outputPath.parent_path(), ec);
|
|
if (ec) {
|
|
return Error{ErrorCode::WriteError,
|
|
"failed to create topology cluster output directory: " + ec.message()};
|
|
}
|
|
}
|
|
std::ofstream output(outputPath, std::ios::trunc);
|
|
if (!output) {
|
|
return Error{ErrorCode::WriteError, "failed to open topology cluster output"};
|
|
}
|
|
output << json{{"algorithm", snapshot.algorithm},
|
|
{"snapshot_id", snapshot.snapshotId},
|
|
{"topology_epoch", snapshot.topologyEpoch},
|
|
{"memberships", std::move(memberships)},
|
|
{"clusters", std::move(clusters)}}
|
|
.dump(2)
|
|
<< '\n';
|
|
if (!output) {
|
|
return Error{ErrorCode::WriteError, "failed to write topology cluster output"};
|
|
}
|
|
return {};
|
|
}
|
|
|
|
static Result<yams::daemon::TopologyManager::RebuildStats>
|
|
rebuildCanonicalDaemonTopology(yams::daemon::ServiceManager& serviceManager,
|
|
std::string_view topologyAlgorithm) {
|
|
auto& topologyManager = serviceManager.getTopologyManager();
|
|
const auto deadline = std::chrono::steady_clock::now() + 60s;
|
|
do {
|
|
if (topologyManager.isRebuildInProgress()) {
|
|
std::this_thread::sleep_for(50ms);
|
|
continue;
|
|
}
|
|
auto rebuilt =
|
|
topologyManager.rebuildArtifacts("benchmark_daemon_native_graph_canonicalization",
|
|
false, {}, std::string(topologyAlgorithm));
|
|
if (!rebuilt) {
|
|
return rebuilt.error();
|
|
}
|
|
if (!rebuilt.value().skipped) {
|
|
if (!rebuilt.value().stored) {
|
|
return Error{ErrorCode::InvalidState,
|
|
"canonical topology rebuild did not publish a stored snapshot"};
|
|
}
|
|
return std::move(rebuilt.value());
|
|
}
|
|
std::this_thread::sleep_for(50ms);
|
|
} while (std::chrono::steady_clock::now() < deadline);
|
|
|
|
return Error{ErrorCode::Timeout, "timed out waiting for canonical topology rebuild"};
|
|
}
|
|
|
|
static Result<std::string>
|
|
loadTopologyRoutingConstructionFingerprint(yams::daemon::ServiceManager& serviceManager,
|
|
std::string_view snapshotId) {
|
|
yams::topology::MetadataKgTopologyArtifactStore store(serviceManager.getMetadataRepo(),
|
|
serviceManager.getKgStore());
|
|
auto loaded = store.loadLatest(snapshotId);
|
|
if (!loaded) {
|
|
return loaded.error();
|
|
}
|
|
if (!loaded.value().has_value()) {
|
|
return Error{ErrorCode::NotFound,
|
|
"canonical topology snapshot unavailable for fingerprinting"};
|
|
}
|
|
return yams::search::topologyRoutingConstructionFingerprint(*loaded.value());
|
|
}
|
|
|
|
// Helper to discover GLiNER model path for entity extraction
|
|
// Searches standard locations where yams init downloads models
|
|
static std::string discoverGlinerModelPath() {
|
|
// Preferred model names in order
|
|
const std::vector<std::string> model_names = {
|
|
"gliner_small-v2.1-quantized",
|
|
"gliner_small-v2.1",
|
|
"gliner_medium-v2.1-quantized",
|
|
"gliner_medium-v2.1",
|
|
};
|
|
|
|
// Build list of base directories to search
|
|
std::vector<fs::path> search_dirs;
|
|
|
|
// YAMS_GLINT_MODEL_PATH - explicit override
|
|
if (const char* env_path = std::getenv("YAMS_GLINT_MODEL_PATH")) {
|
|
if (fs::exists(env_path)) {
|
|
spdlog::info("[Bench] Using GLiNER model from YAMS_GLINT_MODEL_PATH: {}", env_path);
|
|
return env_path;
|
|
}
|
|
}
|
|
|
|
// YAMS_DATA_DIR
|
|
if (const char* data_dir = std::getenv("YAMS_DATA_DIR")) {
|
|
search_dirs.emplace_back(fs::path(data_dir) / "models" / "gliner");
|
|
}
|
|
|
|
// YAMS_STORAGE
|
|
if (const char* storage = std::getenv("YAMS_STORAGE")) {
|
|
search_dirs.emplace_back(fs::path(storage) / "models" / "gliner");
|
|
}
|
|
|
|
// ~/.local/share/yams (XDG default)
|
|
if (const char* home = std::getenv("HOME")) {
|
|
search_dirs.emplace_back(fs::path(home) / ".local" / "share" / "yams" / "models" /
|
|
"gliner");
|
|
}
|
|
|
|
// Development path
|
|
search_dirs.emplace_back("/Volumes/picaso/yams/models/gliner");
|
|
|
|
// Homebrew path
|
|
search_dirs.emplace_back("/opt/homebrew/share/yams/models/gliner");
|
|
|
|
// Search for models
|
|
for (const auto& base_dir : search_dirs) {
|
|
if (!fs::exists(base_dir)) {
|
|
continue;
|
|
}
|
|
|
|
// Try preferred model names first
|
|
for (const auto& model_name : model_names) {
|
|
fs::path model_path = base_dir / model_name / "model.onnx";
|
|
if (fs::exists(model_path)) {
|
|
spdlog::info("[Bench] Found GLiNER model at: {}", model_path.string());
|
|
return model_path.string();
|
|
}
|
|
}
|
|
|
|
// Fall back to scanning directory for any model
|
|
std::error_code ec;
|
|
for (const auto& entry : fs::directory_iterator(base_dir, ec)) {
|
|
if (!entry.is_directory())
|
|
continue;
|
|
fs::path model_path = entry.path() / "model.onnx";
|
|
if (fs::exists(model_path)) {
|
|
spdlog::info("[Bench] Found GLiNER model at: {}", model_path.string());
|
|
return model_path.string();
|
|
}
|
|
}
|
|
}
|
|
|
|
spdlog::warn("[Bench] No GLiNER model found - entity extraction will use mock mode");
|
|
return "";
|
|
}
|
|
|
|
static bool forceMockEmbeddingsForBench() {
|
|
const char* env = std::getenv("YAMS_BENCH_FORCE_MOCK_EMBEDDINGS");
|
|
return env && std::string_view(env) == "1";
|
|
}
|
|
|
|
static void ensureBenchmarkEmbeddingsReady(yams::test::DaemonHarness* harness, bool vectorsDisabled,
|
|
bool forceMockEmbeddings, bool embedBackendSimeon) {
|
|
if (!harness || !harness->daemon() || vectorsDisabled) {
|
|
return;
|
|
}
|
|
|
|
auto serviceManager = harness->daemon()->getServiceManager();
|
|
if (!serviceManager) {
|
|
return;
|
|
}
|
|
|
|
if (forceMockEmbeddings) {
|
|
std::size_t dim = 768;
|
|
if (auto vectorDb = serviceManager->getVectorDatabase()) {
|
|
dim = vectorDb->getConfig().embedding_dim;
|
|
}
|
|
auto sharedProvider = std::make_shared<BenchmarkMockModelProvider>(dim);
|
|
serviceManager->testing_setModelProvider(sharedProvider);
|
|
}
|
|
|
|
if (embedBackendSimeon) {
|
|
auto ready = serviceManager->ensureEmbeddingModelReadySync("simeon-default", {}, 10000,
|
|
false, false);
|
|
if (!ready) {
|
|
spdlog::warn("[Bench] Simeon provider not ready for retrieval bench: {}",
|
|
ready.error().message);
|
|
}
|
|
return;
|
|
}
|
|
|
|
auto ready =
|
|
serviceManager->ensureEmbeddingModelReadySync("all-MiniLM-L6-v2", {}, 10000, false, false);
|
|
if (!ready) {
|
|
spdlog::warn("[Bench] Embedding model not ready for retrieval bench: {}",
|
|
ready.error().message);
|
|
}
|
|
}
|
|
|
|
using yams::bench::BEIRCorpusLoader;
|
|
using yams::bench::BEIRDataset;
|
|
using yams::bench::BEIRDocument;
|
|
using yams::bench::BEIRQuery;
|
|
using yams::bench::CorpusGenerator;
|
|
using yams::bench::RetrievalMetrics;
|
|
using yams::bench::StageMetricAccumulator;
|
|
using yams::bench::SyntheticCorpusMode;
|
|
using yams::bench::TestQuery;
|
|
|
|
static Result<BEIRDataset> loadSciFactDataset(const fs::path& cacheDir) {
|
|
const char* home = std::getenv("HOME");
|
|
if (!home) {
|
|
return Error{ErrorCode::InvalidArgument, "HOME environment variable not set"};
|
|
}
|
|
|
|
fs::path scifactDir =
|
|
cacheDir.empty() ? fs::path(home) / ".cache" / "yams" / "benchmarks" / "scifact" : cacheDir;
|
|
|
|
BEIRDataset dataset;
|
|
dataset.name = "SciFact";
|
|
dataset.basePath = scifactDir;
|
|
|
|
fs::path corpusFile = scifactDir / "corpus.jsonl";
|
|
fs::path queriesFile = scifactDir / "queries.jsonl";
|
|
fs::path qrelsFile = scifactDir / "qrels" / "test.tsv"; // Standard BEIR structure
|
|
|
|
if (!fs::exists(corpusFile) || !fs::exists(queriesFile) || !fs::exists(qrelsFile)) {
|
|
return Error{
|
|
ErrorCode::NotFound,
|
|
"SciFact dataset not found. Please download manually:\n"
|
|
" mkdir -p ~/.cache/yams/benchmarks && cd ~/.cache/yams/benchmarks\n"
|
|
" curl -L -o scifact.zip "
|
|
"https://public.ukp.informatik.tu-darmstadt.de/thakur/BEIR/datasets/scifact.zip\n"
|
|
" unzip scifact.zip"};
|
|
}
|
|
|
|
std::ifstream corpusIn(corpusFile);
|
|
std::string line;
|
|
while (std::getline(corpusIn, line)) {
|
|
if (line.empty())
|
|
continue;
|
|
try {
|
|
auto j = json::parse(line);
|
|
BEIRDocument doc;
|
|
doc.id = j.value("_id", "");
|
|
if (doc.id.empty())
|
|
doc.id = j.value("id", "");
|
|
doc.title = j.value("title", "");
|
|
doc.text = j.value("text", "");
|
|
yams::bench::readBEIRDocumentMetadata(j, doc);
|
|
if (!doc.id.empty() && !doc.text.empty()) {
|
|
dataset.documents[doc.id] = doc;
|
|
}
|
|
} catch (const json::exception& e) {
|
|
spdlog::warn("Failed to parse corpus line: {}", e.what());
|
|
}
|
|
}
|
|
corpusIn.close();
|
|
|
|
std::ifstream queriesIn(queriesFile);
|
|
while (std::getline(queriesIn, line)) {
|
|
if (line.empty())
|
|
continue;
|
|
try {
|
|
auto j = json::parse(line);
|
|
BEIRQuery q;
|
|
q.id = j.value("_id", "");
|
|
if (q.id.empty())
|
|
q.id = j.value("id", "");
|
|
q.text = j.value("text", "");
|
|
if (!q.id.empty() && !q.text.empty()) {
|
|
dataset.queries[q.id] = q;
|
|
}
|
|
} catch (const json::exception& e) {
|
|
spdlog::warn("Failed to parse query line: {}", e.what());
|
|
}
|
|
}
|
|
queriesIn.close();
|
|
|
|
std::ifstream qrelsIn(qrelsFile);
|
|
bool firstLine = true;
|
|
while (std::getline(qrelsIn, line)) {
|
|
if (line.empty() || line[0] == '#')
|
|
continue;
|
|
// Skip header row
|
|
if (firstLine) {
|
|
firstLine = false;
|
|
if (line.find("query-id") != std::string::npos)
|
|
continue;
|
|
}
|
|
std::istringstream iss(line);
|
|
std::string queryId, docId, scoreStr;
|
|
// BEIR format: query-id, corpus-id, score (3 columns, no iteration)
|
|
if (std::getline(iss, queryId, '\t') && std::getline(iss, docId, '\t') &&
|
|
std::getline(iss, scoreStr, '\t')) {
|
|
int score = std::stoi(scoreStr);
|
|
dataset.qrels.emplace(queryId, yams::bench::BEIRQrel{.docId = docId, .score = score});
|
|
}
|
|
}
|
|
qrelsIn.close();
|
|
|
|
spdlog::info("Loaded BEIR dataset: {} documents, {} queries, {} qrels",
|
|
dataset.documents.size(), dataset.queries.size(), dataset.qrels.size());
|
|
|
|
return dataset;
|
|
}
|
|
|
|
// Generic BEIR dataset loader — delegates to yams::bench::loadBEIRDataset.
|
|
static Result<BEIRDataset> loadBEIRDataset(const std::string& datasetName,
|
|
const fs::path& cacheDir) {
|
|
if (datasetName == "local-manifest") {
|
|
return yams::bench::loadBenchmarkManifestDataset(datasetName, cacheDir);
|
|
}
|
|
return yams::bench::loadBEIRDataset(datasetName, cacheDir);
|
|
}
|
|
|
|
static Result<BEIRDataset> selectBenchmarkBEIRDataset(const std::string& datasetName,
|
|
const fs::path& cacheDir,
|
|
const std::optional<int>& maxDocsOpt,
|
|
const std::optional<int>& maxQueriesOpt) {
|
|
Result<BEIRDataset> dsResult;
|
|
if (datasetName == "scifact") {
|
|
dsResult = loadSciFactDataset(cacheDir);
|
|
} else {
|
|
dsResult = loadBEIRDataset(datasetName, cacheDir);
|
|
}
|
|
if (!dsResult) {
|
|
return dsResult;
|
|
}
|
|
|
|
const BEIRDataset& fullDataset = dsResult.value();
|
|
BEIRDataset dataset;
|
|
dataset.name = fullDataset.name;
|
|
dataset.basePath = fullDataset.basePath;
|
|
|
|
const int maxDocs =
|
|
maxDocsOpt.has_value() ? *maxDocsOpt : static_cast<int>(fullDataset.documents.size());
|
|
const int maxQueries =
|
|
maxQueriesOpt.has_value() ? *maxQueriesOpt : static_cast<int>(fullDataset.queries.size());
|
|
|
|
std::set<std::string> qrelDocIds;
|
|
for (const auto& [queryId, docScore] : fullDataset.qrels) {
|
|
(void)queryId;
|
|
qrelDocIds.insert(docScore.docId);
|
|
}
|
|
spdlog::info("BEIR dataset has {} documents referenced by qrels", qrelDocIds.size());
|
|
|
|
std::set<std::string> includedDocIds;
|
|
for (const auto& docId : qrelDocIds) {
|
|
if (static_cast<int>(includedDocIds.size()) >= maxDocs) {
|
|
break;
|
|
}
|
|
if (fullDataset.documents.count(docId) > 0) {
|
|
dataset.documents[docId] = fullDataset.documents.at(docId);
|
|
includedDocIds.insert(docId);
|
|
}
|
|
}
|
|
spdlog::info("Included {} qrel-referenced documents", includedDocIds.size());
|
|
|
|
for (const auto& [id, doc] : fullDataset.documents) {
|
|
if (static_cast<int>(includedDocIds.size()) >= maxDocs) {
|
|
break;
|
|
}
|
|
if (includedDocIds.count(id) == 0) {
|
|
dataset.documents[id] = doc;
|
|
includedDocIds.insert(id);
|
|
}
|
|
}
|
|
|
|
int queryCount = 0;
|
|
for (const auto& [qid, query] : fullDataset.queries) {
|
|
if (queryCount >= maxQueries) {
|
|
break;
|
|
}
|
|
auto range = fullDataset.qrels.equal_range(qid);
|
|
bool hasRelevantDoc = false;
|
|
for (auto it = range.first; it != range.second; ++it) {
|
|
if (includedDocIds.count(it->second.docId) > 0) {
|
|
hasRelevantDoc = true;
|
|
break;
|
|
}
|
|
}
|
|
if (!hasRelevantDoc) {
|
|
continue;
|
|
}
|
|
|
|
dataset.queries[qid] = query;
|
|
for (auto it = range.first; it != range.second; ++it) {
|
|
if (includedDocIds.count(it->second.docId) > 0) {
|
|
dataset.qrels.emplace(qid, it->second);
|
|
}
|
|
}
|
|
queryCount++;
|
|
}
|
|
|
|
for (const auto& entity : fullDataset.kgEntities) {
|
|
if (includedDocIds.contains(entity.docId)) {
|
|
dataset.kgEntities.push_back(entity);
|
|
}
|
|
}
|
|
|
|
spdlog::debug(
|
|
"Limited BEIR dataset to {} docs, {} queries, {} qrels, {} kg entities (from {} docs, {} "
|
|
"queries)",
|
|
dataset.documents.size(), dataset.queries.size(), dataset.qrels.size(),
|
|
dataset.kgEntities.size(), fullDataset.documents.size(), fullDataset.queries.size());
|
|
|
|
return dataset;
|
|
}
|
|
|
|
using yams::test::DaemonHarness;
|
|
|
|
struct QueryDiagnosticsSummary {
|
|
std::uint64_t queryCount = 0;
|
|
std::uint64_t queryWithResultsCount = 0;
|
|
std::uint64_t queryWithoutResultsCount = 0;
|
|
std::uint64_t queryWithoutRelevantHitCount = 0;
|
|
std::uint64_t queryWithRelevantInPreFusionCount = 0;
|
|
std::uint64_t queryWithRelevantInPostFusionCount = 0;
|
|
std::uint64_t queryWithRelevantInGraphlessPostFusionCount = 0;
|
|
std::uint64_t queryWithRelevantInFinalWindowCount = 0;
|
|
std::uint64_t queryWithGraphAddedRelevantPostFusionCount = 0;
|
|
std::uint64_t queryWithGraphDisplacedRelevantPostFusionCount = 0;
|
|
std::uint64_t queryWithTopologyAddedRelevantPostFusionCount = 0;
|
|
std::uint64_t queryWithTopologyAddedRelevantFusionDroppedCount = 0;
|
|
std::uint64_t queryWithTopKDuplicateCount = 0;
|
|
std::uint64_t degradedQueryCount = 0;
|
|
std::uint64_t traceEnabledQueryCount = 0;
|
|
std::uint64_t graphRerankAppliedQueryCount = 0;
|
|
std::uint64_t compressedAnnEnabledQueryCount = 0;
|
|
std::uint64_t compressedAnnAppliedQueryCount = 0;
|
|
std::uint64_t semanticRescueNonZeroQueryCount = 0;
|
|
std::uint64_t missMissingPreFusionCount = 0;
|
|
std::uint64_t missFusionCutoffCount = 0;
|
|
std::uint64_t missRerankWindowDropCount = 0;
|
|
std::uint64_t missTopKDropCount = 0;
|
|
std::vector<double> semanticRescueRateSamples;
|
|
std::vector<double> semanticRescueFinalCountSamples;
|
|
std::vector<double> semanticRescueTargetSamples;
|
|
std::vector<double> compressedAnnResultCountSamples;
|
|
std::unordered_map<std::string, std::uint64_t> compressedAnnSkipReasonCounts;
|
|
std::vector<double> fusionPreFusionUniqueCountSamples;
|
|
std::vector<double> fusionPostFusionCountSamples;
|
|
std::vector<double> fusionDroppedCountSamples;
|
|
std::vector<double> fusionDroppedRateSamples;
|
|
std::vector<double> anchoredPreFusionCountSamples;
|
|
std::vector<double> anchoredFusionDroppedCountSamples;
|
|
std::vector<double> anchoredFusionDroppedRateSamples;
|
|
std::vector<double> topTextPreFusionCountSamples;
|
|
std::vector<double> topTextFusionDroppedCountSamples;
|
|
std::vector<double> topTextFusionDroppedRateSamples;
|
|
std::vector<double> vectorOnlyDocsSamples;
|
|
std::vector<double> vectorOnlyBelowThresholdSamples;
|
|
std::vector<double> vectorOnlyAboveThresholdSamples;
|
|
std::vector<double> vectorOnlyNearMissEligibleSamples;
|
|
std::uint64_t adaptiveFusionEnabledQueryCount = 0;
|
|
std::vector<double> strongVectorOnlyDocsSamples;
|
|
std::vector<double> strongVectorOnlyScoreEligibleDocsSamples;
|
|
std::vector<double> strongVectorOnlyRankEligibleDocsSamples;
|
|
std::vector<double> graphAddedPostFusionSamples;
|
|
std::vector<double> graphDisplacedPostFusionSamples;
|
|
std::vector<double> topologyRoutedDocsSamples;
|
|
std::vector<double> topologyRouteBestScoreSamples;
|
|
std::vector<double> topologyRouteMeanAcceptedScoreSamples;
|
|
std::vector<double> topologyRouteAcceptedCountSamples;
|
|
std::vector<double> topologySeedCountSamples;
|
|
std::vector<double> topologySeedCoverageCountSamples;
|
|
std::vector<double> topologyCoreCandidateCoverageSamples;
|
|
std::vector<double> topologyAddedCandidatesSamples;
|
|
std::vector<double> topologyDuplicateCandidatesSamples;
|
|
std::vector<double> topologyStaleCandidatesSamples;
|
|
std::vector<double> graphCommunitySupportedDocsSamples;
|
|
std::vector<double> graphCommunityEdgeCountSamples;
|
|
std::vector<double> graphCommunityLargestSizeSamples;
|
|
std::vector<double> graphCommunitySignalMassSamples;
|
|
std::vector<double> graphCommunityBoostedDocsSamples;
|
|
std::vector<double> graphCommunityWeightSamples;
|
|
std::vector<double> multiVectorGeneratedPhraseSamples;
|
|
std::vector<double> multiVectorRawHitSamples;
|
|
std::vector<double> multiVectorAddedNewSamples;
|
|
std::vector<double> multiVectorReplacedBaseSamples;
|
|
std::vector<double> subPhraseGeneratedSamples;
|
|
std::vector<double> subPhraseClauseSamples;
|
|
std::vector<double> subPhraseFtsHitSamples;
|
|
std::vector<double> subPhraseFtsAddedSamples;
|
|
std::vector<double> aggressiveFtsClauseSamples;
|
|
std::vector<double> aggressiveFtsHitSamples;
|
|
std::vector<double> aggressiveFtsAddedSamples;
|
|
std::unordered_map<std::string, std::vector<double>> timingSamplesMs;
|
|
std::unordered_map<std::string, std::uint64_t> stageEnabledCount;
|
|
std::unordered_map<std::string, std::uint64_t> stageAttemptedCount;
|
|
std::unordered_map<std::string, std::uint64_t> stageContributedCount;
|
|
std::unordered_map<std::string, std::uint64_t> stageSkippedCount;
|
|
std::unordered_map<std::string, std::uint64_t> stageRelevantPresenceCount;
|
|
std::unordered_map<std::string, std::vector<std::string>> stageSkipReasons;
|
|
std::unordered_map<std::string, std::vector<double>> fusionSourceScoreMassSamples;
|
|
std::unordered_map<std::string, std::vector<double>> fusionSourceFinalDocSamples;
|
|
std::unordered_map<std::string, std::vector<double>> stageUniqueDocCountSamples;
|
|
std::unordered_map<std::string, std::vector<double>> stageRelevantDocCountSamples;
|
|
std::map<std::string, StageMetricAccumulator> stageRetrievalMetrics;
|
|
std::unordered_map<std::string, std::vector<double>> tunerSignalSamples;
|
|
std::unordered_map<std::string, std::uint64_t> tunerDecisionCounts;
|
|
std::unordered_map<std::string, std::uint64_t> zoomLevelCounts;
|
|
};
|
|
|
|
static const std::vector<std::string>& trackedTimingKeys() {
|
|
static const std::vector<std::string> kKeys = {
|
|
"latency",
|
|
"embedding",
|
|
"concepts",
|
|
"text",
|
|
"vector",
|
|
"entity_vector",
|
|
"kg",
|
|
"path",
|
|
"tag",
|
|
"metadata",
|
|
"multi_vector",
|
|
"graph_rerank",
|
|
"topology_weak_query",
|
|
"topology_load",
|
|
"topology_validate",
|
|
"topology_request_prep",
|
|
"topology_route",
|
|
"topology_cluster_lookup",
|
|
"topology_doc_lookup",
|
|
"topology_candidate_insert",
|
|
};
|
|
return kKeys;
|
|
}
|
|
|
|
static std::string timingStatKey(const std::string& name) {
|
|
return std::string("timing_") + name + "_ms";
|
|
}
|
|
|
|
static std::unordered_set<std::string> splitTabSet(const std::string& value) {
|
|
std::unordered_set<std::string> out;
|
|
if (value.empty()) {
|
|
return out;
|
|
}
|
|
size_t start = 0;
|
|
while (start <= value.size()) {
|
|
const size_t end = value.find('\t', start);
|
|
const size_t len = (end == std::string::npos) ? (value.size() - start) : (end - start);
|
|
if (len > 0) {
|
|
out.insert(value.substr(start, len));
|
|
}
|
|
if (end == std::string::npos) {
|
|
break;
|
|
}
|
|
start = end + 1;
|
|
}
|
|
return out;
|
|
}
|
|
|
|
static std::vector<std::string> splitTabList(const std::string& value, size_t limit) {
|
|
std::vector<std::string> out;
|
|
if (value.empty() || limit == 0) {
|
|
return out;
|
|
}
|
|
|
|
size_t start = 0;
|
|
while (start <= value.size() && out.size() < limit) {
|
|
const size_t end = value.find('\t', start);
|
|
const size_t len = (end == std::string::npos) ? (value.size() - start) : (end - start);
|
|
if (len > 0) {
|
|
out.push_back(value.substr(start, len));
|
|
}
|
|
if (end == std::string::npos) {
|
|
break;
|
|
}
|
|
start = end + 1;
|
|
}
|
|
return out;
|
|
}
|
|
|
|
static std::optional<size_t> findRankInTabList(const std::string& value,
|
|
const std::string& needle) {
|
|
if (value.empty() || needle.empty()) {
|
|
return std::nullopt;
|
|
}
|
|
|
|
size_t start = 0;
|
|
size_t rank = 0;
|
|
while (start <= value.size()) {
|
|
const size_t end = value.find('\t', start);
|
|
const size_t len = (end == std::string::npos) ? (value.size() - start) : (end - start);
|
|
if (len > 0) {
|
|
++rank;
|
|
if (value.compare(start, len, needle) == 0 && len == needle.size()) {
|
|
return rank;
|
|
}
|
|
}
|
|
if (end == std::string::npos) {
|
|
break;
|
|
}
|
|
start = end + 1;
|
|
}
|
|
return std::nullopt;
|
|
}
|
|
|
|
static json buildRelevantDecisionTrace(const std::vector<std::string>& relevantDocIds,
|
|
const std::vector<std::string>& returnedDocIds,
|
|
const std::map<std::string, std::string>& searchStats) {
|
|
const std::string preFusionDocIds = searchStats.contains("trace_pre_fusion_doc_ids")
|
|
? searchStats.at("trace_pre_fusion_doc_ids")
|
|
: "";
|
|
const std::string postFusionDocIds = searchStats.contains("trace_post_fusion_doc_ids")
|
|
? searchStats.at("trace_post_fusion_doc_ids")
|
|
: "";
|
|
const std::string graphlessPostFusionDocIds =
|
|
searchStats.contains("trace_graphless_post_fusion_doc_ids")
|
|
? searchStats.at("trace_graphless_post_fusion_doc_ids")
|
|
: "";
|
|
const std::string finalWindowDocIds =
|
|
searchStats.contains("trace_final_doc_ids") ? searchStats.at("trace_final_doc_ids") : "";
|
|
|
|
const auto preSet = splitTabSet(searchStats.contains("trace_pre_fusion_doc_ids")
|
|
? searchStats.at("trace_pre_fusion_doc_ids")
|
|
: "");
|
|
const auto postSet = splitTabSet(searchStats.contains("trace_post_fusion_doc_ids")
|
|
? searchStats.at("trace_post_fusion_doc_ids")
|
|
: "");
|
|
const auto graphlessPostSet =
|
|
splitTabSet(searchStats.contains("trace_graphless_post_fusion_doc_ids")
|
|
? searchStats.at("trace_graphless_post_fusion_doc_ids")
|
|
: "");
|
|
const auto finalWindowSet = splitTabSet(
|
|
searchStats.contains("trace_final_doc_ids") ? searchStats.at("trace_final_doc_ids") : "");
|
|
std::string topologyAddedIds;
|
|
for (const std::string_view key : {
|
|
std::string_view{"topology_candidate_rescue_added_candidate_doc_ids"},
|
|
std::string_view{"topology_candidate_rescue_added_candidate_hashes"},
|
|
std::string_view{"topology_weak_query_added_candidate_doc_ids"},
|
|
std::string_view{"topology_weak_query_added_candidate_hashes"},
|
|
}) {
|
|
if (const auto it = searchStats.find(std::string(key));
|
|
it != searchStats.end() && !it->second.empty()) {
|
|
topologyAddedIds = it->second;
|
|
break;
|
|
}
|
|
}
|
|
const auto topologyAddedSet = splitTabSet(topologyAddedIds);
|
|
const auto topologyNovelSet =
|
|
splitTabSet(searchStats.contains("topology_candidate_rescue_novel_candidate_doc_ids")
|
|
? searchStats.at("topology_candidate_rescue_novel_candidate_doc_ids")
|
|
: "");
|
|
const auto topologyEvidenceRescueSet =
|
|
splitTabSet(searchStats.contains("topology_candidate_rescue_evidence_rescue_doc_ids")
|
|
? searchStats.at("topology_candidate_rescue_evidence_rescue_doc_ids")
|
|
: "");
|
|
const std::unordered_set<std::string> returnedTopKSet(returnedDocIds.begin(),
|
|
returnedDocIds.end());
|
|
|
|
json fusionTop = json::array();
|
|
if (auto it = searchStats.find("trace_fusion_top_json"); it != searchStats.end()) {
|
|
try {
|
|
fusionTop = json::parse(it->second);
|
|
} catch (...) {
|
|
}
|
|
}
|
|
json finalTop = json::array();
|
|
if (auto it = searchStats.find("trace_final_top_json"); it != searchStats.end()) {
|
|
try {
|
|
finalTop = json::parse(it->second);
|
|
} catch (...) {
|
|
}
|
|
}
|
|
json componentHits = json::object();
|
|
if (auto it = searchStats.find("trace_component_hits_json"); it != searchStats.end()) {
|
|
try {
|
|
componentHits = json::parse(it->second);
|
|
} catch (...) {
|
|
}
|
|
}
|
|
json stageSummary = json::object();
|
|
if (auto it = searchStats.find("trace_stage_summary_json"); it != searchStats.end()) {
|
|
try {
|
|
stageSummary = json::parse(it->second);
|
|
} catch (...) {
|
|
}
|
|
}
|
|
const auto findDoc = [](const json& docs, const std::string& docId) -> json {
|
|
if (!docs.is_array()) {
|
|
return json();
|
|
}
|
|
for (const auto& item : docs) {
|
|
if (item.is_object() && item.value("doc_id", "") == docId) {
|
|
return item;
|
|
}
|
|
}
|
|
return json();
|
|
};
|
|
|
|
const auto findComponentHit = [](const json& componentHitsJson,
|
|
const std::string& docId) -> json {
|
|
if (!componentHitsJson.is_object()) {
|
|
return json::array();
|
|
}
|
|
|
|
json matches = json::array();
|
|
for (const auto& [component, details] : componentHitsJson.items()) {
|
|
if (!details.is_object()) {
|
|
continue;
|
|
}
|
|
const auto hitsIt = details.find("top_hits");
|
|
if (hitsIt == details.end() || !hitsIt->is_array()) {
|
|
continue;
|
|
}
|
|
for (const auto& hit : *hitsIt) {
|
|
if (!hit.is_object() || hit.value("doc_id", "") != docId) {
|
|
continue;
|
|
}
|
|
json entry = hit;
|
|
entry["component"] = component;
|
|
matches.push_back(std::move(entry));
|
|
break;
|
|
}
|
|
}
|
|
return matches;
|
|
};
|
|
|
|
json relevant = json::array();
|
|
bool anyPre = false;
|
|
bool anyPost = false;
|
|
bool anyGraphlessPost = false;
|
|
bool anyFinalWindow = false;
|
|
bool anyReturnedTopK = false;
|
|
json graphAddedRelevant = json::array();
|
|
json graphDisplacedRelevant = json::array();
|
|
json topologyAddedRelevantPostFusion = json::array();
|
|
json topologyAddedRelevantDroppedByFusion = json::array();
|
|
json stageRelevantPresence = json::object();
|
|
|
|
if (stageSummary.is_object()) {
|
|
for (const auto& [stageName, stageData] : stageSummary.items()) {
|
|
if (!stageData.is_object()) {
|
|
continue;
|
|
}
|
|
bool anyRelevant = false;
|
|
std::size_t relevantDocCount = 0;
|
|
if (auto idsIt = stageData.find("unique_doc_ids");
|
|
idsIt != stageData.end() && idsIt->is_array()) {
|
|
for (const auto& id : *idsIt) {
|
|
if (!id.is_string()) {
|
|
continue;
|
|
}
|
|
if (std::find(relevantDocIds.begin(), relevantDocIds.end(),
|
|
id.get<std::string>()) != relevantDocIds.end()) {
|
|
anyRelevant = true;
|
|
++relevantDocCount;
|
|
}
|
|
}
|
|
}
|
|
stageRelevantPresence[stageName] = {
|
|
{"enabled", stageData.value("enabled", false)},
|
|
{"attempted", stageData.value("attempted", false)},
|
|
{"contributed", stageData.value("contributed", false)},
|
|
{"unique_doc_count", stageData.value("unique_doc_count", 0UL)},
|
|
{"any_relevant", anyRelevant},
|
|
{"relevant_doc_count", relevantDocCount},
|
|
};
|
|
}
|
|
}
|
|
|
|
for (const auto& docId : relevantDocIds) {
|
|
const bool inPre = preSet.contains(docId);
|
|
const bool inPost = postSet.contains(docId);
|
|
const bool inGraphlessPost = graphlessPostSet.contains(docId);
|
|
const bool inFinalWindow = finalWindowSet.contains(docId);
|
|
const bool inReturnedTopK = returnedTopKSet.contains(docId);
|
|
anyPre = anyPre || inPre;
|
|
anyPost = anyPost || inPost;
|
|
anyGraphlessPost = anyGraphlessPost || inGraphlessPost;
|
|
anyFinalWindow = anyFinalWindow || inFinalWindow;
|
|
anyReturnedTopK = anyReturnedTopK || inReturnedTopK;
|
|
|
|
if (inPost && !inGraphlessPost) {
|
|
graphAddedRelevant.push_back(docId);
|
|
}
|
|
if (inGraphlessPost && !inPost) {
|
|
graphDisplacedRelevant.push_back(docId);
|
|
}
|
|
const bool topologyAddedCandidate = topologyAddedSet.contains(docId);
|
|
const bool topologyNovelCandidate = topologyNovelSet.contains(docId);
|
|
const bool topologyEvidenceRescue = topologyEvidenceRescueSet.contains(docId);
|
|
if (topologyAddedCandidate && inPost) {
|
|
topologyAddedRelevantPostFusion.push_back(docId);
|
|
}
|
|
if (topologyAddedCandidate && !inPost) {
|
|
topologyAddedRelevantDroppedByFusion.push_back(docId);
|
|
}
|
|
json componentSources = json::array();
|
|
if (componentHits.is_object()) {
|
|
for (const auto& [component, details] : componentHits.items()) {
|
|
if (!details.is_object()) {
|
|
continue;
|
|
}
|
|
const auto topIdsIt = details.find("unique_top_doc_ids");
|
|
if (topIdsIt == details.end() || !topIdsIt->is_array()) {
|
|
continue;
|
|
}
|
|
for (const auto& id : *topIdsIt) {
|
|
if (id.is_string() && id.get<std::string>() == docId) {
|
|
componentSources.push_back(component);
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
json entry = {
|
|
{"doc_id", docId},
|
|
{"in_pre_fusion", inPre},
|
|
{"in_post_fusion", inPost},
|
|
{"in_graphless_post_fusion", inGraphlessPost},
|
|
{"in_final_window", inFinalWindow},
|
|
{"in_returned_topk", inReturnedTopK},
|
|
{"topology_added_candidate", topologyAddedCandidate},
|
|
{"topology_novel_candidate", topologyNovelCandidate},
|
|
{"topology_evidence_rescue", topologyEvidenceRescue},
|
|
{"component_top_hits", componentSources},
|
|
{"component_top_hit_details", findComponentHit(componentHits, docId)},
|
|
};
|
|
|
|
const auto assignOptionalRank = [&entry](std::string_view key,
|
|
const std::optional<size_t>& rank) {
|
|
if (rank.has_value()) {
|
|
entry[std::string(key)] = *rank;
|
|
} else {
|
|
entry[std::string(key)] = nullptr;
|
|
}
|
|
};
|
|
assignOptionalRank("pre_fusion_rank", findRankInTabList(preFusionDocIds, docId));
|
|
assignOptionalRank("post_fusion_rank", findRankInTabList(postFusionDocIds, docId));
|
|
assignOptionalRank("graphless_post_fusion_rank",
|
|
findRankInTabList(graphlessPostFusionDocIds, docId));
|
|
assignOptionalRank("final_window_rank", findRankInTabList(finalWindowDocIds, docId));
|
|
|
|
if (auto match = findDoc(fusionTop, docId); !match.is_null()) {
|
|
entry["post_fusion"] = match;
|
|
}
|
|
if (auto match = findDoc(finalTop, docId); !match.is_null()) {
|
|
entry["final_window"] = match;
|
|
}
|
|
relevant.push_back(std::move(entry));
|
|
}
|
|
|
|
std::string missStage = "hit";
|
|
if (!anyReturnedTopK) {
|
|
if (!anyPre) {
|
|
missStage = "missing_pre_fusion";
|
|
} else if (!anyPost) {
|
|
missStage = "dropped_before_fusion_window";
|
|
} else if (!anyFinalWindow) {
|
|
missStage = "dropped_during_rerank_window";
|
|
} else {
|
|
missStage = "dropped_before_topk";
|
|
}
|
|
}
|
|
|
|
return {
|
|
{"trace_enabled",
|
|
searchStats.contains("trace_enabled") && searchStats.at("trace_enabled") == "1"},
|
|
{"graph_rerank_applied", searchStats.contains("trace_graph_rerank_applied") &&
|
|
searchStats.at("trace_graph_rerank_applied") == "1"},
|
|
{"cross_rerank_applied", searchStats.contains("trace_cross_rerank_applied") &&
|
|
searchStats.at("trace_cross_rerank_applied") == "1"},
|
|
{"any_relevant_in_pre_fusion", anyPre},
|
|
{"any_relevant_in_post_fusion", anyPost},
|
|
{"any_relevant_in_graphless_post_fusion", anyGraphlessPost},
|
|
{"any_relevant_in_final_window", anyFinalWindow},
|
|
{"any_relevant_in_returned_topk", anyReturnedTopK},
|
|
{"graph_added_relevant_post_fusion_count", graphAddedRelevant.size()},
|
|
{"graph_displaced_relevant_post_fusion_count", graphDisplacedRelevant.size()},
|
|
{"graph_added_relevant_post_fusion_doc_ids", graphAddedRelevant},
|
|
{"graph_displaced_relevant_post_fusion_doc_ids", graphDisplacedRelevant},
|
|
{"topology_added_relevant_post_fusion_count", topologyAddedRelevantPostFusion.size()},
|
|
{"topology_added_relevant_fusion_dropped_count",
|
|
topologyAddedRelevantDroppedByFusion.size()},
|
|
{"topology_added_relevant_post_fusion_doc_ids", topologyAddedRelevantPostFusion},
|
|
{"topology_added_relevant_fusion_dropped_doc_ids", topologyAddedRelevantDroppedByFusion},
|
|
{"stage_relevant_presence", stageRelevantPresence},
|
|
{"miss_stage", missStage},
|
|
{"relevant_docs", relevant},
|
|
};
|
|
}
|
|
|
|
struct DebugLogEntry {
|
|
std::string query;
|
|
int queryIndex = -1;
|
|
std::string searchType;
|
|
std::vector<std::string> relevantDocIds;
|
|
std::vector<std::string> relevantFiles;
|
|
std::vector<std::string> returnedPaths;
|
|
std::vector<std::string> returnedDocIds;
|
|
std::vector<float> returnedScores;
|
|
std::vector<int> returnedGrades;
|
|
std::vector<std::string> diagnostics;
|
|
std::uint64_t attempts = 0;
|
|
bool usedStreaming = false;
|
|
bool usedFuzzyFlag = false;
|
|
bool usedLiteralFlag = false;
|
|
bool usedFuzzyRetry = false;
|
|
bool usedLiteralTextRetry = false;
|
|
std::map<std::string, std::string> searchStats;
|
|
json extraFields;
|
|
};
|
|
|
|
struct DebugRunContext {
|
|
std::string runId;
|
|
std::string candidate;
|
|
std::string dataset;
|
|
int corpusSize = 0;
|
|
int numQueries = 0;
|
|
int topK = 0;
|
|
int traceTopN = 0;
|
|
int traceComponentTopN = 0;
|
|
bool stageTraceEnabled = false;
|
|
std::string debugFile;
|
|
};
|
|
|
|
static std::ostream* g_debugOut = nullptr;
|
|
static std::unique_ptr<std::ofstream> g_debugFile;
|
|
static std::mutex g_debugMutex;
|
|
static std::optional<DebugRunContext> g_debugRunContext;
|
|
|
|
static std::optional<double> parseDoubleLoose(const std::string& value) {
|
|
try {
|
|
return std::stod(value);
|
|
} catch (...) {
|
|
return std::nullopt;
|
|
}
|
|
}
|
|
|
|
static std::optional<double> parseDoubleStat(const std::map<std::string, std::string>& stats,
|
|
const std::string& key) {
|
|
if (auto it = stats.find(key); it != stats.end()) {
|
|
return parseDoubleLoose(it->second);
|
|
}
|
|
return std::nullopt;
|
|
}
|
|
|
|
static std::optional<bool> parseBoolStat(const std::map<std::string, std::string>& stats,
|
|
const std::string& key) {
|
|
if (auto it = stats.find(key); it != stats.end()) {
|
|
std::string normalized = it->second;
|
|
std::transform(normalized.begin(), normalized.end(), normalized.begin(), [](char c) {
|
|
return static_cast<char>(std::tolower(static_cast<unsigned char>(c)));
|
|
});
|
|
return normalized == "1" || normalized == "true" || normalized == "yes" ||
|
|
normalized == "on";
|
|
}
|
|
return std::nullopt;
|
|
}
|
|
|
|
static std::optional<json> parseJsonStat(const std::map<std::string, std::string>& stats,
|
|
const std::string& key) {
|
|
if (auto it = stats.find(key); it != stats.end() && !it->second.empty()) {
|
|
try {
|
|
return json::parse(it->second);
|
|
} catch (...) {
|
|
}
|
|
}
|
|
return std::nullopt;
|
|
}
|
|
|
|
static double computePercentile(std::vector<double> samples, double percentile) {
|
|
if (samples.empty()) {
|
|
return 0.0;
|
|
}
|
|
|
|
percentile = std::clamp(percentile, 0.0, 1.0);
|
|
std::sort(samples.begin(), samples.end());
|
|
const double idx = percentile * static_cast<double>(samples.size() - 1);
|
|
const auto lo = static_cast<std::size_t>(std::floor(idx));
|
|
const auto hi = static_cast<std::size_t>(std::ceil(idx));
|
|
if (lo == hi) {
|
|
return samples[lo];
|
|
}
|
|
const double t = idx - static_cast<double>(lo);
|
|
return samples[lo] * (1.0 - t) + samples[hi] * t;
|
|
}
|
|
|
|
static json summarizeSamples(const std::vector<double>& samples) {
|
|
json out = {
|
|
{"count", samples.size()},
|
|
{"mean", 0.0},
|
|
{"min", 0.0},
|
|
{"p50", 0.0},
|
|
{"p95", 0.0},
|
|
{"max", 0.0},
|
|
};
|
|
|
|
if (samples.empty()) {
|
|
return out;
|
|
}
|
|
|
|
const double sum = std::accumulate(samples.begin(), samples.end(), 0.0);
|
|
out["mean"] = sum / static_cast<double>(samples.size());
|
|
out["min"] = *std::min_element(samples.begin(), samples.end());
|
|
out["p50"] = computePercentile(samples, 0.50);
|
|
out["p95"] = computePercentile(samples, 0.95);
|
|
out["max"] = *std::max_element(samples.begin(), samples.end());
|
|
return out;
|
|
}
|
|
|
|
static void ingestQueryDiagnostics(QueryDiagnosticsSummary& summary,
|
|
const std::map<std::string, std::string>& searchStats,
|
|
const json& relevantDecisionTrace, bool degraded,
|
|
bool hadResults, bool hadTopKDuplicate, int firstRelevantRank) {
|
|
summary.queryCount++;
|
|
if (hadResults) {
|
|
summary.queryWithResultsCount++;
|
|
} else {
|
|
summary.queryWithoutResultsCount++;
|
|
}
|
|
if (firstRelevantRank < 0) {
|
|
summary.queryWithoutRelevantHitCount++;
|
|
}
|
|
if (hadTopKDuplicate) {
|
|
summary.queryWithTopKDuplicateCount++;
|
|
}
|
|
if (degraded) {
|
|
summary.degradedQueryCount++;
|
|
}
|
|
|
|
if (relevantDecisionTrace.is_object()) {
|
|
const bool anyPre = relevantDecisionTrace.value("any_relevant_in_pre_fusion", false);
|
|
const bool anyPost = relevantDecisionTrace.value("any_relevant_in_post_fusion", false);
|
|
const bool anyGraphlessPost =
|
|
relevantDecisionTrace.value("any_relevant_in_graphless_post_fusion", false);
|
|
const bool anyFinalWindow =
|
|
relevantDecisionTrace.value("any_relevant_in_final_window", false);
|
|
if (anyPre) {
|
|
summary.queryWithRelevantInPreFusionCount++;
|
|
}
|
|
if (anyPost) {
|
|
summary.queryWithRelevantInPostFusionCount++;
|
|
}
|
|
if (anyGraphlessPost) {
|
|
summary.queryWithRelevantInGraphlessPostFusionCount++;
|
|
}
|
|
if (anyFinalWindow) {
|
|
summary.queryWithRelevantInFinalWindowCount++;
|
|
}
|
|
if (relevantDecisionTrace.value("graph_added_relevant_post_fusion_count", 0) > 0) {
|
|
summary.queryWithGraphAddedRelevantPostFusionCount++;
|
|
}
|
|
if (relevantDecisionTrace.value("graph_displaced_relevant_post_fusion_count", 0) > 0) {
|
|
summary.queryWithGraphDisplacedRelevantPostFusionCount++;
|
|
}
|
|
if (relevantDecisionTrace.value("topology_added_relevant_post_fusion_count", 0) > 0) {
|
|
summary.queryWithTopologyAddedRelevantPostFusionCount++;
|
|
}
|
|
if (relevantDecisionTrace.value("topology_added_relevant_fusion_dropped_count", 0) > 0) {
|
|
summary.queryWithTopologyAddedRelevantFusionDroppedCount++;
|
|
}
|
|
if (firstRelevantRank < 0) {
|
|
const std::string missStage = relevantDecisionTrace.value("miss_stage", "unknown");
|
|
if (missStage == "missing_pre_fusion") {
|
|
summary.missMissingPreFusionCount++;
|
|
} else if (missStage == "dropped_before_fusion_window") {
|
|
summary.missFusionCutoffCount++;
|
|
} else if (missStage == "dropped_during_rerank_window") {
|
|
summary.missRerankWindowDropCount++;
|
|
} else if (missStage == "dropped_before_topk") {
|
|
summary.missTopKDropCount++;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (parseBoolStat(searchStats, "trace_enabled").value_or(false)) {
|
|
summary.traceEnabledQueryCount++;
|
|
}
|
|
if (parseBoolStat(searchStats, "trace_graph_rerank_applied").value_or(false)) {
|
|
summary.graphRerankAppliedQueryCount++;
|
|
}
|
|
if (auto it = searchStats.find("trace_zoom_level");
|
|
it != searchStats.end() && !it->second.empty()) {
|
|
summary.zoomLevelCounts[it->second]++;
|
|
}
|
|
bool compressedAnnEnabled =
|
|
parseBoolStat(searchStats, "compressed_ann_enabled").value_or(false);
|
|
bool compressedAnnApplied =
|
|
parseBoolStat(searchStats, "compressed_ann_applied").value_or(false);
|
|
std::optional<std::string> compressedAnnSkipReason;
|
|
if (auto it = searchStats.find("compressed_ann_skip_reason");
|
|
it != searchStats.end() && !it->second.empty()) {
|
|
compressedAnnSkipReason = it->second;
|
|
}
|
|
if (auto stageSummary = parseJsonStat(searchStats, "trace_stage_summary_json");
|
|
stageSummary && stageSummary->is_object()) {
|
|
if (auto stageIt = stageSummary->find("compressed_ann");
|
|
stageIt != stageSummary->end() && stageIt->is_object()) {
|
|
compressedAnnEnabled = stageIt->value("enabled", compressedAnnEnabled);
|
|
compressedAnnApplied = stageIt->value("contributed", compressedAnnApplied);
|
|
if (stageIt->value("skipped", false)) {
|
|
auto stageSkipReason = stageIt->value("skip_reason", std::string{});
|
|
if (!stageSkipReason.empty()) {
|
|
compressedAnnSkipReason = stageSkipReason;
|
|
}
|
|
} else if (stageIt->value("attempted", false) &&
|
|
compressedAnnSkipReason == "adaptive_vector_skip") {
|
|
compressedAnnSkipReason.reset();
|
|
}
|
|
}
|
|
}
|
|
|
|
if (compressedAnnEnabled) {
|
|
summary.compressedAnnEnabledQueryCount++;
|
|
}
|
|
if (compressedAnnApplied) {
|
|
summary.compressedAnnAppliedQueryCount++;
|
|
}
|
|
if (auto v = parseDoubleStat(searchStats, "compressed_ann_result_count")) {
|
|
summary.compressedAnnResultCountSamples.push_back(*v);
|
|
}
|
|
if (compressedAnnSkipReason.has_value() && !compressedAnnSkipReason->empty()) {
|
|
summary.compressedAnnSkipReasonCounts[*compressedAnnSkipReason]++;
|
|
}
|
|
if (auto v = parseDoubleStat(searchStats, "semantic_rescue_rate")) {
|
|
summary.semanticRescueRateSamples.push_back(*v);
|
|
}
|
|
if (auto v = parseDoubleStat(searchStats, "semantic_rescue_final_count")) {
|
|
summary.semanticRescueFinalCountSamples.push_back(*v);
|
|
if (*v > 0.0) {
|
|
summary.semanticRescueNonZeroQueryCount++;
|
|
}
|
|
}
|
|
if (auto v = parseDoubleStat(searchStats, "semantic_rescue_target")) {
|
|
summary.semanticRescueTargetSamples.push_back(*v);
|
|
}
|
|
if (auto v = parseDoubleStat(searchStats, "fusion_pre_fusion_unique_count")) {
|
|
summary.fusionPreFusionUniqueCountSamples.push_back(*v);
|
|
}
|
|
if (auto v = parseDoubleStat(searchStats, "fusion_post_fusion_count")) {
|
|
summary.fusionPostFusionCountSamples.push_back(*v);
|
|
}
|
|
if (auto v = parseDoubleStat(searchStats, "fusion_dropped_count")) {
|
|
summary.fusionDroppedCountSamples.push_back(*v);
|
|
} else if (auto v = parseDoubleStat(searchStats, "trace_fusion_dropped_count")) {
|
|
summary.fusionDroppedCountSamples.push_back(*v);
|
|
}
|
|
if (auto v = parseDoubleStat(searchStats, "fusion_dropped_rate")) {
|
|
summary.fusionDroppedRateSamples.push_back(*v);
|
|
}
|
|
if (auto v = parseDoubleStat(searchStats, "topology_weak_query_routed_docs")) {
|
|
summary.topologyRoutedDocsSamples.push_back(*v);
|
|
}
|
|
if (auto v = parseDoubleStat(searchStats, "topology_route_best_score")) {
|
|
summary.topologyRouteBestScoreSamples.push_back(*v);
|
|
}
|
|
if (auto v = parseDoubleStat(searchStats, "topology_route_mean_accepted_score")) {
|
|
summary.topologyRouteMeanAcceptedScoreSamples.push_back(*v);
|
|
}
|
|
if (auto v = parseDoubleStat(searchStats, "topology_route_accepted_count")) {
|
|
summary.topologyRouteAcceptedCountSamples.push_back(*v);
|
|
}
|
|
if (auto v = parseDoubleStat(searchStats, "topology_seed_count")) {
|
|
summary.topologySeedCountSamples.push_back(*v);
|
|
}
|
|
if (auto v = parseDoubleStat(searchStats, "topology_seed_coverage_count")) {
|
|
summary.topologySeedCoverageCountSamples.push_back(*v);
|
|
}
|
|
if (auto v = parseDoubleStat(searchStats, "topology_core_candidate_coverage")) {
|
|
summary.topologyCoreCandidateCoverageSamples.push_back(*v);
|
|
}
|
|
if (auto v = parseDoubleStat(searchStats, "topology_weak_query_added_candidates")) {
|
|
summary.topologyAddedCandidatesSamples.push_back(*v);
|
|
}
|
|
if (auto v = parseDoubleStat(searchStats, "topology_weak_query_duplicate_candidates")) {
|
|
summary.topologyDuplicateCandidatesSamples.push_back(*v);
|
|
}
|
|
if (auto v = parseDoubleStat(searchStats, "topology_weak_query_stale_candidates")) {
|
|
summary.topologyStaleCandidatesSamples.push_back(*v);
|
|
}
|
|
if (auto v = parseDoubleStat(searchStats, "anchored_pre_fusion_count")) {
|
|
summary.anchoredPreFusionCountSamples.push_back(*v);
|
|
}
|
|
if (auto v = parseDoubleStat(searchStats, "anchored_fusion_dropped_count")) {
|
|
summary.anchoredFusionDroppedCountSamples.push_back(*v);
|
|
}
|
|
if (auto v = parseDoubleStat(searchStats, "anchored_fusion_dropped_rate")) {
|
|
summary.anchoredFusionDroppedRateSamples.push_back(*v);
|
|
}
|
|
if (auto v = parseDoubleStat(searchStats, "top_text_pre_fusion_count")) {
|
|
summary.topTextPreFusionCountSamples.push_back(*v);
|
|
}
|
|
if (auto v = parseDoubleStat(searchStats, "top_text_fusion_dropped_count")) {
|
|
summary.topTextFusionDroppedCountSamples.push_back(*v);
|
|
}
|
|
if (auto v = parseDoubleStat(searchStats, "top_text_fusion_dropped_rate")) {
|
|
summary.topTextFusionDroppedRateSamples.push_back(*v);
|
|
}
|
|
const bool hasDirectPreFusionStats = searchStats.find("vector_only_docs") != searchStats.end();
|
|
if (auto v = parseDoubleStat(searchStats, "vector_only_docs")) {
|
|
summary.vectorOnlyDocsSamples.push_back(*v);
|
|
}
|
|
if (auto v = parseDoubleStat(searchStats, "vector_only_below_threshold")) {
|
|
summary.vectorOnlyBelowThresholdSamples.push_back(*v);
|
|
}
|
|
if (auto v = parseDoubleStat(searchStats, "vector_only_above_threshold")) {
|
|
summary.vectorOnlyAboveThresholdSamples.push_back(*v);
|
|
}
|
|
if (auto v = parseDoubleStat(searchStats, "vector_only_near_miss_eligible")) {
|
|
summary.vectorOnlyNearMissEligibleSamples.push_back(*v);
|
|
}
|
|
if (parseBoolStat(searchStats, "adaptive_fusion_enabled").value_or(false)) {
|
|
summary.adaptiveFusionEnabledQueryCount++;
|
|
}
|
|
if (auto v = parseDoubleStat(searchStats, "multi_vector_generated_phrases")) {
|
|
summary.multiVectorGeneratedPhraseSamples.push_back(*v);
|
|
}
|
|
if (auto v = parseDoubleStat(searchStats, "multi_vector_raw_hit_count")) {
|
|
summary.multiVectorRawHitSamples.push_back(*v);
|
|
}
|
|
if (auto v = parseDoubleStat(searchStats, "multi_vector_added_new_count")) {
|
|
summary.multiVectorAddedNewSamples.push_back(*v);
|
|
}
|
|
if (auto v = parseDoubleStat(searchStats, "multi_vector_replaced_base_count")) {
|
|
summary.multiVectorReplacedBaseSamples.push_back(*v);
|
|
}
|
|
if (auto v = parseDoubleStat(searchStats, "subphrase_generated_count")) {
|
|
summary.subPhraseGeneratedSamples.push_back(*v);
|
|
}
|
|
if (auto v = parseDoubleStat(searchStats, "subphrase_clause_count")) {
|
|
summary.subPhraseClauseSamples.push_back(*v);
|
|
}
|
|
if (auto v = parseDoubleStat(searchStats, "subphrase_fts_hit_count")) {
|
|
summary.subPhraseFtsHitSamples.push_back(*v);
|
|
}
|
|
if (auto v = parseDoubleStat(searchStats, "subphrase_fts_added_count")) {
|
|
summary.subPhraseFtsAddedSamples.push_back(*v);
|
|
}
|
|
if (auto v = parseDoubleStat(searchStats, "aggressive_fts_clause_count")) {
|
|
summary.aggressiveFtsClauseSamples.push_back(*v);
|
|
}
|
|
if (auto v = parseDoubleStat(searchStats, "aggressive_fts_hit_count")) {
|
|
summary.aggressiveFtsHitSamples.push_back(*v);
|
|
}
|
|
if (auto v = parseDoubleStat(searchStats, "aggressive_fts_added_count")) {
|
|
summary.aggressiveFtsAddedSamples.push_back(*v);
|
|
}
|
|
|
|
for (const auto& timingName : trackedTimingKeys()) {
|
|
const auto key = timingStatKey(timingName);
|
|
if (auto v = parseDoubleStat(searchStats, key)) {
|
|
summary.timingSamplesMs[timingName].push_back(*v);
|
|
}
|
|
}
|
|
|
|
auto prefusion = searchStats.find("trace_prefusion_signal_summary_json");
|
|
if (prefusion != searchStats.end()) {
|
|
try {
|
|
auto parsed = json::parse(prefusion->second);
|
|
if (parsed.is_object()) {
|
|
if (!hasDirectPreFusionStats) {
|
|
if (auto it = parsed.find("vector_only_docs"); it != parsed.end()) {
|
|
if (it->is_number()) {
|
|
summary.vectorOnlyDocsSamples.push_back(it->get<double>());
|
|
}
|
|
}
|
|
if (auto it = parsed.find("vector_only_below_threshold"); it != parsed.end()) {
|
|
if (it->is_number()) {
|
|
summary.vectorOnlyBelowThresholdSamples.push_back(it->get<double>());
|
|
}
|
|
}
|
|
if (auto it = parsed.find("vector_only_above_threshold"); it != parsed.end()) {
|
|
if (it->is_number()) {
|
|
summary.vectorOnlyAboveThresholdSamples.push_back(it->get<double>());
|
|
}
|
|
}
|
|
if (auto it = parsed.find("vector_only_near_miss_eligible");
|
|
it != parsed.end()) {
|
|
if (it->is_number()) {
|
|
summary.vectorOnlyNearMissEligibleSamples.push_back(it->get<double>());
|
|
}
|
|
}
|
|
}
|
|
if (auto it = parsed.find("strong_vector_only_docs"); it != parsed.end()) {
|
|
if (it->is_number()) {
|
|
summary.strongVectorOnlyDocsSamples.push_back(it->get<double>());
|
|
}
|
|
}
|
|
if (auto it = parsed.find("strong_vector_only_score_eligible_docs");
|
|
it != parsed.end()) {
|
|
if (it->is_number()) {
|
|
summary.strongVectorOnlyScoreEligibleDocsSamples.push_back(
|
|
it->get<double>());
|
|
}
|
|
}
|
|
if (auto it = parsed.find("strong_vector_only_rank_eligible_docs");
|
|
it != parsed.end()) {
|
|
if (it->is_number()) {
|
|
summary.strongVectorOnlyRankEligibleDocsSamples.push_back(
|
|
it->get<double>());
|
|
}
|
|
}
|
|
}
|
|
} catch (...) {
|
|
}
|
|
}
|
|
|
|
auto graphDisplacement = searchStats.find("trace_graph_displacement_summary_json");
|
|
if (graphDisplacement != searchStats.end()) {
|
|
try {
|
|
auto parsed = json::parse(graphDisplacement->second);
|
|
if (parsed.is_object()) {
|
|
if (auto it = parsed.find("graph_added_post_fusion_count");
|
|
it != parsed.end() && it->is_number()) {
|
|
summary.graphAddedPostFusionSamples.push_back(it->get<double>());
|
|
}
|
|
if (auto it = parsed.find("graph_displaced_post_fusion_count");
|
|
it != parsed.end() && it->is_number()) {
|
|
summary.graphDisplacedPostFusionSamples.push_back(it->get<double>());
|
|
}
|
|
}
|
|
} catch (...) {
|
|
}
|
|
}
|
|
|
|
if (auto v = parseDoubleStat(searchStats, "graph_community_supported_docs")) {
|
|
summary.graphCommunitySupportedDocsSamples.push_back(*v);
|
|
}
|
|
if (auto v = parseDoubleStat(searchStats, "graph_community_edge_count")) {
|
|
summary.graphCommunityEdgeCountSamples.push_back(*v);
|
|
}
|
|
if (auto v = parseDoubleStat(searchStats, "graph_community_largest_size")) {
|
|
summary.graphCommunityLargestSizeSamples.push_back(*v);
|
|
}
|
|
if (auto v = parseDoubleStat(searchStats, "graph_community_signal_mass")) {
|
|
summary.graphCommunitySignalMassSamples.push_back(*v);
|
|
}
|
|
if (auto v = parseDoubleStat(searchStats, "graph_community_boosted_docs")) {
|
|
summary.graphCommunityBoostedDocsSamples.push_back(*v);
|
|
}
|
|
if (auto v = parseDoubleStat(searchStats, "graph_community_weight")) {
|
|
summary.graphCommunityWeightSamples.push_back(*v);
|
|
}
|
|
|
|
if (auto stageSummary = searchStats.find("trace_stage_summary_json");
|
|
stageSummary != searchStats.end()) {
|
|
try {
|
|
auto parsed = json::parse(stageSummary->second);
|
|
if (parsed.is_object()) {
|
|
for (const auto& [stageName, stageData] : parsed.items()) {
|
|
if (!stageData.is_object()) {
|
|
continue;
|
|
}
|
|
if (stageData.value("enabled", false)) {
|
|
summary.stageEnabledCount[stageName]++;
|
|
}
|
|
if (stageData.value("attempted", false)) {
|
|
summary.stageAttemptedCount[stageName]++;
|
|
}
|
|
if (stageData.value("contributed", false)) {
|
|
summary.stageContributedCount[stageName]++;
|
|
}
|
|
if (stageData.value("skipped", false)) {
|
|
summary.stageSkippedCount[stageName]++;
|
|
auto reason = stageData.value("skip_reason", std::string{});
|
|
if (!reason.empty()) {
|
|
summary.stageSkipReasons[stageName].push_back(reason);
|
|
}
|
|
}
|
|
if (auto it = stageData.find("unique_doc_count");
|
|
it != stageData.end() && it->is_number()) {
|
|
summary.stageUniqueDocCountSamples[stageName].push_back(it->get<double>());
|
|
}
|
|
}
|
|
}
|
|
} catch (...) {
|
|
}
|
|
}
|
|
|
|
if (auto stagePresence = relevantDecisionTrace.find("stage_relevant_presence");
|
|
relevantDecisionTrace.is_object() && stagePresence != relevantDecisionTrace.end() &&
|
|
stagePresence->is_object()) {
|
|
for (const auto& [stageName, stageData] : stagePresence->items()) {
|
|
if (!stageData.is_object()) {
|
|
continue;
|
|
}
|
|
if (stageData.value("any_relevant", false)) {
|
|
summary.stageRelevantPresenceCount[stageName]++;
|
|
}
|
|
if (auto it = stageData.find("relevant_doc_count");
|
|
it != stageData.end() && it->is_number()) {
|
|
summary.stageRelevantDocCountSamples[stageName].push_back(it->get<double>());
|
|
}
|
|
}
|
|
}
|
|
|
|
if (auto fusionSummary = searchStats.find("trace_fusion_source_summary_json");
|
|
fusionSummary != searchStats.end()) {
|
|
try {
|
|
auto parsed = json::parse(fusionSummary->second);
|
|
if (parsed.is_object()) {
|
|
for (const auto& [sourceName, sourceData] : parsed.items()) {
|
|
if (!sourceData.is_object()) {
|
|
continue;
|
|
}
|
|
if (auto it = sourceData.find("final_score_mass");
|
|
it != sourceData.end() && it->is_number()) {
|
|
summary.fusionSourceScoreMassSamples[sourceName].push_back(
|
|
it->get<double>());
|
|
}
|
|
if (auto it = sourceData.find("final_top_doc_count");
|
|
it != sourceData.end() && it->is_number()) {
|
|
summary.fusionSourceFinalDocSamples[sourceName].push_back(
|
|
it->get<double>());
|
|
}
|
|
}
|
|
}
|
|
} catch (...) {
|
|
}
|
|
}
|
|
|
|
if (auto tunerSummary = searchStats.find("tuner_adjustments_json");
|
|
tunerSummary != searchStats.end()) {
|
|
try {
|
|
auto parsed = json::parse(tunerSummary->second);
|
|
if (parsed.is_object()) {
|
|
if (auto decision = parsed.find("last_decision");
|
|
decision != parsed.end() && decision->is_string()) {
|
|
summary.tunerDecisionCounts[decision->get<std::string>()]++;
|
|
}
|
|
static const std::vector<std::string> kKeys = {
|
|
"ewma_latency_ms",
|
|
"ewma_kg_latency_share",
|
|
"ewma_kg_utility",
|
|
"ewma_kg_score_mass_share",
|
|
"ewma_kg_final_doc_share",
|
|
"ewma_kg_contribution_rate",
|
|
"ewma_graph_rerank_latency_ms",
|
|
"ewma_graph_rerank_skip_rate",
|
|
"ewma_graph_rerank_contribution_rate",
|
|
"ewma_fusion_dropped_rate",
|
|
"ewma_anchored_fusion_dropped_rate",
|
|
"ewma_top_text_fusion_dropped_rate",
|
|
"ewma_vector_only_share",
|
|
"ewma_semantic_rescue_rate",
|
|
};
|
|
for (const auto& key : kKeys) {
|
|
if (auto it = parsed.find(key); it != parsed.end() && it->is_number()) {
|
|
summary.tunerSignalSamples[key].push_back(it->get<double>());
|
|
}
|
|
}
|
|
}
|
|
} catch (...) {
|
|
}
|
|
}
|
|
|
|
if (auto runtimeConfig = searchStats.find("tuner_runtime_config_json");
|
|
runtimeConfig != searchStats.end()) {
|
|
try {
|
|
auto parsed = json::parse(runtimeConfig->second);
|
|
if (parsed.is_object()) {
|
|
static const std::vector<std::string> kWeightKeys = {
|
|
"text_weight", "simeon_text_weight", "vector_weight",
|
|
"entity_vector_weight", "path_tree_weight", "kg_weight",
|
|
"tag_weight", "metadata_weight",
|
|
};
|
|
for (const auto& key : kWeightKeys) {
|
|
if (auto it = parsed.find(key); it != parsed.end() && it->is_number()) {
|
|
summary.tunerSignalSamples[key].push_back(it->get<double>());
|
|
}
|
|
}
|
|
}
|
|
} catch (...) {
|
|
}
|
|
}
|
|
}
|
|
|
|
static json queryDiagnosticsToJson(const QueryDiagnosticsSummary& summary) {
|
|
const double queryCount = static_cast<double>(std::max<std::uint64_t>(summary.queryCount, 1));
|
|
|
|
json out = {
|
|
{"query_count", summary.queryCount},
|
|
{"query_with_results_count", summary.queryWithResultsCount},
|
|
{"query_without_results_count", summary.queryWithoutResultsCount},
|
|
{"query_without_relevant_hit_count", summary.queryWithoutRelevantHitCount},
|
|
{"query_with_relevant_in_pre_fusion_count", summary.queryWithRelevantInPreFusionCount},
|
|
{"query_with_relevant_in_post_fusion_count", summary.queryWithRelevantInPostFusionCount},
|
|
{"query_with_relevant_in_graphless_post_fusion_count",
|
|
summary.queryWithRelevantInGraphlessPostFusionCount},
|
|
{"query_with_relevant_in_final_window_count", summary.queryWithRelevantInFinalWindowCount},
|
|
{"query_with_graph_added_relevant_post_fusion_count",
|
|
summary.queryWithGraphAddedRelevantPostFusionCount},
|
|
{"query_with_graph_displaced_relevant_post_fusion_count",
|
|
summary.queryWithGraphDisplacedRelevantPostFusionCount},
|
|
{"query_with_topology_added_relevant_post_fusion_count",
|
|
summary.queryWithTopologyAddedRelevantPostFusionCount},
|
|
{"query_with_topology_added_relevant_fusion_dropped_count",
|
|
summary.queryWithTopologyAddedRelevantFusionDroppedCount},
|
|
{"query_with_topk_duplicate_count", summary.queryWithTopKDuplicateCount},
|
|
{"degraded_query_count", summary.degradedQueryCount},
|
|
{"trace_enabled_query_count", summary.traceEnabledQueryCount},
|
|
{"graph_rerank_applied_query_count", summary.graphRerankAppliedQueryCount},
|
|
{"compressed_ann_enabled_query_count", summary.compressedAnnEnabledQueryCount},
|
|
{"compressed_ann_applied_query_count", summary.compressedAnnAppliedQueryCount},
|
|
{"semantic_rescue_nonzero_query_count", summary.semanticRescueNonZeroQueryCount},
|
|
{"miss_missing_pre_fusion_count", summary.missMissingPreFusionCount},
|
|
{"miss_fusion_cutoff_count", summary.missFusionCutoffCount},
|
|
{"miss_rerank_window_drop_count", summary.missRerankWindowDropCount},
|
|
{"miss_topk_drop_count", summary.missTopKDropCount},
|
|
{"query_with_results_rate",
|
|
static_cast<double>(summary.queryWithResultsCount) / queryCount},
|
|
{"query_without_relevant_hit_rate",
|
|
static_cast<double>(summary.queryWithoutRelevantHitCount) / queryCount},
|
|
{"query_with_relevant_in_pre_fusion_rate",
|
|
static_cast<double>(summary.queryWithRelevantInPreFusionCount) / queryCount},
|
|
{"query_with_relevant_in_post_fusion_rate",
|
|
static_cast<double>(summary.queryWithRelevantInPostFusionCount) / queryCount},
|
|
{"query_with_relevant_in_graphless_post_fusion_rate",
|
|
static_cast<double>(summary.queryWithRelevantInGraphlessPostFusionCount) / queryCount},
|
|
{"query_with_relevant_in_final_window_rate",
|
|
static_cast<double>(summary.queryWithRelevantInFinalWindowCount) / queryCount},
|
|
{"query_with_graph_added_relevant_post_fusion_rate",
|
|
static_cast<double>(summary.queryWithGraphAddedRelevantPostFusionCount) / queryCount},
|
|
{"query_with_graph_displaced_relevant_post_fusion_rate",
|
|
static_cast<double>(summary.queryWithGraphDisplacedRelevantPostFusionCount) / queryCount},
|
|
{"query_with_topology_added_relevant_post_fusion_rate",
|
|
static_cast<double>(summary.queryWithTopologyAddedRelevantPostFusionCount) / queryCount},
|
|
{"query_with_topology_added_relevant_fusion_dropped_rate",
|
|
static_cast<double>(summary.queryWithTopologyAddedRelevantFusionDroppedCount) /
|
|
queryCount},
|
|
{"query_with_topk_duplicate_rate",
|
|
static_cast<double>(summary.queryWithTopKDuplicateCount) / queryCount},
|
|
{"degraded_query_rate", static_cast<double>(summary.degradedQueryCount) / queryCount},
|
|
{"trace_coverage", static_cast<double>(summary.traceEnabledQueryCount) / queryCount},
|
|
{"graph_rerank_apply_rate",
|
|
static_cast<double>(summary.graphRerankAppliedQueryCount) / queryCount},
|
|
{"compressed_ann_enabled_rate",
|
|
static_cast<double>(summary.compressedAnnEnabledQueryCount) / queryCount},
|
|
{"compressed_ann_apply_rate",
|
|
static_cast<double>(summary.compressedAnnAppliedQueryCount) / queryCount},
|
|
{"adaptive_fusion_enabled_query_count", summary.adaptiveFusionEnabledQueryCount},
|
|
{"adaptive_fusion_enabled_rate",
|
|
static_cast<double>(summary.adaptiveFusionEnabledQueryCount) / queryCount},
|
|
{"semantic_rescue_nonzero_rate",
|
|
static_cast<double>(summary.semanticRescueNonZeroQueryCount) / queryCount},
|
|
{"miss_missing_pre_fusion_rate",
|
|
static_cast<double>(summary.missMissingPreFusionCount) / queryCount},
|
|
{"miss_fusion_cutoff_rate",
|
|
static_cast<double>(summary.missFusionCutoffCount) / queryCount},
|
|
{"miss_rerank_window_drop_rate",
|
|
static_cast<double>(summary.missRerankWindowDropCount) / queryCount},
|
|
{"miss_topk_drop_rate", static_cast<double>(summary.missTopKDropCount) / queryCount},
|
|
{"compressed_ann_result_count", summarizeSamples(summary.compressedAnnResultCountSamples)},
|
|
{"compressed_ann_skip_reasons", summary.compressedAnnSkipReasonCounts},
|
|
{"stage_metrics", yams::bench::stageRetrievalMetricsToJson(summary.stageRetrievalMetrics)},
|
|
{"semantic_rescue_rate", summarizeSamples(summary.semanticRescueRateSamples)},
|
|
{"semantic_rescue_final_count", summarizeSamples(summary.semanticRescueFinalCountSamples)},
|
|
{"semantic_rescue_target", summarizeSamples(summary.semanticRescueTargetSamples)},
|
|
{"fusion_pre_fusion_unique_count",
|
|
summarizeSamples(summary.fusionPreFusionUniqueCountSamples)},
|
|
{"fusion_post_fusion_count", summarizeSamples(summary.fusionPostFusionCountSamples)},
|
|
{"fusion_dropped_count", summarizeSamples(summary.fusionDroppedCountSamples)},
|
|
{"fusion_dropped_rate", summarizeSamples(summary.fusionDroppedRateSamples)},
|
|
{"anchored_pre_fusion_count", summarizeSamples(summary.anchoredPreFusionCountSamples)},
|
|
{"anchored_fusion_dropped_count",
|
|
summarizeSamples(summary.anchoredFusionDroppedCountSamples)},
|
|
{"anchored_fusion_dropped_rate",
|
|
summarizeSamples(summary.anchoredFusionDroppedRateSamples)},
|
|
{"top_text_pre_fusion_count", summarizeSamples(summary.topTextPreFusionCountSamples)},
|
|
{"top_text_fusion_dropped_count",
|
|
summarizeSamples(summary.topTextFusionDroppedCountSamples)},
|
|
{"top_text_fusion_dropped_rate", summarizeSamples(summary.topTextFusionDroppedRateSamples)},
|
|
{"vector_only_docs", summarizeSamples(summary.vectorOnlyDocsSamples)},
|
|
{"vector_only_below_threshold", summarizeSamples(summary.vectorOnlyBelowThresholdSamples)},
|
|
{"vector_only_above_threshold", summarizeSamples(summary.vectorOnlyAboveThresholdSamples)},
|
|
{"vector_only_near_miss_eligible",
|
|
summarizeSamples(summary.vectorOnlyNearMissEligibleSamples)},
|
|
{"strong_vector_only_docs", summarizeSamples(summary.strongVectorOnlyDocsSamples)},
|
|
{"strong_vector_only_score_eligible_docs",
|
|
summarizeSamples(summary.strongVectorOnlyScoreEligibleDocsSamples)},
|
|
{"strong_vector_only_rank_eligible_docs",
|
|
summarizeSamples(summary.strongVectorOnlyRankEligibleDocsSamples)},
|
|
{"graph_added_post_fusion_count", summarizeSamples(summary.graphAddedPostFusionSamples)},
|
|
{"graph_displaced_post_fusion_count",
|
|
summarizeSamples(summary.graphDisplacedPostFusionSamples)},
|
|
{"topology_weak_query_routed_docs", summarizeSamples(summary.topologyRoutedDocsSamples)},
|
|
{"topology_route_best_score", summarizeSamples(summary.topologyRouteBestScoreSamples)},
|
|
{"topology_route_mean_accepted_score",
|
|
summarizeSamples(summary.topologyRouteMeanAcceptedScoreSamples)},
|
|
{"topology_route_accepted_count",
|
|
summarizeSamples(summary.topologyRouteAcceptedCountSamples)},
|
|
{"topology_seed_count", summarizeSamples(summary.topologySeedCountSamples)},
|
|
{"topology_seed_coverage_count",
|
|
summarizeSamples(summary.topologySeedCoverageCountSamples)},
|
|
{"topology_core_candidate_coverage",
|
|
summarizeSamples(summary.topologyCoreCandidateCoverageSamples)},
|
|
{"topology_weak_query_added_candidates",
|
|
summarizeSamples(summary.topologyAddedCandidatesSamples)},
|
|
{"topology_weak_query_duplicate_candidates",
|
|
summarizeSamples(summary.topologyDuplicateCandidatesSamples)},
|
|
{"topology_weak_query_stale_candidates",
|
|
summarizeSamples(summary.topologyStaleCandidatesSamples)},
|
|
{"graph_community_supported_docs",
|
|
summarizeSamples(summary.graphCommunitySupportedDocsSamples)},
|
|
{"graph_community_edge_count", summarizeSamples(summary.graphCommunityEdgeCountSamples)},
|
|
{"graph_community_largest_size",
|
|
summarizeSamples(summary.graphCommunityLargestSizeSamples)},
|
|
{"graph_community_signal_mass", summarizeSamples(summary.graphCommunitySignalMassSamples)},
|
|
{"graph_community_boosted_docs",
|
|
summarizeSamples(summary.graphCommunityBoostedDocsSamples)},
|
|
{"graph_community_weight", summarizeSamples(summary.graphCommunityWeightSamples)},
|
|
{"multi_vector_generated_phrases",
|
|
summarizeSamples(summary.multiVectorGeneratedPhraseSamples)},
|
|
{"multi_vector_raw_hit_count", summarizeSamples(summary.multiVectorRawHitSamples)},
|
|
{"multi_vector_added_new_count", summarizeSamples(summary.multiVectorAddedNewSamples)},
|
|
{"multi_vector_replaced_base_count",
|
|
summarizeSamples(summary.multiVectorReplacedBaseSamples)},
|
|
{"subphrase_generated_count", summarizeSamples(summary.subPhraseGeneratedSamples)},
|
|
{"subphrase_clause_count", summarizeSamples(summary.subPhraseClauseSamples)},
|
|
{"subphrase_fts_hit_count", summarizeSamples(summary.subPhraseFtsHitSamples)},
|
|
{"subphrase_fts_added_count", summarizeSamples(summary.subPhraseFtsAddedSamples)},
|
|
{"aggressive_fts_clause_count", summarizeSamples(summary.aggressiveFtsClauseSamples)},
|
|
{"aggressive_fts_hit_count", summarizeSamples(summary.aggressiveFtsHitSamples)},
|
|
{"aggressive_fts_added_count", summarizeSamples(summary.aggressiveFtsAddedSamples)},
|
|
};
|
|
|
|
json timings = json::object();
|
|
for (const auto& timingName : trackedTimingKeys()) {
|
|
auto it = summary.timingSamplesMs.find(timingName);
|
|
if (it != summary.timingSamplesMs.end() && !it->second.empty()) {
|
|
timings[timingName] = summarizeSamples(it->second);
|
|
}
|
|
}
|
|
out["timings_ms"] = timings;
|
|
|
|
json stageUniqueDocCounts = json::object();
|
|
for (const auto& [stageName, samples] : summary.stageUniqueDocCountSamples) {
|
|
stageUniqueDocCounts[stageName] = summarizeSamples(samples);
|
|
}
|
|
out["stage_unique_doc_counts"] = stageUniqueDocCounts;
|
|
|
|
json stageRelevantDocCounts = json::object();
|
|
for (const auto& [stageName, samples] : summary.stageRelevantDocCountSamples) {
|
|
stageRelevantDocCounts[stageName] = summarizeSamples(samples);
|
|
}
|
|
out["stage_relevant_doc_counts"] = stageRelevantDocCounts;
|
|
|
|
json stages = json::object();
|
|
for (const auto& [stageName, enabledCount] : summary.stageEnabledCount) {
|
|
const double enabled = static_cast<double>(enabledCount);
|
|
const auto attemptedIt = summary.stageAttemptedCount.find(stageName);
|
|
const auto contributedIt = summary.stageContributedCount.find(stageName);
|
|
const auto skippedIt = summary.stageSkippedCount.find(stageName);
|
|
const double attempted = attemptedIt != summary.stageAttemptedCount.end()
|
|
? static_cast<double>(attemptedIt->second)
|
|
: 0.0;
|
|
const double contributed = contributedIt != summary.stageContributedCount.end()
|
|
? static_cast<double>(contributedIt->second)
|
|
: 0.0;
|
|
const double skipped = skippedIt != summary.stageSkippedCount.end()
|
|
? static_cast<double>(skippedIt->second)
|
|
: 0.0;
|
|
const auto relevantIt = summary.stageRelevantPresenceCount.find(stageName);
|
|
const double relevantPresence = relevantIt != summary.stageRelevantPresenceCount.end()
|
|
? static_cast<double>(relevantIt->second)
|
|
: 0.0;
|
|
stages[stageName] = {
|
|
{"enabled_rate", enabled / queryCount},
|
|
{"attempt_rate", attempted / queryCount},
|
|
{"contribution_rate", contributed / queryCount},
|
|
{"relevant_presence_rate", relevantPresence / queryCount},
|
|
{"skip_rate", skipped / queryCount},
|
|
{"skip_reasons", summary.stageSkipReasons.contains(stageName)
|
|
? json(summary.stageSkipReasons.at(stageName))
|
|
: json::array()},
|
|
};
|
|
}
|
|
out["stage_contributions"] = stages;
|
|
|
|
json fusionSources = json::object();
|
|
for (const auto& [sourceName, masses] : summary.fusionSourceScoreMassSamples) {
|
|
auto finalDocIt = summary.fusionSourceFinalDocSamples.find(sourceName);
|
|
fusionSources[sourceName] = {
|
|
{"final_score_mass", summarizeSamples(masses)},
|
|
{"final_top_doc_count", finalDocIt != summary.fusionSourceFinalDocSamples.end()
|
|
? summarizeSamples(finalDocIt->second)
|
|
: json{{"count", 0},
|
|
{"mean", 0.0},
|
|
{"p50", 0.0},
|
|
{"p95", 0.0},
|
|
{"min", 0.0},
|
|
{"max", 0.0}}},
|
|
};
|
|
}
|
|
out["fusion_sources"] = fusionSources;
|
|
|
|
json tunerSignals = json::object();
|
|
for (const auto& [signalName, samples] : summary.tunerSignalSamples) {
|
|
tunerSignals[signalName] = summarizeSamples(samples);
|
|
}
|
|
out["tuner_signals"] = tunerSignals;
|
|
out["tuner_decisions"] = summary.tunerDecisionCounts;
|
|
out["zoom_level_counts"] = summary.zoomLevelCounts;
|
|
|
|
return out;
|
|
}
|
|
|
|
static void debugLogWriteJsonLine(const DebugLogEntry& e) {
|
|
if (!g_debugOut)
|
|
return;
|
|
|
|
json j;
|
|
j["query"] = e.query;
|
|
if (e.queryIndex >= 0) {
|
|
j["query_index"] = e.queryIndex;
|
|
}
|
|
j["search_type"] = e.searchType;
|
|
j["relevant_doc_ids"] = e.relevantDocIds;
|
|
j["relevant_files"] = e.relevantFiles;
|
|
j["returned_paths"] = e.returnedPaths;
|
|
j["returned_doc_ids"] = e.returnedDocIds;
|
|
j["returned_scores"] = e.returnedScores;
|
|
j["returned_grades"] = e.returnedGrades;
|
|
j["attempts"] = e.attempts;
|
|
j["used_streaming"] = e.usedStreaming;
|
|
j["used_fuzzy_flag"] = e.usedFuzzyFlag;
|
|
j["used_literal_flag"] = e.usedLiteralFlag;
|
|
j["used_fuzzy_retry"] = e.usedFuzzyRetry;
|
|
j["used_literal_retry"] = e.usedLiteralTextRetry;
|
|
j["search_stats"] = e.searchStats;
|
|
j["diag"] = e.diagnostics;
|
|
|
|
if (const char* embBackend = std::getenv("YAMS_EMBED_BACKEND"); embBackend && *embBackend) {
|
|
j["embedding_backend"] = embBackend;
|
|
} else {
|
|
j["embedding_backend"] = "daemon";
|
|
}
|
|
|
|
if (g_debugRunContext.has_value()) {
|
|
j["run_id"] = g_debugRunContext->runId;
|
|
j["candidate"] = g_debugRunContext->candidate;
|
|
j["dataset"] = g_debugRunContext->dataset;
|
|
j["corpus_size"] = g_debugRunContext->corpusSize;
|
|
j["num_queries"] = g_debugRunContext->numQueries;
|
|
j["top_k"] = g_debugRunContext->topK;
|
|
j["trace_top_n"] = g_debugRunContext->traceTopN;
|
|
j["trace_component_top_n"] = g_debugRunContext->traceComponentTopN;
|
|
j["stage_trace_enabled"] = g_debugRunContext->stageTraceEnabled;
|
|
if (!g_debugRunContext->debugFile.empty()) {
|
|
j["debug_file"] = g_debugRunContext->debugFile;
|
|
}
|
|
}
|
|
|
|
if (e.extraFields.is_object()) {
|
|
for (const auto& [k, v] : e.extraFields.items()) {
|
|
j[k] = v;
|
|
}
|
|
}
|
|
|
|
std::lock_guard<std::mutex> lock(g_debugMutex);
|
|
(*g_debugOut) << j.dump() << "\n";
|
|
g_debugOut->flush();
|
|
}
|
|
|
|
// Store final metrics globally for summary after teardown
|
|
static RetrievalMetrics g_final_metrics;
|
|
static RetrievalMetrics g_keyword_metrics; // FTS5-only metrics for component isolation
|
|
static RetrievalMetrics g_grep_metrics; // grep literal baseline for exact retrieval comparison
|
|
static QueryDiagnosticsSummary g_final_hybrid_diagnostics;
|
|
static QueryDiagnosticsSummary g_final_keyword_diagnostics;
|
|
|
|
struct EnvSetting {
|
|
std::string key;
|
|
std::optional<std::string> value;
|
|
};
|
|
|
|
class ScopedEnvOverrides {
|
|
public:
|
|
explicit ScopedEnvOverrides(const std::vector<EnvSetting>& overrides) {
|
|
environment_.reserve(overrides.size());
|
|
for (const auto& setting : overrides) {
|
|
environment_.emplace_back(setting.key, setting.value);
|
|
}
|
|
}
|
|
|
|
private:
|
|
std::vector<yams::test::ScopedEnvVar> environment_;
|
|
};
|
|
|
|
struct OptimizationCandidate {
|
|
std::string name;
|
|
std::string description;
|
|
std::vector<EnvSetting> envOverrides;
|
|
bool enginePolicyCandidate = false;
|
|
bool diagnosticCandidate = false;
|
|
};
|
|
|
|
struct OptimizationRunResult {
|
|
OptimizationCandidate candidate;
|
|
RetrievalMetrics hybridMetrics;
|
|
RetrievalMetrics keywordMetrics;
|
|
QueryDiagnosticsSummary hybridDiagnostics;
|
|
QueryDiagnosticsSummary keywordDiagnostics;
|
|
std::string tuningState;
|
|
std::string tuningReason;
|
|
std::string runId;
|
|
std::string debugFile;
|
|
int traceTopN = 0;
|
|
int traceComponentTopN = 0;
|
|
bool stageTraceEnabled = false;
|
|
double objectiveScore = 0.0;
|
|
double hybridEvalMs = 0.0;
|
|
double keywordEvalMs = 0.0;
|
|
std::string searchEngine =
|
|
yams::vector::vectorSearchEngineName(yams::vector::VectorSearchEngine::SimeonPqAdc);
|
|
std::uintmax_t vectorsDbBytes = 0;
|
|
std::uintmax_t vectorsWalBytes = 0;
|
|
std::uintmax_t persistedHnswNodes = 0;
|
|
std::uintmax_t persistedHnswMetaRows = 0;
|
|
std::uintmax_t persistedVec0Tables = 0;
|
|
std::uintmax_t persistedVec0Rows = 0;
|
|
std::uintmax_t persistedPqCodes = 0;
|
|
std::uintmax_t persistedPqMeta = 0;
|
|
bool success = false;
|
|
std::string errorMessage;
|
|
};
|
|
|
|
struct BenchCacheMetadata {
|
|
std::string dataset = "synthetic";
|
|
std::string datasetPath;
|
|
int corpusSize = 0;
|
|
int numQueries = 0;
|
|
int topK = 0;
|
|
bool useBEIR = false;
|
|
bool vectorsDisabled = false;
|
|
bool requireKgReady = false;
|
|
bool graphRerankRequested = false;
|
|
std::string searchEngine =
|
|
yams::vector::vectorSearchEngineName(yams::vector::VectorSearchEngine::SimeonPqAdc);
|
|
int expectedDocs = 0;
|
|
int expectedQueries = 0;
|
|
std::uintmax_t corpusFingerprint = 0;
|
|
std::string status = "priming";
|
|
int embeddedDocs = 0;
|
|
std::uintmax_t vectorCount = 0;
|
|
bool vectorIndexReady = false;
|
|
std::uintmax_t vectorsDbBytes = 0;
|
|
std::uintmax_t vectorsWalBytes = 0;
|
|
std::uintmax_t persistedHnswNodes = 0;
|
|
std::uintmax_t persistedHnswMetaRows = 0;
|
|
std::uintmax_t persistedVec0Tables = 0;
|
|
std::uintmax_t persistedVec0Rows = 0;
|
|
std::uintmax_t persistedPqCodes = 0;
|
|
std::uintmax_t persistedPqMeta = 0;
|
|
};
|
|
|
|
struct PersistedHnswState {
|
|
bool vectorsDbPresent = false;
|
|
std::uintmax_t vectorsDbBytes = 0;
|
|
std::uintmax_t vectorsWalBytes = 0;
|
|
std::uintmax_t nodeRows = 0;
|
|
std::uintmax_t metaRows = 0;
|
|
std::uintmax_t vec0Tables = 0;
|
|
std::uintmax_t vec0Rows = 0;
|
|
std::uintmax_t pqCodeRows = 0;
|
|
std::uintmax_t pqMetaRows = 0;
|
|
};
|
|
|
|
static std::string g_benchmark_search_engine =
|
|
yams::vector::vectorSearchEngineName(yams::vector::VectorSearchEngine::SimeonPqAdc);
|
|
static std::string g_simeonEncoderProfile;
|
|
static std::string g_simeonFragmentEncoderProfile;
|
|
static std::optional<bool> g_simeonFragmentGeometryEnabled;
|
|
static std::optional<bool> g_simeonBm25Enabled;
|
|
static std::optional<bool> g_simeonRouterEnabled;
|
|
static bool g_requireDaemonNativeGraph = false;
|
|
static PersistedHnswState g_final_vector_index_state;
|
|
|
|
static void consumeXplanConfigArguments(int& argc, char** argv) {
|
|
const auto takeValue = [](std::string_view argument,
|
|
std::string_view prefix) -> std::optional<std::string> {
|
|
if (!argument.starts_with(prefix)) {
|
|
return std::nullopt;
|
|
}
|
|
return std::string{argument.substr(prefix.size())};
|
|
};
|
|
int outputIndex = 1;
|
|
for (int inputIndex = 1; inputIndex < argc; ++inputIndex) {
|
|
const std::string_view argument{argv[inputIndex]};
|
|
if (auto value = takeValue(argument, "--yams-simeon-encoder-profile="); value.has_value()) {
|
|
g_simeonEncoderProfile = std::move(*value);
|
|
continue;
|
|
}
|
|
if (auto value = takeValue(argument, "--yams-simeon-fragment-encoder-profile=");
|
|
value.has_value()) {
|
|
g_simeonFragmentEncoderProfile = std::move(*value);
|
|
continue;
|
|
}
|
|
if (auto value = takeValue(argument, "--yams-simeon-fragment-geometry=");
|
|
value.has_value()) {
|
|
g_simeonFragmentGeometryEnabled = *value == "true" || *value == "1";
|
|
continue;
|
|
}
|
|
if (auto value = takeValue(argument, "--yams-simeon-bm25="); value.has_value()) {
|
|
g_simeonBm25Enabled = *value == "true" || *value == "1";
|
|
continue;
|
|
}
|
|
if (auto value = takeValue(argument, "--yams-simeon-router="); value.has_value()) {
|
|
g_simeonRouterEnabled = *value == "true" || *value == "1";
|
|
continue;
|
|
}
|
|
if (auto value = takeValue(argument, "--yams-require-daemon-native-graph=");
|
|
value.has_value()) {
|
|
g_requireDaemonNativeGraph = *value == "true" || *value == "1";
|
|
continue;
|
|
}
|
|
argv[outputIndex++] = argv[inputIndex];
|
|
}
|
|
argc = outputIndex;
|
|
}
|
|
|
|
static bool envTruthy(const char* value) {
|
|
if (!value) {
|
|
return false;
|
|
}
|
|
std::string normalized(value);
|
|
std::transform(normalized.begin(), normalized.end(), normalized.begin(), [](char c) {
|
|
return static_cast<char>(std::tolower(static_cast<unsigned char>(c)));
|
|
});
|
|
return normalized == "1" || normalized == "true" || normalized == "yes" || normalized == "on";
|
|
}
|
|
|
|
static yams::vector::VectorSearchEngine benchmarkVectorSearchEngine() {
|
|
if (const char* raw = std::getenv("YAMS_VECTOR_SEARCH_ENGINE"); raw && *raw) {
|
|
std::string normalized(raw);
|
|
std::transform(normalized.begin(), normalized.end(), normalized.begin(), [](char c) {
|
|
return static_cast<char>(std::tolower(static_cast<unsigned char>(c)));
|
|
});
|
|
if (auto parsed = yams::vector::parseVectorSearchEngine(normalized)) {
|
|
return *parsed;
|
|
}
|
|
spdlog::warn(
|
|
"[Bench] Invalid YAMS_VECTOR_SEARCH_ENGINE='{}'; using default {}", raw,
|
|
yams::vector::vectorSearchEngineName(yams::vector::VectorSearchEngine::SimeonPqAdc));
|
|
}
|
|
return yams::vector::VectorSearchEngine::SimeonPqAdc;
|
|
}
|
|
|
|
static std::string benchmarkEngineCacheLabel() {
|
|
const auto engine = benchmarkVectorSearchEngine();
|
|
std::string label = yams::vector::vectorSearchEngineName(engine);
|
|
return label;
|
|
}
|
|
|
|
static bool benchUseStreamingSearch() {
|
|
// Retrieval quality benchmarks measure ranking quality, not IPC chunking behavior.
|
|
// Default to unary search so streaming transport stalls do not skew IR evaluation,
|
|
// while preserving an opt-in path for transport debugging.
|
|
return envTruthy(std::getenv("YAMS_BENCH_ENABLE_STREAMING_SEARCH"));
|
|
}
|
|
|
|
static bool benchEvaluateGrepBaseline() {
|
|
const char* value = std::getenv("YAMS_BENCH_EVALUATE_GREP_BASELINE");
|
|
return value == nullptr || envTruthy(value);
|
|
}
|
|
|
|
static int parseIntEnvOrDefault(const char* key, int defaultValue, int minValue, int maxValue) {
|
|
const int clampedDefault = std::clamp(defaultValue, minValue, maxValue);
|
|
const char* raw = std::getenv(key);
|
|
if (!(raw && *raw)) {
|
|
return clampedDefault;
|
|
}
|
|
try {
|
|
return std::clamp(std::stoi(raw), minValue, maxValue);
|
|
} catch (...) {
|
|
return clampedDefault;
|
|
}
|
|
}
|
|
|
|
static std::size_t parseSizeEnvOrDefault(const char* key, std::size_t defaultValue) {
|
|
const char* raw = std::getenv(key);
|
|
if (!(raw && *raw)) {
|
|
return defaultValue;
|
|
}
|
|
try {
|
|
return static_cast<std::size_t>(std::max<long long>(0, std::stoll(raw)));
|
|
} catch (...) {
|
|
return defaultValue;
|
|
}
|
|
}
|
|
|
|
static std::size_t parseSizeEnvOrFallback(const char* key, const char* fallbackKey,
|
|
std::size_t defaultValue) {
|
|
if (const char* raw = std::getenv(key); raw && *raw) {
|
|
return parseSizeEnvOrDefault(key, defaultValue);
|
|
}
|
|
return parseSizeEnvOrDefault(fallbackKey, defaultValue);
|
|
}
|
|
|
|
static float parseFloatEnvOrDefault(const char* key, float defaultValue) {
|
|
const char* raw = std::getenv(key);
|
|
if (!(raw && *raw)) {
|
|
return defaultValue;
|
|
}
|
|
try {
|
|
return std::stof(raw);
|
|
} catch (...) {
|
|
return defaultValue;
|
|
}
|
|
}
|
|
|
|
static std::string sanitizeLabelForFilename(const std::string& value) {
|
|
std::string out;
|
|
out.reserve(value.size());
|
|
for (char c : value) {
|
|
if (std::isalnum(static_cast<unsigned char>(c)) || c == '-' || c == '_') {
|
|
out.push_back(c);
|
|
} else {
|
|
out.push_back('_');
|
|
}
|
|
}
|
|
return out;
|
|
}
|
|
|
|
static std::string makeRunId(const std::string& candidate) {
|
|
(void)candidate;
|
|
const auto now = std::chrono::system_clock::now().time_since_epoch();
|
|
const auto us = std::chrono::duration_cast<std::chrono::microseconds>(now).count();
|
|
return std::to_string(us);
|
|
}
|
|
|
|
static std::uintmax_t fileSizeOrZero(const fs::path& path) {
|
|
std::error_code ec;
|
|
const auto size = fs::file_size(path, ec);
|
|
return ec ? 0 : size;
|
|
}
|
|
|
|
static std::uintmax_t computePathFingerprint(const fs::path& path) {
|
|
std::error_code ec;
|
|
if (!fs::exists(path, ec)) {
|
|
return 0;
|
|
}
|
|
|
|
if (fs::is_regular_file(path, ec)) {
|
|
return fileSizeOrZero(path);
|
|
}
|
|
|
|
if (!fs::is_directory(path, ec)) {
|
|
return 0;
|
|
}
|
|
|
|
std::uintmax_t fingerprint = 1469598103934665603ull;
|
|
for (fs::recursive_directory_iterator it(path, ec), end; !ec && it != end; it.increment(ec)) {
|
|
const auto entryPath = it->path();
|
|
const auto relative = fs::relative(entryPath, path, ec);
|
|
const std::string relativeString = ec ? entryPath.filename().string() : relative.string();
|
|
for (unsigned char c : relativeString) {
|
|
fingerprint ^= static_cast<std::uintmax_t>(c);
|
|
fingerprint *= 1099511628211ull;
|
|
}
|
|
if (it->is_regular_file(ec)) {
|
|
fingerprint ^= fileSizeOrZero(entryPath);
|
|
fingerprint *= 1099511628211ull;
|
|
}
|
|
ec.clear();
|
|
}
|
|
return fingerprint;
|
|
}
|
|
|
|
static PersistedHnswState inspectPersistedHnswState(const fs::path& dataDir) {
|
|
PersistedHnswState state;
|
|
const fs::path vectorsDbPath = dataDir / "vectors.db";
|
|
if (!fs::exists(vectorsDbPath)) {
|
|
return state;
|
|
}
|
|
|
|
state.vectorsDbPresent = true;
|
|
std::error_code sizeEc;
|
|
state.vectorsDbBytes = fs::file_size(vectorsDbPath, sizeEc);
|
|
if (sizeEc) {
|
|
state.vectorsDbBytes = 0;
|
|
sizeEc.clear();
|
|
}
|
|
const fs::path walPath = vectorsDbPath.string() + "-wal";
|
|
if (fs::exists(walPath)) {
|
|
state.vectorsWalBytes = fs::file_size(walPath, sizeEc);
|
|
if (sizeEc) {
|
|
state.vectorsWalBytes = 0;
|
|
sizeEc.clear();
|
|
}
|
|
}
|
|
|
|
sqlite3* db = nullptr;
|
|
if (sqlite3_open_v2(vectorsDbPath.string().c_str(), &db, SQLITE_OPEN_READONLY, nullptr) !=
|
|
SQLITE_OK) {
|
|
spdlog::warn("[Bench] Failed to open persisted HNSW DB {}: {}", vectorsDbPath.string(),
|
|
db ? sqlite3_errmsg(db) : "open failed");
|
|
if (db) {
|
|
sqlite3_close(db);
|
|
}
|
|
return state;
|
|
}
|
|
sqlite3_busy_timeout(db, 5000);
|
|
|
|
auto closeDb = [&]() {
|
|
if (db) {
|
|
sqlite3_close(db);
|
|
db = nullptr;
|
|
}
|
|
};
|
|
|
|
sqlite3_stmt* stmt = nullptr;
|
|
const char* tableSql =
|
|
"SELECT name FROM sqlite_master WHERE type='table' AND "
|
|
"(name='vectors_hnsw_meta' OR name='vectors_hnsw_nodes' OR "
|
|
" name LIKE 'vectors_%_hnsw_meta' OR name LIKE 'vectors_%_hnsw_nodes' OR "
|
|
" name LIKE 'vectors_%_vec0' OR "
|
|
" name='simeon_pq_codes' OR name='simeon_pq_meta' OR "
|
|
" name LIKE 'vectors_%_simeon_pq_codes' OR name LIKE 'vectors_%_simeon_pq_meta')";
|
|
if (sqlite3_prepare_v2(db, tableSql, -1, &stmt, nullptr) != SQLITE_OK) {
|
|
spdlog::warn("[Bench] Failed to inspect persisted HNSW tables in {}: {}",
|
|
vectorsDbPath.string(), sqlite3_errmsg(db));
|
|
closeDb();
|
|
return state;
|
|
}
|
|
|
|
std::vector<std::string> tables;
|
|
while (sqlite3_step(stmt) == SQLITE_ROW) {
|
|
const auto* text = reinterpret_cast<const char*>(sqlite3_column_text(stmt, 0));
|
|
if (text && *text) {
|
|
tables.emplace_back(text);
|
|
}
|
|
}
|
|
sqlite3_finalize(stmt);
|
|
|
|
for (const auto& table : tables) {
|
|
std::string countTarget = table;
|
|
if (table.find("_vec0") != std::string::npos) {
|
|
// vec0 virtual tables require the module to be loaded on the inspecting connection.
|
|
// For benchmark readiness/footprint checks, count the shadow storage table directly.
|
|
countTarget += "_vectors";
|
|
}
|
|
const std::string countSql = "SELECT COUNT(*) FROM \"" + countTarget + "\"";
|
|
if (sqlite3_prepare_v2(db, countSql.c_str(), -1, &stmt, nullptr) != SQLITE_OK) {
|
|
spdlog::warn("[Bench] Failed to count persisted vector table {} in {}: {}", countTarget,
|
|
vectorsDbPath.string(), sqlite3_errmsg(db));
|
|
continue;
|
|
}
|
|
if (sqlite3_step(stmt) == SQLITE_ROW) {
|
|
const auto count = static_cast<std::uintmax_t>(sqlite3_column_int64(stmt, 0));
|
|
if (table.find("_hnsw_nodes") != std::string::npos) {
|
|
state.nodeRows += count;
|
|
} else if (table.find("_hnsw_meta") != std::string::npos) {
|
|
state.metaRows += count;
|
|
} else if (table.find("_vec0") != std::string::npos) {
|
|
state.vec0Tables += 1;
|
|
state.vec0Rows += count;
|
|
} else if (table.find("simeon_pq_codes") != std::string::npos) {
|
|
state.pqCodeRows += count;
|
|
} else if (table.find("simeon_pq_meta") != std::string::npos) {
|
|
state.pqMetaRows += count;
|
|
}
|
|
}
|
|
sqlite3_finalize(stmt);
|
|
stmt = nullptr;
|
|
}
|
|
|
|
closeDb();
|
|
return state;
|
|
}
|
|
|
|
static json benchCacheMetadataToJson(const BenchCacheMetadata& metadata) {
|
|
return {
|
|
{"dataset", metadata.dataset},
|
|
{"dataset_path", metadata.datasetPath},
|
|
{"corpus_size", metadata.corpusSize},
|
|
{"num_queries", metadata.numQueries},
|
|
{"top_k", metadata.topK},
|
|
{"use_beir", metadata.useBEIR},
|
|
{"vectors_disabled", metadata.vectorsDisabled},
|
|
{"require_kg_ready", metadata.requireKgReady},
|
|
{"graph_rerank_requested", metadata.graphRerankRequested},
|
|
{"search_engine", metadata.searchEngine},
|
|
{"expected_docs", metadata.expectedDocs},
|
|
{"expected_queries", metadata.expectedQueries},
|
|
{"corpus_fingerprint", metadata.corpusFingerprint},
|
|
{"status", metadata.status},
|
|
{"embedded_docs", metadata.embeddedDocs},
|
|
{"vector_count", metadata.vectorCount},
|
|
{"vector_index_ready", metadata.vectorIndexReady},
|
|
{"vectors_db_bytes", metadata.vectorsDbBytes},
|
|
{"vectors_wal_bytes", metadata.vectorsWalBytes},
|
|
{"persisted_hnsw_nodes", metadata.persistedHnswNodes},
|
|
{"persisted_hnsw_meta_rows", metadata.persistedHnswMetaRows},
|
|
{"persisted_vec0_tables", metadata.persistedVec0Tables},
|
|
{"persisted_vec0_rows", metadata.persistedVec0Rows},
|
|
{"persisted_pq_codes", metadata.persistedPqCodes},
|
|
{"persisted_pq_meta", metadata.persistedPqMeta},
|
|
};
|
|
}
|
|
|
|
static std::uintmax_t jsonUintValue(const json& j, const char* key, std::uintmax_t fallback = 0) {
|
|
const auto it = j.find(key);
|
|
if (it == j.end() || it->is_null()) {
|
|
return fallback;
|
|
}
|
|
if (it->is_number_unsigned()) {
|
|
return it->get<std::uintmax_t>();
|
|
}
|
|
if (it->is_number_integer()) {
|
|
const auto value = it->get<std::intmax_t>();
|
|
return value >= 0 ? static_cast<std::uintmax_t>(value) : fallback;
|
|
}
|
|
if (it->is_string()) {
|
|
try {
|
|
return static_cast<std::uintmax_t>(std::stoull(it->get<std::string>()));
|
|
} catch (...) {
|
|
return fallback;
|
|
}
|
|
}
|
|
return fallback;
|
|
}
|
|
|
|
static std::optional<BenchCacheMetadata> parseBenchCacheMetadata(const json& j) {
|
|
if (!j.is_object()) {
|
|
return std::nullopt;
|
|
}
|
|
|
|
BenchCacheMetadata metadata;
|
|
metadata.dataset = j.value("dataset", "synthetic");
|
|
metadata.datasetPath = j.value("dataset_path", "");
|
|
metadata.corpusSize = j.value("corpus_size", 0);
|
|
metadata.numQueries = j.value("num_queries", 0);
|
|
metadata.topK = j.value("top_k", 0);
|
|
metadata.useBEIR = j.value("use_beir", false);
|
|
metadata.vectorsDisabled = j.value("vectors_disabled", false);
|
|
metadata.requireKgReady = j.value("require_kg_ready", false);
|
|
metadata.graphRerankRequested = j.value("graph_rerank_requested", false);
|
|
metadata.searchEngine =
|
|
j.value("search_engine", std::string(yams::vector::vectorSearchEngineName(
|
|
yams::vector::VectorSearchEngine::SimeonPqAdc)));
|
|
metadata.expectedDocs = j.value("expected_docs", 0);
|
|
metadata.expectedQueries = j.value("expected_queries", 0);
|
|
metadata.corpusFingerprint = jsonUintValue(j, "corpus_fingerprint");
|
|
metadata.status = j.value("status", std::string("priming"));
|
|
metadata.embeddedDocs = j.value("embedded_docs", 0);
|
|
metadata.vectorCount = jsonUintValue(j, "vector_count");
|
|
metadata.vectorIndexReady = j.value("vector_index_ready", false);
|
|
metadata.vectorsDbBytes = jsonUintValue(j, "vectors_db_bytes");
|
|
metadata.vectorsWalBytes = jsonUintValue(j, "vectors_wal_bytes");
|
|
metadata.persistedHnswNodes = jsonUintValue(j, "persisted_hnsw_nodes");
|
|
metadata.persistedHnswMetaRows = jsonUintValue(j, "persisted_hnsw_meta_rows");
|
|
metadata.persistedVec0Tables = jsonUintValue(j, "persisted_vec0_tables");
|
|
metadata.persistedVec0Rows = jsonUintValue(j, "persisted_vec0_rows");
|
|
metadata.persistedPqCodes = jsonUintValue(j, "persisted_pq_codes");
|
|
metadata.persistedPqMeta = jsonUintValue(j, "persisted_pq_meta");
|
|
return metadata;
|
|
}
|
|
|
|
static std::string canonicalPathOrEmpty(const fs::path& path) {
|
|
if (path.empty()) {
|
|
return {};
|
|
}
|
|
std::error_code ec;
|
|
const auto canonical = fs::weakly_canonical(path, ec);
|
|
if (!ec) {
|
|
return canonical.string();
|
|
}
|
|
return path.lexically_normal().string();
|
|
}
|
|
|
|
static fs::path benchCacheMetadataPath(const fs::path& cacheDir) {
|
|
return cacheDir / "retrieval_bench_cache.json";
|
|
}
|
|
|
|
static Result<BenchCacheMetadata> readBenchCacheMetadata(const fs::path& cacheDir) {
|
|
const auto metadataPath = benchCacheMetadataPath(cacheDir);
|
|
std::error_code ec;
|
|
if (!fs::exists(metadataPath, ec)) {
|
|
return Error{ErrorCode::NotFound, "Cache metadata not found: " + metadataPath.string()};
|
|
}
|
|
std::ifstream in(metadataPath);
|
|
if (!in) {
|
|
return Error{ErrorCode::IOError, "Failed to open cache metadata: " + metadataPath.string()};
|
|
}
|
|
json j;
|
|
try {
|
|
in >> j;
|
|
} catch (const std::exception& e) {
|
|
return Error{ErrorCode::InvalidArgument,
|
|
"Failed to parse cache metadata: " + std::string(e.what())};
|
|
}
|
|
auto parsed = parseBenchCacheMetadata(j);
|
|
if (!parsed) {
|
|
return Error{ErrorCode::InvalidArgument,
|
|
"Cache metadata schema invalid: " + metadataPath.string()};
|
|
}
|
|
return *parsed;
|
|
}
|
|
|
|
static Result<void> writeBenchCacheMetadata(const fs::path& cacheDir,
|
|
const BenchCacheMetadata& metadata) {
|
|
std::error_code ec;
|
|
fs::create_directories(cacheDir, ec);
|
|
if (ec) {
|
|
return Error{ErrorCode::IOError,
|
|
"Failed to create cache dir: " + cacheDir.string() + ": " + ec.message()};
|
|
}
|
|
const auto metadataPath = benchCacheMetadataPath(cacheDir);
|
|
std::ofstream out(metadataPath);
|
|
if (!out) {
|
|
return Error{ErrorCode::IOError,
|
|
"Failed to write cache metadata: " + metadataPath.string()};
|
|
}
|
|
out << benchCacheMetadataToJson(metadata).dump(2) << "\n";
|
|
return Result<void>{};
|
|
}
|
|
|
|
static bool benchCacheMatches(const BenchCacheMetadata& expected, const BenchCacheMetadata& actual,
|
|
std::string* mismatchReason = nullptr) {
|
|
auto mismatch = [&](std::string reason) {
|
|
if (mismatchReason) {
|
|
*mismatchReason = std::move(reason);
|
|
}
|
|
return false;
|
|
};
|
|
if (expected.dataset != actual.dataset)
|
|
return mismatch("dataset mismatch");
|
|
if (expected.datasetPath != actual.datasetPath)
|
|
return mismatch("dataset path mismatch");
|
|
if (expected.corpusSize != actual.corpusSize)
|
|
return mismatch("corpus size mismatch");
|
|
if (expected.numQueries != actual.numQueries)
|
|
return mismatch("query count mismatch");
|
|
if (expected.topK != actual.topK)
|
|
return mismatch("top-k mismatch");
|
|
if (expected.useBEIR != actual.useBEIR)
|
|
return mismatch("beir flag mismatch");
|
|
if (expected.vectorsDisabled != actual.vectorsDisabled)
|
|
return mismatch("vectors disabled mismatch");
|
|
if (expected.requireKgReady != actual.requireKgReady)
|
|
return mismatch("kg requirement mismatch");
|
|
if (expected.graphRerankRequested != actual.graphRerankRequested)
|
|
return mismatch("graph rerank mismatch");
|
|
if (expected.searchEngine != actual.searchEngine)
|
|
return mismatch("search engine mismatch");
|
|
if (expected.expectedDocs != actual.expectedDocs)
|
|
return mismatch("expected docs mismatch");
|
|
if (expected.expectedQueries != actual.expectedQueries)
|
|
return mismatch("expected queries mismatch");
|
|
if (expected.corpusFingerprint != 0 && actual.corpusFingerprint != 0 &&
|
|
expected.corpusFingerprint != actual.corpusFingerprint) {
|
|
return mismatch("corpus fingerprint mismatch");
|
|
}
|
|
if (actual.status != "primed") {
|
|
return mismatch("cache status not reusable");
|
|
}
|
|
return true;
|
|
}
|
|
|
|
static BenchCacheMetadata
|
|
preferStrongerBenchCacheMetadata(BenchCacheMetadata preferred,
|
|
const std::optional<BenchCacheMetadata>& candidate) {
|
|
if (!candidate) {
|
|
return preferred;
|
|
}
|
|
const auto& c = *candidate;
|
|
if (preferred.datasetPath.empty())
|
|
preferred.datasetPath = c.datasetPath;
|
|
preferred.corpusFingerprint = std::max(preferred.corpusFingerprint, c.corpusFingerprint);
|
|
preferred.expectedDocs = std::max(preferred.expectedDocs, c.expectedDocs);
|
|
preferred.expectedQueries = std::max(preferred.expectedQueries, c.expectedQueries);
|
|
preferred.embeddedDocs = std::max(preferred.embeddedDocs, c.embeddedDocs);
|
|
preferred.vectorCount = std::max(preferred.vectorCount, c.vectorCount);
|
|
preferred.vectorIndexReady = preferred.vectorIndexReady || c.vectorIndexReady;
|
|
preferred.vectorsDbBytes = std::max(preferred.vectorsDbBytes, c.vectorsDbBytes);
|
|
preferred.vectorsWalBytes = std::max(preferred.vectorsWalBytes, c.vectorsWalBytes);
|
|
preferred.persistedHnswNodes = std::max(preferred.persistedHnswNodes, c.persistedHnswNodes);
|
|
preferred.persistedHnswMetaRows =
|
|
std::max(preferred.persistedHnswMetaRows, c.persistedHnswMetaRows);
|
|
preferred.persistedVec0Tables = std::max(preferred.persistedVec0Tables, c.persistedVec0Tables);
|
|
preferred.persistedVec0Rows = std::max(preferred.persistedVec0Rows, c.persistedVec0Rows);
|
|
preferred.persistedPqCodes = std::max(preferred.persistedPqCodes, c.persistedPqCodes);
|
|
preferred.persistedPqMeta = std::max(preferred.persistedPqMeta, c.persistedPqMeta);
|
|
if (preferred.status != "primed" && c.status == "primed") {
|
|
preferred.status = c.status;
|
|
}
|
|
return preferred;
|
|
}
|
|
|
|
static BenchCacheMetadata currentBenchCacheMetadata(BenchCacheMetadata metadata,
|
|
const yams::daemon::StatusResponse& status,
|
|
const fs::path& dataDir,
|
|
bool validatedIndexReusable) {
|
|
auto getCount = [&](std::string_view key) -> std::uintmax_t {
|
|
auto it = status.requestCounts.find(std::string(key));
|
|
return it == status.requestCounts.end() ? 0 : it->second;
|
|
};
|
|
const auto persistedHnsw = inspectPersistedHnswState(dataDir);
|
|
metadata.vectorIndexReady = validatedIndexReusable;
|
|
metadata.vectorsDbBytes = persistedHnsw.vectorsDbBytes;
|
|
metadata.vectorsWalBytes = persistedHnsw.vectorsWalBytes;
|
|
metadata.persistedHnswNodes = persistedHnsw.nodeRows;
|
|
metadata.persistedHnswMetaRows = persistedHnsw.metaRows;
|
|
metadata.persistedVec0Tables = persistedHnsw.vec0Tables;
|
|
metadata.persistedVec0Rows = persistedHnsw.vec0Rows;
|
|
metadata.persistedPqCodes = persistedHnsw.pqCodeRows;
|
|
metadata.persistedPqMeta = persistedHnsw.pqMetaRows;
|
|
metadata.embeddedDocs = static_cast<int>(getCount("documents_embedded"));
|
|
metadata.vectorCount = getCount("vector_count");
|
|
const bool queueDrained = getCount("embed_svc_queued") == 0 && getCount("embed_in_flight") == 0;
|
|
const bool fullCoverage =
|
|
metadata.embeddedDocs >= metadata.expectedDocs && metadata.expectedDocs > 0;
|
|
const bool cacheIndexReady = metadata.vectorsDisabled || metadata.vectorIndexReady;
|
|
metadata.status = (queueDrained && fullCoverage && cacheIndexReady) ? "primed" : "partial";
|
|
return metadata;
|
|
}
|
|
|
|
static fs::path makeCandidateDebugPath(const fs::path& base, const std::string& candidate,
|
|
const std::string& runId) {
|
|
const auto safeCandidate = sanitizeLabelForFilename(candidate);
|
|
const auto stem = base.stem().string();
|
|
const auto ext = base.extension().string();
|
|
const auto suffix = "_" + safeCandidate + "_" + runId;
|
|
if (!stem.empty()) {
|
|
return base.parent_path() / (stem + suffix + ext);
|
|
}
|
|
return base.parent_path() / (base.filename().string() + suffix);
|
|
}
|
|
|
|
static bool hasOverrideKey(const std::vector<EnvSetting>& settings, const std::string& key) {
|
|
return std::any_of(settings.begin(), settings.end(),
|
|
[&](const EnvSetting& setting) { return setting.key == key; });
|
|
}
|
|
|
|
static std::vector<OptimizationCandidate> defaultOptimizationCandidates() {
|
|
return {
|
|
{"auto_baseline",
|
|
"Auto tuner baseline with graph rerank enabled",
|
|
{
|
|
{"YAMS_ENABLE_ENV_OVERRIDES", "1"},
|
|
{"YAMS_BENCH_FORCE_TUNING_OVERRIDE", std::nullopt},
|
|
{"YAMS_TUNING_OVERRIDE", std::nullopt},
|
|
{"YAMS_DISABLE_SEARCH_REBUILDS", "0"},
|
|
{"YAMS_SEARCH_ENABLE_GRAPH_RERANK", "1"},
|
|
{"YAMS_SEARCH_ENABLE_ADAPTIVE_FALLBACK", std::nullopt},
|
|
{"YAMS_CANDIDATE_MULTIPLIER", std::nullopt},
|
|
},
|
|
true},
|
|
{"mixed_precision_profile",
|
|
"MIXED precision preset with lexical and vector guardrails",
|
|
{
|
|
{"YAMS_ENABLE_ENV_OVERRIDES", "1"},
|
|
{"YAMS_BENCH_FORCE_TUNING_OVERRIDE", std::nullopt},
|
|
{"YAMS_TUNING_OVERRIDE", "MIXED_PRECISION"},
|
|
{"YAMS_SEARCH_ENABLE_GRAPH_RERANK", "1"},
|
|
}},
|
|
{"mixed_precision_zoom_map",
|
|
"MIXED_PRECISION with map-level navigation bias",
|
|
{
|
|
{"YAMS_ENABLE_ENV_OVERRIDES", "1"},
|
|
{"YAMS_BENCH_FORCE_TUNING_OVERRIDE", std::nullopt},
|
|
{"YAMS_TUNING_OVERRIDE", "MIXED_PRECISION"},
|
|
{"YAMS_SEARCH_ENABLE_GRAPH_RERANK", "1"},
|
|
{"YAMS_SEARCH_ZOOM_LEVEL", "MAP"},
|
|
}},
|
|
{"mixed_precision_zoom_neighborhood",
|
|
"MIXED_PRECISION with neighborhood navigation bias",
|
|
{
|
|
{"YAMS_ENABLE_ENV_OVERRIDES", "1"},
|
|
{"YAMS_BENCH_FORCE_TUNING_OVERRIDE", std::nullopt},
|
|
{"YAMS_TUNING_OVERRIDE", "MIXED_PRECISION"},
|
|
{"YAMS_SEARCH_ENABLE_GRAPH_RERANK", "1"},
|
|
{"YAMS_SEARCH_ZOOM_LEVEL", "NEIGHBORHOOD"},
|
|
}},
|
|
{"mixed_precision_zoom_street",
|
|
"MIXED_PRECISION with street-level precision bias",
|
|
{
|
|
{"YAMS_ENABLE_ENV_OVERRIDES", "1"},
|
|
{"YAMS_BENCH_FORCE_TUNING_OVERRIDE", std::nullopt},
|
|
{"YAMS_TUNING_OVERRIDE", "MIXED_PRECISION"},
|
|
{"YAMS_SEARCH_ENABLE_GRAPH_RERANK", "1"},
|
|
{"YAMS_SEARCH_ZOOM_LEVEL", "STREET"},
|
|
}},
|
|
{"mixed_precision_semantic_recall_v1",
|
|
"MIXED_PRECISION + looser vector gate + extra semantic rescue",
|
|
{
|
|
{"YAMS_ENABLE_ENV_OVERRIDES", "1"},
|
|
{"YAMS_BENCH_FORCE_TUNING_OVERRIDE", std::nullopt},
|
|
{"YAMS_TUNING_OVERRIDE", "MIXED_PRECISION"},
|
|
{"YAMS_SEARCH_ENABLE_GRAPH_RERANK", "1"},
|
|
{"YAMS_SEARCH_VECTOR_ONLY_THRESHOLD", "0.92"},
|
|
{"YAMS_SEARCH_VECTOR_ONLY_PENALTY", "0.65"},
|
|
{"YAMS_SEARCH_SEMANTIC_RESCUE_SLOTS", "2"},
|
|
{"YAMS_SEARCH_SEMANTIC_RESCUE_MIN_VECTOR_SCORE", "0.30"},
|
|
{"YAMS_SEARCH_ENABLE_ADAPTIVE_FALLBACK", "0"},
|
|
}},
|
|
{"mixed_precision_semantic_recall_lexical14_v1",
|
|
"Semantic recall v1 + stronger lexical floor/tiebreak guardrails",
|
|
{
|
|
{"YAMS_ENABLE_ENV_OVERRIDES", "1"},
|
|
{"YAMS_BENCH_FORCE_TUNING_OVERRIDE", std::nullopt},
|
|
{"YAMS_TUNING_OVERRIDE", "MIXED_PRECISION"},
|
|
{"YAMS_SEARCH_ENABLE_GRAPH_RERANK", "1"},
|
|
{"YAMS_SEARCH_VECTOR_ONLY_THRESHOLD", "0.92"},
|
|
{"YAMS_SEARCH_VECTOR_ONLY_PENALTY", "0.65"},
|
|
{"YAMS_SEARCH_SEMANTIC_RESCUE_SLOTS", "2"},
|
|
{"YAMS_SEARCH_SEMANTIC_RESCUE_MIN_VECTOR_SCORE", "0.30"},
|
|
{"YAMS_SEARCH_ENABLE_ADAPTIVE_FALLBACK", "0"},
|
|
{"YAMS_SEARCH_LEXICAL_FLOOR_TOPN", "14"},
|
|
{"YAMS_SEARCH_LEXICAL_FLOOR_BOOST", "0.22"},
|
|
{"YAMS_SEARCH_ENABLE_LEXICAL_TIEBREAK", "1"},
|
|
{"YAMS_SEARCH_LEXICAL_TIEBREAK_EPS", "0.010"},
|
|
}},
|
|
{"mixed_default",
|
|
"Code/mixed tuned state with benchmark defaults",
|
|
{
|
|
{"YAMS_ENABLE_ENV_OVERRIDES", "1"},
|
|
{"YAMS_BENCH_FORCE_TUNING_OVERRIDE", std::nullopt},
|
|
{"YAMS_TUNING_OVERRIDE", "MIXED"},
|
|
{"YAMS_SEARCH_ENABLE_GRAPH_RERANK", "1"},
|
|
}},
|
|
{"mixed_precision_lexical_floor",
|
|
"MIXED defaults + lexical floor to preserve top text hits",
|
|
{
|
|
{"YAMS_ENABLE_ENV_OVERRIDES", "1"},
|
|
{"YAMS_BENCH_FORCE_TUNING_OVERRIDE", std::nullopt},
|
|
{"YAMS_TUNING_OVERRIDE", "MIXED"},
|
|
{"YAMS_SEARCH_ENABLE_GRAPH_RERANK", "1"},
|
|
{"YAMS_SEARCH_LEXICAL_FLOOR_TOPN", "8"},
|
|
{"YAMS_SEARCH_LEXICAL_FLOOR_BOOST", "0.12"},
|
|
}},
|
|
{"mixed_precision_lexical_floor_strong",
|
|
"MIXED defaults + stronger lexical floor",
|
|
{
|
|
{"YAMS_ENABLE_ENV_OVERRIDES", "1"},
|
|
{"YAMS_BENCH_FORCE_TUNING_OVERRIDE", std::nullopt},
|
|
{"YAMS_TUNING_OVERRIDE", "MIXED"},
|
|
{"YAMS_SEARCH_ENABLE_GRAPH_RERANK", "1"},
|
|
{"YAMS_SEARCH_LEXICAL_FLOOR_TOPN", "12"},
|
|
{"YAMS_SEARCH_LEXICAL_FLOOR_BOOST", "0.20"},
|
|
}},
|
|
{"mixed_precision_lexical_floor_strong_tiebreak",
|
|
"MIXED stronger lexical floor with lexical tie-break",
|
|
{
|
|
{"YAMS_ENABLE_ENV_OVERRIDES", "1"},
|
|
{"YAMS_BENCH_FORCE_TUNING_OVERRIDE", std::nullopt},
|
|
{"YAMS_TUNING_OVERRIDE", "MIXED"},
|
|
{"YAMS_SEARCH_ENABLE_GRAPH_RERANK", "1"},
|
|
{"YAMS_SEARCH_LEXICAL_FLOOR_TOPN", "12"},
|
|
{"YAMS_SEARCH_LEXICAL_FLOOR_BOOST", "0.20"},
|
|
{"YAMS_SEARCH_ENABLE_LEXICAL_TIEBREAK", "1"},
|
|
{"YAMS_SEARCH_LEXICAL_TIEBREAK_EPS", "0.010"},
|
|
}},
|
|
{"small_code_precision",
|
|
"Code-heavy state with stronger lexical/entity bias",
|
|
{
|
|
{"YAMS_ENABLE_ENV_OVERRIDES", "1"},
|
|
{"YAMS_BENCH_FORCE_TUNING_OVERRIDE", std::nullopt},
|
|
{"YAMS_TUNING_OVERRIDE", "SMALL_CODE"},
|
|
{"YAMS_SEARCH_ENABLE_GRAPH_RERANK", "1"},
|
|
{"YAMS_SEARCH_ENABLE_ADAPTIVE_FALLBACK", "0"},
|
|
}},
|
|
{"mixed_recall_push",
|
|
"MIXED state with larger candidate pool and no vector skip",
|
|
{
|
|
{"YAMS_ENABLE_ENV_OVERRIDES", "1"},
|
|
{"YAMS_BENCH_FORCE_TUNING_OVERRIDE", std::nullopt},
|
|
{"YAMS_TUNING_OVERRIDE", "MIXED"},
|
|
{"YAMS_SEARCH_ENABLE_GRAPH_RERANK", "1"},
|
|
{"YAMS_SEARCH_ENABLE_ADAPTIVE_FALLBACK", "0"},
|
|
{"YAMS_CANDIDATE_MULTIPLIER", "2.0"},
|
|
}},
|
|
{"mixed_baseline",
|
|
"MIXED state baseline",
|
|
{
|
|
{"YAMS_ENABLE_ENV_OVERRIDES", "1"},
|
|
{"YAMS_BENCH_FORCE_TUNING_OVERRIDE", std::nullopt},
|
|
{"YAMS_TUNING_OVERRIDE", "MIXED"},
|
|
{"YAMS_SEARCH_ENABLE_GRAPH_RERANK", "1"},
|
|
{"YAMS_SEARCH_ENABLE_ADAPTIVE_FALLBACK", "0"},
|
|
}},
|
|
{"mixed_precision_guardrails",
|
|
"MIXED + weighted reciprocal with vector-only precision guardrails",
|
|
{
|
|
{"YAMS_ENABLE_ENV_OVERRIDES", "1"},
|
|
{"YAMS_BENCH_FORCE_TUNING_OVERRIDE", std::nullopt},
|
|
{"YAMS_TUNING_OVERRIDE", "MIXED"},
|
|
{"YAMS_SEARCH_ENABLE_GRAPH_RERANK", "1"},
|
|
{"YAMS_SEARCH_ENABLE_ADAPTIVE_FALLBACK", "0"},
|
|
{"YAMS_SEARCH_RRF_K", "10"},
|
|
{"YAMS_SEARCH_VECTOR_ONLY_THRESHOLD", "0.93"},
|
|
{"YAMS_SEARCH_VECTOR_ONLY_PENALTY", "0.70"},
|
|
{"YAMS_SEARCH_CONCEPT_BOOST_WEIGHT", "0.07"},
|
|
}},
|
|
{"mixed_precision_mrr_recovery_rrf",
|
|
"MIXED weighted reciprocal with softer vector-only guardrails",
|
|
{
|
|
{"YAMS_ENABLE_ENV_OVERRIDES", "1"},
|
|
{"YAMS_BENCH_FORCE_TUNING_OVERRIDE", std::nullopt},
|
|
{"YAMS_TUNING_OVERRIDE", "MIXED"},
|
|
{"YAMS_SEARCH_ENABLE_GRAPH_RERANK", "1"},
|
|
{"YAMS_SEARCH_ENABLE_ADAPTIVE_FALLBACK", "0"},
|
|
{"YAMS_SEARCH_RRF_K", "16"},
|
|
{"YAMS_SEARCH_VECTOR_ONLY_THRESHOLD", "0.91"},
|
|
{"YAMS_SEARCH_VECTOR_ONLY_PENALTY", "0.82"},
|
|
{"YAMS_SEARCH_CONCEPT_BOOST_WEIGHT", "0.05"},
|
|
{"YAMS_SEARCH_GRAPH_RERANK_WEIGHT", "0.12"},
|
|
}},
|
|
{"mixed_precision_rrf_neighbor_t92_p80_k14",
|
|
"MIXED weighted reciprocal near-neighbor: stricter threshold, lower RRF k",
|
|
{
|
|
{"YAMS_ENABLE_ENV_OVERRIDES", "1"},
|
|
{"YAMS_BENCH_FORCE_TUNING_OVERRIDE", std::nullopt},
|
|
{"YAMS_TUNING_OVERRIDE", "MIXED"},
|
|
{"YAMS_SEARCH_ENABLE_GRAPH_RERANK", "1"},
|
|
{"YAMS_SEARCH_ENABLE_ADAPTIVE_FALLBACK", "0"},
|
|
{"YAMS_SEARCH_RRF_K", "14"},
|
|
{"YAMS_SEARCH_VECTOR_ONLY_THRESHOLD", "0.92"},
|
|
{"YAMS_SEARCH_VECTOR_ONLY_PENALTY", "0.80"},
|
|
{"YAMS_SEARCH_CONCEPT_BOOST_WEIGHT", "0.05"},
|
|
{"YAMS_SEARCH_GRAPH_RERANK_WEIGHT", "0.12"},
|
|
}},
|
|
{"mixed_precision_rrf_neighbor_t92_p78_k16",
|
|
"MIXED weighted reciprocal near-neighbor: stricter threshold and penalty",
|
|
{
|
|
{"YAMS_ENABLE_ENV_OVERRIDES", "1"},
|
|
{"YAMS_BENCH_FORCE_TUNING_OVERRIDE", std::nullopt},
|
|
{"YAMS_TUNING_OVERRIDE", "MIXED"},
|
|
{"YAMS_SEARCH_ENABLE_GRAPH_RERANK", "1"},
|
|
{"YAMS_SEARCH_ENABLE_ADAPTIVE_FALLBACK", "0"},
|
|
{"YAMS_SEARCH_RRF_K", "16"},
|
|
{"YAMS_SEARCH_VECTOR_ONLY_THRESHOLD", "0.92"},
|
|
{"YAMS_SEARCH_VECTOR_ONLY_PENALTY", "0.78"},
|
|
{"YAMS_SEARCH_CONCEPT_BOOST_WEIGHT", "0.05"},
|
|
{"YAMS_SEARCH_GRAPH_RERANK_WEIGHT", "0.12"},
|
|
}},
|
|
{"mixed_precision_rrf_neighbor_t90_p84_k18",
|
|
"MIXED weighted reciprocal near-neighbor: looser threshold, higher RRF k",
|
|
{
|
|
{"YAMS_ENABLE_ENV_OVERRIDES", "1"},
|
|
{"YAMS_BENCH_FORCE_TUNING_OVERRIDE", std::nullopt},
|
|
{"YAMS_TUNING_OVERRIDE", "MIXED"},
|
|
{"YAMS_SEARCH_ENABLE_GRAPH_RERANK", "1"},
|
|
{"YAMS_SEARCH_ENABLE_ADAPTIVE_FALLBACK", "0"},
|
|
{"YAMS_SEARCH_RRF_K", "18"},
|
|
{"YAMS_SEARCH_VECTOR_ONLY_THRESHOLD", "0.90"},
|
|
{"YAMS_SEARCH_VECTOR_ONLY_PENALTY", "0.84"},
|
|
{"YAMS_SEARCH_CONCEPT_BOOST_WEIGHT", "0.05"},
|
|
{"YAMS_SEARCH_GRAPH_RERANK_WEIGHT", "0.12"},
|
|
}},
|
|
{"mixed_precision_rrf_rerank10",
|
|
"MIXED weighted reciprocal with broader rerank window",
|
|
{
|
|
{"YAMS_ENABLE_ENV_OVERRIDES", "1"},
|
|
{"YAMS_BENCH_FORCE_TUNING_OVERRIDE", std::nullopt},
|
|
{"YAMS_TUNING_OVERRIDE", "MIXED"},
|
|
{"YAMS_SEARCH_ENABLE_GRAPH_RERANK", "1"},
|
|
{"YAMS_SEARCH_ENABLE_ADAPTIVE_FALLBACK", "0"},
|
|
{"YAMS_SEARCH_RRF_K", "16"},
|
|
{"YAMS_SEARCH_VECTOR_ONLY_THRESHOLD", "0.91"},
|
|
{"YAMS_SEARCH_VECTOR_ONLY_PENALTY", "0.82"},
|
|
{"YAMS_SEARCH_CONCEPT_BOOST_WEIGHT", "0.05"},
|
|
{"YAMS_SEARCH_GRAPH_RERANK_WEIGHT", "0.12"},
|
|
{"YAMS_SEARCH_ENABLE_RERANKING", "1"},
|
|
{"YAMS_SEARCH_RERANK_TOPK", "10"},
|
|
}},
|
|
{"mixed_ablation_disable_vector_gate",
|
|
"Ablation: disable vector-only threshold/penalty",
|
|
{
|
|
{"YAMS_ENABLE_ENV_OVERRIDES", "1"},
|
|
{"YAMS_BENCH_FORCE_TUNING_OVERRIDE", std::nullopt},
|
|
{"YAMS_TUNING_OVERRIDE", "MIXED"},
|
|
{"YAMS_SEARCH_ENABLE_GRAPH_RERANK", "1"},
|
|
{"YAMS_SEARCH_ENABLE_ADAPTIVE_FALLBACK", "0"},
|
|
{"YAMS_SEARCH_RRF_K", "16"},
|
|
{"YAMS_SEARCH_VECTOR_ONLY_THRESHOLD", "0.00"},
|
|
{"YAMS_SEARCH_VECTOR_ONLY_PENALTY", "1.00"},
|
|
{"YAMS_SEARCH_CONCEPT_BOOST_WEIGHT", "0.05"},
|
|
{"YAMS_SEARCH_GRAPH_RERANK_WEIGHT", "0.12"},
|
|
}},
|
|
{"mixed_ablation_disable_intent_adaptive",
|
|
"Ablation: disable query intent weighting",
|
|
{
|
|
{"YAMS_ENABLE_ENV_OVERRIDES", "1"},
|
|
{"YAMS_BENCH_FORCE_TUNING_OVERRIDE", std::nullopt},
|
|
{"YAMS_TUNING_OVERRIDE", "MIXED"},
|
|
{"YAMS_SEARCH_ENABLE_GRAPH_RERANK", "1"},
|
|
{"YAMS_SEARCH_ENABLE_ADAPTIVE_FALLBACK", "0"},
|
|
{"YAMS_SEARCH_RRF_K", "16"},
|
|
{"YAMS_SEARCH_VECTOR_ONLY_THRESHOLD", "0.91"},
|
|
{"YAMS_SEARCH_VECTOR_ONLY_PENALTY", "0.82"},
|
|
{"YAMS_SEARCH_CONCEPT_BOOST_WEIGHT", "0.05"},
|
|
{"YAMS_SEARCH_GRAPH_RERANK_WEIGHT", "0.12"},
|
|
{"YAMS_SEARCH_ENABLE_INTENT_ADAPTIVE", "0"},
|
|
}},
|
|
{"mixed_ablation_disable_graph_rerank",
|
|
"Ablation: disable graph reranking",
|
|
{
|
|
{"YAMS_ENABLE_ENV_OVERRIDES", "1"},
|
|
{"YAMS_BENCH_FORCE_TUNING_OVERRIDE", std::nullopt},
|
|
{"YAMS_TUNING_OVERRIDE", "MIXED"},
|
|
{"YAMS_SEARCH_ENABLE_GRAPH_RERANK", "0"},
|
|
{"YAMS_SEARCH_ENABLE_ADAPTIVE_FALLBACK", "0"},
|
|
{"YAMS_SEARCH_RRF_K", "16"},
|
|
{"YAMS_SEARCH_VECTOR_ONLY_THRESHOLD", "0.91"},
|
|
{"YAMS_SEARCH_VECTOR_ONLY_PENALTY", "0.82"},
|
|
{"YAMS_SEARCH_CONCEPT_BOOST_WEIGHT", "0.05"},
|
|
{"YAMS_SEARCH_GRAPH_RERANK_WEIGHT", "0.12"},
|
|
}},
|
|
{"mixed_ablation_disable_reranker",
|
|
"Ablation: disable cross-encoder reranking stage",
|
|
{
|
|
{"YAMS_ENABLE_ENV_OVERRIDES", "1"},
|
|
{"YAMS_BENCH_FORCE_TUNING_OVERRIDE", std::nullopt},
|
|
{"YAMS_TUNING_OVERRIDE", "MIXED"},
|
|
{"YAMS_SEARCH_ENABLE_GRAPH_RERANK", "1"},
|
|
{"YAMS_SEARCH_ENABLE_ADAPTIVE_FALLBACK", "0"},
|
|
{"YAMS_SEARCH_RRF_K", "16"},
|
|
{"YAMS_SEARCH_VECTOR_ONLY_THRESHOLD", "0.91"},
|
|
{"YAMS_SEARCH_VECTOR_ONLY_PENALTY", "0.82"},
|
|
{"YAMS_SEARCH_CONCEPT_BOOST_WEIGHT", "0.05"},
|
|
{"YAMS_SEARCH_GRAPH_RERANK_WEIGHT", "0.12"},
|
|
{"YAMS_SEARCH_ENABLE_RERANKING", "0"},
|
|
}},
|
|
{"scientific_precision_guardrails",
|
|
"SCIENTIFIC state with strict vector-only thresholding",
|
|
{
|
|
{"YAMS_ENABLE_ENV_OVERRIDES", "1"},
|
|
{"YAMS_BENCH_FORCE_TUNING_OVERRIDE", std::nullopt},
|
|
{"YAMS_TUNING_OVERRIDE", "SCIENTIFIC"},
|
|
{"YAMS_SEARCH_ENABLE_GRAPH_RERANK", "0"},
|
|
{"YAMS_SEARCH_ENABLE_ADAPTIVE_FALLBACK", "0"},
|
|
{"YAMS_SEARCH_RRF_K", "10"},
|
|
{"YAMS_SEARCH_VECTOR_ONLY_THRESHOLD", "0.94"},
|
|
{"YAMS_SEARCH_VECTOR_ONLY_PENALTY", "0.68"},
|
|
{"YAMS_SEARCH_CONCEPT_BOOST_WEIGHT", "0.08"},
|
|
},
|
|
true},
|
|
// ── SCIENTIFIC retrieval candidates ────────────────────────────
|
|
// Purpose: optimize pre-fusion recall and vector-text balance for
|
|
// scientific/benchmark corpora (SciFact, TREC-COVID, NFCorpus).
|
|
{"scientific_ablation_graph_off",
|
|
"SCIENTIFIC auto baseline with graph rerank disabled",
|
|
{
|
|
{"YAMS_ENABLE_ENV_OVERRIDES", "1"},
|
|
{"YAMS_TUNING_OVERRIDE", std::nullopt},
|
|
{"YAMS_SEARCH_ENABLE_GRAPH_RERANK", "0"},
|
|
{"YAMS_SEARCH_ENABLE_ADAPTIVE_FALLBACK", "0"},
|
|
},
|
|
true},
|
|
{"scientific_ablation_no_semantic_rescue",
|
|
"SCIENTIFIC auto baseline with semantic rescue disabled",
|
|
{
|
|
{"YAMS_ENABLE_ENV_OVERRIDES", "1"},
|
|
{"YAMS_TUNING_OVERRIDE", std::nullopt},
|
|
{"YAMS_SEARCH_ENABLE_GRAPH_RERANK", "1"},
|
|
{"YAMS_SEARCH_SEMANTIC_RESCUE_SLOTS", "0"},
|
|
},
|
|
true},
|
|
{"scientific_semantic_recall_v1",
|
|
"SCIENTIFIC auto + extra semantic rescue + looser vector gate",
|
|
{
|
|
{"YAMS_ENABLE_ENV_OVERRIDES", "1"},
|
|
{"YAMS_TUNING_OVERRIDE", std::nullopt},
|
|
{"YAMS_SEARCH_ENABLE_GRAPH_RERANK", "1"},
|
|
{"YAMS_SEARCH_VECTOR_ONLY_THRESHOLD", "0.92"},
|
|
{"YAMS_SEARCH_VECTOR_ONLY_PENALTY", "0.82"},
|
|
{"YAMS_SEARCH_SEMANTIC_RESCUE_SLOTS", "4"},
|
|
{"YAMS_SEARCH_SEMANTIC_RESCUE_MIN_VECTOR_SCORE", "0.55"},
|
|
{"YAMS_SEARCH_ENABLE_ADAPTIVE_FALLBACK", "0"},
|
|
},
|
|
true},
|
|
{"scientific_vector_fanout_2x",
|
|
"SCIENTIFIC auto + 2x vector fanout (vectorMaxResults 150->300)",
|
|
{
|
|
{"YAMS_ENABLE_ENV_OVERRIDES", "1"},
|
|
{"YAMS_TUNING_OVERRIDE", std::nullopt},
|
|
{"YAMS_SEARCH_ENABLE_GRAPH_RERANK", "1"},
|
|
{"YAMS_CANDIDATE_MULTIPLIER", "2.0"},
|
|
{"YAMS_SEARCH_ENABLE_ADAPTIVE_FALLBACK", "0"},
|
|
},
|
|
true},
|
|
{"scientific_rrf_k6",
|
|
"SCIENTIFIC auto + tighter RRF k=6 (stronger top-rank preference)",
|
|
{
|
|
{"YAMS_ENABLE_ENV_OVERRIDES", "1"},
|
|
{"YAMS_TUNING_OVERRIDE", std::nullopt},
|
|
{"YAMS_SEARCH_ENABLE_GRAPH_RERANK", "1"},
|
|
{"YAMS_SEARCH_RRF_K", "6"},
|
|
{"YAMS_SEARCH_ENABLE_ADAPTIVE_FALLBACK", "0"},
|
|
},
|
|
true},
|
|
{"scientific_rrf_k24",
|
|
"SCIENTIFIC auto + looser RRF k=24 (smoother cross-component fusion)",
|
|
{
|
|
{"YAMS_ENABLE_ENV_OVERRIDES", "1"},
|
|
{"YAMS_TUNING_OVERRIDE", std::nullopt},
|
|
{"YAMS_SEARCH_ENABLE_GRAPH_RERANK", "1"},
|
|
{"YAMS_SEARCH_RRF_K", "24"},
|
|
{"YAMS_SEARCH_ENABLE_ADAPTIVE_FALLBACK", "0"},
|
|
},
|
|
true},
|
|
// ── Aggressive recall-push candidates (corpus-state-aware) ────────
|
|
{"scientific_recall_aggressive_v1",
|
|
"SCI scientific auto + recall push: vec200 sim0.10 rescue6 strong_vec0.60",
|
|
{
|
|
{"YAMS_ENABLE_ENV_OVERRIDES", "1"},
|
|
{"YAMS_TUNING_OVERRIDE", std::nullopt},
|
|
{"YAMS_SEARCH_ENABLE_GRAPH_RERANK", "1"},
|
|
{"YAMS_SEARCH_SIMILARITY_THRESHOLD", "0.10"},
|
|
{"YAMS_SEARCH_VECTOR_MAX_RESULTS", "200"},
|
|
{"YAMS_SEARCH_SEMANTIC_RESCUE_SLOTS", "6"},
|
|
{"YAMS_SEARCH_SEMANTIC_RESCUE_MIN_VECTOR_SCORE", "0.50"},
|
|
{"YAMS_SEARCH_ENABLE_STRONG_VECTOR_ONLY_RELIEF", "1"},
|
|
{"YAMS_SEARCH_STRONG_VECTOR_ONLY_MIN_SCORE", "0.60"},
|
|
{"YAMS_SEARCH_STRONG_VECTOR_ONLY_TOPRANK", "6"},
|
|
{"YAMS_SEARCH_STRONG_VECTOR_ONLY_PENALTY", "0.94"},
|
|
{"YAMS_SEARCH_VECTOR_ONLY_THRESHOLD", "0.82"},
|
|
{"YAMS_SEARCH_VECTOR_ONLY_PENALTY", "0.90"},
|
|
{"YAMS_SEARCH_WEAK_QUERY_VECTOR_FANOUT_MULTIPLIER", "3.0"},
|
|
{"YAMS_SEARCH_ENABLE_ADAPTIVE_FALLBACK", "0"},
|
|
},
|
|
true},
|
|
{"scientific_recall_max_v1",
|
|
"SCI max recall: vec250 sim0.05 rescue8 rerank20 strong_vec0.55 fanout3.5",
|
|
{
|
|
{"YAMS_ENABLE_ENV_OVERRIDES", "1"},
|
|
{"YAMS_TUNING_OVERRIDE", std::nullopt},
|
|
{"YAMS_SEARCH_ENABLE_GRAPH_RERANK", "1"},
|
|
{"YAMS_SEARCH_SIMILARITY_THRESHOLD", "0.05"},
|
|
{"YAMS_SEARCH_VECTOR_MAX_RESULTS", "250"},
|
|
{"YAMS_SEARCH_SEMANTIC_RESCUE_SLOTS", "8"},
|
|
{"YAMS_SEARCH_SEMANTIC_RESCUE_MIN_VECTOR_SCORE", "0.45"},
|
|
{"YAMS_SEARCH_ENABLE_STRONG_VECTOR_ONLY_RELIEF", "1"},
|
|
{"YAMS_SEARCH_STRONG_VECTOR_ONLY_MIN_SCORE", "0.55"},
|
|
{"YAMS_SEARCH_STRONG_VECTOR_ONLY_TOPRANK", "8"},
|
|
{"YAMS_SEARCH_STRONG_VECTOR_ONLY_PENALTY", "0.95"},
|
|
{"YAMS_SEARCH_VECTOR_ONLY_THRESHOLD", "0.80"},
|
|
{"YAMS_SEARCH_VECTOR_ONLY_PENALTY", "0.92"},
|
|
{"YAMS_SEARCH_WEAK_QUERY_VECTOR_FANOUT_MULTIPLIER", "3.5"},
|
|
{"YAMS_SEARCH_RERANK_TOPK", "20"},
|
|
{"YAMS_SEARCH_ENABLE_ADAPTIVE_FALLBACK", "0"},
|
|
},
|
|
true},
|
|
{"scientific_recall_vec300",
|
|
"SCI max pool: vec300 sim0.05 rescue10 rerank25",
|
|
{
|
|
{"YAMS_ENABLE_ENV_OVERRIDES", "1"},
|
|
{"YAMS_TUNING_OVERRIDE", std::nullopt},
|
|
{"YAMS_SEARCH_ENABLE_GRAPH_RERANK", "1"},
|
|
{"YAMS_SEARCH_SIMILARITY_THRESHOLD", "0.05"},
|
|
{"YAMS_SEARCH_VECTOR_MAX_RESULTS", "300"},
|
|
{"YAMS_SEARCH_SEMANTIC_RESCUE_SLOTS", "10"},
|
|
{"YAMS_SEARCH_SEMANTIC_RESCUE_MIN_VECTOR_SCORE", "0.40"},
|
|
{"YAMS_SEARCH_ENABLE_STRONG_VECTOR_ONLY_RELIEF", "1"},
|
|
{"YAMS_SEARCH_STRONG_VECTOR_ONLY_MIN_SCORE", "0.50"},
|
|
{"YAMS_SEARCH_STRONG_VECTOR_ONLY_TOPRANK", "10"},
|
|
{"YAMS_SEARCH_STRONG_VECTOR_ONLY_PENALTY", "0.95"},
|
|
{"YAMS_SEARCH_VECTOR_ONLY_THRESHOLD", "0.78"},
|
|
{"YAMS_SEARCH_VECTOR_ONLY_PENALTY", "0.94"},
|
|
{"YAMS_SEARCH_WEAK_QUERY_VECTOR_FANOUT_MULTIPLIER", "4.0"},
|
|
{"YAMS_SEARCH_RERANK_TOPK", "25"},
|
|
{"YAMS_SEARCH_ENABLE_ADAPTIVE_FALLBACK", "0"},
|
|
},
|
|
true},
|
|
// ── Retrieval-lane candidates (enable disabled lanes for recall) ──
|
|
{"scientific_entity_vec_on",
|
|
"SCI scientific auto + entity vector retrieval enabled (GLiNER concepts)",
|
|
{
|
|
{"YAMS_ENABLE_ENV_OVERRIDES", "1"},
|
|
{"YAMS_TUNING_OVERRIDE", std::nullopt},
|
|
{"YAMS_SEARCH_ENABLE_GRAPH_RERANK", "1"},
|
|
{"YAMS_SEARCH_ENTITY_VECTOR_WEIGHT", "0.15"},
|
|
{"YAMS_SEARCH_ENTITY_VECTOR_MAX_RESULTS", "80"},
|
|
{"YAMS_SEARCH_ENABLE_ADAPTIVE_FALLBACK", "0"},
|
|
},
|
|
true},
|
|
{"scientific_concept_boost_on",
|
|
"SCI scientific auto + concept boosting enabled (mined concepts)",
|
|
{
|
|
{"YAMS_ENABLE_ENV_OVERRIDES", "1"},
|
|
{"YAMS_TUNING_OVERRIDE", std::nullopt},
|
|
{"YAMS_SEARCH_ENABLE_GRAPH_RERANK", "1"},
|
|
{"YAMS_SEARCH_CONCEPT_BOOST_WEIGHT", "0.12"},
|
|
{"YAMS_SEARCH_CONCEPT_MIN_CONFIDENCE", "0.35"},
|
|
{"YAMS_SEARCH_CONCEPT_MAX_COUNT", "8"},
|
|
{"YAMS_SEARCH_CONCEPT_MAX_BOOST", "0.28"},
|
|
{"YAMS_SEARCH_ENABLE_ADAPTIVE_FALLBACK", "0"},
|
|
},
|
|
true},
|
|
{"scientific_entity_concept_combined",
|
|
"SCI entity_vec + concept_boost + rescue=4 (multi-lane recall)",
|
|
{
|
|
{"YAMS_ENABLE_ENV_OVERRIDES", "1"},
|
|
{"YAMS_TUNING_OVERRIDE", std::nullopt},
|
|
{"YAMS_SEARCH_ENABLE_GRAPH_RERANK", "1"},
|
|
{"YAMS_SEARCH_ENTITY_VECTOR_WEIGHT", "0.12"},
|
|
{"YAMS_SEARCH_ENTITY_VECTOR_MAX_RESULTS", "60"},
|
|
{"YAMS_SEARCH_CONCEPT_BOOST_WEIGHT", "0.10"},
|
|
{"YAMS_SEARCH_CONCEPT_MIN_CONFIDENCE", "0.35"},
|
|
{"YAMS_SEARCH_CONCEPT_MAX_COUNT", "6"},
|
|
{"YAMS_SEARCH_CONCEPT_MAX_BOOST", "0.22"},
|
|
{"YAMS_SEARCH_SEMANTIC_RESCUE_SLOTS", "4"},
|
|
{"YAMS_SEARCH_ENABLE_ADAPTIVE_FALLBACK", "0"},
|
|
},
|
|
true},
|
|
{"scientific_rrf_k8",
|
|
"SCI scientific auto + RRF k=8 (tight top-rank focus)",
|
|
{
|
|
{"YAMS_ENABLE_ENV_OVERRIDES", "1"},
|
|
{"YAMS_TUNING_OVERRIDE", std::nullopt},
|
|
{"YAMS_SEARCH_ENABLE_GRAPH_RERANK", "1"},
|
|
{"YAMS_SEARCH_RRF_K", "8"},
|
|
{"YAMS_SEARCH_ENABLE_ADAPTIVE_FALLBACK", "0"},
|
|
},
|
|
true},
|
|
{"scientific_subphrase_aggressive",
|
|
"SCI + aggressive sub-phrase expansion + rescue=4",
|
|
{
|
|
{"YAMS_ENABLE_ENV_OVERRIDES", "1"},
|
|
{"YAMS_TUNING_OVERRIDE", std::nullopt},
|
|
{"YAMS_SEARCH_ENABLE_GRAPH_RERANK", "1"},
|
|
{"YAMS_SEARCH_ENABLE_SUB_PHRASE_EXPANSION", "1"},
|
|
{"YAMS_SEARCH_SUB_PHRASE_EXPANSION_MIN_HITS", "2"},
|
|
{"YAMS_SEARCH_SUB_PHRASE_EXPANSION_PENALTY", "0.60"},
|
|
{"YAMS_SEARCH_SEMANTIC_RESCUE_SLOTS", "4"},
|
|
{"YAMS_SEARCH_ENABLE_ADAPTIVE_FALLBACK", "0"},
|
|
},
|
|
true},
|
|
{"scientific_combined_v1",
|
|
"entity_vec + concept_boost + rrf_k6 + rescue=4 + subphrase aggressive",
|
|
{
|
|
{"YAMS_ENABLE_ENV_OVERRIDES", "1"},
|
|
{"YAMS_TUNING_OVERRIDE", std::nullopt},
|
|
{"YAMS_SEARCH_ENABLE_GRAPH_RERANK", "1"},
|
|
{"YAMS_SEARCH_ENTITY_VECTOR_WEIGHT", "0.12"},
|
|
{"YAMS_SEARCH_ENTITY_VECTOR_MAX_RESULTS", "60"},
|
|
{"YAMS_SEARCH_CONCEPT_BOOST_WEIGHT", "0.10"},
|
|
{"YAMS_SEARCH_CONCEPT_MIN_CONFIDENCE", "0.35"},
|
|
{"YAMS_SEARCH_CONCEPT_MAX_COUNT", "6"},
|
|
{"YAMS_SEARCH_CONCEPT_MAX_BOOST", "0.22"},
|
|
{"YAMS_SEARCH_RRF_K", "6"},
|
|
{"YAMS_SEARCH_SEMANTIC_RESCUE_SLOTS", "4"},
|
|
{"YAMS_SEARCH_ENABLE_SUB_PHRASE_EXPANSION", "1"},
|
|
{"YAMS_SEARCH_SUB_PHRASE_EXPANSION_MIN_HITS", "2"},
|
|
{"YAMS_SEARCH_SUB_PHRASE_EXPANSION_PENALTY", "0.60"},
|
|
{"YAMS_SEARCH_ENABLE_ADAPTIVE_FALLBACK", "0"},
|
|
},
|
|
true},
|
|
{"scientific_text_heavy",
|
|
"SCI text weight 0.55 + simeon 0.15 + vector 0.20 (lexical-first)",
|
|
{
|
|
{"YAMS_ENABLE_ENV_OVERRIDES", "1"},
|
|
{"YAMS_TUNING_OVERRIDE", std::nullopt},
|
|
{"YAMS_SEARCH_ENABLE_GRAPH_RERANK", "1"},
|
|
{"YAMS_SEARCH_TEXT_WEIGHT", "0.55"},
|
|
{"YAMS_SEARCH_SIMEON_TEXT_WEIGHT", "0.15"},
|
|
{"YAMS_SEARCH_VECTOR_WEIGHT", "0.20"},
|
|
{"YAMS_SEARCH_RRF_K", "6"},
|
|
{"YAMS_SEARCH_ENABLE_ADAPTIVE_FALLBACK", "0"},
|
|
},
|
|
true},
|
|
{"scientific_strategy_router",
|
|
"SCI + simeon EntropyRouter (BM25/Keyphrase/LeadField per-query strategy)",
|
|
{
|
|
{"YAMS_ENABLE_ENV_OVERRIDES", "1"},
|
|
{"YAMS_TUNING_OVERRIDE", std::nullopt},
|
|
{"YAMS_SEARCH_ENABLE_GRAPH_RERANK", "1"},
|
|
{"YAMS_SIMEON_STRATEGY_ROUTER_ENABLED", "1"},
|
|
{"YAMS_SEARCH_ENABLE_ADAPTIVE_FALLBACK", "0"},
|
|
},
|
|
true},
|
|
{"scientific_strategy_router_rrf6",
|
|
"SCI EntropyRouter + RRF k=6 + rescue=4 + subphrase aggressive",
|
|
{
|
|
{"YAMS_ENABLE_ENV_OVERRIDES", "1"},
|
|
{"YAMS_TUNING_OVERRIDE", std::nullopt},
|
|
{"YAMS_SEARCH_ENABLE_GRAPH_RERANK", "1"},
|
|
{"YAMS_SIMEON_STRATEGY_ROUTER_ENABLED", "1"},
|
|
{"YAMS_SEARCH_RRF_K", "6"},
|
|
{"YAMS_SEARCH_SEMANTIC_RESCUE_SLOTS", "4"},
|
|
{"YAMS_SEARCH_ENABLE_SUB_PHRASE_EXPANSION", "1"},
|
|
{"YAMS_SEARCH_SUB_PHRASE_EXPANSION_MIN_HITS", "2"},
|
|
{"YAMS_SEARCH_SUB_PHRASE_EXPANSION_PENALTY", "0.60"},
|
|
{"YAMS_SEARCH_ENABLE_ADAPTIVE_FALLBACK", "0"},
|
|
},
|
|
true},
|
|
{"mixed_graph_off",
|
|
"MIXED state with graph rerank disabled",
|
|
{
|
|
{"YAMS_ENABLE_ENV_OVERRIDES", "1"},
|
|
{"YAMS_BENCH_FORCE_TUNING_OVERRIDE", std::nullopt},
|
|
{"YAMS_TUNING_OVERRIDE", "MIXED"},
|
|
{"YAMS_SEARCH_ENABLE_GRAPH_RERANK", "0"},
|
|
{"YAMS_SEARCH_ENABLE_ADAPTIVE_FALLBACK", "0"},
|
|
}},
|
|
|
|
// ── Tier-1 diagnostic candidates ──────────────────────────────
|
|
// Purpose: determine if the 14 both-miss queries are recoverable
|
|
// by expanding the candidate pool and/or the evaluation window.
|
|
|
|
{"diag_winner_3x_candidates",
|
|
"Winner config + 3x candidate multiplier (vector pool 150->450)",
|
|
{
|
|
{"YAMS_ENABLE_ENV_OVERRIDES", "1"},
|
|
{"YAMS_BENCH_FORCE_TUNING_OVERRIDE", std::nullopt},
|
|
{"YAMS_TUNING_OVERRIDE", "MIXED_PRECISION"},
|
|
{"YAMS_SEARCH_ENABLE_GRAPH_RERANK", "1"},
|
|
{"YAMS_SEARCH_VECTOR_ONLY_THRESHOLD", "0.92"},
|
|
{"YAMS_SEARCH_VECTOR_ONLY_PENALTY", "0.65"},
|
|
{"YAMS_SEARCH_SEMANTIC_RESCUE_SLOTS", "2"},
|
|
{"YAMS_SEARCH_SEMANTIC_RESCUE_MIN_VECTOR_SCORE", "0.30"},
|
|
{"YAMS_SEARCH_ENABLE_ADAPTIVE_FALLBACK", "0"},
|
|
{"YAMS_SEARCH_LEXICAL_FLOOR_TOPN", "14"},
|
|
{"YAMS_SEARCH_LEXICAL_FLOOR_BOOST", "0.22"},
|
|
{"YAMS_SEARCH_ENABLE_LEXICAL_TIEBREAK", "1"},
|
|
{"YAMS_SEARCH_LEXICAL_TIEBREAK_EPS", "0.010"},
|
|
{"YAMS_CANDIDATE_MULTIPLIER", "3.0"},
|
|
},
|
|
false,
|
|
true},
|
|
{"diag_winner_topk20",
|
|
"Winner config + topK=20 (recall@20 diagnostic)",
|
|
{
|
|
{"YAMS_ENABLE_ENV_OVERRIDES", "1"},
|
|
{"YAMS_BENCH_FORCE_TUNING_OVERRIDE", std::nullopt},
|
|
{"YAMS_TUNING_OVERRIDE", "MIXED_PRECISION"},
|
|
{"YAMS_SEARCH_ENABLE_GRAPH_RERANK", "1"},
|
|
{"YAMS_SEARCH_VECTOR_ONLY_THRESHOLD", "0.92"},
|
|
{"YAMS_SEARCH_VECTOR_ONLY_PENALTY", "0.65"},
|
|
{"YAMS_SEARCH_SEMANTIC_RESCUE_SLOTS", "2"},
|
|
{"YAMS_SEARCH_SEMANTIC_RESCUE_MIN_VECTOR_SCORE", "0.30"},
|
|
{"YAMS_SEARCH_ENABLE_ADAPTIVE_FALLBACK", "0"},
|
|
{"YAMS_SEARCH_LEXICAL_FLOOR_TOPN", "14"},
|
|
{"YAMS_SEARCH_LEXICAL_FLOOR_BOOST", "0.22"},
|
|
{"YAMS_SEARCH_ENABLE_LEXICAL_TIEBREAK", "1"},
|
|
{"YAMS_SEARCH_LEXICAL_TIEBREAK_EPS", "0.010"},
|
|
{"YAMS_BENCH_TOPK", "20"},
|
|
},
|
|
false,
|
|
true},
|
|
{"diag_winner_topk50",
|
|
"Winner config + topK=50 (recall@50 diagnostic)",
|
|
{
|
|
{"YAMS_ENABLE_ENV_OVERRIDES", "1"},
|
|
{"YAMS_BENCH_FORCE_TUNING_OVERRIDE", std::nullopt},
|
|
{"YAMS_TUNING_OVERRIDE", "MIXED_PRECISION"},
|
|
{"YAMS_SEARCH_ENABLE_GRAPH_RERANK", "1"},
|
|
{"YAMS_SEARCH_VECTOR_ONLY_THRESHOLD", "0.92"},
|
|
{"YAMS_SEARCH_VECTOR_ONLY_PENALTY", "0.65"},
|
|
{"YAMS_SEARCH_SEMANTIC_RESCUE_SLOTS", "2"},
|
|
{"YAMS_SEARCH_SEMANTIC_RESCUE_MIN_VECTOR_SCORE", "0.30"},
|
|
{"YAMS_SEARCH_ENABLE_ADAPTIVE_FALLBACK", "0"},
|
|
{"YAMS_SEARCH_LEXICAL_FLOOR_TOPN", "14"},
|
|
{"YAMS_SEARCH_LEXICAL_FLOOR_BOOST", "0.22"},
|
|
{"YAMS_SEARCH_ENABLE_LEXICAL_TIEBREAK", "1"},
|
|
{"YAMS_SEARCH_LEXICAL_TIEBREAK_EPS", "0.010"},
|
|
{"YAMS_BENCH_TOPK", "50"},
|
|
},
|
|
false,
|
|
true},
|
|
{"diag_winner_3x_topk50",
|
|
"Winner + 3x candidates + topK=50 (maximum retrieval diagnostic)",
|
|
{
|
|
{"YAMS_ENABLE_ENV_OVERRIDES", "1"},
|
|
{"YAMS_BENCH_FORCE_TUNING_OVERRIDE", std::nullopt},
|
|
{"YAMS_TUNING_OVERRIDE", "MIXED_PRECISION"},
|
|
{"YAMS_SEARCH_ENABLE_GRAPH_RERANK", "1"},
|
|
{"YAMS_SEARCH_VECTOR_ONLY_THRESHOLD", "0.92"},
|
|
{"YAMS_SEARCH_VECTOR_ONLY_PENALTY", "0.65"},
|
|
{"YAMS_SEARCH_SEMANTIC_RESCUE_SLOTS", "2"},
|
|
{"YAMS_SEARCH_SEMANTIC_RESCUE_MIN_VECTOR_SCORE", "0.30"},
|
|
{"YAMS_SEARCH_ENABLE_ADAPTIVE_FALLBACK", "0"},
|
|
{"YAMS_SEARCH_LEXICAL_FLOOR_TOPN", "14"},
|
|
{"YAMS_SEARCH_LEXICAL_FLOOR_BOOST", "0.22"},
|
|
{"YAMS_SEARCH_ENABLE_LEXICAL_TIEBREAK", "1"},
|
|
{"YAMS_SEARCH_LEXICAL_TIEBREAK_EPS", "0.010"},
|
|
{"YAMS_CANDIDATE_MULTIPLIER", "3.0"},
|
|
{"YAMS_BENCH_TOPK", "50"},
|
|
},
|
|
false,
|
|
true},
|
|
|
|
// ── Tier-2 cross-encoder reranker candidates ──────────────────
|
|
// Strategy: expand pool (3x) to get ~50 candidates, then use the
|
|
// bge-reranker-large cross-encoder to rescore and promote relevant
|
|
// documents that the bi-encoder ranked at positions 11-49 into
|
|
// the top-10. Tier-1 diagnostics showed 7 of 14 both-miss
|
|
// queries have relevant docs at ranks 11-49 — reranking should
|
|
// recover these.
|
|
// Current benchmark evidence: blend-60 beats blend-70 on both q20 and q300,
|
|
// while graph rerank / semantic rescue stay effectively inactive on SciFact.
|
|
// Treat rerank_3x_blend60_top50 as the engine-policy baseline; heavier recall probes
|
|
// below remain diagnostics unless they win on both quality and latency.
|
|
|
|
{"rerank_3x_replace_top50",
|
|
"3x pool + cross-encoder rerank top-50, replace scores (max reranker impact)",
|
|
{
|
|
{"YAMS_ENABLE_ENV_OVERRIDES", "1"},
|
|
{"YAMS_BENCH_FORCE_TUNING_OVERRIDE", std::nullopt},
|
|
{"YAMS_TUNING_OVERRIDE", "MIXED_PRECISION"},
|
|
{"YAMS_SEARCH_ENABLE_GRAPH_RERANK", "1"},
|
|
{"YAMS_SEARCH_VECTOR_ONLY_THRESHOLD", "0.92"},
|
|
{"YAMS_SEARCH_VECTOR_ONLY_PENALTY", "0.65"},
|
|
{"YAMS_SEARCH_SEMANTIC_RESCUE_SLOTS", "2"},
|
|
{"YAMS_SEARCH_SEMANTIC_RESCUE_MIN_VECTOR_SCORE", "0.30"},
|
|
{"YAMS_SEARCH_ENABLE_ADAPTIVE_FALLBACK", "0"},
|
|
{"YAMS_SEARCH_LEXICAL_FLOOR_TOPN", "14"},
|
|
{"YAMS_SEARCH_LEXICAL_FLOOR_BOOST", "0.22"},
|
|
{"YAMS_SEARCH_ENABLE_LEXICAL_TIEBREAK", "1"},
|
|
{"YAMS_SEARCH_LEXICAL_TIEBREAK_EPS", "0.010"},
|
|
{"YAMS_CANDIDATE_MULTIPLIER", "3.0"},
|
|
// Cross-encoder reranking: rerank the top-50 candidates
|
|
{"YAMS_SEARCH_ENABLE_RERANKING", "1"},
|
|
{"YAMS_SEARCH_RERANK_TOPK", "50"},
|
|
{"YAMS_SEARCH_RERANK_REPLACE_SCORES", "1"},
|
|
{"YAMS_SEARCH_RERANK_SCORE_GAP_THRESHOLD", "0.0"},
|
|
{"YAMS_SEARCH_RERANK_SNIPPET_MAX_CHARS", "256"},
|
|
}},
|
|
{"rerank_3x_blend60_top50",
|
|
"Current q300 winner: 3x pool + cross-encoder rerank top-50, blend 60/40 with fusion "
|
|
"scores",
|
|
{
|
|
{"YAMS_ENABLE_ENV_OVERRIDES", "1"},
|
|
{"YAMS_BENCH_FORCE_TUNING_OVERRIDE", std::nullopt},
|
|
{"YAMS_TUNING_OVERRIDE", "MIXED_PRECISION"},
|
|
{"YAMS_SEARCH_ENABLE_GRAPH_RERANK", "1"},
|
|
{"YAMS_SEARCH_VECTOR_ONLY_THRESHOLD", "0.92"},
|
|
{"YAMS_SEARCH_VECTOR_ONLY_PENALTY", "0.65"},
|
|
{"YAMS_SEARCH_SEMANTIC_RESCUE_SLOTS", "2"},
|
|
{"YAMS_SEARCH_SEMANTIC_RESCUE_MIN_VECTOR_SCORE", "0.30"},
|
|
{"YAMS_SEARCH_ENABLE_ADAPTIVE_FALLBACK", "0"},
|
|
{"YAMS_SEARCH_LEXICAL_FLOOR_TOPN", "14"},
|
|
{"YAMS_SEARCH_LEXICAL_FLOOR_BOOST", "0.22"},
|
|
{"YAMS_SEARCH_ENABLE_LEXICAL_TIEBREAK", "1"},
|
|
{"YAMS_SEARCH_LEXICAL_TIEBREAK_EPS", "0.010"},
|
|
{"YAMS_CANDIDATE_MULTIPLIER", "3.0"},
|
|
// Cross-encoder reranking: rerank the top-50, blend scores
|
|
{"YAMS_SEARCH_ENABLE_RERANKING", "1"},
|
|
{"YAMS_SEARCH_RERANK_TOPK", "50"},
|
|
{"YAMS_SEARCH_RERANK_REPLACE_SCORES", "0"},
|
|
{"YAMS_SEARCH_RERANK_WEIGHT", "0.60"},
|
|
{"YAMS_SEARCH_RERANK_SCORE_GAP_THRESHOLD", "0.0"},
|
|
{"YAMS_SEARCH_RERANK_SNIPPET_MAX_CHARS", "256"},
|
|
},
|
|
true,
|
|
false},
|
|
{"rerank_4x_blend60_top50",
|
|
"Pre-fusion recall probe: winner + larger candidate multiplier (4x), rerank window stays "
|
|
"at top-50",
|
|
{
|
|
{"YAMS_ENABLE_ENV_OVERRIDES", "1"},
|
|
{"YAMS_BENCH_FORCE_TUNING_OVERRIDE", std::nullopt},
|
|
{"YAMS_TUNING_OVERRIDE", "MIXED_PRECISION"},
|
|
{"YAMS_SEARCH_ENABLE_GRAPH_RERANK", "1"},
|
|
{"YAMS_SEARCH_VECTOR_ONLY_THRESHOLD", "0.92"},
|
|
{"YAMS_SEARCH_VECTOR_ONLY_PENALTY", "0.65"},
|
|
{"YAMS_SEARCH_SEMANTIC_RESCUE_SLOTS", "2"},
|
|
{"YAMS_SEARCH_SEMANTIC_RESCUE_MIN_VECTOR_SCORE", "0.30"},
|
|
{"YAMS_SEARCH_ENABLE_ADAPTIVE_FALLBACK", "0"},
|
|
{"YAMS_SEARCH_LEXICAL_FLOOR_TOPN", "14"},
|
|
{"YAMS_SEARCH_LEXICAL_FLOOR_BOOST", "0.22"},
|
|
{"YAMS_SEARCH_ENABLE_LEXICAL_TIEBREAK", "1"},
|
|
{"YAMS_SEARCH_LEXICAL_TIEBREAK_EPS", "0.010"},
|
|
{"YAMS_CANDIDATE_MULTIPLIER", "4.0"},
|
|
{"YAMS_SEARCH_ENABLE_RERANKING", "1"},
|
|
{"YAMS_SEARCH_RERANK_TOPK", "50"},
|
|
{"YAMS_SEARCH_RERANK_REPLACE_SCORES", "0"},
|
|
{"YAMS_SEARCH_RERANK_WEIGHT", "0.60"},
|
|
{"YAMS_SEARCH_RERANK_SCORE_GAP_THRESHOLD", "0.0"},
|
|
{"YAMS_SEARCH_RERANK_SNIPPET_MAX_CHARS", "256"},
|
|
},
|
|
false,
|
|
true},
|
|
{"rerank_3x_blend60_top50_fuse100",
|
|
"Pre-fusion recall probe: winner + moderately wider fusion window (fusion limit 100)",
|
|
{
|
|
{"YAMS_ENABLE_ENV_OVERRIDES", "1"},
|
|
{"YAMS_BENCH_FORCE_TUNING_OVERRIDE", std::nullopt},
|
|
{"YAMS_TUNING_OVERRIDE", "MIXED_PRECISION"},
|
|
{"YAMS_SEARCH_ENABLE_GRAPH_RERANK", "1"},
|
|
{"YAMS_SEARCH_VECTOR_ONLY_THRESHOLD", "0.92"},
|
|
{"YAMS_SEARCH_VECTOR_ONLY_PENALTY", "0.65"},
|
|
{"YAMS_SEARCH_SEMANTIC_RESCUE_SLOTS", "2"},
|
|
{"YAMS_SEARCH_SEMANTIC_RESCUE_MIN_VECTOR_SCORE", "0.30"},
|
|
{"YAMS_SEARCH_ENABLE_ADAPTIVE_FALLBACK", "0"},
|
|
{"YAMS_SEARCH_LEXICAL_FLOOR_TOPN", "14"},
|
|
{"YAMS_SEARCH_LEXICAL_FLOOR_BOOST", "0.22"},
|
|
{"YAMS_SEARCH_ENABLE_LEXICAL_TIEBREAK", "1"},
|
|
{"YAMS_SEARCH_LEXICAL_TIEBREAK_EPS", "0.010"},
|
|
{"YAMS_CANDIDATE_MULTIPLIER", "3.0"},
|
|
{"YAMS_SEARCH_FUSION_CANDIDATE_LIMIT", "100"},
|
|
{"YAMS_SEARCH_ENABLE_RERANKING", "1"},
|
|
{"YAMS_SEARCH_RERANK_TOPK", "50"},
|
|
{"YAMS_SEARCH_RERANK_REPLACE_SCORES", "0"},
|
|
{"YAMS_SEARCH_RERANK_WEIGHT", "0.60"},
|
|
{"YAMS_SEARCH_RERANK_SCORE_GAP_THRESHOLD", "0.0"},
|
|
{"YAMS_SEARCH_RERANK_SNIPPET_MAX_CHARS", "256"},
|
|
},
|
|
false,
|
|
true},
|
|
{"rerank_3x_blend60_top50_lexical16",
|
|
"Pre-fusion recall probe: winner + milder lexical floor (top-16 / +0.24)",
|
|
{
|
|
{"YAMS_ENABLE_ENV_OVERRIDES", "1"},
|
|
{"YAMS_BENCH_FORCE_TUNING_OVERRIDE", std::nullopt},
|
|
{"YAMS_TUNING_OVERRIDE", "MIXED_PRECISION"},
|
|
{"YAMS_SEARCH_ENABLE_GRAPH_RERANK", "1"},
|
|
{"YAMS_SEARCH_VECTOR_ONLY_THRESHOLD", "0.92"},
|
|
{"YAMS_SEARCH_VECTOR_ONLY_PENALTY", "0.65"},
|
|
{"YAMS_SEARCH_SEMANTIC_RESCUE_SLOTS", "2"},
|
|
{"YAMS_SEARCH_SEMANTIC_RESCUE_MIN_VECTOR_SCORE", "0.30"},
|
|
{"YAMS_SEARCH_ENABLE_ADAPTIVE_FALLBACK", "0"},
|
|
{"YAMS_SEARCH_LEXICAL_FLOOR_TOPN", "16"},
|
|
{"YAMS_SEARCH_LEXICAL_FLOOR_BOOST", "0.24"},
|
|
{"YAMS_SEARCH_ENABLE_LEXICAL_TIEBREAK", "1"},
|
|
{"YAMS_SEARCH_LEXICAL_TIEBREAK_EPS", "0.010"},
|
|
{"YAMS_CANDIDATE_MULTIPLIER", "3.0"},
|
|
{"YAMS_SEARCH_ENABLE_RERANKING", "1"},
|
|
{"YAMS_SEARCH_RERANK_TOPK", "50"},
|
|
{"YAMS_SEARCH_RERANK_REPLACE_SCORES", "0"},
|
|
{"YAMS_SEARCH_RERANK_WEIGHT", "0.60"},
|
|
{"YAMS_SEARCH_RERANK_SCORE_GAP_THRESHOLD", "0.0"},
|
|
{"YAMS_SEARCH_RERANK_SNIPPET_MAX_CHARS", "256"},
|
|
},
|
|
false,
|
|
true},
|
|
{"rerank_3x_blend60_top50_fuse200",
|
|
"Pre-fusion recall probe: winner + wider fusion window (fusion limit 200), rerank window "
|
|
"stays at top-50",
|
|
{
|
|
{"YAMS_ENABLE_ENV_OVERRIDES", "1"},
|
|
{"YAMS_BENCH_FORCE_TUNING_OVERRIDE", std::nullopt},
|
|
{"YAMS_TUNING_OVERRIDE", "MIXED_PRECISION"},
|
|
{"YAMS_SEARCH_ENABLE_GRAPH_RERANK", "1"},
|
|
{"YAMS_SEARCH_VECTOR_ONLY_THRESHOLD", "0.92"},
|
|
{"YAMS_SEARCH_VECTOR_ONLY_PENALTY", "0.65"},
|
|
{"YAMS_SEARCH_SEMANTIC_RESCUE_SLOTS", "2"},
|
|
{"YAMS_SEARCH_SEMANTIC_RESCUE_MIN_VECTOR_SCORE", "0.30"},
|
|
{"YAMS_SEARCH_ENABLE_ADAPTIVE_FALLBACK", "0"},
|
|
{"YAMS_SEARCH_LEXICAL_FLOOR_TOPN", "14"},
|
|
{"YAMS_SEARCH_LEXICAL_FLOOR_BOOST", "0.22"},
|
|
{"YAMS_SEARCH_ENABLE_LEXICAL_TIEBREAK", "1"},
|
|
{"YAMS_SEARCH_LEXICAL_TIEBREAK_EPS", "0.010"},
|
|
{"YAMS_CANDIDATE_MULTIPLIER", "3.0"},
|
|
{"YAMS_SEARCH_FUSION_CANDIDATE_LIMIT", "200"},
|
|
{"YAMS_SEARCH_ENABLE_RERANKING", "1"},
|
|
{"YAMS_SEARCH_RERANK_TOPK", "50"},
|
|
{"YAMS_SEARCH_RERANK_REPLACE_SCORES", "0"},
|
|
{"YAMS_SEARCH_RERANK_WEIGHT", "0.60"},
|
|
{"YAMS_SEARCH_RERANK_SCORE_GAP_THRESHOLD", "0.0"},
|
|
{"YAMS_SEARCH_RERANK_SNIPPET_MAX_CHARS", "256"},
|
|
}},
|
|
{"rerank_3x_blend60_top50_lexical20",
|
|
"Pre-fusion recall probe: winner + stronger lexical floor (top-20 / +0.30)",
|
|
{
|
|
{"YAMS_ENABLE_ENV_OVERRIDES", "1"},
|
|
{"YAMS_BENCH_FORCE_TUNING_OVERRIDE", std::nullopt},
|
|
{"YAMS_TUNING_OVERRIDE", "MIXED_PRECISION"},
|
|
{"YAMS_SEARCH_ENABLE_GRAPH_RERANK", "1"},
|
|
{"YAMS_SEARCH_VECTOR_ONLY_THRESHOLD", "0.92"},
|
|
{"YAMS_SEARCH_VECTOR_ONLY_PENALTY", "0.65"},
|
|
{"YAMS_SEARCH_SEMANTIC_RESCUE_SLOTS", "2"},
|
|
{"YAMS_SEARCH_SEMANTIC_RESCUE_MIN_VECTOR_SCORE", "0.30"},
|
|
{"YAMS_SEARCH_ENABLE_ADAPTIVE_FALLBACK", "0"},
|
|
{"YAMS_SEARCH_LEXICAL_FLOOR_TOPN", "20"},
|
|
{"YAMS_SEARCH_LEXICAL_FLOOR_BOOST", "0.30"},
|
|
{"YAMS_SEARCH_ENABLE_LEXICAL_TIEBREAK", "1"},
|
|
{"YAMS_SEARCH_LEXICAL_TIEBREAK_EPS", "0.010"},
|
|
{"YAMS_CANDIDATE_MULTIPLIER", "3.0"},
|
|
{"YAMS_SEARCH_ENABLE_RERANKING", "1"},
|
|
{"YAMS_SEARCH_RERANK_TOPK", "50"},
|
|
{"YAMS_SEARCH_RERANK_REPLACE_SCORES", "0"},
|
|
{"YAMS_SEARCH_RERANK_WEIGHT", "0.60"},
|
|
{"YAMS_SEARCH_RERANK_SCORE_GAP_THRESHOLD", "0.0"},
|
|
{"YAMS_SEARCH_RERANK_SNIPPET_MAX_CHARS", "256"},
|
|
}},
|
|
{"kg_budget_5ms",
|
|
"KG tradeoff probe: keep winner policy but tighten graph scoring budget to 5ms",
|
|
{
|
|
{"YAMS_ENABLE_ENV_OVERRIDES", "1"},
|
|
{"YAMS_BENCH_FORCE_TUNING_OVERRIDE", std::nullopt},
|
|
{"YAMS_TUNING_OVERRIDE", "MIXED_PRECISION"},
|
|
{"YAMS_SEARCH_ENABLE_GRAPH_RERANK", "1"},
|
|
{"YAMS_SEARCH_GRAPH_BUDGET_MS", "5"},
|
|
{"YAMS_SEARCH_VECTOR_ONLY_THRESHOLD", "0.92"},
|
|
{"YAMS_SEARCH_VECTOR_ONLY_PENALTY", "0.65"},
|
|
{"YAMS_SEARCH_SEMANTIC_RESCUE_SLOTS", "2"},
|
|
{"YAMS_SEARCH_SEMANTIC_RESCUE_MIN_VECTOR_SCORE", "0.30"},
|
|
{"YAMS_SEARCH_ENABLE_ADAPTIVE_FALLBACK", "0"},
|
|
{"YAMS_SEARCH_LEXICAL_FLOOR_TOPN", "14"},
|
|
{"YAMS_SEARCH_LEXICAL_FLOOR_BOOST", "0.22"},
|
|
{"YAMS_SEARCH_ENABLE_LEXICAL_TIEBREAK", "1"},
|
|
{"YAMS_SEARCH_LEXICAL_TIEBREAK_EPS", "0.010"},
|
|
{"YAMS_CANDIDATE_MULTIPLIER", "3.0"},
|
|
{"YAMS_SEARCH_ENABLE_RERANKING", "1"},
|
|
{"YAMS_SEARCH_RERANK_TOPK", "50"},
|
|
{"YAMS_SEARCH_RERANK_REPLACE_SCORES", "0"},
|
|
{"YAMS_SEARCH_RERANK_WEIGHT", "0.60"},
|
|
{"YAMS_SEARCH_RERANK_SCORE_GAP_THRESHOLD", "0.0"},
|
|
{"YAMS_SEARCH_RERANK_SNIPPET_MAX_CHARS", "256"},
|
|
{"YAMS_SEARCH_INCLUDE_COMPONENT_TIMING", "1"},
|
|
},
|
|
false,
|
|
true},
|
|
{"kg_topn_12",
|
|
"KG tradeoff probe: keep winner policy but limit graph rerank window to top-12",
|
|
{
|
|
{"YAMS_ENABLE_ENV_OVERRIDES", "1"},
|
|
{"YAMS_BENCH_FORCE_TUNING_OVERRIDE", std::nullopt},
|
|
{"YAMS_TUNING_OVERRIDE", "MIXED_PRECISION"},
|
|
{"YAMS_SEARCH_ENABLE_GRAPH_RERANK", "1"},
|
|
{"YAMS_SEARCH_GRAPH_RERANK_TOPN", "12"},
|
|
{"YAMS_SEARCH_VECTOR_ONLY_THRESHOLD", "0.92"},
|
|
{"YAMS_SEARCH_VECTOR_ONLY_PENALTY", "0.65"},
|
|
{"YAMS_SEARCH_SEMANTIC_RESCUE_SLOTS", "2"},
|
|
{"YAMS_SEARCH_SEMANTIC_RESCUE_MIN_VECTOR_SCORE", "0.30"},
|
|
{"YAMS_SEARCH_ENABLE_ADAPTIVE_FALLBACK", "0"},
|
|
{"YAMS_SEARCH_LEXICAL_FLOOR_TOPN", "14"},
|
|
{"YAMS_SEARCH_LEXICAL_FLOOR_BOOST", "0.22"},
|
|
{"YAMS_SEARCH_ENABLE_LEXICAL_TIEBREAK", "1"},
|
|
{"YAMS_SEARCH_LEXICAL_TIEBREAK_EPS", "0.010"},
|
|
{"YAMS_CANDIDATE_MULTIPLIER", "3.0"},
|
|
{"YAMS_SEARCH_ENABLE_RERANKING", "1"},
|
|
{"YAMS_SEARCH_RERANK_TOPK", "50"},
|
|
{"YAMS_SEARCH_RERANK_REPLACE_SCORES", "0"},
|
|
{"YAMS_SEARCH_RERANK_WEIGHT", "0.60"},
|
|
{"YAMS_SEARCH_RERANK_SCORE_GAP_THRESHOLD", "0.0"},
|
|
{"YAMS_SEARCH_RERANK_SNIPPET_MAX_CHARS", "256"},
|
|
{"YAMS_SEARCH_INCLUDE_COMPONENT_TIMING", "1"},
|
|
},
|
|
false,
|
|
true},
|
|
{"kg_weight_002_topn12",
|
|
"KG tradeoff probe: keep KG enabled with lighter weight 0.02 and top-12 rerank window",
|
|
{
|
|
{"YAMS_ENABLE_ENV_OVERRIDES", "1"},
|
|
{"YAMS_BENCH_FORCE_TUNING_OVERRIDE", std::nullopt},
|
|
{"YAMS_TUNING_OVERRIDE", "MIXED_PRECISION"},
|
|
{"YAMS_SEARCH_KG_WEIGHT", "0.02"},
|
|
{"YAMS_SEARCH_ENABLE_GRAPH_RERANK", "1"},
|
|
{"YAMS_SEARCH_GRAPH_RERANK_TOPN", "12"},
|
|
{"YAMS_SEARCH_GRAPH_RERANK_WEIGHT", "0.12"},
|
|
{"YAMS_SEARCH_VECTOR_ONLY_THRESHOLD", "0.92"},
|
|
{"YAMS_SEARCH_VECTOR_ONLY_PENALTY", "0.65"},
|
|
{"YAMS_SEARCH_SEMANTIC_RESCUE_SLOTS", "2"},
|
|
{"YAMS_SEARCH_SEMANTIC_RESCUE_MIN_VECTOR_SCORE", "0.30"},
|
|
{"YAMS_SEARCH_ENABLE_ADAPTIVE_FALLBACK", "0"},
|
|
{"YAMS_SEARCH_LEXICAL_FLOOR_TOPN", "14"},
|
|
{"YAMS_SEARCH_LEXICAL_FLOOR_BOOST", "0.22"},
|
|
{"YAMS_SEARCH_ENABLE_LEXICAL_TIEBREAK", "1"},
|
|
{"YAMS_SEARCH_LEXICAL_TIEBREAK_EPS", "0.010"},
|
|
{"YAMS_CANDIDATE_MULTIPLIER", "3.0"},
|
|
{"YAMS_SEARCH_ENABLE_RERANKING", "1"},
|
|
{"YAMS_SEARCH_RERANK_TOPK", "50"},
|
|
{"YAMS_SEARCH_RERANK_REPLACE_SCORES", "0"},
|
|
{"YAMS_SEARCH_RERANK_WEIGHT", "0.60"},
|
|
{"YAMS_SEARCH_RERANK_SCORE_GAP_THRESHOLD", "0.0"},
|
|
{"YAMS_SEARCH_RERANK_SNIPPET_MAX_CHARS", "256"},
|
|
{"YAMS_SEARCH_INCLUDE_COMPONENT_TIMING", "1"},
|
|
},
|
|
false,
|
|
true},
|
|
{"rerank_3x_blend40_top50",
|
|
"3x pool + cross-encoder rerank top-50, blend 40/60 (conservative)",
|
|
{
|
|
{"YAMS_ENABLE_ENV_OVERRIDES", "1"},
|
|
{"YAMS_BENCH_FORCE_TUNING_OVERRIDE", std::nullopt},
|
|
{"YAMS_TUNING_OVERRIDE", "MIXED_PRECISION"},
|
|
{"YAMS_SEARCH_ENABLE_GRAPH_RERANK", "1"},
|
|
{"YAMS_SEARCH_VECTOR_ONLY_THRESHOLD", "0.92"},
|
|
{"YAMS_SEARCH_VECTOR_ONLY_PENALTY", "0.65"},
|
|
{"YAMS_SEARCH_SEMANTIC_RESCUE_SLOTS", "2"},
|
|
{"YAMS_SEARCH_SEMANTIC_RESCUE_MIN_VECTOR_SCORE", "0.30"},
|
|
{"YAMS_SEARCH_ENABLE_ADAPTIVE_FALLBACK", "0"},
|
|
{"YAMS_SEARCH_LEXICAL_FLOOR_TOPN", "14"},
|
|
{"YAMS_SEARCH_LEXICAL_FLOOR_BOOST", "0.22"},
|
|
{"YAMS_SEARCH_ENABLE_LEXICAL_TIEBREAK", "1"},
|
|
{"YAMS_SEARCH_LEXICAL_TIEBREAK_EPS", "0.010"},
|
|
{"YAMS_CANDIDATE_MULTIPLIER", "3.0"},
|
|
// Cross-encoder reranking: rerank the top-50, conservative blend
|
|
{"YAMS_SEARCH_ENABLE_RERANKING", "1"},
|
|
{"YAMS_SEARCH_RERANK_TOPK", "50"},
|
|
{"YAMS_SEARCH_RERANK_REPLACE_SCORES", "0"},
|
|
{"YAMS_SEARCH_RERANK_WEIGHT", "0.40"},
|
|
{"YAMS_SEARCH_RERANK_SCORE_GAP_THRESHOLD", "0.0"},
|
|
{"YAMS_SEARCH_RERANK_SNIPPET_MAX_CHARS", "256"},
|
|
}},
|
|
{"rerank_3x_replace_top50_snippet512",
|
|
"3x pool + cross-encoder rerank top-50, replace scores, 512-char snippets",
|
|
{
|
|
{"YAMS_ENABLE_ENV_OVERRIDES", "1"},
|
|
{"YAMS_BENCH_FORCE_TUNING_OVERRIDE", std::nullopt},
|
|
{"YAMS_TUNING_OVERRIDE", "MIXED_PRECISION"},
|
|
{"YAMS_SEARCH_ENABLE_GRAPH_RERANK", "1"},
|
|
{"YAMS_SEARCH_VECTOR_ONLY_THRESHOLD", "0.92"},
|
|
{"YAMS_SEARCH_VECTOR_ONLY_PENALTY", "0.65"},
|
|
{"YAMS_SEARCH_SEMANTIC_RESCUE_SLOTS", "2"},
|
|
{"YAMS_SEARCH_SEMANTIC_RESCUE_MIN_VECTOR_SCORE", "0.30"},
|
|
{"YAMS_SEARCH_ENABLE_ADAPTIVE_FALLBACK", "0"},
|
|
{"YAMS_SEARCH_LEXICAL_FLOOR_TOPN", "14"},
|
|
{"YAMS_SEARCH_LEXICAL_FLOOR_BOOST", "0.22"},
|
|
{"YAMS_SEARCH_ENABLE_LEXICAL_TIEBREAK", "1"},
|
|
{"YAMS_SEARCH_LEXICAL_TIEBREAK_EPS", "0.010"},
|
|
{"YAMS_CANDIDATE_MULTIPLIER", "3.0"},
|
|
// Cross-encoder reranking: rerank top-50, larger snippets for more context
|
|
{"YAMS_SEARCH_ENABLE_RERANKING", "1"},
|
|
{"YAMS_SEARCH_RERANK_TOPK", "50"},
|
|
{"YAMS_SEARCH_RERANK_REPLACE_SCORES", "1"},
|
|
{"YAMS_SEARCH_RERANK_SCORE_GAP_THRESHOLD", "0.0"},
|
|
{"YAMS_SEARCH_RERANK_SNIPPET_MAX_CHARS", "512"},
|
|
}},
|
|
// --- Faster rerank candidates (top-20) for quicker iteration ---
|
|
{"rerank_3x_replace_top20",
|
|
"3x pool + cross-encoder rerank top-20, replace scores",
|
|
{
|
|
{"YAMS_ENABLE_ENV_OVERRIDES", "1"},
|
|
{"YAMS_BENCH_FORCE_TUNING_OVERRIDE", std::nullopt},
|
|
{"YAMS_TUNING_OVERRIDE", "MIXED_PRECISION"},
|
|
{"YAMS_SEARCH_ENABLE_GRAPH_RERANK", "1"},
|
|
{"YAMS_SEARCH_VECTOR_ONLY_THRESHOLD", "0.92"},
|
|
{"YAMS_SEARCH_VECTOR_ONLY_PENALTY", "0.65"},
|
|
{"YAMS_SEARCH_SEMANTIC_RESCUE_SLOTS", "2"},
|
|
{"YAMS_SEARCH_SEMANTIC_RESCUE_MIN_VECTOR_SCORE", "0.30"},
|
|
{"YAMS_SEARCH_ENABLE_ADAPTIVE_FALLBACK", "0"},
|
|
{"YAMS_SEARCH_LEXICAL_FLOOR_TOPN", "14"},
|
|
{"YAMS_SEARCH_LEXICAL_FLOOR_BOOST", "0.22"},
|
|
{"YAMS_SEARCH_ENABLE_LEXICAL_TIEBREAK", "1"},
|
|
{"YAMS_SEARCH_LEXICAL_TIEBREAK_EPS", "0.010"},
|
|
{"YAMS_CANDIDATE_MULTIPLIER", "3.0"},
|
|
{"YAMS_SEARCH_ENABLE_RERANKING", "1"},
|
|
{"YAMS_SEARCH_RERANK_TOPK", "20"},
|
|
{"YAMS_SEARCH_RERANK_REPLACE_SCORES", "1"},
|
|
{"YAMS_SEARCH_RERANK_SCORE_GAP_THRESHOLD", "0.0"},
|
|
{"YAMS_SEARCH_RERANK_SNIPPET_MAX_CHARS", "256"},
|
|
}},
|
|
{"rerank_3x_blend40_top20",
|
|
"3x pool + cross-encoder rerank top-20, blend 40/60 (conservative)",
|
|
{
|
|
{"YAMS_ENABLE_ENV_OVERRIDES", "1"},
|
|
{"YAMS_BENCH_FORCE_TUNING_OVERRIDE", std::nullopt},
|
|
{"YAMS_TUNING_OVERRIDE", "MIXED_PRECISION"},
|
|
{"YAMS_SEARCH_ENABLE_GRAPH_RERANK", "1"},
|
|
{"YAMS_SEARCH_VECTOR_ONLY_THRESHOLD", "0.92"},
|
|
{"YAMS_SEARCH_VECTOR_ONLY_PENALTY", "0.65"},
|
|
{"YAMS_SEARCH_SEMANTIC_RESCUE_SLOTS", "2"},
|
|
{"YAMS_SEARCH_SEMANTIC_RESCUE_MIN_VECTOR_SCORE", "0.30"},
|
|
{"YAMS_SEARCH_ENABLE_ADAPTIVE_FALLBACK", "0"},
|
|
{"YAMS_SEARCH_LEXICAL_FLOOR_TOPN", "14"},
|
|
{"YAMS_SEARCH_LEXICAL_FLOOR_BOOST", "0.22"},
|
|
{"YAMS_SEARCH_ENABLE_LEXICAL_TIEBREAK", "1"},
|
|
{"YAMS_SEARCH_LEXICAL_TIEBREAK_EPS", "0.010"},
|
|
{"YAMS_CANDIDATE_MULTIPLIER", "3.0"},
|
|
{"YAMS_SEARCH_ENABLE_RERANKING", "1"},
|
|
{"YAMS_SEARCH_RERANK_TOPK", "20"},
|
|
{"YAMS_SEARCH_RERANK_REPLACE_SCORES", "0"},
|
|
{"YAMS_SEARCH_RERANK_WEIGHT", "0.40"},
|
|
{"YAMS_SEARCH_RERANK_SCORE_GAP_THRESHOLD", "0.0"},
|
|
{"YAMS_SEARCH_RERANK_SNIPPET_MAX_CHARS", "256"},
|
|
}},
|
|
{"rerank_3x_blend60_top20",
|
|
"3x pool + cross-encoder rerank top-20, blend 60/40",
|
|
{
|
|
{"YAMS_ENABLE_ENV_OVERRIDES", "1"},
|
|
{"YAMS_BENCH_FORCE_TUNING_OVERRIDE", std::nullopt},
|
|
{"YAMS_TUNING_OVERRIDE", "MIXED_PRECISION"},
|
|
{"YAMS_SEARCH_ENABLE_GRAPH_RERANK", "1"},
|
|
{"YAMS_SEARCH_VECTOR_ONLY_THRESHOLD", "0.92"},
|
|
{"YAMS_SEARCH_VECTOR_ONLY_PENALTY", "0.65"},
|
|
{"YAMS_SEARCH_SEMANTIC_RESCUE_SLOTS", "2"},
|
|
{"YAMS_SEARCH_SEMANTIC_RESCUE_MIN_VECTOR_SCORE", "0.30"},
|
|
{"YAMS_SEARCH_ENABLE_ADAPTIVE_FALLBACK", "0"},
|
|
{"YAMS_SEARCH_LEXICAL_FLOOR_TOPN", "14"},
|
|
{"YAMS_SEARCH_LEXICAL_FLOOR_BOOST", "0.22"},
|
|
{"YAMS_SEARCH_ENABLE_LEXICAL_TIEBREAK", "1"},
|
|
{"YAMS_SEARCH_LEXICAL_TIEBREAK_EPS", "0.010"},
|
|
{"YAMS_CANDIDATE_MULTIPLIER", "3.0"},
|
|
{"YAMS_SEARCH_ENABLE_RERANKING", "1"},
|
|
{"YAMS_SEARCH_RERANK_TOPK", "20"},
|
|
{"YAMS_SEARCH_RERANK_REPLACE_SCORES", "0"},
|
|
{"YAMS_SEARCH_RERANK_WEIGHT", "0.60"},
|
|
{"YAMS_SEARCH_RERANK_SCORE_GAP_THRESHOLD", "0.0"},
|
|
{"YAMS_SEARCH_RERANK_SNIPPET_MAX_CHARS", "256"},
|
|
}},
|
|
|
|
// Adaptive blend: scale reranker weight by max-score confidence
|
|
{"rerank_3x_adaptive80_top20",
|
|
"3x pool + cross-encoder rerank top-20, adaptive blend (base 0.80, floor 0.10)",
|
|
{
|
|
{"YAMS_ENABLE_ENV_OVERRIDES", "1"},
|
|
{"YAMS_BENCH_FORCE_TUNING_OVERRIDE", std::nullopt},
|
|
{"YAMS_TUNING_OVERRIDE", "MIXED_PRECISION"},
|
|
{"YAMS_SEARCH_ENABLE_GRAPH_RERANK", "1"},
|
|
{"YAMS_SEARCH_VECTOR_ONLY_THRESHOLD", "0.92"},
|
|
{"YAMS_SEARCH_VECTOR_ONLY_PENALTY", "0.65"},
|
|
{"YAMS_SEARCH_SEMANTIC_RESCUE_SLOTS", "2"},
|
|
{"YAMS_SEARCH_SEMANTIC_RESCUE_MIN_VECTOR_SCORE", "0.30"},
|
|
{"YAMS_SEARCH_ENABLE_ADAPTIVE_FALLBACK", "0"},
|
|
{"YAMS_SEARCH_LEXICAL_FLOOR_TOPN", "14"},
|
|
{"YAMS_SEARCH_LEXICAL_FLOOR_BOOST", "0.22"},
|
|
{"YAMS_SEARCH_ENABLE_LEXICAL_TIEBREAK", "1"},
|
|
{"YAMS_SEARCH_LEXICAL_TIEBREAK_EPS", "0.010"},
|
|
{"YAMS_CANDIDATE_MULTIPLIER", "3.0"},
|
|
{"YAMS_SEARCH_ENABLE_RERANKING", "1"},
|
|
{"YAMS_SEARCH_RERANK_TOPK", "20"},
|
|
{"YAMS_SEARCH_RERANK_REPLACE_SCORES", "0"},
|
|
{"YAMS_SEARCH_RERANK_WEIGHT", "0.80"},
|
|
{"YAMS_SEARCH_RERANK_ADAPTIVE_BLEND", "1"},
|
|
{"YAMS_SEARCH_RERANK_ADAPTIVE_BLEND_FLOOR", "0.10"},
|
|
{"YAMS_SEARCH_RERANK_SCORE_GAP_THRESHOLD", "0.0"},
|
|
{"YAMS_SEARCH_RERANK_SNIPPET_MAX_CHARS", "256"},
|
|
}},
|
|
|
|
{"rerank_3x_adaptive60_top20",
|
|
"3x pool + cross-encoder rerank top-20, adaptive blend (base 0.60, floor 0.05)",
|
|
{
|
|
{"YAMS_ENABLE_ENV_OVERRIDES", "1"},
|
|
{"YAMS_BENCH_FORCE_TUNING_OVERRIDE", std::nullopt},
|
|
{"YAMS_TUNING_OVERRIDE", "MIXED_PRECISION"},
|
|
{"YAMS_SEARCH_ENABLE_GRAPH_RERANK", "1"},
|
|
{"YAMS_SEARCH_VECTOR_ONLY_THRESHOLD", "0.92"},
|
|
{"YAMS_SEARCH_VECTOR_ONLY_PENALTY", "0.65"},
|
|
{"YAMS_SEARCH_SEMANTIC_RESCUE_SLOTS", "2"},
|
|
{"YAMS_SEARCH_SEMANTIC_RESCUE_MIN_VECTOR_SCORE", "0.30"},
|
|
{"YAMS_SEARCH_ENABLE_ADAPTIVE_FALLBACK", "0"},
|
|
{"YAMS_SEARCH_LEXICAL_FLOOR_TOPN", "14"},
|
|
{"YAMS_SEARCH_LEXICAL_FLOOR_BOOST", "0.22"},
|
|
{"YAMS_SEARCH_ENABLE_LEXICAL_TIEBREAK", "1"},
|
|
{"YAMS_SEARCH_LEXICAL_TIEBREAK_EPS", "0.010"},
|
|
{"YAMS_CANDIDATE_MULTIPLIER", "3.0"},
|
|
{"YAMS_SEARCH_ENABLE_RERANKING", "1"},
|
|
{"YAMS_SEARCH_RERANK_TOPK", "20"},
|
|
{"YAMS_SEARCH_RERANK_REPLACE_SCORES", "0"},
|
|
{"YAMS_SEARCH_RERANK_WEIGHT", "0.60"},
|
|
{"YAMS_SEARCH_RERANK_ADAPTIVE_BLEND", "1"},
|
|
{"YAMS_SEARCH_RERANK_ADAPTIVE_BLEND_FLOOR", "0.05"},
|
|
{"YAMS_SEARCH_RERANK_SCORE_GAP_THRESHOLD", "0.0"},
|
|
{"YAMS_SEARCH_RERANK_SNIPPET_MAX_CHARS", "256"},
|
|
}},
|
|
|
|
{"rerank_3x_blend70_top20",
|
|
"3x pool + cross-encoder rerank top-20, blend 70/30 with fusion scores",
|
|
{
|
|
{"YAMS_ENABLE_ENV_OVERRIDES", "1"},
|
|
{"YAMS_BENCH_FORCE_TUNING_OVERRIDE", std::nullopt},
|
|
{"YAMS_TUNING_OVERRIDE", "MIXED_PRECISION"},
|
|
{"YAMS_SEARCH_ENABLE_GRAPH_RERANK", "1"},
|
|
{"YAMS_SEARCH_VECTOR_ONLY_THRESHOLD", "0.92"},
|
|
{"YAMS_SEARCH_VECTOR_ONLY_PENALTY", "0.65"},
|
|
{"YAMS_SEARCH_SEMANTIC_RESCUE_SLOTS", "2"},
|
|
{"YAMS_SEARCH_SEMANTIC_RESCUE_MIN_VECTOR_SCORE", "0.30"},
|
|
{"YAMS_SEARCH_ENABLE_ADAPTIVE_FALLBACK", "0"},
|
|
{"YAMS_SEARCH_LEXICAL_FLOOR_TOPN", "14"},
|
|
{"YAMS_SEARCH_LEXICAL_FLOOR_BOOST", "0.22"},
|
|
{"YAMS_SEARCH_ENABLE_LEXICAL_TIEBREAK", "1"},
|
|
{"YAMS_SEARCH_LEXICAL_TIEBREAK_EPS", "0.010"},
|
|
{"YAMS_CANDIDATE_MULTIPLIER", "3.0"},
|
|
// Cross-encoder reranking: rerank the top-20, stronger blend
|
|
{"YAMS_SEARCH_ENABLE_RERANKING", "1"},
|
|
{"YAMS_SEARCH_RERANK_TOPK", "20"},
|
|
{"YAMS_SEARCH_RERANK_REPLACE_SCORES", "0"},
|
|
{"YAMS_SEARCH_RERANK_WEIGHT", "0.70"},
|
|
{"YAMS_SEARCH_RERANK_SCORE_GAP_THRESHOLD", "0.0"},
|
|
{"YAMS_SEARCH_RERANK_SNIPPET_MAX_CHARS", "256"},
|
|
}},
|
|
{"rerank_3x_blend80_top20",
|
|
"3x pool + cross-encoder rerank top-20, blend 80/20 with fusion scores",
|
|
{
|
|
{"YAMS_ENABLE_ENV_OVERRIDES", "1"},
|
|
{"YAMS_BENCH_FORCE_TUNING_OVERRIDE", std::nullopt},
|
|
{"YAMS_TUNING_OVERRIDE", "MIXED_PRECISION"},
|
|
{"YAMS_SEARCH_ENABLE_GRAPH_RERANK", "1"},
|
|
{"YAMS_SEARCH_VECTOR_ONLY_THRESHOLD", "0.92"},
|
|
{"YAMS_SEARCH_VECTOR_ONLY_PENALTY", "0.65"},
|
|
{"YAMS_SEARCH_SEMANTIC_RESCUE_SLOTS", "2"},
|
|
{"YAMS_SEARCH_SEMANTIC_RESCUE_MIN_VECTOR_SCORE", "0.30"},
|
|
{"YAMS_SEARCH_ENABLE_ADAPTIVE_FALLBACK", "0"},
|
|
{"YAMS_SEARCH_LEXICAL_FLOOR_TOPN", "14"},
|
|
{"YAMS_SEARCH_LEXICAL_FLOOR_BOOST", "0.22"},
|
|
{"YAMS_SEARCH_ENABLE_LEXICAL_TIEBREAK", "1"},
|
|
{"YAMS_SEARCH_LEXICAL_TIEBREAK_EPS", "0.010"},
|
|
{"YAMS_CANDIDATE_MULTIPLIER", "3.0"},
|
|
// Cross-encoder reranking: rerank the top-20, aggressive blend
|
|
{"YAMS_SEARCH_ENABLE_RERANKING", "1"},
|
|
{"YAMS_SEARCH_RERANK_TOPK", "20"},
|
|
{"YAMS_SEARCH_RERANK_REPLACE_SCORES", "0"},
|
|
{"YAMS_SEARCH_RERANK_WEIGHT", "0.80"},
|
|
{"YAMS_SEARCH_RERANK_SCORE_GAP_THRESHOLD", "0.0"},
|
|
{"YAMS_SEARCH_RERANK_SNIPPET_MAX_CHARS", "256"},
|
|
}},
|
|
{"rerank_3x_blend70_top50",
|
|
"3x pool + cross-encoder rerank top-50, blend 70/30 with fusion scores",
|
|
{
|
|
{"YAMS_ENABLE_ENV_OVERRIDES", "1"},
|
|
{"YAMS_BENCH_FORCE_TUNING_OVERRIDE", std::nullopt},
|
|
{"YAMS_TUNING_OVERRIDE", "MIXED_PRECISION"},
|
|
{"YAMS_SEARCH_ENABLE_GRAPH_RERANK", "1"},
|
|
{"YAMS_SEARCH_VECTOR_ONLY_THRESHOLD", "0.92"},
|
|
{"YAMS_SEARCH_VECTOR_ONLY_PENALTY", "0.65"},
|
|
{"YAMS_SEARCH_SEMANTIC_RESCUE_SLOTS", "2"},
|
|
{"YAMS_SEARCH_SEMANTIC_RESCUE_MIN_VECTOR_SCORE", "0.30"},
|
|
{"YAMS_SEARCH_ENABLE_ADAPTIVE_FALLBACK", "0"},
|
|
{"YAMS_SEARCH_LEXICAL_FLOOR_TOPN", "14"},
|
|
{"YAMS_SEARCH_LEXICAL_FLOOR_BOOST", "0.22"},
|
|
{"YAMS_SEARCH_ENABLE_LEXICAL_TIEBREAK", "1"},
|
|
{"YAMS_SEARCH_LEXICAL_TIEBREAK_EPS", "0.010"},
|
|
{"YAMS_CANDIDATE_MULTIPLIER", "3.0"},
|
|
// Cross-encoder reranking: rerank the top-50, strong blend
|
|
{"YAMS_SEARCH_ENABLE_RERANKING", "1"},
|
|
{"YAMS_SEARCH_RERANK_TOPK", "50"},
|
|
{"YAMS_SEARCH_RERANK_REPLACE_SCORES", "0"},
|
|
{"YAMS_SEARCH_RERANK_WEIGHT", "0.70"},
|
|
{"YAMS_SEARCH_RERANK_SCORE_GAP_THRESHOLD", "0.0"},
|
|
{"YAMS_SEARCH_RERANK_SNIPPET_MAX_CHARS", "256"},
|
|
}},
|
|
|
|
// Wide fusion window: pass many more fused results through the reranker
|
|
{"rerank_3x_blend70_top200_fuse200",
|
|
"3x pool + cross-encoder rerank top-200, blend 70/30, fusion window 200",
|
|
{
|
|
{"YAMS_ENABLE_ENV_OVERRIDES", "1"},
|
|
{"YAMS_BENCH_FORCE_TUNING_OVERRIDE", std::nullopt},
|
|
{"YAMS_TUNING_OVERRIDE", "MIXED_PRECISION"},
|
|
{"YAMS_SEARCH_ENABLE_GRAPH_RERANK", "1"},
|
|
{"YAMS_SEARCH_VECTOR_ONLY_THRESHOLD", "0.92"},
|
|
{"YAMS_SEARCH_VECTOR_ONLY_PENALTY", "0.65"},
|
|
{"YAMS_SEARCH_SEMANTIC_RESCUE_SLOTS", "2"},
|
|
{"YAMS_SEARCH_SEMANTIC_RESCUE_MIN_VECTOR_SCORE", "0.30"},
|
|
{"YAMS_SEARCH_ENABLE_ADAPTIVE_FALLBACK", "0"},
|
|
{"YAMS_SEARCH_LEXICAL_FLOOR_TOPN", "14"},
|
|
{"YAMS_SEARCH_LEXICAL_FLOOR_BOOST", "0.22"},
|
|
{"YAMS_SEARCH_ENABLE_LEXICAL_TIEBREAK", "1"},
|
|
{"YAMS_SEARCH_LEXICAL_TIEBREAK_EPS", "0.010"},
|
|
{"YAMS_CANDIDATE_MULTIPLIER", "3.0"},
|
|
// Cross-encoder reranking: rerank the top-200, strong blend
|
|
{"YAMS_SEARCH_ENABLE_RERANKING", "1"},
|
|
{"YAMS_SEARCH_RERANK_TOPK", "200"},
|
|
{"YAMS_SEARCH_RERANK_REPLACE_SCORES", "0"},
|
|
{"YAMS_SEARCH_RERANK_WEIGHT", "0.70"},
|
|
{"YAMS_SEARCH_RERANK_SCORE_GAP_THRESHOLD", "0.0"},
|
|
{"YAMS_SEARCH_RERANK_SNIPPET_MAX_CHARS", "256"},
|
|
// Wider fusion window: keep 200 fused results (vs default 50)
|
|
{"YAMS_SEARCH_FUSION_CANDIDATE_LIMIT", "200"},
|
|
}},
|
|
|
|
// ── Tier 3: query expansion before rerank ───────────────────────────
|
|
|
|
{"rerank_3x_blend70_top50_multivec",
|
|
"3x pool + rerank top-50, blend 70/30, multi-vector sub-phrase search",
|
|
{
|
|
{"YAMS_ENABLE_ENV_OVERRIDES", "1"},
|
|
{"YAMS_BENCH_FORCE_TUNING_OVERRIDE", std::nullopt},
|
|
{"YAMS_TUNING_OVERRIDE", "MIXED_PRECISION"},
|
|
{"YAMS_SEARCH_ENABLE_GRAPH_RERANK", "1"},
|
|
{"YAMS_SEARCH_VECTOR_ONLY_THRESHOLD", "0.92"},
|
|
{"YAMS_SEARCH_VECTOR_ONLY_PENALTY", "0.65"},
|
|
{"YAMS_SEARCH_SEMANTIC_RESCUE_SLOTS", "2"},
|
|
{"YAMS_SEARCH_SEMANTIC_RESCUE_MIN_VECTOR_SCORE", "0.30"},
|
|
{"YAMS_SEARCH_ENABLE_ADAPTIVE_FALLBACK", "0"},
|
|
{"YAMS_SEARCH_LEXICAL_FLOOR_TOPN", "14"},
|
|
{"YAMS_SEARCH_LEXICAL_FLOOR_BOOST", "0.22"},
|
|
{"YAMS_SEARCH_ENABLE_LEXICAL_TIEBREAK", "1"},
|
|
{"YAMS_SEARCH_LEXICAL_TIEBREAK_EPS", "0.010"},
|
|
{"YAMS_CANDIDATE_MULTIPLIER", "3.0"},
|
|
{"YAMS_SEARCH_ENABLE_RERANKING", "1"},
|
|
{"YAMS_SEARCH_RERANK_TOPK", "50"},
|
|
{"YAMS_SEARCH_RERANK_REPLACE_SCORES", "0"},
|
|
{"YAMS_SEARCH_RERANK_WEIGHT", "0.70"},
|
|
{"YAMS_SEARCH_RERANK_SCORE_GAP_THRESHOLD", "0.0"},
|
|
{"YAMS_SEARCH_RERANK_SNIPPET_MAX_CHARS", "256"},
|
|
{"YAMS_SEARCH_MULTI_VECTOR_QUERY", "1"},
|
|
{"YAMS_SEARCH_MULTI_VECTOR_MAX_PHRASES", "3"},
|
|
{"YAMS_SEARCH_MULTI_VECTOR_SCORE_DECAY", "0.85"},
|
|
}},
|
|
|
|
{"rerank_3x_blend70_top50_subphrase_fts",
|
|
"3x pool + rerank top-50, blend 70/30, sub-phrase FTS expansion",
|
|
{
|
|
{"YAMS_ENABLE_ENV_OVERRIDES", "1"},
|
|
{"YAMS_BENCH_FORCE_TUNING_OVERRIDE", std::nullopt},
|
|
{"YAMS_TUNING_OVERRIDE", "MIXED_PRECISION"},
|
|
{"YAMS_SEARCH_ENABLE_GRAPH_RERANK", "1"},
|
|
{"YAMS_SEARCH_VECTOR_ONLY_THRESHOLD", "0.92"},
|
|
{"YAMS_SEARCH_VECTOR_ONLY_PENALTY", "0.65"},
|
|
{"YAMS_SEARCH_SEMANTIC_RESCUE_SLOTS", "2"},
|
|
{"YAMS_SEARCH_SEMANTIC_RESCUE_MIN_VECTOR_SCORE", "0.30"},
|
|
{"YAMS_SEARCH_ENABLE_ADAPTIVE_FALLBACK", "0"},
|
|
{"YAMS_SEARCH_LEXICAL_FLOOR_TOPN", "14"},
|
|
{"YAMS_SEARCH_LEXICAL_FLOOR_BOOST", "0.22"},
|
|
{"YAMS_SEARCH_ENABLE_LEXICAL_TIEBREAK", "1"},
|
|
{"YAMS_SEARCH_LEXICAL_TIEBREAK_EPS", "0.010"},
|
|
{"YAMS_CANDIDATE_MULTIPLIER", "3.0"},
|
|
{"YAMS_SEARCH_ENABLE_RERANKING", "1"},
|
|
{"YAMS_SEARCH_RERANK_TOPK", "50"},
|
|
{"YAMS_SEARCH_RERANK_REPLACE_SCORES", "0"},
|
|
{"YAMS_SEARCH_RERANK_WEIGHT", "0.70"},
|
|
{"YAMS_SEARCH_RERANK_SCORE_GAP_THRESHOLD", "0.0"},
|
|
{"YAMS_SEARCH_RERANK_SNIPPET_MAX_CHARS", "256"},
|
|
{"YAMS_SEARCH_SUB_PHRASE_EXPANSION", "1"},
|
|
{"YAMS_SEARCH_SUB_PHRASE_MIN_HITS", "5"},
|
|
{"YAMS_SEARCH_SUB_PHRASE_PENALTY", "0.70"},
|
|
}},
|
|
|
|
{"rerank_3x_blend70_top50_multivec_subphrase",
|
|
"3x pool + rerank top-50, blend 70/30, multi-vector + sub-phrase FTS",
|
|
{
|
|
{"YAMS_ENABLE_ENV_OVERRIDES", "1"},
|
|
{"YAMS_BENCH_FORCE_TUNING_OVERRIDE", std::nullopt},
|
|
{"YAMS_TUNING_OVERRIDE", "MIXED_PRECISION"},
|
|
{"YAMS_SEARCH_ENABLE_GRAPH_RERANK", "1"},
|
|
{"YAMS_SEARCH_VECTOR_ONLY_THRESHOLD", "0.92"},
|
|
{"YAMS_SEARCH_VECTOR_ONLY_PENALTY", "0.65"},
|
|
{"YAMS_SEARCH_SEMANTIC_RESCUE_SLOTS", "2"},
|
|
{"YAMS_SEARCH_SEMANTIC_RESCUE_MIN_VECTOR_SCORE", "0.30"},
|
|
{"YAMS_SEARCH_ENABLE_ADAPTIVE_FALLBACK", "0"},
|
|
{"YAMS_SEARCH_LEXICAL_FLOOR_TOPN", "14"},
|
|
{"YAMS_SEARCH_LEXICAL_FLOOR_BOOST", "0.22"},
|
|
{"YAMS_SEARCH_ENABLE_LEXICAL_TIEBREAK", "1"},
|
|
{"YAMS_SEARCH_LEXICAL_TIEBREAK_EPS", "0.010"},
|
|
{"YAMS_CANDIDATE_MULTIPLIER", "3.0"},
|
|
{"YAMS_SEARCH_ENABLE_RERANKING", "1"},
|
|
{"YAMS_SEARCH_RERANK_TOPK", "50"},
|
|
{"YAMS_SEARCH_RERANK_REPLACE_SCORES", "0"},
|
|
{"YAMS_SEARCH_RERANK_WEIGHT", "0.70"},
|
|
{"YAMS_SEARCH_RERANK_SCORE_GAP_THRESHOLD", "0.0"},
|
|
{"YAMS_SEARCH_RERANK_SNIPPET_MAX_CHARS", "256"},
|
|
{"YAMS_SEARCH_MULTI_VECTOR_QUERY", "1"},
|
|
{"YAMS_SEARCH_MULTI_VECTOR_MAX_PHRASES", "3"},
|
|
{"YAMS_SEARCH_MULTI_VECTOR_SCORE_DECAY", "0.85"},
|
|
{"YAMS_SEARCH_SUB_PHRASE_EXPANSION", "1"},
|
|
{"YAMS_SEARCH_SUB_PHRASE_MIN_HITS", "5"},
|
|
{"YAMS_SEARCH_SUB_PHRASE_PENALTY", "0.70"},
|
|
}},
|
|
|
|
{"rerank_3x_blend50_top50_multivec_subphrase",
|
|
"3x pool + rerank top-50, blend 50/50, multi-vector + sub-phrase FTS",
|
|
{
|
|
{"YAMS_ENABLE_ENV_OVERRIDES", "1"},
|
|
{"YAMS_BENCH_FORCE_TUNING_OVERRIDE", std::nullopt},
|
|
{"YAMS_TUNING_OVERRIDE", "MIXED_PRECISION"},
|
|
{"YAMS_SEARCH_ENABLE_GRAPH_RERANK", "1"},
|
|
{"YAMS_SEARCH_VECTOR_ONLY_THRESHOLD", "0.92"},
|
|
{"YAMS_SEARCH_VECTOR_ONLY_PENALTY", "0.65"},
|
|
{"YAMS_SEARCH_SEMANTIC_RESCUE_SLOTS", "2"},
|
|
{"YAMS_SEARCH_SEMANTIC_RESCUE_MIN_VECTOR_SCORE", "0.30"},
|
|
{"YAMS_SEARCH_ENABLE_ADAPTIVE_FALLBACK", "0"},
|
|
{"YAMS_SEARCH_LEXICAL_FLOOR_TOPN", "14"},
|
|
{"YAMS_SEARCH_LEXICAL_FLOOR_BOOST", "0.22"},
|
|
{"YAMS_SEARCH_ENABLE_LEXICAL_TIEBREAK", "1"},
|
|
{"YAMS_SEARCH_LEXICAL_TIEBREAK_EPS", "0.010"},
|
|
{"YAMS_CANDIDATE_MULTIPLIER", "3.0"},
|
|
{"YAMS_SEARCH_ENABLE_RERANKING", "1"},
|
|
{"YAMS_SEARCH_RERANK_TOPK", "50"},
|
|
{"YAMS_SEARCH_RERANK_REPLACE_SCORES", "0"},
|
|
{"YAMS_SEARCH_RERANK_WEIGHT", "0.50"},
|
|
{"YAMS_SEARCH_RERANK_SCORE_GAP_THRESHOLD", "0.0"},
|
|
{"YAMS_SEARCH_RERANK_SNIPPET_MAX_CHARS", "256"},
|
|
{"YAMS_SEARCH_MULTI_VECTOR_QUERY", "1"},
|
|
{"YAMS_SEARCH_MULTI_VECTOR_MAX_PHRASES", "3"},
|
|
{"YAMS_SEARCH_MULTI_VECTOR_SCORE_DECAY", "0.85"},
|
|
{"YAMS_SEARCH_SUB_PHRASE_EXPANSION", "1"},
|
|
{"YAMS_SEARCH_SUB_PHRASE_MIN_HITS", "5"},
|
|
{"YAMS_SEARCH_SUB_PHRASE_PENALTY", "0.70"},
|
|
}},
|
|
};
|
|
}
|
|
|
|
static std::set<std::string> parseCandidateFilter(const char* raw) {
|
|
std::set<std::string> selected;
|
|
if (!raw || !*raw) {
|
|
return selected;
|
|
}
|
|
|
|
std::string value(raw);
|
|
std::stringstream ss(value);
|
|
std::string token;
|
|
while (std::getline(ss, token, ',')) {
|
|
auto trim = [](std::string& s) {
|
|
const auto first = s.find_first_not_of(" \t\n\r");
|
|
if (first == std::string::npos) {
|
|
s.clear();
|
|
return;
|
|
}
|
|
const auto last = s.find_last_not_of(" \t\n\r");
|
|
s = s.substr(first, last - first + 1);
|
|
};
|
|
trim(token);
|
|
if (!token.empty()) {
|
|
selected.insert(token);
|
|
}
|
|
}
|
|
return selected;
|
|
}
|
|
|
|
enum class OptimizationProfile {
|
|
EnginePolicy,
|
|
Diagnostics,
|
|
All,
|
|
};
|
|
|
|
static OptimizationProfile parseOptimizationProfile(const char* raw) {
|
|
if (!raw || !*raw) {
|
|
return OptimizationProfile::EnginePolicy;
|
|
}
|
|
|
|
std::string value(raw);
|
|
std::transform(value.begin(), value.end(), value.begin(),
|
|
[](unsigned char c) { return static_cast<char>(std::tolower(c)); });
|
|
|
|
if (value == "diagnostic" || value == "diagnostics" || value == "probe" || value == "probes") {
|
|
return OptimizationProfile::Diagnostics;
|
|
}
|
|
if (value == "all") {
|
|
return OptimizationProfile::All;
|
|
}
|
|
return OptimizationProfile::EnginePolicy;
|
|
}
|
|
|
|
static const char* optimizationProfileName(OptimizationProfile profile) {
|
|
switch (profile) {
|
|
case OptimizationProfile::EnginePolicy:
|
|
return "engine-policy";
|
|
case OptimizationProfile::Diagnostics:
|
|
return "diagnostics";
|
|
case OptimizationProfile::All:
|
|
return "all";
|
|
}
|
|
return "engine-policy";
|
|
}
|
|
|
|
static double computeHybridRegressionPenalty(const RetrievalMetrics& hybrid,
|
|
const RetrievalMetrics& keyword) {
|
|
const double hybridMrrLoss = std::max(0.0, keyword.mrr - hybrid.mrr);
|
|
return std::min(0.12, hybridMrrLoss * 2.0);
|
|
}
|
|
|
|
static double computeDuplicatePenalty(const RetrievalMetrics& hybrid) {
|
|
return std::min(0.08, std::max(0.0, hybrid.duplicateRateAtK) * 0.30);
|
|
}
|
|
|
|
static double computeLatencyPenalty(double avgHybridQueryMs) {
|
|
// Keep latency meaningful in the 3-7s/query range seen in real SciFact runs.
|
|
// The previous cap saturated far too early and made materially slower candidates
|
|
// look effectively free once they crossed ~800ms/query.
|
|
return std::clamp(std::max(0.0, avgHybridQueryMs) / 25000.0, 0.0, 0.25);
|
|
}
|
|
|
|
static double computePrecisionFailurePenalty(const QueryDiagnosticsSummary& summary) {
|
|
const double queryCount = static_cast<double>(std::max<std::uint64_t>(summary.queryCount, 1));
|
|
const double noRelevantHitRate =
|
|
static_cast<double>(summary.queryWithoutRelevantHitCount) / queryCount;
|
|
const double missPreFusionRate =
|
|
static_cast<double>(summary.missMissingPreFusionCount) / queryCount;
|
|
const double fusionCutoffRate = static_cast<double>(summary.missFusionCutoffCount) / queryCount;
|
|
const double rerankDropRate =
|
|
static_cast<double>(summary.missRerankWindowDropCount) / queryCount;
|
|
const double topKDropRate = static_cast<double>(summary.missTopKDropCount) / queryCount;
|
|
|
|
return std::min(0.25, missPreFusionRate * 0.25 + noRelevantHitRate * 0.10 +
|
|
fusionCutoffRate * 0.04 + rerankDropRate * 0.05 +
|
|
topKDropRate * 0.03);
|
|
}
|
|
|
|
static std::string summarizeTimingTradeoffs(const json& hybridDiag) {
|
|
if (!hybridDiag.is_object()) {
|
|
return "timings=unavailable";
|
|
}
|
|
const auto timingsIt = hybridDiag.find("timings_ms");
|
|
if (timingsIt == hybridDiag.end() || !timingsIt->is_object()) {
|
|
return "timings=unavailable";
|
|
}
|
|
|
|
const json& timings = *timingsIt;
|
|
std::vector<std::pair<std::string, double>> ranked;
|
|
ranked.reserve(timings.size());
|
|
for (const auto& [name, stats] : timings.items()) {
|
|
if (!stats.is_object()) {
|
|
continue;
|
|
}
|
|
auto meanIt = stats.find("mean");
|
|
if (meanIt == stats.end() || !meanIt->is_number()) {
|
|
continue;
|
|
}
|
|
ranked.emplace_back(name, meanIt->get<double>());
|
|
}
|
|
|
|
if (ranked.empty()) {
|
|
return "timings=unavailable";
|
|
}
|
|
|
|
std::sort(ranked.begin(), ranked.end(),
|
|
[](const auto& a, const auto& b) { return a.second > b.second; });
|
|
|
|
std::ostringstream oss;
|
|
oss << std::fixed << std::setprecision(1);
|
|
const size_t limit = std::min<size_t>(3, ranked.size());
|
|
for (size_t i = 0; i < limit; ++i) {
|
|
if (i > 0) {
|
|
oss << " ";
|
|
}
|
|
oss << ranked[i].first << '=' << ranked[i].second << "ms";
|
|
}
|
|
return oss.str();
|
|
}
|
|
|
|
static std::string summarizeStageTradeoffs(const json& hybridDiag) {
|
|
if (!hybridDiag.is_object()) {
|
|
return "stages=unavailable";
|
|
}
|
|
const auto stagesIt = hybridDiag.find("stage_contributions");
|
|
if (stagesIt == hybridDiag.end() || !stagesIt->is_object()) {
|
|
return "stages=unavailable";
|
|
}
|
|
|
|
const json& stages = *stagesIt;
|
|
std::vector<std::string> parts;
|
|
for (const auto& [name, stats] : stages.items()) {
|
|
if (!stats.is_object()) {
|
|
continue;
|
|
}
|
|
const double contributionRate = stats.value("contribution_rate", 0.0);
|
|
const double skipRate = stats.value("skip_rate", 0.0);
|
|
if (contributionRate <= 0.0 && skipRate <= 0.0) {
|
|
continue;
|
|
}
|
|
std::ostringstream oss;
|
|
oss << name << ":contrib=" << std::fixed << std::setprecision(2) << contributionRate
|
|
<< ",skip=" << skipRate;
|
|
parts.push_back(oss.str());
|
|
}
|
|
|
|
if (parts.empty()) {
|
|
return "stages=unavailable";
|
|
}
|
|
|
|
std::sort(parts.begin(), parts.end());
|
|
if (parts.size() > 3) {
|
|
parts.resize(3);
|
|
}
|
|
|
|
std::ostringstream out;
|
|
for (size_t i = 0; i < parts.size(); ++i) {
|
|
if (i > 0) {
|
|
out << " ";
|
|
}
|
|
out << parts[i];
|
|
}
|
|
return out.str();
|
|
}
|
|
|
|
static std::string summarizeStageMetricGaps(const json& hybridDiag) {
|
|
if (!hybridDiag.is_object()) {
|
|
return "stage_metrics=unavailable";
|
|
}
|
|
const auto metricsIt = hybridDiag.find("stage_metrics");
|
|
if (metricsIt == hybridDiag.end() || !metricsIt->is_object() || metricsIt->empty()) {
|
|
return "stage_metrics=unavailable";
|
|
}
|
|
|
|
static const std::vector<std::string> kStageOrder = {
|
|
"pre_fusion", "graphless_post_fusion", "post_fusion", "post_graph", "final",
|
|
};
|
|
|
|
std::vector<std::string> parts;
|
|
for (const auto& stage : kStageOrder) {
|
|
auto stageIt = metricsIt->find(stage);
|
|
if (stageIt == metricsIt->end() || !stageIt->is_object()) {
|
|
continue;
|
|
}
|
|
const double mrr = stageIt->value("mrr", 0.0);
|
|
const double recall = stageIt->value("recall_at_k", 0.0);
|
|
const double ndcg = stageIt->value("ndcg_at_k", 0.0);
|
|
const double mrrDelta = stageIt->value("mrr_delta_vs_final", 0.0);
|
|
const double ndcgDelta = stageIt->value("ndcg_delta_vs_final", 0.0);
|
|
std::ostringstream oss;
|
|
oss << stage << ":mrr=" << std::fixed << std::setprecision(3) << mrr
|
|
<< "(d=" << std::showpos << mrrDelta << std::noshowpos << ")"
|
|
<< ",rec=" << recall << ",ndcg=" << ndcg << "(d=" << std::showpos << ndcgDelta
|
|
<< std::noshowpos << ")";
|
|
parts.push_back(oss.str());
|
|
}
|
|
|
|
if (parts.empty()) {
|
|
return "stage_metrics=unavailable";
|
|
}
|
|
|
|
std::ostringstream out;
|
|
for (size_t i = 0; i < parts.size(); ++i) {
|
|
if (i > 0) {
|
|
out << " ";
|
|
}
|
|
out << parts[i];
|
|
}
|
|
return out.str();
|
|
}
|
|
|
|
static double computeOptimizationObjective(const RetrievalMetrics& hybrid,
|
|
const RetrievalMetrics& keyword,
|
|
const QueryDiagnosticsSummary& hybridDiagnostics,
|
|
double avgHybridQueryMs) {
|
|
// Precision-first objective for machine-first navigation: keep top-K correctness high while
|
|
// still rewarding rank quality and recall. Miss-stage penalties keep the loop focused on the
|
|
// actual places where relevant documents are lost.
|
|
const double quality = 0.32 * hybrid.mrr + 0.24 * hybrid.ndcgAtK + 0.24 * hybrid.precisionAtK +
|
|
0.12 * hybrid.recallAtK + 0.08 * hybrid.map;
|
|
const double hybridGain = std::max(0.0, hybrid.mrr - keyword.mrr);
|
|
const double regressionPenalty = computeHybridRegressionPenalty(hybrid, keyword);
|
|
const double duplicatePenalty = computeDuplicatePenalty(hybrid);
|
|
const double latencyPenalty = computeLatencyPenalty(avgHybridQueryMs);
|
|
const double precisionFailurePenalty = computePrecisionFailurePenalty(hybridDiagnostics);
|
|
return quality + 0.10 * hybridGain - regressionPenalty - duplicatePenalty - latencyPenalty -
|
|
precisionFailurePenalty;
|
|
}
|
|
|
|
static void appendOptimizationResultJson(const fs::path& outputFile,
|
|
const OptimizationRunResult& result) {
|
|
std::ofstream out(outputFile, std::ios::out | std::ios::app);
|
|
if (!out.is_open()) {
|
|
spdlog::warn("[OptLoop] Failed to open results file: {}", outputFile.string());
|
|
return;
|
|
}
|
|
|
|
json j;
|
|
j["candidate"] = result.candidate.name;
|
|
j["description"] = result.candidate.description;
|
|
j["success"] = result.success;
|
|
j["objective"] = result.objectiveScore;
|
|
j["hybrid_eval_ms"] = result.hybridEvalMs;
|
|
j["keyword_eval_ms"] = result.keywordEvalMs;
|
|
j["search_engine"] = result.searchEngine;
|
|
if (const char* embBackend = std::getenv("YAMS_EMBED_BACKEND"); embBackend && *embBackend) {
|
|
j["embedding_backend"] = embBackend;
|
|
} else {
|
|
j["embedding_backend"] = "daemon";
|
|
}
|
|
j["vectors_db_bytes"] = result.vectorsDbBytes;
|
|
j["vectors_wal_bytes"] = result.vectorsWalBytes;
|
|
j["persisted_hnsw_nodes"] = result.persistedHnswNodes;
|
|
j["persisted_hnsw_meta_rows"] = result.persistedHnswMetaRows;
|
|
j["persisted_vec0_tables"] = result.persistedVec0Tables;
|
|
j["persisted_vec0_rows"] = result.persistedVec0Rows;
|
|
j["persisted_pq_codes"] = result.persistedPqCodes;
|
|
j["persisted_pq_meta"] = result.persistedPqMeta;
|
|
j["tuning_state"] = result.tuningState;
|
|
j["tuning_reason"] = result.tuningReason;
|
|
j["error"] = result.errorMessage;
|
|
j["run_id"] = result.runId;
|
|
j["debug_file"] = result.debugFile;
|
|
j["trace_top_n"] = result.traceTopN;
|
|
j["trace_component_top_n"] = result.traceComponentTopN;
|
|
j["stage_trace_enabled"] = result.stageTraceEnabled;
|
|
j["hybrid_keyword_mrr_delta"] = result.hybridMetrics.mrr - result.keywordMetrics.mrr;
|
|
|
|
const double objectiveRegressionPenalty =
|
|
computeHybridRegressionPenalty(result.hybridMetrics, result.keywordMetrics);
|
|
const double objectiveDuplicatePenalty = computeDuplicatePenalty(result.hybridMetrics);
|
|
const double objectivePrecisionFailurePenalty =
|
|
computePrecisionFailurePenalty(result.hybridDiagnostics);
|
|
j["objective_penalties"] = {
|
|
{"hybrid_regression", objectiveRegressionPenalty},
|
|
{"duplicate_rate", objectiveDuplicatePenalty},
|
|
{"precision_failures", objectivePrecisionFailurePenalty},
|
|
};
|
|
|
|
j["hybrid"] = {
|
|
{"mrr", result.hybridMetrics.mrr},
|
|
{"recall_at_k", result.hybridMetrics.recallAtK},
|
|
{"precision_at_k", result.hybridMetrics.precisionAtK},
|
|
{"ndcg_at_k", result.hybridMetrics.ndcgAtK},
|
|
{"map", result.hybridMetrics.map},
|
|
{"duplicate_rate_at_k", result.hybridMetrics.duplicateRateAtK},
|
|
{"num_queries", result.hybridMetrics.numQueries},
|
|
};
|
|
|
|
j["keyword"] = {
|
|
{"mrr", result.keywordMetrics.mrr},
|
|
{"recall_at_k", result.keywordMetrics.recallAtK},
|
|
{"precision_at_k", result.keywordMetrics.precisionAtK},
|
|
{"ndcg_at_k", result.keywordMetrics.ndcgAtK},
|
|
{"map", result.keywordMetrics.map},
|
|
{"duplicate_rate_at_k", result.keywordMetrics.duplicateRateAtK},
|
|
{"num_queries", result.keywordMetrics.numQueries},
|
|
};
|
|
|
|
{
|
|
auto sampleMean = [](const std::vector<double>& v) -> double {
|
|
if (v.empty())
|
|
return 0.0;
|
|
double sum = 0.0;
|
|
for (double x : v)
|
|
sum += x;
|
|
return sum / static_cast<double>(v.size());
|
|
};
|
|
auto meanSignal = [&](const std::string& key) -> double {
|
|
auto it = result.hybridDiagnostics.tunerSignalSamples.find(key);
|
|
return it == result.hybridDiagnostics.tunerSignalSamples.end() ? 0.0
|
|
: sampleMean(it->second);
|
|
};
|
|
auto meanScoreMass = [&](const std::string& src) -> double {
|
|
auto it = result.hybridDiagnostics.fusionSourceScoreMassSamples.find(src);
|
|
return it == result.hybridDiagnostics.fusionSourceScoreMassSamples.end()
|
|
? 0.0
|
|
: sampleMean(it->second);
|
|
};
|
|
const double denseMass = meanScoreMass("vector") + meanScoreMass("compressed_ann") +
|
|
meanScoreMass("graph_vector") + meanScoreMass("entity_vector");
|
|
const double lexicalMass =
|
|
meanScoreMass("text") + meanScoreMass("graph_text") + meanScoreMass("path_tree");
|
|
const double otherMass = meanScoreMass("kg") + meanScoreMass("tag") +
|
|
meanScoreMass("metadata") + meanScoreMass("symbol");
|
|
const double totalMass = denseMass + lexicalMass + otherMass;
|
|
const double denseContrib = totalMass > 0.0 ? denseMass / totalMass : 0.0;
|
|
const double bm25Contrib = totalMass > 0.0 ? lexicalMass / totalMass : 0.0;
|
|
|
|
j["fusion_alpha"] = meanSignal("vector_weight");
|
|
j["text_weight"] = meanSignal("text_weight");
|
|
j["dense_contrib"] = denseContrib;
|
|
j["bm25_contrib"] = bm25Contrib;
|
|
j["other_contrib"] = totalMass > 0.0 ? otherMass / totalMass : 0.0;
|
|
|
|
j["ngram_range_used"] = "3-5";
|
|
j["idf_weighted"] = false;
|
|
}
|
|
|
|
j["hybrid_debug_summary"] = queryDiagnosticsToJson(result.hybridDiagnostics);
|
|
j["keyword_debug_summary"] = queryDiagnosticsToJson(result.keywordDiagnostics);
|
|
|
|
json env = json::object();
|
|
for (const auto& setting : result.candidate.envOverrides) {
|
|
if (setting.value.has_value()) {
|
|
env[setting.key] = *setting.value;
|
|
} else {
|
|
env[setting.key] = nullptr;
|
|
}
|
|
}
|
|
j["env_overrides"] = env;
|
|
|
|
out << j.dump() << "\n";
|
|
}
|
|
|
|
static std::chrono::milliseconds defaultBenchQueryTimeout(const std::string& searchType) {
|
|
if (searchType == "hybrid") {
|
|
// Hybrid benchmark runs exercise graph rerank, ONNX reranking, and stage tracing.
|
|
// The default 20s request timeout is too short for cold-ish benchmark slices and
|
|
// causes partial JSONL output because timed-out queries are skipped entirely.
|
|
return std::chrono::milliseconds{120000};
|
|
}
|
|
return std::chrono::milliseconds{20000};
|
|
}
|
|
|
|
static std::chrono::milliseconds configuredBenchQueryTimeout(const std::string& searchType) {
|
|
if (const char* env = std::getenv("YAMS_BENCH_QUERY_TIMEOUT_MS")) {
|
|
return std::chrono::milliseconds{
|
|
static_cast<std::chrono::milliseconds::rep>(std::stoll(env))};
|
|
}
|
|
return defaultBenchQueryTimeout(searchType);
|
|
}
|
|
|
|
static void ingestStageRetrievalMetrics(QueryDiagnosticsSummary& summary,
|
|
const std::map<std::string, std::string>& searchStats,
|
|
const TestQuery& tq, int k) {
|
|
static const std::vector<std::pair<std::string, std::string>> kStageDocIdStats = {
|
|
{"pre_fusion", "trace_pre_fusion_doc_ids"},
|
|
{"graphless_post_fusion", "trace_graphless_post_fusion_doc_ids"},
|
|
{"post_fusion", "trace_post_fusion_doc_ids"},
|
|
{"post_graph", "trace_post_graph_doc_ids"},
|
|
{"final", "trace_final_doc_ids"},
|
|
};
|
|
|
|
for (const auto& [stage, statKey] : kStageDocIdStats) {
|
|
auto it = searchStats.find(statKey);
|
|
if (it == searchStats.end() || it->second.empty()) {
|
|
continue;
|
|
}
|
|
auto rankedDocIds = splitTabList(it->second, static_cast<size_t>(std::max(0, k)));
|
|
yams::bench::addStageMetricSample(summary.stageRetrievalMetrics[stage], rankedDocIds, tq,
|
|
k);
|
|
}
|
|
}
|
|
|
|
RetrievalMetrics evaluateQueries(yams::daemon::DaemonClient& client, const fs::path& corpusDir,
|
|
const std::vector<TestQuery>& queries, int k,
|
|
const std::string& searchType = "hybrid",
|
|
QueryDiagnosticsSummary* diagnostics = nullptr,
|
|
bool emitDebug = true,
|
|
std::vector<double>* latencySamplesMs = nullptr,
|
|
std::vector<TestQuery>* failedQueries = nullptr) {
|
|
RetrievalMetrics metrics;
|
|
metrics.numQueries = static_cast<int>(queries.size());
|
|
double totalMRR = 0.0, totalRecall = 0.0, totalPrecision = 0.0, totalNDCG = 0.0, totalMAP = 0.0;
|
|
|
|
std::uint64_t totalAttempts = 0;
|
|
std::uint64_t streamingCount = 0;
|
|
std::uint64_t fuzzyRetryCount = 0;
|
|
std::uint64_t literalRetryCount = 0;
|
|
std::uint64_t duplicateTopKCount = 0;
|
|
std::uint64_t totalTopKCount = 0;
|
|
|
|
for (std::size_t queryIndex = 0; queryIndex < queries.size(); ++queryIndex) {
|
|
const auto& tq = queries[queryIndex];
|
|
yams::cli::search_runner::DaemonSearchOptions opts;
|
|
opts.query = tq.query;
|
|
opts.searchType = searchType;
|
|
opts.limit = static_cast<std::size_t>(k);
|
|
const auto queryTimeout = configuredBenchQueryTimeout(searchType);
|
|
opts.timeout = queryTimeout;
|
|
opts.allowFuzzyRetry = false;
|
|
opts.allowLiteralTextRetry = false;
|
|
const bool benchDiagEnabled = []() -> bool {
|
|
if (const char* env = std::getenv("YAMS_BENCH_DIAG"); env && std::string(env) == "1") {
|
|
return true;
|
|
}
|
|
return false;
|
|
}();
|
|
const bool benchVerbose = []() -> bool {
|
|
if (const char* env = std::getenv("YAMS_BENCH_VERBOSE"); env) {
|
|
std::string value(env);
|
|
std::transform(value.begin(), value.end(), value.begin(), [](char c) {
|
|
return static_cast<char>(std::tolower(static_cast<unsigned char>(c)));
|
|
});
|
|
return value == "1" || value == "true" || value == "yes" || value == "on";
|
|
}
|
|
return false;
|
|
}();
|
|
|
|
if (benchVerbose) {
|
|
spdlog::debug("Executing search query: '{}'", tq.query);
|
|
}
|
|
// Wait at least as long as the query timeout, plus a small margin.
|
|
// Using a shorter run_sync deadline can leave in-flight requests behind,
|
|
// which can cascade into timeouts/backpressure across subsequent queries.
|
|
auto waitBudget = std::chrono::duration_cast<std::chrono::milliseconds>(opts.timeout) +
|
|
std::chrono::seconds(10);
|
|
if (waitBudget < std::chrono::seconds(10)) {
|
|
waitBudget = std::chrono::seconds(10);
|
|
}
|
|
auto run = benchRunSync(
|
|
yams::cli::search_runner::daemon_search(client, opts, benchUseStreamingSearch()),
|
|
waitBudget);
|
|
if (!run) {
|
|
spdlog::warn("Search failed for query '{}': {}", tq.query, run.error().message);
|
|
if (failedQueries) {
|
|
failedQueries->push_back(tq);
|
|
}
|
|
DebugLogEntry failedEntry;
|
|
failedEntry.query = tq.query;
|
|
failedEntry.queryIndex = static_cast<int>(queryIndex);
|
|
failedEntry.searchType = searchType;
|
|
failedEntry.attempts = 0;
|
|
failedEntry.usedStreaming = false;
|
|
failedEntry.usedFuzzyFlag = false;
|
|
failedEntry.usedLiteralFlag = false;
|
|
failedEntry.usedFuzzyRetry = false;
|
|
failedEntry.usedLiteralTextRetry = false;
|
|
failedEntry.relevantDocIds.assign(tq.relevantDocIds.begin(), tq.relevantDocIds.end());
|
|
failedEntry.relevantFiles.assign(tq.relevantFiles.begin(), tq.relevantFiles.end());
|
|
failedEntry.diagnostics.push_back("search_error=" + run.error().message);
|
|
failedEntry.diagnostics.push_back("searchType=" + opts.searchType);
|
|
failedEntry.diagnostics.push_back("limit=" + std::to_string(opts.limit));
|
|
failedEntry.diagnostics.push_back(
|
|
"timeout_ms=" +
|
|
std::to_string(
|
|
std::chrono::duration_cast<std::chrono::milliseconds>(opts.timeout).count()));
|
|
const json relevantDecisionTrace = buildRelevantDecisionTrace(
|
|
failedEntry.relevantDocIds, failedEntry.returnedDocIds, failedEntry.searchStats);
|
|
failedEntry.extraFields["relevant_decision_trace"] = relevantDecisionTrace;
|
|
if (emitDebug) {
|
|
debugLogWriteJsonLine(failedEntry);
|
|
}
|
|
if (diagnostics) {
|
|
ingestQueryDiagnostics(*diagnostics, failedEntry.searchStats, relevantDecisionTrace,
|
|
true, false, false, -1);
|
|
}
|
|
continue;
|
|
}
|
|
|
|
totalAttempts += run.value().attempts;
|
|
if (run.value().usedStreaming)
|
|
streamingCount++;
|
|
if (run.value().usedFuzzyRetry)
|
|
fuzzyRetryCount++;
|
|
if (run.value().usedLiteralTextRetry)
|
|
literalRetryCount++;
|
|
|
|
const auto& results = run.value().response.results;
|
|
if (latencySamplesMs) {
|
|
const auto latencyIt = run.value().response.searchStats.find("latency_ms");
|
|
if (latencyIt != run.value().response.searchStats.end()) {
|
|
try {
|
|
const double latencyMs = std::stod(latencyIt->second);
|
|
if (std::isfinite(latencyMs) && latencyMs >= 0.0) {
|
|
latencySamplesMs->push_back(latencyMs);
|
|
}
|
|
} catch (const std::exception& e) {
|
|
if (benchVerbose) {
|
|
spdlog::debug("Ignoring malformed benchmark latency '{}': {}",
|
|
latencyIt->second, e.what());
|
|
}
|
|
}
|
|
}
|
|
}
|
|
if (benchVerbose) {
|
|
spdlog::debug("Search returned {} results for query '{}' (attempts={}, streaming={}, "
|
|
"fuzzy_retry={}, literal_retry={})",
|
|
results.size(), tq.query, run.value().attempts, run.value().usedStreaming,
|
|
run.value().usedFuzzyRetry, run.value().usedLiteralTextRetry);
|
|
}
|
|
|
|
// Detailed result logging for debugging retrieval quality
|
|
if (benchDiagEnabled) {
|
|
spdlog::debug(" [{}] Expected relevant: {} docs", searchType,
|
|
tq.relevantDocIds.size());
|
|
for (size_t i = 0; i < std::min((size_t)5, results.size()); ++i) {
|
|
const std::string filename = fs::path(results[i].path).filename().string();
|
|
const std::string docId = yams::bench::canonicalResultDocId(tq, filename);
|
|
const bool relevant = yams::bench::isRelevantDocForQuery(tq, docId);
|
|
spdlog::debug(" [{}] Result {}: path='{}' docId='{}' score={:.4f} {}", searchType,
|
|
i, results[i].path, docId, results[i].score,
|
|
relevant ? "RELEVANT" : "");
|
|
}
|
|
}
|
|
|
|
DebugLogEntry debugEntry;
|
|
debugEntry.query = tq.query;
|
|
debugEntry.queryIndex = static_cast<int>(queryIndex);
|
|
debugEntry.searchType = searchType;
|
|
debugEntry.attempts = run.value().attempts;
|
|
debugEntry.usedStreaming = run.value().usedStreaming;
|
|
debugEntry.usedFuzzyFlag = false;
|
|
debugEntry.usedLiteralFlag = false;
|
|
debugEntry.usedFuzzyRetry = run.value().usedFuzzyRetry;
|
|
debugEntry.usedLiteralTextRetry = run.value().usedLiteralTextRetry;
|
|
debugEntry.searchStats = run.value().response.searchStats;
|
|
debugEntry.relevantDocIds.assign(tq.relevantDocIds.begin(), tq.relevantDocIds.end());
|
|
debugEntry.relevantFiles.assign(tq.relevantFiles.begin(), tq.relevantFiles.end());
|
|
|
|
if (benchDiagEnabled) {
|
|
debugEntry.diagnostics.push_back("searchType=" + opts.searchType);
|
|
debugEntry.diagnostics.push_back("limit=" + std::to_string(opts.limit));
|
|
debugEntry.diagnostics.push_back(
|
|
"timeout_ms=" +
|
|
std::to_string(
|
|
std::chrono::duration_cast<std::chrono::milliseconds>(opts.timeout).count()));
|
|
}
|
|
|
|
for (size_t i = 0; i < std::min((size_t)k, results.size()); ++i) {
|
|
debugEntry.returnedPaths.push_back(results[i].path);
|
|
debugEntry.returnedScores.push_back(static_cast<float>(results[i].score));
|
|
|
|
const std::string filename = fs::path(results[i].path).filename().string();
|
|
debugEntry.returnedDocIds.push_back(yams::bench::canonicalResultDocId(tq, filename));
|
|
totalTopKCount++;
|
|
}
|
|
|
|
const auto scoreSample = yams::bench::scoreRankedDocIds(tq, debugEntry.returnedDocIds, k);
|
|
debugEntry.returnedGrades = scoreSample.retrievedGrades;
|
|
for (const auto& docId : scoreSample.duplicateDocIds) {
|
|
duplicateTopKCount++;
|
|
debugEntry.diagnostics.push_back("duplicate_topk_doc=" + docId);
|
|
}
|
|
|
|
const json relevantDecisionTrace = buildRelevantDecisionTrace(
|
|
debugEntry.relevantDocIds, debugEntry.returnedDocIds, debugEntry.searchStats);
|
|
debugEntry.extraFields["relevant_decision_trace"] = relevantDecisionTrace;
|
|
debugEntry.extraFields["query_metrics"] = {
|
|
{"mrr", scoreSample.reciprocalRank},
|
|
{"recall_at_k", scoreSample.recallAtK},
|
|
{"precision_at_k", scoreSample.precisionAtK},
|
|
{"ndcg_at_k", scoreSample.ndcgAtK},
|
|
{"map", scoreSample.averagePrecision},
|
|
{"first_relevant_rank", scoreSample.firstRelevantRank},
|
|
};
|
|
|
|
if (benchDiagEnabled && results.empty() && tq.useDocIds && !tq.relevantDocIds.empty()) {
|
|
// Opt-in diagnostic: show a snippet of the relevant BEIR doc file(s).
|
|
std::size_t printed = 0;
|
|
for (const auto& docId : tq.relevantDocIds) {
|
|
if (printed >= 2)
|
|
break;
|
|
const fs::path p = corpusDir / (docId + ".txt");
|
|
if (!fs::exists(p)) {
|
|
debugEntry.diagnostics.push_back("relevant_doc_missing=" + p.string());
|
|
continue;
|
|
}
|
|
std::ifstream in(p);
|
|
|
|
if (!in.is_open()) {
|
|
debugEntry.diagnostics.push_back("relevant_doc_open_failed=" + p.string());
|
|
continue;
|
|
}
|
|
std::string snippet;
|
|
snippet.resize(240);
|
|
in.read(snippet.data(), static_cast<std::streamsize>(snippet.size()));
|
|
snippet.resize(static_cast<std::size_t>(in.gcount()));
|
|
for (auto& c : snippet) {
|
|
if (c == '\n' || c == '\r' || c == '\t')
|
|
c = ' ';
|
|
}
|
|
debugEntry.diagnostics.push_back("relevant_doc_snippet[" + docId + "]=" + snippet);
|
|
++printed;
|
|
}
|
|
}
|
|
|
|
// Opt-in diagnostic: run a few "shadow" queries for failing cases.
|
|
// This does not affect scoring; it only emits diagnostic entries.
|
|
if (emitDebug && benchDiagEnabled && results.empty()) {
|
|
// 1) Try a shorter query with a few salient tokens to test whether the full
|
|
// sentence query is failing due to strict AND semantics / tokenization.
|
|
std::istringstream iss(tq.query);
|
|
std::string token;
|
|
std::vector<std::string> picked;
|
|
picked.reserve(3);
|
|
while (iss >> token && picked.size() < 3) {
|
|
// Strip trailing punctuation for the diagnostic query.
|
|
while (!token.empty() && std::ispunct(static_cast<unsigned char>(token.back()))) {
|
|
token.pop_back();
|
|
}
|
|
if (token.size() < 4)
|
|
continue;
|
|
picked.push_back(token);
|
|
}
|
|
|
|
if (!picked.empty()) {
|
|
std::string diagQuery;
|
|
for (size_t i = 0; i < picked.size(); ++i) {
|
|
if (i)
|
|
diagQuery += ' ';
|
|
diagQuery += picked[i];
|
|
}
|
|
|
|
yams::cli::search_runner::DaemonSearchOptions shadow;
|
|
shadow.query = diagQuery;
|
|
shadow.searchType = opts.searchType;
|
|
shadow.limit = opts.limit;
|
|
shadow.timeout = opts.timeout;
|
|
shadow.allowFuzzyRetry = false;
|
|
shadow.allowLiteralTextRetry = false;
|
|
|
|
DebugLogEntry shadowEntry;
|
|
shadowEntry.query = "__diag_shadow_query__";
|
|
shadowEntry.queryIndex = static_cast<int>(queryIndex);
|
|
shadowEntry.searchType = searchType;
|
|
shadowEntry.relevantDocIds.assign(tq.relevantDocIds.begin(),
|
|
tq.relevantDocIds.end());
|
|
shadowEntry.diagnostics.push_back("original_query=" + tq.query);
|
|
shadowEntry.diagnostics.push_back("shadow_query=" + diagQuery);
|
|
|
|
auto shadowRun = benchRunSync(
|
|
yams::cli::search_runner::daemon_search(client, shadow,
|
|
benchUseStreamingSearch()),
|
|
std::chrono::duration_cast<std::chrono::milliseconds>(shadow.timeout) +
|
|
std::chrono::seconds(10));
|
|
if (shadowRun) {
|
|
shadowEntry.attempts = shadowRun.value().attempts;
|
|
shadowEntry.usedStreaming = shadowRun.value().usedStreaming;
|
|
shadowEntry.usedFuzzyRetry = shadowRun.value().usedFuzzyRetry;
|
|
shadowEntry.usedLiteralTextRetry = shadowRun.value().usedLiteralTextRetry;
|
|
shadowEntry.returnedDocIds.push_back(
|
|
"result_count=" +
|
|
std::to_string(shadowRun.value().response.results.size()));
|
|
for (size_t i = 0;
|
|
i < std::min<size_t>(5, shadowRun.value().response.results.size()); ++i) {
|
|
shadowEntry.returnedPaths.push_back(
|
|
shadowRun.value().response.results[i].path);
|
|
}
|
|
} else {
|
|
shadowEntry.returnedDocIds.push_back("shadow_error=" +
|
|
shadowRun.error().message);
|
|
}
|
|
|
|
debugLogWriteJsonLine(shadowEntry);
|
|
}
|
|
}
|
|
|
|
if (emitDebug) {
|
|
debugLogWriteJsonLine(debugEntry);
|
|
}
|
|
|
|
if (diagnostics) {
|
|
ingestQueryDiagnostics(*diagnostics, run.value().response.searchStats,
|
|
relevantDecisionTrace, false, !results.empty(),
|
|
scoreSample.hadDuplicateInTopK, scoreSample.firstRelevantRank);
|
|
ingestStageRetrievalMetrics(*diagnostics, run.value().response.searchStats, tq, k);
|
|
}
|
|
|
|
totalMRR += scoreSample.reciprocalRank;
|
|
totalRecall += scoreSample.recallAtK;
|
|
totalPrecision += scoreSample.precisionAtK;
|
|
totalMAP += scoreSample.averagePrecision;
|
|
totalNDCG += scoreSample.ndcgAtK;
|
|
}
|
|
|
|
if (metrics.numQueries > 0) {
|
|
metrics.mrr = totalMRR / metrics.numQueries;
|
|
metrics.recallAtK = totalRecall / metrics.numQueries;
|
|
metrics.precisionAtK = totalPrecision / metrics.numQueries;
|
|
metrics.ndcgAtK = totalNDCG / metrics.numQueries;
|
|
metrics.map = totalMAP / metrics.numQueries;
|
|
if (totalTopKCount > 0) {
|
|
metrics.duplicateRateAtK =
|
|
static_cast<double>(duplicateTopKCount) / static_cast<double>(totalTopKCount);
|
|
}
|
|
|
|
spdlog::info("Search execution stats: queries={} total_attempts={} avg_attempts={:.2f} "
|
|
"streaming={} fuzzy_retries={} literal_retries={} duplicate_rate@k={:.4f}",
|
|
metrics.numQueries, totalAttempts,
|
|
static_cast<double>(totalAttempts) / metrics.numQueries, streamingCount,
|
|
fuzzyRetryCount, literalRetryCount, metrics.duplicateRateAtK);
|
|
}
|
|
return metrics;
|
|
}
|
|
|
|
RetrievalMetrics evaluateGrepQueries(yams::daemon::DaemonClient& client,
|
|
const std::vector<TestQuery>& queries, int k) {
|
|
RetrievalMetrics metrics;
|
|
metrics.numQueries = static_cast<int>(queries.size());
|
|
|
|
double totalMRR = 0.0;
|
|
double totalRecall = 0.0;
|
|
double totalPrecision = 0.0;
|
|
double totalNDCG = 0.0;
|
|
double totalMAP = 0.0;
|
|
std::uint64_t totalTopKCount = 0;
|
|
std::uint64_t duplicateTopKCount = 0;
|
|
|
|
for (const auto& tq : queries) {
|
|
yams::daemon::GrepRequest req;
|
|
req.pattern = tq.query;
|
|
req.literalText = true;
|
|
req.pathsOnly = true;
|
|
req.maxMatches = 1;
|
|
req.useSession = false;
|
|
|
|
const auto queryTimeout = configuredBenchQueryTimeout("grep");
|
|
auto waitBudget = queryTimeout + std::chrono::seconds(10);
|
|
if (waitBudget < std::chrono::seconds(10)) {
|
|
waitBudget = std::chrono::seconds(10);
|
|
}
|
|
|
|
auto run = benchRunSync(client.grep(req), waitBudget);
|
|
if (!run) {
|
|
spdlog::warn("Grep failed for query '{}': {}", tq.query, run.error().message);
|
|
continue;
|
|
}
|
|
|
|
std::vector<std::string> rankedDocIds;
|
|
rankedDocIds.reserve(std::min<std::size_t>(static_cast<std::size_t>(std::max(0, k)),
|
|
run.value().pathsOnly.size()));
|
|
for (size_t i = 0; i < std::min<std::size_t>(static_cast<std::size_t>(std::max(0, k)),
|
|
run.value().pathsOnly.size());
|
|
++i) {
|
|
const std::string filename = fs::path(run.value().pathsOnly[i]).filename().string();
|
|
rankedDocIds.push_back(yams::bench::canonicalResultDocId(tq, filename));
|
|
totalTopKCount++;
|
|
}
|
|
|
|
const auto scoreSample = yams::bench::scoreRankedDocIds(tq, rankedDocIds, k);
|
|
duplicateTopKCount += scoreSample.duplicateDocIds.size();
|
|
totalMRR += scoreSample.reciprocalRank;
|
|
totalRecall += scoreSample.recallAtK;
|
|
totalPrecision += scoreSample.precisionAtK;
|
|
totalMAP += scoreSample.averagePrecision;
|
|
totalNDCG += scoreSample.ndcgAtK;
|
|
}
|
|
|
|
if (metrics.numQueries > 0) {
|
|
const double queryCount = static_cast<double>(metrics.numQueries);
|
|
metrics.mrr = totalMRR / queryCount;
|
|
metrics.recallAtK = totalRecall / queryCount;
|
|
metrics.precisionAtK = totalPrecision / queryCount;
|
|
metrics.ndcgAtK = totalNDCG / queryCount;
|
|
metrics.map = totalMAP / queryCount;
|
|
if (totalTopKCount > 0) {
|
|
metrics.duplicateRateAtK =
|
|
static_cast<double>(duplicateTopKCount) / static_cast<double>(totalTopKCount);
|
|
}
|
|
}
|
|
|
|
return metrics;
|
|
}
|
|
|
|
struct BenchFixture {
|
|
struct SetupTimingMetrics {
|
|
bool coldIngestPerformed = false;
|
|
int64_t admissionMs = 0;
|
|
int64_t storageReadyMs = 0;
|
|
int64_t pipelineDrainMs = 0;
|
|
int64_t searchabilityReadyMs = 0;
|
|
int64_t enrichmentReadyMs = 0;
|
|
bool vectorIndexPrepared = false;
|
|
bool vectorIndexPersisted = false;
|
|
bool vectorIndexReusableBefore = false;
|
|
bool vectorIndexReusableAfter = false;
|
|
bool vectorQueryReadyAfter = false;
|
|
int64_t vectorIndexPrepareMs = 0;
|
|
int64_t vectorIndexPersistMs = 0;
|
|
int64_t vectorIndexReusableProbeMs = 0;
|
|
std::uint64_t vectorIndexReusableProbeCount = 0;
|
|
int64_t setupTotalMs = 0;
|
|
};
|
|
|
|
std::unique_ptr<DaemonHarness> harness;
|
|
std::unique_ptr<ScopedEnvOverrides> scopedEnv;
|
|
std::unique_ptr<ScopedEnvOverrides> pluginScopedEnv;
|
|
std::unique_ptr<yams::daemon::DaemonClient> client;
|
|
std::unique_ptr<CorpusGenerator> corpus;
|
|
std::unique_ptr<BEIRCorpusLoader> beirCorpus;
|
|
fs::path benchCorpusDir;
|
|
fs::path warmDataDirPath;
|
|
std::optional<BenchCacheMetadata> warmCacheMetadata;
|
|
std::vector<TestQuery> queries;
|
|
int corpusSize = 50, numQueries = 10, topK = 10;
|
|
bool useBEIR = false;
|
|
SyntheticCorpusMode syntheticCorpusMode = SyntheticCorpusMode::Generic;
|
|
bool warmDataDir = false; // True when reusing a pre-ingested data directory
|
|
SetupTimingMetrics setupTimings;
|
|
std::string datasetName = "synthetic";
|
|
std::string beirDatasetName; // Name of BEIR dataset (scifact, cqadupstack, etc.)
|
|
|
|
bool useSyntheticCommunityCorpus() const {
|
|
return syntheticCorpusMode == SyntheticCorpusMode::CommunityGraph;
|
|
}
|
|
|
|
std::string datasetDisplayName() const {
|
|
if (useBEIR) {
|
|
return beirDatasetName == "local-manifest" ? std::string("local manifest")
|
|
: beirDatasetName + " BEIR";
|
|
}
|
|
if (syntheticCorpusMode == SyntheticCorpusMode::CommunityGraph) {
|
|
return "synthetic-community";
|
|
}
|
|
if (syntheticCorpusMode == SyntheticCorpusMode::HardLexical) {
|
|
return "synthetic-hard";
|
|
}
|
|
return datasetName;
|
|
}
|
|
|
|
bool benchmarkSemanticNeighborSeedingEnabled() const {
|
|
const char* raw = std::getenv("YAMS_BENCH_SEED_SEMANTIC_NEIGHBORS");
|
|
if (!(raw && *raw)) {
|
|
return false;
|
|
}
|
|
std::string value(raw);
|
|
std::transform(value.begin(), value.end(), value.begin(),
|
|
[](unsigned char c) { return static_cast<char>(std::tolower(c)); });
|
|
return value == "1" || value == "true" || value == "yes" || value == "on";
|
|
}
|
|
|
|
void seedSyntheticCommunityEdges() {
|
|
if (!useSyntheticCommunityCorpus()) {
|
|
return;
|
|
}
|
|
|
|
auto* daemon = harness ? harness->daemon() : nullptr;
|
|
auto* serviceManager = daemon ? daemon->getServiceManager() : nullptr;
|
|
auto metadataRepo = serviceManager ? serviceManager->getMetadataRepo() : nullptr;
|
|
auto kgStore = metadataRepo ? metadataRepo->getKnowledgeGraphStore() : nullptr;
|
|
if (!metadataRepo || !kgStore) {
|
|
throw std::runtime_error(
|
|
"Synthetic community corpus requires metadata and KG stores after ingest");
|
|
}
|
|
|
|
const std::array<std::string, 3> docNames = {
|
|
"community_target.txt", "community_partner.txt", "community_rival.txt"};
|
|
std::vector<std::int64_t> nodeIds;
|
|
nodeIds.reserve(docNames.size());
|
|
|
|
for (const auto& docName : docNames) {
|
|
const auto docPath = (benchCorpusDir / docName).string();
|
|
auto docResult = metadataRepo->findDocumentByExactPath(docPath);
|
|
if (!docResult) {
|
|
throw std::runtime_error("Failed to resolve synthetic community document " +
|
|
docPath + ": " + docResult.error().message);
|
|
}
|
|
if (!docResult.value().has_value()) {
|
|
throw std::runtime_error("Synthetic community document missing from metadata: " +
|
|
docPath);
|
|
}
|
|
|
|
const auto& doc = *docResult.value();
|
|
auto nodeIdResult = kgStore->ensureDocumentNode(doc.sha256Hash, doc.fileName);
|
|
if (!nodeIdResult) {
|
|
throw std::runtime_error("Failed to ensure synthetic community KG node: " +
|
|
nodeIdResult.error().message);
|
|
}
|
|
nodeIds.push_back(nodeIdResult.value());
|
|
}
|
|
|
|
std::vector<metadata::KGEdge> edges;
|
|
metadata::KGEdge forward;
|
|
forward.srcNodeId = nodeIds[0];
|
|
forward.dstNodeId = nodeIds[1];
|
|
forward.relation = "semantic_neighbor";
|
|
forward.weight = 1.0f;
|
|
edges.push_back(forward);
|
|
|
|
metadata::KGEdge reverse;
|
|
reverse.srcNodeId = nodeIds[1];
|
|
reverse.dstNodeId = nodeIds[0];
|
|
reverse.relation = "semantic_neighbor";
|
|
reverse.weight = 1.0f;
|
|
edges.push_back(reverse);
|
|
|
|
auto addEdgesResult = kgStore->addEdgesUnique(edges);
|
|
if (!addEdgesResult) {
|
|
throw std::runtime_error("Failed to seed synthetic community KG edges: " +
|
|
addEdgesResult.error().message);
|
|
}
|
|
|
|
spdlog::info("Seeded synthetic community KG edges for {} documents", docNames.size());
|
|
}
|
|
|
|
bool seedBenchmarkSemanticNeighbors() {
|
|
if (!benchmarkSemanticNeighborSeedingEnabled()) {
|
|
return false;
|
|
}
|
|
|
|
auto* daemon = harness ? harness->daemon() : nullptr;
|
|
auto* serviceManager = daemon ? daemon->getServiceManager() : nullptr;
|
|
auto metadataRepo = serviceManager ? serviceManager->getMetadataRepo() : nullptr;
|
|
auto kgStore = metadataRepo ? metadataRepo->getKnowledgeGraphStore() : nullptr;
|
|
auto vectorDb = serviceManager ? serviceManager->getVectorDatabase() : nullptr;
|
|
if (!metadataRepo || !kgStore) {
|
|
throw std::runtime_error("Semantic benchmark seeding requires metadata and KG stores");
|
|
}
|
|
|
|
auto parseSizeEnv = [](const char* primary, const char* fallback,
|
|
std::size_t defaultValue) {
|
|
for (const char* key : {primary, fallback}) {
|
|
if (!key) {
|
|
continue;
|
|
}
|
|
if (const char* raw = std::getenv(key); raw && *raw) {
|
|
try {
|
|
return static_cast<std::size_t>(std::max(1, std::stoi(raw)));
|
|
} catch (...) {
|
|
}
|
|
}
|
|
}
|
|
return defaultValue;
|
|
};
|
|
auto parseFloatEnv = [](const char* primary, const char* fallback, float defaultValue) {
|
|
for (const char* key : {primary, fallback}) {
|
|
if (!key) {
|
|
continue;
|
|
}
|
|
if (const char* raw = std::getenv(key); raw && *raw) {
|
|
try {
|
|
return std::clamp(std::stof(raw), 0.0f, 1.0f);
|
|
} catch (...) {
|
|
}
|
|
}
|
|
}
|
|
return defaultValue;
|
|
};
|
|
auto parseBoolEnv = [](const char* key, bool defaultValue) {
|
|
if (!(key && *key)) {
|
|
return defaultValue;
|
|
}
|
|
if (const char* raw = std::getenv(key); raw && *raw) {
|
|
std::string value(raw);
|
|
std::transform(value.begin(), value.end(), value.begin(),
|
|
[](unsigned char c) { return static_cast<char>(std::tolower(c)); });
|
|
return value == "1" || value == "true" || value == "yes" || value == "on";
|
|
}
|
|
return defaultValue;
|
|
};
|
|
|
|
std::string topologySource = "vector";
|
|
if (const char* raw = std::getenv("YAMS_BENCH_TOPOLOGY_SOURCE"); raw && *raw) {
|
|
topologySource = raw;
|
|
std::transform(topologySource.begin(), topologySource.end(), topologySource.begin(),
|
|
[](unsigned char c) { return static_cast<char>(std::tolower(c)); });
|
|
}
|
|
if (topologySource == "lexical" || topologySource == "bm25") {
|
|
topologySource = "fts5";
|
|
}
|
|
if (topologySource == "graph") {
|
|
topologySource = "kg";
|
|
}
|
|
if (topologySource == "title" || topologySource == "heading") {
|
|
topologySource = "header";
|
|
}
|
|
if (topologySource == "segment-keyphrase" || topologySource == "segmentkeyphrase") {
|
|
topologySource = "segment_keyphrase";
|
|
}
|
|
if (topologySource != "vector" && topologySource != "fts5" &&
|
|
topologySource != "keyphrase" && topologySource != "segment_keyphrase" &&
|
|
topologySource != "kg" && topologySource != "gliner" && topologySource != "header" &&
|
|
topologySource != "theme") {
|
|
throw std::runtime_error("YAMS_BENCH_TOPOLOGY_SOURCE must be vector, fts5, "
|
|
"keyphrase, segment_keyphrase, kg, gliner, header, or theme");
|
|
}
|
|
if (topologySource == "vector" && !vectorDb) {
|
|
throw std::runtime_error("Vector topology-source seeding requires vector store");
|
|
}
|
|
|
|
const std::size_t semanticTopK =
|
|
parseSizeEnv("YAMS_BENCH_SEED_SEMANTIC_TOPK", "YAMS_GRAPH_SEMANTIC_TOPK", 6);
|
|
const float semanticThreshold = parseFloatEnv("YAMS_BENCH_SEED_SEMANTIC_THRESHOLD",
|
|
"YAMS_GRAPH_SEMANTIC_THRESHOLD", 0.30f);
|
|
const float semanticWeightFloor =
|
|
parseFloatEnv("YAMS_BENCH_SEED_SEMANTIC_WEIGHT_FLOOR", nullptr, 0.10f);
|
|
// Off: mutual pairs only (the historical seeding). On: also one-way kNN edges, as the
|
|
// daemon writes them; the connected engine's roads need those (see
|
|
// buildSemanticNeighborSeedEdges).
|
|
const auto seedingMode = parseBoolEnv("YAMS_BENCH_SEED_SEMANTIC_DIRECTED", false)
|
|
? yams::bench::SemanticNeighborSeeding::DirectedKnn
|
|
: yams::bench::SemanticNeighborSeeding::MutualPairs;
|
|
const bool seedQrelCommunities = parseBoolEnv("YAMS_BENCH_SEED_QREL_COMMUNITIES", false) &&
|
|
useBEIR && static_cast<bool>(beirCorpus);
|
|
const std::size_t qrelTopK = parseSizeEnv("YAMS_BENCH_SEED_QREL_TOPK", nullptr, 4);
|
|
const std::size_t kgFeatureLimit =
|
|
parseSizeEnv("YAMS_BENCH_SEED_KG_FEATURE_LIMIT", nullptr, 512);
|
|
const std::size_t keyphraseFeatureLimit =
|
|
parseSizeEnv("YAMS_BENCH_KEYPHRASE_FEATURE_LIMIT", nullptr, 16);
|
|
const std::size_t keyphraseMaxDocumentFrequency =
|
|
parseSizeEnv("YAMS_BENCH_KEYPHRASE_MAX_DF", nullptr, 64);
|
|
const std::size_t segmentTokenBudget =
|
|
parseSizeEnv("YAMS_BENCH_SEGMENT_TOKENS", nullptr, 192);
|
|
const std::size_t segmentTokenOverlap =
|
|
parseSizeEnv("YAMS_BENCH_SEGMENT_OVERLAP", nullptr, 32);
|
|
const std::size_t segmentFeatureLimit =
|
|
parseSizeEnv("YAMS_BENCH_SEGMENT_FEATURE_LIMIT", nullptr, 12);
|
|
const std::size_t segmentMaxDocumentFrequency =
|
|
parseSizeEnv("YAMS_BENCH_SEGMENT_MAX_DF", nullptr, keyphraseMaxDocumentFrequency);
|
|
const std::size_t segmentMaxEdgesPerFeature =
|
|
parseSizeEnv("YAMS_BENCH_SEGMENT_FEATURE_EDGE_BUDGET", nullptr, 64);
|
|
const std::size_t topologyMaxComponentDocs =
|
|
parseSizeEnv("YAMS_BENCH_TOPOLOGY_MAX_COMPONENT_DOCS", nullptr, 64);
|
|
|
|
auto validateAndLogTopologyCertificate =
|
|
[&](const yams::bench::BenchmarkTopologyConstructionCertificate& cert) {
|
|
auto checkedCert = cert;
|
|
checkedCert.gates.maxComponentDocs = topologyMaxComponentDocs;
|
|
const auto validation =
|
|
yams::bench::validateTopologyConstructionCertificate(checkedCert);
|
|
if (g_debugOut) {
|
|
DebugLogEntry certificateEntry;
|
|
certificateEntry.searchType = "benchmark_setup";
|
|
certificateEntry.extraFields = {
|
|
{"event", "topology_construction_certificate"},
|
|
{"topology_source", topologySource},
|
|
{"certificate", yams::bench::topologyConstructionCertificateToJson(
|
|
checkedCert, validation)},
|
|
};
|
|
debugLogWriteJsonLine(certificateEntry);
|
|
}
|
|
if (!validation.ok) {
|
|
const std::string firstError =
|
|
validation.errors.empty()
|
|
? "unknown topology construction certificate error"
|
|
: validation.errors.front();
|
|
throw std::runtime_error(
|
|
"Benchmark topology construction certificate failed: " + firstError);
|
|
}
|
|
};
|
|
|
|
std::vector<std::string> documentPaths;
|
|
if (useBEIR && beirCorpus) {
|
|
documentPaths = beirCorpus->getDocumentPaths();
|
|
} else if (corpus) {
|
|
documentPaths.reserve(corpus->createdFiles.size());
|
|
for (const auto& filename : corpus->createdFiles) {
|
|
documentPaths.push_back((benchCorpusDir / filename).string());
|
|
}
|
|
}
|
|
if (documentPaths.empty()) {
|
|
throw std::runtime_error("Semantic benchmark seeding found no document paths");
|
|
}
|
|
|
|
struct DocSeedInfo {
|
|
std::string hash;
|
|
std::string benchmarkDocId;
|
|
std::string text;
|
|
std::string headerText;
|
|
std::string themeText;
|
|
std::vector<float> embedding;
|
|
std::int64_t documentId = 0;
|
|
std::int64_t nodeId = 0;
|
|
};
|
|
|
|
std::vector<DocSeedInfo> docs;
|
|
docs.reserve(documentPaths.size());
|
|
yams::crypto::SHA256Hasher contentHasher;
|
|
std::size_t warmPathRebases = 0;
|
|
|
|
auto trimCopy = [](std::string value) {
|
|
const auto first = std::find_if_not(
|
|
value.begin(), value.end(), [](unsigned char ch) { return std::isspace(ch) != 0; });
|
|
const auto last = std::find_if_not(value.rbegin(), value.rend(), [](unsigned char ch) {
|
|
return std::isspace(ch) != 0;
|
|
}).base();
|
|
if (first >= last) {
|
|
return std::string{};
|
|
}
|
|
return std::string(first, last);
|
|
};
|
|
auto deriveHeaderText = [&](std::string_view benchmarkDocId, std::string_view body,
|
|
std::string_view fallbackName) {
|
|
if (useBEIR && beirCorpus && !benchmarkDocId.empty()) {
|
|
if (auto it = beirCorpus->dataset.documents.find(std::string(benchmarkDocId));
|
|
it != beirCorpus->dataset.documents.end()) {
|
|
auto title = trimCopy(it->second.title);
|
|
if (!title.empty()) {
|
|
return title;
|
|
}
|
|
}
|
|
}
|
|
std::istringstream lines{std::string(body)};
|
|
std::string line;
|
|
while (std::getline(lines, line)) {
|
|
line = trimCopy(line);
|
|
if (line.empty()) {
|
|
continue;
|
|
}
|
|
while (!line.empty() && line.front() == '#') {
|
|
line.erase(line.begin());
|
|
}
|
|
line = trimCopy(line);
|
|
if (!line.empty()) {
|
|
return line;
|
|
}
|
|
}
|
|
return trimCopy(std::string(fallbackName));
|
|
};
|
|
|
|
std::unordered_set<std::string> topologyInputHashes;
|
|
std::size_t duplicateContentDocuments = 0;
|
|
for (const auto& docPath : documentPaths) {
|
|
auto docResult = metadataRepo->findDocumentByExactPath(docPath);
|
|
if (!docResult) {
|
|
throw std::runtime_error("Failed to resolve benchmark document " + docPath + ": " +
|
|
docResult.error().message);
|
|
}
|
|
auto resolvedDoc = std::move(docResult.value());
|
|
// Content-addressed ingest stores identical files once (warm caches rebase paths,
|
|
// and corpora such as nfcorpus contain duplicate texts), so fall back to the
|
|
// content hash to find the surviving document.
|
|
if (!resolvedDoc.has_value()) {
|
|
const auto contentHash = contentHasher.hashFile(docPath);
|
|
auto hashResult = metadataRepo->getDocumentByHash(contentHash);
|
|
if (!hashResult) {
|
|
throw std::runtime_error("Failed to resolve benchmark document " + docPath +
|
|
" by content hash: " + hashResult.error().message);
|
|
}
|
|
resolvedDoc = std::move(hashResult.value());
|
|
if (resolvedDoc.has_value() && warmDataDir) {
|
|
++warmPathRebases;
|
|
}
|
|
}
|
|
if (!resolvedDoc.has_value()) {
|
|
throw std::runtime_error("Benchmark document missing from metadata: " + docPath);
|
|
}
|
|
// A duplicate-content file resolves to a document already added as a topology input.
|
|
if (!topologyInputHashes.insert(resolvedDoc->sha256Hash).second) {
|
|
++duplicateContentDocuments;
|
|
continue;
|
|
}
|
|
|
|
const auto& doc = *resolvedDoc;
|
|
std::vector<float> embedding;
|
|
if (topologySource == "vector") {
|
|
const auto vectors = vectorDb->getVectorsByDocument(doc.sha256Hash);
|
|
if (vectors.empty()) {
|
|
continue;
|
|
}
|
|
|
|
const auto documentLevel = std::find_if(
|
|
vectors.begin(), vectors.end(), [](const yams::vector::VectorRecord& record) {
|
|
return record.level == yams::vector::EmbeddingLevel::DOCUMENT &&
|
|
!record.embedding.empty();
|
|
});
|
|
const auto chosen =
|
|
(documentLevel != vectors.end() && !documentLevel->embedding.empty())
|
|
? documentLevel
|
|
: std::find_if(vectors.begin(), vectors.end(),
|
|
[](const yams::vector::VectorRecord& record) {
|
|
return !record.embedding.empty();
|
|
});
|
|
if (chosen == vectors.end() || chosen->embedding.empty()) {
|
|
continue;
|
|
}
|
|
embedding = chosen->embedding;
|
|
}
|
|
|
|
std::string text;
|
|
std::string headerText;
|
|
std::string themeText;
|
|
if (topologySource == "fts5" || topologySource == "keyphrase" ||
|
|
topologySource == "segment_keyphrase" || topologySource == "header" ||
|
|
topologySource == "theme") {
|
|
auto contentResult = metadataRepo->getContent(doc.id);
|
|
if (contentResult && contentResult.value().has_value()) {
|
|
text = contentResult.value()->contentText;
|
|
}
|
|
if (text.empty()) {
|
|
std::ifstream in(docPath, std::ios::binary);
|
|
if (in) {
|
|
std::ostringstream buffer;
|
|
buffer << in.rdbuf();
|
|
text = buffer.str();
|
|
}
|
|
}
|
|
if (text.empty()) {
|
|
continue;
|
|
}
|
|
headerText =
|
|
deriveHeaderText(fs::path(docPath).stem().string(), text, doc.fileName);
|
|
themeText = headerText;
|
|
const std::size_t snippetSize = std::min<std::size_t>(text.size(), 512);
|
|
if (snippetSize > 0) {
|
|
if (!themeText.empty()) {
|
|
themeText.push_back('\n');
|
|
}
|
|
themeText.append(text.data(), snippetSize);
|
|
}
|
|
if (topologySource == "header" && headerText.empty()) {
|
|
continue;
|
|
}
|
|
if (topologySource == "theme" && themeText.empty()) {
|
|
continue;
|
|
}
|
|
}
|
|
|
|
auto nodeIdResult = kgStore->ensureDocumentNode(doc.sha256Hash, doc.fileName);
|
|
if (!nodeIdResult) {
|
|
throw std::runtime_error("Failed to ensure benchmark semantic KG node: " +
|
|
nodeIdResult.error().message);
|
|
}
|
|
docs.push_back({doc.sha256Hash, fs::path(docPath).stem().string(), text, headerText,
|
|
themeText, embedding, doc.id, nodeIdResult.value()});
|
|
}
|
|
if (duplicateContentDocuments > 0) {
|
|
spdlog::info("Topology benchmark skipped {} duplicate-content documents",
|
|
duplicateContentDocuments);
|
|
}
|
|
if (warmPathRebases > 0) {
|
|
spdlog::info("Resolved {} cloned warm-cache documents by stable content hash",
|
|
warmPathRebases);
|
|
}
|
|
|
|
if (docs.size() < 2) {
|
|
spdlog::warn("Skipping {} topology-source benchmark seeding: only {} usable documents",
|
|
topologySource, docs.size());
|
|
return false;
|
|
}
|
|
|
|
if ((topologySource == "kg" || topologySource == "gliner") && useBEIR && beirCorpus &&
|
|
!beirCorpus->dataset.kgEntities.empty()) {
|
|
std::unordered_map<std::string, const DocSeedInfo*> docByBenchmarkId;
|
|
docByBenchmarkId.reserve(docs.size());
|
|
for (const auto& doc : docs) {
|
|
if (!doc.benchmarkDocId.empty()) {
|
|
docByBenchmarkId.emplace(doc.benchmarkDocId, &doc);
|
|
}
|
|
}
|
|
|
|
std::vector<metadata::DocEntity> docEntities;
|
|
std::vector<metadata::KGEdge> manifestEdges;
|
|
std::size_t manifestEntitiesSeeded = 0;
|
|
for (const auto& entity : beirCorpus->dataset.kgEntities) {
|
|
const auto docIt = docByBenchmarkId.find(entity.docId);
|
|
if (docIt == docByBenchmarkId.end()) {
|
|
continue;
|
|
}
|
|
const auto* doc = docIt->second;
|
|
metadata::KGNode entityNode;
|
|
entityNode.nodeKey = "bench:entity:" + entity.entityKey;
|
|
entityNode.label = entity.label.empty() ? entity.entityKey : entity.label;
|
|
entityNode.type = entity.type.empty() ? "benchmark_entity" : entity.type;
|
|
entityNode.properties = json{
|
|
{"source", "benchmark_manifest"},
|
|
{"doc_id", entity.docId},
|
|
{"entity_key",
|
|
entity.entityKey}}.dump();
|
|
auto entityNodeResult = kgStore->upsertNode(entityNode);
|
|
if (!entityNodeResult) {
|
|
throw std::runtime_error(
|
|
"Failed to seed benchmark KG entity node for topology source: " +
|
|
entityNodeResult.error().message);
|
|
}
|
|
|
|
const float weight = std::clamp(entity.weight, 0.05F, 1.0F);
|
|
metadata::DocEntity docEntity;
|
|
docEntity.documentId = doc->documentId;
|
|
docEntity.entityText = entity.label.empty() ? entity.entityKey : entity.label;
|
|
docEntity.nodeId = entityNodeResult.value();
|
|
docEntity.confidence = weight;
|
|
docEntity.extractor =
|
|
entity.extractor.empty() ? "benchmark_manifest" : entity.extractor;
|
|
docEntities.push_back(docEntity);
|
|
|
|
metadata::KGEdge edge;
|
|
edge.srcNodeId = entityNodeResult.value();
|
|
edge.dstNodeId = doc->nodeId;
|
|
edge.relation = entity.relation.empty() || entity.relation == "semantic_neighbor"
|
|
? "manifest_entity_of"
|
|
: entity.relation;
|
|
edge.weight = weight;
|
|
edge.properties = json{{"source", "benchmark_manifest"},
|
|
{"doc_id", entity.docId},
|
|
{"entity_key", entity.entityKey}}
|
|
.dump();
|
|
manifestEdges.push_back(edge);
|
|
++manifestEntitiesSeeded;
|
|
}
|
|
|
|
if (!docEntities.empty()) {
|
|
auto addDocEntitiesResult = kgStore->addDocEntities(docEntities);
|
|
if (!addDocEntitiesResult) {
|
|
throw std::runtime_error(
|
|
"Failed to seed benchmark KG doc entities for topology source: " +
|
|
addDocEntitiesResult.error().message);
|
|
}
|
|
}
|
|
if (!manifestEdges.empty()) {
|
|
auto addEdgesResult = kgStore->addEdgesUnique(manifestEdges);
|
|
if (!addEdgesResult) {
|
|
throw std::runtime_error(
|
|
"Failed to seed benchmark KG relation edges for topology source: " +
|
|
addEdgesResult.error().message);
|
|
}
|
|
}
|
|
spdlog::info("Seeded {} manifest KG entity annotations and {} relation edges for "
|
|
"topology-source graph features",
|
|
manifestEntitiesSeeded, manifestEdges.size());
|
|
}
|
|
|
|
std::unordered_map<std::string, std::unordered_map<std::string, float>> neighborScores;
|
|
neighborScores.reserve(docs.size());
|
|
|
|
if (topologySource == "vector") {
|
|
yams::vector::VectorSearchParams params;
|
|
params.k = semanticTopK + 4;
|
|
params.similarity_threshold = semanticThreshold;
|
|
|
|
for (const auto& doc : docs) {
|
|
const auto neighbors = vectorDb->searchSimilar(doc.embedding, params);
|
|
auto& scores = neighborScores[doc.hash];
|
|
for (const auto& rec : neighbors) {
|
|
if (rec.level != yams::vector::EmbeddingLevel::DOCUMENT) {
|
|
continue;
|
|
}
|
|
if (rec.document_hash.empty() || rec.document_hash == doc.hash) {
|
|
continue;
|
|
}
|
|
if (rec.relevance_score < semanticThreshold) {
|
|
continue;
|
|
}
|
|
auto [it, inserted] = scores.emplace(rec.document_hash, rec.relevance_score);
|
|
if (!inserted) {
|
|
it->second = std::max(it->second, rec.relevance_score);
|
|
}
|
|
if (scores.size() >= semanticTopK) {
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
} else if (topologySource == "keyphrase" || topologySource == "segment_keyphrase") {
|
|
std::vector<yams::bench::KeyphraseTopologyDocument> keyphraseDocs;
|
|
keyphraseDocs.reserve(docs.size());
|
|
for (const auto& doc : docs) {
|
|
keyphraseDocs.push_back({.documentHash = doc.hash, .text = doc.text});
|
|
}
|
|
yams::bench::BenchmarkTopologyConstructionCertificate certificate;
|
|
std::vector<yams::bench::LexicalTopologyNeighbor> keyphraseNeighbors;
|
|
if (topologySource == "segment_keyphrase") {
|
|
keyphraseNeighbors = yams::bench::buildSegmentKeyphraseTopologyNeighbors(
|
|
keyphraseDocs, semanticTopK, semanticThreshold, segmentTokenBudget,
|
|
segmentTokenOverlap, segmentFeatureLimit, segmentMaxDocumentFrequency,
|
|
topologyMaxComponentDocs, segmentMaxEdgesPerFeature, true, &certificate);
|
|
} else {
|
|
keyphraseNeighbors = yams::bench::buildKeyphraseTopologyNeighbors(
|
|
keyphraseDocs, semanticTopK, semanticThreshold, keyphraseFeatureLimit,
|
|
keyphraseMaxDocumentFrequency, true, &certificate);
|
|
}
|
|
validateAndLogTopologyCertificate(certificate);
|
|
for (const auto& neighbor : keyphraseNeighbors) {
|
|
neighborScores[neighbor.sourceHash][neighbor.targetHash] = neighbor.score;
|
|
}
|
|
} else if (topologySource == "fts5" || topologySource == "header" ||
|
|
topologySource == "theme") {
|
|
std::vector<yams::bench::LexicalTopologyDocument> lexicalDocs;
|
|
lexicalDocs.reserve(docs.size());
|
|
for (const auto& doc : docs) {
|
|
std::string_view sourceText = doc.text;
|
|
if (topologySource == "header") {
|
|
sourceText = doc.headerText;
|
|
} else if (topologySource == "theme") {
|
|
sourceText = doc.themeText;
|
|
}
|
|
lexicalDocs.push_back({.documentHash = doc.hash, .text = std::string(sourceText)});
|
|
}
|
|
yams::bench::BenchmarkTopologyConstructionCertificate certificate;
|
|
const auto lexicalNeighbors = yams::bench::buildLexicalTopologyNeighbors(
|
|
lexicalDocs, semanticTopK, semanticThreshold, true, &certificate);
|
|
validateAndLogTopologyCertificate(certificate);
|
|
for (const auto& neighbor : lexicalNeighbors) {
|
|
neighborScores[neighbor.sourceHash][neighbor.targetHash] = neighbor.score;
|
|
}
|
|
} else {
|
|
const bool glinerOnly = topologySource == "gliner";
|
|
std::vector<yams::bench::GraphTopologyDocument> graphDocs;
|
|
graphDocs.reserve(docs.size());
|
|
std::size_t docsWithGraphFeatures = 0;
|
|
std::size_t docEntitiesScanned = 0;
|
|
std::size_t graphEdgesScanned = 0;
|
|
for (const auto& doc : docs) {
|
|
std::unordered_map<std::int64_t, float> featureByNode;
|
|
|
|
auto entityResult =
|
|
kgStore->getDocEntitiesForDocument(doc.documentId, kgFeatureLimit, 0);
|
|
if (!entityResult) {
|
|
throw std::runtime_error(
|
|
"Failed to read KG doc entities for topology source: " +
|
|
entityResult.error().message);
|
|
}
|
|
docEntitiesScanned += entityResult.value().size();
|
|
for (const auto& entity : entityResult.value()) {
|
|
if (glinerOnly) {
|
|
const auto extractor = entity.extractor.value_or(std::string{});
|
|
if (extractor.find("gliner") == std::string::npos &&
|
|
extractor.find("title_nl") == std::string::npos) {
|
|
continue;
|
|
}
|
|
}
|
|
if (!entity.nodeId.has_value() || entity.nodeId.value() <= 0 ||
|
|
entity.nodeId.value() == doc.nodeId) {
|
|
continue;
|
|
}
|
|
const float weight = std::clamp(entity.confidence.value_or(1.0F), 0.05F, 1.0F);
|
|
auto [it, inserted] = featureByNode.emplace(entity.nodeId.value(), weight);
|
|
if (!inserted) {
|
|
it->second = std::max(it->second, weight);
|
|
}
|
|
}
|
|
|
|
auto edgeResult =
|
|
kgStore->getEdgesBidirectional(doc.nodeId, std::nullopt, kgFeatureLimit);
|
|
if (!edgeResult) {
|
|
throw std::runtime_error("Failed to read KG edges for topology source: " +
|
|
edgeResult.error().message);
|
|
}
|
|
graphEdgesScanned += edgeResult.value().size();
|
|
for (const auto& edge : edgeResult.value()) {
|
|
if (glinerOnly && edge.relation != "title_mentions" &&
|
|
edge.relation != "primary_topic_of" && edge.relation != "mentioned_in") {
|
|
continue;
|
|
}
|
|
if (edge.relation == "semantic_neighbor") {
|
|
continue;
|
|
}
|
|
const std::int64_t otherNodeId =
|
|
edge.srcNodeId == doc.nodeId ? edge.dstNodeId : edge.srcNodeId;
|
|
if (otherNodeId <= 0 || otherNodeId == doc.nodeId) {
|
|
continue;
|
|
}
|
|
const float weight = std::clamp(edge.weight, 0.05F, 1.0F);
|
|
auto [it, inserted] = featureByNode.emplace(otherNodeId, weight);
|
|
if (!inserted) {
|
|
it->second = std::max(it->second, weight);
|
|
}
|
|
}
|
|
|
|
yams::bench::GraphTopologyDocument graphDoc;
|
|
graphDoc.documentHash = doc.hash;
|
|
graphDoc.relatedNodes.reserve(featureByNode.size());
|
|
for (const auto& [nodeId, weight] : featureByNode) {
|
|
graphDoc.relatedNodes.push_back({.nodeId = nodeId, .weight = weight});
|
|
}
|
|
if (!graphDoc.relatedNodes.empty()) {
|
|
++docsWithGraphFeatures;
|
|
}
|
|
graphDocs.push_back(graphDoc);
|
|
}
|
|
spdlog::info("{} topology-source scanned {} doc-entity rows and {} doc-adjacent KG "
|
|
"edges; {} / {} documents had graph features",
|
|
topologySource, docEntitiesScanned, graphEdgesScanned,
|
|
docsWithGraphFeatures, graphDocs.size());
|
|
yams::bench::BenchmarkTopologyConstructionCertificate certificate;
|
|
const auto graphNeighbors = yams::bench::buildGraphTopologyNeighbors(
|
|
graphDocs, semanticTopK, semanticThreshold, true, &certificate);
|
|
validateAndLogTopologyCertificate(certificate);
|
|
for (const auto& neighbor : graphNeighbors) {
|
|
neighborScores[neighbor.sourceHash][neighbor.targetHash] = neighbor.score;
|
|
}
|
|
}
|
|
|
|
std::unordered_map<std::string, std::int64_t> nodeIdByHash;
|
|
nodeIdByHash.reserve(docs.size());
|
|
std::unordered_map<std::string, std::string> hashByBenchmarkDocId;
|
|
hashByBenchmarkDocId.reserve(docs.size());
|
|
for (const auto& doc : docs) {
|
|
nodeIdByHash.emplace(doc.hash, doc.nodeId);
|
|
if (!doc.benchmarkDocId.empty()) {
|
|
hashByBenchmarkDocId.emplace(doc.benchmarkDocId, doc.hash);
|
|
}
|
|
}
|
|
|
|
std::vector<metadata::KGEdge> edges;
|
|
std::size_t mutualEdges = 0;
|
|
std::size_t oneWayEdges = 0;
|
|
for (const auto& seed : yams::bench::buildSemanticNeighborSeedEdges(
|
|
neighborScores, semanticWeightFloor, seedingMode)) {
|
|
const auto srcNodeIt = nodeIdByHash.find(seed.sourceHash);
|
|
const auto dstNodeIt = nodeIdByHash.find(seed.targetHash);
|
|
if (srcNodeIt == nodeIdByHash.end() || dstNodeIt == nodeIdByHash.end()) {
|
|
continue;
|
|
}
|
|
metadata::KGEdge edge;
|
|
edge.srcNodeId = srcNodeIt->second;
|
|
edge.dstNodeId = dstNodeIt->second;
|
|
edge.relation = "semantic_neighbor";
|
|
edge.weight = seed.weight;
|
|
edges.push_back(edge);
|
|
(seed.mutual ? mutualEdges : oneWayEdges) += 1;
|
|
}
|
|
const std::size_t reciprocalPairs = mutualEdges / 2;
|
|
|
|
std::size_t qrelPairs = 0;
|
|
if (seedQrelCommunities) {
|
|
std::unordered_map<std::string, std::unordered_map<std::string, int>> overlapCounts;
|
|
for (auto it = beirCorpus->dataset.qrels.begin();
|
|
it != beirCorpus->dataset.qrels.end();) {
|
|
const auto queryId = it->first;
|
|
std::vector<std::string> docsForQuery;
|
|
for (; it != beirCorpus->dataset.qrels.end() && it->first == queryId; ++it) {
|
|
if (it->second.score <= 0 || !hashByBenchmarkDocId.contains(it->second.docId)) {
|
|
continue;
|
|
}
|
|
docsForQuery.push_back(it->second.docId);
|
|
}
|
|
|
|
std::sort(docsForQuery.begin(), docsForQuery.end());
|
|
docsForQuery.erase(std::unique(docsForQuery.begin(), docsForQuery.end()),
|
|
docsForQuery.end());
|
|
for (std::size_t i = 0; i < docsForQuery.size(); ++i) {
|
|
for (std::size_t j = i + 1; j < docsForQuery.size(); ++j) {
|
|
overlapCounts[docsForQuery[i]][docsForQuery[j]] += 1;
|
|
overlapCounts[docsForQuery[j]][docsForQuery[i]] += 1;
|
|
}
|
|
}
|
|
}
|
|
|
|
std::set<std::pair<std::string, std::string>> addedPairs;
|
|
for (const auto& [docId, neighbors] : overlapCounts) {
|
|
const auto srcHashIt = hashByBenchmarkDocId.find(docId);
|
|
if (srcHashIt == hashByBenchmarkDocId.end()) {
|
|
continue;
|
|
}
|
|
const auto srcNodeIt = nodeIdByHash.find(srcHashIt->second);
|
|
if (srcNodeIt == nodeIdByHash.end()) {
|
|
continue;
|
|
}
|
|
|
|
std::vector<std::pair<std::string, int>> ranked(neighbors.begin(), neighbors.end());
|
|
std::sort(ranked.begin(), ranked.end(), [](const auto& left, const auto& right) {
|
|
if (left.second != right.second) {
|
|
return left.second > right.second;
|
|
}
|
|
return left.first < right.first;
|
|
});
|
|
if (ranked.size() > qrelTopK) {
|
|
ranked.resize(qrelTopK);
|
|
}
|
|
|
|
for (const auto& [neighborDocId, sharedCount] : ranked) {
|
|
const auto dstHashIt = hashByBenchmarkDocId.find(neighborDocId);
|
|
if (dstHashIt == hashByBenchmarkDocId.end()) {
|
|
continue;
|
|
}
|
|
const auto dstNodeIt = nodeIdByHash.find(dstHashIt->second);
|
|
if (dstNodeIt == nodeIdByHash.end() || srcNodeIt->second == dstNodeIt->second) {
|
|
continue;
|
|
}
|
|
|
|
const auto ordered = std::minmax(srcHashIt->second, dstHashIt->second);
|
|
if (!addedPairs.insert(ordered).second) {
|
|
continue;
|
|
}
|
|
|
|
const float weight = std::clamp(
|
|
std::max(semanticWeightFloor, 0.25f * static_cast<float>(sharedCount)),
|
|
0.0f, 1.0f);
|
|
metadata::KGEdge forward;
|
|
forward.srcNodeId = srcNodeIt->second;
|
|
forward.dstNodeId = dstNodeIt->second;
|
|
forward.relation = "semantic_neighbor";
|
|
forward.weight = weight;
|
|
edges.push_back(forward);
|
|
|
|
metadata::KGEdge reverse;
|
|
reverse.srcNodeId = dstNodeIt->second;
|
|
reverse.dstNodeId = srcNodeIt->second;
|
|
reverse.relation = "semantic_neighbor";
|
|
reverse.weight = weight;
|
|
edges.push_back(reverse);
|
|
qrelPairs += 1;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (edges.empty()) {
|
|
spdlog::warn("{} topology-source benchmark seeding produced no reciprocal neighbors "
|
|
"(docs={}, topk={}, threshold={:.3f})",
|
|
topologySource, docs.size(), semanticTopK, semanticThreshold);
|
|
return false;
|
|
}
|
|
|
|
auto deleteExistingResult = kgStore->deleteEdgesByRelation("semantic_neighbor");
|
|
if (!deleteExistingResult) {
|
|
throw std::runtime_error("Failed to clear pre-existing benchmark semantic KG edges: " +
|
|
deleteExistingResult.error().message);
|
|
}
|
|
|
|
auto addEdgesResult = kgStore->addEdgesUnique(edges);
|
|
if (!addEdgesResult) {
|
|
throw std::runtime_error("Failed to seed benchmark semantic KG edges: " +
|
|
addEdgesResult.error().message);
|
|
}
|
|
|
|
spdlog::info("Seeded {} {} topology-source neighbor pairs, {} one-way kNN edges and {} "
|
|
"qrel-neighbor pairs ({} directed edges) for benchmark corpus after clearing "
|
|
"{} prior semantic edges",
|
|
reciprocalPairs, topologySource, oneWayEdges, qrelPairs, edges.size(),
|
|
deleteExistingResult.value());
|
|
return true;
|
|
}
|
|
|
|
void setup() {
|
|
using SetupClock = std::chrono::steady_clock;
|
|
const auto setupStartedAt = SetupClock::now();
|
|
std::optional<SetupClock::time_point> coldIngestStartedAt;
|
|
std::optional<SetupClock::time_point> admissionCompletedAt;
|
|
const auto elapsedMsSince = [](SetupClock::time_point startedAt) {
|
|
return std::chrono::duration_cast<std::chrono::milliseconds>(SetupClock::now() -
|
|
startedAt)
|
|
.count();
|
|
};
|
|
|
|
const char* env_dataset = std::getenv("YAMS_BENCH_DATASET");
|
|
const char* env_debug_file = std::getenv("YAMS_BENCH_DEBUG_FILE");
|
|
if (env_debug_file && std::strlen(env_debug_file) > 0) {
|
|
g_debugFile =
|
|
std::make_unique<std::ofstream>(env_debug_file, std::ios::out | std::ios::app);
|
|
if (g_debugFile->is_open()) {
|
|
g_debugOut = g_debugFile.get();
|
|
spdlog::info("Benchmark debug logging enabled: {}", env_debug_file);
|
|
DebugLogEntry startEntry;
|
|
startEntry.searchType = "benchmark_setup";
|
|
startEntry.extraFields = {
|
|
{"event", "setup"},
|
|
{"debug_file", env_debug_file},
|
|
{"topology_source", std::getenv("YAMS_BENCH_TOPOLOGY_SOURCE")
|
|
? std::getenv("YAMS_BENCH_TOPOLOGY_SOURCE")
|
|
: "vector"},
|
|
{"seed_semantic_neighbors",
|
|
std::getenv("YAMS_BENCH_SEED_SEMANTIC_NEIGHBORS")
|
|
? std::getenv("YAMS_BENCH_SEED_SEMANTIC_NEIGHBORS")
|
|
: "0"},
|
|
// Requested arm values; per-query search_stats carry the effective
|
|
// topology_representative_rule / topology_route_bq_rotation.
|
|
{"topology_representative_rule",
|
|
yams::config::getenv_optional("YAMS_BENCH_TOPOLOGY_REPRESENTATIVE_RULE")
|
|
.value_or("degree")},
|
|
{"topology_bq_rotation",
|
|
yams::config::getenv_optional("YAMS_BENCH_TOPOLOGY_BQ_ROTATION")
|
|
.value_or("none")},
|
|
};
|
|
debugLogWriteJsonLine(startEntry);
|
|
} else {
|
|
spdlog::warn("Failed to open YAMS_BENCH_DEBUG_FILE={} for writing", env_debug_file);
|
|
g_debugFile.reset();
|
|
g_debugOut = nullptr;
|
|
}
|
|
}
|
|
|
|
const char* env_path = std::getenv("YAMS_BENCH_DATASET_PATH");
|
|
const char* env_size = std::getenv("YAMS_BENCH_CORPUS_SIZE");
|
|
const char* env_queries = std::getenv("YAMS_BENCH_NUM_QUERIES");
|
|
const char* env_topk = std::getenv("YAMS_BENCH_TOPK");
|
|
|
|
if (env_dataset) {
|
|
std::string requestedDatasetName = env_dataset;
|
|
// Supported BEIR datasets from
|
|
// https://public.ukp.informatik.tu-darmstadt.de/thakur/BEIR/datasets/
|
|
static const std::set<std::string> SUPPORTED_BEIR_DATASETS = {
|
|
"scifact", "nfcorpus", "arguana", "scidocs", "fiqa",
|
|
"quora", "cqadupstack", "hotpotqa", "fever", "climate-fever",
|
|
"dbpedia-entity", "trec-covid", "touche-2020", "longmemeval_s", "local-manifest"};
|
|
if (SUPPORTED_BEIR_DATASETS.count(requestedDatasetName) > 0) {
|
|
useBEIR = true;
|
|
beirDatasetName = requestedDatasetName;
|
|
datasetName = requestedDatasetName;
|
|
// Default to SearchTuner auto mode so benchmark reflects dynamic tuning behavior.
|
|
// Keep explicit override support for controlled experiments:
|
|
// YAMS_BENCH_FORCE_TUNING_OVERRIDE=SCIENTIFIC
|
|
const char* forcedOverride = std::getenv("YAMS_BENCH_FORCE_TUNING_OVERRIDE");
|
|
const char* existingOverride = std::getenv("YAMS_TUNING_OVERRIDE");
|
|
if (forcedOverride && std::strlen(forcedOverride) > 0) {
|
|
spdlog::info(
|
|
"Will set YAMS_TUNING_OVERRIDE={} via YAMS_BENCH_FORCE_TUNING_OVERRIDE "
|
|
"for dataset-backed benchmark ({})",
|
|
forcedOverride, requestedDatasetName);
|
|
} else if (existingOverride && std::strlen(existingOverride) > 0) {
|
|
spdlog::info(
|
|
"Using pre-set YAMS_TUNING_OVERRIDE={} for dataset-backed benchmark ({})",
|
|
existingOverride, requestedDatasetName);
|
|
} else {
|
|
spdlog::info("Using SearchTuner auto mode for dataset-backed benchmark ({})",
|
|
requestedDatasetName);
|
|
}
|
|
} else if (requestedDatasetName == "synthetic-community") {
|
|
datasetName = requestedDatasetName;
|
|
syntheticCorpusMode = SyntheticCorpusMode::CommunityGraph;
|
|
} else if (requestedDatasetName == "synthetic-hard") {
|
|
datasetName = requestedDatasetName;
|
|
syntheticCorpusMode = SyntheticCorpusMode::HardLexical;
|
|
} else {
|
|
spdlog::warn("Unknown dataset '{}', using synthetic. Supported: scifact, "
|
|
"cqadupstack, nfcorpus, synthetic-hard, local-manifest, etc.",
|
|
requestedDatasetName);
|
|
}
|
|
}
|
|
|
|
if (env_size)
|
|
corpusSize = std::stoi(env_size);
|
|
if (env_queries)
|
|
numQueries = std::stoi(env_queries);
|
|
if (env_topk)
|
|
topK = std::stoi(env_topk);
|
|
|
|
std::vector<EnvSetting> benchmarkEnvOverrides;
|
|
benchmarkEnvOverrides.reserve(24);
|
|
auto addEnvDefault = [&](const char* key, const char* value) {
|
|
const char* current = std::getenv(key);
|
|
if (!(current && std::strlen(current) > 0)) {
|
|
benchmarkEnvOverrides.push_back({key, std::string(value)});
|
|
}
|
|
};
|
|
|
|
if (useSyntheticCommunityCorpus()) {
|
|
corpusSize = 3;
|
|
if (!(env_queries && *env_queries)) {
|
|
numQueries = 1;
|
|
}
|
|
if (!(env_topk && *env_topk)) {
|
|
topK = 1;
|
|
}
|
|
|
|
// Force the graph rerank stage fully on for the synthetic community A/B.
|
|
// Tiny synthetic corpora are still classified as no_kg by the tuner, so enabling the
|
|
// stage alone is not enough to produce an actual rerank window.
|
|
addEnvDefault("YAMS_ENABLE_ENV_OVERRIDES", "1");
|
|
addEnvDefault("YAMS_SEARCH_KG_WEIGHT", "0.00");
|
|
addEnvDefault("YAMS_SEARCH_GRAPH_RERANK_TOPN", "3");
|
|
addEnvDefault("YAMS_SEARCH_GRAPH_RERANK_WEIGHT", "1.00");
|
|
addEnvDefault("YAMS_SEARCH_GRAPH_RERANK_MAX_BOOST", "1.00");
|
|
addEnvDefault("YAMS_SEARCH_GRAPH_RERANK_MIN_SIGNAL", "0.01");
|
|
addEnvDefault("YAMS_SEARCH_KG_MAX_RESULTS", "3");
|
|
addEnvDefault("YAMS_SEARCH_GRAPH_SCORING_BUDGET_MS", "8");
|
|
}
|
|
|
|
auto optionalPositiveInt = [](const char* raw) -> std::optional<int> {
|
|
if (!(raw && *raw)) {
|
|
return std::nullopt;
|
|
}
|
|
const int parsed = std::stoi(raw);
|
|
if (parsed <= 0) {
|
|
return std::nullopt;
|
|
}
|
|
return parsed;
|
|
};
|
|
|
|
const std::optional<int> corpusLimit = optionalPositiveInt(env_size);
|
|
const std::optional<int> queryLimit = optionalPositiveInt(env_queries);
|
|
|
|
spdlog::info("Setting up RAG benchmark: {} dataset, {} docs, {} queries, k={}",
|
|
datasetDisplayName(), corpusSize, numQueries, topK);
|
|
|
|
// PBI-05b: Create summary log file for important embedding metrics
|
|
fs::path summaryLogPath = fs::temp_directory_path() / "yams_bench_summary.log";
|
|
std::ofstream summaryLog(summaryLogPath, std::ios::trunc);
|
|
if (summaryLog) {
|
|
auto now = std::chrono::system_clock::now();
|
|
auto time_t_now = std::chrono::system_clock::to_time_t(now);
|
|
summaryLog << "=== YAMS Retrieval Quality Benchmark ===" << std::endl;
|
|
summaryLog << "Started: " << std::ctime(&time_t_now);
|
|
summaryLog << "Dataset: " << datasetDisplayName() << std::endl;
|
|
summaryLog << "Corpus size: " << corpusSize << std::endl;
|
|
summaryLog << "Num queries: " << numQueries << std::endl;
|
|
summaryLog << "Top K: " << topK << std::endl;
|
|
summaryLog << std::endl;
|
|
summaryLog.flush();
|
|
}
|
|
spdlog::info("Summary log: {}", summaryLogPath.string());
|
|
|
|
const char* disableVectors = std::getenv("YAMS_DISABLE_VECTORS");
|
|
bool vectorsDisabled = disableVectors && std::string(disableVectors) == "1";
|
|
const bool forceMockEmbeddings = forceMockEmbeddingsForBench();
|
|
|
|
auto envFlagEnabled = [](const char* value) -> bool {
|
|
if (!value) {
|
|
return false;
|
|
}
|
|
std::string normalized(value);
|
|
std::transform(normalized.begin(), normalized.end(), normalized.begin(), [](char c) {
|
|
return static_cast<char>(std::tolower(static_cast<unsigned char>(c)));
|
|
});
|
|
return normalized == "1" || normalized == "true" || normalized == "yes" ||
|
|
normalized == "on";
|
|
};
|
|
|
|
if (useBEIR) {
|
|
const char* forcedOverride = std::getenv("YAMS_BENCH_FORCE_TUNING_OVERRIDE");
|
|
if (forcedOverride && std::strlen(forcedOverride) > 0) {
|
|
benchmarkEnvOverrides.push_back({"YAMS_ENABLE_ENV_OVERRIDES", std::string("1")});
|
|
benchmarkEnvOverrides.push_back(
|
|
{"YAMS_TUNING_OVERRIDE", std::string(forcedOverride)});
|
|
}
|
|
}
|
|
|
|
// Benchmark determinism defaults (can still be overridden by explicit env values).
|
|
// Real-ingest graph validation needs the post-ingest rebuild so search tuning sees the
|
|
// final corpus stats instead of the startup-empty corpus.
|
|
addEnvDefault("YAMS_DISABLE_SEARCH_REBUILDS", "0");
|
|
addEnvDefault("YAMS_SEARCH_WAIT_FOR_CONCEPTS", "1");
|
|
addEnvDefault("YAMS_HNSW_RANDOM_SEED", "42");
|
|
addEnvDefault("YAMS_HNSW_PARALLEL_BUILD_THRESHOLD", "0");
|
|
// Use higher concurrency for embedding/extraction to reduce ingestion time.
|
|
// HNSW seed + wait-for-concepts provide sufficient determinism for ranking;
|
|
// single-threaded embedding only added wall-clock cost without improving reproducibility.
|
|
addEnvDefault("YAMS_POST_EMBED_CONCURRENT", "4");
|
|
addEnvDefault("YAMS_POST_EXTRACTION_CONCURRENT", "4");
|
|
addEnvDefault("YAMS_POST_KG_CONCURRENT", "1");
|
|
addEnvDefault("YAMS_POST_TITLE_CONCURRENT", "1");
|
|
|
|
// Retrieval-quality benchmark default: use Simeon's built-in embedding backend. This avoids
|
|
// loading ONNX Runtime on the common benchmark path; operators can still request the legacy
|
|
// ONNX/ORT path with YAMS_BENCH_EMBED_BACKEND=onnx or YAMS_EMBED_BACKEND=onnx.
|
|
if (const char* benchEmbedBackend = std::getenv("YAMS_BENCH_EMBED_BACKEND");
|
|
benchEmbedBackend && *benchEmbedBackend) {
|
|
benchmarkEnvOverrides.push_back({"YAMS_EMBED_BACKEND", std::string(benchEmbedBackend)});
|
|
} else {
|
|
addEnvDefault("YAMS_EMBED_BACKEND", "simeon");
|
|
}
|
|
|
|
// Adaptive sub-batch tuning: the default 15s warning threshold causes premature
|
|
// batch-cap collapse (8→4→1) on machines where ONNX inference is legitimately slow.
|
|
// Raise to 60s so the adaptive logic only shrinks on true stalls, not normal latency.
|
|
addEnvDefault("YAMS_EMBED_SUBBATCH_WARN_MS", "60000");
|
|
|
|
// Benchmark default: graph rerank is ON unless explicitly overridden.
|
|
// Canonical env key used by SearchEngineBuilder is YAMS_SEARCH_ENABLE_GRAPH_RERANK.
|
|
const char* graphRerankCanonical = std::getenv("YAMS_SEARCH_ENABLE_GRAPH_RERANK");
|
|
if (!(graphRerankCanonical && *graphRerankCanonical)) {
|
|
benchmarkEnvOverrides.push_back({"YAMS_SEARCH_ENABLE_GRAPH_RERANK", std::string("1")});
|
|
graphRerankCanonical = "1";
|
|
}
|
|
|
|
if (env_debug_file && std::strlen(env_debug_file) > 0) {
|
|
addEnvDefault("YAMS_SEARCH_STAGE_TRACE", "1");
|
|
const int traceTopNDefault =
|
|
parseIntEnvOrDefault("YAMS_BENCH_TRACE_TOP_N", 50, 1, 10000);
|
|
addEnvDefault("YAMS_SEARCH_STAGE_TRACE_TOP_N",
|
|
std::to_string(traceTopNDefault).c_str());
|
|
const int traceComponentDefault = parseIntEnvOrDefault(
|
|
"YAMS_BENCH_TRACE_COMPONENT_TOP_N", std::min(traceTopNDefault, 50), 1, 10000);
|
|
addEnvDefault("YAMS_SEARCH_STAGE_TRACE_COMPONENT_TOP_N",
|
|
std::to_string(traceComponentDefault).c_str());
|
|
}
|
|
|
|
const bool graphRerankRequested = envFlagEnabled(graphRerankCanonical);
|
|
std::optional<bool> explicitRequireKgReady;
|
|
if (const char* envRequireKgReady = std::getenv("YAMS_BENCH_REQUIRE_KG_READY");
|
|
envRequireKgReady && *envRequireKgReady) {
|
|
explicitRequireKgReady = envFlagEnabled(envRequireKgReady);
|
|
}
|
|
const auto kgReadinessPolicy = yams::search::resolveBenchmarkKgReadinessPolicy(
|
|
graphRerankRequested, explicitRequireKgReady, useBEIR);
|
|
const bool requireKgReady =
|
|
kgReadinessPolicy != yams::search::BenchmarkKgReadinessPolicy::Skip;
|
|
|
|
scopedEnv = std::make_unique<ScopedEnvOverrides>(benchmarkEnvOverrides);
|
|
|
|
spdlog::info("[Bench] KG readiness policy: {} (graph_rerank_requested={}, beir={}, "
|
|
"explicit_override={})",
|
|
yams::search::benchmarkKgReadinessPolicyToString(kgReadinessPolicy),
|
|
graphRerankRequested ? "true" : "false", useBEIR ? "true" : "false",
|
|
explicitRequireKgReady.has_value()
|
|
? (*explicitRequireKgReady ? "true" : "false")
|
|
: "none");
|
|
|
|
fs::path datasetPathForMetadata = env_path ? fs::path(env_path) : fs::path();
|
|
Result<BEIRDataset> preparedBeirDataset =
|
|
Error{ErrorCode::Unknown, "BEIR dataset not requested"};
|
|
if (useBEIR) {
|
|
preparedBeirDataset = selectBenchmarkBEIRDataset(
|
|
beirDatasetName, datasetPathForMetadata, corpusLimit, queryLimit);
|
|
if (!preparedBeirDataset) {
|
|
throw std::runtime_error("Failed to load BEIR dataset: " +
|
|
preparedBeirDataset.error().message);
|
|
}
|
|
corpusSize = static_cast<int>(preparedBeirDataset.value().documents.size());
|
|
numQueries = static_cast<int>(preparedBeirDataset.value().queries.size());
|
|
}
|
|
|
|
BenchCacheMetadata expectedCacheMetadata;
|
|
expectedCacheMetadata.dataset = useBEIR ? beirDatasetName : datasetName;
|
|
expectedCacheMetadata.datasetPath = canonicalPathOrEmpty(datasetPathForMetadata);
|
|
expectedCacheMetadata.corpusSize = corpusSize;
|
|
expectedCacheMetadata.numQueries = numQueries;
|
|
expectedCacheMetadata.topK = topK;
|
|
expectedCacheMetadata.useBEIR = useBEIR;
|
|
expectedCacheMetadata.vectorsDisabled = vectorsDisabled;
|
|
expectedCacheMetadata.requireKgReady = requireKgReady;
|
|
expectedCacheMetadata.graphRerankRequested = graphRerankRequested;
|
|
expectedCacheMetadata.searchEngine =
|
|
std::string(yams::vector::vectorSearchEngineName(benchmarkVectorSearchEngine()));
|
|
expectedCacheMetadata.expectedDocs = corpusSize;
|
|
expectedCacheMetadata.expectedQueries = numQueries;
|
|
|
|
spdlog::info("[Bench] Effective graph rerank env: YAMS_SEARCH_ENABLE_GRAPH_RERANK={}",
|
|
graphRerankCanonical ? graphRerankCanonical : "<unset>");
|
|
spdlog::info(
|
|
"[Bench] Determinism envs: YAMS_DISABLE_SEARCH_REBUILDS={} "
|
|
"YAMS_SEARCH_WAIT_FOR_CONCEPTS={} YAMS_HNSW_RANDOM_SEED={} "
|
|
"YAMS_HNSW_PARALLEL_BUILD_THRESHOLD={} YAMS_POST_EMBED_CONCURRENT={} "
|
|
"YAMS_POST_EXTRACTION_CONCURRENT={} YAMS_POST_KG_CONCURRENT={}",
|
|
std::getenv("YAMS_DISABLE_SEARCH_REBUILDS")
|
|
? std::getenv("YAMS_DISABLE_SEARCH_REBUILDS")
|
|
: "<unset>",
|
|
std::getenv("YAMS_SEARCH_WAIT_FOR_CONCEPTS")
|
|
? std::getenv("YAMS_SEARCH_WAIT_FOR_CONCEPTS")
|
|
: "<unset>",
|
|
std::getenv("YAMS_HNSW_RANDOM_SEED") ? std::getenv("YAMS_HNSW_RANDOM_SEED") : "<unset>",
|
|
std::getenv("YAMS_HNSW_PARALLEL_BUILD_THRESHOLD")
|
|
? std::getenv("YAMS_HNSW_PARALLEL_BUILD_THRESHOLD")
|
|
: "<unset>",
|
|
std::getenv("YAMS_POST_EMBED_CONCURRENT") ? std::getenv("YAMS_POST_EMBED_CONCURRENT")
|
|
: "<unset>",
|
|
std::getenv("YAMS_POST_EXTRACTION_CONCURRENT")
|
|
? std::getenv("YAMS_POST_EXTRACTION_CONCURRENT")
|
|
: "<unset>",
|
|
std::getenv("YAMS_POST_KG_CONCURRENT") ? std::getenv("YAMS_POST_KG_CONCURRENT")
|
|
: "<unset>");
|
|
|
|
const bool embedBackendSimeon = [] {
|
|
const char* v = std::getenv("YAMS_EMBED_BACKEND");
|
|
return v && std::string{v} == "simeon";
|
|
}();
|
|
|
|
DaemonHarness::Options harnessOptions;
|
|
harnessOptions.isolateState = true;
|
|
harnessOptions.useMockModelProvider = vectorsDisabled || forceMockEmbeddings;
|
|
harnessOptions.autoLoadPlugins =
|
|
!vectorsDisabled && !forceMockEmbeddings && !embedBackendSimeon;
|
|
harnessOptions.configureModelPool = !vectorsDisabled && !forceMockEmbeddings;
|
|
harnessOptions.modelPoolLazyLoading = false;
|
|
harnessOptions.pluginDirStrict =
|
|
!vectorsDisabled && !forceMockEmbeddings && !embedBackendSimeon;
|
|
if (embedBackendSimeon) {
|
|
harnessOptions.enableModelProvider = true;
|
|
harnessOptions.useMockModelProvider = false;
|
|
}
|
|
|
|
// YAMS_BENCH_DATA_DIR: reuse a pre-ingested data directory to skip
|
|
// corpus ingestion + embedding wait on repeated benchmark runs.
|
|
// YAMS_BENCH_WARM_CACHE_DIR: like YAMS_BENCH_DATA_DIR, but validates a metadata file and
|
|
// auto-writes one after a cold run so repeated full-corpus benchmarks are practical.
|
|
const char* envWarmCacheDir = std::getenv("YAMS_BENCH_WARM_CACHE_DIR");
|
|
const char* envDataDir = std::getenv("YAMS_BENCH_DATA_DIR");
|
|
if (envWarmCacheDir && *envWarmCacheDir) {
|
|
fs::path cacheDir = fs::path(envWarmCacheDir);
|
|
warmDataDirPath = cacheDir;
|
|
if (fs::exists(cacheDir)) {
|
|
auto metadataResult = readBenchCacheMetadata(cacheDir);
|
|
if (metadataResult) {
|
|
const auto& cachedMetadata = metadataResult.value();
|
|
std::string mismatchReason;
|
|
if (benchCacheMatches(expectedCacheMetadata, cachedMetadata, &mismatchReason)) {
|
|
harnessOptions.dataDir = cacheDir;
|
|
warmDataDir = true;
|
|
warmCacheMetadata = cachedMetadata;
|
|
spdlog::info(
|
|
"[Bench] Reusing warm cache dir: {} (status={}, embedded_docs={}, "
|
|
"vectors={})",
|
|
cacheDir.string(), cachedMetadata.status, cachedMetadata.embeddedDocs,
|
|
cachedMetadata.vectorCount);
|
|
} else {
|
|
std::error_code removeEc;
|
|
const bool canResumePartial = cachedMetadata.status == "partial" &&
|
|
mismatchReason == "cache status not reusable";
|
|
if (!canResumePartial) {
|
|
fs::remove_all(cacheDir, removeEc);
|
|
if (removeEc) {
|
|
spdlog::warn("[Bench] Failed to clear stale warm cache dir {}: {}",
|
|
cacheDir.string(), removeEc.message());
|
|
}
|
|
}
|
|
harnessOptions.dataDir = cacheDir;
|
|
if (canResumePartial) {
|
|
warmDataDir = true;
|
|
warmCacheMetadata = metadataResult.value();
|
|
spdlog::warn(
|
|
"[Bench] Warm cache dir {} is partially primed; reusing it "
|
|
"to resume embedding completion",
|
|
cacheDir.string());
|
|
} else {
|
|
spdlog::warn(
|
|
"[Bench] Warm cache dir {} metadata mismatch ({}); running "
|
|
"cold and refreshing cache",
|
|
cacheDir.string(), mismatchReason);
|
|
}
|
|
}
|
|
} else {
|
|
harnessOptions.dataDir = cacheDir;
|
|
spdlog::warn("[Bench] Warm cache dir {} missing/invalid metadata ({}); "
|
|
"running cold and refreshing cache",
|
|
cacheDir.string(), metadataResult.error().message);
|
|
}
|
|
} else {
|
|
harnessOptions.dataDir = cacheDir;
|
|
spdlog::info("[Bench] Warm cache dir {} does not exist yet; cold run will create "
|
|
"it",
|
|
cacheDir.string());
|
|
}
|
|
} else if (envDataDir && *envDataDir) {
|
|
fs::path dataDirPath(envDataDir);
|
|
if (fs::exists(dataDirPath)) {
|
|
harnessOptions.dataDir = dataDirPath;
|
|
warmDataDir = true;
|
|
warmDataDirPath = dataDirPath;
|
|
spdlog::info("[Bench] Reusing warm data dir: {} (skip ingestion + embed wait)",
|
|
dataDirPath.string());
|
|
} else {
|
|
spdlog::warn("[Bench] YAMS_BENCH_DATA_DIR={} does not exist, ignoring", envDataDir);
|
|
}
|
|
}
|
|
|
|
if (!vectorsDisabled && !forceMockEmbeddings) {
|
|
bool resetPluginTrustFile = harnessOptions.dataDir.has_value();
|
|
bool useIsolatedBenchmarkConfig = true;
|
|
const char* topologyModeEnv = std::getenv("YAMS_BENCH_TOPOLOGY_MODE");
|
|
const bool topologyDisabled =
|
|
topologyModeEnv && std::string_view{topologyModeEnv} == "disabled";
|
|
const char* topologyEngineEnv = std::getenv("YAMS_BENCH_TOPOLOGY_ENGINE");
|
|
const char* topologyRepresentativesEnv =
|
|
std::getenv("YAMS_BENCH_TOPOLOGY_ROUTING_REPRESENTATIVES");
|
|
const char* topologyBoundarySpillEnv =
|
|
std::getenv("YAMS_BENCH_TOPOLOGY_BOUNDARY_SPILL");
|
|
const char* topologyRepresentativeLimitEnv =
|
|
std::getenv("YAMS_BENCH_TOPOLOGY_ROUTE_REPRESENTATIVE_LIMIT");
|
|
const char* topologyAnnCandidateLimitEnv =
|
|
std::getenv("YAMS_BENCH_TOPOLOGY_ROUTE_ANN_CANDIDATE_LIMIT");
|
|
const auto envSet = [](const std::optional<std::string>& value) {
|
|
return value.has_value() && !value->empty();
|
|
};
|
|
const auto topologyRoadHopsEnv =
|
|
yams::config::getenv_optional("YAMS_BENCH_TOPOLOGY_ROAD_HOPS");
|
|
const auto topologyRoadBeamWidthEnv =
|
|
yams::config::getenv_optional("YAMS_BENCH_TOPOLOGY_ROAD_BEAM_WIDTH");
|
|
const auto topologyRoadWalkWindowEnv =
|
|
yams::config::getenv_optional("YAMS_BENCH_TOPOLOGY_ROAD_WALK_WINDOW");
|
|
const auto topologyRoadMaxPerClusterEnv =
|
|
yams::config::getenv_optional("YAMS_BENCH_TOPOLOGY_ROAD_MAX_PER_CLUSTER");
|
|
const auto topologyRoadOrderEnv =
|
|
yams::config::getenv_optional("YAMS_BENCH_TOPOLOGY_ROAD_ORDER");
|
|
const auto topologyBqCandidateLimitEnv =
|
|
yams::config::getenv_optional("YAMS_BENCH_TOPOLOGY_ROUTE_BQ_CANDIDATE_LIMIT");
|
|
const auto topologyBqPrefixDimEnv =
|
|
yams::config::getenv_optional("YAMS_BENCH_TOPOLOGY_ROUTE_BQ_PREFIX_DIM");
|
|
const auto topologyBqRotationEnv =
|
|
yams::config::getenv_optional("YAMS_BENCH_TOPOLOGY_BQ_ROTATION");
|
|
const auto topologyRepresentativeRuleEnv =
|
|
yams::config::getenv_optional("YAMS_BENCH_TOPOLOGY_REPRESENTATIVE_RULE");
|
|
const auto topologySgcHopsEnv =
|
|
yams::config::getenv_optional("YAMS_BENCH_TOPOLOGY_SGC_HOPS");
|
|
const auto simeonOuterMaxSimEnv =
|
|
yams::config::getenv_optional("YAMS_BENCH_SIMEON_OUTER_MAXSIM");
|
|
const auto graphCommunitySourceEnv =
|
|
yams::config::getenv_optional("YAMS_BENCH_GRAPH_COMMUNITY_SOURCE");
|
|
const char* topologyExpansionOutputLimitEnv =
|
|
std::getenv("YAMS_BENCH_TOPOLOGY_EXPANSION_OUTPUT_LIMIT");
|
|
const char* topologyGraphWeightedSeedRankingEnv =
|
|
std::getenv("YAMS_BENCH_TOPOLOGY_GRAPH_WEIGHTED_SEED_RANKING");
|
|
const auto hasBenchmarkSetting = [](const char* name) {
|
|
const char* value = std::getenv(name);
|
|
return value && *value;
|
|
};
|
|
const bool writeTopologyAdmissionConfig =
|
|
hasBenchmarkSetting("YAMS_BENCH_TOPOLOGY_ROUTE_CALIBRATION_FINGERPRINT") ||
|
|
hasBenchmarkSetting("YAMS_BENCH_TOPOLOGY_ROUTE_CALIBRATION_QUERIES") ||
|
|
hasBenchmarkSetting("YAMS_BENCH_TOPOLOGY_ROUTE_CALIBRATION_PROTECTED_CANDIDATES") ||
|
|
hasBenchmarkSetting(
|
|
"YAMS_BENCH_TOPOLOGY_ROUTE_CALIBRATION_MISSED_PROTECTED_CANDIDATES") ||
|
|
hasBenchmarkSetting("YAMS_BENCH_TOPOLOGY_ROUTE_MIN_CALIBRATION_QUERIES") ||
|
|
hasBenchmarkSetting("YAMS_BENCH_TOPOLOGY_ROUTE_MAX_MISSES_PER_THOUSAND") ||
|
|
hasBenchmarkSetting("YAMS_BENCH_TOPOLOGY_ROUTE_CALIBRATION_MIN_BOUNDARY_MARGIN") ||
|
|
hasBenchmarkSetting("YAMS_BENCH_TOPOLOGY_ROUTE_CALIBRATION_MIN_SEED_HITS") ||
|
|
hasBenchmarkSetting("YAMS_BENCH_TOPOLOGY_ROUTE_WORK_MAX_ROWS_VISITED") ||
|
|
hasBenchmarkSetting(
|
|
"YAMS_BENCH_TOPOLOGY_ROUTE_WORK_MAX_EXACT_DISTANCE_EVALUATIONS") ||
|
|
hasBenchmarkSetting("YAMS_BENCH_TOPOLOGY_ROUTE_WORK_MAX_ANN_CANDIDATES");
|
|
const bool writeTopologyEngineConfig =
|
|
(topologyEngineEnv && *topologyEngineEnv) ||
|
|
(topologyRepresentativesEnv && *topologyRepresentativesEnv) ||
|
|
(topologyBoundarySpillEnv && *topologyBoundarySpillEnv) ||
|
|
envSet(topologySgcHopsEnv) || envSet(topologyRepresentativeRuleEnv) ||
|
|
envSet(topologyRoadMaxPerClusterEnv) || envSet(topologyRoadOrderEnv);
|
|
const bool writeTopologyRouteConfig =
|
|
yams::config::getenv_optional("YAMS_BENCH_TOPOLOGY_VECTOR_POLICY").has_value() ||
|
|
(std::getenv("YAMS_BENCH_TOPOLOGY_ROUTE_SCORING") != nullptr) ||
|
|
(std::getenv("YAMS_BENCH_TOPOLOGY_SPARSE_DENSE_ALPHA") != nullptr) ||
|
|
(std::getenv("YAMS_BENCH_TOPOLOGY_MIN_ROUTE_SCORE") != nullptr) ||
|
|
(std::getenv("YAMS_BENCH_TOPOLOGY_MEDOID_ONLY_EXPANSION") != nullptr) ||
|
|
(std::getenv("YAMS_BENCH_TOPOLOGY_MIN_CLUSTERS") != nullptr) ||
|
|
(std::getenv("YAMS_BENCH_TOPOLOGY_MAX_SEED_DOCUMENTS") != nullptr) ||
|
|
envSet(topologyRoadHopsEnv) || envSet(topologyRoadBeamWidthEnv) ||
|
|
envSet(topologyRoadWalkWindowEnv) ||
|
|
(topologyRepresentativeLimitEnv && *topologyRepresentativeLimitEnv) ||
|
|
(topologyAnnCandidateLimitEnv && *topologyAnnCandidateLimitEnv) ||
|
|
envSet(topologyBqCandidateLimitEnv) || envSet(topologyBqPrefixDimEnv) ||
|
|
envSet(topologyBqRotationEnv) || envSet(graphCommunitySourceEnv) ||
|
|
(topologyExpansionOutputLimitEnv && *topologyExpansionOutputLimitEnv) ||
|
|
(topologyGraphWeightedSeedRankingEnv && *topologyGraphWeightedSeedRankingEnv) ||
|
|
(std::getenv("YAMS_BENCH_TOPOLOGY_ADAPTIVE_PROBE_SCORE_GAP") != nullptr) ||
|
|
(std::getenv("YAMS_BENCH_TOPOLOGY_NARROW_MIN_BOUNDARY_MARGIN") != nullptr) ||
|
|
(std::getenv("YAMS_BENCH_TOPOLOGY_EXPANSION") != nullptr) ||
|
|
(std::getenv("YAMS_SEARCH_TOPOLOGY_EXPANSION_SOURCE") != nullptr) ||
|
|
writeTopologyAdmissionConfig;
|
|
|
|
// Select a plugin root that satisfies the plugins the benchmark enables.
|
|
// Preferring the first ONNX-capable tree can silently disable entity extraction
|
|
// when that tree lacks the Glint plugin.
|
|
const bool enableGlintPlugin =
|
|
!embedBackendSimeon || envTruthy(std::getenv("YAMS_BENCH_ENABLE_GLINT"));
|
|
std::string glinerModelPath;
|
|
if (enableGlintPlugin) {
|
|
glinerModelPath = discoverGlinerModelPath();
|
|
} else {
|
|
spdlog::info("Simeon benchmark path: Glint/GLiNER disabled by default");
|
|
}
|
|
const bool needsGlintPlugin = !glinerModelPath.empty();
|
|
const bool needsOnnxPlugin = !embedBackendSimeon;
|
|
const char* envPluginDir = std::getenv("YAMS_PLUGIN_DIR");
|
|
if (envPluginDir) {
|
|
harnessOptions.pluginDir = fs::path(envPluginDir);
|
|
} else {
|
|
const fs::path cwd = fs::current_path();
|
|
const fs::path installedPluginDir = fs::path("/opt/homebrew/lib/yams/plugins");
|
|
const fs::path installedOnnxPlugin =
|
|
installedPluginDir / "libyams_onnx_plugin.dylib";
|
|
const fs::path installedGlintPlugin = installedPluginDir / "yams_glint.dylib";
|
|
const fs::path localPluginDir = cwd / "plugins";
|
|
const fs::path localOnnxPlugin =
|
|
localPluginDir / "onnx" / "libyams_onnx_plugin.dylib";
|
|
const fs::path localGlintPlugin = localPluginDir / "glint" / "yams_glint.dylib";
|
|
|
|
const fs::path root = cwd;
|
|
const fs::path nosanPluginDir = root / "builddir-nosan" / "plugins";
|
|
const fs::path nosanOnnxPlugin =
|
|
nosanPluginDir / "onnx" / "libyams_onnx_plugin.dylib";
|
|
const fs::path nosanGlintPlugin = nosanPluginDir / "glint" / "yams_glint.dylib";
|
|
const fs::path defaultPluginDir = root / "builddir" / "plugins";
|
|
const fs::path defaultOnnxPlugin =
|
|
defaultPluginDir / "onnx" / "libyams_onnx_plugin.dylib";
|
|
const fs::path defaultGlintPlugin = defaultPluginDir / "glint" / "yams_glint.dylib";
|
|
|
|
const auto choosePreferredPluginDir = [&]() -> std::optional<fs::path> {
|
|
struct Candidate {
|
|
fs::path dir;
|
|
fs::path onnxPlugin;
|
|
fs::path glintPlugin;
|
|
};
|
|
const std::array<Candidate, 3> candidates = {
|
|
{{localPluginDir, localOnnxPlugin, localGlintPlugin},
|
|
{nosanPluginDir, nosanOnnxPlugin, nosanGlintPlugin},
|
|
{defaultPluginDir, defaultOnnxPlugin, defaultGlintPlugin}}};
|
|
for (const auto& candidate : candidates) {
|
|
if (needsOnnxPlugin && !fs::exists(candidate.onnxPlugin)) {
|
|
continue;
|
|
}
|
|
if (needsGlintPlugin && !fs::exists(candidate.glintPlugin)) {
|
|
continue;
|
|
}
|
|
return candidate.dir;
|
|
}
|
|
return std::nullopt;
|
|
};
|
|
|
|
if (auto preferredPluginDir = choosePreferredPluginDir();
|
|
preferredPluginDir.has_value()) {
|
|
harnessOptions.pluginDir = *preferredPluginDir;
|
|
} else if (needsOnnxPlugin && fs::exists(installedOnnxPlugin) &&
|
|
(!needsGlintPlugin || fs::exists(installedGlintPlugin))) {
|
|
const fs::path stagedPluginDir =
|
|
fs::temp_directory_path() / "yams_retrieval_bench_plugins";
|
|
const fs::path stagedOnnxPlugin =
|
|
stagedPluginDir / installedOnnxPlugin.filename();
|
|
const fs::path stagedGlintPlugin =
|
|
stagedPluginDir / installedGlintPlugin.filename();
|
|
std::error_code ec;
|
|
fs::path installedOnnxSource = fs::weakly_canonical(installedOnnxPlugin, ec);
|
|
if (ec || installedOnnxSource.empty()) {
|
|
ec.clear();
|
|
installedOnnxSource = installedOnnxPlugin;
|
|
}
|
|
fs::path installedGlintSource = installedGlintPlugin;
|
|
if (needsGlintPlugin) {
|
|
installedGlintSource = fs::weakly_canonical(installedGlintPlugin, ec);
|
|
if (ec || installedGlintSource.empty()) {
|
|
ec.clear();
|
|
installedGlintSource = installedGlintPlugin;
|
|
}
|
|
}
|
|
yams::common::ensureDirectories(stagedPluginDir);
|
|
if (ec) {
|
|
spdlog::warn("Failed to create staged plugin dir {}: {}",
|
|
stagedPluginDir.string(), ec.message());
|
|
harnessOptions.pluginDir = installedPluginDir;
|
|
} else {
|
|
fs::remove(stagedOnnxPlugin, ec);
|
|
ec.clear();
|
|
fs::copy_file(installedOnnxSource, stagedOnnxPlugin,
|
|
fs::copy_options::overwrite_existing, ec);
|
|
if (ec) {
|
|
spdlog::warn("Failed to stage installed ONNX plugin {} -> {}: {}",
|
|
installedOnnxSource.string(), stagedOnnxPlugin.string(),
|
|
ec.message());
|
|
harnessOptions.pluginDir = installedPluginDir;
|
|
} else if (needsGlintPlugin) {
|
|
fs::remove(stagedGlintPlugin, ec);
|
|
ec.clear();
|
|
fs::copy_file(installedGlintSource, stagedGlintPlugin,
|
|
fs::copy_options::overwrite_existing, ec);
|
|
if (ec) {
|
|
throw std::runtime_error("Failed to stage installed Glint plugin " +
|
|
installedGlintSource.string() + " -> " +
|
|
stagedGlintPlugin.string() + ": " +
|
|
ec.message());
|
|
}
|
|
harnessOptions.pluginDir = stagedPluginDir;
|
|
spdlog::info("Using isolated installed ONNX+Glint plugin dir: {}",
|
|
stagedPluginDir.string());
|
|
} else {
|
|
harnessOptions.pluginDir = stagedPluginDir;
|
|
spdlog::info("Using isolated installed ONNX plugin dir: {}",
|
|
stagedPluginDir.string());
|
|
}
|
|
}
|
|
} else if (needsOnnxPlugin && needsGlintPlugin) {
|
|
throw std::runtime_error(
|
|
"Glint plugin required for benchmark entity extraction but no plugin root "
|
|
"contains both ONNX and Glint plugins. Build with -Dplugin-glint=true or "
|
|
"set YAMS_PLUGIN_DIR to a directory containing libyams_onnx_plugin.dylib "
|
|
"and yams_glint.dylib.");
|
|
} else if (needsGlintPlugin) {
|
|
// Glint-only (simeon backend): find any dir with Glint
|
|
if (fs::exists(localGlintPlugin))
|
|
harnessOptions.pluginDir = localPluginDir;
|
|
else if (fs::exists(nosanGlintPlugin))
|
|
harnessOptions.pluginDir = nosanPluginDir;
|
|
else if (fs::exists(defaultGlintPlugin))
|
|
harnessOptions.pluginDir = defaultPluginDir;
|
|
else
|
|
spdlog::warn("[Bench] Glint plugin not found; entity extraction disabled");
|
|
} else if (fs::exists(localPluginDir)) {
|
|
harnessOptions.pluginDir = localPluginDir;
|
|
} else if (fs::exists(nosanPluginDir)) {
|
|
harnessOptions.pluginDir = nosanPluginDir;
|
|
} else {
|
|
harnessOptions.pluginDir = defaultPluginDir;
|
|
}
|
|
}
|
|
|
|
if (embedBackendSimeon) {
|
|
// Simeon backend is the preferred fast, built-in benchmark path.
|
|
// Keep live-ish runs off ONNX entirely unless the operator explicitly
|
|
// opts in via YAMS_BENCH_ENABLE_ONNX_RERANK=1.
|
|
const bool enableOnnxRerank =
|
|
envTruthy(std::getenv("YAMS_BENCH_ENABLE_ONNX_RERANK"));
|
|
if (enableOnnxRerank) {
|
|
json onnxConfig;
|
|
onnxConfig["reranker_model"] = "bge-reranker-base";
|
|
harnessOptions.pluginConfigs["onnx_plugin"] = onnxConfig.dump();
|
|
spdlog::info("Configured ONNX plugin: reranker=bge-reranker-base (explicit "
|
|
"opt-in on simeon backend)");
|
|
} else {
|
|
spdlog::info("Simeon benchmark path: ONNX reranker disabled");
|
|
}
|
|
} else {
|
|
json onnxPluginConfig;
|
|
onnxPluginConfig["preferred_model"] = "embeddinggemma-300m";
|
|
onnxPluginConfig["reranker_model"] = "bge-reranker-base";
|
|
harnessOptions.pluginConfigs["onnx_plugin"] = onnxPluginConfig.dump();
|
|
spdlog::info("Configured ONNX plugin models: embedding=embeddinggemma-300m "
|
|
"reranker=bge-reranker-base");
|
|
}
|
|
|
|
std::vector<EnvSetting> pluginEnvOverrides;
|
|
if (harnessOptions.pluginConfigs.contains("onnx_plugin") &&
|
|
!std::getenv("YAMS_ONNX_RERANK_FORCE_CPU") &&
|
|
!std::getenv("YAMS_ONNX_RERANK_FORCE_GPU")) {
|
|
pluginEnvOverrides.push_back({"YAMS_ONNX_RERANK_FORCE_CPU", std::string("1")});
|
|
spdlog::info("Enabled CPU-preferred ONNX reranker for benchmark evaluation");
|
|
}
|
|
if (!pluginEnvOverrides.empty()) {
|
|
pluginScopedEnv = std::make_unique<ScopedEnvOverrides>(pluginEnvOverrides);
|
|
}
|
|
|
|
// Configure Glint plugin with GLiNER model path for NL entity extraction
|
|
if (!glinerModelPath.empty()) {
|
|
harnessOptions.autoLoadPlugins = true;
|
|
harnessOptions.pluginDirStrict = false;
|
|
json glintConfig;
|
|
glintConfig["model_path"] = glinerModelPath;
|
|
glintConfig["threshold"] = 0.5; // Default confidence threshold
|
|
harnessOptions.pluginConfigs["glint"] = glintConfig.dump();
|
|
spdlog::info("Configured Glint plugin with model: {}", glinerModelPath);
|
|
}
|
|
|
|
if (resetPluginTrustFile && harnessOptions.dataDir) {
|
|
const fs::path trustFile = *harnessOptions.dataDir / "plugins.trust";
|
|
std::set<fs::path> trusted;
|
|
if (harnessOptions.pluginDir) {
|
|
trusted.insert(
|
|
yams::daemon::plugin_trust::normalizePath(*harnessOptions.pluginDir));
|
|
}
|
|
if (!yams::daemon::plugin_trust::writeTrustStore(trustFile, trusted)) {
|
|
spdlog::warn("Failed to rewrite plugin trust file {}", trustFile.string());
|
|
} else {
|
|
spdlog::info("Rewrote plugin trust file for isolated benchmark plugins: {}",
|
|
trustFile.string());
|
|
}
|
|
}
|
|
|
|
if (useIsolatedBenchmarkConfig) {
|
|
std::string configSuffix = "yams_retrieval_bench_config.toml";
|
|
const std::string engineLabel = benchmarkEngineCacheLabel();
|
|
configSuffix = "yams_retrieval_bench_config_" + engineLabel + ".toml";
|
|
if (g_debugRunContext.has_value() && !g_debugRunContext->candidate.empty()) {
|
|
configSuffix = "yams_retrieval_bench_config_" + engineLabel + "_" +
|
|
g_debugRunContext->candidate + ".toml";
|
|
}
|
|
const fs::path configPath = fs::temp_directory_path() / configSuffix;
|
|
std::ofstream configOut(configPath, std::ios::trunc);
|
|
if (!configOut) {
|
|
spdlog::warn("Failed to write isolated benchmark config: {}",
|
|
configPath.string());
|
|
} else {
|
|
configOut << "# Isolated benchmark config\n";
|
|
if (harnessOptions.dataDir) {
|
|
configOut << "[core]\n";
|
|
configOut << "data_dir = \"" << harnessOptions.dataDir->string()
|
|
<< "\"\n\n";
|
|
configOut << "[storage]\n";
|
|
configOut << "base_path = \"" << harnessOptions.dataDir->string()
|
|
<< "\"\n\n";
|
|
}
|
|
configOut << "[embeddings]\n";
|
|
if (embedBackendSimeon) {
|
|
configOut << "preferred_model = \"simeon-default\"\n";
|
|
const bool fixedHash384 = g_simeonEncoderProfile == "fixed_hash_384";
|
|
configOut << "embedding_dim = " << (fixedHash384 ? 384 : 1024) << "\n\n";
|
|
if (!g_simeonEncoderProfile.empty()) {
|
|
configOut << "[embeddings.simeon]\n";
|
|
configOut << "encoder_profile = \"" << g_simeonEncoderProfile
|
|
<< "\"\n\n";
|
|
}
|
|
if (g_simeonBm25Enabled.has_value()) {
|
|
configOut << "[embeddings.simeon.bm25]\n";
|
|
configOut << "enabled = " << (*g_simeonBm25Enabled ? "true" : "false")
|
|
<< "\n\n";
|
|
}
|
|
if (g_simeonRouterEnabled.has_value()) {
|
|
configOut << "[embeddings.simeon.bm25.router]\n";
|
|
configOut << "enabled = " << (*g_simeonRouterEnabled ? "true" : "false")
|
|
<< "\n\n";
|
|
}
|
|
if (g_simeonFragmentGeometryEnabled.has_value() ||
|
|
!g_simeonFragmentEncoderProfile.empty()) {
|
|
configOut << "[embeddings.simeon.bm25.fragment_geometry]\n";
|
|
if (g_simeonFragmentGeometryEnabled.has_value()) {
|
|
configOut << "enabled = "
|
|
<< (*g_simeonFragmentGeometryEnabled ? "true" : "false")
|
|
<< "\n";
|
|
}
|
|
if (!g_simeonFragmentEncoderProfile.empty()) {
|
|
configOut << "encoder_profile = \""
|
|
<< g_simeonFragmentEncoderProfile << "\"\n";
|
|
}
|
|
configOut << "\n";
|
|
}
|
|
configOut << "[daemon.models]\n";
|
|
configOut << "preload_models = []\n";
|
|
} else {
|
|
configOut << "preferred_model = \"embeddinggemma-300m\"\n";
|
|
configOut << "embedding_dim = 768\n\n";
|
|
configOut << "[daemon.models]\n";
|
|
configOut << "preload_models = [\"embeddinggemma-300m\"]\n";
|
|
}
|
|
yams::bench::writeTopologyDisabledEmbeddingSelection(configOut,
|
|
topologyDisabled);
|
|
if (envSet(simeonOuterMaxSimEnv)) {
|
|
configOut << "\n[search]\n";
|
|
configOut << "simeon_rerank_outer_maxsim = "
|
|
<< (envTruthy(simeonOuterMaxSimEnv->c_str()) ? "true" : "false")
|
|
<< "\n";
|
|
}
|
|
configOut << "\n[vector_database]\n";
|
|
configOut << "search_engine = \"" << g_benchmark_search_engine << "\"\n";
|
|
if (std::getenv("YAMS_BENCH_SIMEON_PQ_RERANK_FACTOR") != nullptr) {
|
|
configOut << "simeon_pq_rerank_factor = "
|
|
<< std::max<std::size_t>(
|
|
1, parseSizeEnvOrDefault(
|
|
"YAMS_BENCH_SIMEON_PQ_RERANK_FACTOR", 2))
|
|
<< "\n";
|
|
}
|
|
if ((topologyModeEnv && *topologyModeEnv) || writeTopologyRouteConfig) {
|
|
configOut << "\n[search.topology]\n";
|
|
if (topologyModeEnv && *topologyModeEnv) {
|
|
configOut << "mode = \"" << topologyModeEnv << "\"\n";
|
|
}
|
|
if (const char* policy = std::getenv("YAMS_BENCH_TOPOLOGY_VECTOR_POLICY");
|
|
policy && *policy) {
|
|
configOut << "vector_policy = \"" << policy << "\"\n";
|
|
}
|
|
if (const char* selector =
|
|
std::getenv("YAMS_BENCH_TOPOLOGY_RESCUE_SELECTOR");
|
|
selector && *selector) {
|
|
configOut << "rescue_selector = \"" << selector << "\"\n";
|
|
}
|
|
configOut << "min_clusters = "
|
|
<< parseSizeEnvOrDefault("YAMS_BENCH_TOPOLOGY_MIN_CLUSTERS", 1)
|
|
<< "\n";
|
|
configOut << "max_clusters = "
|
|
<< parseSizeEnvOrFallback("YAMS_BENCH_TOPOLOGY_MAX_CLUSTERS",
|
|
"YAMS_SEARCH_TOPOLOGY_MAX_CLUSTERS", 2)
|
|
<< "\n";
|
|
configOut << "max_docs = "
|
|
<< parseSizeEnvOrFallback("YAMS_BENCH_TOPOLOGY_MAX_DOCS",
|
|
"YAMS_SEARCH_TOPOLOGY_MAX_DOCS", 64)
|
|
<< "\n";
|
|
if (topologyExpansionOutputLimitEnv && *topologyExpansionOutputLimitEnv) {
|
|
configOut << "expansion_output_limit = "
|
|
<< parseSizeEnvOrDefault(
|
|
"YAMS_BENCH_TOPOLOGY_EXPANSION_OUTPUT_LIMIT", 0)
|
|
<< "\n";
|
|
}
|
|
configOut << "max_seed_documents = "
|
|
<< parseSizeEnvOrDefault("YAMS_BENCH_TOPOLOGY_MAX_SEED_DOCUMENTS",
|
|
32)
|
|
<< "\n";
|
|
// Road beam arms (route_only / augment); unset keeps the product default
|
|
// of no road hops.
|
|
if (envSet(topologyRoadHopsEnv)) {
|
|
configOut << "road_hops = "
|
|
<< parseSizeEnvOrDefault("YAMS_BENCH_TOPOLOGY_ROAD_HOPS", 0)
|
|
<< "\n";
|
|
}
|
|
if (envSet(topologyRoadBeamWidthEnv)) {
|
|
configOut
|
|
<< "road_beam_width = "
|
|
<< parseSizeEnvOrDefault("YAMS_BENCH_TOPOLOGY_ROAD_BEAM_WIDTH", 4)
|
|
<< "\n";
|
|
}
|
|
// Road walk arm (route_only / augment); unset keeps the product default
|
|
// of no walk.
|
|
if (envSet(topologyRoadWalkWindowEnv)) {
|
|
configOut
|
|
<< "road_walk_window = "
|
|
<< parseSizeEnvOrDefault("YAMS_BENCH_TOPOLOGY_ROAD_WALK_WINDOW", 0)
|
|
<< "\n";
|
|
}
|
|
if (topologyRepresentativeLimitEnv && *topologyRepresentativeLimitEnv) {
|
|
configOut << "representative_limit = "
|
|
<< parseSizeEnvOrDefault(
|
|
"YAMS_BENCH_TOPOLOGY_ROUTE_REPRESENTATIVE_LIMIT", 0)
|
|
<< "\n";
|
|
}
|
|
if (topologyAnnCandidateLimitEnv && *topologyAnnCandidateLimitEnv) {
|
|
configOut << "ann_candidate_limit = "
|
|
<< parseSizeEnvOrDefault(
|
|
"YAMS_BENCH_TOPOLOGY_ROUTE_ANN_CANDIDATE_LIMIT", 64)
|
|
<< "\n";
|
|
}
|
|
if (envSet(topologyBqCandidateLimitEnv)) {
|
|
configOut << "bq_candidate_limit = "
|
|
<< parseSizeEnvOrDefault(
|
|
"YAMS_BENCH_TOPOLOGY_ROUTE_BQ_CANDIDATE_LIMIT", 0)
|
|
<< "\n";
|
|
}
|
|
if (envSet(graphCommunitySourceEnv)) {
|
|
configOut << "graph_community_source = \"" << *graphCommunitySourceEnv
|
|
<< "\"\n";
|
|
}
|
|
if (envSet(topologyBqPrefixDimEnv)) {
|
|
configOut << "bq_prefix_dim = "
|
|
<< parseSizeEnvOrDefault(
|
|
"YAMS_BENCH_TOPOLOGY_ROUTE_BQ_PREFIX_DIM", 0)
|
|
<< "\n";
|
|
}
|
|
if (envSet(topologyBqRotationEnv)) {
|
|
configOut << "bq_rotation = \"" << *topologyBqRotationEnv << "\"\n";
|
|
}
|
|
configOut << "adaptive_probe_score_gap = "
|
|
<< parseFloatEnvOrDefault(
|
|
"YAMS_BENCH_TOPOLOGY_ADAPTIVE_PROBE_SCORE_GAP", 0.0F)
|
|
<< "\n";
|
|
configOut << "narrow_min_boundary_margin = "
|
|
<< parseFloatEnvOrDefault(
|
|
"YAMS_BENCH_TOPOLOGY_NARROW_MIN_BOUNDARY_MARGIN", 0.0F)
|
|
<< "\n";
|
|
if (const char* scoring = std::getenv("YAMS_BENCH_TOPOLOGY_ROUTE_SCORING");
|
|
scoring && *scoring) {
|
|
configOut << "route_scoring = \"" << scoring << "\"\n";
|
|
}
|
|
if (std::getenv("YAMS_BENCH_TOPOLOGY_SPARSE_DENSE_ALPHA") != nullptr) {
|
|
configOut << "sparse_dense_alpha = "
|
|
<< parseFloatEnvOrDefault(
|
|
"YAMS_BENCH_TOPOLOGY_SPARSE_DENSE_ALPHA", 0.5F)
|
|
<< "\n";
|
|
}
|
|
if (std::getenv("YAMS_BENCH_TOPOLOGY_MIN_ROUTE_SCORE") != nullptr) {
|
|
configOut << "min_route_score = "
|
|
<< parseFloatEnvOrDefault(
|
|
"YAMS_BENCH_TOPOLOGY_MIN_ROUTE_SCORE", 0.0F)
|
|
<< "\n";
|
|
}
|
|
if (writeTopologyAdmissionConfig) {
|
|
const auto writeSizeSetting = [&](const char* envName,
|
|
const char* key) {
|
|
if (hasBenchmarkSetting(envName)) {
|
|
configOut << key << " = " << parseSizeEnvOrDefault(envName, 0)
|
|
<< "\n";
|
|
}
|
|
};
|
|
const auto writeFloatSetting = [&](const char* envName,
|
|
const char* key) {
|
|
if (hasBenchmarkSetting(envName)) {
|
|
configOut << key << " = "
|
|
<< parseFloatEnvOrDefault(envName, 0.0F) << "\n";
|
|
}
|
|
};
|
|
const auto writeStringSetting = [&](const char* envName,
|
|
const char* key) {
|
|
if (const auto value = yams::config::getenv_optional(envName);
|
|
value && !value->empty()) {
|
|
configOut << key << " = " << nlohmann::json(*value).dump()
|
|
<< "\n";
|
|
}
|
|
};
|
|
writeStringSetting("YAMS_BENCH_TOPOLOGY_ROUTE_CALIBRATION_FINGERPRINT",
|
|
"route_calibration_fingerprint");
|
|
writeStringSetting(
|
|
"YAMS_BENCH_TOPOLOGY_ROUTE_CALIBRATION_POLICY_FINGERPRINT",
|
|
"route_calibration_policy_fingerprint");
|
|
writeStringSetting(
|
|
"YAMS_BENCH_TOPOLOGY_ROUTE_CALIBRATION_DATASET_IDENTITY",
|
|
"route_calibration_dataset_identity");
|
|
writeSizeSetting("YAMS_BENCH_TOPOLOGY_ROUTE_CALIBRATION_QUERIES",
|
|
"route_calibration_queries");
|
|
writeSizeSetting(
|
|
"YAMS_BENCH_TOPOLOGY_ROUTE_CALIBRATION_PROTECTED_CANDIDATES",
|
|
"route_calibration_protected_candidates");
|
|
writeSizeSetting(
|
|
"YAMS_BENCH_TOPOLOGY_ROUTE_CALIBRATION_MISSED_PROTECTED_CANDIDATES",
|
|
"route_calibration_missed_protected_candidates");
|
|
writeSizeSetting("YAMS_BENCH_TOPOLOGY_ROUTE_MIN_CALIBRATION_QUERIES",
|
|
"route_min_calibration_queries");
|
|
writeSizeSetting("YAMS_BENCH_TOPOLOGY_ROUTE_MAX_MISSES_PER_THOUSAND",
|
|
"route_max_misses_per_thousand");
|
|
writeFloatSetting(
|
|
"YAMS_BENCH_TOPOLOGY_ROUTE_CALIBRATION_MIN_BOUNDARY_MARGIN",
|
|
"route_calibration_min_boundary_margin");
|
|
writeSizeSetting("YAMS_BENCH_TOPOLOGY_ROUTE_CALIBRATION_MIN_SEED_HITS",
|
|
"route_calibration_min_seed_hits");
|
|
writeSizeSetting("YAMS_BENCH_TOPOLOGY_ROUTE_WORK_MAX_ROWS_VISITED",
|
|
"route_work_max_rows_visited");
|
|
writeSizeSetting(
|
|
"YAMS_BENCH_TOPOLOGY_ROUTE_WORK_MAX_EXACT_DISTANCE_EVALUATIONS",
|
|
"route_work_max_exact_distance_evaluations");
|
|
writeSizeSetting("YAMS_BENCH_TOPOLOGY_ROUTE_WORK_MAX_ANN_CANDIDATES",
|
|
"route_work_max_ann_candidates");
|
|
}
|
|
if (const char* exp = std::getenv("YAMS_BENCH_TOPOLOGY_EXPANSION");
|
|
exp && *exp) {
|
|
configOut << "expansion_source = \"" << exp << "\"\n";
|
|
} else if (const char* exp =
|
|
std::getenv("YAMS_SEARCH_TOPOLOGY_EXPANSION_SOURCE");
|
|
exp && *exp) {
|
|
configOut << "expansion_source = \"" << exp << "\"\n";
|
|
}
|
|
if (std::getenv("YAMS_SEARCH_TOPOLOGY_GRAPH_NEIGHBOR_MIN_SCORE") !=
|
|
nullptr) {
|
|
configOut << "graph_neighbor_min_score = "
|
|
<< parseFloatEnvOrDefault(
|
|
"YAMS_SEARCH_TOPOLOGY_GRAPH_NEIGHBOR_MIN_SCORE", 0.25F)
|
|
<< "\n";
|
|
}
|
|
if (topologyGraphWeightedSeedRankingEnv &&
|
|
*topologyGraphWeightedSeedRankingEnv) {
|
|
configOut << "graph_weighted_seed_ranking = "
|
|
<< (envTruthy(topologyGraphWeightedSeedRankingEnv) ? "true"
|
|
: "false")
|
|
<< "\n";
|
|
}
|
|
}
|
|
// route_only is an evaluation arm: it serves uncertified routes, so the daemon
|
|
// only runs it behind the explicit gate. The bench opts in whenever the arm is
|
|
// selected; without this the policy downgrades to Shadow.
|
|
if (const auto policy =
|
|
yams::config::getenv_optional("YAMS_BENCH_TOPOLOGY_VECTOR_POLICY");
|
|
policy && !policy->empty()) {
|
|
std::string normalizedPolicy(*policy);
|
|
std::transform(normalizedPolicy.begin(), normalizedPolicy.end(),
|
|
normalizedPolicy.begin(), [](unsigned char c) {
|
|
return c == '-' ? '_'
|
|
: static_cast<char>(std::tolower(c));
|
|
});
|
|
if (normalizedPolicy == "route_only") {
|
|
configOut << "\n[search.evaluation]\n";
|
|
configOut << "allow_uncertified_routes = true\n";
|
|
}
|
|
}
|
|
if (writeTopologyEngineConfig) {
|
|
configOut << "\n[topology]\n";
|
|
if (topologyEngineEnv && *topologyEngineEnv) {
|
|
configOut << "engine = \"" << topologyEngineEnv << "\"\n";
|
|
}
|
|
if (topologyRepresentativesEnv && *topologyRepresentativesEnv) {
|
|
configOut << "routing_representatives = "
|
|
<< parseSizeEnvOrDefault(
|
|
"YAMS_BENCH_TOPOLOGY_ROUTING_REPRESENTATIVES", 1)
|
|
<< "\n";
|
|
}
|
|
if (envSet(topologyRepresentativeRuleEnv)) {
|
|
configOut << "representative_rule = \""
|
|
<< *topologyRepresentativeRuleEnv << "\"\n";
|
|
}
|
|
if (envSet(topologySgcHopsEnv)) {
|
|
configOut << "sgc_hops = "
|
|
<< parseSizeEnvOrDefault("YAMS_BENCH_TOPOLOGY_SGC_HOPS", 0)
|
|
<< "\n";
|
|
}
|
|
// Road network arms: roads per cluster and the walk order kind the build
|
|
// stores (Eval 3 sets the order per arm).
|
|
if (envSet(topologyRoadMaxPerClusterEnv)) {
|
|
configOut << "road_max_per_cluster = "
|
|
<< parseSizeEnvOrDefault(
|
|
"YAMS_BENCH_TOPOLOGY_ROAD_MAX_PER_CLUSTER", 0)
|
|
<< "\n";
|
|
}
|
|
if (envSet(topologyRoadOrderEnv)) {
|
|
configOut << "road_order = \"" << *topologyRoadOrderEnv << "\"\n";
|
|
}
|
|
if (topologyBoundarySpillEnv && *topologyBoundarySpillEnv) {
|
|
configOut << "boundary_spill = "
|
|
<< (envTruthy(topologyBoundarySpillEnv) ? "true" : "false")
|
|
<< "\n";
|
|
configOut << "boundary_spill_limit = "
|
|
<< parseSizeEnvOrDefault(
|
|
"YAMS_BENCH_TOPOLOGY_BOUNDARY_SPILL_LIMIT", 1)
|
|
<< "\n";
|
|
configOut
|
|
<< "boundary_spill_distance_ratio = "
|
|
<< parseFloatEnvOrDefault(
|
|
"YAMS_BENCH_TOPOLOGY_BOUNDARY_SPILL_DISTANCE_RATIO", 1.05F)
|
|
<< "\n";
|
|
configOut
|
|
<< "boundary_spill_residual_penalty = "
|
|
<< parseFloatEnvOrDefault(
|
|
"YAMS_BENCH_TOPOLOGY_BOUNDARY_SPILL_RESIDUAL_PENALTY", 1.0F)
|
|
<< "\n";
|
|
}
|
|
}
|
|
configOut.close();
|
|
harnessOptions.configPath = configPath;
|
|
spdlog::info("Using isolated benchmark config: {}", configPath.string());
|
|
}
|
|
}
|
|
} else {
|
|
spdlog::info("Using mock model provider (YAMS_DISABLE_VECTORS=1)");
|
|
}
|
|
|
|
harness = std::make_unique<DaemonHarness>(harnessOptions);
|
|
std::chrono::milliseconds daemonReadyTimeout{30000};
|
|
if (const char* env = std::getenv("YAMS_BENCH_DAEMON_READY_TIMEOUT_MS")) {
|
|
daemonReadyTimeout = std::chrono::milliseconds{
|
|
static_cast<std::chrono::milliseconds::rep>(std::stoll(env))};
|
|
}
|
|
if (!harness->start(daemonReadyTimeout))
|
|
throw std::runtime_error("Failed to start daemon");
|
|
|
|
ensureBenchmarkEmbeddingsReady(harness.get(), vectorsDisabled, forceMockEmbeddings,
|
|
embedBackendSimeon);
|
|
|
|
if (useBEIR) {
|
|
const BEIRDataset& dataset = preparedBeirDataset.value();
|
|
fs::path corpusDir = harness->rootDir() / "corpus";
|
|
beirCorpus = std::make_unique<BEIRCorpusLoader>(dataset, corpusDir);
|
|
beirCorpus->writeDocumentsAsFiles();
|
|
benchCorpusDir = corpusDir;
|
|
corpusSize = static_cast<int>(dataset.documents.size());
|
|
numQueries = static_cast<int>(dataset.queries.size());
|
|
expectedCacheMetadata.expectedDocs = corpusSize;
|
|
expectedCacheMetadata.expectedQueries = numQueries;
|
|
expectedCacheMetadata.corpusFingerprint = computePathFingerprint(benchCorpusDir);
|
|
} else {
|
|
benchCorpusDir = harness->rootDir() / "corpus";
|
|
corpus = std::make_unique<CorpusGenerator>(benchCorpusDir, syntheticCorpusMode);
|
|
corpus->generateDocuments(corpusSize);
|
|
corpusSize = static_cast<int>(corpus->createdFiles.size());
|
|
expectedCacheMetadata.expectedDocs = corpusSize;
|
|
expectedCacheMetadata.corpusFingerprint = computePathFingerprint(benchCorpusDir);
|
|
}
|
|
|
|
yams::daemon::ClientConfig clientCfg;
|
|
// Prefer proxy socket for long-running benchmark connections; it skips
|
|
// maxConnectionLifetime enforcement (main socket forces close at ~300s).
|
|
{
|
|
// Proxy socket is derived from daemon socket stem + ".proxy.sock".
|
|
fs::path daemonSock = harness->socketPath();
|
|
auto base = daemonSock.stem().string();
|
|
if (base.empty())
|
|
base = daemonSock.filename().string();
|
|
if (base.empty())
|
|
base = "yams-daemon";
|
|
fs::path proxySock = daemonSock.parent_path() / (base + ".proxy.sock");
|
|
if (fs::exists(proxySock)) {
|
|
clientCfg.socketPath = proxySock;
|
|
spdlog::info("Using proxy socket for benchmark client: {}", proxySock.string());
|
|
} else {
|
|
clientCfg.socketPath = harness->socketPath();
|
|
spdlog::info("Proxy socket not found; using main socket: {}",
|
|
clientCfg.socketPath.string());
|
|
}
|
|
}
|
|
clientCfg.connectTimeout = 5s;
|
|
const auto benchmarkRequestTimeout =
|
|
std::max(std::chrono::duration_cast<std::chrono::milliseconds>(300s),
|
|
configuredBenchQueryTimeout("hybrid"));
|
|
clientCfg.requestTimeout = benchmarkRequestTimeout;
|
|
clientCfg.autoStart = false;
|
|
|
|
auto isEofLikeError = [](const yams::Error& error) {
|
|
return error.message.find("[ipc:eof]") != std::string::npos ||
|
|
error.message.find("End of file") != std::string::npos ||
|
|
error.message.find("Connection closed") != std::string::npos;
|
|
};
|
|
|
|
auto connectClientWithProbe = [&](const char* reason) {
|
|
if (reason && *reason) {
|
|
spdlog::info("[Bench] Reconnecting benchmark client ({})", reason);
|
|
}
|
|
|
|
client = std::make_unique<yams::daemon::DaemonClient>(clientCfg);
|
|
// Override header/body timeouts to handle slow reranker queries.
|
|
// Default headerTimeout=30s is too short when cross-encoder reranking
|
|
// takes 20-60s before any response header is sent.
|
|
client->setHeaderTimeout(benchmarkRequestTimeout);
|
|
client->setBodyTimeout(benchmarkRequestTimeout);
|
|
auto connectResult = benchRunSync(client->connect(), 5s);
|
|
if (!connectResult) {
|
|
throw std::runtime_error("Failed to connect: " + connectResult.error().message);
|
|
}
|
|
|
|
// Probe the exact selected socket path before ingesting to catch startup races.
|
|
constexpr int kStatusProbeAttempts = 8;
|
|
for (int attempt = 1; attempt <= kStatusProbeAttempts; ++attempt) {
|
|
auto statusProbe = benchRunSync(client->status(true), 5s);
|
|
if (statusProbe) {
|
|
if (attempt > 1) {
|
|
spdlog::info("[Bench] Client status probe succeeded after {} attempts",
|
|
attempt);
|
|
}
|
|
return;
|
|
}
|
|
|
|
if (attempt == kStatusProbeAttempts) {
|
|
throw std::runtime_error("Client status probe failed: " +
|
|
statusProbe.error().message);
|
|
}
|
|
|
|
const bool eofLike = isEofLikeError(statusProbe.error());
|
|
const auto backoff = std::chrono::milliseconds(125 * attempt);
|
|
spdlog::warn("[Bench] Client status probe attempt {}/{} failed ({}{}), retrying in "
|
|
"{}ms",
|
|
attempt, kStatusProbeAttempts, statusProbe.error().message,
|
|
eofLike ? ", eof-like" : "", backoff.count());
|
|
|
|
if (eofLike) {
|
|
// Drop any potentially stale pooled transport before retrying.
|
|
client.reset();
|
|
yams::daemon::AsioConnectionPool::shutdown_all(std::chrono::milliseconds(250));
|
|
auto reconnectWait = std::chrono::milliseconds(80);
|
|
std::this_thread::sleep_for(reconnectWait);
|
|
client = std::make_unique<yams::daemon::DaemonClient>(clientCfg);
|
|
client->setHeaderTimeout(std::chrono::milliseconds(300000));
|
|
client->setBodyTimeout(std::chrono::milliseconds(300000));
|
|
auto reconnectResult = benchRunSync(client->connect(), 5s);
|
|
if (!reconnectResult) {
|
|
throw std::runtime_error("Failed to reconnect after probe EOF: " +
|
|
reconnectResult.error().message);
|
|
}
|
|
}
|
|
|
|
std::this_thread::sleep_for(backoff);
|
|
}
|
|
};
|
|
|
|
connectClientWithProbe("initial setup");
|
|
|
|
const bool usingExternalBenchDataDir = harnessOptions.dataDir.has_value();
|
|
const fs::path effectiveDataDir =
|
|
harnessOptions.dataDir.has_value() ? *harnessOptions.dataDir : harness->dataDir();
|
|
auto initialStatusResult = benchRunSync(client->status(true), 5s);
|
|
const auto getInitialCount = [&](const std::string& key) -> uint64_t {
|
|
if (!initialStatusResult) {
|
|
return 0;
|
|
}
|
|
auto it = initialStatusResult.value().requestCounts.find(key);
|
|
return (it == initialStatusResult.value().requestCounts.end()) ? 0ULL : it->second;
|
|
};
|
|
|
|
const bool warmCacheMetadataReusable =
|
|
warmCacheMetadata.has_value() &&
|
|
(warmCacheMetadata->status == "primed" || warmCacheMetadata->status == "partial");
|
|
|
|
const uint64_t initialIndexedDocCount = [&]() -> uint64_t {
|
|
if (warmCacheMetadataReusable) {
|
|
return static_cast<uint64_t>(warmCacheMetadata->expectedDocs);
|
|
}
|
|
if (!initialStatusResult) {
|
|
return 0;
|
|
}
|
|
auto itIndexed = initialStatusResult.value().requestCounts.find("documents_indexed");
|
|
if (itIndexed != initialStatusResult.value().requestCounts.end()) {
|
|
return itIndexed->second;
|
|
}
|
|
auto itTotal = initialStatusResult.value().requestCounts.find("documents_total");
|
|
return (itTotal == initialStatusResult.value().requestCounts.end()) ? 0ULL
|
|
: itTotal->second;
|
|
}();
|
|
|
|
const uint64_t initialDocsEmbedded =
|
|
warmCacheMetadataReusable ? static_cast<uint64_t>(warmCacheMetadata->embeddedDocs)
|
|
: getInitialCount("documents_embedded");
|
|
const uint64_t initialVectorCount =
|
|
warmCacheMetadataReusable ? static_cast<uint64_t>(warmCacheMetadata->vectorCount)
|
|
: getInitialCount("vector_count");
|
|
const uint64_t initialEmbedQueued =
|
|
warmCacheMetadataReusable ? 0 : getInitialCount("embed_svc_queued");
|
|
const uint64_t initialEmbedInFlight =
|
|
warmCacheMetadataReusable ? 0 : getInitialCount("embed_in_flight");
|
|
const PersistedHnswState initialPersistedHnsw = inspectPersistedHnswState(effectiveDataDir);
|
|
bool initialBackendIndexReusable = false;
|
|
if (auto* daemon = harness ? harness->daemon() : nullptr) {
|
|
if (auto* serviceManager = daemon->getServiceManager()) {
|
|
if (auto vectorDb = serviceManager->getVectorDatabase();
|
|
vectorDb && vectorDb->isInitialized()) {
|
|
const auto probeStart = std::chrono::steady_clock::now();
|
|
initialBackendIndexReusable = vectorDb->hasReusablePersistedSearchIndex();
|
|
setupTimings.vectorIndexReusableProbeMs +=
|
|
std::chrono::duration_cast<std::chrono::milliseconds>(
|
|
std::chrono::steady_clock::now() - probeStart)
|
|
.count();
|
|
++setupTimings.vectorIndexReusableProbeCount;
|
|
}
|
|
}
|
|
}
|
|
const bool cacheHasIndexedCorpus =
|
|
usingExternalBenchDataDir &&
|
|
initialIndexedDocCount >= static_cast<uint64_t>(corpusSize);
|
|
const bool cacheHasFullEmbeddings =
|
|
usingExternalBenchDataDir && !vectorsDisabled &&
|
|
initialDocsEmbedded >= static_cast<uint64_t>(corpusSize) && initialEmbedQueued == 0 &&
|
|
initialEmbedInFlight == 0;
|
|
const bool cacheHasReusableVectorIndex =
|
|
vectorsDisabled || (usingExternalBenchDataDir &&
|
|
yams::bench::benchmarkSearchIndexReusable(
|
|
benchmarkVectorSearchEngine(), initialBackendIndexReusable));
|
|
|
|
if (usingExternalBenchDataDir) {
|
|
spdlog::info("[Bench] External data dir status: indexed_docs={} embedded_docs={} "
|
|
"vector_count={} embed_queue={} embed_in_flight={} vector_index_ready={} "
|
|
"persisted_hnsw_nodes={} persisted_hnsw_meta={}{}",
|
|
initialIndexedDocCount, initialDocsEmbedded, initialVectorCount,
|
|
initialEmbedQueued, initialEmbedInFlight,
|
|
(cacheHasReusableVectorIndex ? "true" : "false"),
|
|
initialPersistedHnsw.nodeRows, initialPersistedHnsw.metaRows,
|
|
warmCacheMetadataReusable ? " (trusted metadata)" : "");
|
|
}
|
|
|
|
if (!(cacheHasFullEmbeddings && cacheHasReusableVectorIndex)) {
|
|
if (cacheHasIndexedCorpus) {
|
|
spdlog::info("[Bench] Reusing indexed corpus from external data dir; skipping "
|
|
"directory ingest");
|
|
}
|
|
|
|
if (!cacheHasIndexedCorpus) {
|
|
// Use directory ingestion for faster bulk add via IndexingService
|
|
fs::path corpusDir = useBEIR ? beirCorpus->corpusDir : corpus->corpusDir;
|
|
spdlog::info("Ingesting {} documents from {}...", corpusSize, corpusDir.string());
|
|
setupTimings.coldIngestPerformed = true;
|
|
coldIngestStartedAt = SetupClock::now();
|
|
|
|
yams::daemon::AddDocumentRequest addReq;
|
|
addReq.path = corpusDir.string();
|
|
addReq.recursive = true;
|
|
addReq.noEmbeddings = false;
|
|
addReq.includePatterns = {"*.txt"};
|
|
|
|
Result<yams::daemon::AddDocumentResponse> addResult =
|
|
Error{ErrorCode::Unknown, "ingest not attempted"};
|
|
constexpr int kIngestAttempts = 2;
|
|
for (int ingestAttempt = 1; ingestAttempt <= kIngestAttempts; ++ingestAttempt) {
|
|
if (!client) {
|
|
connectClientWithProbe("directory ingest attempt");
|
|
if (!client) {
|
|
throw std::runtime_error(
|
|
"Benchmark client unavailable before directory ingest attempt");
|
|
}
|
|
}
|
|
|
|
addResult = benchRunSync(client->streamingAddDocument(addReq), 120s);
|
|
if (addResult) {
|
|
break;
|
|
}
|
|
|
|
const bool eofLike = isEofLikeError(addResult.error());
|
|
if (!(eofLike && ingestAttempt < kIngestAttempts)) {
|
|
break;
|
|
}
|
|
|
|
spdlog::warn(
|
|
"[Bench] Directory ingest attempt {}/{} failed with eof-like transport "
|
|
"error ({}). Retrying once with a fresh client.",
|
|
ingestAttempt, kIngestAttempts, addResult.error().message);
|
|
client.reset();
|
|
yams::daemon::AsioConnectionPool::shutdown_all(std::chrono::milliseconds(250));
|
|
std::this_thread::sleep_for(std::chrono::milliseconds(120));
|
|
connectClientWithProbe("ingest retry after eof");
|
|
}
|
|
|
|
if (!addResult) {
|
|
throw std::runtime_error("Failed to ingest directory: " +
|
|
addResult.error().message);
|
|
}
|
|
admissionCompletedAt = SetupClock::now();
|
|
setupTimings.admissionMs = elapsedMsSince(*coldIngestStartedAt);
|
|
spdlog::info("Directory ingestion request accepted: {}", addResult.value().message);
|
|
|
|
// Wait for directory ingestion to complete by monitoring document count
|
|
// Use progress-based timeout: only timeout if no progress for N seconds
|
|
// This is more robust than a hard deadline because slow ingestion won't fail,
|
|
// but true hangs will still be detected.
|
|
auto progressTimeoutSec = std::chrono::seconds(
|
|
300); // 5 minutes without progress = stalled (embedding/model load)
|
|
if (const char* env = std::getenv("YAMS_BENCH_PROGRESS_TIMEOUT")) {
|
|
progressTimeoutSec = std::chrono::seconds(std::stoi(env));
|
|
}
|
|
double ingestMinFraction = 1.0;
|
|
if (const char* env = std::getenv("YAMS_BENCH_INGEST_MIN_FRACTION")) {
|
|
ingestMinFraction = std::clamp(std::stod(env), 0.0, 1.0);
|
|
}
|
|
const auto ingestTargetDocs = static_cast<uint64_t>(
|
|
std::ceil(static_cast<double>(corpusSize) * ingestMinFraction));
|
|
spdlog::info("Waiting for ingestion to complete (expecting {} documents, "
|
|
"progress timeout {}s)...",
|
|
corpusSize, progressTimeoutSec.count());
|
|
|
|
auto lastProgressTime = std::chrono::steady_clock::now();
|
|
auto ingestHeartbeatAt = lastProgressTime + std::chrono::seconds(5);
|
|
uint64_t lastDocCount = 0;
|
|
uint64_t lastIndexedDocCount = 0;
|
|
uint64_t lastContentExtracted = 0;
|
|
uint64_t lastPostProcessed = 0;
|
|
uint64_t lastEmbedQueued = 0;
|
|
uint64_t lastEmbedInFlight = 0;
|
|
uint64_t lastVectorCount = 0;
|
|
uint32_t lastDepth = 0;
|
|
int stableChecks = 0;
|
|
|
|
// Helper to get metric from requestCounts with presence flag
|
|
auto getMetric = [](const auto& counts,
|
|
const std::string& key) -> std::pair<uint64_t, bool> {
|
|
auto it = counts.find(key);
|
|
if (it == counts.end())
|
|
return {0, false};
|
|
return {it->second, true};
|
|
};
|
|
|
|
uint64_t lastKgInFlight = 0, lastSymbolInFlight = 0, lastEntityInFlight = 0;
|
|
uint64_t lastExtractionInFlight = 0;
|
|
|
|
auto ingestionLooksCompleteFromLastObserved = [&]() -> bool {
|
|
const bool targetReached = (lastIndexedDocCount >= ingestTargetDocs) ||
|
|
(lastDocCount >= ingestTargetDocs);
|
|
const bool allQueuesDrained =
|
|
(lastDepth == 0 && lastExtractionInFlight == 0 && lastKgInFlight == 0 &&
|
|
lastSymbolInFlight == 0 && lastEntityInFlight == 0 &&
|
|
lastEmbedQueued == 0 && lastEmbedInFlight == 0);
|
|
const bool extractionOrPostDone = (lastContentExtracted >= ingestTargetDocs) ||
|
|
(lastPostProcessed >= ingestTargetDocs);
|
|
return targetReached && allQueuesDrained && extractionOrPostDone;
|
|
};
|
|
|
|
while (true) {
|
|
auto now = std::chrono::steady_clock::now();
|
|
auto timeSinceProgress =
|
|
std::chrono::duration_cast<std::chrono::seconds>(now - lastProgressTime);
|
|
|
|
if (now >= ingestHeartbeatAt) {
|
|
ingestHeartbeatAt = now + std::chrono::seconds(5);
|
|
spdlog::info("Ingest wait heartbeat: since_progress={}s last_total={} "
|
|
"last_indexed={} last_queue={} "
|
|
"last_processed={}",
|
|
timeSinceProgress.count(), lastDocCount, lastIndexedDocCount,
|
|
lastDepth, lastPostProcessed);
|
|
}
|
|
|
|
// Check for stall (no progress for progressTimeoutSec)
|
|
if (timeSinceProgress > progressTimeoutSec) {
|
|
if (ingestionLooksCompleteFromLastObserved()) {
|
|
spdlog::warn(
|
|
"Ingestion progress timeout reached, but last observed metrics "
|
|
"already indicate completion; accepting completion.");
|
|
break;
|
|
}
|
|
|
|
// Status polls may intermittently fail even when work is done; perform one
|
|
// final verification poll before classifying as stalled.
|
|
auto finalVerify = benchRunSync(client->status(true), 10s);
|
|
if (finalVerify) {
|
|
const auto& counts = finalVerify.value().requestCounts;
|
|
auto get = [&](const std::string& key) -> uint64_t {
|
|
auto it = counts.find(key);
|
|
return (it == counts.end()) ? 0ULL : it->second;
|
|
};
|
|
|
|
const uint64_t docsTotal = get("documents_total");
|
|
const uint64_t docsIndexed = get("documents_indexed");
|
|
const uint64_t extracted = get("documents_content_extracted");
|
|
const uint64_t postQueued = get("post_ingest_queued");
|
|
const uint64_t postInFlight = get("post_ingest_inflight");
|
|
const uint64_t postProcessed = get("post_ingest_processed");
|
|
const uint64_t extractionInFlight = get("extraction_inflight");
|
|
const uint64_t kgInFlight = get("kg_inflight");
|
|
const uint64_t symbolInFlight = get("symbol_inflight");
|
|
const uint64_t entityInFlight = get("entity_inflight");
|
|
const uint64_t embedQueued = get("embed_svc_queued");
|
|
const uint64_t embedInFlight = get("embed_in_flight");
|
|
|
|
const bool targetReached = (docsIndexed >= ingestTargetDocs) ||
|
|
(docsTotal >= ingestTargetDocs);
|
|
const bool allQueuesDrained =
|
|
(postQueued == 0 && postInFlight == 0 && extractionInFlight == 0 &&
|
|
kgInFlight == 0 && symbolInFlight == 0 && entityInFlight == 0 &&
|
|
embedQueued == 0 && embedInFlight == 0);
|
|
const bool extractionOrPostDone = (extracted >= ingestTargetDocs) ||
|
|
(postProcessed >= ingestTargetDocs);
|
|
|
|
if (targetReached && allQueuesDrained && extractionOrPostDone) {
|
|
spdlog::warn(
|
|
"Ingestion progress timeout reached but final verification "
|
|
"shows completion (docs_total={}, docs_indexed={}, "
|
|
"extracted={}, post_processed={}); accepting completion.",
|
|
docsTotal, docsIndexed, extracted, postProcessed);
|
|
break;
|
|
}
|
|
}
|
|
|
|
spdlog::error(
|
|
"Ingestion stalled - no progress for {}s (docs: total={} indexed={} "
|
|
"target={} "
|
|
"queue={}, inflight: extract={} kg={} symbol={} entity={} extracted={} "
|
|
"processed={} embed_queue={} embed_in_flight={} vectors={})",
|
|
timeSinceProgress.count(), lastDocCount, lastIndexedDocCount,
|
|
corpusSize, lastDepth, lastExtractionInFlight, lastKgInFlight,
|
|
lastSymbolInFlight, lastEntityInFlight, lastContentExtracted,
|
|
lastPostProcessed, lastEmbedQueued, lastEmbedInFlight, lastVectorCount);
|
|
throw std::runtime_error(
|
|
"Ingestion stalled - benchmark results would be invalid. "
|
|
"Set YAMS_BENCH_PROGRESS_TIMEOUT=300 for slower systems. "
|
|
"For faster ingestion: YAMS_POST_EMBED_CONCURRENT=12 "
|
|
"YAMS_POST_EXTRACTION_CONCURRENT=12 YAMS_DB_POOL_MAX=48");
|
|
}
|
|
|
|
auto statusResult = benchRunSync(client->status(true), 5s);
|
|
if (statusResult) {
|
|
const auto& counts = statusResult.value().requestCounts;
|
|
uint32_t depth = statusResult.value().postIngestQueueDepth;
|
|
auto [docCount, docCountPresent] = getMetric(counts, "documents_total");
|
|
auto [indexedCount, indexedPresent] =
|
|
getMetric(counts, "documents_indexed");
|
|
auto [contentExtracted, extractedPresent] =
|
|
getMetric(counts, "documents_content_extracted");
|
|
auto [postQueued, postQueuedPresent] =
|
|
getMetric(counts, "post_ingest_queued");
|
|
auto [postInflight, postInflightPresent] =
|
|
getMetric(counts, "post_ingest_inflight");
|
|
auto [postProcessed, postProcessedPresent] =
|
|
getMetric(counts, "post_ingest_processed");
|
|
auto [embedQueued, embedQueuedPresent] =
|
|
getMetric(counts, "embed_svc_queued");
|
|
auto [embedInFlight, embedInFlightPresent] =
|
|
getMetric(counts, "embed_in_flight");
|
|
auto [vectorCount, vectorPresent] = getMetric(counts, "vector_count");
|
|
uint64_t queuedTotal = std::max<uint64_t>(depth, postQueued);
|
|
|
|
// Check all processing stage in-flight counters
|
|
auto [extractionInFlight, extractionPresent] =
|
|
getMetric(counts, "extraction_inflight");
|
|
auto [kgInFlight, kgPresent] = getMetric(counts, "kg_inflight");
|
|
auto [symbolInFlight, symbolPresent] = getMetric(counts, "symbol_inflight");
|
|
auto [entityInFlight, entityPresent] = getMetric(counts, "entity_inflight");
|
|
|
|
// Total in-flight across all stages (matches
|
|
// PostIngestQueue::totalInFlight())
|
|
uint64_t totalInFlight =
|
|
extractionInFlight + kgInFlight + symbolInFlight + entityInFlight;
|
|
|
|
bool statusChanged =
|
|
(queuedTotal != lastDepth || docCount != lastDocCount ||
|
|
indexedCount != lastIndexedDocCount ||
|
|
contentExtracted != lastContentExtracted ||
|
|
postProcessed != lastPostProcessed || kgInFlight != lastKgInFlight ||
|
|
symbolInFlight != lastSymbolInFlight ||
|
|
entityInFlight != lastEntityInFlight ||
|
|
extractionInFlight != lastExtractionInFlight ||
|
|
embedQueued != lastEmbedQueued || embedInFlight != lastEmbedInFlight ||
|
|
vectorCount != lastVectorCount);
|
|
|
|
if (statusChanged) {
|
|
bool vectorDbReady = statusResult.value().vectorDbReady;
|
|
if (auto it = statusResult.value().readinessStates.find("vector_db");
|
|
it != statusResult.value().readinessStates.end()) {
|
|
vectorDbReady = it->second;
|
|
}
|
|
spdlog::debug(
|
|
"Documents: total={} indexed={} / {} | queue={} inflight={} | "
|
|
"extracted={} "
|
|
"processed={} | extract={} kg={} symbol={} entity={} (total={}) | "
|
|
"embed_queue={} embed_in_flight={} vectors={} (ready={})",
|
|
docCount, indexedCount, corpusSize, queuedTotal, postInflight,
|
|
contentExtracted, postProcessed, extractionInFlight, kgInFlight,
|
|
symbolInFlight, entityInFlight, totalInFlight, embedQueued,
|
|
embedInFlight, vectorCount, (vectorDbReady ? "true" : "false"));
|
|
lastDepth = static_cast<uint32_t>(queuedTotal);
|
|
lastDocCount = docCount;
|
|
lastIndexedDocCount = indexedCount;
|
|
lastContentExtracted = contentExtracted;
|
|
lastPostProcessed = postProcessed;
|
|
lastEmbedQueued = embedQueued;
|
|
lastEmbedInFlight = embedInFlight;
|
|
lastVectorCount = vectorCount;
|
|
lastKgInFlight = kgInFlight;
|
|
lastSymbolInFlight = symbolInFlight;
|
|
lastEntityInFlight = entityInFlight;
|
|
lastExtractionInFlight = extractionInFlight;
|
|
stableChecks = 0;
|
|
lastProgressTime = now; // Reset progress timer on any change
|
|
} else {
|
|
stableChecks++;
|
|
}
|
|
|
|
// Complete when ALL stages are idle:
|
|
// - post_ingest_queued == 0 AND post_ingest_inflight == 0 (queue empty)
|
|
// - totalInFlight == 0 (all stages: extraction, KG, symbol, entity)
|
|
// - indexedCount >= corpusSize (or stable for 5+ seconds after reaching
|
|
// target)
|
|
bool allStagesDrained =
|
|
(queuedTotal == 0 && postInflight == 0 && totalInFlight == 0);
|
|
bool extractedReady =
|
|
extractedPresent
|
|
? (contentExtracted >= static_cast<uint64_t>(corpusSize))
|
|
: true;
|
|
bool processedReady =
|
|
postProcessedPresent
|
|
? (postProcessed >= static_cast<uint64_t>(corpusSize))
|
|
: true;
|
|
// `documents_indexed` is a search/FTS visibility counter and can lag or
|
|
// remain stale under sanitizer/slow asynchronous rebuild runs even after
|
|
// the directory ingest and post-ingest pipeline have accepted and processed
|
|
// the whole corpus. For benchmark setup, do not block forever on that
|
|
// secondary visibility counter once the canonical document total reached
|
|
// the requested corpus and extraction/post-processing has completed.
|
|
const bool corpusStored = docCount >= static_cast<uint64_t>(corpusSize);
|
|
bool indexedReady =
|
|
indexedPresent ? (indexedCount >= static_cast<uint64_t>(corpusSize) ||
|
|
(corpusStored && (extractedReady || processedReady)))
|
|
: corpusStored;
|
|
bool stableAfterDrain = (stableChecks >= 6);
|
|
if (allStagesDrained && indexedReady &&
|
|
(extractedReady || processedReady || stableAfterDrain)) {
|
|
spdlog::info(
|
|
"Ingestion complete: total={} indexed={} (target={}), all stages "
|
|
"drained (extracted={}, processed={})",
|
|
docCount, indexedCount, corpusSize, contentExtracted,
|
|
postProcessed);
|
|
break;
|
|
}
|
|
} else {
|
|
if (now >= ingestHeartbeatAt - std::chrono::seconds(5)) {
|
|
spdlog::warn("Status polling failed during ingest wait: {}",
|
|
statusResult.error().message);
|
|
}
|
|
}
|
|
std::this_thread::sleep_for(500ms);
|
|
}
|
|
|
|
setupTimings.storageReadyMs = elapsedMsSince(*coldIngestStartedAt);
|
|
setupTimings.pipelineDrainMs =
|
|
admissionCompletedAt.has_value() ? elapsedMsSince(*admissionCompletedAt) : 0;
|
|
|
|
} else if (!vectorsDisabled) {
|
|
yams::daemon::RepairRequest repairReq;
|
|
repairReq.repairEmbeddings = true;
|
|
repairReq.embeddingModel = "embeddinggemma-300m";
|
|
repairReq.foreground = true;
|
|
repairReq.maxRetries = 3;
|
|
spdlog::info("[Bench] Requesting embedding repair to resume warm-cache priming...");
|
|
auto repairResult = benchRunSync(client->callRepair(repairReq), 300s);
|
|
if (!repairResult) {
|
|
spdlog::warn("[Bench] Embedding repair request failed: {}",
|
|
repairResult.error().message);
|
|
} else {
|
|
spdlog::info("[Bench] Embedding repair queued: success={} ops={} succeeded={} "
|
|
"failed={} skipped={}",
|
|
repairResult.value().success, repairResult.value().totalOperations,
|
|
repairResult.value().totalSucceeded,
|
|
repairResult.value().totalFailed,
|
|
repairResult.value().totalSkipped);
|
|
}
|
|
}
|
|
|
|
// Directory ingest carries no per-file metadata; copy the corpus metadata
|
|
// (event_time, session_id, round_index, ...) onto the stored documents so the
|
|
// topology extractor sees it. Idempotent, so warm caches repeat it.
|
|
if (useBEIR && beirCorpus) {
|
|
auto* daemon = harness ? harness->daemon() : nullptr;
|
|
auto* serviceManager = daemon ? daemon->getServiceManager() : nullptr;
|
|
auto metadataRepo = serviceManager ? serviceManager->getMetadataRepo() : nullptr;
|
|
if (metadataRepo) {
|
|
std::size_t missing = 0;
|
|
const auto applied = yams::bench::applyBenchmarkDocumentMetadata(
|
|
*metadataRepo, *beirCorpus, &missing);
|
|
if (applied > 0 || missing > 0) {
|
|
spdlog::info("[Bench] Applied corpus metadata to {} documents ({} not "
|
|
"found)",
|
|
applied, missing);
|
|
}
|
|
}
|
|
}
|
|
|
|
// Wait for embedding provider to become available
|
|
spdlog::info("Waiting for embedding provider to become available...");
|
|
auto deadline = std::chrono::steady_clock::now() + 30s;
|
|
bool embeddingReady = false;
|
|
while (std::chrono::steady_clock::now() < deadline) {
|
|
auto statusResult = benchRunSync(client->status(true), 5s);
|
|
if (statusResult && statusResult.value().embeddingAvailable) {
|
|
spdlog::info("Embedding provider is available (backend: {}, model: {})",
|
|
statusResult.value().embeddingBackend,
|
|
statusResult.value().embeddingModel);
|
|
embeddingReady = true;
|
|
break;
|
|
}
|
|
std::this_thread::sleep_for(500ms);
|
|
}
|
|
if (!embeddingReady) {
|
|
spdlog::warn("Embedding provider did not become available within 30s");
|
|
}
|
|
|
|
// Wait for embeddings to be generated by checking document-level embedding progress
|
|
// (documents_embedded) and queue drain. We still track vector_count for row-level
|
|
// visibility, but vector_count is not a document counter.
|
|
bool vectorsEnabled = !vectorsDisabled;
|
|
if (vectorsEnabled && !embeddingReady) {
|
|
vectorsEnabled = false;
|
|
spdlog::warn("Disabling vector wait for this benchmark run: embedding provider is "
|
|
"unavailable; proceeding with keyword-dominant evaluation");
|
|
if (summaryLog) {
|
|
summaryLog << "Embedding provider unavailable after 30s; skipping vector wait"
|
|
<< std::endl;
|
|
summaryLog.flush();
|
|
}
|
|
}
|
|
|
|
if (vectorsEnabled) {
|
|
spdlog::info("Waiting for embeddings to be generated (target: {} docs)...",
|
|
corpusSize);
|
|
|
|
auto embedHeartbeatAt = std::chrono::steady_clock::now() + std::chrono::seconds(5);
|
|
|
|
// Log to summary file
|
|
if (summaryLog) {
|
|
summaryLog << "=== Embedding Generation ===" << std::endl;
|
|
summaryLog << "Target: " << corpusSize << " documents" << std::endl;
|
|
}
|
|
|
|
auto embedProgressTimeoutSec = std::chrono::seconds(600);
|
|
if (const char* env = std::getenv("YAMS_BENCH_EMBED_PROGRESS_TIMEOUT")) {
|
|
embedProgressTimeoutSec = std::chrono::seconds(std::stoi(env));
|
|
}
|
|
auto embedMaxWaitSec = std::chrono::seconds(7200); // 2h hard cap (configurable)
|
|
if (const char* env = std::getenv("YAMS_BENCH_EMBED_MAX_WAIT")) {
|
|
embedMaxWaitSec = std::chrono::seconds(std::stoi(env));
|
|
}
|
|
struct EmbedWaitSnapshot {
|
|
uint64_t vectorCount = 0;
|
|
uint64_t docsEmbedded = 0;
|
|
uint64_t preparedDocs = 0;
|
|
uint64_t preparedChunks = 0;
|
|
uint64_t queueDepth = 0;
|
|
uint64_t inFlight = 0;
|
|
uint64_t dropped = 0;
|
|
uint64_t inferActive = 0;
|
|
uint64_t inferOldestMs = 0;
|
|
uint64_t inferStarted = 0;
|
|
uint64_t inferCompleted = 0;
|
|
uint64_t inferWarnCount = 0;
|
|
bool hasQueueMetrics = false;
|
|
bool vectorDbReady = false;
|
|
|
|
bool queueDrained() const { return queueDepth == 0 && inFlight == 0; }
|
|
|
|
bool inferLikelyActive(std::chrono::seconds timeout) const {
|
|
return inferActive > 0 &&
|
|
inferOldestMs < static_cast<uint64_t>(timeout.count()) * 1000ull;
|
|
}
|
|
|
|
double coverageFor(int totalDocs) const {
|
|
return totalDocs > 0 ? (docsEmbedded * 100.0 / totalDocs) : 0.0;
|
|
}
|
|
|
|
double avgChunksPerDoc() const {
|
|
return preparedDocs > 0 ? static_cast<double>(preparedChunks) /
|
|
static_cast<double>(preparedDocs)
|
|
: 0.0;
|
|
}
|
|
};
|
|
|
|
auto progressSignalChanged = [](const EmbedWaitSnapshot& lhs,
|
|
const EmbedWaitSnapshot& rhs) {
|
|
return lhs.vectorCount != rhs.vectorCount ||
|
|
lhs.docsEmbedded != rhs.docsEmbedded ||
|
|
lhs.preparedDocs != rhs.preparedDocs ||
|
|
lhs.preparedChunks != rhs.preparedChunks ||
|
|
lhs.queueDepth != rhs.queueDepth || lhs.inFlight != rhs.inFlight ||
|
|
lhs.dropped != rhs.dropped || lhs.inferActive != rhs.inferActive ||
|
|
lhs.inferStarted != rhs.inferStarted ||
|
|
lhs.inferCompleted != rhs.inferCompleted ||
|
|
lhs.inferWarnCount != rhs.inferWarnCount;
|
|
};
|
|
|
|
auto getRequestCount = [](const auto& status, std::string_view key) {
|
|
auto it = status.requestCounts.find(std::string(key));
|
|
return (it == status.requestCounts.end()) ? 0ULL : it->second;
|
|
};
|
|
|
|
auto collectEmbedSnapshot = [&](const auto& status) {
|
|
EmbedWaitSnapshot snapshot;
|
|
snapshot.vectorDbReady = status.vectorDbReady;
|
|
if (auto it = status.readinessStates.find("vector_db");
|
|
it != status.readinessStates.end()) {
|
|
snapshot.vectorDbReady = it->second;
|
|
}
|
|
|
|
snapshot.vectorCount = getRequestCount(status, "vector_count");
|
|
snapshot.docsEmbedded = getRequestCount(status, "documents_embedded");
|
|
snapshot.preparedDocs =
|
|
getRequestCount(status, "bus_embed_prepared_docs_queued");
|
|
snapshot.preparedChunks =
|
|
getRequestCount(status, "bus_embed_prepared_chunks_queued");
|
|
snapshot.queueDepth = getRequestCount(status, "embed_svc_queued");
|
|
snapshot.inFlight = getRequestCount(status, "embed_in_flight");
|
|
snapshot.dropped = getRequestCount(status, "bus_embed_dropped");
|
|
snapshot.inferActive = getRequestCount(status, "embed_infer_active");
|
|
snapshot.inferOldestMs = getRequestCount(status, "embed_infer_oldest_ms");
|
|
snapshot.inferStarted = getRequestCount(status, "embed_infer_started");
|
|
snapshot.inferCompleted = getRequestCount(status, "embed_infer_completed");
|
|
snapshot.inferWarnCount = getRequestCount(status, "embed_infer_warn_count");
|
|
snapshot.hasQueueMetrics = status.requestCounts.contains("embed_svc_queued") ||
|
|
status.requestCounts.contains("embed_in_flight");
|
|
return snapshot;
|
|
};
|
|
|
|
EmbedWaitSnapshot observedSnapshot;
|
|
EmbedWaitSnapshot lastProgressSnapshot;
|
|
bool haveObservedSnapshot = false;
|
|
bool haveProgressSnapshot = false;
|
|
int stableCount = 0;
|
|
bool embeddingDrainSatisfied = false;
|
|
auto embedStartTime = std::chrono::steady_clock::now();
|
|
auto lastEmbedProgressTime = embedStartTime;
|
|
|
|
// Phase 1: Wait for embedding queue to drain and embedded documents to reach
|
|
// target. Uses progress-based stall detection (like ingestion wait) to avoid
|
|
// failing while work is still actively progressing on large corpora/slow
|
|
// accelerators.
|
|
while (true) {
|
|
auto now = std::chrono::steady_clock::now();
|
|
auto elapsed =
|
|
std::chrono::duration_cast<std::chrono::seconds>(now - embedStartTime);
|
|
auto sinceProgress = std::chrono::duration_cast<std::chrono::seconds>(
|
|
now - lastEmbedProgressTime);
|
|
|
|
if (embedMaxWaitSec.count() > 0 && elapsed > embedMaxWaitSec) {
|
|
throw std::runtime_error(
|
|
"Embedding wait exceeded hard limit (" +
|
|
std::to_string(embedMaxWaitSec.count()) +
|
|
"s) before benchmark queries (embed_svc_queued=" +
|
|
std::to_string(observedSnapshot.queueDepth) +
|
|
", embed_in_flight=" + std::to_string(observedSnapshot.inFlight) +
|
|
", docs_embedded=" + std::to_string(observedSnapshot.docsEmbedded) +
|
|
", vectors=" + std::to_string(observedSnapshot.vectorCount) +
|
|
", dropped=" + std::to_string(observedSnapshot.dropped) +
|
|
"). Increase YAMS_BENCH_EMBED_MAX_WAIT or tune embedding concurrency.");
|
|
}
|
|
|
|
const bool inferLikelyActive =
|
|
observedSnapshot.inferLikelyActive(embedProgressTimeoutSec);
|
|
|
|
if (sinceProgress > embedProgressTimeoutSec && !inferLikelyActive) {
|
|
throw std::runtime_error(
|
|
"Embedding wait stalled for " +
|
|
std::to_string(embedProgressTimeoutSec.count()) +
|
|
"s before benchmark queries (embed_svc_queued=" +
|
|
std::to_string(observedSnapshot.queueDepth) +
|
|
", embed_in_flight=" + std::to_string(observedSnapshot.inFlight) +
|
|
", docs_embedded=" + std::to_string(observedSnapshot.docsEmbedded) +
|
|
", vectors=" + std::to_string(observedSnapshot.vectorCount) +
|
|
", dropped=" + std::to_string(observedSnapshot.dropped) +
|
|
"). Increase timeout/concurrency or investigate model/provider "
|
|
"stalls.");
|
|
}
|
|
|
|
if (now >= embedHeartbeatAt) {
|
|
embedHeartbeatAt = now + std::chrono::seconds(5);
|
|
spdlog::info("Embed wait heartbeat: elapsed={}s since_progress={}s "
|
|
"last_docs_embedded={} last_vectors={} queue={} in_flight={} "
|
|
"infer_active={} infer_oldest_ms={} infer_started={} "
|
|
"infer_completed={}",
|
|
elapsed.count(), sinceProgress.count(),
|
|
observedSnapshot.docsEmbedded, observedSnapshot.vectorCount,
|
|
observedSnapshot.queueDepth, observedSnapshot.inFlight,
|
|
observedSnapshot.inferActive, observedSnapshot.inferOldestMs,
|
|
observedSnapshot.inferStarted,
|
|
observedSnapshot.inferCompleted);
|
|
}
|
|
auto statusResult = benchRunSync(client->status(true), 5s);
|
|
if (statusResult) {
|
|
const auto snapshot = collectEmbedSnapshot(statusResult.value());
|
|
const bool metricsChanged =
|
|
!haveObservedSnapshot ||
|
|
progressSignalChanged(snapshot, observedSnapshot);
|
|
observedSnapshot = snapshot;
|
|
haveObservedSnapshot = true;
|
|
|
|
if (metricsChanged) {
|
|
lastEmbedProgressTime = now;
|
|
stableCount = 0;
|
|
} else {
|
|
stableCount++;
|
|
}
|
|
|
|
if (!haveProgressSnapshot ||
|
|
observedSnapshot.vectorCount != lastProgressSnapshot.vectorCount ||
|
|
observedSnapshot.docsEmbedded != lastProgressSnapshot.docsEmbedded) {
|
|
const double docCoverage = observedSnapshot.coverageFor(corpusSize);
|
|
const double preparedAvgChunksPerDoc =
|
|
observedSnapshot.avgChunksPerDoc();
|
|
spdlog::info(
|
|
"Embedding progress: docs={} / {} ({:.1f}%) vectors={} "
|
|
"prepared_docs={} prepared_chunks={} avg_chunks_per_doc={:.2f} "
|
|
"| queue={} in_flight={} dropped={}",
|
|
observedSnapshot.docsEmbedded, corpusSize, docCoverage,
|
|
observedSnapshot.vectorCount, observedSnapshot.preparedDocs,
|
|
observedSnapshot.preparedChunks, preparedAvgChunksPerDoc,
|
|
observedSnapshot.queueDepth, observedSnapshot.inFlight,
|
|
observedSnapshot.dropped);
|
|
|
|
// Log progress to summary file
|
|
if (summaryLog) {
|
|
auto elapsed =
|
|
std::chrono::duration_cast<std::chrono::milliseconds>(
|
|
std::chrono::steady_clock::now() - embedStartTime)
|
|
.count();
|
|
summaryLog << "[" << elapsed
|
|
<< "ms] DocsEmbedded: " << observedSnapshot.docsEmbedded
|
|
<< " / " << corpusSize << " (" << std::fixed
|
|
<< std::setprecision(1) << docCoverage << "%)"
|
|
<< " | vectors=" << observedSnapshot.vectorCount
|
|
<< " | prepared_docs=" << observedSnapshot.preparedDocs
|
|
<< " prepared_chunks=" << observedSnapshot.preparedChunks
|
|
<< " avg_chunks_per_doc=" << std::fixed
|
|
<< std::setprecision(2) << preparedAvgChunksPerDoc
|
|
<< " | queue=" << observedSnapshot.queueDepth
|
|
<< " in_flight=" << observedSnapshot.inFlight
|
|
<< " dropped=" << observedSnapshot.dropped << std::endl;
|
|
summaryLog.flush();
|
|
}
|
|
|
|
lastProgressSnapshot = observedSnapshot;
|
|
haveProgressSnapshot = true;
|
|
}
|
|
|
|
// Success condition: embedded docs >= corpusSize AND queue drained.
|
|
// vector_count tracks rows (chunks + doc records), not documents.
|
|
if (observedSnapshot.hasQueueMetrics &&
|
|
observedSnapshot.docsEmbedded >= static_cast<uint64_t>(corpusSize) &&
|
|
observedSnapshot.queueDrained()) {
|
|
auto elapsed = std::chrono::duration_cast<std::chrono::milliseconds>(
|
|
std::chrono::steady_clock::now() - embedStartTime)
|
|
.count();
|
|
spdlog::info(
|
|
"Full embedding coverage achieved: {} embedded docs for {} "
|
|
"target docs (queue drained, vector_db_ready={})",
|
|
observedSnapshot.docsEmbedded, corpusSize,
|
|
(observedSnapshot.vectorDbReady ? "true" : "false"));
|
|
if (summaryLog) {
|
|
summaryLog << "\n*** SUCCESS: Full coverage in " << elapsed
|
|
<< "ms ***" << std::endl;
|
|
summaryLog
|
|
<< "Final embedded docs: " << observedSnapshot.docsEmbedded
|
|
<< std::endl;
|
|
summaryLog << "Final vectors: " << observedSnapshot.vectorCount
|
|
<< std::endl;
|
|
summaryLog << "Total dropped: " << observedSnapshot.dropped
|
|
<< std::endl;
|
|
summaryLog.flush();
|
|
}
|
|
embeddingDrainSatisfied = true;
|
|
break;
|
|
}
|
|
|
|
// If very stable (20s) with queue drained, we're done even if < corpusSize
|
|
// This handles cases where some docs have no content to embed
|
|
// IMPORTANT: Require minimum 90% coverage to prevent premature exit from
|
|
// transient queue drain (e.g., between embedding batches)
|
|
const double coverage = observedSnapshot.coverageFor(corpusSize);
|
|
constexpr double MIN_COVERAGE_THRESHOLD = 90.0;
|
|
if (observedSnapshot.hasQueueMetrics && stableCount >= 40 &&
|
|
observedSnapshot.queueDrained() && observedSnapshot.docsEmbedded > 0 &&
|
|
coverage >= MIN_COVERAGE_THRESHOLD) {
|
|
auto elapsed = std::chrono::duration_cast<std::chrono::milliseconds>(
|
|
std::chrono::steady_clock::now() - embedStartTime)
|
|
.count();
|
|
spdlog::info(
|
|
"Embedding generation complete (queue drained, stable): {} "
|
|
"embedded docs ({:.1f}% coverage, vector_db_ready={})",
|
|
observedSnapshot.docsEmbedded, coverage,
|
|
(observedSnapshot.vectorDbReady ? "true" : "false"));
|
|
if (summaryLog) {
|
|
summaryLog << "\n*** Complete (queue drained) in " << elapsed
|
|
<< "ms ***" << std::endl;
|
|
summaryLog
|
|
<< "Final embedded docs: " << observedSnapshot.docsEmbedded
|
|
<< " (" << coverage << "%)" << std::endl;
|
|
summaryLog << "Final vectors: " << observedSnapshot.vectorCount
|
|
<< std::endl;
|
|
summaryLog.flush();
|
|
}
|
|
embeddingDrainSatisfied = true;
|
|
break;
|
|
}
|
|
|
|
if (observedSnapshot.hasQueueMetrics && stableCount >= 40 &&
|
|
observedSnapshot.queueDrained() && observedSnapshot.docsEmbedded == 0) {
|
|
throw std::runtime_error(
|
|
"Embedding failed before benchmark queries (queue "
|
|
"drained, no documents "
|
|
"embedded, dropped=" +
|
|
std::to_string(observedSnapshot.dropped) +
|
|
"). Check embedding provider/model availability.");
|
|
}
|
|
|
|
if (observedSnapshot.hasQueueMetrics && observedSnapshot.queueDrained() &&
|
|
observedSnapshot.docsEmbedded == 0 &&
|
|
elapsed > std::chrono::seconds(60)) {
|
|
throw std::runtime_error(
|
|
"Embedding appears unavailable for this candidate (queue drained, "
|
|
"no "
|
|
"documents embedded after " +
|
|
std::to_string(elapsed.count()) +
|
|
"s, dropped=" + std::to_string(observedSnapshot.dropped) +
|
|
"). Aborting candidate to keep optimization loop progressing.");
|
|
}
|
|
|
|
// If stable but queue NOT drained, warn about potential issues
|
|
if (observedSnapshot.hasQueueMetrics && stableCount >= 20 &&
|
|
!observedSnapshot.queueDrained() &&
|
|
!observedSnapshot.inferLikelyActive(embedProgressTimeoutSec) &&
|
|
observedSnapshot.docsEmbedded > 0) {
|
|
spdlog::warn("Embedding stalled at {:.1f}% but queue not drained "
|
|
"(queue={}, in_flight={}). Continuing to wait...",
|
|
coverage, observedSnapshot.queueDepth,
|
|
observedSnapshot.inFlight);
|
|
// Don't break - keep waiting for queue to drain
|
|
}
|
|
}
|
|
std::this_thread::sleep_for(500ms);
|
|
}
|
|
|
|
if (auto* daemon = harness->daemon()) {
|
|
if (auto* serviceManager = daemon->getServiceManager()) {
|
|
auto vectorDb = serviceManager->getVectorDatabase();
|
|
auto metadataRepo = serviceManager->getMetadataRepo();
|
|
|
|
if (vectorDb && metadataRepo) {
|
|
auto embeddedHashes = vectorDb->getEmbeddedDocumentHashes();
|
|
if (embeddedHashes.size() > observedSnapshot.docsEmbedded) {
|
|
std::vector<std::string> reconciledHashes;
|
|
reconciledHashes.reserve(embeddedHashes.size());
|
|
for (const auto& hash : embeddedHashes) {
|
|
reconciledHashes.push_back(hash);
|
|
}
|
|
auto reconcileResult =
|
|
metadataRepo->batchUpdateDocumentEmbeddingStatusByHashes(
|
|
reconciledHashes, true,
|
|
serviceManager->getEmbeddingModelName());
|
|
if (!reconcileResult) {
|
|
spdlog::warn("Failed to reconcile embedded-doc status from "
|
|
"vector rows: {}",
|
|
reconcileResult.error().message);
|
|
} else {
|
|
spdlog::info("Reconciled embedded-doc status from vector rows: "
|
|
"{} -> {}",
|
|
observedSnapshot.docsEmbedded,
|
|
reconciledHashes.size());
|
|
observedSnapshot.docsEmbedded = reconciledHashes.size();
|
|
lastProgressSnapshot.docsEmbedded = reconciledHashes.size();
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Final status check
|
|
auto finalStatus = benchRunSync(client->status(true), 5s);
|
|
if (finalStatus) {
|
|
uint64_t finalVectorCount = 0;
|
|
auto it = finalStatus.value().requestCounts.find("vector_count");
|
|
if (it != finalStatus.value().requestCounts.end()) {
|
|
finalVectorCount = it->second;
|
|
}
|
|
uint64_t finalDocsEmbedded = 0;
|
|
auto itDocsEmbedded =
|
|
finalStatus.value().requestCounts.find("documents_embedded");
|
|
if (itDocsEmbedded != finalStatus.value().requestCounts.end()) {
|
|
finalDocsEmbedded = itDocsEmbedded->second;
|
|
}
|
|
uint64_t finalEmbedQueued = 0;
|
|
auto itQ = finalStatus.value().requestCounts.find("embed_svc_queued");
|
|
if (itQ != finalStatus.value().requestCounts.end()) {
|
|
finalEmbedQueued = itQ->second;
|
|
}
|
|
uint64_t finalEmbedInFlight = 0;
|
|
auto itInFlight = finalStatus.value().requestCounts.find("embed_in_flight");
|
|
if (itInFlight != finalStatus.value().requestCounts.end()) {
|
|
finalEmbedInFlight = itInFlight->second;
|
|
}
|
|
double finalCoverage =
|
|
corpusSize > 0 ? (finalDocsEmbedded * 100.0 / corpusSize) : 0;
|
|
bool vectorDbReady = finalStatus.value().vectorDbReady;
|
|
if (auto it = finalStatus.value().readinessStates.find("vector_db");
|
|
it != finalStatus.value().readinessStates.end()) {
|
|
vectorDbReady = it->second;
|
|
}
|
|
spdlog::info("Vector DB: ready={} dim={}, docs_embedded={} vectors={} "
|
|
"({:.1f}% doc coverage)",
|
|
(vectorDbReady ? "true" : "false"),
|
|
finalStatus.value().vectorDbDim, finalDocsEmbedded,
|
|
finalVectorCount, finalCoverage);
|
|
|
|
if (!embeddingDrainSatisfied) {
|
|
const bool queueDrained =
|
|
(finalEmbedQueued == 0 && finalEmbedInFlight == 0);
|
|
if (!queueDrained) {
|
|
throw std::runtime_error(
|
|
"Embedding drain not reached before benchmark queries "
|
|
"(embed_svc_queued=" +
|
|
std::to_string(finalEmbedQueued) +
|
|
", embed_in_flight=" + std::to_string(finalEmbedInFlight) +
|
|
", docs_embedded=" + std::to_string(finalDocsEmbedded) +
|
|
", vectors=" + std::to_string(finalVectorCount) +
|
|
"). Increase timeout/concurrency or reduce corpus size to obtain "
|
|
"valid "
|
|
"results.");
|
|
}
|
|
|
|
// Queue may only be fully drained on the final status check.
|
|
embeddingDrainSatisfied = true;
|
|
}
|
|
|
|
if (finalDocsEmbedded == 0) {
|
|
spdlog::warn("No embedded documents present after embedding wait");
|
|
} else if (finalCoverage < 90.0) {
|
|
spdlog::error("WARNING: Low document embedding coverage ({:.1f}%) will "
|
|
"degrade retrieval "
|
|
"quality!",
|
|
finalCoverage);
|
|
spdlog::error(
|
|
"Consider reducing corpus size or increasing embed channel capacity.");
|
|
}
|
|
|
|
if (!vectorDbReady) {
|
|
spdlog::warn("Vector DB not ready yet; waiting briefly before queries...");
|
|
const auto guardDeadline = std::chrono::steady_clock::now() + 30s;
|
|
while (std::chrono::steady_clock::now() < guardDeadline) {
|
|
auto st = benchRunSync(client->status(true), 5s);
|
|
if (st) {
|
|
bool ready = st.value().vectorDbReady;
|
|
if (auto it = st.value().readinessStates.find("vector_db");
|
|
it != st.value().readinessStates.end()) {
|
|
ready = it->second;
|
|
}
|
|
if (ready) {
|
|
spdlog::info("Vector DB is now ready after guard wait");
|
|
break;
|
|
}
|
|
}
|
|
std::this_thread::sleep_for(500ms);
|
|
}
|
|
}
|
|
} else {
|
|
spdlog::warn("Final status check failed after embedding wait: {}",
|
|
finalStatus.error().message);
|
|
if (!embeddingDrainSatisfied) {
|
|
throw std::runtime_error("Final status check failed after embedding wait; "
|
|
"cannot verify embedding drain. "
|
|
"Benchmark results would be unreliable.");
|
|
}
|
|
}
|
|
} else {
|
|
spdlog::info("Vectors disabled - skipping embedding generation wait");
|
|
}
|
|
|
|
} else {
|
|
spdlog::info("[Bench] Warm data dir: skipping ingestion + embedding wait (usable "
|
|
"vector index already present)");
|
|
} // end if (!warmDataDir) / else
|
|
|
|
bool benchmarkVectorIndexReusableAfter = vectorsDisabled;
|
|
auto ensureVectorIndexReady = [&]() -> bool {
|
|
if (vectorsDisabled) {
|
|
return true;
|
|
}
|
|
|
|
const fs::path vectorIndexDataDir =
|
|
harnessOptions.dataDir.has_value() ? *harnessOptions.dataDir : harness->dataDir();
|
|
|
|
auto* daemon = harness ? harness->daemon() : nullptr;
|
|
auto* serviceManager = daemon ? daemon->getServiceManager() : nullptr;
|
|
auto vectorDb = serviceManager ? serviceManager->getVectorDatabase() : nullptr;
|
|
if (!vectorDb || !vectorDb->isInitialized()) {
|
|
spdlog::warn("[Bench] Cannot ensure vector index readiness: vector DB unavailable");
|
|
return false;
|
|
}
|
|
const auto probeBackendReusable = [&]() {
|
|
const auto probeStart = std::chrono::steady_clock::now();
|
|
const bool reusable = vectorDb->hasReusablePersistedSearchIndex();
|
|
setupTimings.vectorIndexReusableProbeMs +=
|
|
std::chrono::duration_cast<std::chrono::milliseconds>(
|
|
std::chrono::steady_clock::now() - probeStart)
|
|
.count();
|
|
++setupTimings.vectorIndexReusableProbeCount;
|
|
return reusable;
|
|
};
|
|
|
|
auto persisted = vectorIndexDataDir.empty()
|
|
? PersistedHnswState{}
|
|
: inspectPersistedHnswState(vectorIndexDataDir);
|
|
const auto benchmarkEngine = benchmarkVectorSearchEngine();
|
|
const bool backendReusableBefore = probeBackendReusable();
|
|
setupTimings.vectorIndexReusableBefore =
|
|
yams::bench::benchmarkSearchIndexReusable(benchmarkEngine, backendReusableBefore);
|
|
const auto updateVectorIndexReadiness = [&](bool ready) {
|
|
if (!daemon) {
|
|
return;
|
|
}
|
|
daemon->state_.readiness.vectorIndexReady.store(ready, std::memory_order_relaxed);
|
|
daemon->state_.readiness.vectorIndexProgress.store(
|
|
ready ? 100
|
|
: (daemon->state_.readiness.vectorDbReady.load(std::memory_order_relaxed)
|
|
? 50
|
|
: 0),
|
|
std::memory_order_relaxed);
|
|
};
|
|
|
|
const auto vectorRows = vectorDb->getVectorCount();
|
|
if (vectorRows == 0) {
|
|
spdlog::warn("[Bench] Cannot ensure vector index readiness: vector DB is empty");
|
|
return false;
|
|
}
|
|
|
|
const bool indexlessExact =
|
|
benchmarkEngine == yams::vector::VectorSearchEngine::ExactScan;
|
|
bool preparedThisRun = false;
|
|
if (!indexlessExact && !setupTimings.vectorIndexReusableBefore) {
|
|
const char* modeLabel = warmDataDirPath.empty() ? "cold-run" : "warm-cache";
|
|
spdlog::info("[Bench] Finalizing {} vector index (vectors={} persisted_nodes={} "
|
|
"persisted_meta={})",
|
|
modeLabel, vectorRows, persisted.nodeRows, persisted.metaRows);
|
|
const auto start = std::chrono::steady_clock::now();
|
|
const bool prepared = vectorDb->prepareSearchIndex();
|
|
const auto elapsedMs = std::chrono::duration_cast<std::chrono::milliseconds>(
|
|
std::chrono::steady_clock::now() - start)
|
|
.count();
|
|
setupTimings.vectorIndexPrepared = prepared;
|
|
setupTimings.vectorIndexPrepareMs = elapsedMs;
|
|
|
|
if (!prepared) {
|
|
spdlog::warn("[Bench] Vector index finalize failed after {} ms: {}", elapsedMs,
|
|
vectorDb->getLastError());
|
|
return false;
|
|
}
|
|
preparedThisRun = true;
|
|
const auto persistStart = std::chrono::steady_clock::now();
|
|
const bool saved = vectorDb->persistIndex();
|
|
setupTimings.vectorIndexPersistMs =
|
|
std::chrono::duration_cast<std::chrono::milliseconds>(
|
|
std::chrono::steady_clock::now() - persistStart)
|
|
.count();
|
|
setupTimings.vectorIndexPersisted = saved;
|
|
if (!saved) {
|
|
spdlog::warn("[Bench] Vector index persist failed after {} ms: {}",
|
|
setupTimings.vectorIndexPersistMs, vectorDb->getLastError());
|
|
return false;
|
|
}
|
|
|
|
persisted = vectorIndexDataDir.empty()
|
|
? PersistedHnswState{}
|
|
: inspectPersistedHnswState(vectorIndexDataDir);
|
|
const bool backendReusableAfterPersist = probeBackendReusable();
|
|
if (yams::bench::benchmarkSearchIndexReusable(benchmarkEngine,
|
|
backendReusableAfterPersist)) {
|
|
spdlog::info("[Bench] Vector index finalized in {} ms "
|
|
"(persisted_nodes={} persisted_meta={})",
|
|
elapsedMs, persisted.nodeRows, persisted.metaRows);
|
|
} else if (benchmarkEngine == yams::vector::VectorSearchEngine::SimeonPqAdc) {
|
|
spdlog::info("[Bench] Vector index finalized in {} ms for {} "
|
|
"(persisted PQ rows not required for this cold-run process)",
|
|
elapsedMs, yams::vector::vectorSearchEngineName(benchmarkEngine));
|
|
} else if (benchmarkEngine == yams::vector::VectorSearchEngine::Vec0L2) {
|
|
spdlog::info("[Bench] Vec0 index finalized in {} ms and is query-ready for "
|
|
"this process (restart persistence unavailable)",
|
|
elapsedMs);
|
|
} else {
|
|
spdlog::info("[Bench] Exact-scan engine query-ready after {} ms; no persisted "
|
|
"search index is required",
|
|
elapsedMs);
|
|
}
|
|
}
|
|
|
|
bool reusable =
|
|
yams::bench::benchmarkSearchIndexReusable(benchmarkEngine, probeBackendReusable());
|
|
bool ready = yams::bench::benchmarkSearchIndexQueryReady(benchmarkEngine, reusable,
|
|
preparedThisRun);
|
|
benchmarkVectorIndexReusableAfter = reusable;
|
|
setupTimings.vectorIndexReusableAfter = reusable;
|
|
setupTimings.vectorQueryReadyAfter = ready;
|
|
updateVectorIndexReadiness(ready);
|
|
|
|
int readyTimeoutSec = 60;
|
|
if (const char* env = std::getenv("YAMS_BENCH_VECTOR_INDEX_READY_TIMEOUT")) {
|
|
readyTimeoutSec = std::max(0, std::stoi(env));
|
|
}
|
|
const auto readyDeadline =
|
|
std::chrono::steady_clock::now() + std::chrono::seconds(readyTimeoutSec);
|
|
while (!ready && std::chrono::steady_clock::now() < readyDeadline) {
|
|
if (!vectorIndexDataDir.empty()) {
|
|
persisted = inspectPersistedHnswState(vectorIndexDataDir);
|
|
reusable = yams::bench::benchmarkSearchIndexReusable(benchmarkEngine,
|
|
probeBackendReusable());
|
|
}
|
|
ready = yams::bench::benchmarkSearchIndexQueryReady(benchmarkEngine, reusable,
|
|
preparedThisRun);
|
|
if (!ready) {
|
|
std::this_thread::sleep_for(500ms);
|
|
}
|
|
}
|
|
|
|
benchmarkVectorIndexReusableAfter = reusable;
|
|
setupTimings.vectorIndexReusableAfter = reusable;
|
|
setupTimings.vectorQueryReadyAfter = ready;
|
|
updateVectorIndexReadiness(ready);
|
|
return ready;
|
|
};
|
|
|
|
if (!ensureVectorIndexReady()) {
|
|
throw std::runtime_error("Vector index readiness not reached before benchmark queries; "
|
|
"benchmark would mix brute-force fallback with ANN timing.");
|
|
}
|
|
|
|
if (!warmDataDirPath.empty()) {
|
|
auto finalCacheMetadata =
|
|
warmCacheMetadata.has_value()
|
|
? preferStrongerBenchCacheMetadata(expectedCacheMetadata, warmCacheMetadata)
|
|
: expectedCacheMetadata;
|
|
auto finalStatusForCache = benchRunSync(client->status(true), 5s);
|
|
if (finalStatusForCache) {
|
|
finalCacheMetadata =
|
|
currentBenchCacheMetadata(finalCacheMetadata, finalStatusForCache.value(),
|
|
warmDataDirPath, benchmarkVectorIndexReusableAfter);
|
|
}
|
|
finalCacheMetadata =
|
|
preferStrongerBenchCacheMetadata(finalCacheMetadata, warmCacheMetadata);
|
|
if (!expectedCacheMetadata.datasetPath.empty() &&
|
|
finalCacheMetadata.datasetPath.empty()) {
|
|
finalCacheMetadata.datasetPath = expectedCacheMetadata.datasetPath;
|
|
}
|
|
|
|
const auto metadataWrite = writeBenchCacheMetadata(warmDataDirPath, finalCacheMetadata);
|
|
if (!metadataWrite) {
|
|
spdlog::warn("[Bench] Failed to write warm cache metadata for {}: {}",
|
|
warmDataDirPath.string(), metadataWrite.error().message);
|
|
} else {
|
|
spdlog::info(
|
|
"[Bench] Warm cache metadata written to {} (status={}, embedded_docs={}, "
|
|
"vectors={})",
|
|
(warmDataDirPath / "retrieval_bench_cache.json").string(),
|
|
finalCacheMetadata.status, finalCacheMetadata.embeddedDocs,
|
|
finalCacheMetadata.vectorCount);
|
|
}
|
|
}
|
|
|
|
// Wait for search engine to be ready (critical for hybrid search!)
|
|
// Without this, all hybrid searches fall back to keyword-only
|
|
spdlog::info("Waiting for hybrid/vector search readiness...");
|
|
const auto searchEngineDeadline = std::chrono::steady_clock::now() + 60s;
|
|
bool searchEngineReady = false;
|
|
while (std::chrono::steady_clock::now() < searchEngineDeadline) {
|
|
auto statusCheck = benchRunSync(client->status(true), 5s);
|
|
if (statusCheck) {
|
|
const auto& readiness = statusCheck.value().readinessStates;
|
|
bool hybridReady = false;
|
|
if (auto it = readiness.find("search_engine_hybrid_usable");
|
|
it != readiness.end()) {
|
|
hybridReady = it->second;
|
|
} else {
|
|
const bool lexicalReady = [&]() {
|
|
if (auto it = readiness.find("search_engine_lexical");
|
|
it != readiness.end()) {
|
|
return it->second;
|
|
}
|
|
if (auto it = readiness.find("search_engine"); it != readiness.end()) {
|
|
return it->second;
|
|
}
|
|
return false;
|
|
}();
|
|
const bool vectorReady = [&]() {
|
|
if (auto it = readiness.find("search_engine_vector_usable");
|
|
it != readiness.end()) {
|
|
return it->second;
|
|
}
|
|
if (auto it = readiness.find("vector_db_ready"); it != readiness.end()) {
|
|
return it->second;
|
|
}
|
|
if (auto it = readiness.find("vector_db"); it != readiness.end()) {
|
|
return it->second;
|
|
}
|
|
return statusCheck.value().vectorDbReady;
|
|
}();
|
|
hybridReady = lexicalReady && vectorReady;
|
|
}
|
|
if (hybridReady) {
|
|
searchEngineReady = true;
|
|
spdlog::info("Hybrid search is ready (lexical + vector usable)");
|
|
break;
|
|
}
|
|
}
|
|
std::this_thread::sleep_for(500ms);
|
|
}
|
|
if (!searchEngineReady) {
|
|
spdlog::error(
|
|
"Hybrid/vector search not ready after 60s - hybrid searches will fall back to "
|
|
"keyword-only!");
|
|
spdlog::error("This is likely due to vector publication not completing.");
|
|
}
|
|
|
|
// Wait for Simeon lexical enhancement and concept mining to complete.
|
|
// The async build thread can still be running after the search engine reports
|
|
// ready, and concept mining can take additional wall-clock time.
|
|
if (!vectorsDisabled) {
|
|
const bool waitForConcepts =
|
|
std::getenv("YAMS_SEARCH_WAIT_FOR_CONCEPTS") &&
|
|
std::string(std::getenv("YAMS_SEARCH_WAIT_FOR_CONCEPTS")) != "0";
|
|
if (waitForConcepts) {
|
|
const auto lexicalReadyDeadline = std::chrono::steady_clock::now() + 120s;
|
|
bool lexicalReady = false;
|
|
spdlog::info("Waiting for Simeon lexical enhancement readiness...");
|
|
while (std::chrono::steady_clock::now() < lexicalReadyDeadline) {
|
|
auto statusCheck = benchRunSync(client->status(true), 5s);
|
|
if (statusCheck) {
|
|
const auto& rc = statusCheck.value().requestCounts;
|
|
const auto get = [&](const std::string& key) -> uint64_t {
|
|
auto it = rc.find(key);
|
|
return (it == rc.end()) ? 0ULL : it->second;
|
|
};
|
|
const bool configured =
|
|
get("status_search_engine_lexical_enhancement_configured") > 0;
|
|
const bool ready =
|
|
get("status_search_engine_lexical_enhancement_ready") > 0;
|
|
const bool building =
|
|
get("status_search_engine_lexical_enhancement_building") > 0;
|
|
const uint64_t docCount =
|
|
get("status_search_engine_lexical_enhancement_doc_count");
|
|
const uint64_t conceptCount =
|
|
get("status_search_engine_lexical_enhancement_concept_count");
|
|
const bool conceptMiningEnabled =
|
|
get("status_search_engine_lexical_enhancement_concept_mining_enabled") >
|
|
0;
|
|
|
|
if (!configured) {
|
|
spdlog::info("Simeon lexical enhancement not configured; skipping "
|
|
"lexical readiness wait");
|
|
lexicalReady = true;
|
|
break;
|
|
}
|
|
if (ready && !building && docCount >= static_cast<uint64_t>(corpusSize)) {
|
|
if (conceptMiningEnabled && corpusSize >= 100 && conceptCount == 0) {
|
|
spdlog::info("Simeon lexical ready (docs={}) but concept mining "
|
|
"still pending; waiting...",
|
|
docCount);
|
|
} else {
|
|
spdlog::info("Simeon lexical enhancement ready: configured={} "
|
|
"ready={} building={} doc_count={} concept_count={} "
|
|
"concept_mining_enabled={}",
|
|
configured, ready, building, docCount, conceptCount,
|
|
conceptMiningEnabled);
|
|
lexicalReady = true;
|
|
break;
|
|
}
|
|
} else {
|
|
spdlog::info("Simeon lexical enhancement not yet ready: "
|
|
"configured={} ready={} building={} doc_count={} "
|
|
"concept_count={}",
|
|
configured, ready, building, docCount, conceptCount);
|
|
}
|
|
}
|
|
std::this_thread::sleep_for(500ms);
|
|
}
|
|
if (!lexicalReady) {
|
|
spdlog::error("Simeon lexical enhancement not ready after 120s");
|
|
}
|
|
}
|
|
}
|
|
|
|
if (coldIngestStartedAt.has_value()) {
|
|
setupTimings.searchabilityReadyMs = elapsedMsSince(*coldIngestStartedAt);
|
|
}
|
|
|
|
auto effectiveKgReadinessPolicy = kgReadinessPolicy;
|
|
if (kgReadinessPolicy != yams::search::BenchmarkKgReadinessPolicy::Skip) {
|
|
auto statusForKgPolicy = benchRunSync(client->status(true), 5s);
|
|
if (statusForKgPolicy) {
|
|
bool tunedGraphRerank = graphRerankRequested;
|
|
if (auto it =
|
|
statusForKgPolicy.value().searchTuningParams.find("enable_graph_rerank");
|
|
it != statusForKgPolicy.value().searchTuningParams.end()) {
|
|
tunedGraphRerank = it->second > 0.5f;
|
|
}
|
|
|
|
effectiveKgReadinessPolicy = yams::search::refineBenchmarkKgReadinessPolicy(
|
|
kgReadinessPolicy, explicitRequireKgReady, tunedGraphRerank);
|
|
if (effectiveKgReadinessPolicy != kgReadinessPolicy) {
|
|
spdlog::info("[Bench] Refined KG readiness policy to {} after search tuning "
|
|
"(state='{}', enable_graph_rerank={})",
|
|
yams::search::benchmarkKgReadinessPolicyToString(
|
|
effectiveKgReadinessPolicy),
|
|
statusForKgPolicy.value().searchTuningState,
|
|
tunedGraphRerank ? "true" : "false");
|
|
}
|
|
} else {
|
|
spdlog::warn("[Bench] Unable to inspect tuned graph rerank before KG wait: {}",
|
|
statusForKgPolicy.error().message);
|
|
}
|
|
}
|
|
|
|
if (effectiveKgReadinessPolicy != yams::search::BenchmarkKgReadinessPolicy::Skip) {
|
|
int kgReadyTimeoutSec = 180;
|
|
if (const char* env = std::getenv("YAMS_BENCH_KG_READY_TIMEOUT")) {
|
|
kgReadyTimeoutSec = std::max(0, std::stoi(env));
|
|
}
|
|
|
|
spdlog::info("Waiting for KG readiness before query evaluation (timeout={}s)...",
|
|
kgReadyTimeoutSec);
|
|
|
|
const auto kgDeadline =
|
|
std::chrono::steady_clock::now() + std::chrono::seconds(kgReadyTimeoutSec);
|
|
int stickyInflightGraceSec = 20;
|
|
if (const char* env = std::getenv("YAMS_BENCH_KG_STICKY_INFLIGHT_GRACE_SEC")) {
|
|
stickyInflightGraceSec = std::max(0, std::stoi(env));
|
|
}
|
|
const auto stickyInflightGrace = std::chrono::seconds(stickyInflightGraceSec);
|
|
int stableReadyChecks = 0;
|
|
bool kgReady = false;
|
|
bool acceptedStickyInflight = false;
|
|
bool acceptedWarmReuseIdle = false;
|
|
bool lastHasKg = false;
|
|
uint32_t lastQueueDepth = 0;
|
|
uint64_t lastPostQueued = 0;
|
|
uint64_t lastPostInflight = 0;
|
|
uint64_t lastKgInflight = 0;
|
|
uint64_t lastKgConsumed = 0;
|
|
uint64_t lastSymbolInflight = 0;
|
|
uint64_t lastEntityInflight = 0;
|
|
uint64_t lastTitleInflight = 0;
|
|
uint64_t lastTitleQueueDepth = 0;
|
|
uint64_t lastExtractionInflight = 0;
|
|
bool stickyInflightActive = false;
|
|
uint64_t stickyInflightValue = 0;
|
|
auto stickyInflightSince = std::chrono::steady_clock::time_point{};
|
|
|
|
while (std::chrono::steady_clock::now() < kgDeadline) {
|
|
const auto now = std::chrono::steady_clock::now();
|
|
auto statusCheck = benchRunSync(client->status(true), 5s);
|
|
auto statsCheck =
|
|
benchRunSync(client->getStats(yams::daemon::GetStatsRequest{}), 10s);
|
|
|
|
bool queuesDrained = false;
|
|
if (statusCheck) {
|
|
const auto& st = statusCheck.value();
|
|
const auto getCount = [&st](const std::string& key) -> uint64_t {
|
|
auto it = st.requestCounts.find(key);
|
|
return (it == st.requestCounts.end()) ? 0ULL : it->second;
|
|
};
|
|
|
|
lastQueueDepth = st.postIngestQueueDepth;
|
|
lastPostQueued = getCount("post_ingest_queued");
|
|
lastPostInflight = getCount("post_ingest_inflight");
|
|
lastExtractionInflight = getCount("extraction_inflight");
|
|
lastKgInflight = getCount("kg_inflight");
|
|
lastKgConsumed = getCount("kg_consumed");
|
|
lastSymbolInflight = getCount("symbol_inflight");
|
|
lastEntityInflight = getCount("entity_inflight");
|
|
lastTitleInflight = getCount("title_inflight");
|
|
lastTitleQueueDepth = getCount("title_queue_depth");
|
|
|
|
queuesDrained =
|
|
(lastQueueDepth == 0 && lastPostQueued == 0 && lastPostInflight == 0 &&
|
|
lastExtractionInflight == 0 && lastKgInflight == 0 &&
|
|
lastSymbolInflight == 0 && lastEntityInflight == 0 &&
|
|
lastTitleInflight == 0 && lastTitleQueueDepth == 0);
|
|
}
|
|
|
|
bool hasKg = false;
|
|
uint64_t docEntitiesInserted = 0;
|
|
if (statsCheck) {
|
|
const auto& stats = statsCheck.value();
|
|
if (auto it = stats.additionalStats.find("corpus_stats");
|
|
it != stats.additionalStats.end() && !it->second.empty()) {
|
|
try {
|
|
auto corpusStats = json::parse(it->second);
|
|
if (corpusStats.contains("classification") &&
|
|
corpusStats["classification"].is_object()) {
|
|
hasKg = corpusStats["classification"].value("has_kg", false);
|
|
}
|
|
} catch (const json::exception&) {
|
|
}
|
|
}
|
|
if (auto it = stats.additionalStats.find("kg_write_doc_entities_inserted");
|
|
it != stats.additionalStats.end() && !it->second.empty()) {
|
|
docEntitiesInserted = static_cast<uint64_t>(std::stoull(it->second));
|
|
}
|
|
}
|
|
|
|
lastHasKg = hasKg;
|
|
const bool kgSignalReady =
|
|
hasKg || (lastKgConsumed > 0) || (docEntitiesInserted > 0);
|
|
|
|
const bool queueIdle = (lastQueueDepth == 0 && lastPostQueued == 0);
|
|
const bool stageInflightIdle =
|
|
(lastExtractionInflight == 0 && lastKgInflight == 0 &&
|
|
lastSymbolInflight == 0 && lastEntityInflight == 0 && lastTitleInflight == 0 &&
|
|
lastTitleQueueDepth == 0);
|
|
|
|
bool stickyInflightReady = false;
|
|
if (kgSignalReady && queueIdle && stageInflightIdle && lastPostInflight > 0) {
|
|
if (!stickyInflightActive || stickyInflightValue != lastPostInflight) {
|
|
stickyInflightActive = true;
|
|
stickyInflightValue = lastPostInflight;
|
|
stickyInflightSince = now;
|
|
} else if (now - stickyInflightSince >= stickyInflightGrace) {
|
|
stickyInflightReady = true;
|
|
}
|
|
} else {
|
|
stickyInflightActive = false;
|
|
stickyInflightValue = 0;
|
|
}
|
|
|
|
const bool reusedWarmCacheWithoutIngest =
|
|
usingExternalBenchDataDir && cacheHasIndexedCorpus && cacheHasFullEmbeddings &&
|
|
cacheHasReusableVectorIndex;
|
|
const bool warmReuseIdleReady =
|
|
reusedWarmCacheWithoutIngest && queuesDrained && lastPostInflight == 0;
|
|
|
|
if (yams::search::benchmarkKgReadinessSatisfied(effectiveKgReadinessPolicy,
|
|
queuesDrained, kgSignalReady)) {
|
|
stableReadyChecks++;
|
|
if (stableReadyChecks >= 4) {
|
|
kgReady = true;
|
|
break;
|
|
}
|
|
} else if (warmReuseIdleReady) {
|
|
kgReady = true;
|
|
acceptedWarmReuseIdle = true;
|
|
break;
|
|
} else if (stickyInflightReady) {
|
|
kgReady = true;
|
|
acceptedStickyInflight = true;
|
|
break;
|
|
} else {
|
|
stableReadyChecks = 0;
|
|
}
|
|
|
|
std::this_thread::sleep_for(500ms);
|
|
}
|
|
|
|
if (!kgReady) {
|
|
throw std::runtime_error(
|
|
"KG not ready before benchmark queries (has_kg=" +
|
|
std::string(lastHasKg ? "true" : "false") + ", policy=" +
|
|
std::string(yams::search::benchmarkKgReadinessPolicyToString(
|
|
effectiveKgReadinessPolicy)) +
|
|
", kg_consumed=" + std::to_string(lastKgConsumed) +
|
|
", post_ingest_queue_depth=" + std::to_string(lastQueueDepth) +
|
|
", post_ingest_queued=" + std::to_string(lastPostQueued) +
|
|
", post_ingest_inflight=" + std::to_string(lastPostInflight) +
|
|
", extraction_inflight=" + std::to_string(lastExtractionInflight) +
|
|
", kg_inflight=" + std::to_string(lastKgInflight) +
|
|
", symbol_inflight=" + std::to_string(lastSymbolInflight) +
|
|
", entity_inflight=" + std::to_string(lastEntityInflight) +
|
|
", title_inflight=" + std::to_string(lastTitleInflight) +
|
|
", title_queue_depth=" + std::to_string(lastTitleQueueDepth) +
|
|
"). Increase YAMS_BENCH_KG_READY_TIMEOUT or reduce ingestion load.");
|
|
}
|
|
|
|
if (effectiveKgReadinessPolicy ==
|
|
yams::search::BenchmarkKgReadinessPolicy::AcceptDrainedWithoutSignal &&
|
|
!lastHasKg && lastKgConsumed == 0) {
|
|
spdlog::warn(
|
|
"KG readiness satisfied with drained queues under relaxed synthetic policy; "
|
|
"no explicit KG signal was observed. Graph signals may be weak.");
|
|
} else if (!lastHasKg) {
|
|
if (acceptedWarmReuseIdle) {
|
|
spdlog::warn(
|
|
"KG readiness accepted for reused warm cache with drained queues and no "
|
|
"active KG work, but corpus_stats.has_kg=false and kg_consumed={} in this "
|
|
"process. Graph signals may still be weak.",
|
|
lastKgConsumed);
|
|
} else {
|
|
spdlog::warn(
|
|
"KG readiness satisfied via kg_consumed={}, but corpus_stats.has_kg=false "
|
|
"(low symbol_density). Graph signals may still be weak.",
|
|
lastKgConsumed);
|
|
}
|
|
} else if (acceptedStickyInflight) {
|
|
spdlog::warn(
|
|
"KG readiness accepted with sticky post_ingest_inflight={} after {}s grace "
|
|
"(queues drained + stage inflight idle + corpus_stats.has_kg=true).",
|
|
lastPostInflight, stickyInflightGraceSec);
|
|
} else if (acceptedWarmReuseIdle) {
|
|
spdlog::info(
|
|
"KG readiness accepted for reused warm cache with drained queues and no "
|
|
"active KG work");
|
|
} else {
|
|
spdlog::info("KG readiness satisfied (queues drained + corpus_stats.has_kg=true)");
|
|
}
|
|
} else if (requireKgReady) {
|
|
spdlog::info(
|
|
"Skipping KG readiness wait because the effective tuned search policy disables "
|
|
"graph rerank.");
|
|
}
|
|
|
|
yams::search::BenchmarkDaemonGraphState daemonGraphState;
|
|
bool daemonGraphReady = false;
|
|
bool daemonGraphCanonicalized = false;
|
|
std::size_t daemonGraphCanonicalEdgesGenerated = 0;
|
|
std::string daemonGraphCanonicalTopologySnapshot;
|
|
std::string daemonGraphCanonicalTopologyFingerprint;
|
|
if (g_requireDaemonNativeGraph) {
|
|
auto graphState =
|
|
waitForDaemonNativeGraph(*client, static_cast<std::uint64_t>(corpusSize));
|
|
if (!graphState) {
|
|
throw std::runtime_error(graphState.error().message);
|
|
}
|
|
daemonGraphState = std::move(graphState.value());
|
|
|
|
auto* daemon = harness ? harness->daemon() : nullptr;
|
|
auto* serviceManager = daemon ? daemon->getServiceManager() : nullptr;
|
|
if (!serviceManager) {
|
|
throw std::runtime_error(
|
|
"Daemon-native graph canonicalization requires ServiceManager");
|
|
}
|
|
auto semanticRebuild = serviceManager->rebuildSemanticNeighborGraph(
|
|
"benchmark_daemon_native_graph_canonicalization");
|
|
if (!semanticRebuild) {
|
|
throw std::runtime_error("Failed to canonicalize daemon semantic graph: " +
|
|
semanticRebuild.error().message);
|
|
}
|
|
daemonGraphCanonicalEdgesGenerated = semanticRebuild.value();
|
|
|
|
auto canonicalGraphState =
|
|
waitForDaemonNativeGraph(*client, static_cast<std::uint64_t>(corpusSize));
|
|
if (!canonicalGraphState) {
|
|
throw std::runtime_error(canonicalGraphState.error().message);
|
|
}
|
|
daemonGraphState = std::move(canonicalGraphState.value());
|
|
|
|
std::string topologyAlgorithm = "connected";
|
|
if (const char* raw = std::getenv("YAMS_BENCH_TOPOLOGY_ENGINE"); raw && *raw) {
|
|
topologyAlgorithm = raw;
|
|
}
|
|
auto topologyRebuild =
|
|
rebuildCanonicalDaemonTopology(*serviceManager, topologyAlgorithm);
|
|
if (!topologyRebuild) {
|
|
throw std::runtime_error("Failed to canonicalize daemon topology artifacts: " +
|
|
topologyRebuild.error().message);
|
|
}
|
|
daemonGraphCanonicalTopologySnapshot = topologyRebuild.value().snapshotId;
|
|
auto topologyFingerprint = loadTopologyRoutingConstructionFingerprint(
|
|
*serviceManager, daemonGraphCanonicalTopologySnapshot);
|
|
if (!topologyFingerprint) {
|
|
throw std::runtime_error(
|
|
"Failed to fingerprint canonical daemon topology artifacts: " +
|
|
topologyFingerprint.error().message);
|
|
}
|
|
daemonGraphCanonicalTopologyFingerprint = std::move(topologyFingerprint.value());
|
|
daemonGraphCanonicalized = true;
|
|
daemonGraphReady = true;
|
|
}
|
|
|
|
if (coldIngestStartedAt.has_value()) {
|
|
setupTimings.enrichmentReadyMs = elapsedMsSince(*coldIngestStartedAt);
|
|
}
|
|
|
|
// Verify document count using status metrics (avoids degraded search false negatives)
|
|
uint64_t indexedDocCount = 0;
|
|
auto statusResult = benchRunSync(client->status(true), 5s);
|
|
if (warmCacheMetadata && warmCacheMetadata->status == "primed") {
|
|
indexedDocCount = static_cast<uint64_t>(warmCacheMetadata->expectedDocs);
|
|
} else if (statusResult) {
|
|
if (auto it = statusResult.value().requestCounts.find("documents_indexed");
|
|
it != statusResult.value().requestCounts.end()) {
|
|
indexedDocCount = it->second;
|
|
} else if (auto it = statusResult.value().requestCounts.find("documents_total");
|
|
it != statusResult.value().requestCounts.end()) {
|
|
indexedDocCount = it->second;
|
|
}
|
|
}
|
|
|
|
if (indexedDocCount == 0) {
|
|
yams::daemon::SearchRequest testReq;
|
|
testReq.query = "test";
|
|
testReq.searchType = "hybrid";
|
|
testReq.limit = 1000;
|
|
const auto queryTimeout = configuredBenchQueryTimeout("hybrid");
|
|
testReq.timeout = queryTimeout;
|
|
auto waitBudget = queryTimeout + std::chrono::seconds(10);
|
|
if (waitBudget < std::chrono::seconds(10)) {
|
|
waitBudget = std::chrono::seconds(10);
|
|
}
|
|
auto testSearch =
|
|
[&]() -> boost::asio::awaitable<Result<yams::daemon::SearchResponse>> {
|
|
if (benchUseStreamingSearch()) {
|
|
co_return co_await client->search(testReq);
|
|
}
|
|
co_return co_await client->unarySearch(testReq);
|
|
};
|
|
auto testResult = benchRunSync(testSearch(), waitBudget);
|
|
indexedDocCount = testResult ? testResult.value().results.size() : 0;
|
|
}
|
|
|
|
spdlog::info("Verified indexed documents: {} (expected: {})", indexedDocCount, corpusSize);
|
|
if (indexedDocCount == 0) {
|
|
spdlog::error("NO DOCUMENTS IN INDEX! Ingestion failed completely.");
|
|
throw std::runtime_error("No documents indexed - benchmark cannot proceed");
|
|
}
|
|
|
|
seedSyntheticCommunityEdges();
|
|
const bool reuseSeededTopologyInputs =
|
|
warmDataDir && envTruthy(std::getenv("YAMS_BENCH_REUSE_SEEDED_TOPOLOGY_INPUTS"));
|
|
const bool seededBenchmarkTopology =
|
|
reuseSeededTopologyInputs || seedBenchmarkSemanticNeighbors();
|
|
if (reuseSeededTopologyInputs) {
|
|
spdlog::info("Reusing immutable semantic-neighbor topology inputs from warm cache");
|
|
}
|
|
|
|
if (benchmarkSemanticNeighborSeedingEnabled()) {
|
|
if (!seededBenchmarkTopology) {
|
|
throw std::runtime_error(
|
|
"Benchmark topology source seeding did not produce semantic_neighbor edges");
|
|
}
|
|
auto* daemon = harness ? harness->daemon() : nullptr;
|
|
auto* serviceManager = daemon ? daemon->getServiceManager() : nullptr;
|
|
if (!serviceManager) {
|
|
throw std::runtime_error("Topology source benchmark requires ServiceManager");
|
|
}
|
|
std::string topologyEngine = "connected";
|
|
if (const char* raw = std::getenv("YAMS_BENCH_TOPOLOGY_ENGINE"); raw && *raw) {
|
|
topologyEngine = raw;
|
|
}
|
|
auto& topologyManager = serviceManager->getTopologyManager();
|
|
const auto rebuildDeadline = std::chrono::steady_clock::now() + 60s;
|
|
std::optional<yams::daemon::TopologyManager::RebuildStats> rebuildStats;
|
|
do {
|
|
if (topologyManager.isRebuildInProgress()) {
|
|
std::this_thread::sleep_for(50ms);
|
|
continue;
|
|
}
|
|
auto attempt = topologyManager.rebuildArtifacts("benchmark_topology_source_seed",
|
|
false, {}, topologyEngine);
|
|
if (!attempt) {
|
|
throw std::runtime_error("Failed to rebuild topology after benchmark source "
|
|
"seeding: " +
|
|
attempt.error().message);
|
|
}
|
|
if (!attempt.value().skipped) {
|
|
rebuildStats = std::move(attempt.value());
|
|
break;
|
|
}
|
|
std::this_thread::sleep_for(50ms);
|
|
} while (std::chrono::steady_clock::now() < rebuildDeadline);
|
|
if (!rebuildStats.has_value()) {
|
|
throw std::runtime_error(
|
|
"Timed out waiting to publish benchmark topology snapshot");
|
|
}
|
|
if (!rebuildStats->stored) {
|
|
throw std::runtime_error(
|
|
"Topology source benchmark rebuild did not publish a stored snapshot");
|
|
}
|
|
spdlog::info("Rebuilt topology after source seeding: source={} clusters={} "
|
|
"memberships={} snapshot={}",
|
|
std::getenv("YAMS_BENCH_TOPOLOGY_SOURCE")
|
|
? std::getenv("YAMS_BENCH_TOPOLOGY_SOURCE")
|
|
: "vector",
|
|
rebuildStats->clustersBuilt, rebuildStats->membershipsBuilt,
|
|
rebuildStats->snapshotId);
|
|
auto exportResult = exportTopologyClusters(*serviceManager, rebuildStats->snapshotId);
|
|
if (!exportResult) {
|
|
throw std::runtime_error("Failed to export topology cluster composition: " +
|
|
exportResult.error().message);
|
|
}
|
|
}
|
|
|
|
// Post-ingest status/stats snapshot + sanity searches.
|
|
// Goal: distinguish "no docs" vs "docs but query returns empty".
|
|
{
|
|
// Log StatusResponse (it already powers our ingestion waits).
|
|
auto statusRes = benchRunSync(client->status(true), 5s);
|
|
if (statusRes) {
|
|
const auto& st = statusRes.value();
|
|
uint64_t documentsTotal = 0;
|
|
uint64_t documentsEmbedded = 0;
|
|
uint64_t vectorCount = 0;
|
|
uint64_t preparedDocsQueued = 0;
|
|
uint64_t preparedChunksQueued = 0;
|
|
uint64_t titleInflight = 0;
|
|
uint64_t titleQueueDepth = 0;
|
|
uint64_t titleQueued = 0;
|
|
uint64_t titleDropped = 0;
|
|
uint64_t titleConsumed = 0;
|
|
if (auto it = st.requestCounts.find("documents_total");
|
|
it != st.requestCounts.end()) {
|
|
documentsTotal = it->second;
|
|
}
|
|
if (auto it = st.requestCounts.find("documents_embedded");
|
|
it != st.requestCounts.end()) {
|
|
documentsEmbedded = it->second;
|
|
}
|
|
if (auto it = st.requestCounts.find("vector_count"); it != st.requestCounts.end()) {
|
|
vectorCount = it->second;
|
|
}
|
|
if (auto it = st.requestCounts.find("bus_embed_prepared_docs_queued");
|
|
it != st.requestCounts.end()) {
|
|
preparedDocsQueued = it->second;
|
|
}
|
|
if (auto it = st.requestCounts.find("bus_embed_prepared_chunks_queued");
|
|
it != st.requestCounts.end()) {
|
|
preparedChunksQueued = it->second;
|
|
}
|
|
if (auto it = st.requestCounts.find("title_inflight");
|
|
it != st.requestCounts.end()) {
|
|
titleInflight = it->second;
|
|
}
|
|
if (auto it = st.requestCounts.find("title_queue_depth");
|
|
it != st.requestCounts.end()) {
|
|
titleQueueDepth = it->second;
|
|
}
|
|
if (auto it = st.requestCounts.find("title_queued"); it != st.requestCounts.end()) {
|
|
titleQueued = it->second;
|
|
}
|
|
if (auto it = st.requestCounts.find("title_dropped");
|
|
it != st.requestCounts.end()) {
|
|
titleDropped = it->second;
|
|
}
|
|
if (auto it = st.requestCounts.find("title_consumed");
|
|
it != st.requestCounts.end()) {
|
|
titleConsumed = it->second;
|
|
}
|
|
|
|
// Log search tuning state (epic yams-7ez4)
|
|
if (!st.searchTuningState.empty()) {
|
|
spdlog::info("Search tuning state: {} ({})", st.searchTuningState,
|
|
st.searchTuningReason);
|
|
spdlog::info("Search tuning params:");
|
|
for (const auto& [k, v] : st.searchTuningParams) {
|
|
spdlog::info(" {}: {:.4f}", k, v);
|
|
}
|
|
}
|
|
|
|
DebugLogEntry statusEntry;
|
|
statusEntry.query = "__post_ingest_status__";
|
|
statusEntry.returnedPaths.push_back("documents_total=" +
|
|
std::to_string(documentsTotal));
|
|
statusEntry.returnedPaths.push_back("documents_embedded=" +
|
|
std::to_string(documentsEmbedded));
|
|
statusEntry.returnedPaths.push_back("vector_count=" + std::to_string(vectorCount));
|
|
statusEntry.returnedPaths.push_back("bus_embed_prepared_docs_queued=" +
|
|
std::to_string(preparedDocsQueued));
|
|
statusEntry.returnedPaths.push_back("bus_embed_prepared_chunks_queued=" +
|
|
std::to_string(preparedChunksQueued));
|
|
statusEntry.returnedPaths.push_back("title_inflight=" +
|
|
std::to_string(titleInflight));
|
|
statusEntry.returnedPaths.push_back("title_queue_depth=" +
|
|
std::to_string(titleQueueDepth));
|
|
statusEntry.returnedPaths.push_back("title_queued=" + std::to_string(titleQueued));
|
|
statusEntry.returnedPaths.push_back("title_dropped=" +
|
|
std::to_string(titleDropped));
|
|
statusEntry.returnedPaths.push_back("title_consumed=" +
|
|
std::to_string(titleConsumed));
|
|
statusEntry.returnedPaths.push_back("post_ingest_queue_depth=" +
|
|
std::to_string(st.postIngestQueueDepth));
|
|
statusEntry.returnedPaths.push_back(std::string("embedding_available=") +
|
|
(st.embeddingAvailable ? "true" : "false"));
|
|
bool vectorDbReady = st.vectorDbReady;
|
|
if (auto it = st.readinessStates.find("vector_db");
|
|
it != st.readinessStates.end()) {
|
|
vectorDbReady = it->second;
|
|
}
|
|
statusEntry.returnedPaths.push_back(std::string("vector_db_ready=") +
|
|
(vectorDbReady ? "true" : "false"));
|
|
statusEntry.returnedPaths.push_back("vector_db_dim=" +
|
|
std::to_string(st.vectorDbDim));
|
|
statusEntry.returnedPaths.push_back("embedding_backend=" + st.embeddingBackend);
|
|
statusEntry.returnedPaths.push_back("embedding_model=" + st.embeddingModel);
|
|
// Search tuning state (epic yams-7ez4)
|
|
statusEntry.returnedPaths.push_back("search_tuning_state=" + st.searchTuningState);
|
|
statusEntry.returnedPaths.push_back("search_tuning_reason=" +
|
|
st.searchTuningReason);
|
|
|
|
double tunedKgWeight = 0.0;
|
|
bool tunedGraphRerank = graphRerankRequested;
|
|
if (auto it = st.searchTuningParams.find("kg_weight");
|
|
it != st.searchTuningParams.end()) {
|
|
tunedKgWeight = static_cast<double>(it->second);
|
|
}
|
|
if (auto it = st.searchTuningParams.find("enable_graph_rerank");
|
|
it != st.searchTuningParams.end()) {
|
|
tunedGraphRerank = it->second > 0.5;
|
|
}
|
|
spdlog::info("[Bench] Effective graph rerank: env={} tuned={} kg_weight={:.4f}",
|
|
graphRerankRequested ? "on" : "off", tunedGraphRerank ? "on" : "off",
|
|
tunedKgWeight);
|
|
|
|
for (const auto& [k, v] : st.searchTuningParams) {
|
|
statusEntry.returnedPaths.push_back("tuning_" + k + "=" + std::to_string(v));
|
|
}
|
|
debugLogWriteJsonLine(statusEntry);
|
|
} else {
|
|
spdlog::warn("Status failed (post-ingest): {}", statusRes.error().message);
|
|
}
|
|
|
|
// Log GetStatsResponse (extra counters);
|
|
// we also emit structured JSON (additional_stats) for easier parsing.
|
|
yams::daemon::GetStatsRequest statsReq;
|
|
auto statsRes = benchRunSync(client->getStats(statsReq), 10s);
|
|
if (statsRes) {
|
|
const auto& st = statsRes.value();
|
|
spdlog::info("GetStats: totalDocuments={} indexedDocuments={} vectorIndexSize={} "
|
|
"additionalStats.size={}",
|
|
st.totalDocuments, st.indexedDocuments, st.vectorIndexSize,
|
|
st.additionalStats.size());
|
|
|
|
DebugLogEntry statsEntry;
|
|
statsEntry.query = "__post_ingest_stats__";
|
|
statsEntry.returnedPaths.push_back("totalDocuments=" +
|
|
std::to_string(st.totalDocuments));
|
|
statsEntry.returnedPaths.push_back("indexedDocuments=" +
|
|
std::to_string(st.indexedDocuments));
|
|
statsEntry.returnedPaths.push_back("vectorIndexSize=" +
|
|
std::to_string(st.vectorIndexSize));
|
|
|
|
const auto appendIfPresent = [&](std::string_view key) {
|
|
auto it = st.additionalStats.find(std::string(key));
|
|
if (it != st.additionalStats.end()) {
|
|
statsEntry.returnedPaths.push_back(std::string(key) + "=" + it->second);
|
|
}
|
|
};
|
|
appendIfPresent("kg_doc_entities_total");
|
|
appendIfPresent("post_title_nl_docs_processed");
|
|
appendIfPresent("post_title_nl_docs_with_entities");
|
|
appendIfPresent("post_title_nl_entities_extracted");
|
|
appendIfPresent("post_deferred_doc_entities_queued");
|
|
appendIfPresent("post_deferred_doc_entity_queue_failures");
|
|
appendIfPresent("kg_write_doc_entities_inserted");
|
|
appendIfPresent("kg_write_deferred_doc_entities_skipped");
|
|
|
|
statsEntry.extraFields = {
|
|
{"additional_stats", st.additionalStats},
|
|
{"total_documents", st.totalDocuments},
|
|
{"indexed_documents", st.indexedDocuments},
|
|
{"vector_index_size", st.vectorIndexSize},
|
|
};
|
|
|
|
for (const auto& [k, v] : st.additionalStats) {
|
|
statsEntry.returnedPaths.push_back(k + "=" + v);
|
|
}
|
|
|
|
debugLogWriteJsonLine(statsEntry);
|
|
} else {
|
|
spdlog::warn("GetStats failed: {}", statsRes.error().message);
|
|
}
|
|
|
|
auto doSanitySearch = [&](std::string label, std::string query,
|
|
std::string searchType) {
|
|
yams::cli::search_runner::DaemonSearchOptions opts;
|
|
opts.query = std::move(query);
|
|
opts.searchType = std::move(searchType);
|
|
opts.limit = 25;
|
|
const auto queryTimeout = configuredBenchQueryTimeout(opts.searchType);
|
|
opts.timeout = queryTimeout;
|
|
opts.allowFuzzyRetry = false;
|
|
opts.allowLiteralTextRetry = false;
|
|
|
|
DebugLogEntry sanityEntry;
|
|
sanityEntry.query = std::move(label);
|
|
|
|
auto waitBudget =
|
|
std::chrono::duration_cast<std::chrono::milliseconds>(opts.timeout) +
|
|
std::chrono::seconds(10);
|
|
if (waitBudget < std::chrono::seconds(10)) {
|
|
waitBudget = std::chrono::seconds(10);
|
|
}
|
|
auto run = benchRunSync(yams::cli::search_runner::daemon_search(
|
|
*client, opts, benchUseStreamingSearch()),
|
|
waitBudget);
|
|
if (!run) {
|
|
sanityEntry.returnedPaths.push_back("error=" + run.error().message);
|
|
debugLogWriteJsonLine(sanityEntry);
|
|
return;
|
|
}
|
|
|
|
sanityEntry.attempts = run.value().attempts;
|
|
sanityEntry.usedStreaming = run.value().usedStreaming;
|
|
sanityEntry.usedFuzzyRetry = run.value().usedFuzzyRetry;
|
|
sanityEntry.usedLiteralTextRetry = run.value().usedLiteralTextRetry;
|
|
|
|
const auto& results = run.value().response.results;
|
|
for (size_t i = 0; i < std::min<size_t>(results.size(), 10); ++i) {
|
|
sanityEntry.returnedPaths.push_back(results[i].path);
|
|
}
|
|
|
|
sanityEntry.returnedDocIds.push_back("result_count=" +
|
|
std::to_string(results.size()));
|
|
debugLogWriteJsonLine(sanityEntry);
|
|
};
|
|
|
|
// Keyword search should return something if FTS is populated.
|
|
doSanitySearch("__sanity_keyword_the__", "the", "keyword");
|
|
|
|
// Hybrid search without symbol rank to avoid symbol-only decisions.
|
|
doSanitySearch("__sanity_hybrid_the__", "the", "hybrid");
|
|
|
|
// If BEIR corpus is used, easily-searchable token is doc id.
|
|
if (useBEIR && beirCorpus && !beirCorpus->dataset.documents.empty()) {
|
|
const auto& firstDocId = beirCorpus->dataset.documents.begin()->first;
|
|
doSanitySearch("__sanity_keyword_docid__", firstDocId, "keyword");
|
|
}
|
|
}
|
|
|
|
if (useBEIR) {
|
|
queries = beirCorpus->generateTestQueries();
|
|
} else {
|
|
queries = corpus->generateQueries(numQueries);
|
|
}
|
|
spdlog::info("Generated {} test queries", queries.size());
|
|
setupTimings.setupTotalMs = elapsedMsSince(setupStartedAt);
|
|
if (g_debugOut) {
|
|
DebugLogEntry timingEntry;
|
|
timingEntry.searchType = "benchmark_setup";
|
|
timingEntry.extraFields = {
|
|
{"event", "benchmark_setup_metrics"},
|
|
{"ingestion",
|
|
{{"cold_performed", setupTimings.coldIngestPerformed},
|
|
{"admission_ms", setupTimings.admissionMs},
|
|
{"storage_ready_ms", setupTimings.storageReadyMs},
|
|
{"pipeline_drain_ms", setupTimings.pipelineDrainMs},
|
|
{"searchability_ready_ms", setupTimings.searchabilityReadyMs},
|
|
{"enrichment_ready_ms", setupTimings.enrichmentReadyMs},
|
|
{"measurement_n", setupTimings.coldIngestPerformed ? 1 : 0}}},
|
|
{"vector_index",
|
|
{{"prepared", setupTimings.vectorIndexPrepared},
|
|
{"persisted", setupTimings.vectorIndexPersisted},
|
|
{"reusable_before", setupTimings.vectorIndexReusableBefore},
|
|
{"reusable_after", setupTimings.vectorIndexReusableAfter},
|
|
{"query_ready_after", setupTimings.vectorQueryReadyAfter},
|
|
{"prepare_ms", setupTimings.vectorIndexPrepareMs},
|
|
{"persist_ms", setupTimings.vectorIndexPersistMs},
|
|
{"reusable_probe_ms", setupTimings.vectorIndexReusableProbeMs},
|
|
{"reusable_probe_count", setupTimings.vectorIndexReusableProbeCount}}},
|
|
{"setup_total_ms", setupTimings.setupTotalMs},
|
|
{"dataset", datasetName},
|
|
{"corpus_size", corpusSize},
|
|
{"daemon_graph",
|
|
{{"required", g_requireDaemonNativeGraph},
|
|
{"ready", daemonGraphReady},
|
|
{"source", "post_ingest_queue_embedding_service"},
|
|
{"canonicalized", daemonGraphCanonicalized},
|
|
{"canonical_edges_generated", daemonGraphCanonicalEdgesGenerated},
|
|
{"canonical_topology_snapshot", daemonGraphCanonicalTopologySnapshot},
|
|
{"canonical_topology_fingerprint", daemonGraphCanonicalTopologyFingerprint},
|
|
{"semantic_documents_processed", daemonGraphState.semanticDocumentsProcessed},
|
|
{"semantic_edges_created", daemonGraphState.semanticEdgesCreated},
|
|
{"semantic_update_errors", daemonGraphState.semanticUpdateErrors},
|
|
{"topology_document_nodes", daemonGraphState.topologyDocumentNodes},
|
|
{"topology_semantic_edges", daemonGraphState.topologySemanticEdges},
|
|
{"topology_documents_with_neighbors",
|
|
daemonGraphState.topologyDocumentsWithNeighbors},
|
|
{"topology_isolated_documents", daemonGraphState.topologyIsolatedDocuments},
|
|
{"post_ingest_queued", daemonGraphState.postIngestQueued},
|
|
{"post_ingest_in_flight", daemonGraphState.postIngestInFlight},
|
|
{"embedding_queued", daemonGraphState.embeddingQueued},
|
|
{"embedding_in_flight", daemonGraphState.embeddingInFlight},
|
|
{"write_queued", daemonGraphState.writeQueued},
|
|
{"write_in_flight", daemonGraphState.writeInFlight}}},
|
|
};
|
|
debugLogWriteJsonLine(timingEntry);
|
|
}
|
|
}
|
|
|
|
void teardown() {
|
|
client.reset();
|
|
harness.reset();
|
|
corpus.reset();
|
|
|
|
g_debugRunContext.reset();
|
|
g_debugOut = nullptr;
|
|
g_debugFile.reset();
|
|
}
|
|
};
|
|
|
|
static std::unique_ptr<BenchFixture> g_fixture;
|
|
void SetupFixture() {
|
|
if (!g_fixture) {
|
|
g_fixture = std::make_unique<BenchFixture>();
|
|
g_fixture->setup();
|
|
}
|
|
}
|
|
void CleanupFixture() {
|
|
if (g_fixture) {
|
|
g_fixture->teardown();
|
|
g_fixture.reset();
|
|
}
|
|
}
|
|
|
|
static OptimizationRunResult runOptimizationCandidate(const OptimizationCandidate& candidate,
|
|
const std::optional<fs::path>& outputFile) {
|
|
OptimizationRunResult result;
|
|
result.candidate = candidate;
|
|
result.runId = makeRunId(candidate.name);
|
|
|
|
std::vector<EnvSetting> candidateOverrides = candidate.envOverrides;
|
|
const char* debugBaseEnv = std::getenv("YAMS_BENCH_DEBUG_FILE");
|
|
if (debugBaseEnv && std::strlen(debugBaseEnv) > 0) {
|
|
const fs::path basePath(debugBaseEnv);
|
|
const fs::path candidateDebugPath =
|
|
makeCandidateDebugPath(basePath, candidate.name, result.runId);
|
|
result.debugFile = candidateDebugPath.string();
|
|
candidateOverrides.push_back({"YAMS_BENCH_DEBUG_FILE", result.debugFile});
|
|
|
|
const bool hasExplicitTrace = hasOverrideKey(candidateOverrides, "YAMS_SEARCH_STAGE_TRACE");
|
|
if (!hasExplicitTrace && std::getenv("YAMS_SEARCH_STAGE_TRACE") == nullptr) {
|
|
candidateOverrides.push_back({"YAMS_SEARCH_STAGE_TRACE", "1"});
|
|
result.stageTraceEnabled = true;
|
|
} else {
|
|
result.stageTraceEnabled = envTruthy(std::getenv("YAMS_SEARCH_STAGE_TRACE"));
|
|
}
|
|
|
|
const int traceTopNDefault = parseIntEnvOrDefault("YAMS_BENCH_TRACE_TOP_N", 50, 1, 10000);
|
|
const int traceComponentDefault = parseIntEnvOrDefault(
|
|
"YAMS_BENCH_TRACE_COMPONENT_TOP_N", std::min(traceTopNDefault, 50), 1, 10000);
|
|
|
|
const bool hasExplicitTraceTop =
|
|
hasOverrideKey(candidateOverrides, "YAMS_SEARCH_STAGE_TRACE_TOP_N");
|
|
if (!hasExplicitTraceTop && std::getenv("YAMS_SEARCH_STAGE_TRACE_TOP_N") == nullptr) {
|
|
candidateOverrides.push_back(
|
|
{"YAMS_SEARCH_STAGE_TRACE_TOP_N", std::to_string(traceTopNDefault)});
|
|
}
|
|
|
|
const bool hasExplicitComponentTop =
|
|
hasOverrideKey(candidateOverrides, "YAMS_SEARCH_STAGE_TRACE_COMPONENT_TOP_N");
|
|
if (!hasExplicitComponentTop &&
|
|
std::getenv("YAMS_SEARCH_STAGE_TRACE_COMPONENT_TOP_N") == nullptr) {
|
|
candidateOverrides.push_back(
|
|
{"YAMS_SEARCH_STAGE_TRACE_COMPONENT_TOP_N", std::to_string(traceComponentDefault)});
|
|
}
|
|
} else {
|
|
result.stageTraceEnabled = envTruthy(std::getenv("YAMS_SEARCH_STAGE_TRACE"));
|
|
}
|
|
|
|
ScopedEnvOverrides scopedEnv(candidateOverrides);
|
|
result.traceTopN = parseIntEnvOrDefault("YAMS_SEARCH_STAGE_TRACE_TOP_N", 0, 0, 10000);
|
|
result.traceComponentTopN =
|
|
parseIntEnvOrDefault("YAMS_SEARCH_STAGE_TRACE_COMPONENT_TOP_N", 0, 0, 10000);
|
|
g_debugRunContext = DebugRunContext{
|
|
.runId = result.runId,
|
|
.candidate = result.candidate.name,
|
|
.dataset = "",
|
|
.corpusSize = 0,
|
|
.numQueries = 0,
|
|
.topK = 0,
|
|
.traceTopN = result.traceTopN,
|
|
.traceComponentTopN = result.traceComponentTopN,
|
|
.stageTraceEnabled = envTruthy(std::getenv("YAMS_SEARCH_STAGE_TRACE")),
|
|
.debugFile = result.debugFile,
|
|
};
|
|
CleanupFixture();
|
|
|
|
try {
|
|
SetupFixture();
|
|
if (!g_fixture || !g_fixture->client) {
|
|
throw std::runtime_error("Benchmark fixture did not initialize a daemon client");
|
|
}
|
|
|
|
auto status = benchRunSync(g_fixture->client->status(true), 5s);
|
|
if (status) {
|
|
result.tuningState = status.value().searchTuningState;
|
|
result.tuningReason = status.value().searchTuningReason;
|
|
}
|
|
result.searchEngine =
|
|
std::string(yams::vector::vectorSearchEngineName(benchmarkVectorSearchEngine()));
|
|
if (g_fixture && g_fixture->harness) {
|
|
const auto persisted = inspectPersistedHnswState(g_fixture->harness->dataDir());
|
|
result.vectorsDbBytes = persisted.vectorsDbBytes;
|
|
result.vectorsWalBytes = persisted.vectorsWalBytes;
|
|
result.persistedHnswNodes = persisted.nodeRows;
|
|
result.persistedHnswMetaRows = persisted.metaRows;
|
|
result.persistedVec0Tables = persisted.vec0Tables;
|
|
result.persistedVec0Rows = persisted.vec0Rows;
|
|
result.persistedPqCodes = persisted.pqCodeRows;
|
|
result.persistedPqMeta = persisted.pqMetaRows;
|
|
}
|
|
|
|
g_debugRunContext->dataset =
|
|
g_fixture->useBEIR ? g_fixture->beirDatasetName : g_fixture->datasetName;
|
|
g_debugRunContext->corpusSize = g_fixture->corpusSize;
|
|
g_debugRunContext->numQueries = static_cast<int>(g_fixture->queries.size());
|
|
g_debugRunContext->topK = g_fixture->topK;
|
|
g_debugRunContext->traceTopN =
|
|
parseIntEnvOrDefault("YAMS_SEARCH_STAGE_TRACE_TOP_N", result.traceTopN, 0, 10000);
|
|
g_debugRunContext->traceComponentTopN = parseIntEnvOrDefault(
|
|
"YAMS_SEARCH_STAGE_TRACE_COMPONENT_TOP_N", result.traceComponentTopN, 0, 10000);
|
|
g_debugRunContext->stageTraceEnabled = envTruthy(std::getenv("YAMS_SEARCH_STAGE_TRACE"));
|
|
result.traceTopN = g_debugRunContext->traceTopN;
|
|
result.traceComponentTopN = g_debugRunContext->traceComponentTopN;
|
|
result.stageTraceEnabled = g_debugRunContext->stageTraceEnabled;
|
|
|
|
const auto hybridStart = std::chrono::steady_clock::now();
|
|
result.hybridMetrics =
|
|
evaluateQueries(*g_fixture->client, g_fixture->benchCorpusDir, g_fixture->queries,
|
|
g_fixture->topK, "hybrid", &result.hybridDiagnostics);
|
|
const auto hybridEnd = std::chrono::steady_clock::now();
|
|
result.hybridEvalMs =
|
|
std::chrono::duration<double, std::milli>(hybridEnd - hybridStart).count();
|
|
|
|
const auto keywordStart = std::chrono::steady_clock::now();
|
|
result.keywordMetrics =
|
|
evaluateQueries(*g_fixture->client, g_fixture->benchCorpusDir, g_fixture->queries,
|
|
g_fixture->topK, "keyword", &result.keywordDiagnostics);
|
|
const auto keywordEnd = std::chrono::steady_clock::now();
|
|
result.keywordEvalMs =
|
|
std::chrono::duration<double, std::milli>(keywordEnd - keywordStart).count();
|
|
|
|
if (result.hybridMetrics.numQueries <= 0) {
|
|
throw std::runtime_error("No benchmark queries generated for optimization candidate");
|
|
}
|
|
|
|
const double avgHybridQueryMs =
|
|
result.hybridEvalMs / std::max(1, result.hybridMetrics.numQueries);
|
|
result.objectiveScore =
|
|
computeOptimizationObjective(result.hybridMetrics, result.keywordMetrics,
|
|
result.hybridDiagnostics, avgHybridQueryMs);
|
|
result.success = true;
|
|
} catch (const std::exception& e) {
|
|
result.success = false;
|
|
result.errorMessage = e.what();
|
|
}
|
|
|
|
g_debugRunContext.reset();
|
|
CleanupFixture();
|
|
|
|
if (outputFile.has_value()) {
|
|
appendOptimizationResultJson(*outputFile, result);
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
static int runOptimizationLoop() {
|
|
std::vector<OptimizationCandidate> candidates = defaultOptimizationCandidates();
|
|
const auto optProfile = parseOptimizationProfile(std::getenv("YAMS_BENCH_OPT_PROFILE"));
|
|
const bool hasExplicitCandidateFilter =
|
|
std::getenv("YAMS_BENCH_OPT_CANDIDATE") &&
|
|
std::strlen(std::getenv("YAMS_BENCH_OPT_CANDIDATE")) > 0;
|
|
|
|
if (!hasExplicitCandidateFilter) {
|
|
std::vector<OptimizationCandidate> filteredByProfile;
|
|
filteredByProfile.reserve(candidates.size());
|
|
for (const auto& candidate : candidates) {
|
|
const bool keep =
|
|
optProfile == OptimizationProfile::All ||
|
|
(optProfile == OptimizationProfile::EnginePolicy &&
|
|
candidate.enginePolicyCandidate) ||
|
|
(optProfile == OptimizationProfile::Diagnostics && candidate.diagnosticCandidate);
|
|
if (keep) {
|
|
filteredByProfile.push_back(candidate);
|
|
}
|
|
}
|
|
if (!filteredByProfile.empty()) {
|
|
spdlog::info("[OptLoop] No candidate filter set; using optimization profile '{}' with "
|
|
"{} candidates",
|
|
optimizationProfileName(optProfile), filteredByProfile.size());
|
|
candidates = std::move(filteredByProfile);
|
|
}
|
|
}
|
|
|
|
const auto selectedCandidates = parseCandidateFilter(std::getenv("YAMS_BENCH_OPT_CANDIDATE"));
|
|
if (!selectedCandidates.empty()) {
|
|
std::vector<OptimizationCandidate> filtered;
|
|
filtered.reserve(candidates.size());
|
|
for (const auto& candidate : candidates) {
|
|
if (selectedCandidates.count(candidate.name) > 0) {
|
|
filtered.push_back(candidate);
|
|
}
|
|
}
|
|
candidates = std::move(filtered);
|
|
if (candidates.empty()) {
|
|
std::cerr << "No optimization candidates matched YAMS_BENCH_OPT_CANDIDATE='"
|
|
<< std::getenv("YAMS_BENCH_OPT_CANDIDATE") << "'\n";
|
|
return 2;
|
|
}
|
|
}
|
|
|
|
bool pruneKnownBadCandidates = true;
|
|
if (const char* pruneEnv = std::getenv("YAMS_BENCH_OPT_PRUNE_KNOWN_BAD"); pruneEnv) {
|
|
pruneKnownBadCandidates = envTruthy(pruneEnv);
|
|
}
|
|
|
|
if (pruneKnownBadCandidates && selectedCandidates.empty()) {
|
|
static const std::unordered_set<std::string> kKnownBadCandidates = {
|
|
"mixed_precision_rrf_neighbor_t92_p80_k14",
|
|
"mixed_precision_rrf_neighbor_t92_p78_k16",
|
|
"mixed_precision_rrf_neighbor_t90_p84_k18",
|
|
"mixed_ablation_disable_graph_rerank",
|
|
};
|
|
|
|
const auto beforeCount = candidates.size();
|
|
candidates.erase(std::remove_if(candidates.begin(), candidates.end(),
|
|
[](const OptimizationCandidate& candidate) {
|
|
return kKnownBadCandidates.count(candidate.name) > 0;
|
|
}),
|
|
candidates.end());
|
|
|
|
const auto removed = beforeCount - candidates.size();
|
|
if (removed > 0) {
|
|
spdlog::info("[OptLoop] Pruned {} known low-value candidates (set "
|
|
"YAMS_BENCH_OPT_PRUNE_KNOWN_BAD=0 to disable)",
|
|
removed);
|
|
}
|
|
}
|
|
|
|
if (const char* maxCandidatesEnv = std::getenv("YAMS_BENCH_OPT_MAX_CANDIDATES")) {
|
|
try {
|
|
const auto parsed = std::max(1, std::stoi(maxCandidatesEnv));
|
|
if (static_cast<std::size_t>(parsed) < candidates.size()) {
|
|
candidates.resize(static_cast<std::size_t>(parsed));
|
|
}
|
|
} catch (...) {
|
|
spdlog::warn("[OptLoop] Invalid YAMS_BENCH_OPT_MAX_CANDIDATES='{}', using default {}",
|
|
maxCandidatesEnv, candidates.size());
|
|
}
|
|
}
|
|
|
|
std::optional<fs::path> outputFile;
|
|
if (const char* outputEnv = std::getenv("YAMS_BENCH_OPT_RESULTS_FILE");
|
|
outputEnv && std::strlen(outputEnv) > 0) {
|
|
outputFile = fs::path(outputEnv);
|
|
const bool appendMode = envTruthy(std::getenv("YAMS_BENCH_OPT_APPEND_RESULTS"));
|
|
std::ofstream probe(*outputFile, appendMode ? (std::ios::out | std::ios::app)
|
|
: (std::ios::out | std::ios::trunc));
|
|
if (!probe.is_open()) {
|
|
spdlog::warn("[OptLoop] Could not create optimization results file: {}",
|
|
outputFile->string());
|
|
outputFile.reset();
|
|
} else {
|
|
spdlog::info("[OptLoop] Writing optimization results to {} ({})", outputFile->string(),
|
|
appendMode ? "append" : "truncate");
|
|
}
|
|
}
|
|
|
|
std::cout << "\n" << std::string(78, '=') << "\n";
|
|
std::cout << " RETRIEVAL OPTIMIZATION LOOP (code/mixed first)\n";
|
|
std::cout << std::string(78, '=') << "\n";
|
|
std::cout << "Profile: " << optimizationProfileName(optProfile)
|
|
<< (hasExplicitCandidateFilter ? " (explicit candidate filter)" : "") << "\n";
|
|
|
|
std::vector<OptimizationRunResult> allResults;
|
|
allResults.reserve(candidates.size());
|
|
|
|
for (std::size_t i = 0; i < candidates.size(); ++i) {
|
|
const auto& candidate = candidates[i];
|
|
spdlog::info("[OptLoop] Candidate {}/{}: {} - {}", i + 1, candidates.size(), candidate.name,
|
|
candidate.description);
|
|
|
|
auto result = runOptimizationCandidate(candidate, outputFile);
|
|
allResults.push_back(result);
|
|
|
|
if (!result.success) {
|
|
std::cout << "\n [" << (i + 1) << "/" << candidates.size() << "] " << candidate.name
|
|
<< " -> FAILED: " << result.errorMessage << "\n";
|
|
continue;
|
|
}
|
|
|
|
const double avgMs = result.hybridEvalMs / std::max(1, result.hybridMetrics.numQueries);
|
|
const double mrrDelta = result.hybridMetrics.mrr - result.keywordMetrics.mrr;
|
|
std::cout << "\n [" << (i + 1) << "/" << candidates.size() << "] " << candidate.name
|
|
<< "\n";
|
|
std::cout << " objective=" << std::fixed << std::setprecision(4) << result.objectiveScore
|
|
<< " hybrid_mrr=" << result.hybridMetrics.mrr
|
|
<< " keyword_mrr=" << result.keywordMetrics.mrr << " mrr_delta=" << std::showpos
|
|
<< std::setprecision(4) << mrrDelta << std::noshowpos
|
|
<< " dup@k=" << std::setprecision(4) << result.hybridMetrics.duplicateRateAtK
|
|
<< " avg_hybrid_ms=" << std::setprecision(1) << avgMs << "\n";
|
|
const auto hybridDiag = queryDiagnosticsToJson(result.hybridDiagnostics);
|
|
std::cout << " trace_cov=" << std::setprecision(3)
|
|
<< hybridDiag["trace_coverage"].get<double>() << " sem_rescue_mean="
|
|
<< hybridDiag["semantic_rescue_rate"]["mean"].get<double>()
|
|
<< " ca_apply_rate=" << hybridDiag["compressed_ann_apply_rate"].get<double>()
|
|
<< " vec_only_below_mean="
|
|
<< hybridDiag["vector_only_below_threshold"]["mean"].get<double>()
|
|
<< " no_relevant_hit_rate="
|
|
<< hybridDiag["query_without_relevant_hit_rate"].get<double>()
|
|
<< " graph_apply_rate=" << hybridDiag["graph_rerank_apply_rate"].get<double>()
|
|
<< " miss_pre_fusion_rate="
|
|
<< hybridDiag["miss_missing_pre_fusion_rate"].get<double>() << "\n";
|
|
std::cout << " timing_tradeoffs=" << summarizeTimingTradeoffs(hybridDiag) << "\n";
|
|
std::cout << " ca_skip_reasons=" << hybridDiag["compressed_ann_skip_reasons"].dump()
|
|
<< "\n";
|
|
std::cout << " stage_tradeoffs=" << summarizeStageTradeoffs(hybridDiag) << "\n";
|
|
std::cout << " stage_quality=" << summarizeStageMetricGaps(hybridDiag) << "\n";
|
|
if (result.traceTopN > 0 || result.traceComponentTopN > 0) {
|
|
std::cout << " trace_top_n=" << result.traceTopN
|
|
<< " trace_component_top_n=" << result.traceComponentTopN << "\n";
|
|
}
|
|
std::cout << " tuning_state="
|
|
<< (result.tuningState.empty() ? "<unknown>" : result.tuningState) << "\n";
|
|
if (!result.debugFile.empty()) {
|
|
std::cout << " debug_file=" << result.debugFile << "\n";
|
|
}
|
|
}
|
|
|
|
std::vector<OptimizationRunResult> successful;
|
|
for (const auto& result : allResults) {
|
|
if (result.success) {
|
|
successful.push_back(result);
|
|
}
|
|
}
|
|
|
|
if (successful.empty()) {
|
|
std::cout << "\nNo successful optimization candidates completed.\n";
|
|
return 2;
|
|
}
|
|
|
|
std::sort(successful.begin(), successful.end(),
|
|
[](const OptimizationRunResult& a, const OptimizationRunResult& b) {
|
|
return a.objectiveScore > b.objectiveScore;
|
|
});
|
|
|
|
const auto& best = successful.front();
|
|
g_final_metrics = best.hybridMetrics;
|
|
g_keyword_metrics = best.keywordMetrics;
|
|
|
|
std::cout << "\n" << std::string(78, '-') << "\n";
|
|
std::cout << "Top candidates by objective\n";
|
|
std::cout << std::string(78, '-') << "\n";
|
|
for (std::size_t i = 0; i < successful.size(); ++i) {
|
|
const auto& result = successful[i];
|
|
const double avgMs = result.hybridEvalMs / std::max(1, result.hybridMetrics.numQueries);
|
|
const double mrrDelta = result.hybridMetrics.mrr - result.keywordMetrics.mrr;
|
|
std::cout << " " << (i + 1) << ". " << result.candidate.name
|
|
<< " objective=" << std::fixed << std::setprecision(4) << result.objectiveScore
|
|
<< " mrr=" << result.hybridMetrics.mrr
|
|
<< " ndcg=" << result.hybridMetrics.ndcgAtK << " mrr_delta=" << std::showpos
|
|
<< std::setprecision(4) << mrrDelta << std::noshowpos
|
|
<< " dup@k=" << std::setprecision(4) << result.hybridMetrics.duplicateRateAtK
|
|
<< " avg_ms=" << std::setprecision(1) << avgMs << "\n";
|
|
}
|
|
|
|
std::cout << "\nBest candidate: " << best.candidate.name << "\n";
|
|
std::cout << " " << best.candidate.description << "\n";
|
|
|
|
std::ostringstream envLine;
|
|
for (const auto& setting : best.candidate.envOverrides) {
|
|
if (setting.value.has_value()) {
|
|
envLine << setting.key << "=" << *setting.value << " ";
|
|
}
|
|
}
|
|
std::cout << " Recommended env overrides: " << envLine.str() << "\n";
|
|
|
|
if (outputFile.has_value()) {
|
|
std::cout << " Saved per-run JSONL to: " << outputFile->string() << "\n";
|
|
}
|
|
|
|
const char* datasetEnv = std::getenv("YAMS_BENCH_DATASET");
|
|
if (!(datasetEnv && std::strlen(datasetEnv) > 0)) {
|
|
std::cout << " Next: validate this winner on a harder dataset, e.g. "
|
|
<< "YAMS_BENCH_DATASET=synthetic-hard or YAMS_BENCH_DATASET=scifact ...\n";
|
|
}
|
|
|
|
std::cout << std::string(78, '=') << "\n";
|
|
return 0;
|
|
}
|
|
|
|
void BM_RetrievalQuality(benchmark::State& state) {
|
|
SetupFixture();
|
|
auto& fixture = *g_fixture;
|
|
RetrievalMetrics metrics;
|
|
for (auto _ : state) {
|
|
std::vector<double> warmupLatencyMs;
|
|
std::vector<TestQuery> failedWarmupQueries;
|
|
evaluateQueries(*fixture.client, fixture.benchCorpusDir, fixture.queries, fixture.topK,
|
|
"hybrid", nullptr, false, &warmupLatencyMs, &failedWarmupQueries);
|
|
const auto retriedWarmupQueries = failedWarmupQueries.size();
|
|
if (!failedWarmupQueries.empty()) {
|
|
std::vector<TestQuery> retryFailures;
|
|
evaluateQueries(*fixture.client, fixture.benchCorpusDir, failedWarmupQueries,
|
|
fixture.topK, "hybrid", nullptr, false, &warmupLatencyMs,
|
|
&retryFailures);
|
|
failedWarmupQueries = std::move(retryFailures);
|
|
}
|
|
if (g_debugOut) {
|
|
DebugLogEntry warmupEntry;
|
|
warmupEntry.searchType = "benchmark_warmup";
|
|
warmupEntry.extraFields = {
|
|
{"event", "query_warmup_summary"},
|
|
{"query_count", fixture.queries.size()},
|
|
{"completed_queries", warmupLatencyMs.size()},
|
|
{"first_query_latency_ms", warmupLatencyMs.empty() ? 0.0 : warmupLatencyMs.front()},
|
|
{"retried_queries", retriedWarmupQueries},
|
|
{"retry_failed_queries", failedWarmupQueries.size()},
|
|
};
|
|
debugLogWriteJsonLine(warmupEntry);
|
|
}
|
|
g_final_hybrid_diagnostics = QueryDiagnosticsSummary{};
|
|
metrics = evaluateQueries(*fixture.client, fixture.benchCorpusDir, fixture.queries,
|
|
fixture.topK, "hybrid", &g_final_hybrid_diagnostics);
|
|
benchmark::DoNotOptimize(metrics);
|
|
}
|
|
|
|
// Store metrics globally for post-teardown summary
|
|
g_final_metrics = metrics;
|
|
g_benchmark_search_engine =
|
|
std::string(yams::vector::vectorSearchEngineName(benchmarkVectorSearchEngine()));
|
|
if (fixture.harness) {
|
|
g_final_vector_index_state = inspectPersistedHnswState(fixture.harness->dataDir());
|
|
}
|
|
|
|
state.counters["MRR"] = metrics.mrr;
|
|
state.counters["Recall@K"] = metrics.recallAtK;
|
|
state.counters["Precision@K"] = metrics.precisionAtK;
|
|
state.counters["nDCG@K"] = metrics.ndcgAtK;
|
|
state.counters["MAP"] = metrics.map;
|
|
state.counters["num_queries"] = metrics.numQueries;
|
|
|
|
// Also evaluate keyword-only (FTS5) for component isolation
|
|
// This helps diagnose whether low MRR is from FTS5 or vector search
|
|
spdlog::info("=== Evaluating KEYWORD-ONLY (FTS5) for component isolation ===");
|
|
g_final_keyword_diagnostics = QueryDiagnosticsSummary{};
|
|
g_keyword_metrics = evaluateQueries(*fixture.client, fixture.benchCorpusDir, fixture.queries,
|
|
fixture.topK, "keyword", &g_final_keyword_diagnostics);
|
|
|
|
if (g_debugOut) {
|
|
DebugLogEntry summaryEntry;
|
|
summaryEntry.searchType = "benchmark_summary";
|
|
summaryEntry.extraFields = {
|
|
{"event", "hybrid_summary"},
|
|
{"hybrid_metrics",
|
|
{{"mrr", metrics.mrr},
|
|
{"recall_at_k", metrics.recallAtK},
|
|
{"precision_at_k", metrics.precisionAtK},
|
|
{"ndcg_at_k", metrics.ndcgAtK},
|
|
{"map", metrics.map}}},
|
|
{"hybrid_diag_summary", queryDiagnosticsToJson(g_final_hybrid_diagnostics)},
|
|
{"keyword_metrics",
|
|
{{"mrr", g_keyword_metrics.mrr},
|
|
{"recall_at_k", g_keyword_metrics.recallAtK},
|
|
{"precision_at_k", g_keyword_metrics.precisionAtK},
|
|
{"ndcg_at_k", g_keyword_metrics.ndcgAtK},
|
|
{"map", g_keyword_metrics.map}}},
|
|
{"keyword_diag_summary", queryDiagnosticsToJson(g_final_keyword_diagnostics)},
|
|
};
|
|
debugLogWriteJsonLine(summaryEntry);
|
|
}
|
|
|
|
state.counters["MRR_keyword"] = g_keyword_metrics.mrr;
|
|
state.counters["Recall_keyword"] = g_keyword_metrics.recallAtK;
|
|
|
|
if (benchEvaluateGrepBaseline()) {
|
|
spdlog::info("=== Evaluating GREP literal baseline for exact-match comparison ===");
|
|
g_grep_metrics = evaluateGrepQueries(*fixture.client, fixture.queries, fixture.topK);
|
|
state.counters["MRR_grep"] = g_grep_metrics.mrr;
|
|
state.counters["Recall_grep"] = g_grep_metrics.recallAtK;
|
|
} else {
|
|
spdlog::info("=== Skipping GREP literal baseline for this benchmark plan ===");
|
|
}
|
|
|
|
if (g_debugOut) {
|
|
const auto peakRssMb = yams::bench::processPeakRssMb();
|
|
DebugLogEntry resourceEntry;
|
|
resourceEntry.searchType = "benchmark_summary";
|
|
resourceEntry.extraFields = {
|
|
{"event", "process_resource_metrics"},
|
|
{"process",
|
|
{{"peak_rss_observed", peakRssMb.has_value()},
|
|
{"peak_rss_mb", peakRssMb.value_or(0.0)}}},
|
|
};
|
|
debugLogWriteJsonLine(resourceEntry);
|
|
}
|
|
}
|
|
// Retrieval quality is a long-running, end-to-end evaluation.
|
|
// Force a single iteration to avoid repeated full-corpus evaluations.
|
|
BENCHMARK(BM_RetrievalQuality)->Iterations(1);
|
|
|
|
int main(int argc, char** argv) {
|
|
consumeXplanConfigArguments(argc, argv);
|
|
if (envTruthy(std::getenv("YAMS_BENCH_OPT_LOOP"))) {
|
|
return runOptimizationLoop();
|
|
}
|
|
|
|
benchmark::Initialize(&argc, argv);
|
|
if (benchmark::ReportUnrecognizedArguments(argc, argv))
|
|
return 1;
|
|
benchmark::RunSpecifiedBenchmarks();
|
|
benchmark::Shutdown();
|
|
CleanupFixture();
|
|
|
|
// Re-print summary after all teardown logs
|
|
// This ensures results are visible even when daemon teardown is verbose
|
|
std::cout << "\n" << std::string(70, '=') << "\n";
|
|
std::cout << " RETRIEVAL QUALITY BENCHMARK RESULTS\n";
|
|
std::cout << std::string(70, '=') << "\n";
|
|
std::cout << std::fixed << std::setprecision(4);
|
|
|
|
// Hybrid search results
|
|
std::cout << "\n --- HYBRID SEARCH (text + vector) ---\n";
|
|
std::cout << " Search engine: " << std::setw(10)
|
|
<< g_benchmark_search_engine << "\n";
|
|
std::cout << " MRR (Mean Reciprocal Rank): " << std::setw(10) << g_final_metrics.mrr
|
|
<< "\n";
|
|
std::cout << " Recall@K: " << std::setw(10) << g_final_metrics.recallAtK
|
|
<< "\n";
|
|
std::cout << " Precision@K: " << std::setw(10)
|
|
<< g_final_metrics.precisionAtK << "\n";
|
|
std::cout << " nDCG@K (Normalized DCG): " << std::setw(10) << g_final_metrics.ndcgAtK
|
|
<< "\n";
|
|
std::cout << " MAP (Mean Average Precision): " << std::setw(10) << g_final_metrics.map
|
|
<< "\n";
|
|
const auto hybridDiag = queryDiagnosticsToJson(g_final_hybrid_diagnostics);
|
|
std::cout << " Stage quality gaps: " << summarizeStageMetricGaps(hybridDiag)
|
|
<< "\n";
|
|
std::cout << " vectors.db bytes: " << std::setw(10)
|
|
<< g_final_vector_index_state.vectorsDbBytes << "\n";
|
|
std::cout << " vectors.db-wal bytes: " << std::setw(10)
|
|
<< g_final_vector_index_state.vectorsWalBytes << "\n";
|
|
std::cout << " persisted_hnsw_nodes: " << std::setw(10)
|
|
<< g_final_vector_index_state.nodeRows << "\n";
|
|
std::cout << " persisted_hnsw_meta_rows: " << std::setw(10)
|
|
<< g_final_vector_index_state.metaRows << "\n";
|
|
std::cout << " persisted_vec0_tables: " << std::setw(10)
|
|
<< g_final_vector_index_state.vec0Tables << "\n";
|
|
std::cout << " persisted_vec0_rows: " << std::setw(10)
|
|
<< g_final_vector_index_state.vec0Rows << "\n";
|
|
std::cout << " persisted_simeon_pq_codes: " << std::setw(10)
|
|
<< g_final_vector_index_state.pqCodeRows << "\n";
|
|
std::cout << " persisted_simeon_pq_meta: " << std::setw(10)
|
|
<< g_final_vector_index_state.pqMetaRows << "\n";
|
|
|
|
// Keyword-only search results (FTS5 isolation)
|
|
std::cout << "\n --- KEYWORD SEARCH (FTS5 only) ---\n";
|
|
std::cout << " MRR (Mean Reciprocal Rank): " << std::setw(10) << g_keyword_metrics.mrr
|
|
<< "\n";
|
|
std::cout << " Recall@K: " << std::setw(10)
|
|
<< g_keyword_metrics.recallAtK << "\n";
|
|
std::cout << " Precision@K: " << std::setw(10)
|
|
<< g_keyword_metrics.precisionAtK << "\n";
|
|
std::cout << " nDCG@K (Normalized DCG): " << std::setw(10) << g_keyword_metrics.ndcgAtK
|
|
<< "\n";
|
|
std::cout << " MAP (Mean Average Precision): " << std::setw(10) << g_keyword_metrics.map
|
|
<< "\n";
|
|
|
|
// Grep baseline results
|
|
std::cout << "\n --- GREP BASELINE (literal exact match) ---\n";
|
|
std::cout << " MRR (Mean Reciprocal Rank): " << std::setw(10) << g_grep_metrics.mrr
|
|
<< "\n";
|
|
std::cout << " Recall@K: " << std::setw(10) << g_grep_metrics.recallAtK
|
|
<< "\n";
|
|
std::cout << " Precision@K: " << std::setw(10)
|
|
<< g_grep_metrics.precisionAtK << "\n";
|
|
std::cout << " nDCG@K (Normalized DCG): " << std::setw(10) << g_grep_metrics.ndcgAtK
|
|
<< "\n";
|
|
std::cout << " MAP (Mean Average Precision): " << std::setw(10) << g_grep_metrics.map
|
|
<< "\n";
|
|
|
|
// Component comparison summary
|
|
std::cout << "\n --- COMPONENT COMPARISON ---\n";
|
|
std::cout << " Number of queries evaluated: " << std::setw(10) << g_final_metrics.numQueries
|
|
<< "\n";
|
|
auto describeDelta = [](double delta, std::string_view positive, std::string_view negative) {
|
|
constexpr double kTieEpsilon = 1e-9;
|
|
if (std::abs(delta) <= kTieEpsilon) {
|
|
return std::string(" (tied)");
|
|
}
|
|
return std::string(delta > 0 ? positive : negative);
|
|
};
|
|
double keywordMrrDelta = g_final_metrics.mrr - g_keyword_metrics.mrr;
|
|
double grepMrrDelta = g_final_metrics.mrr - g_grep_metrics.mrr;
|
|
std::cout << " MRR delta (hybrid - keyword): " << std::setw(10) << keywordMrrDelta
|
|
<< describeDelta(keywordMrrDelta, " (hybrid better)", " (keyword better)") << "\n";
|
|
std::cout << " MRR delta (hybrid - grep): " << std::setw(10) << grepMrrDelta
|
|
<< describeDelta(grepMrrDelta, " (hybrid better)", " (grep better)") << "\n";
|
|
std::cout << std::string(70, '=') << "\n";
|
|
|
|
return 0;
|
|
}
|