<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:media="http://search.yahoo.com/mrss/">
  <channel>
    <title>Vespa Blog</title>
    <description>The AI Search Platform</description>
    <link>https://blog.vespa.ai/</link>
    <atom:link href="https://blog.vespa.ai/feed.xml" rel="self" type="application/rss+xml"/>
    <pubDate>Wed, 14 Oct 2026 11:12:23 +0000</pubDate>
    <lastBuildDate>Wed, 14 Oct 2026 11:12:23 +0000</lastBuildDate>
    <generator>Jekyll v4.4.1</generator>
    
      <item>
        <title>Why retrieval quality is becoming the defining challenge in AI agent architecture</title>
        <description>Why retrieval quality is the ultimate bottleneck in AI agents—and how fixing context-building solves hidden LLM generation failures.</description>
        
        <media:thumbnail url="https://blog.vespa.ai/assets/2026-09-30-retrieval-ai-agent-architecture/img-5.png" />
        
        <content:encoded><![CDATA[<p><em>Disclosure: Vespa asked me to evaluate where retrieval fits in large-scale agentic AI workflows.
This post is my view of the retrieval problem and where Vespa is used as an example implementation.</em></p>

<p>Agentic systems usually have two jobs: build context, then use that context to produce an answer or action.</p>

<p>Many failures that look like LLM problems start in the context-building step.
The answer the LLM gives is limited by the context it was given or it finds through tool calls.
If the agent model cannot find the right sources then improving the generation model will not improve the overall system.</p>

<blockquote>
  <p>Many failures that look like LLM problems start in the context-building step.</p>
</blockquote>

<p>A client, <a href="https://specstory.com/">Specstory</a>, wanted to give users the ability to ask questions from agent history.
For example, why a team chose Authlib for authentication and what alternatives they considered.
The chatbot needs the right prior conversations, decisions, and tradeoffs from a large corpus of coding sessions.
The model and system prompt help only after those chat turns are retrieved and in context.</p>

<p>The same pattern showed up in an AnkiHub operator review in our <a href="https://www.skool.com/ai-eng/about">private community</a>.
A request for help studying based on lecture slides only works if the agent’s tool calls retrieve the right flashcards.</p>

<p>The setup changes by product. A human may see retrieval as a search box.
An agent may call local search, semantic search, a database, or a SaaS API.
Either way, the process is the same: gather the right context, then generate from it.</p>

<p>An order-status agent might pull customer records from Salesforce, shipment data from SAP, and tracking events from a logistics API.
Those sources are structured application state rather than a document corpus.
The agent still has to gather the right context before it answers.</p>

<p>A <a href="https://thenewstack.io/github-mcp-security-scanning/">coding agent</a>
runs <code class="language-plaintext highlighter-rouge">rg</code>, opens files, reads logs, and inspects tests before writing a patch.
A research agent searches the web and internal notes before writing an answer.
A study assistant searches deck facts and user context before suggesting what to learn next.</p>

<p>When context building fails the symptoms look like generation failures.</p>

<table>
  <thead>
    <tr>
      <th style="text-align: left">Symptom</th>
      <th style="text-align: left">Retrieval cause</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td style="text-align: left">Hallucination</td>
      <td style="text-align: left">The answer source never made it into context.</td>
    </tr>
    <tr>
      <td style="text-align: left">Context rot</td>
      <td style="text-align: left">Low recall forces a high <em>top_k</em>, so noisy results fill the context window.</td>
    </tr>
    <tr>
      <td style="text-align: left">Latency</td>
      <td style="text-align: left">Weak retrieval causes more tool calls, larger candidate sets, and bigger context windows.</td>
    </tr>
  </tbody>
</table>

<p>A useful source can disappear at several points in the path. The trace should tell you which one.</p>

<table>
  <thead>
    <tr>
      <th style="text-align: left">Failed step</th>
      <th style="text-align: left">What the trace shows</th>
      <th style="text-align: left">Change to make</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td style="text-align: left"><code class="language-plaintext highlighter-rouge">rg</code> / <code class="language-plaintext highlighter-rouge">grep</code></td>
      <td style="text-align: left">A conceptual query returns literal matches and misses the relevant files.</td>
      <td style="text-align: left">Add semantic search over files or chunks, or generate better keyword queries before <code class="language-plaintext highlighter-rouge">rg</code>.</td>
    </tr>
    <tr>
      <td style="text-align: left">BM25</td>
      <td style="text-align: left">The query uses the right concept but different words from the source material.</td>
      <td style="text-align: left">Add semantic search, synonyms, or query expansion.</td>
    </tr>
    <tr>
      <td style="text-align: left">Semantic search</td>
      <td style="text-align: left">Exact names, error strings, document IDs, or domain terms are missing from the results.</td>
      <td style="text-align: left">Add a keyword or BM25 path, or boost exact term matches.</td>
    </tr>
    <tr>
      <td style="text-align: left">Hybrid retrieval</td>
      <td style="text-align: left">The relevant passage is ranked 7th, but the context only takes the top 5.</td>
      <td style="text-align: left">Add or tune a reranker, or raise candidate <code class="language-plaintext highlighter-rouge">top_k</code> before reranking.</td>
    </tr>
    <tr>
      <td style="text-align: left">SQL / database</td>
      <td style="text-align: left">The generated query uses the wrong joins, filters, or time window.</td>
      <td style="text-align: left">Ground query generation in the schema, validate filters, and trace the returned rows.</td>
    </tr>
    <tr>
      <td style="text-align: left">SaaS / API call</td>
      <td style="text-align: left">The agent calls the wrong endpoint, omits an account ID, or requests too few fields.</td>
      <td style="text-align: left">Tighten tool schemas, required arguments, and response summaries.</td>
    </tr>
  </tbody>
</table>

<blockquote>
  <p>A better model helps with reasoning and writing, but it cannot give a better answer <a href="https://thenewstack.io/better-context-will-always-beat-a-better-model/">without the right context</a>.</p>
</blockquote>

<p>The <a href="https://www.mixedbread.com/evals#officeqa-pro">Mixedbread OfficeQA-Pro Eval</a> shows the pattern at benchmark scale.
OfficeQA-Pro uses 89,000 pages of financial documents, dense tables, scanned PDFs, and questions that require reasoning across documents.
Giving Codex better search tools reduced tool calls and improved answer quality.</p>

<p><img src="/assets/2026-09-30-retrieval-ai-agent-architecture/img-1.png" alt="A scatter plot mapping Accuracy against Tool Calls for three AI configurations" /></p>

<p>Plain text tools like <code class="language-plaintext highlighter-rouge">grep</code> and <code class="language-plaintext highlighter-rouge">rg</code> work (ish) over flat code files.
They do not work well when context lives in PDFs, tables, chat histories, multi-modal inputs, web results, SaaS records, and permissioned data.
In those cases the agent needs retrieval that can combine exact terms, meaning, metadata, permissions, and ranking quality.</p>

<h2 id="retrieval-needs-traces-and-evals">Retrieval needs traces and evals</h2>

<p>Once retrieval enters the architecture, the next question is whether it finds the right information.</p>

<p>For that you need traces and evals.
For each retrieval step the minimum trace is the input, the outputs, and a way to label whether each output was relevant.</p>

<p><img src="/assets/2026-09-30-retrieval-ai-agent-architecture/img-2.png" alt="Trace and evaluation for one retrieval step" /></p>

<p>For a coding agent using <code class="language-plaintext highlighter-rouge">rg</code> the input is the command, the output is the returned snippets,
and the label says which snippets helped, which were noise, and which relevant files were missing.</p>

<p>For product retrieval the step might be BM25, semantic search, hybrid search with reranking, a generated SQL query, or something else. Capture the query or arguments, the returned documents or chunks, and whether those results were helpful.</p>

<p>Trace each retrieval step by itself, then evaluate the full context-building pass. The local trace answers “Did this query return useful material?” The full trace answers “Did the system collect everything the model needed before generation?” If it did, failures are a generation problem. If not, it’s a retrieval problem.</p>

<p>You cannot know where the failure started or what to fix without traces.</p>

<h2 id="different-failures-need-different-fixes">Different failures need different fixes</h2>

<p>“Improve retrieval” is too broad to be useful because different problems have different solutions. If a relevant document is missing the trace should show where it disappeared: query building, retrieval, filtering, ranking, or final context assembly.</p>

<p><img src="/assets/2026-09-30-retrieval-ai-agent-architecture/img-3.png" alt="Diagnosing where retrieval failed" /><br />
That diagnosis should land on a concrete system change. The right fix depends on what the system was trying to retrieve. A decision-history question needs the decision, the alternatives, and the chats where the team worked through them. A study question depends on the lecture material, deck metadata, semantic matches, and the user’s study context.</p>

<h2 id="the-architecture">The architecture</h2>

<p>Once you trace individual retrieval calls, the full architecture has a simple shape:
fan out to context-building tools, then fan in to generate the final output.</p>

<p><img src="/assets/2026-09-30-retrieval-ai-agent-architecture/img-4.png" alt="Agentic context building flow" /></p>

<h2 id="where-vespa-fits">Where Vespa fits</h2>

<p>Vespa is useful once retrieval has to serve many filtered, ranked requests instead of one broad search call. The app still owns the user request, workflow, and final context budget. Vespa stores the retrievable chunks and serves filtered, ranked evidence back to the app.</p>

<p>That boundary matters because agents rarely make one clean search. They ask follow-up questions, widen or narrow filters, and need enough trace data to explain why a source did or did not reach the model.</p>

<p><img src="/assets/2026-09-30-retrieval-ai-agent-architecture/img-5.png" alt="Vespa-backed retrieval architecture" />
Consider a research agent answering, “What changed in the refund policy after the February outage?” The answer might live across the current policy doc, a support ticket thread, an incident review, and a changelog. The app knows the user, tenant, allowed sources, time window, and why the agent is asking. Vespa serves the evidence.</p>

<p>The same shape applies when the retrievable units are customer records, shipment events, product rows, or permissioned SaaS objects instead of document chunks.</p>

<p>In Vespa, that starts with storage. For policy docs, notes, tickets, chat histories, and long PDFs, the retrievable unit is usually a chunk with a stable source id, chunk id, timestamp, permission fields, metadata, and embedding. For records and SaaS objects, it might be a row or object with the same retrieval fields. The schema makes those fields available for search, filters, ranking, and summaries.</p>

<p>At query time, the app sends the request text, filters, query embedding, and rank profile. Permission and tenant filters narrow the candidate set before ranking. Vespa can retrieve candidates with text matching, nearest-neighbor search, or both. Ranking can favor the current policy doc, exact issue IDs, and semantically related incident notes.</p>

<p>Document summaries return the chunk text, ids, metadata, scores, and source fields the app needs. The app chooses what enters the model context. For debugging, the trace should preserve the query, filters, hits, selected context, answer id, and reviewer labels. Vespa provides query traces and logs; the labeling workflow is usually app-owned.</p>

<h3 id="ranking">Ranking</h3>

<p>Ranking is where Vespa decides which allowed candidates deserve the limited context budget.</p>

<p>Better ranking improves precision (more of the returned chunks are relevant). If the right chunk is ranked 40th and the context only includes the top 10, the system behaves as if retrieval missed it. Returning the top 40 is one answer but it costs a lot of tokens and latency. If the relevant chunks are near the top the system can pass fewer chunks into the model, spend fewer tokens, reduce latency, and give the model less noise.</p>

<p>Semantic similarity is one ranking signal. A chunk with a strong vector score may still be less useful than a newer source, an authoritative source type, or a chunk with an exact error code, file name, ticket ID, or quoted phrase. Permissions and tenant boundaries should already have narrowed the candidate set. Ranking decides how to order the allowed candidates.</p>

<p>A ranker usually uses several kinds of information:</p>

<table>
  <thead>
    <tr>
      <th style="text-align: left">Information</th>
      <th style="text-align: left">What it means</th>
      <th style="text-align: left">How it changes ranking</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td style="text-align: left">Exact text match</td>
      <td style="text-align: left">The chunk contains the same words, names, IDs, error strings, or quoted phrases.</td>
      <td style="text-align: left">Keeps precise terms from being buried by vaguely related semantic matches.</td>
    </tr>
    <tr>
      <td style="text-align: left">Semantic match</td>
      <td style="text-align: left">The query and chunk have similar embeddings, even if they use different words.</td>
      <td style="text-align: left">Finds conceptually related chunks when the wording differs.</td>
    </tr>
    <tr>
      <td style="text-align: left">Metadata</td>
      <td style="text-align: left">Structured fields on the chunk, such as source type, owner, status, or selected collection.</td>
      <td style="text-align: left">Boosts or lowers results based on product context.</td>
    </tr>
    <tr>
      <td style="text-align: left">Freshness</td>
      <td style="text-align: left">The timestamp, version, or update status attached to the chunk.</td>
      <td style="text-align: left">Gives current sources more weight when old context is less useful.</td>
    </tr>
    <tr>
      <td style="text-align: left">Model score</td>
      <td style="text-align: left">A slower model compares the query and candidate after cheaper retrieval.</td>
      <td style="text-align: left">Rescores a smaller candidate set when simple ranking is not enough.</td>
    </tr>
  </tbody>
</table>

<p>The Vespa schema defines the document fields and ranking options. A rank profile is a named scoring recipe in that schema. A profile can combine exact text scores, semantic scores, metadata, values sent with the query, and model scores in one formula.</p>

<p>Vespa separates the query that finds candidates from the rank profile that orders them. A nearestNeighbor query uses the vector index to find candidate chunks near the query embedding. A text query uses term matching. A hybrid query can use both. After that, Vespa scores each candidate so it can return the best ones first. For example, a hybrid rank profile might start with vector closeness plus BM25, then use a slower model such as a cross-encoder to rescore the best candidates.</p>

<h3 id="scale-introduces-more-challenges">Scale introduces more challenges</h3>

<p>With a small corpus a prototype can search and still feel fast. With millions or billions of chunks, every extra retrieval call, candidate, ranking pass, and returned token adds up.</p>

<p>One user request can trigger several retrieval calls in an agentic system. Each of those could be multi-stage retrieval calls (query rewriting, keyword/semantic, ranking, etc). Combine that with many concurrent users and it becomes clear the retrieval layer has to keep quality high while controlling latency and cost.</p>

<table>
  <thead>
    <tr>
      <th style="text-align: left">Scale pressure</th>
      <th style="text-align: left">What has to hold</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td style="text-align: left">More possible matches</td>
      <td style="text-align: left">Permissions, tenants, dates, and source types should reduce the candidate set before ranking.</td>
    </tr>
    <tr>
      <td style="text-align: left">Expensive reranking</td>
      <td style="text-align: left">Stronger models need a controlled number of candidates.</td>
    </tr>
    <tr>
      <td style="text-align: left">Chained retrieval calls</td>
      <td style="text-align: left">Slow tail latency compounds across query rewrites, follow-up searches, and citation lookups.</td>
    </tr>
    <tr>
      <td style="text-align: left">High write volume</td>
      <td style="text-align: left">Freshness matters in small corpora too, but high write volume makes stale context harder to avoid.</td>
    </tr>
    <tr>
      <td style="text-align: left">Harder cost debugging</td>
      <td style="text-align: left">Traces need timing, candidate counts, ranking stages, and returned token counts.</td>
    </tr>
    <tr>
      <td style="text-align: left">Larger returned context</td>
      <td style="text-align: left">Summaries need to stay narrow so every request does not carry unnecessary text.</td>
    </tr>
  </tbody>
</table>

<p>When retrieval and ranking run in the same serving path, filters can shrink the candidate set before expensive ranking, summaries can limit returned text, and traces can show whether latency came from matching, ranking, or response size.</p>

<h3 id="multi-stage-retrieval-is-the-common-shape">Multi-stage retrieval is the common shape</h3>

<p>Most production systems split retrieval into stages, even when the UI looks like one chat box. The trace should show whether the app asked the wrong retrieval question, the candidate set never included the source, filters removed it, ranking buried it, the summary left out needed fields, or context assembly dropped it before generation.</p>

<p>If the query builder chose the wrong filters then changing the embedding model will not help. If the right source was retrieved but ranked low, the ranking must be tweaked. You must be able to drill into any part of the system. That’s much easier if it’s all in one system</p>

<h3 id="vespa-references">Vespa references</h3>

<table>
  <thead>
    <tr>
      <th style="text-align: left">Need</th>
      <th style="text-align: left">Vespa concept</th>
      <th style="text-align: left">Why it matters</th>
      <th style="text-align: left">Docs</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td style="text-align: left">Store searchable units</td>
      <td style="text-align: left">Schema and chunks</td>
      <td style="text-align: left">Defines text, metadata, embeddings, ids, permissions, summaries, and rank profiles.</td>
      <td style="text-align: left"><a href="https://docs.vespa.ai/en/basics/schemas.html">schema</a>, <a href="https://docs.vespa.ai/en/rag/working-with-chunks.html">chunks</a></td>
    </tr>
    <tr>
      <td style="text-align: left">Build constrained queries</td>
      <td style="text-align: left">Query API and YQL</td>
      <td style="text-align: left">Adds filters, permissions, source limits, dates, rank profiles, and query embeddings outside the prompt.</td>
      <td style="text-align: left"><a href="https://docs.vespa.ai/en/querying/query-api.html">Query API</a>, <a href="https://docs.vespa.ai/en/querying/query-language.html">YQL</a></td>
    </tr>
    <tr>
      <td style="text-align: left">Retrieve candidates</td>
      <td style="text-align: left">Text matching and nearest neighbor</td>
      <td style="text-align: left">Supports exact terms, domain vocabulary, semantic search, and hybrid retrieval.</td>
      <td style="text-align: left"><a href="https://docs.vespa.ai/en/querying/text-matching.html">text matching</a>, <a href="https://docs.vespa.ai/en/ranking/bm25.html">BM25</a>, <a href="https://docs.vespa.ai/en/querying/nearest-neighbor-search.html">nearest neighbor</a></td>
    </tr>
    <tr>
      <td style="text-align: left">Rank results</td>
      <td style="text-align: left">Rank profiles and phased ranking</td>
      <td style="text-align: left">Combines cheap first-pass ranking with stronger second-phase or global reranking.</td>
      <td style="text-align: left"><a href="https://docs.vespa.ai/en/ranking/phased-ranking.html">phased ranking</a></td>
    </tr>
    <tr>
      <td style="text-align: left">Return usable context</td>
      <td style="text-align: left">Document summaries</td>
      <td style="text-align: left">Controls which chunks, metadata, ids, scores, and source fields come back to the agent.</td>
      <td style="text-align: left"><a href="https://docs.vespa.ai/en/querying/document-summaries.html">document summaries</a></td>
    </tr>
    <tr>
      <td style="text-align: left">Debug failures</td>
      <td style="text-align: left">Query tracing and app logs</td>
      <td style="text-align: left">Shows the query, filters, hits, ranking, selected context, and answer id.</td>
      <td style="text-align: left"><a href="https://docs.vespa.ai/en/performance/practical-search-performance-guide.html#advanced-query-tracing">query tracing</a></td>
    </tr>
    <tr>
      <td style="text-align: left">Build eval data</td>
      <td style="text-align: left">App-owned annotation store</td>
      <td style="text-align: left">Keeps relevance labels tied to query ids, document ids, chunk ids, and answer ids.</td>
      <td style="text-align: left">App-owned</td>
    </tr>
    <tr>
      <td style="text-align: left">Build a full RAG app</td>
      <td style="text-align: left">RAG blueprint</td>
      <td style="text-align: left">Shows schema design, chunks, query profiles, hybrid retrieval, phased ranking, and ranking evals.</td>
      <td style="text-align: left"><a href="https://docs.vespa.ai/en/learn/tutorials/rag-blueprint.html">RAG blueprint</a></td>
    </tr>
  </tbody>
</table>

<p>Whatever stack you use, when an answer is wrong you should be able to see whether the source was never retrieved, filtered out, ranked too low, or dropped before generation.
Vespa is strongest when that diagnostic path needs to run over large, changing corpora with ranking, filtering, and serving constraints built into the same retrieval layer.</p>
]]></content:encoded>
        <pubDate>Wed, 30 Sep 2026 00:00:00 +0000</pubDate>
        <link>https://blog.vespa.ai/retrieval-ai-agent-architecture/</link>
        <guid isPermaLink="true">https://blog.vespa.ai/retrieval-ai-agent-architecture/</guid>
        
        <category>AI Agents</category>
        
        <category>AI Engineering</category>
        
        <category>AI Infrastructure</category>
        
        
      </item>
    
      <item>
        <title>Deploy, Discover, Inspect, Observe: A Summer Spent Making a Public Vespa MCP Server</title>
        <description>We built a standalone Vespa Cloud MCP server as a summer interns project</description>
        
        <media:thumbnail url="https://blog.vespa.ai/assets/2026-08-27-public-mcp-interns/title-image.png" />
        
        <content:encoded><![CDATA[<p>This summer, we built a standalone, public Model Context Protocol (MCP) server for Vespa Cloud. It lets AI assistants, such as <a href="https://claude.ai">Claude</a> or <a href="https://openai.com/codex/">Codex</a>, interact directly with your Vespa Cloud applications. Alongside this, we looked at how to evaluate an agent’s use of MCP servers and built a harness for scoring it against simply handing the assistant a terminal and access to Vespa CLI.</p>

<p>In this post, we want to tell you about our summer internship at Vespa, and what we managed to build. We will quickly explain what the Model Context Protocol is, and what its benefits and limitations are, before showing off our implementation’s capabilities and discussing our experiences.</p>

<h1 id="what-is-mcp">What is MCP?</h1>
<p>So, what actually is the <a href="https://modelcontextprotocol.io/docs/latest/getting-started/intro">Model Context Protocol</a>? Plainly, it is an open standard for how to connect AI assistants to external data sources, systems, and environments. Prior to MCP, letting an agent interact with a system meant a custom integration for that specific agent to that specific system. Now, you design and implement the service once, and then any compatible LLM or AI agent can connect to the system.</p>

<p>MCP enables this by standardizing the interface between a system and any assistant that wants to use it. A service (Vespa Cloud in our case) implements an MCP server and exposes capabilities in three distinct ways:</p>
<ul>
  <li><strong>Resources</strong>: read-only context for the LLM to understand the system</li>
  <li><strong>Tools</strong>: executable actions the assistant can invoke to actually operate the system</li>
  <li><strong>Prompts</strong>: reusable templates or shortcuts for structuring interactions with language models</li>
</ul>

<p>Our implementation mainly focuses on implementing tools and letting an LLM take advantage of the right resources. Later, you can read about why designing tools is both difficult to get right and important for how an assistant uses an MCP server.</p>

<p>This is not the first time people at Vespa have looked into the possibilities of the Model Context Protocol. <a href="https://blog.vespa.ai/interns-mcp-server/#what-even-is-an-mcp-server?">Last year’s interns</a> implemented a server that ships with the application package, and there also exists an internal tool that runs locally, which served as great inspiration. What makes our approach unique is that the server is standalone and can be publicly hosted, meaning that any user with a Vespa Cloud account can connect to the server and have access to their own tenants and applications.</p>

<p>The distinction matters more than it might first appear. An application-specific server has to be shipped together with one application and typically only sees that application. This means that every application that wants an MCP connection has to ship a new server. A locally run MCP server requires installation and needs to be kept up to date on each user’s machine. Our server requires neither. It runs as a publicly hosted service, requiring no install step and no separate server to run each distinct application. The tradeoff, discussed later, is that a publicly hosted server is currently unable to securely conduct most Data Plane operations, and this is why our server currently targets the Control Plane.</p>

<h2 id="mcp-tool-design">MCP Tool design</h2>
<p>Designing good MCP tools requires many considerations. Working with a non-deterministic model makes it much harder to predict what even minor changes to tools will do to the end result. <a href="https://www.anthropic.com/engineering/writing-tools-for-agents">Anthropic</a> provides a variety of guidelines for designing tools, which we have used, along with our own experiences, to shape our toolset. We will walk through the design of the <code class="language-plaintext highlighter-rouge">vespa_get_logs</code> tool as an example of our design process.</p>

<p>Before an LLM can use a tool, it must know it exists and how to call it. In MCP, this is provided by the <a href="https://modelcontextprotocol.io/specification/2026-07-28/server/tools#listing-tools">Tool Description</a>. Our experience, reflecting that of <a href="https://www.cloudquery.io/blog/mcp-server-gotchas-we-learned-the-hard-way">others</a>, was that small wording and format changes could significantly improve the likelihood of correct tool use. A good example is the <code class="language-plaintext highlighter-rouge">time_range</code> parameter in our <code class="language-plaintext highlighter-rouge">vespa_get_logs</code> tool.</p>

<p>Our first implementation required an agent to supply epoch milliseconds directly, giving the timeframe in absolute terms. This mostly worked, but the agent was prone to miscalculations. Using relative expressions like “now-1h”, and falling back to ISO 8601 datetime strings when needed, seemed to help. Most queries are relative anyway, and such strings are likely more frequent in agents’ training data than raw epoch integers. We learned that you should prefer parameter formats that match the use case and the model’s training data.</p>

<p>Protecting the agent’s context window is also critical. A naive implementation of the log tool would return the entire log for a given timeframe straight from the Vespa Cloud API. Logs produce large volumes of data, most of which is irrelevant to any given question. Feeding all of it to the agent bloats the context window, which slows down reasoning and drives up cost, without necessarily improving the answer. To address this, the tool lets the LLM choose how much pre-processing it wants: a <code class="language-plaintext highlighter-rouge">detail</code> parameter toggles between a compact summary and raw log lines, and a <code class="language-plaintext highlighter-rouge">severity</code> filter excludes lower-priority entries like info logs. This way, the agent can start with a lightweight summary and only request the full detail if the situation calls for it.</p>

<p>This culminated in the following tool description:</p>
<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nd">@mcp.tool</span><span class="p">(...)</span>
<span class="k">async</span> <span class="k">def</span> <span class="nf">vespa_get_logs</span><span class="p">(...):</span>
    <span class="sh">"""</span><span class="s">Search a deployed instance</span><span class="sh">'</span><span class="s">s logs. Defaults to warning-and-above from the last hour.

    Repeated events (identical except for their timestamp) are collapsed into
    one entry carrying `count` and `first_seen`. The response is capped to a
    fixed size budget; when it overflows, the oldest events drop first.

    Args:
        start_time:
            Start of the time range.
            Accepts relative expressions (</span><span class="sh">"</span><span class="s">now-1h</span><span class="sh">"</span><span class="s">, </span><span class="sh">"</span><span class="s">now-7d</span><span class="sh">"</span><span class="s">) or ISO datetime strings.
            Defaults to </span><span class="sh">"</span><span class="s">now-1h</span><span class="sh">"</span><span class="s">.
        end_time:
            End of the time range.
            Accepts the same formats as start_time. Defaults to </span><span class="sh">"</span><span class="s">now</span><span class="sh">"</span><span class="s">.
        min_level:
            Minimum severity, most-to-least severe:
            fatal, error, warning, info, config, event, debug, spam.
            Pass </span><span class="sh">"</span><span class="s">info</span><span class="sh">"</span><span class="s"> for normal logs, None for all.
        contains:
            Case-insensitive substring over service, component, and message.
        limit:
            Max events returned, most recent kept (default: 100, range: 1-100).
            Values outside this range are clamped and reported in the
            response warnings.
        detail:
            </span><span class="sh">"</span><span class="s">concise</span><span class="sh">"</span><span class="s"> (time, level, service, message)
            or </span><span class="sh">"</span><span class="s">detailed</span><span class="sh">"</span><span class="s"> (adds host, pid, component).
</span><span class="gp">        ...</span>
    <span class="sh">"""</span>
</code></pre></div></div>
<p>Both point to the same conclusion: good tool design is less about exposing everything an API can do, and more about shaping the interface so the agent makes good decisions when deciding how to use tools.</p>

<h2 id="benefits">Benefits</h2>
<p>Now, why would having a publicly hosted Vespa MCP server be useful? One might think that just handing an LLM the Vespa CLI and a terminal would give it the same capabilities as having an MCP server. That is not quite true. What separates the two approaches is <em>control</em>, and as it turns out, <em>capability</em> as well.</p>

<p>Let’s start with control. A tool-based interface lets us define how an agent is able to use the access we provide. We can decide what actions are possible, instruct on when to use certain tools, and ensure that unwanted actions are impossible. This control starts before a tool is even called. Each tool defines the exact structure of the input it expects, making it easier for the agent to know how the tool should be used in the first place. This results in fewer attempts needed to call it correctly, as opposed to a trial-and-error approach towards the CLI.</p>

<p>Structuring available tools and giving the necessary context also guides the LLM toward how to solve problems in the first place. As we will see later, this also results in the agent making fewer mistakes. It will always know what actions are available to it and have information about how to use different tools. On top of this, the server chooses how to present error messages, meaning that tools fail informatively and hand the assistant the next step for fixing the problem, rather than the model having to interpret raw error messages.</p>

<p>An MCP server is not limited to wrapping existing commands; it can expose entirely new capabilities built specifically for how an assistant uses it. Our tools surface Grafana metrics directly, giving the assistant real observability data with no current CLI equivalent. More on what possibilities this provides is elaborated on later in the post.</p>

<p>Lastly, a server is convenient. Connecting an LLM to a hosted MCP server is basically plug and play. Meaning that both new and experienced developers can start using it in a matter of minutes. For new Vespa devs, having an MCP server to help your AI assistant poke at Vespa, explain features, and teach you how to design applications becomes invaluable. For the experienced dev, letting the LLM handle time consuming jobs, such as debugging why an application has poor quality of service, opens up time for other tasks that are more important.</p>

<h2 id="mcp-limitations">MCP Limitations</h2>

<p>During our development of the MCP server, we experienced some protocol limitations we had to work around.</p>

<p>The first case occurred when designing the tool for deploying applications. An application package consists of several files that define the application; node structure, schemas, authentication, etc. The challenge was how the LLM should upload these files to the MCP server. There exists no built-in way to upload files in MCP today, so we ended up exploring several possible solutions.</p>

<p>Our first attempt let the LLM upload a JSON object with a dictionary mapping from file names to file contents. This approach was simple and required only one tool call, making it fast for a single deployment. On the other hand, small tweaks to applications required the LLM to rewrite the entire package, and uploading binary files, such as embedding models, was not possible. Therefore, we had to look into other solutions.</p>

<p>Most agents have access to some sort of terminal, either locally or in a sandbox environment. Therefore, we assume that the agent is able to zip the application package, but how do we send this over the MCP server? We tried Base64 encoding the files and using them as input for the tool. Copying encoded data is, however, something LLMs <a href="https://arxiv.org/html/2601.03640v1">struggle to do accurately and quickly</a>. So, our second attempt wasn’t satisfactory either.</p>

<p>Our solution was to avoid the protocol altogether. Instead, the tool returns a unique URL to a separate HTTP endpoint for uploading the package. Then the LLM can zip the package and use <code class="language-plaintext highlighter-rouge">curl</code> to upload. On one hand, this means more tool calls and slower execution for a single deployment. On the other, it ends up being faster with several deployments, as it does not have to rewrite the entire package to make small adjustments. From our experience, several deployments are often needed, making us prefer this solution over the others.</p>

<p>The second limitation we faced while developing the server was also related to the deployment tool. The time it takes to deploy a Vespa application can vary from seconds to several minutes. Therefore, we had to decide if the MCP tool should return immediately and have the LLM poll for the deployment result with a separate tool, or block until the deployment is finished.</p>

<p>Letting an LLM poll the status means many tool calls per deployment, costing valuable tokens, and often resulting in the assistant just setting a timer after a few attempts. The deployment might, however, finish early during the timer, leaving the LLM to do unnecessary waiting. In contrast, letting the tool block might keep the agent from working in parallel. It also gives no feedback to the user, leaving room for confusion as to why the tool appears frozen.</p>

<p>We decided that having the tool block was best, but added a timeout as a parameter so it doesn’t wait too long. This way, there is some feedback along the way, and the LLM can choose to do other tasks while deploying. It still requires several tool calls for a deployment, so the solution is not optimal. During the summer, a new mechanism for <a href="https://blog.modelcontextprotocol.io/posts/2026-07-28/">“long running tasks”</a> was added to MCP, which seems to solve our problem. Once this is adopted by most MCP clients, the deployment tool will probably be refactored to use this feature.</p>

<h1 id="using-the-mcp-server">Using the MCP server</h1>
<p>Using the MCP server, we could now create a Vespa application from scratch only through prompts. In this case, we started with a simple prompt about wanting a blog application with Vespa serving as the search engine. In a few minutes the model created the Vespa Cloud application and a web page, then connected the two. To tune the search, we just needed to specify what results we wanted to surface for a given query, and the model would make appropriate changes to the schema and redeploy.</p>

<p><img src="/assets/2026-08-27-public-mcp-interns/demo-llm-generated-app.gif" alt="App Demo" /></p>

<p>This showcases one of the more interesting use cases for our MCP server. Simply explain to the LLM what kind of application you want, and in a matter of minutes it is up and running. Perfect for new Vespa developers who want to get started, understand how Vespa works, and see what possibilities it provides.</p>

<h1 id="extending-the-capabilities-with-metrics">Extending the capabilities with metrics</h1>
<p>Thus far, most of the functionality of the MCP server can also be done using the Vespa CLI. The next step was to add something beyond what Vespa CLI can do currently. In the Vespa Cloud Console, there is a Grafana metrics dashboard for each application, with hundreds of metrics for any time window. Each metric panel has a short explanation of how to interpret the data. Giving the MCP server access to all of these metrics is useful for debugging and monitoring applications.</p>

<p><img src="/assets/2026-08-27-public-mcp-interns/vespa-grafana.gif" alt="App Demo" /></p>

<p>To implement this, we first looked at what API endpoints Vespa has for metrics. We quickly realized that all the endpoints only gave the metrics at the current time, with no way of looking at the metrics for the last week or month. The capability of looking at metrics for a specified time would require storing all metrics, for all timestamps, for all applications, at all times. This approach was unnecessarily complex, and it would essentially mean duplicating the existing functionality of Grafana.</p>

<p>Instead, we started looking at how the Vespa Cloud dashboard gets all its metrics. After some reverse engineering, we found the endpoints used to get the structure of the dashboard and also how to query the actual metrics data. Then, the only thing needed was to add some tools so that the LLM could get access to the panels with descriptions and data, directly from Grafana.</p>

<p>When this was implemented, we wanted to see what it could help us do. We started by consulting an LLM on whether or not our application needed to be scaled up. After looking at several metrics of resource usage, it correctly decided that it does not need upscaling.</p>

<p><img src="/assets/2026-08-27-public-mcp-interns/demo-scale-up.gif" alt="App Demo" /></p>

<p>Next, we tried to restart one of the content nodes of an application, to see if the LLM could explain the sudden increase in the number of active documents. After 17 tool calls checking different metrics, deployments, and logs, the LLM was able to correctly identify that a node was restarted, in addition to explaining that it was likely done by a Vespa Cloud host.</p>

<p><img src="/assets/2026-08-27-public-mcp-interns/demo-debug-app.gif" alt="App Demo" /></p>

<p>Looking through metrics data manually can be time-consuming, but from our testing, the LLM seems to be good at crunching through the numbers. This shifts the role of the developer from diagnosing to fixing. We are excited to see if this can be useful in real world scenarios.</p>

<h1 id="authentication-issues-and-server-hosting">Authentication Issues and Server Hosting</h1>
<p>An internet-exposed MCP server requires tight security, as anyone can send requests to it. We have to treat each incoming request as potentially malicious, while also serving legitimate requests. Vespa Cloud handles authentication in two different ways, based on the type of information you want to access. Our server needed both in order to get the functionality we wanted.</p>

<p>The <a href="https://docs.vespa.ai/en/security/guide.html#control-plane">Control Plane</a> was the easy half. It authenticates with Auth0, so we could re-use the same logic used by the Vespa Cloud Console, where one authentication flow gives the user access to their applications. The MCP Server would act as an OAuth proxy in front of Auth0, where clients register with the MCP server before they’re handed off to Auth0 for the actual authentication.</p>

<p>The <a href="https://docs.vespa.ai/en/security/guide.html#data-plane">Data Plane</a> is where things got messy. To query a Vespa Cloud application, you need a separate token, scoped per application, so the Auth0 magic simply doesn’t work here. Our workaround, for testing purposes, was to hardcode a pre-generated token into the server. It worked for the single tenant we tested against, but wouldn’t scale to multiple users from different tenants.</p>

<p>The solution for this was not quite obvious, so we explored some options. The simplest fix would be to require the tools to provide a token parameter; however, this is blatantly insecure and the MCP spec disallows it. Another was to mint temporary Data Plane tokens using your Control Plane authentication; however, this would require changes to how tokens are minted, which was out of scope for our summer project. We ultimately settled on our workaround until a cleaner solution is available.</p>

<p>Authentication to third party APIs (e.g., the Vespa Cloud API) seems to be an ongoing discussion in the MCP protocol, with recent developments like URL elicitation representing early attempts at addressing it.</p>

<p>One of the goals we had for this project was to host the MCP server ourselves. During the summer, the <a href="https://blog.modelcontextprotocol.io/posts/2026-07-28/">2026-07-28 MCP</a> spec was released, which removed protocol level sessions along with the initial handshake. This change made hosting considerably easier. With this change, any request could be served by any instance, so the server deploys and scales like more traditional HTTP APIs, instead of needing the sticky routing or shared state that was required previously.</p>
<h1 id="does-it-actually-help">Does it actually help?</h1>
<p>Handing an LLM a big toolbox does not automatically mean it will use it well, or even better than it would have without the toolbox at all. We therefore wanted to find out:</p>
<blockquote>
  <p>Does having this MCP server actually help an LLM operate a Vespa Cloud application better than just giving it the Vespa CLI and a terminal?</p>
</blockquote>

<p>Evaluating that, however, turned out to be easier <a href="https://www.anthropic.com/engineering/demystifying-evals-for-ai-agents">said than done</a>. There’s rarely one correct sequence of tool calls for a task. For example, an LLM might discover a schema by first listing applications or simply by jumping to <code class="language-plaintext highlighter-rouge">vespa_list_schemas</code> if it has already guessed the application’s name, and both can be perfectly good. Worse, an LLM’s behaviour would change from run to run. Two different runs on the same suite, with the same code, might yield two completely different results, making the “correct solution” even harder to define. Any single run is a weak signal, and the “answer” is not something you can hard-code.</p>

<p>So, we grade two ways. Deterministic assertions check the <strong>outcome</strong> against the real Vespa Cloud API: does the application exist? Are its nodes healthy? Does the deployed schema contain the fields it should? An LLM judge scores the <strong>process</strong> against a per-scenario rubric: did the agent reach for the right concepts? Did it fix the actual bug rather than paper over it? We found that neither is sufficient alone. Assertions can’t distinguish a lucky guess from sound reasoning, and an agentic judge should not be trusted with facts that can be checked deterministically.</p>

<p>Across three full sweeps of the evaluation scenarios, both agents mostly got there in the end. The MCP agent passed 97% of the deterministic assertions versus the CLI agent’s 95%. Seeing this, you might be tempted to think that the tools barely help.</p>

<p>We don’t think so, and the giveaway is the number of tries it took to get there. Agents using the MCP server averaged less than half the number of attempts before deploying successfully. Further, the use of MCP resulted in more scenarios passing cleanly, resolving faster, and averaging a better rubric score when judged by another LLM.</p>

<p><img src="/assets/2026-08-27-public-mcp-interns/mcp-comparison-graph.png" alt="Mcp comparison graph" /></p>

<blockquote>
  <p>Tools don’t make the model smarter; they make it wrong less often.</p>
</blockquote>

<p>A determined enough model will eventually find the correct path to make it work, regardless of interface. However, the MCP server buys you fewer wrong turns, even if it costs a few more tokens.</p>

<p>A model typically needs to discover the tools before using them, resulting in actions that don’t produce task progress. The extra cost comes from every tool’s description having to be held in context on every turn, whether or not they are used. An assistant using the CLI carries none of this overhead. It already knows or guesses the commands and has no need to discover them or keeping track of the descriptions. However, mistakes are not free either: a malformed deploy still costs time and tokens, and has to be fixed afterwards.</p>

<p>Meaning, it is not so much “cheap CLI” versus “expensive MCP” as it is two ways of spending the same budget. Either up front on discovery and structure, or on the back end recovering from mistakes. We’d expect that trade to favor the MCP server more as tasks get harder and wrong turns become more costly.</p>

<h1 id="whats-missing">What’s missing</h1>
<p>The most obvious missing part is proper Data Plane access. The workaround we created limited us to one hardcoded tenant, so it’s not ready for production. That said, our explorations suggest a Control Plane only server is feasible without too much additional effort.</p>

<p>Furthermore, the whole server is undergoing a security review to ensure it is up to our standard before a possible public release. We are also taking a harder look at what tool guardrails we should include to ensure safe usage.</p>

<p>Lastly, we need feedback from real people using the MCP server. One of the hardest challenges in creating the server has been figuring out what tools to include, and feedback from actual Vespa Cloud users would help a lot.</p>

<h1 id="our-summer-at-vespa">Our Summer at Vespa</h1>
<p>After a summer spent exploring the Model Context Protocol, with both its limitations and strengths, we end with a working MCP prototype that lets you deploy, discover, inspect, and observe your Vespa Cloud applications. We have had to solve problems facing a continuously evolving protocol and implement functionality not already existing in the current ecosystem. Overall, we are proud of what we have created, and hope that it can be of use to Vespa Cloud developers in the near future.</p>

<p>Finally, we would like to thank all our colleagues at Vespa for trusting four interns with a surprising amount of freedom and production access, as well as a lot of patience for all of our questions. We have had the opportunity to learn an incredible amount this summer, and we wish the whole team further good luck.</p>

]]></content:encoded>
        <pubDate>Sun, 13 Sep 2026 00:00:00 +0000</pubDate>
        <link>https://blog.vespa.ai/public-mcp-interns/</link>
        <guid isPermaLink="true">https://blog.vespa.ai/public-mcp-interns/</guid>
        
        <category>mcp</category>
        
        
        <category>internships</category>
        
      </item>
    
      <item>
        <title>Vespa Newsletter, September 2026</title>
        <description>Advances in Vespa include time-constrained ANN search, sub-query ranking support, flexible provisioning, new rank features and telemetry export
