For now:
Context: why these questions feel “harder” in Vespa than in Solr
Solr encourages request-time dynamism: field lists (qf), boosts, and sometimes scoring tweaks can be pushed into query params. Vespa is designed around a different contract:
- Ranking logic lives in the application package as rank profiles and is compiled and validated as part of deployment. (VespaDocument)
- Request-time flexibility is mostly “select a pre-defined profile” + “pass inputs into that profile.” (VespaDocument)
- This yields predictable latency and safety (invalid changes rejected before activation), but you must engineer experimentation differently than Solr. (VespaDocument)
With that framing, here are direct answers and the production patterns that map closest to your Solr expectations.
1) External rank profiles. Separate files? Dynamically loaded? Updated by API?
What Vespa supports
Yes, external ranking config files exist, but they are not “dynamic runtime plugins.”
- You can define rank profiles either inline in the schema or in separate files:
schemas/<schema-name>/<profile-name>.profile. (VespaDocument)
- Rank profiles can also inherit from each other to reduce duplication. (VespaDocument)
This solves “giant schema file” and supports clean modularity.
What Vespa does not support
No, you cannot hot-load or update rank profiles via query-time API calls.
Rank expressions are stored in a rank profile, and rank profiles are part of the deployed config. (VespaDocument)
The production workaround that gives Solr-like iteration speed
Engineer rank profiles as templates plus query-time inputs:
- Deploy stable rank profiles infrequently.
- Change weights and thresholds frequently via
input.query(...) or other query features. The Query API explicitly supports passing input data used in ranking. (VespaDocument)
- For “structural” changes (new features, new phases, new expression graph), deploy. Deployments are designed to be safe under live traffic. (VespaDocument)
2) Query-time ranking definition. Can we send ranking expressions in the query?
Direct answer
No. You cannot pass an arbitrary ranking expression in a request and have Vespa execute it.
This is a known limitation and is commonly explained as an efficiency/compilation issue. (Stack Overflow)
Related: even some deeper rank-feature configuration knobs (example: bm25 properties) are explicitly “rank-profile only,” not request-settable. (GitHub)
What you do instead for A/B testing
You get two practical degrees of freedom that cover most experimentation needs:
-
Choose among pre-defined rank profiles at query time
ranking.profile=<name> (often abbreviated as ranking=<name>). Vespa docs describe this selection model. (VespaDocument)
-
Parameterize rank profiles
Define expressions that read query features and set them per request:
input.query(w_title)=2.0
input.query(w_body)=0.5
- etc. The Query API guide shows query inputs as a first-class part of requests. (VespaDocument)
High-leverage pattern: pre-deploy a small family of profiles (baseline, expA, expB) and then vary weights via query inputs. This matches how Vespa teams describe running experiments without redeploying. (Medium)
3) Deployment impact on search. Any disruption? How to do zero-downtime ranking updates?
What Vespa promises (and generally delivers)
Vespa documentation is explicit:
- Deploying application package changes is generally safe at any time.
- It does not disrupt queries and writes.
- Invalid or destructive changes are rejected before taking effect. (VespaDocument)
Vespa Cloud adds orchestrated multi-zone deployments and CD hooks. (VespaDocument)
The real operational nuance
Even when requests are not dropped, some changes can increase load:
- Schema/indexing changes that trigger reindexing.
- Changes that increase ranking cost per hit or increase
rerank-count in phased ranking. (VespaDocument)
So, “no disruption” is about availability. “No performance change” depends on what you deployed.
Recommended approach for zero-downtime ranking iteration
Use a two-lane strategy:
-
Lane A: No-deploy iteration
Keep rank profile structure stable. Change weights via query inputs. (VespaDocument)
-
Lane B: Safe deploy iteration
When structure must change, deploy new rank profile names (or inherited variants), then switch traffic gradually by selecting ranking.profile per request. (VespaDocument)
This maps closely to how teams run experiments: predefine multiple profiles and select them in the query. (Medium)
4) Relevance score with filters only. Why is relevance > 0 with no text match?
Background: “relevance” is just the output of the active rank profile
In Vespa, each hit’s relevance is the rank score computed by the chosen rank profile. (VespaDocument)
If you do not specify a profile, Vespa uses:
default if present in your schema
- otherwise a builtin default ranking by
nativeRank. (VespaDocument)
Why filter-only queries still show non-zero
Because rank expressions can be query-independent or mostly query-independent:
- A rank profile can use document features only (attributes), producing a non-zero score even without text matching. Vespa’s ranking intro shows an example where the relevance equals
attribute(inlinks).count, explicitly noting that the expression does not use query features. (VespaDocument)
Also note a subtle pitfall:
- Even if you override ordering with
order by, Vespa still computes the rank score unless you choose unranked. The YQL reference states Vespa does not optimize away rank-score computation just because you discard it via sorting. (VespaDocument)
If you truly want “no ranking” for filter endpoints
Use the built-in rank profile unranked:
ranking.profile=unranked
This is explicitly recommended for optimal performance of sorting queries. (VespaDocument)
How to debug “where did this score come from?”
Turn on rank feature listing for debugging and offline analysis:
ranking.listFeatures=true is documented in the ML tutorial as a way to return rank features. (VespaDocument)
5) Sort by relevance ascending. How to get “least relevant first”?
Direct answer
Use the special sorting attribute '[relevance]' and sort ascending:
order by '[relevance]' asc (VespaDocument)
- Sorting reference notes
[relevance] defaults to descending if you omit direction, so you must be explicit. (VespaDocument)
Example YQL:
select * from mydoc where userQuery() order by '[relevance]' asc;
If you want stable results when many documents have equal relevance, add a deterministic secondary sort (for example on an id attribute). The YQL reference recommends secondary ordering after '[relevance]'. (VespaDocument)
6) Grouping order logic. What order do groups come back in? Can we control it?
Default group order
If you do not specify order(...) in the grouping expression, default ordering is:
-max(relevance()) (descending by the best hit’s relevance per group). (VespaDocument)
If your query is effectively “all documents” or relevance ties across groups, order will look random. The grouping guide explicitly calls this out. (VespaDocument)
How to control group order
Add order(...) at the group level. The grouping guide and reference both emphasize:
order(...) controls group ordering, not hit ordering inside each group. (VespaDocument)
- Use aggregators like
count(), avg(), sum() for deterministic ordering after merges. The reference notes limitations for max/min ordering after merge operations. (VespaDocument)
Practical examples:
- Most frequent groups first:
order(-count())
- Least frequent first:
order(count())
- Order by a numeric attribute aggregate:
order(-max(attribute(price)))
Important operational constraint
Fields used in grouping must be attributes. The Query API docs state this explicitly. (VespaDocument)
7) Dynamic field selection for userQuery(). Solr qf equivalent?
What Vespa does by default
If you do not specify fields in the query, Vespa searches a fieldset named default (or a field named default), if you defined one. The text search tutorial describes this and shows a fieldset default { fields: ... }. (VespaDocument)
How to choose fields at query time
You typically do this by switching the default searched field or fieldset:
- Use
model.defaultIndex to specify a field or fieldset if not using the default one. The YQL reference explicitly points to model.defaultIndex for this purpose. (VespaDocument)
So the Vespa version of Solr qf is usually:
- Predefine a handful of fieldsets (
qf_short, qf_long, qf_title_only, …)
- Choose among them per request via
model.defaultIndex=<fieldset> (VespaDocument)
Where boosts belong in Vespa
In Solr, boosts often live in qf. In Vespa, boosts usually live in ranking:
- weight
bm25(title) vs bm25(body) etc using query inputs.
- That keeps query parsing stable and puts tuning where Vespa expects it. (VespaDocument)
How to keep this safe at scale
Use query profiles to provide defaults and reduce client complexity, and query profile types to restrict what parameters are legal (typed, optionally “no extra params”). Vespa docs describe query profile types explicitly. (VespaDocument)
Migration reality check: what teams report when moving from Solr or Elasticsearch
If your system looks like “production commerce search with lots of filters, facets, and frequent tuning,” two public migrations are especially relevant:
- Vinted describes a multi-stage migration from Elasticsearch to Vespa, including switching search traffic and later facet traffic. (Vinted Engineering)
- OkCupid compares Vespa and Elasticsearch for large-scale matching and highlights operational scaling and distribution as a major reason for choosing Vespa. (OkCupid Tech Blog)
- The Solr vs Vespa guide is specifically written to map Solr concepts to Vespa behavior (ranking defaults, query parameters, request model). (Vespa Blog)
What tends to surprise Solr teams (and directly matches your questions):
- “Dynamic ranking” becomes “dynamic inputs + profile selection.” You do not ship new expressions per request. (Stack Overflow)
- Filter and group features are fast when modeled as attributes, but you must decide that up front. (VespaDocument)
- Sorting does not remove ranking cost unless you switch to
unranked. (VespaDocument)
A concrete architecture that fits your “Solr-like agility” requirement
If your core requirement is “we tune ranking a lot, and we do A/B tests constantly,” the most robust Vespa pattern is:
-
Define a small family of rank profiles (structural variants). (VespaDocument)
-
Parameterize everything you expect to tune weekly or daily using query inputs (input.query(...)). (VespaDocument)
-
Do A/B routing outside Vespa (gateway or search service) and set:
ranking.profile
input.query(...) weights
model.defaultIndex fieldset (VespaDocument)
-
Make filter-only endpoints use unranked so you are not paying for rank-score computation you do not use. (VespaDocument)
-
Always specify group ordering in grouping expressions if you need deterministic behavior. (VespaDocument)
-
Use debug tooling sparingly but deliberately (ranking.listFeatures=true) to explain scoring and build training or regression datasets. (VespaDocument)
Summary bullets
- Rank profiles can live in separate
.profile files, but they are deployed config, not hot-loaded runtime files. (VespaDocument)
- You cannot send ranking expressions in a query. Use profile selection plus query inputs for experiments. (Stack Overflow)
- Deployments are designed to be online and not disrupt queries and writes. Performance impact depends on what changed. (VespaDocument)
- Filter-only queries still get a rank score because ranking still runs unless you use
unranked. (VespaDocument)
- Sort by lowest relevance using
order by '[relevance]' asc. (VespaDocument)
- Grouping defaults to
-max(relevance()) and can look random on ties. Control it with order(...). (VespaDocument)
- Solr
qf maps to schema fieldsets plus model.defaultIndex and ranking-time boosts. (VespaDocument)