Skip to content

feature(fts): add the search phase to the full-text benchmark - #15719

Draft
m-szymon wants to merge 12 commits into
scylladb:masterfrom
m-szymon:pr/fts-search
Draft

feature(fts): add the search phase to the full-text benchmark#15719
m-szymon wants to merge 12 commits into
scylladb:masterfrom
m-szymon:pr/fts-search

Conversation

@m-szymon

@m-szymon m-szymon commented Aug 6, 2026

Copy link
Copy Markdown
Contributor

Add the search half of the full-text benchmark. After a step has loaded its
shards and built its index, run the query sets the plan lists against that index
and report their latency to Argus. Index build time and throughput are #15697.
With this the docker-backend test is complete. AWS runs follow.

Stacked on #15697.

Four commits. The two framework changes come first, because the query phase
cannot run without either.

'latency_calculator_decorator' gets two opt-in parameters. 'extra_columns' lets a
caller add columns to the latency table and fill them per row, which is how the
query configuration and an example query are reported next to the numbers instead
of being packed into the row label. 'error_thresholds' overrides the test-wide
'latency_decorator_error_thresholds' for one call, because here the expected
latency comes from the plan and differs per query set. Both are inert for every
existing caller.

The hdr-tag fix is one word, and it is not cosmetic. latte tags its histograms
after the rune function, so this test produces 'fn--search'.
'_get_workload_type_by_hdr_tag' has no keyword for it, 'get_latte_operation_type'
has no 'search' verb either, so the WRITE/READ fallback does not apply and the
lookup raises. The raise happens in the streaming exporter thread and surfaces
outside the try that wraps the command, so the latte invocation fails. Adding
'search' to the read verbs is the whole fix.

Then the query phase in the shared flow, and last the full-text plan and
descriptor that turn it on.

Every query set states an expected P99, on the entry or the dataset's 'defaults'.
That one value picks the Argus table, becomes its P99 validation rule and is
written into its description. It is not a column, because it is the same on every
row of the table it selects.

Row labels carry dataset, record count, step number and query set. Everything that
varies per row is a column. Two entries in one step can therefore collide on a
single row, and Argus would keep whichever was submitted last, so a colliding
label is suffixed with the query configuration, and entries identical in every
parameter are numbered as well. The suffix names every parameter, not only the
ones that differ, so that adding an entry cannot rename rows that already have
Argus history.

Follow-up: #15697
Closes: VECTOR-784

Testing

  • [ ]

PR pre-checks (self review)

  • I added the relevant backport labels
  • I didn't leave commented-out/debugging code

Reminders

  • Add New configuration option and document them (in sdcm/sct_config.py)
  • Add unit tests to cover my changes (under unit-test/ folder)
  • Update the Readme/doc folder relevant to this change (if needed)

@scylladb-promoter

scylladb-promoter commented Aug 6, 2026

Copy link
Copy Markdown
Collaborator

✅ Test Summary: PASSED

✅ Precommit: PASSED

Total Passed Failed Skipped
171 27 0 144

✅ Tests: PASSED

Total Passed Failed Errors Skipped
4930 4899 0 0 31

Full build log

@m-szymon m-szymon added the backport/none Backport is not required label Aug 6, 2026
@m-szymon m-szymon changed the title Pr/fts search feature(fts): add the search phase to the full-text benchmark Aug 6, 2026
@scylladbbot

Copy link
Copy Markdown

@m-szymon new branch manager-3.12 was added, please add backport label if needed

@dimakr

dimakr commented Aug 13, 2026

Copy link
Copy Markdown
Contributor

@claude review

@claude

claude Bot commented Aug 13, 2026

Copy link
Copy Markdown

Claude finished @dimakr's task in 6m 23s —— View job


Code Review: feature(fts): add the search phase to the full-text benchmark

  • Gather context and understand the PR
  • Review framework changes (decorators.py, hdrhistogram.py, argus_results.py)
  • Review the search performance flow (search_perf_test.py)
  • Review latte/vector store additions (vector_store_client.py, vector_store_index.py)
  • Review test coverage (unit and integration tests)
  • Review configuration and documentation
  • Compile and post final review

Summary

The PR is well-structured and implements a clean search performance flow. The framework additions (extra_columns, error_thresholds in the decorator, search in HDR tag classification) are backward-compatible and well-tested. Test coverage is notably thorough — the label disambiguation stability invariants in particular are well thought through. A few issues below, one of which (Prometheus gauge naming) may be worth addressing before merge.


Issues