</description>
        
        <media:thumbnail url="https://blog.vespa.ai/assets/logo/logo-pi.jpg" />
        
        <content:encoded><![CDATA[<p>Welcome to the latest edition of the Vespa newsletter.
In the <a href="https://blog.vespa.ai/vespa-newsletter-may-2026/">previous update</a>, we announced Vespa Cloud operability features
like Backup and Dashboards together with new Matching and Ranking features,
and <a href="https://blog.vespa.ai/vespa-newsletter-may-2026/">more</a>.</p>

<p>This month, we’re shipping updates that give you time-constrained ANN search, sub-query ranking support, flexible provisioning, new rank features and telemetry export.</p>

<p>Let’s dive into what’s new.</p>

<h3 id="calling-all-vespians-vespaai-live-has-landed">Calling All Vespians: Vespa.ai Live Has Landed</h3>

<p><img src="/assets/2026-05-26-newsletter/Vespa.ai-live.png" alt="Vespa.ai Live" /></p>

<p>As more teams build with Vespa, bringing the community closer together has become a major focus for us.
Earlier this year, we ran our first virtual meetups, extending our Slack community and were joined by more than 100 Vespians from around the world -
from the US and Ukraine to Singapore, Australia, Kazakhstan, Egypt, and beyond.
But while virtual events are great, nothing quite compares to meeting in person -
learning from peers, exchanging ideas, and continuing the conversation over coffee, beer, or wine.
That’s why we’re excited to announce our first in-person community meetup: Vespa.ai Live!</p>

<p>Vespa.ai Live brings together the engineers, architects, researchers, and practitioners building modern retrieval and ranking systems.
The event includes technical sessions, real-world user stories, expert panels, interactive unconference discussions,
and plenty of opportunities to connect with others building in this space.
Leading authors Trey Grainger and Doug Turnbull will also be in the lineup,
sharing their perspectives on where the industry is heading.
Optional pre-event training the day before includes beginner and advanced tracks
to help attendees sharpen their Vespa skills before the main event.</p>

<p>Most of all, Vespa.ai Live is intended to be community-driven - where Vespians share lessons learned and boldly go beyond the frontier of modern search.</p>

<p><a href="https://content.vespa.ai/vespa-live">Learn more about Vespa Live!</a></p>

<h3 id="product-updates">Product updates</h3>

<h4 id="meet-latency-targets-more-easily-with-time-bounded-vector-search">Meet latency targets more easily with time-bounded vector search</h4>
<p>The standard HNSW algorithm for vector search explores the graph to find the closest vector matches,
using exploration parameters like <a href="https://docs.vespa.ai/en/reference/querying/yql.html#targethits">targetHits</a>
and <a href="https://docs.vespa.ai/en/reference/api/query.html#ranking.matching.filterFirstExploration">filterFirstExploration</a>
to determine how extensively it searches.
You often need to tune these parameters to achieve a particular latency budget, which can be challenging.</p>

<p><strong>What’s new:</strong> Vespa now provides a simpler alternative where you set the latency budget and let the algorithm find the best matches within that time and then return.</p>

<p>To use it, set the new <a href="https://docs.vespa.ai/en/reference/api/query.html#ranking.matching.anntimebudget">ranking.matching.anntimebudget</a> query parameter
to the amount of time you want to allow for ANN search.
Vespa will stop searching when it reaches that limit,
eliminating the need to tune exploration parameters to indirectly meet a latency target.</p>

<p><strong>Pro-tip:</strong> Use the new <a href="https://docs.vespa.ai/en/reference/operations/metrics/searchnode.html#content_proton_documentdb_matching_query_approximate_nns_time">query_approximate_nns_time</a> metric
to track the time spent on ANN.
Because this metric only counts queries that actually perform ANN searches,
its <em>count</em> dimension gives you the precise number of queries that performed ANN searches!
This way, you can monitor how many queries actually perform ANN searches and how many fall back to an exact search.</p>

<h4 id="keep-clusters-scaling-when-cloud-providers-capacity-is-limited-with-max-cost-factor">Keep clusters scaling when cloud providers’ capacity is limited with max-cost-factor</h4>
<p>In Vespa Cloud, <em>resource specifications</em> configure settings like vCPU, memory and disk for a cluster. Example:</p>

<div class="language-xml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nt">&lt;resources</span> <span class="na">vcpu=</span><span class="s">"32"</span> <span class="na">memory=</span><span class="s">"64Gb"</span> <span class="na">disk=</span><span class="s">"1900Gb"</span><span class="nt">&gt;</span>
</code></pre></div></div>

<p>Cloud providers may not always have the instance types needed to match your resource specifications.
When the required resources are unavailable, Vespa isn’t able to add the necessary capacity,
which blocks deployment and autoscaling.</p>

<p><strong>What’s new:</strong> Vespa Cloud now lets you prioritize availability over cost when your preferred instance type isn’t available.
With the new <a href="https://docs.vespa.ai/en/reference/applications/services/services.html#resources">max-cost-factor</a> setting,
you can configure Vespa to select larger instance types that meet your resource requirements instead of blocking cluster re-scaling.</p>

<p>E.g., with a <em>max-cost-factor=2</em>, an instance like</p>

<div class="language-xml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nt">&lt;resources</span> <span class="na">vcpu=</span><span class="s">"64"</span> <span class="na">memory=</span><span class="s">"128Gb"</span> <span class="na">disk=</span><span class="s">"3800Gb"</span><span class="nt">&gt;</span>
</code></pre></div></div>

<p>matches the specification in the first example.</p>

<p>Note that <a href="https://docs.vespa.ai/en/performance/node-resources.html#automated-instance-type-migration">auto migration to optimal instance types</a>
is enabled by default in Vespa Cloud for all applications,
and runs in the <a href="https://docs.vespa.ai/en/reference/applications/deployment.html#block-change">maintenance window</a> only,
for off-peak configuration.
This continuously migrates workloads to higher-performance nodes for a better cost/performance ratio,
and automates the chore of optimizing your resource pool.
So even if you have temporarily provisioned an oversized instance due to low availability,
Vespa Cloud will move you back to the most cost efficient instance type when available.</p>

<h4 id="gain-more-control-over-how-query-signals-affect-ranking-with-labels">Gain more control over how query signals affect ranking with labels</h4>
<p>Not all parts of a query should contribute equally to ranking.
<em>Labels</em> give you a way to identify those parts and score them independently.
For example, you might want to add a smaller rank contribution for alternative forms.
Labeling the parts lets you do this, and use the values in BM25 scoring,
or in a general ranking expression using the raw scores.</p>

<p><strong>What’s new:</strong> Vespa gives you more control over how each part of a query affects ranking.
You can score BM25 for specific labeled terms, get separate BM25 scores per query-item label,
or assign scores to labeled subqueries and use those scores in a ranking expression.</p>

<h5 id="scoring-labeled-parts-of-the-query---bm25field-name-label-label">Scoring labeled parts of the query - bm25(field: name, label: label)</h5>
<p><em>bm25(content)</em> sums the contribution of every query term searching content.
When a query is built from several parts, it is often useful to score those parts separately.
This is done by attaching a <a href="https://docs.vespa.ai/en/ranking/multivalue-query-operators.html#raw-scores-and-query-item-labeling">label</a>
to the query items, using the YQL <a href="https://docs.vespa.ai/en/reference/querying/yql.html#label">label</a> annotation,
and referring to that label from the rank profile:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>select * from example where
  ({label:"must"}content contains "vespa") and
  ({label:"nice"}(content contains "ranking" or content contains "relevance"))
</code></pre></div></div>

<p>A label set on an operator enclosing others, as on the or above, is inherited by every term inside it,
so a label can be given per clause instead of per term.
The <em>bm25(field: fieldname, label: label)</em> rank feature scores only the terms carrying the given label:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>rank-profile labeled {
  first-phase {
    expression: bm25(field: content, label: must) + 0.5 * bm25(field: content, label: nice)
  }
}
</code></pre></div></div>

<p>Since Vespa 8.738. <a href="https://docs.vespa.ai/en/reference/ranking/rank-features.html#bm25(field,label)">Read more</a>.</p>

<h5 id="scoring-labeled-parts-of-the-query---bm25_for_labelsname">Scoring labeled parts of the query - bm25_for_labels(name)</h5>

<p>Return a <em>tensor&lt;float&gt;(label{})</em> holding one BM25 score per query item label in the indexed string field name.
Each cell is labeled with the query item label and holds the sum of the BM25 scores of that label’s terms in name.
This makes it possible to rank on the labeled parts of the query individually
without adding one <em>bm25(field, label)</em> feature per label.
A label gets a cell only if it actually scores, that is when at least one term carrying it searches name and matches the document.</p>

<p>Since Vespa 8.738. <a href="https://docs.vespa.ai/en/reference/ranking/rank-features.html#bm25_for_labels">Read more</a>.</p>

<h5 id="the-labeled-query-operator---itemrawscore">The labeled() query operator - itemRawScore</h5>
<p>Using <em>labeled(subQuery, “mylabel”, 4.25)</em> wraps an arbitrary sub-query with a label “mylabel” and score.
If the sub-query matches a document, the score (4.25 in this example) can be picked up by ranking expressions
using the <a href="https://docs.vespa.ai/en/reference/ranking/rank-features.html#itemRawScore(label)">itemRawScore</a> rank feature.
Typically used together with the <a href="https://docs.vespa.ai/en/reference/querying/yql.html#rank">rank</a> operator:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>where rank(title contains "running shoes",
  labeled(memberDiscountPercent &gt;= 20 AND inStock &gt; 0, "memberDiscount", 2.0),
  labeled(premiumBrand = true OR todaysFeaturedBrand = true, "premiumOrFeaturedBrand", 1.0))
</code></pre></div></div>

<p>A ranking profile example using the scores:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>expression: bm25(title) + itemRawScore(memberDiscount) + 31.5 * itemRawScore(premiumOrFeaturedBrand)
</code></pre></div></div>

<p>This is similar to Lucene’s <em>ConstantScoreQuery</em>, often used in Elasticsearch/OpenSearch through <em>function_score</em>,
or in Solr with ^= query syntax.</p>

<p>Since Vespa 8.748.3. <a href="https://docs.vespa.ai/en/reference/querying/yql.html#rank">Read more</a>.</p>

<h4 id="build-and-debug-custom-ranking-with-more-flexibility-through-new-rank-features">Build and debug custom ranking with more flexibility through new rank features</h4>
<p>Building sophisticated ranking functions often requires working with data that isn’t readily available in the form you need,
whether that means connecting values across parallel arrays
or accessing the collection statistics used by text ranking algorithms like BM25.</p>

<p><strong>What’s New:</strong> Vespa now has three new rank features:</p>

<ul>
  <li><a href="https://docs.google.com/document/d/1ugigJQWUtfzs8z5wr9p6Sa69_FhbI9vIZS9_Zus5PFQ/edit?tab=t.0#heading=h.o4nrfxg4ezfx">tensorFromLabelsWithOffset</a>:
constructs a tensor from an array attribute while preserving array positions.
That tensor can then be used in ranking calculations.</li>
  <li><a href="https://docs.google.com/document/d/1ugigJQWUtfzs8z5wr9p6Sa69_FhbI9vIZS9_Zus5PFQ/edit?tab=t.0#heading=h.ycd1yw85t1m5">averageFieldLength</a>:
gives you the average length of an indexed field, which BM25 uses to normalize scores,
so you can use it for debugging or custom text ranking formulas.</li>
  <li><a href="https://docs.google.com/document/d/1ugigJQWUtfzs8z5wr9p6Sa69_FhbI9vIZS9_Zus5PFQ/edit?tab=t.0#heading=h.pocbcv456hdr">queryTermDocumentFrequency</a>:
exposes the number of documents that contain each query term, which BM25 uses to calculate IDF,
so you can use it for debugging or in custom ranking formulas.</li>
</ul>

<p>These features give you more flexibility to work with structured data, understand how ranking scores are calculated, and build custom ranking logic.</p>

<h5 id="tensorfromlabelswithoffset">tensorFromLabelsWithOffset</h5>
<p>Creates a <em>tensor&lt;float&gt;</em> with one mapped dimension with labels from the given array attribute,
and another mapped dimension where labels are constructed from the array index (offset) of that label.
The cell values of the tensor are always 1.0 (suitable for multiplication).
The attribute values must be integers or strings.
The attribute is specified as the full feature name, <em>attribute(name)</em>.
The label-dimension parameter is required, using the same as the attribute name is common.
The offset-dimension will get label value “0”, “1” and so on.
This feature may be useful if there are several arrays populated in parallel,
and the array index of a label is needed in order to correlate with a different array.
Example: Given an attribute field <em>myField</em> containing the array value:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>[v1, v2, v3]
</code></pre></div></div>

<p><em>tensorFromLabelsWithOffset(attribute(myField), dim, off)</em> produces:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>tensor&lt;float&gt;(dim{},off{}):{ {dim:v1,off:0}:1.0, {dim:v2,off:1}:1.0, {dim:v3,off:2}:1.0 }
</code></pre></div></div>

<h5 id="averagefieldlength">averageFieldLength</h5>
<p>The average length, in number of terms, of the indexed field name,
as computed from the local content node index (in memory or on disk).
This is the same index statistic that <a href="https://docs.vespa.ai/en/ranking/bm25.html">BM25</a> uses by default
for field length normalization (the avg_field_len term),
exposed here so it can be used for debugging and in custom text ranking formulas such as
<a href="https://github.com/vespa-engine/sample-apps/tree/master/examples/bayesian_bm25">Bayesian BM25</a>.</p>

<h5 id="querytermdocumentfrequency">queryTermDocumentFrequency</h5>
<p>A <em>tensor&lt;double&gt;(term{})</em> holding the document frequency that BM25 would use for each query term
that searches the <a href="https://docs.vespa.ai/en/reference/schemas/schemas.html#indexing-index">index</a> field name.
The document frequency is the number of documents that contain the term;
it is the input to the inverse document frequency (IDF) component of BM25.
This feature is exposed for debugging and for use in custom text ranking formulas such as
<a href="https://github.com/vespa-engine/sample-apps/tree/master/examples/bm25f">BM25F</a> or Bayesian BM25.</p>

<p>Example: for a query where term 0 and term 2 search the content field (term 1 searches a different field),
<em>queryTermDocumentFrequency(content)</em> is:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>tensor&lt;double&gt;(term{}):{ {term:0}:1200.0, {term:2}:57.0 }
</code></pre></div></div>

<p>See the <a href="https://docs.vespa.ai/en/reference/ranking/rank-features.html#queryTermDocumentFrequency(name)">reference</a>.</p>

<h4 id="represent-more-complex-structured-data-in-tensors-with-tensorfromstructs">Represent more complex structured data in tensors with tensorFromStructs</h4>
<p>When converting structured attribute data into tensors, you may need to represent values across more than one dimension.</p>

<p><strong>What’s new:</strong> <a href="https://docs.vespa.ai/en/reference/ranking/rank-features.html#tensorFromStructs(attribute,key,value,type)">tensorFromStructs</a>
now supports <strong>multiple mapped dimensions</strong>, letting you use multiple fields as keys to represent these relationships in a tensor.</p>

<p>For example, use:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>tensorFromStructs(attribute(items), name, region, price, float)
</code></pre></div></div>

<p>to get a <em>tensor&lt;float&gt;(name{},region{})</em> for different prices in different regions.</p>

<p>This function lets you represent more complex relationships in structured data directly as tensors for use in ranking.</p>

<h4 id="send-vespa-telemetry-directly-to-your-observability-tools-with-telemetry-export">Send Vespa telemetry directly to your observability tools with telemetry export</h4>
<p><a href="https://docs.vespa.ai/en/operations/telemetry-export.html">Telemetry export</a>
lets you ship your application’s metrics and logs from Vespa Cloud directly to your own observability backend
via standard OpenTelemetry export - a fully self-service, push-based alternative to the pull-based
<a href="https://docs.vespa.ai/en/operations/monitoring.html#prometheus-metrics-api">Prometheus metrics API</a>.
You declare one or more <a href="https://docs.vespa.ai/en/operations/telemetry-export.html#exporter">exporters</a>
in <a href="https://docs.vespa.ai/en/reference/applications/services/services.html">services.xml</a>;
Vespa Cloud then pushes the selected metrics and logs you configure to the backend(s) you configure,
authenticated with credentials from your vault.
Configure your vault and secrets using the <a href="https://docs.vespa.ai/en/security/secret-store.html">Vespa secret store</a>,
grant infrastructure access, add the telemetry exporter configuration, deploy, and telemetry starts flowing in minutes.</p>

<p><strong>Telemetry export is available for <a href="https://docs.vespa.ai/en/operations/enclave/enclave.html">Vespa Cloud Enclave</a> only.</strong></p>

<h4 id="simplify-search-across-chinese-language-variants-through-asia-linguistics-sample-apps">Simplify search across Chinese language variants through Asia linguistics sample apps</h4>
<p>Vespa offers several ways to handle Chinese text — Simplified, Traditional,
and the mix you typically see across zh-CN, zh-TW, and zh-HK markets.
<a href="https://github.com/vespa-engine/sample-apps/tree/master/examples/linguistics-asia">linguistics-asia</a>
compares those options side by side.
It ships eight runnable sub-applications — one per integration shape — over the same dataset and the same five compare-queries,
so the trade-offs are observable, not just theoretical.
The basic idea is to provide a complete solution for CJK characters / terms support,
and how Vespa can handle different cases with minimal custom code / logic implementation.</p>

<h4 id="other-new-features">Other new features</h4>

<p><strong><a href="https://github.com/vespa-engine/vespa/pull/37582">Documentid indexing expression</a>:</strong>
Allows using the documentid or parts of it in the indexing language, like <em>documentid [part]</em> - potential uses:</p>

<ul>
  <li>Enabling searching over parts of the document ID, e.g. namespace.
Removes the need to send the value explicitly, or writing a custom document processor.</li>
  <li>Creating a stable and properly randomized hash value to be used as a tie-breaker for iterating over many documents.</li>
</ul>

<p>See the <a href="https://docs.vespa.ai/en/reference/writing/indexing-language.html#other-expressions">reference</a>
and <a href="https://docs.vespa.ai/en/writing/indexing.html#documentid">examples</a>.
Many thanks to <a href="https://github.com/dainiusjocas">Dainius Jocas (@dainiusjocas)</a> for the contribution!</p>

<p><strong>Disk size re-sampling:</strong>
For users of the Vespa Enterprise Image / Vespa OSS.
For environments when the disk size can change dynamically, the proton process now re-checks the disk space.
The (free) size is used to <a href="https://docs.vespa.ai/en/writing/feed-block.html">block writes</a> when the disk is close to full.,</p>

<p><strong><a href="https://github.com/vespa-engine/vespa/pull/37563">Optimized chunked Hamming MaxSim</a>:</strong>
The existing <em>SumMaxInvHammingFunction</em> optimizer introduced in <a href="https://github.com/vespa-engine/vespa/pull/32320">#32320</a>
recognizes Hamming MaxSim over a 2D document tensor. This change adds the corresponding 3D optimizer:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>query:    tensor&lt;int8&gt;(qt{},x[N])
document: tensor&lt;int8&gt;(chunk{},t{},x[N])
max(chunk, sum(qt, max(t, 1 / (1 + sum(x, hamming(query, document))))))
</code></pre></div></div>

<p>Many thanks to <a href="https://github.com/oskrim">Oskari Mantere (@oskrim)</a> for the contribution!</p>

<h3 id="whats-new-on-youtube">What’s New on YouTube</h3>

<ul>
  <li><a href="https://youtu.be/FOr156rd1Qs">Simplifying Your Chunking Strategy</a> (Short video)</li>
  <li><a href="https://youtu.be/8WUpp_qk4AA">The Vespa Kubernetes Operator: A Better Way to Run Vespa on K8s</a> (Short video)</li>
  <li><a href="https://www.youtube.com/watch?v=GN_WpdM7CG0">Tensor-Native Retrieval and Ranking, Explained</a> (Short video)</li>
  <li><a href="https://youtu.be/loZayfite8Q">Vespa Now: Q2 Product Update</a> ( Webinar)</li>
  <li><a href="https://youtu.be/PVNodvy5WYs">Vespa Operator Demo</a></li>
</ul>

<p>Find more videos in the <a href="https://www.youtube.com/@vespaai">@vespaai</a> channel.</p>

<h3 id="blogs-and-ebooks">Blogs and ebooks</h3>
<ul>
  <li><a href="https://blog.vespa.ai/re-autoresearching-msmarco-bm25-on-vespa/">Re-autoresearching MSMARCO BM25, on Vespa</a></li>
  <li><a href="https://blog.vespa.ai/the-human-context-advantage/">The Human Context Advantage: Takeaways from the Ai4 Keynote with Andrew Ng, Geoffrey Hinton, and Fei-Fei Li</a></li>
  <li><a href="https://blog.vespa.ai/your-agent-wants-to-search-like-a-2010-quant/">Your agent wants to search like a 2010 quant</a></li>
</ul>

<h3 id="upcoming-events">Upcoming events</h3>

<h4 id="vespaai-live-london-9---10-september">Vespa.ai Live: London, 9 - 10 September</h4>
<p>Join the Vespa and retrieval community in London for a full day dedicated to building better AI search and retrieval systems.
The event brings together engineers, developers, and AI practitioners to share ideas, learn from real-world deployments,
and connect with others shaping the future of AI-powered search.</p>

<p><a href="https://content.vespa.ai/vespa-live">Learn more about Vespa Live!</a></p>

<h4 id="it-dagene-trondheim-14-september">it DAGENE: Trondheim, 14 September</h4>
<p>Vespa.ai will be attending itDAGENE 2026 at Realfagbygget, NTNU in Trondheim. The event brings IT students and businesses together for two days of company stands, professional sessions, and meaningful conversations about careers and technology.</p>

<h4 id="shoptalk-fall-nashville-29-september---1-october">Shoptalk Fall: Nashville, 29 September - 1 October</h4>
<p>Vespa.ai is heading to <a href="https://fall.shoptalk.com/">Shoptalk Fall</a>,
the essential H2 event for retail leaders shaping what comes next.
Join thousands of retail changemakers in Nashville to exchange ideas, build valuable connections, and explore the innovations helping businesses stay ahead in a fast-moving retail landscape.</p>

<hr />
<p>👉 <a href="https://www.linkedin.com/company/vespa-ai/">Follow us on LinkedIn</a> to stay in the loop on upcoming events, blog posts, and announcements.</p>

<hr />

<p>Thanks for joining us in exploring the frontiers of AI with Vespa. Ready to take your projects to the next level?
<a href="https://vespa.ai/free-trial/">Deploy your application for free</a> on Vespa Cloud today.</p>

]]></content:encoded>
        <pubDate>Tue, 08 Sep 2026 00:00:00 +0000</pubDate>
        <link>https://blog.vespa.ai/vespa-newsletter-sept-2026/</link>
        <guid isPermaLink="true">https://blog.vespa.ai/vespa-newsletter-sept-2026/</guid>
        
        
        <category>newsletter</category>
        
      </item>
    
      <item>
        <title>The Human Context Advantage: Takeaways from the Ai4 Keynote with Andrew Ng, Geoffrey Hinton, and Fei-Fei Li</title>
        <description>From jobs and education to open models and AI infrastructure, the AI4 keynote made one thing clear: competitive advantage won&apos;t come from AI alone, but from connecting it to the right context.</description>
        
        <media:thumbnail url="https://blog.vespa.ai/assets/images/IMG_0533.jpg" />
        
        <content:encoded><![CDATA[<p>One of the biggest talking points coming out of AI4 wasn’t a product announcement or benchmark. It was the keynote discussion between three of the most influential voices in AI: Andrew Ng, Geoffrey Hinton, and Fei-Fei Li. </p>

<p>Their differing perspectives generated a lot of conversation online. They challenged each other on AI’s impact on jobs, regulation, education, and the future of technology itself.</p>

<p>What stood out to me wasn’t who was right. It was how their disagreements revealed the questions the AI industry is still trying to answer.</p>

<h2 id="humans-still-have-the-context-advantage">Humans Still Have the Context Advantage</h2>

<p>As the conversation shifted from AI capabilities to education, reskilling, and the future of work, the moderator asked a practical question: </p>

<p><strong>How should we prepare people for a future where AI becomes part of nearly every job?</strong></p>

<p>While Hinton warned that “routine intellectual labor is going to be done by AI,” Ng added that “humans for a long time will have a fundamental context advantage.”</p>

<p>At first glance, those ideas may seem contradictory. They’re not.</p>

<p>AI is becoming remarkably capable at routine cognitive work, but capability isn’t the same as context.</p>

<p>Ng explained that humans possess something today’s AI systems still lack: years of accumulated contextual knowledge. We understand our organizations, customers, business priorities, relationships, constraints, and the countless unwritten rules that shape good decisions. We know when an idea looks plausible but would never work in practice.</p>

<p>A model may know more facts than anyone in the room, but it still doesn’t know your business, and that distinction matters.</p>

<p><strong>The future of enterprise AI won’t be determined solely by which model is the smartest. It will be determined by which systems deliver the right context at the right moment.</strong></p>

<h2 id="the-future-of-work-is-more-complicated-than-ai-takes-jobs">The Future of Work Is More Complicated Than “AI Takes Jobs”</h2>

<p>The panel also pushed back on one of AI’s biggest headlines: that jobs will simply disappear.</p>

<p>The discussion wasn’t about whether AI will change work. Everyone agreed it will. The real debate is how.</p>

<p>Ng emphasized that jobs are changing, developers are broadening their scope, and tasks are being automated. He described how developers are already using AI to automate portions of their work and expand into broader responsibilities. Instead of specializing narrowly, they’re increasingly becoming full-stack builders capable of owning larger portions of a product. He has made this case publicly before, trading the “jobpocalypse” narrative for what he calls an “AI jobapalooza.”</p>

<p>Hinton’s view is that people who worry about jobs are worrying for good reason. He pointed to routine intellectual work, the kind done in call centers, as the clear near-term target. His analogy was manual labor after the mechanical digger arrived. The machine did the digging better, workers moved to other manual jobs, and offices and services absorbed them. His worry is that once AI handles routine intellectual labor, there is no obvious next place for those workers to go.</p>

<p>Li reframed the whole thing in layers. Every existing job is made of many tasks. Some tasks get automated, some stay untouched, and some get more of your time because the tedious parts sped up. On top of that sit three categories: jobs that transform, jobs that get created, and jobs that get genuinely displaced. Rather than asking whether AI replaces jobs, she encouraged the audience to think about how it changes the work inside those jobs.</p>

<p>All three perspectives can be true. Some tasks will disappear, while others will become dramatically faster, and entirely new responsibilities will emerge.</p>

<p>A developer may spend less time writing boilerplate code and more time designing systems. A marketer may spend less time producing first drafts and more time shaping strategy. A customer service representative may handle fewer routine requests and more complex customer conversations.</p>

<p>The organizations that benefit most won’t simply automate work. They’ll redesign work.</p>

<h2 id="productivity-isnt-the-same-as-prosperity">Productivity isn’t the Same as Prosperity</h2>

<p>One of the strongest moments of the keynote came from Li: <strong>“Increased productivity does not translate to shared prosperity.”</strong></p>

<p>AI is already making individuals and organizations more productive. Hinton even described a future where AI could drive “massive increases in productivity.”</p>

<p>But productivity is only one measure of success.</p>

<p><strong>If AI allows one employee to complete five times as much work, what happens next?</strong></p>

<p>Do organizations create new opportunities? Do employees take on broader responsibilities? Do customers receive better service? Or do companies simply reduce headcount?</p>

<p>Those are business decisions, not technological ones. The future of AI won’t be defined only by what the technology can do. It will also be shaped by how organizations choose to apply it. </p>

<p>The point that stayed with me is that productivity and prosperity are not the same word. AI reliably raises productivity. Whether that turns into shared prosperity is a choice we make, not a result we are handed.</p>

<h2 id="the-best-ai-systems-increase-human-agency">The Best AI Systems Increase Human Agency</h2>

<p>The keynote repeatedly returned to education, but the lessons apply far beyond the classroom.</p>

<p>Li reminded the audience: “The core of learning is agency and motivation.”</p>

<p>Hinton offered an optimistic vision of AI tutors helping students learn more effectively through individualized instruction.</p>

<p>Despite approaching the topic from different perspectives, both speakers arrived at a similar conclusion: </p>

<p><strong>The goal isn’t to remove humans from the process. It’s to help people become more capable.</strong></p>

<p>That’s true whether you’re teaching students, helping developers build software, enabling customer support teams, or assisting knowledge workers.</p>

<p>The best AI systems don’t replace human judgment. They amplify it.</p>

<h2 id="security-is-more-nuanced-than-open-vs-closed">Security Is More Nuanced Than “Open vs. Closed”</h2>

<p>Another part of the keynote that caught my attention was the discussion around open source. It was one of the most nuanced parts of the keynote, and it was a good reminder that our industry often oversimplifies complex issues.</p>

<p>Hinton began by making an important distinction that is often lost in AI conversations: <strong>open source is not the same as open weights</strong>.</p>

<p>Open source means publishing the code so others can inspect and improve it. Open weights means releasing the trained model itself, making it easy for anyone to download, fine-tune, and adapt.</p>

<p>Hinton explained that he opposed releasing open weights for years because it dramatically lowered the cost of repurposing powerful models for harmful uses. But he also acknowledged that the landscape has changed. The capable open-weight models are already here. As he put it, the barrier he once worried about has largely disappeared.</p>

<p>Ng approached the question from a different angle. Rather than focusing on risk, he focused on competitiveness.</p>

<p>He argued that open models such as DeepSeek and Moonshot’s Kimi K3 are already gaining global adoption. That matters not only economically but strategically. AI models influence how people access information, solve problems, and increasingly interact with the digital world. In that sense, widespread adoption becomes a form of soft power.</p>

<p>His concern wasn’t that openness is inherently risky; it was that fear-driven policy could leave American open models unable to compete globally.</p>

<p>Then, Li argued that treating openness as a binary choice is itself the mistake.</p>

<p>Drawing on examples from nuclear physics and the Human Genome Project, she pointed out that society has long balanced openness and control. Scientific research can remain open while sensitive materials, infrastructure, or applications are governed differently.</p>

<p>Her message was simple: stop asking whether AI should be <em>open</em> or <em>closed</em>. Instead, ask <strong>what should be open, for whom, and at which layer of the technology stack.</strong></p>

<p>That framing resonated with me because it’s the same kind of thinking we need across AI infrastructure.</p>

<p>Enterprise AI isn’t built from a single component. Models, retrieval systems, business logic, data, evaluation, and applications all have different requirements for openness, governance, and control.</p>

<p>My biggest takeaway here wasn’t that one speaker “won” this debate. It was that <strong>“open versus closed” is the wrong question.</strong></p>

<p>The better question is: <strong>Open at which layer?</strong></p>

<h2 id="context-is-the-real-infrastructure-challenge">Context Is the Real Infrastructure Challenge</h2>

<p>Listening to the discussion, I kept coming back to Ng’s observation about context. It also reinforces something I’ve seen repeatedly in enterprise AI:</p>

<p><strong>Models often don’t fail because they can’t reason. They fail because they lack the context needed to reason well.</strong></p>

<p>Ng described using AI to brainstorm ideas. Some suggestions were genuinely useful, while others were obviously wrong. The model generated all of them, but the human knew immediately which ones didn’t fit the situation.</p>

<p>That’s the difference between intelligence and understanding. </p>

<p>For enterprise AI, the next challenge isn’t simply making models more intelligent. It’s giving them the context they need to make intelligent decisions.</p>

<p>A model can’t reliably use information that it can’t access. It can’t distinguish fresh information from stale information unless the system tells it. It can’t understand business rules, user permissions, customer history, or organizational priorities unless those signals are part of the retrieval process.</p>

<p>Without access to current data, business logic, organizational knowledge, and user-specific information, even the most capable model can produce an answer that is fluent, convincing (and completely wrong).</p>

<p>That makes context an infrastructure problem. The model may generate the response, but the system determines whether the model has the right information to generate the <em>right</em> response.</p>

<p>It’s why retrieval, ranking, freshness, personalization, and grounding matter so much.</p>

<p>This is exactly the problem platforms like <a href="http://vespa.ai">Vespa.ai</a> are designed to solve.</p>

<p>Rather than treating retrieval, ranking, inference, and serving as separate systems stitched together with multiple network hops, Vespa.ai brings them into a single query pipeline that runs computation where the data lives. AI applications can retrieve fresh information, combine lexical, semantic, and structured signals, apply business-specific ranking logic, personalize results, and perform machine-learned inference in real time. And all of this happens before the model generates a response.</p>

<p>As AI moves from answering questions to making decisions and taking actions, delivering the <em>right</em> context, not just <em>more</em> context, becomes the competitive advantage.</p>

<h2 id="my-biggest-takeaway">My Biggest Takeaway</h2>

<p>The Ai4 keynote didn’t provide definitive answers about the future of AI. Instead, it showed why thoughtful disagreement is valuable.</p>

<p>AI may create enormous abundance, but it may also create significant disruption, and it will almost certainly transform how we work.</p>

<p>But the lesson I’ll remember most wasn’t about jobs, regulation, or model capabilities. It was Ng’s observation that humans retain a context advantage.</p>

<p>As foundation models become increasingly capable, competitive advantage won’t come from simply using AI. It will come from building systems that connect AI to the right information, the right business logic, the right user signals, and the right moment.</p>

<p>The model may be smart, but <strong>the system that understands your business</strong> will be the one that creates value.</p>
]]></content:encoded>
        <pubDate>Mon, 10 Aug 2026 00:00:00 +0000</pubDate>
        <link>https://blog.vespa.ai/the-human-context-advantage/</link>
        <guid isPermaLink="true">https://blog.vespa.ai/the-human-context-advantage/</guid>
        
        <category>rag</category>
        
        <category>AI</category>
        
        <category>GenAI</category>
        
        <category>agents</category>
        
        
      </item>
    
      <item>
        <title>Your agent wants to search like a 2010 quant</title>
        <description>The idea of empowering AI agents to retrieve information like a professional is going mainstream.</description>
        
        <media:thumbnail url="https://blog.vespa.ai/assets/2026-07-07-your-agent-wants-to-search-like-a-2010-quant/quant.png" />
        
        <content:encoded><![CDATA[<p><strong>With the debut of Perplexity’s search as code announcement, the idea of empowering AIs to retrieve information 
like a professional is going mainstream. It’s rare that an idea that is so simple to implement can have such a 
large impact on what you can achieve.</strong></p>

<p>AI agents need the right information to work well. Whether they manage to find it is the difference between success 
and failure in most real-world cases, and increasingly so as models get smarter.</p>

<p>The practitioners in the field of <em>letting large language models find the information they need</em> have passed through 
several stages of enlightenment.</p>

<p>The first stage was the vector database period of ’24, when the belief was that all you needed to do was chop text 
into independent chunks, create an embedding vector for each, and retrieve them via a nearest-neighbor search. 
This was simple, but alas, it did not work well. Chunks had too little context, and scoring based solely on vector 
similarity failed to reliably surface useful information.</p>

<p>Enter the second stage, where the learnings from human information retrieval over the last half century were 
combined with vector retrieval: Hybrid search, BM25, machine-learned ranking, and so on. This was a huge improvement 
which brought many use cases from demo stage to production quality.</p>

<p>But there is still vast room for improvement, and with Perplexity’s announcement of 
<a href="https://research.perplexity.ai/articles/rethinking-search-as-code-generation">search as code</a>, we might be 
officially entering the third stage. See, the search field has needed to deal with one big problem: Human users are 
lazy and clueless, at least when it comes to search. Given this, it turns out that the words typed into query boxes 
should be treated as vague indicators of what they might want, and providing any finer control is pointless, 
as nobody would be bothered to use it. You can see this most clearly in the evolution of Google search from letting you 
find web pages containing specific words to whatever it is now; 
<a href="https://www.quora.com/Why-is-Google-search-so-absolutely-garbage-useless-and-annoying-Its-total-trash-so-please-dont-respond-if-youre-just-a-nerd-loyalist-loser">thoroughly lamented</a> 
but of course also deeply grounded in observation of real users.</p>

<p>Agents, in contrast, are not so clueless. And they certainly aren’t lazy! There’s no reason to limit their options to 
those of a casual human user. They should be able to:</p>

<ul>
  <li>Search for the names of those involved near each other in text when researching a legal case</li>
  <li>Do a pure semantic search prioritizing high-quality sources when seeking a broad overview of a topic</li>
  <li>Select a year range and group by month when constructing a timeline of some events</li>
</ul>

<p>The list goes on. Typically, an agent will string together many of these queries to reach its goal. First gaining an 
overview, then researching more specific topics, forming hypotheses, verifying important details in them, and so on. 
In short, search like an expert who knows what they are doing and really cares about the results— like a quant 
doing financial analysis.</p>

<p>It seems obvious that this will yield better results, and this is also what evaluations such as those published by 
Perplexity in its <a href="https://research.perplexity.ai/articles/rethinking-search-as-code-generation">search as code</a> 
blog post show (ignore the “code” aspect, as code execution is generally useful 
and where that code runs doesn’t impact quality).</p>

<p>Doing this in practice, with your own data, is actually quite easy. The models already know how to write complex 
<a href="https://docs.vespa.ai/en/reference/querying/yql.html">YQL queries</a>; they just need to be told</p>

<ul>
  <li>That they can</li>
  <li>What fields are available and what they mean</li>
  <li>What choices they have in ranking the results</li>
</ul>

<p>We’re working on convenience tooling for this, but how you tell them doesn’t actually matter that much; 
a simple textual description of what fields and rank profiles they
should consider using and what they mean will do. Some example queries are also helpful. Models today are smart enough 
to use this effectively to connect their intents to practical YQL queries.</p>

<p>When creating search for humans, developers need to implement solutions that work well across a broad set of use cases, 
which involves making trade-offs where some types of queries cannot be improved because doing so would impact 
other types. When implementing for agents, the focus shifts to providing a wide toolbox for the agent to use to 
address their varied informational needs: broad and highly specific lexical recall, metadata attributes for 
filtering, grouping, and aggregation, as well as different ranking methods suited to different needs.</p>

<p>Accordingly, developers working on agentic search need to shift their focus from reusing techniques that have 
worked well for casual humans to the much richer capabilities traditionally provided by solutions for 
competent professionals.</p>

<p>It’s time to let your agents search like a 2010 quant.</p>
]]></content:encoded>
        <pubDate>Tue, 07 Jul 2026 00:00:00 +0000</pubDate>
        <link>https://blog.vespa.ai/your-agent-wants-to-search-like-a-2010-quant/</link>
        <guid isPermaLink="true">https://blog.vespa.ai/your-agent-wants-to-search-like-a-2010-quant/</guid>
        
        <category>rag</category>
        
        <category>AI</category>
        
        <category>GenAI</category>
        
        <category>agents</category>
        
        
      </item>
    
      <item>
        <title>Re-autoresearching MSMARCO BM25, on Vespa</title>
        <description>BM25 is having a moment. We reproduce Doug Turnbull&apos;s MSMARCO autoresearch experiment in Vespa and get a comparable MRR@10 lift from existing rank features — with twice the generalization to full MSMARCO.</description>
        
        <media:thumbnail url="https://blog.vespa.ai/assets/2026-05-29-re-autoresearching-msmarco-bm25-on-vespa/lead-card-bg.png" />
        
        <content:encoded><![CDATA[<h2 id="bm25-is-having-a-moment">BM25 is having a moment</h2>

<p>Google search interest in “BM25” jumped about 5× in early August 2025. Around
the same time, OpenAI’s models started volunteering BM25 noticeably more —
gpt-4o named it on 12% of neutral retrieval prompts; gpt-4.1 and
gpt-5-chat on 30–35%.<sup id="fnref:bm25-probe"><a href="#fn:bm25-probe" class="footnote" rel="footnote" role="doc-noteref">1</a></sup> The <a href="https://arxiv.org/abs/2508.21038">LIMIT paper</a>
showing dense embedding models flubbing trivial retrieval landed three
weeks later.<sup id="fnref:limit"><a href="#fn:limit" class="footnote" rel="footnote" role="doc-noteref">2</a></sup></p>

<p><img src="/assets/2026-05-29-re-autoresearching-msmarco-bm25-on-vespa/bm25-google-trends.png" alt="Google Trends: worldwide search interest in &quot;bm25&quot; over the past five years, roughly 5x in late 2025" /></p>

<p>Whatever the reasons for this spike, the renewed interest in lexical search is likely a good thing.
Lexical scoring is still a very robust baseline, especially in
out-of-domain or zero-shot settings. And BM25 is a great baseline, but
can we do better?</p>

<h2 id="building-on-bm25">Building on BM25</h2>

<p>Earlier this month, <a href="https://softwaredoug.com/">Doug Turnbull</a>
published a really neat
<a href="https://softwaredoug.com/blog/2026/05/17/autoresearching-a-better-msmarco-bm25">autoresearch experiment</a>:
let an LLM iterate on a Python BM25 reranker for 8 rounds and see how much
better it gets on the MSMARCO<sup id="fnref:msmarco"><a href="#fn:msmarco" class="footnote" rel="footnote" role="doc-noteref">3</a></sup> passage-ranking benchmark. He randomly selects a 650k-passage slice he
calls “minimarco”, and his agent is able to improve MRR@10 from <strong>0.4913 → 0.5350</strong>
(+0.044). Very cool!</p>

<p>Of course, we wanted to try it for ourselves too. Our Vespa twist on it: instead of letting an LLM write arbitrary code in <code class="language-plaintext highlighter-rouge">reranker.py</code>, can
we get a similar lift while limiting our search space to existing Vespa rank features?</p>

<p><img src="/assets/2026-05-29-re-autoresearching-msmarco-bm25-on-vespa/lead.png" alt="BM25 vs BM25 + three Vespa rank features on the full MSMARCO benchmark: MRR@10 lifts from 0.1901 to 0.2106, a 10.8% improvement" /></p>

<p>Spoiler: It turns out we can indeed get a significant improvement - and one that generalises better to the full dataset too. Let’s show you exactly how we did it!</p>

<h2 id="reproducing-dougs-setup">Reproducing Doug’s setup</h2>

<p>His recipe to create the “minimarco” subset is two lines of pandas:</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">collection</span> <span class="o">=</span> <span class="n">pd</span><span class="p">.</span><span class="nf">read_csv</span><span class="p">(</span><span class="sh">"</span><span class="s">collection.tsv</span><span class="sh">"</span><span class="p">,</span> <span class="n">sep</span><span class="o">=</span><span class="sh">"</span><span class="se">\t</span><span class="sh">"</span><span class="p">,</span> <span class="n">names</span><span class="o">=</span><span class="p">[</span><span class="sh">"</span><span class="s">doc_id</span><span class="sh">"</span><span class="p">,</span> <span class="sh">"</span><span class="s">description</span><span class="sh">"</span><span class="p">])</span>
<span class="n">minimarco</span> <span class="o">=</span> <span class="n">collection</span><span class="p">.</span><span class="nf">sample</span><span class="p">(</span><span class="n">n</span><span class="o">=</span><span class="mi">650_000</span><span class="p">,</span> <span class="n">random_state</span><span class="o">=</span><span class="mi">42</span><span class="p">).</span><span class="nf">reset_index</span><span class="p">(</span><span class="n">drop</span><span class="o">=</span><span class="bp">True</span><span class="p">)</span>
</code></pre></div></div>

<p>That trims the corpus from 8.84M MSMARCO passages down to 650k. Of the
6,980 dev queries, 543 still have a labeled relevant passage that landed in our
random subset (the rest are unscoreable here, since their answer isn’t in
the sample).</p>

<p>We loaded the 650k passages into Vespa with a minimal schema, setting
the two BM25 hyperparameters to the Anserini-tuned MSMARCO defaults
commonly used on this benchmark.</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>rank-profile bm25 inherits default {
    first-phase {
        expression: bm25(description)
    }
    rank-properties {
        bm25(description).k1: 0.6      # Anserini-tuned MSMARCO defaults
        bm25(description).b: 0.62
    }
}
</code></pre></div></div>

<p>Running the 543 scoreable queries against this schema gives
<strong>MRR@10 = 0.4907</strong>. Doug’s number is <strong>0.4913</strong> - close enough that
the small gap could be explained by e.g. differences in stemming - the way his <a href="https://github.com/softwaredoug/searcharray">SearchArray</a>’s Snowball
vs. Vespa’s <a href="https://docs.vespa.ai/en/linguistics.html">OpenNLP English</a><sup id="fnref:stemming"><a href="#fn:stemming" class="footnote" rel="footnote" role="doc-noteref">4</a></sup>
normalize words. So we’re starting from the same baseline - now let’s see what we can add!</p>

<h2 id="the-three-tweaks-that-moved-the-needle">The three tweaks that moved the needle</h2>

<p>We had time to try about 20 things in an afternoon. Three of them robustly survived 10 paired rotations
against the running baseline — meaning each round we drew 10 different
random subsets of the dev queries, evaluated both the candidate and the
current best config on the same queries, and only kept changes that beat
the previous best across all 10 subsets.</p>

<h3 id="1-stopword-limit-on-weakand">#1: Stopword-limit on weakAnd</h3>
<p>Vespa’s <a href="https://docs.vespa.ai/en/reference/query-language-reference.html#text"><code class="language-plaintext highlighter-rouge">text()</code></a>
operator searches its tokens with a
<a href="https://docs.vespa.ai/en/reference/query-language-reference.html#weakand"><code class="language-plaintext highlighter-rouge">weakAnd</code></a> by default,
which has a built-in document-frequency (DF) based stopword filter. We set it extremely aggressively here - 
<a href="https://docs.vespa.ai/en/reference/query-api-reference.html#ranking.matching.weakand.stopwordLimit"><code class="language-plaintext highlighter-rouge">ranking.matching.weakand.stopwordLimit = 0.05</code></a>, which means Vespa automatically drops query terms that appear in more than 5% of docs.  No need to create a hand-curated
list. This makes our queries faster too - the high-DF terms have the longest posting lists,
so skipping them more than halved our wall-clock latency.<sup id="fnref:tripling"><a href="#fn:tripling" class="footnote" rel="footnote" role="doc-noteref">5</a></sup></p>

<table>
  <thead>
    <tr>
      <th>stopword-limit</th>
      <th>Δ MRR@10 paired</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>off</td>
      <td>-</td>
    </tr>
    <tr>
      <td><strong>0.05</strong></td>
      <td><strong>+0.0136</strong></td>
    </tr>
    <tr>
      <td>0.02</td>
      <td>-0.0088</td>
    </tr>
  </tbody>
</table>

<p>At 0.02 the filter has become way too aggressive and starts dropping important content words.</p>

<h3 id="2-nativeproximity">#2: nativeProximity</h3>
<p>The <strong><code class="language-plaintext highlighter-rouge">nativeProximity</code></strong> feature is a continuous proximity score that rewards docs
where the matched query terms are close together. Doug’s agent did something
loosely similar by writing code to score based on adjacent-bigram phrase term frequencies. We just pulled it from
<a href="https://docs.vespa.ai/en/reference/ranking/rank-features.html">Vespa’s rank-feature catalog</a>:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>rank-profile lexical inherits default {
    inputs {
        query(w_prox) double: 0.0      # query-time tunable weight
    }
    first-phase {
        expression: bm25(description) + query(w_prox) * nativeProximity
    }
    ...
}
</code></pre></div></div>

<p>Our sweep results: <code class="language-plaintext highlighter-rouge">w_prox = 10</code> is a good value here, with a wide plateau (8-14 are all within
noise). Wide plateaus like this are a positive sign that the gain is real, not a noise-fitted spike.</p>

<h3 id="3-fieldmatchearliness">#3: fieldMatch.earliness</h3>
<p><strong><a href="https://docs.vespa.ai/en/reference/rank-features.html#fieldMatch(name).earliness"><code class="language-plaintext highlighter-rouge">fieldMatch(description).earliness</code></a></strong> is a feature that rewards matches near the start
of the field. A match is often a stronger signal if it appears early —
writing tends to introduce its main topic up front (headline, abstract,
summary). <code class="language-plaintext highlighter-rouge">w_fm_early = 8</code> peaks at +0.0189 paired over the proximity-only anchor.</p>

<h3 id="iterating-fast--and-validating-more-thoroughly">Iterating fast — and validating more thoroughly</h3>
<p>A nice property of doing this in Vespa: every “did this weight help?”
question is just a query parameter against the index which is already built when we fed the data.
~33 ms per query, ~0.4 s for a 109-query training eval, ~22 s to test
one weight value across 10 paired rotations. It stayed fast because we
weren’t paying for re-indexing or re-scoring of every query/document pair;
we were just passing numbers in an HTTP body.</p>

<p>That speed is also what lets us validate each candidate more thoroughly.
The eval set is tiny - 109 training queries per rotation - so any single split is noisy. Try 20 weight
combos against one split, and one or two will look like wins by pure luck
(the paired noise is ±0.014, bigger than the gains we’re chasing). Our fix
is to test each candidate across many <em>different</em> random splits and keep
only what holds up everywhere. That’s painful if every eval is an LLM call
or a re-index, but nearly free when it’s just another query - so we ran 10
rotations per weight. That’s the difference between a real +0.005 and a
lucky one, and possibly a big reason the single-split agent loop overfits:
not enough independent looks at the data before committing to a change.</p>

<h3 id="our-best-recipe-from-this-run">Our best recipe from this run</h3>

<p><strong><code class="language-plaintext highlighter-rouge">bm25 + 10·nativeProximity + 8·fieldMatch.earliness + sw=0.05</code></strong>.
On the full 543-query minimarco dev set: MRR@10 = <strong>0.5163</strong> (+0.0256
over BM25 = 0.4907). Doug’s agent gets <strong>0.5350</strong> (+0.044), so it’s ahead of us by
about 0.019 in absolute terms.</p>

<h2 id="what-happens-on-full-msmarco">What happens on full MSMARCO?</h2>

<p>First, what these numbers mean: minimarco is also where we <em>tuned</em>, so the
minimarco column is in-sample. The 10-rotation gate guards against getting lucky
on a single split — but not against fitting the 543-query subset as a whole. The
full 8.84M-doc corpus is the real out-of-sample test, so that’s the column to
trust for generalization.</p>

<p>Doug honestly flags this in his post: his agent’s gains don’t generalize
well. On the full 8.84M-doc benchmark, his round-8 reranker scores 0.1991
vs BM25’s 0.1897 - only +0.0094 of the original +0.044 improvement survives.
We tried our configurations on the full corpus too:</p>

<p><img src="/assets/2026-05-29-re-autoresearching-msmarco-bm25-on-vespa/hero.png" alt="MRR@10 by approach on the minimarco tuning subset vs the full MSMARCO benchmark" /></p>

<p>How well do our results transfer to the full dataset?</p>

<table>
  <thead>
    <tr>
      <th> </th>
      <th>minimarco</th>
      <th>full MSMARCO</th>
      <th>retention</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>BM25</td>
      <td>0.4907</td>
      <td>0.1901</td>
      <td>N/A</td>
    </tr>
    <tr>
      <td>Free-form-Python agent</td>
      <td>0.5350 (+0.044)</td>
      <td>0.1991 (+0.009)</td>
      <td><strong>21%</strong></td>
    </tr>
    <tr>
      <td>Our agent (Vespa rank features)</td>
      <td>0.5060 (+0.015)</td>
      <td>0.2053 (+0.015)</td>
      <td><strong>99%</strong></td>
    </tr>
    <tr>
      <td>Our manual sweep (Vespa rank features)</td>
      <td>0.5163 (+0.026)</td>
      <td>0.2106 (+0.021)</td>
      <td><strong>80%</strong></td>
    </tr>
  </tbody>
</table>

<p>Most of our minimarco lift from both attempts survives the jump to full MSMARCO - these
are all features with generalizable signal, re-tuning the weights on the full dataset might squeeze out more.</p>

<p>Why this gap?</p>

<ul>
  <li>An LLM with free-form Python has more rope. By round 8 the agent
hard-codes a stopword list containing <code class="language-plaintext highlighter-rouge">vacat</code> and <code class="language-plaintext highlighter-rouge">medicin</code> and a
conditional like <code class="language-plaintext highlighter-rouge">toks[1] not in ("can", "invent")</code>. Those help on the
minimarco subset; they don’t help anywhere else - it’s overfitting.</li>
  <li>We’re using rank features that IR researchers and Vespa engineers have
already validated as carrying generalizable signal. We didn’t have to invent
<code class="language-plaintext highlighter-rouge">nativeProximity</code> from scratch here. The LLM has to rediscover something like it
from termfreq primitives within a single round, which is harder.</li>
  <li>The LLM writing arbitrary python does have the freedom to invent completely
novel techniques, but on this extremely well-studied dataset we can
perhaps consider it somewhat unlikely.</li>
</ul>

<h2 id="our-own-autoresearch-loop">Our own “autoresearch” loop</h2>

<p>In our results, we refer to our first experiment as “manual”, but it’s 2026 - that sweep wasn’t a human hand-typing
weights either. We used a coding assistant (Claude Code) to do the
exploration, with us steering at a high level and holding it to the
10-rotation rule. So really, it’s LLM-vs-LLM - what changes
between the runs is the search space and how rigorously each one accepts a
change, not human vs machine.</p>

<p>After the quick sweep we wondered: would a fully <em>autonomous</em> LLM agent like Doug’s,
inside our constrained search space, generalize any better? We built a small loop
(same <code class="language-plaintext highlighter-rouge">eval_margin = 0.002</code>, same rotating seeds, same gpt-5.5 model with xhigh
reasoning) that edits the Vespa first-phase rank <em>expression</em> instead of
<code class="language-plaintext highlighter-rouge">reranker.py</code>. The key difference from Doug’s setup: a change is accepted only if it
clears the <strong>same 10-rotation paired-robustness check our manual sweep uses</strong> — so the
agent and the sweep differ only in search space and autonomous-vs-steered, not in how
rigorously a change is accepted. ~700 lines of Python, $6 of OpenAI spend, 30
minutes.</p>

<p>In our run the agent found two valuable features — <code class="language-plaintext highlighter-rouge">nativeProximity</code> and
<code class="language-plaintext highlighter-rouge">fieldMatch.earliness</code>, the same pair the manual sweep landed on — and reached
<strong>MRR@10 = 0.2053 on full MSMARCO (+0.0152)</strong>: about <strong>99% retention</strong> of its
minimarco lift, even higher than the manual sweep’s 80% and far above the
free-form-Python run’s 21%. Its absolute score sits a touch below the manual sweep’s
(0.2053 vs 0.2106) because it didn’t add the <code class="language-plaintext highlighter-rouge">stopwordLimit</code> matching-side lever in
this run — but almost all of the lift it <em>did</em> find carried over. That’s the
constrained search space doing its job: the agent can’t encode token-specific tactics
like a hard-coded stopword list, so its ceiling on overfitting is lower.</p>

<h2 id="our-final-config">Our final config</h2>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">body</span> <span class="o">=</span> <span class="p">{</span>
    <span class="sh">"</span><span class="s">yql</span><span class="sh">"</span><span class="p">:</span> <span class="sh">"</span><span class="s">select doc_id from passage where description contains ({language:</span><span class="sh">'</span><span class="s">en</span><span class="sh">'</span><span class="s">}text(@q))</span><span class="sh">"</span><span class="p">,</span>
    <span class="sh">"</span><span class="s">q</span><span class="sh">"</span><span class="p">:</span> <span class="n">user_query_text</span><span class="p">,</span>
    <span class="sh">"</span><span class="s">ranking.profile</span><span class="sh">"</span><span class="p">:</span> <span class="sh">"</span><span class="s">lexical</span><span class="sh">"</span><span class="p">,</span>
    <span class="sh">"</span><span class="s">hits</span><span class="sh">"</span><span class="p">:</span> <span class="mi">10</span><span class="p">,</span>
    <span class="sh">"</span><span class="s">language</span><span class="sh">"</span><span class="p">:</span> <span class="sh">"</span><span class="s">en</span><span class="sh">"</span><span class="p">,</span>
    <span class="sh">"</span><span class="s">input.query(w_prox)</span><span class="sh">"</span><span class="p">:</span> <span class="mf">10.0</span><span class="p">,</span>
    <span class="sh">"</span><span class="s">input.query(w_fm_early)</span><span class="sh">"</span><span class="p">:</span> <span class="mf">8.0</span><span class="p">,</span>
    <span class="sh">"</span><span class="s">ranking.matching.weakand.stopwordLimit</span><span class="sh">"</span><span class="p">:</span> <span class="mf">0.05</span><span class="p">,</span>
<span class="p">}</span>
</code></pre></div></div>

<p>This is all that’s needed to use these features. Just a simple linear combination of well-known signals,
and a demonstration that there <em>is</em> much more tuning potential to be had from lexical search too.
And your coding agent already knows how to do it!</p>

<p>BM25 has served the IR community well for 30+ years, and we don’t think it’s
going anywhere. But Vespa has all the pieces in place to go beyond the baseline -
with lexical signals, advanced multi-vector ranking and more, and we keep working to raise the bar.
<a href="https://vespa.ai/subscribe/">Subscribe</a> to the newsletter if you’d like to hear about it!</p>

<h2 id="going-further">Going further</h2>

<p>The full code for this experiment — the Vespa app, the
paired-rotation sweep harness, and the LLM agent loop, with a step-by-step
reproduction guide — is at <a href="https://github.com/vespaai-playground/msmarco-bm25-autoresearch"><code class="language-plaintext highlighter-rouge">vespaai-playground/msmarco-bm25-autoresearch</code></a>.</p>

<p>To actually give <em>your</em> coding agent the Vespa knowledge to quickly succeed, there’s an
official skills pack for Claude Code / Codex / Cursor / Gemini CLI:
<a href="https://github.com/vespaai-playground/skills">github.com/vespaai-playground/skills</a> —
which includes schema authoring, rank features, query building, etc.</p>

<p>Want to go even further? A weighted sum of three features is still on the simple end of
the spectrum. The next step is to stop hand-weighting and let a model learn
the best combination on your dataset: the <a href="https://blog.vespa.ai/the-rag-blueprint/">RAG Blueprint</a>
collects ~190 lexical <em>and</em> semantic match/rank features and trains a GBDT
(<a href="https://docs.vespa.ai/en/lightgbm.html">LightGBM</a>) model to combine them — a more advanced version of
what we did by hand here, and it mixes BM25-style lexical signals with
vector-semantic ones.</p>

<p>Want to play with Vespa? Start with the <a href="https://vespa.ai/free-trial/">free trial</a>
or pull the <a href="https://hub.docker.com/r/vespaengine/vespa/">vespaengine/vespa</a>
container.</p>

<h2 id="notes">Notes</h2>

<p>Come hang out in <a href="https://vespatalk.slack.com/">Vespa Slack</a> or
<a href="https://discord.vespa.ai/">Discord</a> if you want to chat ranking features
or compare notes on retrieval evals. Thanks to Doug for publishing the experiment!</p>
<div class="footnotes" role="doc-endnotes">
  <ol>
    <li id="fn:bm25-probe">
      <p>We asked the three models the same six neutral retrieval prompts, 10 reps each. BM25 mention rates: gpt-4o 12%, gpt-4.1 30%, gpt-5-chat 35%. The shift happens at gpt-4.1, not GPT-5. <a href="#fnref:bm25-probe" class="reversefootnote" role="doc-backlink">&#8617;</a></p>
    </li>
    <li id="fn:limit">
      <p><em>On the Theoretical Limitations of Embedding-Based Retrieval</em>, Google DeepMind &amp; Johns Hopkins, arXiv:2508.21038 (Aug 2025). LIMIT is a benchmark of deliberately simple retrieval queries — top embedding models scored under 20% recall@100 on queries as simple as “who likes apples?”. <a href="#fnref:limit" class="reversefootnote" role="doc-backlink">&#8617;</a></p>
    </li>
    <li id="fn:msmarco">
      <p><a href="https://microsoft.github.io/msmarco/">MSMARCO</a> is a widely-used passage-ranking benchmark — 8.84M short passages and a dev set of 7,437 <em>qrels</em> (query + relevant-passage-ID pairs collected by human annotators). <strong>MRR@10</strong> = mean reciprocal rank of the first relevant passage in each query’s top-10 results, averaged across queries (0 if no relevant passage in top-10). <a href="#fnref:msmarco" class="reversefootnote" role="doc-backlink">&#8617;</a></p>
    </li>
    <li id="fn:stemming">
      <p>Vespa also ships <a href="https://docs.vespa.ai/en/linguistics/lucene-linguistics.html">Lucene Linguistics</a> as an alternative for stemming, tokenization, and language detection — we stuck with the default OpenNLP here and did no tuning of linguistics. <a href="#fnref:stemming" class="reversefootnote" role="doc-backlink">&#8617;</a></p>
    </li>
    <li id="fn:tripling">
      <p><a href="https://blog.vespa.ai/tripling-the-query-performance-of-lexical-search/">Tripling the query performance of lexical search</a> takes a complementary latency-and-cost-focused approach — <code class="language-plaintext highlighter-rouge">stopwordLimit=0.6</code> combined with <code class="language-plaintext highlighter-rouge">adjust-target</code> and <code class="language-plaintext highlighter-rouge">filter-threshold</code> for 3–11× speedup with negligible quality change. We pushed <code class="language-plaintext highlighter-rouge">stopwordLimit</code> to the aggressive end of the range for quality, since on MSMARCO the benefit comes from setting the parameter low enough to exclude common question words — about 72% of dev queries are question-style (“what is X”, “how do Y”), and at <code class="language-plaintext highlighter-rouge">0.6</code> those terms are not excluded. On keyword-heavy workloads the optimum would be higher — re-measure on your own query distribution before adopting. Query rewriting on lexical queries is likely stronger, but we didn’t do it here. <a href="#fnref:tripling" class="reversefootnote" role="doc-backlink">&#8617;</a></p>
    </li>
  </ol>
</div>
]]></content:encoded>
        <pubDate>Fri, 29 May 2026 00:00:00 +0000</pubDate>
        <link>https://blog.vespa.ai/re-autoresearching-msmarco-bm25-on-vespa/</link>
        <guid isPermaLink="true">https://blog.vespa.ai/re-autoresearching-msmarco-bm25-on-vespa/</guid>
        
        <category>ranking</category>
        
        <category>bm25</category>
        
        <category>information retrieval</category>
        
        <category>evaluation</category>
        
        
      </item>
    
      <item>
        <title>Vespa Newsletter, May 2026</title>
        <description>Advances in Vespa include finer control over deployments, smarter ranking, richer embedding integrations, and more scalable vector search.
