Session resources

Patterns from Shipped Migrations

Practical patterns from shipped migrations to Vespa, covering schema design, query logic, ranking, validation, and migration automation.
Ravindra Harige·London Information Retrieval & AI Meetup Group·9 September 2026·London, United Kingdom

Resources

Watch the recording

Written version

Search systems rarely stay simple for long. A field mapping is added for one use case. A query branch handles a special customer rule. A score adjustment fixes one ranking problem. Years later, the behavior of the system is spread across mappings, analyzers, query builders, ranking code, and application logic.

That is the system we inherit when we migrate from Elasticsearch or OpenSearch to Vespa.

This written version of my talk covers the patterns that have been useful in real migration work. It focuses on schema design, query logic, ranking, validation, and the repetitive work that can be automated.

Where these patterns come from

I have worked on production search systems for more than a decade. At Searchplex, we have worked on migrations into Vespa from Elasticsearch, OpenSearch, and Qdrant. The work has covered e-commerce and regulated search systems.

The details differ between projects, but the same problems appear often:

  • the source schema does not fully describe how fields are used;
  • query builders contain product logic as well as search logic;
  • ranking rules mix relevance signals with business policy;
  • old behavior has to be understood before anyone can decide whether to keep it;
  • a technically valid Vespa application still needs behavioral validation.

This is why a migration cannot be reduced to a table of source and target field types.

Search behavior is spread across the system

By the time a search application is mature, its behavior usually lives in several places.

The mapping defines field types, indexing flags, multi-fields, and nested structures. Analysis settings define tokenizers, analyzers, synonyms, normalization, and language rules. The query builder adds filters, rewrites, boosts, and special branches. Ranking may depend on function_score, Painless scripts, freshness, commercial signals, or a separate reranking service.

Application code may apply permissions, availability rules, channel-specific behavior, and post-processing. Sorting, aggregations, facets, grouping, and pagination affect what users see too.

Most of this did not appear at once. It accumulated one change at a time. Many of those changes were reasonable when they were made. The problem is that their purpose is no longer cleanly separated from their Elasticsearch implementation.

Some behavior is essential. Some is no longer needed. Some remains because nobody wants to remove it without knowing what will break.

Treat the old system as evidence

The source system contains valuable evidence, but it should not define the target design.

Consider a few examples:

Source constructQuestion to recover
keyword field used in an aggregationDoes the application need exact filtering, grouping, sorting, or all three?
nested queryMust several conditions match the same child object?
function_score or PainlessWhich signals should change ranking, and under which conditions?
custom analyzerWhich input forms must normalize and match in the same way?

These questions describe required behavior. They do not prescribe a Vespa feature.

That distinction matters. If a requirement still says that Vespa needs an equivalent of nested, function_score, or minimum_should_match, we may still be describing the old implementation rather than the actual need.

Recover the query model before translating queries

Query migration becomes difficult when the query builder also acts as the product rules engine.

An early search implementation may have a clear flow:

product requirement
    ↓
search behavior
    ↓
Elasticsearch query

After a few years, the middle of that flow may be a set of conditions that add bool.should clauses, wrap parts of the query in function_score, add nested filters, or change boosts for special cases.

At that point, branch-for-branch translation is risky. We first need to understand why each branch exists.

One approach that has worked well is to separate the application query model from Elasticsearch before introducing Vespa.

The first step keeps Elasticsearch in place:

application search logic
    ↓
application query model
    ↓
Elasticsearch adapter
    ↓
Elasticsearch query

We then verify that the refactored path still produces the expected Elasticsearch query and behavior. This isolates the refactor from the engine migration.

The second step adds a Vespa adapter over the same application query model:

application query model
    ├── Elasticsearch adapter
    └── Vespa adapter

The application query model does not need to become a universal search language. It only needs to describe what that application asks search to do.

This gives us two smaller changes instead of one large change. First, decouple the application from Elasticsearch syntax. Then express the same application intent with Vespa constructs.

Separate matching, ranking, and product rules

An Elasticsearch query tree can mix several concerns in one structure. A bool query might contain availability and permission filters, a lexical query, an exact-title boost, a phrase boost, and a function_score block for freshness and commercial signals.

Before migrating it, I find it useful to separate those parts:

Matching decides which documents are eligible. This includes permissions, availability, and the candidate condition.

Ranking changes the order of eligible documents. Exact-title matches, phrase matches, and freshness often belong here.

Product rules express commercial policy, channel rules, and exceptional cases. They may influence ranking, but they deserve a clear name and owner.

Once these concerns are separate, Vespa can use its own query and ranking model. We no longer need to preserve the shape of the Elasticsearch query tree.

Field usage matters more than the source type

The same principle applies to schemas.

Suppose the Elasticsearch mapping declares:

tags: keyword

The mapping alone does not tell us everything we need. Sample documents may show that tags contains arrays. Representative queries may show exact filters and a terms aggregation.

Together, that evidence gives us the actual requirement:

  • multiple values;
  • exact matching;
  • filtering;
  • grouping by value.

A reasonable Vespa plan may then be:

field tags type array<string> {
    indexing: attribute | summary
    match: word
}

The source and target representations differ, but they serve the same required behavior. I classify this as an adaptation rather than a direct mapping.

Mappings tell us what the source declared. Documents show the values that fields actually hold. Queries show how the application uses those fields. A useful migration plan draws on all three.

Nested data is about matching semantics

Nested fields are a good example of why field-type conversion is too narrow.

Consider this document:

{
  "variants": [
    { "color": "red",  "size": "S" },
    { "color": "blue", "size": "L" }
  ]
}

Now consider a query for color:red AND size:L.

Should the document match?

If the answer is no, the requirement is that both predicates must match the same variant. That requirement is much clearer than saying that we need a Vespa equivalent of Elasticsearch nested.

The required matching behavior should drive the Vespa data model and query design.

Four useful migration decisions

For each source construct, we use one of four decisions.

Direct

The same job has a straightforward Vespa representation. An integer used for range filtering is a simple example.

Adapt

The required behavior stays the same, but the representation changes. A multi-value exact field used for grouping is one example.

Review

The supplied evidence is not enough to choose safely. An unusual combination of analyzer settings and query use may need a person to confirm the plan.

Redesign

The old implementation should not be reproduced mechanically. Custom score code that mixes ranking with product policy usually needs an explicit Vespa ranking design.

These decisions depend on required behavior, not on the name of the source construct. Direct conversion is only one possible outcome.

Build a working baseline before optimizing

The first Vespa application should provide a working baseline that can be tested. It does not need to be the final production design.

We first check the behavior that matters: matching, filters, ranking intent, and product rules. Once those checks pass, we can optimize how Vespa delivers that behavior.

That may involve changing query execution, ranking phases, candidate limits, topology, resources, concurrency, or update handling. Those are target-system decisions. They should be made with Vespa in mind rather than copied from the Elasticsearch deployment.

After each meaningful change, run the acceptance checks again.

Vector migration also needs a behavioral baseline.

Keep the embeddings and similarity measure fixed for the initial comparison. Use exact nearest-neighbor search to produce a reference result set. Then compare approximate-nearest-neighbor configurations against that reference.

The useful question is not whether both systems support a vector field. The useful questions are:

  • how much recall each approximate configuration loses;
  • the latency it achieves;
  • its memory use;
  • the cost of indexing and serving;
  • what changes when vectors are quantized.

Quantization can reduce memory and cost, but it can also change recall. The right operating point depends on the workload. When exact search is too expensive across the full corpus, use a reproducible representative test set.

Automate the inventory, not the decisions

The first migration pass is repetitive enough to automate: inspect mappings, settings, documents, and queries; recover how fields are used; and turn that evidence into a traceable plan and conservative starter application. That is the role of migrate2vespa, with its installation, supported constructs, output, and current limitations documented on the dedicated tool page. Production readiness still requires deliberate Vespa-native design across schemas, queries, ranking, linguistic and vector behavior, feeding, operations, validation, and cutover.

What to remember

Three ideas have held up across migration work.

First, describe the required behavior before choosing Vespa constructs.

Second, use the old system as evidence and as a comparison point. Do not let its implementation define the target design.

Third, automate the repetitive inventory work while keeping engineering decisions visible.

A working Vespa baseline is an executable migration hypothesis. The next step is to test it against the behavior that users and the application depend on.

Next steps

Planning an Elasticsearch or OpenSearch migration to Vespa?

Searchplex helps teams assess the workload, design the target architecture, and deliver a staged production migration.

See also

Panel discussion

Building Search for the Modern Age

A Vespa.ai Live panel on conversational, multimodal, and agentic search, covering personalization, evaluation, hybrid retrieval, and modern search architecture.
Conference talk

The Three-Body Problem of Inverse Hybrid Search

A practical case study of migrating Elasticsearch Percolator to Vespa to power image-search alerts, combining vector similarity, boolean filters, and fetch-all retrieval at production scale.