1. get_latte_operation_type not updated — search Prometheus metrics filed under "user" Fix this →

The PR description mentions both issues: _get_workload_type_by_hdr_tag (crash, fixed) and get_latte_operation_type (no search verb). The crash is fixed; the operation type gap remains.

# sdcm/stress/latte_thread.py:78 — "search" not in the read verbs
elif re.findall(r"(?:^|_)(read|select|get|count)(?:_|$)", fn):
    read_found = True
else:
    return "user"  # ← "-f search" falls through to "user"

Consequence: LatteHDRExporter is initialised with stress_operation="user" for the search command, so Prometheus gauges for search latency appear under latte_hdr_user_* rather than latte_hdr_read_*. Doesn't break the HDR pipeline (the tag itself is now correctly classified to READ), but monitoring dashboards that key on the gauge name will not see the search results under the expected series.

2. Missing file check before staging qrels file

In search_perf_test.py:651:

if qrels_file:
    files_to_stage.append(
        (os.path.join(local_ds_dir, qrels_file), os.path.join(remote_ds_dir, qrels_file))
    )

When qrels: true is set in the plan, the qrels file path is staged directly. If the local dataset directory doesn't contain the expected qrels_{qset}.tsv, the error surfaces as a staging failure rather than an early, actionable message. _first_query_example already handles a missing queries_*.tsv with a FileNotFoundError catch; a similar early check here (or at validate_plan_queries time) would give a cleaner failure for a mistyped qrels: true entry.

3. No length cap on query_example stored in Argus

In search_perf_test.py:211-229, _first_query_example returns the raw text of the first TSV row. For BM25 benchmarks this is typically a short phrase, but for natural-language queries nothing prevents a very long string from being stored verbatim in the Argus TEXT column. A [:256] truncation on the returned value would be consistent with how other ResultType.TEXT cells are used.

4. _parse_shard_spec applied after _checked_data_file when using shard files

_load_step_shards (from the base PR) calls _checked_data_file after building the shard path from shard_suffix.format(sid):

shard_files = ["shards/" + shard_suffix.format(sid) for sid in shard_ids]
shard_files = [_checked_data_file(shard_file, "records file") for shard_file in shard_files]

shard_suffix is taken directly from the plan (step.get("shard_suffix", workload.default_shard_suffix)) without first passing it through _checked_data_file, so a malicious shard_suffix could inject shell metacharacters before the substitution. Since the plan is a committed YAML file and not user-supplied data this is low risk, but worth noting.