</description>
        
        <media:thumbnail url="https://blog.vespa.ai/assets/logo/logo-pi.jpg" />
        
        <content:encoded><![CDATA[<p>Welcome to the latest edition of the Vespa newsletter.
In the <a href="https://blog.vespa.ai/vespa-newsletter-december-2025/">previous update</a>, we introduced the Vespa.ai Playground, the Vespa Kubernetes Operator, Pyvespa 1.0,
and <a href="https://blog.vespa.ai/vespa-newsletter-february-2026/">more</a>.</p>

<p>This month, we’re announcing several updates focused on retrieval quality, ranking flexibility, and developer productivity. Each feature is designed to help engineering teams build faster, more accurate, and more maintainable retrieval and ranking systems, while giving businesses better relevance, lower operational overhead, and more predictable performance at scale.</p>

<p>Let’s dive into what’s new.</p>

<h3 id="calling-all-vespians-vespaai-live-has-landed">Calling All Vespians: Vespa.ai Live Has Landed</h3>

<p><img src="/assets/2026-05-26-newsletter/Vespa.ai-live.png" alt="Vespa.ai Live" /></p>

<p>As more teams build with Vespa, bringing the community closer together has become a major focus for us.
Earlier this year, we ran our first virtual meetups,
extending our Slack community and were joined by more than 100 Vespians from around the world —
from the US and Ukraine to Singapore, Australia, Kazakhstan, Egypt, and beyond.
But while virtual events are great, nothing quite compares to meeting in person —
learning from peers, exchanging ideas, and continuing the conversation over coffee, beer, or wine.
That’s why we’re excited to announce our first in-person community meetup: Vespa.ai Live!</p>

<p>The event includes technical sessions, real-world user experiences, expert panels,
interactive unconference discussions,
and plenty of opportunities to connect with others building in this space.
Hosted by The Search Juggler, Charlie Hull,
the day will bring together external experts and leading authors Trey Grainger and Doug Turnbull,
Vespa engineers,
community voices with speakers from Walmart, Etsy and RavenPack,
and a keynote from Vespa co-founder and CEO Jon Bratseth.</p>

<p>On September 9, join the pre-event training, with <em>Vespa 101: Getting started with Vespa</em>,
and <em>Ranking 202: A deeper dive into improving retrieval quality</em> - <a href="https://www.tickettailor.com/events/vespaaias/2158210">details and registration</a>.</p>

<p>Most of all, Vespa.ai Live is intended to be community-driven —
where Vespians share lessons learned and boldly go beyond the frontier of modern AI retrieval.</p>

<p><a href="https://content.vespa.ai/vespa-live">Learn more about Vespa Live!</a></p>

<h3 id="product-updates">Product updates</h3>

<ul>
  <li>Vespa Cloud: Detailed metric dashboards</li>
  <li>Vespa Cloud: Index backup</li>
  <li>Vespa Cloud: Fine-grained maintenance controls</li>
  <li>Vespa Cloud: Voyage AI, OpenAI, and Mistral AI embedding integration</li>
  <li>Vespa Cloud: Custom resource tags</li>
  <li>Vespa skills for agents</li>
  <li>A new query operator for text matching</li>
  <li>Cluster-size independent configuration of relevance effort</li>
  <li>Boolean array fields</li>
  <li>Match specific array elements</li>
  <li>In-memory document ids</li>
  <li>Search group pinning</li>
  <li><em>Near</em> matching aware ranking</li>
  <li>Detect ignored write operations</li>
  <li>Accessing the max first phase score in re-ranking</li>
  <li>Geo distance in grouping</li>
</ul>

<h3 id="vespa-cloud-detailed-metric-dashboards">Vespa Cloud: Detailed metric dashboards</h3>
<p>As companies deploy their large-scale latency-sensitive applications on Vespa Cloud
there is a need for more detailed insights into how the application is performing.
While the Vespa Cloud Console has always provided an overview metrics dashboard,
many finer details have only been available to Vespa’s operations engineers.</p>

<p><strong>What’s new:</strong> We have added all the metrics dashboard used by Vespa engineers to the console
so that customers who want to are empowered to dig as deeply as they like.
We’ve also added explanations to the dashboards to make them easier to understand.</p>

<p><img src="/assets/2026-05-26-newsletter/dashboard.png" alt="Dashboard" /></p>

<p><a href="https://blog.vespa.ai/the-vespa-cloud-metrics-dashboard/">Read more</a>.</p>

<h3 id="vespa-cloud-index-backup">Vespa Cloud: Index backup</h3>
<p>Reliability at scale means being prepared for the unexpected.
Vespa Cloud now provides automated snapshot backups of indexes from content nodes,
enabling catastrophe recovery without a full re-index.</p>

<p><strong>What’s New:</strong> Vespa Cloud now supports backing up indexes from content nodes.
The snapshot backups can be used for catastrophe restore of nodes.
<a href="https://docs.vespa.ai/en/operations/data-management.html#backup">Read more</a>.</p>

<h3 id="vespa-cloud-fine-grained-maintenance-controls">Vespa Cloud: Fine-Grained maintenance controls</h3>
<p>Vespa Cloud has always provided control over when and how application changes and Vespa upgrades
are rolled out in production by the <a href="https://docs.vespa.ai/en/operations/automated-deployments.html">CD pipeline</a>.
In addition, Vespa Cloud does occasional OS upgrades as a background host level operation which is orchestrated
but not rolled out by the CD pipeline.</p>

<p>We already see a need to run these processes more aggressively
and anticipate that this trend will accelerate as capabilities similar to Claude Mythos become widely available.
While this is necessary to maintain a strong security posture in the times ahead
it has the potential to increase the impact from OS level maintenance on observed metrics,
such as when these operations happen during peak traffic.</p>

<p><strong>What’s new:</strong> Vespa Cloud now lets you control when these maintenance operations are allowed to happen
in the same way as deployments are controlled.
See the maintenance attribute in <a href="https://docs.vespa.ai/en/reference/applications/deployment.html#block-change">deployment.xml’s block-change tag</a>.</p>

<h3 id="vespa-cloud-voyage-ai-openai-and-mistral-ai-embedding-integration">Vespa Cloud: Voyage AI, OpenAI, and Mistral AI embedding integration</h3>
<p>Embedding models are central to AI search, and it’s now simpler to use the most popular ones in Vespa Cloud.</p>

<p><strong>What’s new:</strong> You can now save your API key in Vespa Cloud and invoke the embedding APIs of
<a href="https://docs.voyageai.com/docs/introduction">Voyage AI</a>,
<a href="https://developers.openai.com/api/docs/guides/embeddings">OpenAI</a>,
and <a href="https://docs.mistral.ai/studio-api/knowledge-rag/embeddings">Mistral AI</a>.
These APIs can be invoked at document processing time (indexing), as well as query time. With Voyage AI and the voyage-4 model family, you can use the API for documents and a smaller, local model for queries, eliminating the need for an API call in the query path.</p>

<p>Read more in the Vespa embedding documentation for:
<a href="https://docs.vespa.ai/en/rag/embedding.html#voyageai-embedder">Voyage AI</a>,
<a href="https://docs.vespa.ai/en/rag/embedding.html#openai-embedder">OpenAI</a>,
and <a href="https://docs.vespa.ai/en/rag/embedding.html#mistral-embedder">Mistral AI</a>.</p>

<h3 id="vespa-cloud-custom-resource-tags">Vespa Cloud: Custom resource tags</h3>
<p>With Vespa Cloud Enclave, Vespa will provision resources in accounts owned by the customer.
Many companies want to track these resources, e.g. for financial monitoring.</p>

<p><strong>What’s New</strong>: Vespa now lets you declare custom resource tags in deployment.xml that will be applied on provisioned resources.
The tag declarations can contain template variables such that resources can be tagged with e.g. the application they belong to.
<a href="https://docs.vespa.ai/en/reference/applications/deployment.html#resource-tags">Read more</a>.</p>

<h3 id="vespa-skills-for-agents">Vespa skills for agents</h3>
<p>Coding agents are good at working with Vespa applications.
Giving them the relevant skills makes this even more efficient.</p>

<p><strong>What’s new:</strong> We have released a collection of skills for agents working with Vespa applications,
available <a href="https://github.com/vespaai-playground/skills">here</a>.
This includes skills for working with application packages, feeding and queries, as well as migrating from ElasticSearch to Vespa.
We run evaluations over these skills to ensure that they actually improve outcomes with current models.</p>

<h3 id="a-new-query-operator-for-text-matching">A new query operator for text matching</h3>
<p>To do lexical matching with an arbitrary input string in Vespa, you can use <code class="language-plaintext highlighter-rouge">userInput(“my text”)</code> in YQL.
This assumes that the text can control simple syntax for controlling the matching, such as “some-field:” to specify the field to match.
Sometimes, the text should just be interpreted as raw text with no such query syntax.</p>

<p><strong>What’s new:</strong> Vespa now supports a new <code class="language-plaintext highlighter-rouge">text()</code> operator which interprets the argument text simply as raw text with no syntax.
When there’s no syntax, the text can only end up searching one field or fieldSet
and so the regular syntax can be used to specify the field: <code class="language-plaintext highlighter-rouge">where my-field contains text(“my text”)</code>.
<a href="https://docs.vespa.ai/en/reference/querying/yql.html#text">Read more</a>.</p>

<h3 id="cluster-size-independent-configuration-of-relevance-effort">Cluster-size independent configuration of relevance effort</h3>
<p>Vespa has various parameters to set how much effort (CPU) should be spent on providing good results in a query.
These parameters are specified as a value <em>per content node</em>,
so if you want the total expenditure to stay constant when you change the number of content nodes,
you must remember to update these parameters.</p>

<p>This is easy to forget, and of course impossible with autoscaling activated.
What’s more, when new nodes are added to clusters, they will initially have less data than other nodes,
but will get the same setting as nodes with a full share of data.</p>

<p><strong>What’s new:</strong> Vespa now supports alternatives of these configuration parameters prefixed by “total”,
which allows you to specify values across all the content nodes.
Vespa will automatically calculate the right share for each node in the cluster group,
including when nodes temporarily have less data than normally.</p>

<p>The new “total-” parameters are:</p>

<ul>
  <li>NearestNeighbor and WeakAnd <a href="https://docs.vespa.ai/en/reference/querying/yql.html#totaltargethits">totalTargetHits</a>:
The minimum number of hits the query operator should produce</li>
  <li>Match-phase <a href="https://docs.vespa.ai/en/reference/schemas/schemas.html#match-phase-total-max-hits">total-max-hits</a> /
<a href="https://docs.vespa.ai/en/reference/api/query.html#ranking.matchphase.totalmaxhits">ranking.matchphase.totalmaxhits</a></li>
  <li>First-phase <a href="https://docs.vespa.ai/en/reference/schemas/schemas.html#total-keep-rank-count">total-keep-rank-count</a> /
<a href="https://docs.vespa.ai/en/reference/api/query.html#ranking.totalkeeprankcount">ranking.totalKeepRankCount</a></li>
  <li>Second-phase <a href="https://docs.vespa.ai/en/reference/schemas/schemas.html#secondphase-total-rerank-count">total-rerank-count</a> /
<a href="https://docs.vespa.ai/en/reference/api/query.html#ranking.secondphase.totalrerankcount">ranking.secondPhase.totalRerankCount</a></li>
</ul>

<h3 id="boolean-array-fields">Boolean array fields</h3>
<p>Once developers move beyond the basics to really put the power of Vespa to work
they often want to pack large amounts of dynamic metadata into documents,
such as for example storing information about each document’s relationship to each zip code in the US.
At scale the memory efficiency with such usage really matters.</p>

<p><strong>What’s new:</strong> Vespa now lets you create arrays of bits fields: <code class="language-plaintext highlighter-rouge">field my_bits type array&lt;bool&gt;</code>.
Booleans can both be standalone or part of a struct type which is wrapped in an array.
<a href="https://docs.vespa.ai/en/reference/schemas/schemas.html#array">Read more</a>.</p>

<h3 id="match-specific-array-elements">Match specific array elements</h3>
<p>Arrays in documents can be searched both as attributes and text indexes.
You can also match multiple struct values or text tokens of the same array element by using the sameElement operator.
In some use cases, you also want to match a specific index in the array.</p>

<p><strong>What’s new:</strong> Vespa now lets you specify the array index you want to match in queries: <code class="language-plaintext highlighter-rouge">select … where my_bits[94085] = true</code>.
You can also search for multiple indexes in the same query by using slightly more complicated syntax:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>select … where my_array contains ({elementFilter:[33, 34]}sameElement(first_name contains "John", last_name contains "Doe")
</code></pre></div></div>

<p>See the <a href="https://docs.vespa.ai/en/reference/querying/yql.html#elementfilter">elementFilter</a> documentation.
This is also supported in <a href="https://docs.vespa.ai/en/reference/querying/json-query-language.html#search-within-same-struct-element">JSON queries</a>
by using an index attribute.</p>

<h3 id="search-group-pinning">Search group pinning</h3>
<p>When a content cluster has <a href="https://docs.vespa.ai/en/performance/topology-and-resizing.html">multiple groups</a>,
they will all have the same data,
but their indexes will be slightly different since each node in each group has a different subset of the data written in a different order.
This can lead to some inconsistency when a user is paging over a result set and hitting different groups.</p>

<p><strong>What’s new:</strong> Vespa now lets you pin queries to a specific group to make pagination queries consistent.
<a href="https://docs.vespa.ai/en/content/elasticity.html#pinning-groups">Read more</a>.</p>

<h3 id="in-memory-document-ids">In-memory document ids</h3>
<p>Document ids in Vespa are only stored on disk only.
This saves memory, but makes it impossible to retrieve the full ids of many documents really fast in queries and visiting.</p>

<p><strong>What’s new:</strong> From Vespa 8.691 you can declare in the schema that document ids should reside in memory for fast access similar to attributes.
<a href="https://docs.vespa.ai/en/schemas/documents.html#docid-in-results">Read more</a>.</p>

<h3 id="near-matching-aware-ranking"><em>Near</em> matching aware ranking</h3>
<p>When using the <em>near</em> and <em>onear</em> query operators,
the most intuitive ranking is using only the terms matching in the operator itself for rank scoring. Example:</p>

<p>Suppose near(term1, term2) matches document1 because of a single window where term1 and term2 appear close enough.
If document1 contains term1 many additional times outside the valid proximity window,
this is less relevant with respect to ranking.</p>

<p><strong>What’s new:</strong> From Vespa 8.672, terms outside the match window are not considered in relevance calculations.
<a href="https://docs.vespa.ai/en/reference/querying/yql.html#near">Read more</a>.</p>

<h3 id="detect-ignored-write-operations">Detect ignored write operations</h3>
<p>Content clusters can specify what documents they should receive in a document selection.
Sending a document operation which is ignored by every cluster is not an error, but you may want to know.</p>

<p><strong>What’s New:</strong> From Vespa 8.680, the document/v1 API includes a dedicated
<a href="https://docs.vespa.ai/en/reference/api/document-v1.html#x-vespa-ignored-operation">X-Vespa-Ignored-Operation</a> HTTP response header.
When an operation is ignored during routing (for example, because the target document no longer exists),
this header is present and set to “true”.
Read more in <a href="https://github.com/vespa-engine/vespa/issues/36397">this issue</a>.</p>

<h3 id="accessing-the-max-first-phase-score-in-re-ranking">Accessing the max first phase score in re-ranking</h3>
<p><strong>What’s new:</strong> <em>firstPhaseMax</em> is a new <a href="https://docs.vespa.ai/en/reference/ranking/rank-features.html">rank feature</a>
which exposes the rank score of the top scoring document locally on the node in second-phase ranking.</p>

<p>One usage of this is to enable dropping documents that score too low <em>relative</em> to the best-scoring document,
by combining it with a <a href="https://docs.vespa.ai/en/reference/schemas/schemas.html#rank-score-drop-limit">rank-score-drop-limit</a>.</p>

<h3 id="geo-distance-in-grouping">Geo distance in grouping</h3>
<p><strong>What’s new:</strong> Grouping now lets you group by <a href="https://docs.vespa.ai/en/reference/querying/grouping-language.html#geo_distance">geo_distance</a>:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>all( group(fixedwidth(geo_distance(attribute(location), 63.4, 10.4).km, 10)) each(output(count())) )
</code></pre></div></div>

<h3 id="image-search-demo">Image Search demo</h3>
<p><img src="/assets/2026-05-26-newsletter/image-search.png" alt="Image search demo" /></p>

<p>Try the <a href="https://reverse-image-search.vespa-demos.ai/">image search demo</a> to test how ranking profiles with different tensor precision in ranking affect results. Hint: binarized embeddings perform really well,
<a href="https://reverse-image-search.vespa-demos.ai/article">read the report</a>!</p>

<h3 id="vespa-learn">Vespa Learn</h3>
<p><a href="http://learn.vespa.ai">learn.vespa.ai</a> is a self-paced course that teaches you how to build search, recommendation, and RAG applications with Vespa.
You will go from zero to a working e-commerce search engine with hybrid retrieval and machine learning ranking,
building it up one piece at a time across six modules.</p>

<h3 id="whats-new-on-youtube">What’s New on YouTube</h3>
<ul>
  <li><a href="https://vespa.ai/resource/multimodal-intelligence-for-life-sciences-on-aws/">Multimodal Intelligence for Life Sciences on AWS</a> <em>(Webinar)</em></li>
  <li><a href="https://vespa.ai/resource/the-personalization-problem-in-ecommerce-am/">The Personalization Problem in eCommerce AM</a> <em>(Webinar)</em></li>
  <li><a href="https://vespa.ai/resource/the-personalization-problem-in-ecommerce-em/">The Personalization Problem in eCommerce EM</a> <em>(Webinar)</em></li>
  <li><a href="https://vespa.ai/resource/the-relevance-problem-in-ecommerce/">The Relevance Problem in eCommerce</a> <em>(Webinar)</em></li>
  <li><a href="https://vespa.ai/resource/vespa-now-q1-product-update/">Vespa Now: Q1 Product Update</a> <em>(Webinar)</em></li>
  <li><a href="https://vespa.ai/resource/webinar-zero-results/">Zero Results Webinar</a></li>
</ul>

<p>Find more videos in the <a href="https://www.youtube.com/@vespaai">@vespaai</a> channel.</p>

<h3 id="blogs-and-ebooks">Blogs and ebooks</h3>
<ul>
  <li>Learn how <a href="https://www.kleinanzeigen.de/">Kleinanzeigen</a> built a single system with user behavioral profiles alongside ads,
WAND for fast inner-product retrieval over sparse attribute vectors,
embedding-based ANN search,
and click and search events processed as document updates:
<a href="https://medium.com/berlin-tech-blog/from-elasticsearch-to-vespa-rebuilding-the-kleinanzeigen-homepage-feed-part-1-b6164e366ab8">From Elasticsearch to Vespa: Rebuilding the Kleinanzeigen Homepage Feed</a>.</li>
  <li><a href="https://blog.vespa.ai/scaling-a-vespa-application-feeding-fast-and-furiously/">Scaling a Vespa Application: Feeding Fast and Furiously</a></li>
  <li><a href="https://blog.vespa.ai/the-vespa-cloud-metrics-dashboard/">The Vespa Cloud Metrics Dashboard</a></li>
  <li><a href="https://blog.vespa.ai/onnx-external-data-in-vespa-embedders/">Using Large ONNX Models with External Data in Vespa Embedders</a></li>
  <li><a href="https://blog.vespa.ai/asymmetric-retrieval-spend-on-docs-queries-for-free/">Asymmetric Retrieval: Spend on Docs, Embed your Queries for Free</a></li>
  <li><a href="https://blog.vespa.ai/agent-driven-intelligence-on-vespa-cloud/">How Metal AI Built an Agent-Driven Intelligence Platform on Vespa Cloud</a></li>
  <li><a href="https://blog.vespa.ai/build-a-high-quality-rag-app-on-vespa-cloud-in-15-minutes/">Build a High-Quality RAG App on Vespa Cloud in 15 Minutes</a></li>
</ul>

<h3 id="upcoming-events">Upcoming events</h3>
<ul>
  <li><a href="https://retailx.events/commerceai-summit-2026">Commerce AI Summit London</a>: June 3, London, UK. An executive-style event connecting retailers, brands, and AI solution providers.</li>
  <li><a href="https://2026.berlinbuzzwords.de/">Berlin Buzzwords</a>:  June 7-9, Berlin, Germany. Europe’s leading conference for data infrastructure, search, and machine learning.</li>
  <li><a href="https://europe.shoptalk.com/home">Shoptalk Europe</a>: June 9-11, Fira Gran Via, Barcelona. Europe’s home for retail innovation, bringing together 4,500+ trailblazers and 180+ speakers focused on AI and the future of commerce.</li>
  <li><a href="https://etailuk.wbresearch.com/">Etail UK</a>: June 16-17, Manchester, England. A leading eCommerce and retail conference focused on digital commerce strategy, customer experience, and AI-driven personalization for modern retail teams.</li>
</ul>

<hr />
<p>👉 <a href="https://www.linkedin.com/company/vespa-ai/">Follow us on LinkedIn</a> to stay in the loop on upcoming events, blog posts, and announcements.</p>

<hr />

<p>Thanks for joining us in exploring the frontiers of AI with Vespa. Ready to take your projects to the next level? <a href="https://vespa.ai/free-trial/">Deploy your application for free</a> on Vespa Cloud today.</p>

]]></content:encoded>
        <pubDate>Wed, 27 May 2026 00:00:00 +0000</pubDate>
        <link>https://blog.vespa.ai/vespa-newsletter-may-2026/</link>
        <guid isPermaLink="true">https://blog.vespa.ai/vespa-newsletter-may-2026/</guid>
        
        
        <category>newsletter</category>
        
      </item>
    
      <item>
        <title>Scaling a Vespa Application: Feeding Fast and Furiously</title>
        <description>A tutorial on how to scale the resources in a Vespa application to increase feed throughput. Using the metrics dashboard for informed and optimised scaling.</description>
        
        <media:thumbnail url="https://blog.vespa.ai/assets/2026-04-16-scaling-a-vespa-application-feeding-fast-and-furiously/shaun-sullivan-4Ia69jX7rq4-unsplash.jpg" />
        
        <content:encoded><![CDATA[<p><em>This is a blog/series on how to scale and evaluate a Vespa Application for serving enterprise-scale workloads and customer facing applications with potentially millions of users. Vespa is the AI search platform and all-in-one solution for all your retrieval and large scale computation needs.</em></p>

<p>In this blog I will show you how to feed a large dataset to a Vespa Application. We will be using the full MS_marco passages dataset, which is perhaps the most comprehensive open dataset for information retrieval. It is around 4GB and contains more than 8 million passages on a wide range of topics. The goal in this blog is to show how scaling works in Vespa through feeding the entire dataset as fast as we can.</p>

<h1 id="creating-the-vespa-application">Creating the Vespa Application</h1>

<p>We will be using a pre-made sample application as our basis for scaling but the concepts are the same for any other application.</p>

<p>Setup:</p>

<ol>
  <li>
    <p><strong>Create a <a href="https://docs.vespa.ai/en/learn/tenant-apps-instances.html">tenant</a> on Vespa Cloud:</strong></p>

    <p>Go to <a href="https://console.vespa-cloud.com/">console.vespa-cloud.com</a> and create your tenant (unless you already have one).</p>
  </li>
  <li><strong>Install the <a href="https://docs.vespa.ai/en/clients/vespa-cli.html">Vespa CLI</a></strong> using <a href="https://brew.sh/">Homebrew</a>:
    <div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>$ brew install vespa-cli
</code></pre></div>    </div>
    <p>Windows/No Homebrew? See the <a href="https://docs.vespa.ai/en/clients/vespa-cli.html">Vespa CLI page</a> to download directly.</p>
  </li>
  <li><strong>Configure the Vespa client:</strong>
    <div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>$ vespa config set target cloud
$ vespa config set application your-tenant-name-here.scalingtutorial
</code></pre></div>    </div>
  </li>
  <li><strong>Get Vespa Cloud control plane access:</strong>
    <div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>$ vespa auth login
</code></pre></div>    </div>
    <p>Follow the instructions from the command to authenticate.</p>
  </li>
  <li><strong>Clone the sample application:</strong>
    <div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>$ vespa clone scaling-tutorial scaling-app &amp;&amp; cd scaling-app
</code></pre></div>    </div>
    <p>This sample app is perfect for demonstrating scaling and performance as it is quite intensive to run both for feeding and querying.
You can also check out <a href="https://github.com/vespa-engine/sample-apps">sample-apps</a> for other sample apps you can clone.</p>
  </li>
  <li><strong>Add a certificate for <a href="https://docs.vespa.ai/en/security/guide#data-plane">data plane access</a> to the application:</strong>
    <div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>$ vespa auth cert
</code></pre></div>    </div>
    <p>It is a good idea to take note of the path to the .pem files written here.</p>
  </li>
  <li>
    <p><strong>Add the cross-encoder and Colbert model</strong></p>

    <p>Export the cross-encoder ranker model to onnx format using the <a href="https://huggingface.co/docs/optimum/index">Optimum</a> library from HF or download an exported ONNX version of the model (like in this example)</p>
    <div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>$ mkdir -p models
$ curl -L https://huggingface.co/Xenova/ms-marco-MiniLM-L-6-v2/resolve/main/onnx/model.onnx -o models/model.onnx
$ curl -L https://huggingface.co/Xenova/ms-marco-MiniLM-L-6-v2/raw/main/tokenizer.json -o models/tokenizer.json
</code></pre></div>    </div>
  </li>
  <li>
    <p><strong>Download the dataset</strong></p>

    <p>The msmarco passages dataset can be found <a href="https://huggingface.co/datasets/Tevatron/msmarco-passage-corpus">here</a>. Download, unzip it and place it in the <code class="language-plaintext highlighter-rouge">ext/</code> folder in our application.</p>

    <p><strong>NOTE: You will need around 8GB of free disk space for the dataset and the subsets we will be creating.</strong></p>
  </li>
  <li>
    <p><strong>Prepare the dataset for Vespa</strong></p>

    <p>Then run the script to convert it into the vespa feed format:</p>
    <div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>$ python3 ext/transform_ms_marco.py
</code></pre></div>    </div>
    <p>which gives us the dataset and a few subsets of various sizes to feed to our application.</p>
  </li>
</ol>

<h1 id="deploying-and-feeding">Deploying and Feeding</h1>

<p>We now have everything we need for deployment, feeding and scaling! Scaling a vespa application is largely managed through the services.xml file. This is what the file currently looks like:</p>

<style>
.code-block{overflow:hidden;border:1px solid #e0e0e0;background:#fafafa;font-family:monospace}
.code-header{display:flex;align-items:center;justify-content:space-between;padding:8px 14px;background:#f4f4f4;border-bottom:1px solid #e0e0e0}
.code-lang{font-size:11px;color:#999;letter-spacing:.06em;text-transform:uppercase;font-family:sans-serif}
.copy-btn{font-size:11px;color:#999;background:none;border:1px solid #ddd;padding:2px 9px;cursor:pointer;font-family:sans-serif}
.copy-btn:hover{color:#333;border-color:#aaa}
.code-body{position:relative}
.code-scroll{overflow:hidden;max-height:132px;transition:max-height 0.4s cubic-bezier(0.4,0,0.2,1)}
.code-scroll.expanded{max-height:4000px}
.code-block pre{padding:14px 16px;font-size:12.5px;line-height:22px;color:#222;overflow-x:auto;white-space:pre;tab-size:2;margin:0;background:none}
.fade-overlay{position:absolute;bottom:0;left:0;right:0;height:72px;background:linear-gradient(to bottom,transparent,#fafafa);pointer-events:none;transition:opacity 0.3s ease}
.code-scroll.expanded~.fade-overlay{opacity:0}
.show-btn-wrap{display:flex;justify-content:center;padding:8px 0 12px;background:#fafafa;border-top:1px solid #e0e0e0}
.show-btn{font-size:12px;color:#555;background:none;border:none;padding:4px 12px;cursor:pointer;display:flex;align-items:center;gap:5px;font-family:sans-serif}
.show-btn:hover{color:#000}
.show-btn svg{transition:transform 0.3s ease}
.show-btn.open svg{transform:rotate(180deg)}
</style>

<div class="code-block">
  <div class="code-header">
    <span class="code-lang">XML — services.xml</span>
    <button class="copy-btn" onclick="(function(){var t=document.getElementById('vespa-raw').innerText;navigator.clipboard.writeText(t).then(function(){var b=document.querySelector('.copy-btn');b.textContent='Copied!';setTimeout(function(){b.textContent='Copy'},1500)})})()">Copy</button>
  </div>
  <div class="code-body">
    <div class="code-scroll" id="vespa-scroll">
      <pre id="vespa-raw">&lt;?xml version="1.0" encoding="utf-8" ?&gt;
&lt;!-- Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. --&gt;
&lt;services version="1.0" xmlns:deploy="vespa" xmlns:preprocess="properties" minimum-required-vespa-version="8.311.28"&gt;

  &lt;container id="default" version="1.0"&gt;

    &lt;nodes deploy:environment="dev" count="1"&gt;
      &lt;resources vcpu="1.0" memory="8Gb" architecture="arm64" storage-type="local" disk="59Gb"/&gt;
    &lt;/nodes&gt;
   
    &lt;search/&gt;
    &lt;document-api/&gt;

     &lt;!-- See https://docs.vespa.ai/en/embedding.html#huggingface-embedder --&gt;
    &lt;component id="e5_embedding_model" type="hugging-face-embedder"&gt;
            &lt;transformer-model url="https://huggingface.co/intfloat/e5-small-v2/resolve/main/model.onnx"/&gt;
            &lt;tokenizer-model url="https://huggingface.co/intfloat/e5-small-v2/raw/main/tokenizer.json"/&gt;
            &lt;prepend&gt;
                &lt;query&gt;query:&lt;/query&gt;
                &lt;document&gt;passage:&lt;/document&gt;
            &lt;/prepend&gt;
    &lt;/component&gt;

    &lt;!-- See https://docs.vespa.ai/en/embedding.html#colbert-embedder --&gt;
    &lt;component id="colbert_embedding_model" type="colbert-embedder"&gt;
      &lt;transformer-model url="https://huggingface.co/colbert-ir/colbertv2.0/resolve/main/model.onnx"/&gt;
      &lt;tokenizer-model url="https://huggingface.co/colbert-ir/colbertv2.0/raw/main/tokenizer.json"/&gt;
    &lt;/component&gt;

     &lt;!-- See https://docs.vespa.ai/en/reference/embedding-reference.html#huggingface-tokenizer-embedder--&gt;
    &lt;component id="tokenizer" type="hugging-face-tokenizer"&gt;
      &lt;model path="models/tokenizer.json"/&gt;
    &lt;/component&gt;

  &lt;/container&gt;

  &lt;content id="msmarco" version="1.0"&gt;
    &lt;min-redundancy&gt;1&lt;/min-redundancy&gt;
    &lt;documents&gt;
      &lt;document mode="index" type="passage"/&gt;
    &lt;/documents&gt;
    &lt;nodes deploy:environment="dev" count="1"&gt;
      &lt;resources vcpu="1.0" memory="8Gb" architecture="arm64" storage-type="local" disk="59Gb"/&gt;
    &lt;/nodes&gt; 
    &lt;engine&gt;
      &lt;proton&gt;
        &lt;tuning&gt;
          &lt;searchnode&gt;
            &lt;requestthreads&gt;
              &lt;persearch&gt;4&lt;/persearch&gt;
            &lt;/requestthreads&gt;
            &lt;feeding&gt;
              &lt;concurrency&gt;1.0&lt;/concurrency&gt;
            &lt;/feeding&gt;
          &lt;/searchnode&gt;
        &lt;/tuning&gt;
      &lt;/proton&gt;
    &lt;/engine&gt;
  &lt;/content&gt;

&lt;/services&gt;
</pre>
    </div>
    <div class="fade-overlay"></div>
  </div>
  <div class="show-btn-wrap">
    <button class="show-btn" id="vespa-btn" onclick="(function(){var s=document.getElementById('vespa-scroll');var b=document.getElementById('vespa-btn');var open=s.classList.toggle('expanded');b.classList.toggle('open',open);b.innerHTML=open?'&lt;svg width=\'14\' height=\'14\' viewBox=\'0 0 14 14\' fill=\'none\'&gt;&lt;path d=\'M2 4.5L7 9.5L12 4.5\' stroke=\'#89b4fa\' stroke-width=\'1.8\' stroke-linecap=\'round\' stroke-linejoin=\'round\'/&gt;&lt;/svg&gt; Show less':'&lt;svg width=\'14\' height=\'14\' viewBox=\'0 0 14 14\' fill=\'none\'&gt;&lt;path d=\'M2 4.5L7 9.5L12 4.5\' stroke=\'#89b4fa\' stroke-width=\'1.8\' stroke-linecap=\'round\' stroke-linejoin=\'round\'/&gt;&lt;/svg&gt; Show all'})()">
      <svg width="14" height="14" viewBox="0 0 14 14" fill="none"><path d="M2 4.5L7 9.5L12 4.5" stroke="#888" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" /></svg>
      Show all
    </button>
  </div>
</div>

<p><br /></p>

<p>The important parts to take note of in this tutorial are the two resource specifiers in the &lt;container&gt; and &lt;content&gt; tags:</p>

<p><strong>Container</strong></p>
<div class="language-xml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nt">&lt;nodes</span> <span class="na">deploy:environment=</span><span class="s">"dev"</span> <span class="na">count=</span><span class="s">"1"</span><span class="nt">&gt;</span>
    <span class="nt">&lt;resources</span> <span class="na">vcpu=</span><span class="s">"1.0"</span> <span class="na">memory=</span><span class="s">"8Gb"</span> <span class="na">architecture=</span><span class="s">"arm64"</span> <span class="na">storage-type=</span><span class="s">"local"</span> <span class="na">disk=</span><span class="s">"59Gb"</span><span class="nt">/&gt;</span>
<span class="nt">&lt;/nodes&gt;</span>
</code></pre></div></div>

<p><strong>Content</strong></p>
<div class="language-xml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nt">&lt;nodes</span> <span class="na">deploy:environment=</span><span class="s">"dev"</span> <span class="na">count=</span><span class="s">"1"</span><span class="nt">&gt;</span>
    <span class="nt">&lt;resources</span> <span class="na">vcpu=</span><span class="s">"1.0"</span> <span class="na">memory=</span><span class="s">"8Gb"</span> <span class="na">architecture=</span><span class="s">"arm64"</span> <span class="na">storage-type=</span><span class="s">"local"</span> <span class="na">disk=</span><span class="s">"59Gb"</span><span class="nt">/&gt;</span>
<span class="nt">&lt;/nodes&gt;</span>
</code></pre></div></div>

<p>This is where we configure the machine resources that our Vespa application runs on in Vespa Cloud.</p>

<p><strong>NOTE: when deploying to dev we need to add the <code class="language-plaintext highlighter-rouge">&lt;nodes deploy:environment="dev"&gt;</code> specifier to ensure we actually get the resources we ask for,
otherwise we default to what is quickly available</strong>.</p>

<p>Adding more resources or more nodes are the main parameters that need to be tweaked in order to scale your application. Right now we have provisioned the smallest amount of resources to our application.</p>

<p>Deploy the application to Vespa Cloud:</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>$ vespa deploy --wait 900
</code></pre></div></div>
<p>(It might take a little bit of time for all services and nodes to go up and start running.)</p>

<p>You can follow the progress of the deployment from the terminal or in your tenant in your cloud console. When it is finished you should get the message:</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>Application up!
</code></pre></div></div>
<p>If you go to your cloud console you should be able to see your application. Note that we haven’t fed it any documents yet, so it should look something like this:</p>

<p><img src="/assets/2026-04-16-scaling-a-vespa-application-feeding-fast-and-furiously/1howitshouldlook.png" alt="application view in console" /></p>

<p>Let’s feed some documents. Feed the smallest dataset to Vespa using:</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>vespa feed ext/corpus_transformed_1000.jsonl
</code></pre></div></div>
<p>or, on Unix systems:</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>time vespa feed ext/corpus_transformed_1000.jsonl
</code></pre></div></div>
<p>to see how long it takes.</p>

<p>It will take a few minutes as we are doing heavy computations on very modest resources.</p>

<p>If you want to see a live count of how many documents that are in Vespa you can run:</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>vespa query 'yql=select * from passage where true' 'hits=0' 'ranking=unranked'
</code></pre></div></div>
<p>to see how many documents have been processed so far (under <code class="language-plaintext highlighter-rouge">documents</code>).</p>

<p>On this lowest resource configuration we get this result.</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>vespa feed ext/corpus_transformed_1000.jsonl  4.96s user 7.11s system 3% cpu 5:56.05 total 
</code></pre></div></div>

<p>If we were to try and feed the whole 8.8 million passage msmarco dataset on this instance it would take more than a month to finish feeding!</p>

<p>We can do better!</p>

<h1 id="scaling">Scaling</h1>

<p>Before scaling the application we’ll delete the documents from our instance so that we have a fresh start.</p>

<p><img src="/assets/2026-04-16-scaling-a-vespa-application-feeding-fast-and-furiously/deleteDocs.png" alt="Deleting documents" /></p>

<p>Now lets assign more resources to our Vespa instance. From our schema we see that  we are doing extensive computations during feeding (notice the configuration in the <code class="language-plaintext highlighter-rouge">indexing</code> parameters)</p>

<p><strong>Schema</strong></p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>  field e5_embedding type tensor&lt;bfloat16&gt;(x[384]) {
    # Using the e5 embedding model defined in services.xml
    indexing: input text | embed e5_embedding_model | attribute | index
    attribute {
      distance-metric: angular
    }
    index { # override default hnsw settings 
      hnsw {
        max-links-per-node: 32
        neighbors-to-explore-at-insert: 400
      } 
    }
  }

  field colbert_embeddings type tensor&lt;int8&gt;(dt{}, x[16]) {
    # No index - used for ranking, not retrieval 
    indexing: input text | embed colbert_embedding_model | attribute
    attribute: paged
  }
</code></pre></div></div>

<p>Embedding in Vespa happens in the container cluster, so it is a very reasonable guess that if we can make the embeddings go faster, our whole system will be faster (bellow in this blog we will show how to more thouroughly deduce scaling parameters). So lets start by scaling up the resources for the <strong>container</strong> node. To see what resource configurations we have available we must look at the <a href="https://docs.vespa.ai/en/performance/instance-types/aws-instance-types.html">instance type</a> page in the documentation.
Embedding-computations are best suited to run on GPUs, so we will select an instance type with a GPU:</p>

<p><strong>Container</strong></p>
<div class="language-xml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nt">&lt;nodes</span> <span class="na">deploy:environment=</span><span class="s">"dev"</span> <span class="na">count=</span><span class="s">"1"</span><span class="nt">&gt;</span>
    <span class="nt">&lt;resources</span> <span class="na">vcpu=</span><span class="s">"4.0"</span> <span class="na">memory=</span><span class="s">"16Gb"</span> <span class="na">architecture=</span><span class="s">"x86_64"</span> <span class="na">storage-type=</span><span class="s">"local"</span> <span class="na">disk=</span><span class="s">"125Gb"</span><span class="nt">&gt;</span>
        <span class="nt">&lt;gpu</span> <span class="na">count=</span><span class="s">"1"</span> <span class="na">memory=</span><span class="s">"16.0Gb"</span><span class="nt">/&gt;</span>
    <span class="nt">&lt;/resources&gt;</span>
<span class="nt">&lt;/nodes&gt;</span>
</code></pre></div></div>

<p>Replace the resources in the container node in services.xml with the new instance type (see above). Leave the content node resources as is for now.</p>

<p>Run the command for checking document count to make sure that it is zero:</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>vespa query 'yql=select * from passage where true' 'hits=0' 'ranking=unranked'
</code></pre></div></div>
<p>and redeploy:</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>$ vespa deploy --wait 900
</code></pre></div></div>
<p>When the deployment is finished we’ll time the feeding process again.</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>time vespa feed ext/corpus_transformed_1000.jsonl
</code></pre></div></div>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>vespa feed ext/corpus_transformed_1000.jsonl  0.40s user 0.53s system 8% cpu 11.371 total 
</code></pre></div></div>
<p>11.4 seconds, that’s more like it! Instead of a month, this new instance would be able to crunch through the full dataset in just around a day!</p>

<p>We have now significantly upgraded a part of the hardware Vespa is running on. But before we scale up further we shall take a look at the <strong>metrics</strong> tab for our application. Go to <strong>Metrics</strong> and then <strong>resources</strong>.</p>

<p><img src="/assets/2026-04-16-scaling-a-vespa-application-feeding-fast-and-furiously/metrics_resources.png" alt="Metrics and Resources" /></p>

<p>This is where you see the resource usage history in your vespa instance, but most importantly it gives you a clear image of where your application is bottlenecked. The bottleneck for your application will be different depending on how your application is configured and the kind of computations you do. The previous 1000-line dataset was no match for the upgraded instance, so lets give it a bigger one to get some proper bottleneck data:</p>

<p>Delete the documents from the instance again, wait a bit, and run the command to ensure that we have no documents in our application</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>vespa query 'yql=select * from passage where true' 'hits=0' 'ranking=unranked'
</code></pre></div></div>
<p>Now we’ll feed the 50 000 line dataset to properly test and time the upgraded instance.</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>time vespa feed ext/corpus_transformed_50000.jsonl
</code></pre></div></div>
<p><strong>Result</strong></p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>vespa feed ext/corpus_transformed_50000.jsonl  17.44s user 22.68s system 11% cpu 5:56.41 total (~17.6 hours for full dataset)
</code></pre></div></div>

<p>This is a more accurate reading of the instance’s performance, and at 5min 56s to feed 50 000 documents, the full dataset would take around 17 and a half hours.</p>

<p>Look at the resources in the metrics and set it to show only the last 30 minutes so that we can see more clearly what went on. Notice the CPU-utilisation and the GPU-utilisation graphs. Notice that the GPU usage on the container node hit 100% and stayed there for the entire feeding process. The CPU usage on the container node peaked at 80% but leveled at around 60% and the content node’s CPU barely went over 50%.</p>

<p><img src="/assets/2026-04-16-scaling-a-vespa-application-feeding-fast-and-furiously/1g1c_container.png" alt="5 GPU 1 content - container node resources" /></p>

<p><img src="/assets/2026-04-16-scaling-a-vespa-application-feeding-fast-and-furiously/1g1c_gpu.png" alt="1 GPU 1 content - GPU resources" /></p>

<p><img src="/assets/2026-04-16-scaling-a-vespa-application-feeding-fast-and-furiously/1g1c_content.png" alt="1 GPU 1 content - content node resources" /></p>

<p>It is clear that on this Vespa Instance, the bottleneck for better feeding performance lies in the GPU processing. If we want to improve the feeding performance of the system, then we must increase the amount of GPUs in the container node.</p>

<p>Now that we know where the problem lies: Lets make it go faster! We’ll increase the amount of GPU nodes to 5 with the <code class="language-plaintext highlighter-rouge">count="5"</code> parameter in the container node in <code class="language-plaintext highlighter-rouge">services.xml</code>.</p>

<p><strong>Container</strong></p>
<div class="language-xml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nt">&lt;nodes</span> <span class="na">deploy:environment=</span><span class="s">"dev"</span> <span class="na">count=</span><span class="s">"5"</span><span class="nt">&gt;</span>
    <span class="nt">&lt;resources</span> <span class="na">vcpu=</span><span class="s">"4.0"</span> <span class="na">memory=</span><span class="s">"16Gb"</span> <span class="na">architecture=</span><span class="s">"x86_64"</span> <span class="na">storage-type=</span><span class="s">"local"</span> <span class="na">disk=</span><span class="s">"125Gb"</span><span class="nt">&gt;</span>
        <span class="nt">&lt;gpu</span> <span class="na">count=</span><span class="s">"1"</span> <span class="na">memory=</span><span class="s">"16.0Gb"</span><span class="nt">/&gt;</span>
    <span class="nt">&lt;/resources&gt;</span>
<span class="nt">&lt;/nodes&gt;</span>
</code></pre></div></div>
<p>save the <code class="language-plaintext highlighter-rouge">services.xml</code> file and redeploy:</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>$ vespa deploy --wait 900
</code></pre></div></div>
<p>Now lets feed the larger dataset:</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>time vespa feed ext/corpus_transformed_500000.jsonl
</code></pre></div></div>
<p><strong>Result</strong></p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>vespa feed ext/corpus_transformed_500000.jsonl  97.59s user 95.22s system 10% cpu 29:38.18 total (~8.8 hours for full dataset)
</code></pre></div></div>
<p>If we extrapolate the results we see we got around twice the speed of the single-container node instance. But why not 5 times the speed? Let’s look at the metrics.</p>

<p><img src="/assets/2026-04-16-scaling-a-vespa-application-feeding-fast-and-furiously/5g1c_container.png" alt="5 GPU 1 content - container node resources" /></p>

<p><img src="/assets/2026-04-16-scaling-a-vespa-application-feeding-fast-and-furiously/5g1c_gpu.png" alt="5 GPU 1 content - container GPU resources" /></p>

<p><img src="/assets/2026-04-16-scaling-a-vespa-application-feeding-fast-and-furiously/5g1c_content.png" alt="5 GPU 1 content - content node resources" /></p>

<p>We see that the container GPU utilization now sits comfortably at around 50% and the container CPU at around 20-30%. But the content node CPU utilization sits near 100%. The 5 content nodes with GPUs saturate the single content node’s ability to take in data. We have found the new bottleneck of the system.</p>

<p>We’ll add some more content nodes:</p>

<p><strong>Content</strong></p>
<div class="language-xml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nt">&lt;nodes</span> <span class="na">deploy:environment=</span><span class="s">"dev"</span> <span class="na">count=</span><span class="s">"2"</span><span class="nt">&gt;</span>
    <span class="nt">&lt;resources</span> <span class="na">vcpu=</span><span class="s">"1.0"</span> <span class="na">memory=</span><span class="s">"8Gb"</span> <span class="na">architecture=</span><span class="s">"arm64"</span> <span class="na">storage-type=</span><span class="s">"local"</span> <span class="na">disk=</span><span class="s">"59Gb"</span><span class="nt">/&gt;</span>
<span class="nt">&lt;/nodes&gt;</span> 
</code></pre></div></div>
<p>Delete the documents, redeploy, and refeed:</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>$ vespa deploy --wait 900
</code></pre></div></div>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>time vespa feed ext/corpus_transformed_500000.jsonl
</code></pre></div></div>
<p><strong>Result</strong></p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>vespa feed ext/corpus_transformed_500000.jsonl  92.22s user 88.21s system 17% cpu 16:51.51 total (~5.0 hours for full dataset)
</code></pre></div></div>
<p>Adding the second content node almost doubles the performance again. Look at the metrics to see what is going on.</p>

<p><img src="/assets/2026-04-16-scaling-a-vespa-application-feeding-fast-and-furiously/5g2c_container.png" alt="5 GPUs 2 content - container node resources" /></p>

<p><img src="/assets/2026-04-16-scaling-a-vespa-application-feeding-fast-and-furiously/5g2c_gpu.png" alt="5 GPUs 2 content - container GPU resources" /></p>

<p><img src="/assets/2026-04-16-scaling-a-vespa-application-feeding-fast-and-furiously/5g2c_content.png" alt="5 GPUs 2 content - content node resources" /></p>

<p>We see now that the container GPU (70-80%) and the content node CPU (80-90%) are both highly utilised, whilst the container node CPU is around 40%. Since we are already on the smallest instance type with a GPU we can’t scale down the cpu to match the others, so we have actually found a near optimal balance of container and content node resources for feeding this application.</p>

<p>Now that we have found a good balance, lets really scale up!</p>

<h1 id="feeding-fast-20-gpus">Feeding Fast: 20 GPUs</h1>

<p>If we want serious feed throughput, we need serious hardware. Let’s scale the container and content nodes proportionately and jump to 20 GPU container nodes and 8 content nodes at the same time:</p>

<p><strong>Container</strong></p>
<div class="language-xml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nt">&lt;nodes</span> <span class="na">deploy:environment=</span><span class="s">"dev"</span> <span class="na">count=</span><span class="s">"20"</span><span class="nt">&gt;</span>
    <span class="nt">&lt;resources</span> <span class="na">vcpu=</span><span class="s">"4.0"</span> <span class="na">memory=</span><span class="s">"16Gb"</span> <span class="na">architecture=</span><span class="s">"x86_64"</span> <span class="na">storage-type=</span><span class="s">"local"</span> <span class="na">disk=</span><span class="s">"125Gb"</span><span class="nt">&gt;</span>
        <span class="nt">&lt;gpu</span> <span class="na">count=</span><span class="s">"1"</span> <span class="na">memory=</span><span class="s">"16.0Gb"</span><span class="nt">/&gt;</span>
    <span class="nt">&lt;/resources&gt;</span>
<span class="nt">&lt;/nodes&gt;</span>
</code></pre></div></div>

<p><strong>Content</strong></p>
<div class="language-xml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nt">&lt;nodes</span> <span class="na">deploy:environment=</span><span class="s">"dev"</span> <span class="na">count=</span><span class="s">"8"</span><span class="nt">&gt;</span>
    <span class="nt">&lt;resources</span> <span class="na">vcpu=</span><span class="s">"1.0"</span> <span class="na">memory=</span><span class="s">"8Gb"</span> <span class="na">architecture=</span><span class="s">"arm64"</span> <span class="na">storage-type=</span><span class="s">"local"</span> <span class="na">disk=</span><span class="s">"59Gb"</span><span class="nt">/&gt;</span>
<span class="nt">&lt;/nodes&gt;</span>
</code></pre></div></div>

<p><strong>NOTE:</strong> At this point you will most likely hit the <code class="language-plaintext highlighter-rouge">quotaExceeded</code> error when you try to deploy. Vespa Cloud tenants have a default quota that prevents you from accidentally spending a lot of money. If you want to go past it, reach out to Vespa support. With the limit raised, redeploy:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>$ vespa deploy --wait 900
</code></pre></div></div>
<p>Delete any existing documents, wait for the count to hit zero, and feed the 500 000 line dataset again:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>time vespa feed ext/corpus_transformed_500000.jsonl
</code></pre></div></div>

<p><strong>Result</strong></p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>vespa feed ext/corpus_transformed_500000.jsonl  59.66s user 48.08s system 42% cpu 4:13.19 total (~1 hour 15 min for full dataset)
</code></pre></div></div>

<p><img src="/assets/2026-04-16-scaling-a-vespa-application-feeding-fast-and-furiously/20g8c_container.png" alt="20 GPUs 8 content - container node resources" /></p>

<p><img src="/assets/2026-04-16-scaling-a-vespa-application-feeding-fast-and-furiously/20g8c_gpu.png" alt="20 GPUs 8 content - container GPU resources" /></p>

<p><img src="/assets/2026-04-16-scaling-a-vespa-application-feeding-fast-and-furiously/20g8c_content.png" alt="20 GPUs 8 content - content node resources" /></p>

<p>At an estimated 1 hour and 15 minutes for the full dataset we see that we got pretty much exactly 4x feeding speed with 4x the resources. We also see that the utilisation metrics are essentially the same as the last run (feeding at 11:30), just faster.</p>

<h1 id="feeding-furiously-100-gpus">Feeding Furiously: 100 GPUs</h1>

<p>Finally, because we can: 100 GPU container nodes and 40 content nodes, and this time we will feed the full 8.8 million passage dataset in one go.</p>

<p><strong>Container</strong></p>
<div class="language-xml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nt">&lt;nodes</span> <span class="na">deploy:environment=</span><span class="s">"dev"</span> <span class="na">count=</span><span class="s">"100"</span><span class="nt">&gt;</span>
    <span class="nt">&lt;resources</span> <span class="na">vcpu=</span><span class="s">"4.0"</span> <span class="na">memory=</span><span class="s">"16Gb"</span> <span class="na">architecture=</span><span class="s">"x86_64"</span> <span class="na">storage-type=</span><span class="s">"local"</span> <span class="na">disk=</span><span class="s">"125Gb"</span><span class="nt">&gt;</span>
        <span class="nt">&lt;gpu</span> <span class="na">count=</span><span class="s">"1"</span> <span class="na">memory=</span><span class="s">"16.0Gb"</span><span class="nt">/&gt;</span>
    <span class="nt">&lt;/resources&gt;</span>
<span class="nt">&lt;/nodes&gt;</span>
</code></pre></div></div>

<p><strong>Content</strong></p>
<div class="language-xml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nt">&lt;nodes</span> <span class="na">deploy:environment=</span><span class="s">"dev"</span> <span class="na">count=</span><span class="s">"40"</span><span class="nt">&gt;</span>
    <span class="nt">&lt;resources</span> <span class="na">vcpu=</span><span class="s">"1.0"</span> <span class="na">memory=</span><span class="s">"8Gb"</span> <span class="na">architecture=</span><span class="s">"arm64"</span> <span class="na">storage-type=</span><span class="s">"local"</span> <span class="na">disk=</span><span class="s">"59Gb"</span><span class="nt">/&gt;</span>
<span class="nt">&lt;/nodes&gt;</span>
</code></pre></div></div>

<p>Delete the documents, Deploy, then feed the full dataset:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>time vespa feed ext/corpus_transformed_full.jsonl
</code></pre></div></div>

<p><strong>Result</strong></p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>vespa feed ext/corpus_transformed_full.jsonl  695.31s user 605.48s system 108% cpu 20:04.23 total
</code></pre></div></div>

<p>The Vespa instance managed to process more than 8.8 million passages, with embeddings and ColBERT vectors computed for every single one, in just over 20 minutes (over a fast internet connection).</p>

<p><img src="/assets/2026-04-16-scaling-a-vespa-application-feeding-fast-and-furiously/100g40c_console_3.png" alt="100 GPUs 40 content - feed complete at 8.84M documents" /></p>

<p>The feed client also gives us a nice summary at the end:</p>

<div class="language-json highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">{</span><span class="w">
  </span><span class="nl">"feeder.operation.count"</span><span class="p">:</span><span class="w"> </span><span class="mi">8841823</span><span class="p">,</span><span class="w">
  </span><span class="nl">"feeder.seconds"</span><span class="p">:</span><span class="w"> </span><span class="mf">1201.608</span><span class="p">,</span><span class="w">
  </span><span class="nl">"feeder.ok.count"</span><span class="p">:</span><span class="w"> </span><span class="mi">8841823</span><span class="p">,</span><span class="w">
  </span><span class="nl">"feeder.ok.rate"</span><span class="p">:</span><span class="w"> </span><span class="mf">7358.324</span><span class="p">,</span><span class="w">
  </span><span class="nl">"feeder.error.count"</span><span class="p">:</span><span class="w"> </span><span class="mi">399</span><span class="p">,</span><span class="w">
  </span><span class="nl">"http.request.count"</span><span class="p">:</span><span class="w"> </span><span class="mi">8844266</span><span class="p">,</span><span class="w">
  </span><span class="nl">"http.response.latency.millis.avg"</span><span class="p">:</span><span class="w"> </span><span class="mi">167</span><span class="p">,</span><span class="w">
  </span><span class="nl">"http.response.code.counts"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w">
    </span><span class="nl">"200"</span><span class="p">:</span><span class="w"> </span><span class="mi">8841823</span><span class="p">,</span><span class="w">
    </span><span class="nl">"429"</span><span class="p">:</span><span class="w"> </span><span class="mi">2044</span><span class="w">
  </span><span class="p">}</span><span class="w">
</span><span class="p">}</span><span class="w">
</span></code></pre></div></div>

<p>The feeding process had an average feed rate of around <strong>7358 documents per second</strong>. Now that is fast and furious!</p>

<h1 id="conclusion">Conclusion</h1>

<p>The best way to scale your Vespa instance is to use the metrics dashboards to see where the bottlenecks lie. There is no singular best instance of Vespa as the computational requirements are highly dependent on how you define your application. Feed the instance a sizable corpus to see how it performs under sustained load, and adjust its resources accordingly.</p>
]]></content:encoded>
        <pubDate>Tue, 28 Apr 2026 00:00:00 +0000</pubDate>
        <link>https://blog.vespa.ai/scaling-a-vespa-application-feeding-fast-and-furiously/</link>
        <guid isPermaLink="true">https://blog.vespa.ai/scaling-a-vespa-application-feeding-fast-and-furiously/</guid>
        
        <category>rag</category>
        
        <category>AI</category>
        
        <category>GenAI</category>
        
        <category>ranking</category>
        
        <category>tensors</category>
        
        
      </item>
    
      <item>
        <title>The Vespa Cloud Metrics Dashboard</title>
        <description>A guide to the Vespa Cloud metrics dashboard — how to move from symptom to bottleneck to action, and what&apos;s new in the latest revision.</description>
        
        <media:thumbnail url="https://blog.vespa.ai/assets/2026-04-24-the-vespa-cloud-metrics-dashboard/dashboard-overview.png" />
        
        <content:encoded><![CDATA[<p>When something goes wrong in production, the hard part is rarely finding a metric.
The hard part is figuring out <strong>which metric tells you where to look next</strong>.</p>

<p>The Vespa Cloud metrics dashboard is designed for exactly that.
Instead of treating monitoring as a wall of graphs, it helps you move from
<strong>symptom → bottleneck → action</strong>.</p>

<h2 id="start-with-three-questions">Start with three questions</h2>

<p>Most production issues can be reduced to three questions:</p>

<ol>
  <li><strong>Is the system healthy?</strong></li>
  <li><strong>Where is latency added?</strong></li>
  <li><strong>Are we running out of resources?</strong></li>
</ol>

<p>The dashboard mirrors that flow.</p>

<h3 id="1-is-the-system-healthy">1. Is the system healthy?</h3>

<p>Start on the <strong>Overview</strong> tab. This is the fastest place to answer “is anything
obviously broken?”. A healthy system keeps read and write QoS close to 100%.
If it drops, look at whether 4xx or 5xx responses are rising — 5xx responses
usually mean the problem is on the server side. A rise in degraded or failed queries
means it is time to continue into the Query tab.</p>

<p><img src="/assets/2026-04-24-the-vespa-cloud-metrics-dashboard/dashboard-overview.png" alt="Metrics Overview" /></p>

<p>See the docs for the full reference:
<a href="https://docs.vespa.ai/en/operations/monitoring.html#overview-tab">Metrics Overview tab</a>.</p>

<h3 id="2-where-is-latency-added">2. Where is latency added?</h3>

<p>Latency in Vespa is layered — a slow request is not just “slow”, it can be
slow in different parts of the path:</p>

<p><strong>HTTP → container → content nodes → ranking</strong></p>

<p>That is why the dashboard shows several latency metrics for what feels like
the same request. If HTTP latency is much higher than query latency,
payload size or network overhead may be the issue. If search-protocol latency
on the content nodes is high, the bottleneck is deeper in the system.</p>

<p><img src="/assets/2026-04-24-the-vespa-cloud-metrics-dashboard/dashboard-query-rate-latency.png" alt="Query rate / Latency" /></p>

<p>See the docs for a layer-by-layer walkthrough:
<a href="https://docs.vespa.ai/en/operations/monitoring.html#query-tab">Query tab</a>
and <a href="https://docs.vespa.ai/en/operations/monitoring.html#feed-tab">Feed tab</a>.</p>

<h3 id="3-are-we-running-out-of-resources">3. Are we running out of resources?</h3>

<p>Once you know where the slowdown is, switch to the <strong>Resources</strong> tab.
As a rule of thumb, sustained utilization above roughly 80% is a sign the
cluster may need more headroom. If one host is much hotter than the others,
enable per-host metrics and look for uneven load distribution.</p>

<p><img src="/assets/2026-04-24-the-vespa-cloud-metrics-dashboard/dashboard-resources.png" alt="Node Resources" /></p>

<p>See the docs for healthy-value tables and scaling guidance:
<a href="https://docs.vespa.ai/en/operations/monitoring.html#resources-tab">Resources tab</a>.</p>

<h2 id="whats-new-in-the-latest-revision">What’s new in the latest revision</h2>

<p>The dashboard has picked up a few improvements worth calling out.</p>

<h3 id="health-indicators-on-the-overview-tab">Health Indicators on the Overview tab</h3>

<p>The Overview tab now opens with a dedicated <strong>Health Indicators</strong> row —
five stat panels that surface stability issues in a single glance:
Core Dumps (1h), Restarts (1h), Feed Blocked, Content Cluster with Groups/Nodes Down,
and Container Nodes with Services Down.</p>

<p><img src="/assets/2026-04-24-the-vespa-cloud-metrics-dashboard/dashboard-health.png" alt="Health Indicators" /></p>

<p>Details and healthy values:
<a href="https://docs.vespa.ai/en/operations/monitoring.html#health-indicators">Health Indicators</a>.</p>

<h3 id="annotations-for-service-restart-and-core-dump">Annotations for Service restart and Core dump</h3>

<p><strong>Annotations</strong> are the vertical lines drawn across every chart when an
operational event happens — Vespa upgrades, feed blocked, data migration, reindexing,
autoscaling changes. Two annotations were added recently and they are worth flagging:</p>

<ul>
  <li><strong>Service restart</strong> — fires when a Vespa service process restarts.
Outside of planned upgrades, restarts usually mean a crash, OOM, or forced stop.</li>
  <li><strong>Core dump</strong> — fires when a process core-dumps. Should be extremely rare.</li>
</ul>

<p>When a latency anomaly lines up with one of these annotations,
you get the context for the change without having to infer it from the graph alone.
Both signals also feed the Overview’s Health Indicators row, so the same event
shows up in three places: the counter, the annotation line, and the Health tab’s
historical time series.</p>

<p><img src="/assets/2026-04-24-the-vespa-cloud-metrics-dashboard/dashboard-annotation.png" alt="Dashboard Annotation" /></p>

<p>Full annotation reference:
<a href="https://docs.vespa.ai/en/operations/monitoring.html#dashboard-annotations">Annotations</a>.</p>

<h3 id="container-thread-pool-rows-one-per-configuration-case">Container thread pool rows, one per configuration case</h3>

<p>The Resources tab used to have a single thread-pool row that was mostly empty —
a container only has the thread pools that match its <code class="language-plaintext highlighter-rouge">services.xml</code> configuration
(<code class="language-plaintext highlighter-rouge">&lt;search&gt;</code>, <code class="language-plaintext highlighter-rouge">&lt;document-api&gt;</code>, or both). The row has been split into three
case-specific rows:</p>

<ul>
  <li><strong>Thread Pools (search + document-api)</strong> for full-feature containers</li>
  <li><strong>Thread Pools (search only)</strong> for query-only containers</li>
  <li><strong>Thread Pools (document-api only)</strong> for feed-only containers</li>
</ul>

<p>Classification is automatic — hidden variables derive the cluster list per case
from Prometheus set operations, so only relevant rows render for a given deployment.
Each thread pool now gets its own panel with avg (green) and max (yellow dashed)
on the same chart.</p>

<p><img src="/assets/2026-04-24-the-vespa-cloud-metrics-dashboard/dashboard-thread-pools.png" alt="Dashboard Thread Pools" /></p>

<p>Details:
<a href="https://docs.vespa.ai/en/operations/monitoring.html#container-thread-pools">Container Thread Pools</a>.</p>

<h3 id="jvm-memory-breakdown-heap--direct--native">JVM memory breakdown (heap / direct / native)</h3>

<p>The Resources tab separates the three layers of container memory: <strong>heap</strong>,
<strong>direct</strong>, and <strong>native</strong>. This matters on container nodes that run embedders
or local LLM components — model weights are memory-mapped and partially resident,
but KV cache and compute buffers are allocated upfront as <strong>native</strong> memory.
When node memory is high but heap and direct look normal, the native layer
is usually where to look.</p>

<p><img src="/assets/2026-04-24-the-vespa-cloud-metrics-dashboard/dashboard-jvm.png" alt="Dashboard JVM" /></p>

<p>Details:
<a href="https://docs.vespa.ai/en/operations/monitoring.html#jvm-memory">JVM memory breakdown</a>.</p>

<h2 id="a-simple-workflow">A simple workflow</h2>

<p>A practical way to use the dashboard during an incident:</p>

<ol>
  <li>Open <strong>Overview</strong> and scan the Health Indicators row.</li>
  <li>Confirm the symptom (QoS drop, latency spike, error-rate increase).</li>
  <li>Use <strong>Query</strong> or <strong>Feed</strong> to find the slow layer.</li>
  <li>Use <strong>Resources</strong> to confirm whether the cluster is saturated.</li>
  <li>Cross-reference <strong>annotations</strong> for restarts, upgrades, reindexing, or migration.</li>
</ol>

<p>That flow gets from “latency is up” to “this is the actual bottleneck” much faster
than scanning every chart. The
<a href="https://docs.vespa.ai/en/operations/monitoring.html#dashboard-workflows">common workflows</a>
section of the docs has recipes for the most frequent scenarios.</p>

<h2 id="summary">Summary</h2>

<p>The Vespa Cloud metrics dashboard works best as a troubleshooting tool —
not a metrics catalog. Start with health, follow the latency path, confirm with
resources, and use annotations to connect spikes to real events. The tab reference,
healthy-value tables, and step-by-step workflows live in the
<a href="https://docs.vespa.ai/en/operations/monitoring.html#vespa-cloud-dashboard">Monitoring documentation</a>.</p>
]]></content:encoded>
        <pubDate>Fri, 24 Apr 2026 00:00:00 +0000</pubDate>
        <link>https://blog.vespa.ai/the-vespa-cloud-metrics-dashboard/</link>
        <guid isPermaLink="true">https://blog.vespa.ai/the-vespa-cloud-metrics-dashboard/</guid>
        
        <category>monitoring</category>
        
        <category>metrics</category>
        
        <category>performance</category>
        
        
      </item>
    
      <item>
        <title>Using Large ONNX Models with External Data in Vespa Embedders</title>
        <description>Many ONNX models exceed the 2GB protobuf limit and store weights in external data files. Vespa now supports these models for embedders.
</description>
        
        <media:thumbnail url="https://blog.vespa.ai/assets/2026-03-27-onnx-external-data-in-vespa-embedders/onnx-external-data-splash.png" />
        
        <content:encoded><![CDATA[<p>Many popular ONNX models exceed the 2 GB <a href="https://protobuf.dev/">protobuf</a> format limit and store their weights in separate external data files.
Until recently, these models could not be used directly in Vespa’s built-in embedders.</p>

<p>This was a long requested feature on our tracker (see <a href="https://github.com/vespa-engine/vespa/issues/28761">GitHub issue #28761</a>).</p>

<h2 id="the-2-gb-limitation">The 2 GB limitation</h2>

<p><a href="https://onnx.ai/">ONNX</a> uses Google’s Protocol Buffers as its serialization format.
Protobuf has a hard limit of 2 GB on message size.
For smaller models, this is not a problem — all tensor data (the model weights) is embedded directly in the <code class="language-plaintext highlighter-rouge">.onnx</code> file,
making it self-contained.</p>

<p>As models grow larger, they inevitably hit this limitation.
For a model exceeding 2 GB, ONNX tooling splits it into two parts:</p>

<ul>
  <li>A small <strong><code class="language-plaintext highlighter-rouge">.onnx</code> file</strong> containing the model graph structure (typically a few hundred KB to a few MB).</li>
  <li>One or more <strong>external data files</strong> (commonly named <code class="language-plaintext highlighter-rouge">.onnx_data</code>) containing the actual tensor weights.</li>
</ul>

<p>Note that reduced-precision variants of these models (INT8, FP16, etc.) are often small enough to fit in a single self-contained <code class="language-plaintext highlighter-rouge">.onnx</code> file.
The external data split primarily affects the full-precision versions.</p>

<p>Previously, if you pointed a Vespa embedder at a model with external data files, ONNX Runtime would fail to load it
because the data files were not available alongside the model file.</p>

<h2 id="what-changed">What changed</h2>

<p>Vespa embedders now automatically handle ONNX models with external data files.
When you configure an embedder with a URL pointing to an <code class="language-plaintext highlighter-rouge">.onnx</code> file,
Vespa inspects the model to check whether it references any external data files.
If it does, Vespa downloads those files automatically before loading the model.</p>

<p>This feature is available starting from Vespa 8.544.</p>

<h2 id="how-to-use-it">How to use it</h2>

<p>Here is an example using EmbeddingGemma 300M, which uses external data:</p>

<div class="language-xml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nt">&lt;container</span> <span class="na">id=</span><span class="s">"default"</span> <span class="na">version=</span><span class="s">"1.0"</span><span class="nt">&gt;</span>
  <span class="nt">&lt;component</span> <span class="na">id=</span><span class="s">"gemma"</span> <span class="na">type=</span><span class="s">"hugging-face-embedder"</span><span class="nt">&gt;</span>
    <span class="nt">&lt;transformer-model</span>
      <span class="na">url=</span><span class="s">"https://huggingface.co/onnx-community/embeddinggemma-300m-ONNX/resolve/main/onnx/model.onnx"</span><span class="nt">/&gt;</span>
    <span class="nt">&lt;tokenizer-model</span>
      <span class="na">url=</span><span class="s">"https://huggingface.co/onnx-community/embeddinggemma-300m-ONNX/resolve/main/tokenizer.json"</span><span class="nt">/&gt;</span>
    <span class="nt">&lt;max-tokens&gt;</span>2048<span class="nt">&lt;/max-tokens&gt;</span>
    <span class="nt">&lt;prepend&gt;</span>
      <span class="nt">&lt;query&gt;</span>task: search result | query: <span class="nt">&lt;/query&gt;</span>
      <span class="nt">&lt;document&gt;</span>title: none | text: <span class="nt">&lt;/document&gt;</span>
    <span class="nt">&lt;/prepend&gt;</span>
  <span class="nt">&lt;/component&gt;</span>
<span class="nt">&lt;/container&gt;</span>
</code></pre></div></div>

<p>If you are deploying to <a href="https://cloud.vespa.ai/">Vespa Cloud</a>, you can also use models from the
<a href="https://docs.vespa.ai/en/rag/model-hub.html">Vespa Model Hub</a> that use external data.
For example, the Multilingual-E5-large model (will be available on Vespa Cloud 8.668+):</p>

<div class="language-xml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nt">&lt;container</span> <span class="na">id=</span><span class="s">"default"</span> <span class="na">version=</span><span class="s">"1.0"</span><span class="nt">&gt;</span>
  <span class="nt">&lt;component</span> <span class="na">id=</span><span class="s">"e5"</span> <span class="na">type=</span><span class="s">"hugging-face-embedder"</span><span class="nt">&gt;</span>
    <span class="nt">&lt;transformer-model</span> <span class="na">model-id=</span><span class="s">"multilingual-e5-large"</span><span class="nt">/&gt;</span>
    <span class="nt">&lt;max-tokens&gt;</span>512<span class="nt">&lt;/max-tokens&gt;</span>
    <span class="nt">&lt;prepend&gt;</span>
      <span class="nt">&lt;query&gt;</span>query: <span class="nt">&lt;/query&gt;</span>
      <span class="nt">&lt;document&gt;</span>passage: <span class="nt">&lt;/document&gt;</span>
    <span class="nt">&lt;/prepend&gt;</span>
  <span class="nt">&lt;/component&gt;</span>
<span class="nt">&lt;/container&gt;</span>
</code></pre></div></div>

<p>This works with our ONNX-based embedders:</p>

<ul>
  <li><a href="https://docs.vespa.ai/en/embedding.html#huggingface-embedder"><code class="language-plaintext highlighter-rouge">hugging-face-embedder</code></a></li>
  <li><a href="https://docs.vespa.ai/en/embedding.html#colbert-embedder"><code class="language-plaintext highlighter-rouge">colbert-embedder</code></a></li>
  <li><a href="https://docs.vespa.ai/en/embedding.html#splade-embedder"><code class="language-plaintext highlighter-rouge">splade-embedder</code></a></li>
</ul>

<p>It’s also possible to use <a href="https://docs.vespa.ai/en/reference/rag/embedding.html#private-model-hub">private models</a> — authentication tokens are propagated when downloading external data files.</p>

<h2 id="current-limitations">Current limitations</h2>

<p>There are a few constraints to be aware of:</p>

<ul>
  <li>
    <p><strong>Embedders only.</strong> Models used directly in <a href="https://docs.vespa.ai/en/ranking/onnx.html">ranking expressions</a>
must still be self-contained and under 2 GB.</p>
  </li>
  <li>
    <p><strong>URL-referenced or Model Hub models only.</strong> Models bundled in the
<a href="https://docs.vespa.ai/en/application-packages.html">application package</a>
using the <code class="language-plaintext highlighter-rouge">path</code> attribute do not support external data.
Models referenced via <code class="language-plaintext highlighter-rouge">url</code> or <code class="language-plaintext highlighter-rouge">model-id</code> (Vespa Cloud) are supported.</p>
  </li>
  <li>
    <p><strong>External data files must be co-located with the model.</strong>
The external data files are resolved relative to the model URL.
They must be in the same directory (or a subdirectory) as the <code class="language-plaintext highlighter-rouge">.onnx</code> file.</p>
  </li>
</ul>

<p>See the <a href="https://docs.vespa.ai/en/ranking/onnx.html#limitations-on-model-size-and-complexity">ONNX model documentation</a>
for the full list of requirements.</p>

<p>If you need more extensive support for ONNX models with external data — for example in ranking expressions —
feel free to <a href="https://github.com/vespa-engine/vespa/issues">file an issue</a>.</p>
]]></content:encoded>
        <pubDate>Fri, 27 Mar 2026 00:00:00 +0000</pubDate>
        <link>https://blog.vespa.ai/onnx-external-data-in-vespa-embedders/</link>
        <guid isPermaLink="true">https://blog.vespa.ai/onnx-external-data-in-vespa-embedders/</guid>
        
        <category>embedding</category>
        
        <category>onnx</category>
        
        
      </item>
    
  </channel>
</rss>
