Skip to content
Back to skills

Vespa

ASecurity

Vespa hybrid retrieval and ranking. BM25 + ANN + ColBERT late interaction in one query, ranking phases (match-phase, first-phase, second-phase, global-phase), tensor expressions, cloud vs self-hosted, schema definition (.sd files), embedding in-cluster, scale to billions. USE WHEN: user mentions "Vespa", "Vespa schema", "Vespa ranking", "vespa.ai", "tensor ranking", "Vespa hybrid", "Vespa cloud" DO NOT USE FOR: other vector stores - use `vector-stores/*` siblings; ColBERT without Vespa - us...

  • 31 stars
  • 0 votes
  • 0 copies
  • 4 views
  • Added September 8, 2026
ai-agentspythongobashangularnodeexpressdockerapi

Works with

  • cli
  • api

Security analysis

A96/100
  • mediumInstalls packages at runtime which could introduce malicious dependencies

Pro shows the line behind each finding and how to fix it

Scanned September 8, 2026

npx -y skills add claude-dev-suite/claude-dev-suite --skill vespa --agent claude-code

Installs into .claude/skills of the current project.

Are you the author of Vespa?

Add the live security badge to your README. It updates with every re-scan.

Security grade badge for Vespa
[![Security: A — Skills Directory](https://www.skillsdirectory.com/api/skills/claude-dev-suite-vespa/badge)](https://www.skillsdirectory.com/skills/claude-dev-suite-vespa)

More formats (shields.io, HTML) on the badges page. Keep it an A: scan every change in CI with Pro.

Download with Pro
SKILL.md
---
name: vespa
description: |
  Vespa hybrid retrieval and ranking. BM25 + ANN + ColBERT late interaction in one
  query, ranking phases (match-phase, first-phase, second-phase, global-phase), tensor
  expressions, cloud vs self-hosted, schema definition (.sd files), embedding in-cluster,
  scale to billions.

  USE WHEN: user mentions "Vespa", "Vespa schema", "Vespa ranking", "vespa.ai", "tensor
  ranking", "Vespa hybrid", "Vespa cloud"

  DO NOT USE FOR: other vector stores - use `vector-stores/*` siblings; ColBERT without
  Vespa - use `retrieval/colbert-retrieval`; classical reranking - use `rag/reranking`
allowed-tools: Read, Grep, Glob, Write, Edit
---
# Vespa

## Why Vespa

Vespa was built at Yahoo for search+recommendation at web scale. Its vector search is a side-effect of a broader ranking engine — which is also its advantage.

- Hybrid retrieval (BM25 + ANN + ColBERT) composed in one query.
- Ranking phases let you run cheap scoring over millions, expensive scoring over tens.
- Tensor math (sum, reduce, matmul) in the ranking language — you can express almost any scoring function.
- In-cluster embedding inference: encode at write and query time without an external service.
- Proven at billions of documents (Yahoo, Spotify, Perplexity).

Cost: steeper learning curve than purpose-built vector DBs. Use Vespa when ranking control is the requirement, not an afterthought.

## Vespa Cloud vs Self-Hosted

| Option | Best for |
|---|---|
| Vespa Cloud | Managed multi-tenant, zero ops, serverless billing |
| Docker standalone | Dev/local; small single-node deployments |
| Self-hosted multi-node | Air-gapped, custom tuning, cost control at very large scale |

```bash
# Install Vespa CLI
brew install vespa-cli
vespa config set target cloud
vespa config set application my-tenant.my-app
vespa auth login
```

## Application Package Layout

```
my-app/
  services.xml                 # cluster topology
  schemas/
    doc.sd                     # schema + rank profiles (the main thing)
  search/
    query-profiles/            # reusable query templates
  components/
    ...
```

Deploy with `vespa deploy --wait 300`.

## Schema (.sd) with Dense, Sparse, ColBERT

```
schema doc {
    document doc {
        field id type string { indexing: summary | attribute }
        field text type string {
            indexing: summary | index
            index: enable-bm25
        }
        field tenant_id type string {
            indexing: summary | attribute
            attribute: fast-search
        }
        field created_at type long { indexing: summary | attribute }

        # Dense embedding
        field embedding type tensor<float>(x[1024]) {
            indexing: attribute | index
            attribute {
                distance-metric: angular
            }
            index {
                hnsw {
                    max-links-per-node: 24
                    neighbors-to-explore-at-insert: 200
                }
            }
        }

        # ColBERT token-level embeddings
        field colbert_tokens type tensor<bfloat16>(t{}, x[128]) {
            indexing: attribute | summary
        }
    }

    # Rank profile: BM25 + dense + ColBERT in three phases
    rank-profile hybrid inherits default {
        inputs {
            query(q_dense) tensor<float>(x[1024])
            query(q_tokens) tensor<float>(qt{}, x[128])
        }

        first-phase {
            expression: bm25(text) + closeness(field, embedding)
        }

        second-phase {
            rerank-count: 100
            expression {
                sum(
                    reduce(
                        sum(query(q_tokens) * attribute(colbert_tokens), x),
                        max, t
                    ),
                    qt
                )
            }
        }

        match-features: bm25(text) closeness(field, embedding)
    }
}
```

Key moves:

- `bm25(text)` — classical lexical.
- `closeness(field, embedding)` — 1 / (1 + distance) after HNSW retrieval.
- ColBERT MaxSim is a tensor expression over token-level attributes.
- Three-phase: cheap `first-phase` on 100k candidates, expensive `second-phase` on top 100.

## Services.xml (Topology)

```xml
<services version="1.0">
  <container id="default" version="1.0">
    <search/>
    <document-api/>
    <nodes>
      <node hostalias="node0"/>
    </nodes>
  </container>
  <content id="docs" version="1.0">
    <redundancy>2</redundancy>
    <documents>
      <document type="doc" mode="index"/>
    </documents>
    <nodes>
      <node hostalias="node0" distribution-key="0"/>
      <node hostalias="node1" distribution-key="1"/>
    </nodes>
    <engine>
      <proton>
        <tuning><searchnode><requestthreads><persearch>4</persearch></requestthreads></searchnode></tuning>
      </proton>
    </engine>
  </content>
</services>
```

`redundancy=2` gives HA; more nodes shard the index.

## Feed Documents

```python
# pip install pyvespa
from vespa.application import Vespa

app = Vespa(url="https://my-app.my-tenant.vespa-app.cloud")

app.feed_data_point(
    schema="doc",
    data_id="d1",
    fields={
        "id": "d1",
        "text": "OAuth uses refresh tokens.",
        "tenant_id": "acme",
        "created_at": 1700000000,
        "embedding": {"values": dense_vec.tolist()},
        "colbert_tokens": {
            "cells": [
                {"address": {"t": str(i), "x": str(j)}, "value": float(v)}
                for i, row in enumerate(colbert_vecs)
                for j, v in enumerate(row)
            ],
        },
    },
)
```

For bulk, use `app.feed_iterable()` or the Vespa Feeder CLI (`vespa feed`), which streams JSON files at high throughput.

## Query: Hybrid in a Single Request

```python
from vespa.io import VespaQueryResponse

resp: VespaQueryResponse = app.query(
    yql=(
        "select * from doc where "
        "({targetHits:100}nearestNeighbor(embedding, q_dense)) "
        "or userQuery() "
        "and tenant_id contains 'acme';"
    ),
    query="oauth refresh token 403",
    ranking="hybrid",
    hits=10,
    body={
        "input.query(q_dense)": {"values": q_dense.tolist()},
        "input.query(q_tokens)": {
            "cells": [
                {"address": {"qt": str(i), "x": str(j)}, "value": float(v)}
                for i, row in enumerate(q_colbert_vecs)
                for j, v in enumerate(row)
            ],
        },
    },
)

for hit in resp.hits:
    print(hit["relevance"], hit["fields"]["text"][:80])
```

Anatomy:

- `nearestNeighbor(embedding, q_dense)` — HNSW ANN with `targetHits=100`.
- `userQuery()` — BM25 match on `text`.
- `or` — either matches advance to ranking.
- Tenant filter via YQL `contains`.
- Rank profile `hybrid` computes the three-phase score.

## Ranking Phases

| Phase | Runs over | Typical use |
|---|---|---|
| match-phase | All matching candidates | Filter by predicate or attribute |
| first-phase | Millions | Cheap expression: BM25 + dot product |
| second-phase | rerank-count (e.g., 100) | Expensive: ColBERT MaxSim, learned-to-rank |
| global-phase | After merging across shards | Cross-node reranking, diversification |

This is Vespa's killer feature. Expensive ranking runs only on the smallest tier.

## In-Cluster Embedding

Vespa can run transformer models at write and query time — no external embedding service.

```
schema doc {
    field text type string { indexing: summary | index }
    field embedding type tensor<float>(x[384]) {
        indexing: input text | embed e5 | attribute | index
        attribute { distance-metric: angular }
        index { hnsw { ... } }
    }
}
```

Then in `services.xml`:

```xml
<component id="e5" type="hugging-face-embedder">
  <transformer-model url="https://huggingface.co/intfloat/e5-small-v2/resolve/main/onnx/model.onnx"/>
  <tokenizer-model url="https://huggingface.co/intfloat/e5-small-v2/resolve/main/tokenizer.json"/>
</component>
```

Query-side embedding works the same way via `embed()` in YQL.

## ColBERT Inside Vespa

Vespa is the reference production home for ColBERT. MaxSim is a tensor expression, not a special index — so you can combine it freely with BM25 and dense vectors.

```
sum(
    reduce(
        sum(query(q_tokens) * attribute(colbert_tokens), x),
        max, t
    ),
    qt
)
```

- Inner `sum(... , x)`: dot product on last dim (the embedding dim).
- `reduce(..., max, t)`: max over document tokens for each query token.
- Outer `sum(..., qt)`: sum the MaxSim scores across query tokens.

See `retrieval/colbert-retrieval` for the algorithm.

## Scaling to Billions

- Shard by `distribution-key` across content nodes.
- Use tiered storage (some data on SSD, cold on object storage via streaming search).
- Streaming mode sidesteps HNSW for rarely-queried tenants (compute per request, no pre-built graph).
- Attribute-only ranking avoids random disk I/O at query time.

## Anti-Patterns

| Anti-Pattern | Fix |
|---|---|
| Running expensive expressions in `first-phase` | Push heavy work to `second-phase` with `rerank-count` |
| Forgetting `fast-search` on filtered attributes | Add `attribute: fast-search` for pre-filter perf |
| HNSW for single-tenant queries on rare tenants | Use streaming mode for small per-tenant corpora |
| Using Vespa like a simple vector DB | If you only want k-NN, Qdrant/Pinecone is simpler |
| Shipping ColBERT without bfloat16/int8 on tokens | Use `tensor<bfloat16>` to cut token storage in half |
| Skipping match-phase filters | Use attribute predicates to reduce the first-phase pool |
| Deploying schema changes without `vespa status` check | Deploy with `--wait`; schema migrations can be long |

## Production Checklist

- [ ] Rank profiles phased (first / second / global) with matching `rerank-count`
- [ ] `distance-metric` matches your embedding model (angular / innerproduct)
- [ ] HNSW `max-links-per-node` and `neighbors-to-explore-at-insert` tuned
- [ ] `fast-search` attribute on all filtered fields
- [ ] Bulk feed via Vespa Feeder CLI (not one-at-a-time API)
- [ ] Schema changes deployed with `vespa deploy --wait`
- [ ] Redundancy >= 2 for HA
- [ ] In-cluster embedding pinned to a model version
- [ ] Tensor storage types chosen (bfloat16 / int8) at scale
- [ ] Grafana dashboards for first-phase / second-phase latency
- [ ] Streaming mode considered for long-tail tenants
- [ ] A/B harness compares rank profiles on a gold set

Attribution

Is this your skill, or is something wrong with this listing? Request removal or report an issue. Author removals are honored within 72 hours.

Comments

Loading comments…

Newsletter

Get the weekly list of safe new skills, and the ones we pulled.