Minor observations (no action required)

  • _format_ms for zero: _format_ms(0.0) produces "0", giving a cycle name like fts_search_p99_0ms. An expected latency of 0 ms isn't useful as a plan value, but nothing explicitly rejects it.
  • SAFE_NAME_RE allows digit-leading names like 10M_docs. These are safe for the current index naming scheme (always prefixed by {index_prefix}_), and the local_config.yaml doesn't use them, but a future plan author may not expect CQL's no-leading-digit rule for bare identifiers.
  • workload_type variable reuse after the HDR loop in argus_results.py:413 is a pre-existing issue (the summary table uses the last iteration's workload_type), not introduced here.
  • The noqa: PLR0915 / PLR0912 / PLR0914 suppressions on latency_calculator_decorator are pre-existing. The function is big but the additions stay within the existing structure.

Strengths

  • Backward-compatible framework additions: extra_columns/error_thresholds are guarded with if extra_columns: and if error_thresholds is not None, so every existing caller is unaffected. The test_no_extra_columns_means_no_extra_columns_or_cells test in particular is a good regression guard against class-attribute mutation.
  • Input validation: plan-supplied names that reach a CQL identifier, a shell command, or a local path each go through a dedicated validator (_checked_name, _checked_data_file, _checked_duration, _checked_int). Injection tests in test_validate_plan_queries_rejects_a_bad_entry cover the obvious attack patterns.
  • Label stability guarantees are tested: test_inserting_a_query_does_not_rename_the_other_rows and test_reordering_queries_does_not_rename_their_rows verify the key invariant that Argus history is not lost when the plan is edited.
  • hdrhistogram.py fix is minimal and correct: one word, and the PR description explains exactly why (streaming exporter thread, raise outside the command's try, fn--search tag unclassified).
  • parse_full_scan_seconds tests use verbatim lines from real runs for both the docker and aws log-shipper formats — a good hedge against the regex drifting away from production output.

…ient

'get_index_status' raises on the 404 vector-store returns for an index it has not
discovered yet, which makes "not there yet" indistinguishable from a real error
at the call site. 'get_index_status_or_none' returns None for that one case and
re-raises everything else, so a caller can poll an index across the discovery
gap in either direction -- waiting for a fresh index to appear, or for a dropped
one to disappear again.

'wait_for_index_absent' is that second direction, which every caller recreating
an index needs: vector-store forgets a dropped index asynchronously, so without
the wait a 'CREATE CUSTOM INDEX' can race a drop ScyllaDB already considers done.
A failed request counts as "still there", since mistaking a network hiccup for a
completed drop would let the next build start too early.

'wait_for_ready' grows a 'required_statuses' argument. Its default still accepts
BOOTSTRAPPING, which is what cluster readiness means here -- the service answers
and is catching up -- but a caller that measures index build time or query
latency needs the node actually serving first, and can now ask for that.
Vector-store logs both ends of an index's initial table scan ("starting/finished
full scan on <keyspace>.<index>") with a microsecond tracing timestamp. That scan
is the index build, so those two lines are the most direct and most precise
measurement of it available -- latte's clock has whole-second resolution, and
vector-store's index-status endpoint serves a snapshot refreshed on a ~1s ticker,
so polling either observes each edge late.

'wait_for_index_build_seconds' reads them off the node's log once the build is
over, retrying for a bounded window because the lines reach the runner
asynchronously, and returning None rather than raising when they never arrive:
the index is queryable either way, so a missing measurement should not fail a
run. Two log shapes are handled, since 'BaseNode.system_log' resolves to the raw
tracing line on docker and to the same line behind a log-shipper prefix on aws.

'index_key' folds the case the way ScyllaDB folds unquoted identifiers, which is
how vector-store knows an index -- 'fts_idx_10M_20tok_0' is 'fts_idx_10m_20tok_0'
downstream, and matching without folding silently never matches.

Argus reporting lives here too: 'index_build_columns' and
'send_index_build_result' report build time, how much was indexed and the
throughput that follows from the two. The count column is named by the caller in
its own vocabulary (documents, vectors), and the table itself is declared by the
test that owns it, so its name and description stay next to that test.

None of this is specific to full-text search: it applies to any index
vector-store serves, because the measurement comes from vector-store's own log
rather than from whatever issued the DDL.
…tore

The two helpers of the previous commits are unit tested against mocks and captured
log lines, which leaves exactly the parts that can rot unchecked: whether a
missing index really answers 404, whether a dropped one is really forgotten, and
whether vector-store still writes its "starting/finished full scan" lines in the
shape the parser expects and still reaches the runner's copy of the log.

Two integration tests answer those against a live ScyllaDB and vector-store, using
the docker fixtures the existing vector-store tests already use. Only the
deterministic states are asserted -- before an index exists and after it is
dropped -- rather than racing vector-store's discovery window, which would be
flaky.

They run against a vector index, because that is what the released images support
and it keeps the tests runnable anywhere the other vector-store integration tests
are. Neither helper knows which kind of index it is looking at: the full-text
tests call exactly these two, and this is also the evidence for that claim.
'fts.rn' drives the BM25 workload: the schema and index DDL, the document
load, the index build (which waits for the index to become queryable and
reports the build time and the indexing throughput as latte custom metrics)
and the search phase. 'metrics.rn' holds the IR accuracy metrics -- recall,
precision, NDCG and friends -- the search phase reports against the qrels.

Both are copies of the scripts in scylladb/vector-store's
latte/full-text-search/. They have to live here because 'LatteStressThread'
resolves '.rn' paths inside the SCT tree and copies the containing directory
into the loader container, so a script cannot be loaded from another repo.
Keep them a mirror: changes belong upstream first, and the custom metrics stay
declared even where SCT reads none of them. That is also why the search
function and metrics.rn arrive before anything calls them -- trimming the
scripts to what SCT uses today would only create a divergence to undo.
The orchestration a search benchmark needs is the same whichever index is under
test, because vector-store serves both a full-text and a vector index and latte
drives both: a YAML plan names datasets, a dataset is a sequence of steps, and a
step loads more shards on top of the previous ones and rebuilds the index -- so
one dataset yields results at several corpus sizes. Index build time comes from
vector-store's log and every row is streamed to Argus as it is produced.

What differs between workloads is the rune script, the vocabulary results are
reported in, and the names they are reported under. All of it is declared in a
'SearchWorkload' that a subclass points 'WORKLOAD' at, leaving the subclass with
only its Argus table and its 'test_*' entry point.

'LatteScriptParams' is part of that descriptor because the rune scripts are
mirrored from scylladb/vector-store rather than owned here, and each keeps its
own vocabulary -- the full-text one talks about documents. Mapping those names
per workload means adding a workload never requires renaming a parameter in a
script that lives in another repository.

Getting a shard into the loader container uses the only route the stress
framework offers out of the box: 'LatteStressThread.build_stress_cmd' copies
every top-level file of the rune script's directory in, so the shard is copied
there for the run that loads it and removed afterwards. That is enough for a
corpus small enough to live in the repo and no further -- it routes the data
through the SCT source tree and ships it into every latte invocation of the run
-- so two commits from here it is replaced by a copy straight into the container.
Keeping it this way first is deliberate: the framework change that replaces it is
easier to judge against code that shows what it costs.

The query phase is deliberately absent too. Running a step's query sets and
reporting their latency needs the Argus latency-table extras and the hdr-tag
workload detection fix, so it lands in its own PR on top of this one. What is
here is the half that stands alone: corpus load, index build, and build time and
indexing throughput per step.

Every phase is a single latte command -- one schema change, one shard, one index
build -- so each runs on one loader: 'run_latte_thread' otherwise fans its thread
out to every loader in the set, and a two-loader cluster would load each shard
twice and report a record count, and the indexing throughput derived from it, off
by that factor.

Dataset names have to be distinct within a plan, and the flow says so rather than
finding out later: an index is named after its dataset and step and its build time
is read from the first matching 'full scan' pair in vector-store's log, so a
repeated name would re-report the first dataset's build times against the second
one's record counts.

A dataset whose local directory does not exist is reported as that, because the
corpora are generated rather than tracked -- a fresh clone that skipped the
generator would otherwise fail on an open() of a shard inside a directory the flow
had just created empty.

The first consumer is the next commit, which adds the full-text search test as a
descriptor plus an entry point.
Adds 'fts_test.FtsSearchTest.test_fts_search': load documents into ScyllaDB with
latte, build a 'fulltext_index' on them, and report how long that build took and
at what throughput to Argus.

The test itself is a 'SearchWorkload' descriptor and an entry point, because the
orchestration is the shared search flow of the previous commit. What is declared
here is what makes the benchmark full-text: fts.rn, the '-P' names that script
chose, "docs" as the unit results are counted in, and the Argus names the history
is kept under -- the "FTS Index Build Time" table and the 'fts_idx_*' index names.

What to run is a YAML plan, not a pile of SCT params: 'search_test_config' names
it and has no default, because which datasets and shards to load is the definition
of the test rather than something to fall back on. The param is the shared flow's
rather than this test's -- a vector-search test names its own plan through the same
one -- and it takes a path relative to the SCT root, like every other file a test
case points at. 'local_config.yaml' is the tiny plan the docker run uses, and it
exercises every branch of the step loop -- a single shard, a shard range, an
unsharded corpus and a rebuild that loads nothing.

'generate_local_dataset.py' produces the two corpora that plan names, so a
docker-backend run needs no S3 access. It lives with the plan rather than with the
mirrored rune scripts because the two are one unit: the plan names 'local_tiny'
and 'local_smoke', the generator writes exactly those, and the docs tell you to
run it once. What it writes is gitignored because it is generated, not because it
is temporary -- a run reads the corpora in place and leaves them alone.

'fts-search-test-docker.yaml' runs the whole flow on the docker backend against
those corpora, which is how the orchestration is verified without an AWS cluster.
docs/fts-search-test.md is the operator guide: how to run it, what to check
afterwards, and the plan format. It stays a runbook -- the reasoning behind the
measurement path is here rather than in it.

The query phase follows in its own PR, adding a latency row per query
configuration. The entry point stays as it is when it lands.

The unit tests check the descriptor against the two things it names and cannot
control: fts.rn, which is mirrored from scylladb/vector-store, and the Argus table.
A rename in either would otherwise surface as a latte run silently ignoring every
'-P' it was given, or as a new empty history in Argus.
Until now the search flow got a shard into the loader container by copying it next
to the rune script, because 'LatteStressThread.build_stress_cmd' ships every
top-level file of that directory and nothing else offered a way in. Three things
that cost:

  - the corpus travelled through the SCT source tree, so a real shard would write
    hundreds of megabytes into a git working copy;
  - 'build_stress_cmd' ships *every* top-level file of that directory, so the
    staged shard went into the container of every latte invocation of the run --
    schema, index build, index drop -- not just the load that needed it;
  - a shard had to exist in the tree for the whole run, which rules out fetching
    one, loading it and deleting it before the next. That is the only way a corpus
    too big for the runner's disk can be loaded at all, and what the S3 support
    later builds on.

'LatteStressThread' grows an 'extra_files_to_stage' list of '(local, remote)' pairs
and copies them in 'build_stress_cmd'. That is the only point at which it can: the
'RemoteDocker' command runner is created and destroyed per latte invocation, so
nothing staged earlier would survive to the run, and nothing needs cleaning up
afterwards because the container is the cleanup context.

It belongs on the thread rather than on a subclass in the test. The distinction the
thread already draws is what a script needs on every invocation -- shipped with the
script's directory -- against what only this invocation needs, which had no answer
until now; that is a gap in the stress thread, not something specific to a search
benchmark. 'ClusterTester.run_latte_thread' passes the list through, so a caller
keeps the helper's timeout resolution and its loader fan-out. The load phase in
particular hands latte a cycle count as '-d', which the helper rewrites into a
duration when '--stress-duration' is set, so it has to keep passing an explicit
'duration' to stay out of that branch: easy to miss if a caller reimplemented this,
and silently wrong when missed.

Datasets land under the workload's 'remote_root' inside the container, which is a
new descriptor field.

A unit test can only assert that 'send_files' was called for what it was given.
Whether the file lands where the rune script looks for it depends on the container's
working directory, the '-P' names the script declares and the path the flow
interpolates -- so the integration test stages a corpus, loads it against a live
ScyllaDB and reads the rows back over CQL. Its negative half asserts that an
unstaged corpus loads nothing, which is what keeps the positive half honest.
The per-dataset cycle -- create the schema, load a shard, build the index, read
how long that took, drop it, load the next shard and rebuild -- has until now been
verifiable only by running the whole test on the docker backend and reading the
result tables afterwards. That takes three minutes, a hand-driven hydra
invocation, and a person to interpret it.

This runs the same cycle as a test, in about fifty seconds, and asserts what a run
reports: one build row per step with cumulative record counts and a positive build
time, the rows actually in ScyllaDB, and the last index gone once the dataset is
done.

It runs the flow's own methods rather than a re-implementation of them. Two seams
are replaced: '_run_latte', which would otherwise need ClusterTester's loader
provisioning, and '_report_build_metrics', so the rows can be asserted instead of
submitted to Argus. Everything between those -- the step loop, the '-P' mapping,
the index naming, reading the build time out of vector-store's log, waiting for
the drop to be noticed -- is the code that ships.

It needs a ScyllaDB with a full-text index and a vector-store that serves one, and
neither is released yet, so it skips unless the images are present locally. The
check is a collection-time 'skipif' rather than a 'skip' in the body, because the
fixture that starts the container would otherwise try to pull a missing image
during setup, and that is an error rather than a skip. Both images are overridable
by environment variable, so this keeps working as the released ones catch up.
… table

Two opt-in parameters on 'latency_calculator_decorator', both threaded into
'send_result_to_argus' and both inert for every existing caller:

'extra_columns' appends 'ColumnMetadata' to the result table's schema, and the
decorated function may then return an 'extra_values' dict written once per row
alongside the usual latency cells. This is for per-row metadata that belongs
next to the numbers rather than folded into an ever-longer row label -- the FTS
test uses it to report the query configuration and an example query. The columns
are appended to a copy, since 'Meta.Columns' is a class attribute shared by every
test that reports a latency table.

'error_thresholds' overrides the 'latency_decorator_error_thresholds' test param
for one call, for a caller whose expected latency is a property of what it is
running rather than of the test as a whole -- again FTS, where the expectation
comes from the dataset plan and differs per query set.

'extra_values' is only emitted for a result with a single HDR tag, the same
branch that writes 'duration' and 'start time': with several tags there is one
row per tag and no per-tag value to write.
'_HdrRangeHistogramBuilder' infers the workload from the hdr tag name and had
no keyword matching a search operation. For a latte 'search' function that is
not a cosmetic mislabel, it is fatal: 'get_latte_operation_type()' has no
'search' verb either, so it classifies 'latte run -f search' as 'user', which
means the 'stress_operation in ("WRITE", "READ")' fallback does not apply and
'_get_workload_type_by_hdr_tag()' raises ValueError on the 'fn--search' tag.

That raise lands in the streaming 'LatteHDRExporter' thread, which does not
guard 'split_line()', and 'FileFollowerThread.__exit__' re-raises it through
'future.result()' outside the 'try' that 'LatteStressThread._run_stress' wraps
around the command -- so the latte invocation fails rather than just losing a
Grafana label. Only the 'latency_calculator_decorator' path is safe, because
there 'stress_operation' is the caller's 'workload_type'.

Add 'search' next to the other read verbs.
Completes the flow: after a step has loaded its shards and built its index, run
the query sets the plan lists against that index and report their latency to
Argus. The two framework changes this needs are the previous two commits -- the
Argus latency-table extras and the hdr-tag workload detection.

Every query entry must resolve an 'expected_p99_read_ms', on the entry itself or
the dataset's 'defaults'. That single value selects the Argus table, becomes its
P99 validation rule and is stated in its description, so the expectation lives in
the plan rather than half in a label and half in an SCT default nobody set. It is
deliberately not a column: it is a property of the table, identical on every row.

What does vary per row is reported as columns -- limit, concurrency, rate and an
example query -- which is why row labels carry only dataset, record count, step
ordinal and query set. The step ordinal is there for the same reason the build
row already carries one: the record count does not identify a step, because a
step with an empty 'shards' list loads nothing and so repeats its predecessor's
count. Without it, a query set run again on a rebuild would land on the row the
earlier step reported under. It also lines a query row up with its build row.

That leaves collisions within one step: two entries for the same set differing
only in parameters that are now columns would land on one row coordinate, and
Argus would keep whichever value was submitted last. 'row_labels_for_step'
suffixes a colliding label with the query configuration, and additionally numbers
the entries that are identical in every parameter. The suffix names every
parameter rather than only those that differ within the collision group: the
shorter form depends on the whole group, so adding one entry varying a parameter
the others agreed on would rewrite the label of every one of them and lose their
Argus history. As written it depends on nothing but the entry, so reordering
never renames anything and adding an entry only affects rows sharing its label.

The plan's query entries are resolved once, up front, rather than at the point
each query runs. Every check here can raise -- an unknown parameter type, a
duration latte cannot parse, a missing expected latency -- and by the time a
query runs, its step has already loaded its shards and built its index, which on
a real corpus is tens of minutes spent to report a typo. Nothing in the pass
touches the cluster or the dataset files.

limit, concurrency, rate and duration are checked rather than merely read,
because all four are interpolated into a latte command line -- the same reason
'_checked_name' exists for the plan's names. A duration must be latte's time form
specifically: 'get_timeout_from_stress_cmd' parses no other, and one it cannot
read silently gives the search phase the whole 'test_duration' as its timeout.

The workload descriptor grows the four rune parameters and the two Argus names
the query phase needs. Its fields are required, so fts_test.py's instance is
updated here rather than in the next commit -- without it the module would not
import.
Turns on the query phase for the full-text test: the docker plan now names query
sets, so a run reports a latency row per query configuration alongside the index
build rows.

'local_config.yaml' exercises the parts of the reporting that are easy to get
wrong, and covers both ways two query entries can land on one Argus row.
'local_tiny' asks for a rate-limited query set on its first step, and on its
second repeats one query configuration verbatim -- the two entries resolve to the
same set, table and parameters within a single step, so they must come out with
' run scylladb#1' and ' run scylladb#2' appended to their configuration suffix. 'local_smoke'
covers the other way: it carries qrels, which turns on the rune script's
relevance metrics, then a step that neither loads nor queries, then a second
no-load rebuild repeating the first step's query set. Nothing is loaded between
the three, so all of them report the same document count and only the 'step #N'
component of the row label keeps the last one off the first one's row.

The test case documents why 'use_hdrhistogram: true' is required now:
'latency_calculator_decorator' returns early without it, so the latency tables
would silently not appear.

docs/fts-search-test.md gains what a plan author needs -- that every query set has
to state an expected P99 and what that value selects, and how repeated query
configurations are told apart -- plus the latency tables in the post-run checklist.
It stays a runbook: the reasoning behind the label scheme is in this message, not
in it.

It also loses the trailing blank line it was added with, which 'end-of-file-fixer'
rejects. The line predates this branch, but the hook runs on every file a PR
touches, so it would fail CI here.
@m-szymon

Copy link
Copy Markdown
Contributor Author

Claude finished @dimakr's task

Rather minor finding and fixed

This is partial PR - a follow-up of #15697 and the full feature is in #15769.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area/vector backport/none Backport is not required P3 Medium Priority symptom/quality

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants