fix(template): make the tuned hybrid query profile actually rank#11
Merged
afoucret merged 4 commits intoJun 15, 2026
Merged
Conversation
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>
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
approved these changes
Jun 15, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Problem
The starter's
hybrid-searchquery profile was defined with carefully tuned BM25 + vector weights, but those weights never actually affected ranking:ranking.profilewas set toroot. In the generated.sd,rootdeclares all the ranking functions but has nofirst-phase/second-phaseexpression — it's only a base profile to inherit from. Selecting it makes Vespa fall back to defaultnativeRank, so thequery(*_weight)values had no formula to plug into.The default search path never used
hybrid-searchanyway.search.pycalledget_search_index(...)with no profile, which resolves todefault_query_profile_name=hybrid-profile— the toolkit's auto-generated profile, whose weights all default to0. So even after fixing (1), the tuned profile was dead code.Weights can only be supplied via a query profile (the high-level
VectorSearchQueryAPI has no per-query weight field), and the auto-generated default profile can't carry tuned weights (it's generated with0defaults, 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:root→weighted-rank2(the profile that actually consumes these weights:bm25_chunks_max/avgin first-phase;match_chunks+chunks_embeddings_avg_cosine_similarityin second-phase).default_query_profile_namenoting the auto profile ships with zero weights.Wire it up so the tuned profile is actually used:
search.py: new--query-profilearg (defaulthybrid-search), passed toget_search_index(...)and printed in the run header.Makefile: optionalquery_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_weightis kept as an intentional placeholder. It's currently a no-op (the v1 schema has no bm25-enabledtitlefield, so nobm25_titlerank feature), but harmless.chunks_embeddings_avg_distance_weightis left at its0default — 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.Query profile: hybrid-search;query_profile=hybrid-profileoverride works.hybrid-searchviamigrate_to_package:ranking.profile = weighted-rank2with the tuned weights present.Note: on the 2-document sample corpus,
hybrid-searchandhybrid-profilereturn identical output — the displayed score is the client-sidebest_chunksvalue 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.🤖 Generated with Claude Code