⌨ Keyboard shortcuts available
G — waiting for next key…
Vespa.AI Vector Search

Vespa Multi-Stage Ranking: Merging BM25 and Vector Search

An in-depth technical tutorial detailing how to craft hybrid, multi-stage rank profiles inside Vespa.ai. It breaks down the mechanical logic of first-phase (lexical) and second-phase (semantic vector) scoring layers, allowing developers to safely merge exact string matching with high-intent vector representations without sacrificing execution latency.

P
Pradeep Bhandari
May 18, 2026 7 min read 99 views
A high-level software engineering schematic demonstrating the transition of data chunks through Vespa's first-phase text filter and second-phase vector ranking.

Beyond Vector Closeness: Multi-Stage Hybrid Ranking in Vespa

In my previous Vespa.ai vs. Elasticsearch comparison, I highlighted why Vespa is built for the "brain" layer of modern data stacks. But as teams scramble to deploy production-grade AI platforms, many run straight into a wall: the limitations of raw vector search.

If a user searches for "Next.js 15 routing errors," an approximate nearest neighbor (ANN) vector index can easily surface articles about general React framework navigation. However, it might completely miss a document containing the exact cryptographic string or strict error token.

To build an unbreakable search experience, you need Hybrid Search—a system that marries the precision of lexical keyword matching (BM25) with the conceptual awareness of vector spaces.

Let's explore how to configure a multi-stage, high-performance rank profile natively within Vespa without breaking your infrastructure budget.

The Vespa Paradigm: Phased Execution Layers Explained

The secret to Vespa’s sub-millisecond retrieval speed over massive datasets is its Phased Ranking architecture. Instead of running expensive machine learning or complex tensor math across every single document in your database, Vespa structures the computation in two distinct local steps on the content nodes:

  1. First-Phase: A cheap, fast mathematical function run across all matching candidates retrieved by the query (e.g., millions of documents).
  2. Second-Phase: A more intensive re-ranking expression applied only to the top N candidates (e.g., the top 1,000 matches) emerging from the first phase.

Step 1: Declaring Tensors and Inputs in the Schema

Before we can score vectors, we must define how our schema handles the dimensions. Here, we specify a document structure that accommodates traditional full-text searching alongside a bfloat16 tensor for dense vector retrieval.

Code snippet

# schemas/product_doc.sd
schema product_doc {
    document product_doc {
        field title type string {
            indexing: summary | index
            index: enable-bm25
        }
        field body type string {
            indexing: summary | index
            index: enable-bm25
        }
        field embedding type tensor<bfloat16>(v[384]) {
            indexing: input title . " " . input body | embed | attribute | index
            index: hnsw
        }
    }
}

Step 2: Crafting the Multi-Stage Rank Profile

Now, we construct the actual mathematical ranking expressions. We declare the incoming query vector as an input tensor, score lexical text using native BM25 features in the first phase, and layer on semantic closeness during the second phase.

Code snippet

# Inside schemas/product_doc.sd, right below the document block

rank-profile hybrid-ui-rank {
    inputs {
        query(query_vector) tensor<float>(v[384])
    }

    # First Phase: Sift millions of docs instantly using fast lexical signals
    first-phase {
        expression: bm25(title) + 0.5 * bm25(body)
    }

    # Second Phase: Inject heavy vector calculations only on the top 1,000 candidates
    second-phase {
        expression: firstPhase + 15.0 * closeness(field, embedding)
        rerank-count: 1000
    }

    # Summary features reveal execution metrics back to your app logs
    match-features: bm25(title) closeness(field, embedding)
}

By separating the logic, your CPU cores aren't choked calculating dense multi-dimensional matrix multiplications across your entire cold storage partition.

Step 3: Querying the Hybrid Engine with YQL

To invoke this specific logic, your application layer sends a YQL (Yahoo Query Language) request that tells the retrieval engine to execute both a traditional string match and a nearest-neighbor constraint in parallel.

HTTP

# The API Hybrid Request Payload
POST /search/
{
  "yql": "select * from product_doc where userQuery() or ({targetHits:100}nearestNeighbor(embedding, query_vector))",
  "query": "Next.js 15 routing errors",
  "ranking.profile": "hybrid-ui-rank",
  "ranking.features.query(query_vector)": [0.123, -0.456, ..., 0.789]
}

The use of the or operator in the YQL block tells Vespa to pull candidates from both indexing pools before feeding them straight into your custom hybrid-ui-rank pipeline.

Performance Architecture: Memory and CPU Trade-Offs

As a Senior Solution Architect, I have to emphasize: HNSW indexes reside completely in RAM. If your data pipeline scales to hundreds of millions of objects, memory pricing will quickly dwarf compute expenses.

If you are dealing with strict budget guardrails or highly multi-tenant application layouts, consider combining this hybrid layout with Vespa’s Streaming Mode. Streaming mode drops the global memory-resident index structure entirely and runs this exact phased ranking straight off disk partitions per user group.

FAQ: Frequently Asked Questions on Hybrid Ranking

Q: Why not use Reciprocal Rank Fusion (RRF) instead of raw scoring math? 

RRF is excellent when you are blending completely distinct systems (e.g., a standalone Elasticsearch cluster alongside a Pinecone cluster). However, because Vespa handles both text and tensors natively within the exact same thread loop, we can write true scalar-linear expressions. This preserves structural meaning and cuts execution latencies in half.

Q: Can I use machine learning models like XGBoost inside the second-phase? 

Yes. Vespa explicitly supports native ONNX or XGBoost evaluation blocks inside the second-phase or global-phase ranking scopes, meaning your data engineering pipeline can deploy fine-tuned models directly to the search nodes.

Share Twitter / X LinkedIn

Comments

No comments yet. Be the first to share your thoughts.

Leave a comment

Max 2,000 characters. Comments are moderated before appearing.

More Posts