Field Boosting with Multi-Match Queries

Field weights are the highest-return relevance lever available at zero latency cost, and they are almost always left at whatever the first developer typed. A title match and a description match are qualitatively different evidence, and telling the engine so usually moves relevance further than any amount of BM25 parameter tuning. This page sets those weights from measurement rather than intuition. It sits under BM25 tuning and weights in ranking algorithms and relevance tuning.

Prerequisites

  1. A judgment set, since weights tuned by eye reliably overfit to the three queries someone happened to try.
  2. A mapping where the fields you intend to weight are separately searchable — not merged into one catch-all field.
  3. Knowledge of which multi_match type your query uses, because the types combine field scores in fundamentally different ways.
  4. A baseline measurement at the current weights.

Diagnosis: the type matters more than the numbers

multi_match supports several types and they are not interchangeable. Weight tuning on the wrong type produces small, confusing effects, because the type decides how per-field scores combine before the weights are even applied.

best_fields takes the single highest-scoring field and ignores the rest, apart from a fraction controlled by tie_breaker. It is the right choice when the query is expected to match well in one field — the usual case for titles and names.

most_fields sums the scores of every matching field. It rewards documents that match in many places, which sounds appealing and in practice favours long documents with repetitive content across fields.

cross_fields treats the fields as one combined field for term-frequency purposes, which is the correct model when the query’s terms are distributed across fields — a person’s first name in one field and surname in another.

query "acme trail runner"          best_fields   most_fields   cross_fields
doc A: title="Acme Trail Runner"        14.2          15.1          16.8
doc B: title="Trail Runner"
       brand="Acme"                      9.6          13.4          16.2
doc C: desc mentions all three           6.1          11.9           7.4
# best_fields ranks A well ahead; most_fields promotes C by summing weak matches.

Choosing the type is therefore the first decision, and it changes what the weights mean. best_fields weights express “which field is the most trustworthy signal”; most_fields weights express “how much does a match here add”.

How each multi_match type combines field scores best_fields takes the maximum, most_fields sums across fields, and cross_fields treats the fields as one combined field. best_fields max of the fields + tie_breaker share default for titles most_fields sum across fields rewards repetition favours long documents cross_fields one virtual field terms spread across fields names, addresses the type changes what a weight means, so choose it before tuning any numbers weights tuned under one type do not transfer to another
Three combination rules with different biases. Tuning weights before choosing the type means tuning against a model you are about to replace.

Solution Steps

1. Start from a defensible baseline

Order the fields by how much a match in each one tells you about relevance, and assign weights on a small scale. Large numbers create the illusion of precision and make later reasoning harder.

{
  "multi_match": {
    "query": "waterproof trail runner",
    "type": "best_fields",
    "fields": ["title^6", "brand^3", "category^2", "description"],
    "tie_breaker": 0.2
  }
}

2. Understand what tie_breaker is doing

With best_fields, a document scores the maximum field score plus tie_breaker times the sum of the others. At zero, only the best field counts; at one, the behaviour approaches most_fields. Values between 0.1 and 0.3 give a small credit for matching in several fields without letting that dominate.

tie_breaker   doc scoring title 12.0, brand 6.0, desc 3.0
0.0           12.0
0.2           12.0 + 0.2 × 9.0  = 13.8
0.5           12.0 + 0.5 × 9.0  = 16.5

3. Sweep the weights against the judgment set

Sweep one field at a time relative to a fixed reference field. Sweeping all of them jointly is a large search space and the interactions are weak enough that sequential tuning is nearly as good.

# sweep_weights.py — one field at a time, holding the others fixed
BASE = {"title": 6, "brand": 3, "category": 2, "description": 1}

for field in ("brand", "category", "description"):
    for w in (0.5, 1, 2, 3, 5, 8):
        weights = {**BASE, field: w}
        fields = [f"{f}^{v}" for f, v in weights.items()]
        print(field, w, evaluate(lambda q: search(q, fields=fields)))

4. Check the result against the failure cases you started with

An aggregate improvement can hide a regression on the specific queries that motivated the work. Keep those queries as a named subset and report them separately.

WATCHLIST = ["gore-tex", "acme 5000", "waterproof jacket mens"]
for q in WATCHLIST:
    print(q, [h["title"] for h in search(q, fields=tuned_fields)[:3]])

5. Record the weights and their provenance

# config/ranking/fields.yaml
type: best_fields
tie_breaker: 0.2
fields:
  title: 6        # measured 2026-07: +0.041 NDCG over unweighted
  brand: 3        # measured 2026-07: +0.012
  category: 2     # measured 2026-07: +0.004 — marginal, kept for tail queries
  description: 1
judgments_version: v7

6. Consider per-surface weights before per-query ones

Different surfaces genuinely want different weights: an autocomplete dropdown should weight titles almost exclusively, while a full result page benefits from description matches. That is two weight sets, which is manageable. Per-query-class weights are the next step down that road and are usually not worth it — the maintenance cost grows with the number of classes and the gains are small once the surface distinction is made.

Verification

Confirm the weights are actually being applied, which a typo in the field name silently prevents:

curl -s 'localhost:9200/products/_validate/query?explain=true' \
  -H 'Content-Type: application/json' -d '{
    "query": {"multi_match": {"query": "trail runner",
              "fields": ["title^6","brand^3"], "type": "best_fields"}}}' | jq -r '.explanations[0].explanation'
# => (title:trail title:runner)^6.0 | (brand:trail brand:runner)^3.0

Confirm the tuned configuration beats the baseline on the judgment set:

python3 evaluate.py --config config/ranking/fields.yaml --baseline .baseline.json
# ndcg@10  0.6412 -> 0.6889  (+0.0477)
# mrr      0.7104 -> 0.7566  (+0.0462)

Confirm the watchlist queries improved rather than merely the average:

python3 evaluate.py --queries watchlist.txt --verbose
# gore-tex               rank of expected doc: 7 -> 1
# acme 5000              rank of expected doc: 3 -> 1
# waterproof jacket mens rank of expected doc: 2 -> 2   (unchanged, already good)

What weights cannot fix

Field weighting redistributes credit between fields; it cannot create evidence that is not there. Three failures look like weighting problems and are not, and recognising them saves a sweep that was never going to help.

If the target document does not match the query at all, no weight helps — the document is absent from the result set and the problem is analysis or vocabulary. The check is one query: search with match_all filtered to the document id and confirm it exists, then search with the real query and see whether it appears anywhere.

If the target matches only in a field the query does not search, weights on the searched fields are irrelevant. This happens most often after someone adds a field to the mapping without adding it to the query, and it presents as “the new field does nothing”.

If the competing documents win on a boost rather than on text score, weights are competing against a multiplier and will lose. The explain API separates the two in one request, and doing that before a sweep saves the sweep.

Three problems that look like weighting problems A document that does not match at all, a field absent from the query, and a competing boost each resist weight tuning entirely. does not match at all → analysis or vocabulary field not in the query → add it to the field list a boost is winning → explain, then fix the boost All three produce a flat weight sweep, which is the signal to stop and check these instead.
A sweep that shows no effect anywhere is diagnostic. It means the weights are not the constraint, and one of these three is.

Common Pitfalls

Boost inflation over time

Each request to “make X rank higher” raises one field’s weight, and after two years the weights are 40, 25, 18 and 9 — the same ratios as 6, 3, 2 and 1 but impossible to reason about, and any new field has to be inserted into a scale nobody understands. Periodically renormalise so the smallest weight is 1, which changes nothing about ranking and restores the ability to think about the numbers.

Weighting a field that is not separately searchable

If the mapping uses copy_to to merge everything into one catch-all field and the query targets that field, per-field weights have no effect at all — the engine sees one field. Check the mapping before concluding that weights do not work on your corpus; this is a surprisingly common cause of a completely flat sweep.

Tuning weights on a corpus where one field dominates length

A description field twenty times longer than the title will produce low BM25 scores by length normalisation alone, and raising its weight compensates for the normalisation rather than expressing a relevance judgement. If a weight has to exceed the title’s to have any effect, the issue is the field’s length distribution and belongs in BM25 parameter tuning instead.

Operational notes

Keep the field list short. Every field added to a multi_match costs query time proportional to its posting-list work, and fields that contribute nothing to relevance still contribute to latency. A sweep that shows a field’s weight makes no measurable difference across its whole range is telling you to remove the field from the query entirely — which is a latency improvement disguised as a relevance experiment.

Re-measure after any mapping or analyzer change. Weights are relative to the score distributions each field produces, and those distributions shift when analysis changes. A weight set tuned before a stemming change is not obviously wrong afterwards, which is precisely why it goes unnoticed.

Events that invalidate a tuned weight set Analyzer changes, new fields and corpus shifts all change the score distributions the weights were fitted against. analyzer change shifts field scores new searchable field changes the max corpus composition shifts length norms None of these produces an error — the weights simply stop being the ones you measured.
Three routine changes that silently invalidate a weight set. Tying a re-measurement to each is cheaper than discovering the drift through complaints.