Skip to content

fix(template): make the tuned hybrid query profile actually rank#11

Merged
afoucret merged 4 commits into
mistralai:mainfrom
alexandretamborrino:fix/hybrid-search-ranking-profile
Jun 15, 2026
Merged

fix(template): make the tuned hybrid query profile actually rank#11
afoucret merged 4 commits into
mistralai:mainfrom
alexandretamborrino:fix/hybrid-search-ranking-profile

Conversation

@alexandretamborrino

@alexandretamborrino alexandretamborrino commented Jun 12, 2026

Copy link
Copy Markdown
Contributor

Problem

The starter's hybrid-search query profile was defined with carefully tuned BM25 + vector weights, but those weights never actually affected ranking:

  1. ranking.profile was set to root. In the generated .sd, root declares all the ranking functions but has no first-phase/second-phase expression — it's only a base profile to inherit from. Selecting it makes Vespa fall back to default nativeRank, so the query(*_weight) values had no formula to plug into.

  2. The default search path never used hybrid-search anyway. search.py called get_search_index(...) with no profile, which resolves to default_query_profile_name = hybrid-profile — the toolkit's auto-generated profile, whose weights all default to 0. So even after fixing (1), the tuned profile was dead code.

Weights can only be supplied via a query profile (the high-level VectorSearchQuery API has no per-query weight field), and the auto-generated default profile can't carry tuned weights (it's generated with 0 defaults, and the duplicate-name guard blocks overriding it with a same-named custom profile). So the tuned profile has to be selected explicitly at the entrypoint.

Changes

Ranking fix (migrations/001_create_index_schema.py.jinja):

  • ranking.profile: rootweighted-rank2 (the profile that actually consumes these weights: bm25_chunks_max/avg in first-phase; match_chunks + chunks_embeddings_avg_cosine_similarity in second-phase).
  • Comment at default_query_profile_name noting the auto profile ships with zero weights.

Wire it up so the tuned profile is actually used:

  • search.py: new --query-profile arg (default hybrid-search), passed to get_search_index(...) and printed in the run header.
  • Makefile: optional query_profile= passthrough (make search query="..." query_profile=hybrid-profile).
  • README: document the default profile and how to add/select a custom one.

Notes:

  • bm25_title_weight is kept as an intentional placeholder. It's currently a no-op (the v1 schema has no bm25-enabled title field, so no bm25_title rank feature), but harmless.
  • chunks_embeddings_avg_distance_weight is left at its 0 default — cosine similarity is the chosen semantic signal.

Verification

Rendered the committed template with copier (collection_name=exampledocs) and ran the full README workflow against a fresh, isolated Vespa (alternate ports + container):

  • installdeps, setup-vespa (migration "activated": true, Application ready), ingest (text + PDF/OCR, 62 chunks), search — all green.
  • Default search reports Query profile: hybrid-search; query_profile=hybrid-profile override works.
  • Resolved hybrid-search via migrate_to_package: ranking.profile = weighted-rank2 with the tuned weights present.

Note: on the 2-document sample corpus, hybrid-search and hybrid-profile return identical output — the displayed score is the client-side best_chunks value and all matches are returned regardless of document ranking. The weight difference only manifests on a corpus large enough that phase ranking decides which docs reach the returned set.

The migration file's "do not edit once deployed" header refers to generated projects; this changes only the template source, so existing deployments are unaffected.

🤖 Generated with Claude Code

alexandretamborrino and others added 2 commits June 12, 2026 10:58
The hybrid-search query profile set ranking.profile to "root", but the
root rank profile defines the ranking functions without any first-phase
or second-phase expression. Selecting it makes Vespa fall back to default
nativeRank ranking, so the carefully tuned query(*_weight) values were
never applied to result ordering.

Point ranking.profile at "weighted-rank2" — the profile that actually
consumes these weights (bm25_chunks_max/avg in first-phase; match_chunks
and chunks_embeddings_avg_cosine_similarity in second-phase).

Also drop the bm25_title_weight field: the default v1 schema has no
bm25-enabled title field, so no bm25_title rank feature exists and the
weight was a dead no-op.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…hybrid-search)

The default search path used the auto-generated hybrid-profile, whose
ranking weights are all 0, so the tuned hybrid-search profile was never
actually used. Default the entrypoint to hybrid-search so search ranks
with the tuned weights, and allow selecting any other profile by name.

- search.py: --query-profile arg (default hybrid-search), passed to
  get_search_index(); printed in the run header.
- Makefile: optional query_profile= passthrough.
- README: document the default and how to use a custom query profile.
- migration: comment noting the auto default ships with zero weights.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@alexandretamborrino alexandretamborrino changed the title fix(template): hybrid-search query profile must use weighted-rank2, not root fix(template): make the tuned hybrid query profile actually rank Jun 12, 2026
alexandretamborrino and others added 2 commits June 12, 2026 11:37
Keep the bm25_title_weight query field. It is a no-op against the current
v1 schema (no bm25-enabled title field, so no bm25_title rank feature),
but it is harmless and left in place as an intentional placeholder.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
hybrid-profile is the zero-weight neutral default; using it as the
query_profile= example steers users to a profile that ranks worse and
needs a caveat to not mislead. Keep only the custom-profile example,
which demonstrates the query_profile= mechanism without that footgun.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@afoucret afoucret merged commit 3239064 into mistralai:main Jun 15, 2026
2 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants