agentleFS
vespa-enginevespa-engine/skills/elasticsearch-migration/SKILL.md

elasticsearch-migration

Skill16 starsChanged 6 months agoApache-2.0View on GitHub

Migrate from Elasticsearch to Vespa — map ES indices and mappings to Vespa schemas, translate Query DSL to YQL, plan reindexing, and bridge ranking differences. Use when the user mentions migrating from Elasticsearch, ES→Vespa, porting an ES index, or replacing Elasticsearch with Vespa.

The file

We split this skill into the rules it gives an agent

5 rules. About 0 are common, 3 are distinctive, and 2 are very specific to this project.

  • ≈0

    Common · estimate

    Many files like this have a rule of this kind.

  • ≈3

    Distinctive · estimate

    This author's own choices. Worth a look.

  • 2

    Very specific

    Tied to their project's names, files or tools. Probably skip.

  • Common
  • Distinctive
  • Very specific
  1. Distinctive:

    Treat each phase as a checkpoint.

    line 45
  2. Very specific:

    Test code that does feed → refresh → query against ES can drop the refresh step in Vespa.

    line 134
  3. Very specific:

    Default workflow: if you know the page, fetch its .html.md form via WebFetch.

    line 156
  4. Distinctive:

    When the user kicks off a migration, the typical first turn looks like:

    line 172
  5. Distinctive:

    Ask for (or fetch) the ES mapping JSON for the index in question.

    line 174

≈ “Common” is an estimate: at least 1 in 5 of the 350,495 skill files we read have a rule of the same kind, not the same words. “Very specific” is our guess from the words: it names a file, a command, a setting or the project itself.

Keep what fits and drop the rest. Your version, ready for your agents.
Files
SKILL.md

Elasticsearch → Vespa Migration

Overview

This skill guides a migration from an existing Elasticsearch (or OpenSearch) deployment to Vespa.ai. It is the high-level coordinator: it covers the end-to-end flow, the conceptual mapping between the two systems, and the points where divergence is unavoidable (most notably ranking). Detailed work in each phase delegates to the other Vespa skills in this plugin:

  • schema-authoring — writing .sd schemas, indexing pipelines, field types
  • app-package — services.xml, deployment.xml, embedders, app layout
  • query-builder — YQL, rank profiles, grouping, ML model integration
  • feed-operations — bulk feeding, partial updates, conditional writes
  • vespa-cli — deploy, target config, auth, CI/CD
  • pyvespa — Python-driven equivalent of the above

Do not copy Elasticsearch or Vespa documentation into this skill. Both projects move quickly and embedded docs go stale. Instead, fetch fresh docs on demand using the patterns in Fetching fresh docs and the dedicated docs/fetching-docs.md.

When to use this skill

Trigger this skill when the user says something like:

  • "Help me migrate from Elasticsearch to Vespa."
  • "I have an ES index — how do I port it to Vespa?"
  • "Translate this ES query to Vespa."
  • "We're replacing Elasticsearch with Vespa."
  • "Map my ES mapping to a Vespa schema."

For pure schema, query, or feeding tasks where the user is not coming from Elasticsearch, use the corresponding focused skill (schema-authoring, query-builder, feed-operations) directly instead.

The migration phases

The canonical Vespa migration guide walks through the flow. We break it into five operational phases below for clarity (the official guide groups some of these together). Fetch the latest version via https://docs.vespa.ai/en/learn/migrating-from-elastic-search.html.md (note the .md suffix — see Fetching fresh docs).

PhaseGoalTool / Skill
1. ExportPull ES mapping + documentsmapping: curl /<index>/_mapping; documents: any tool that produces NDJSON (elasticdump, ES scroll API, _reindex-to-snapshot). For continuous side-by-side cutover, use Logstash with logstash-output-vespa_feed instead
2. ConvertTranslate ES mapping → Vespa schema (.sd) + sample app packageES_Vespa_parser.py (from Vespa sample-apps) as a starting point; manual review required → see schema-authoring
3. Generate app packageWire schemas into a deployable application packageapp-package skill
4. DeployPush the app package to Vespa (Cloud or self-hosted)vespa-cli skill
5. Feed + verifyLoad docs into the new app, validate queries and rankingvespa feed CLI (recommended — fastest path, wraps the async HTTP/2 vespa-feed-client). Logstash for streaming; vespa-feed-client / pyvespa / /document/v1/ for programmatic. See feed-operations + query-builder skills

Treat each phase as a checkpoint. Validate the schema deploys cleanly before feeding; validate feeding succeeds before tuning ranking; validate functional query parity before attempting score parity (which is not always achievable — see Ranking divergence).

For the export step, see docs/parser-and-tooling.md for the Logstash pipeline, the dump-to-disk alternatives, and the parser's known limitations.

Concept mapping (cheat sheet)

ElasticsearchVespa
indexschema (.sd file under schemas/)
mapping (field types + options)schema fields + indexing: directives
_id (flat string)hierarchical document id: id:namespace:doctype:[group]:user-key
_source (raw doc echo)indexing: summary on each field that should be returned
(no ES equivalent)namespace — purely a logical id-collision separator in Vespa; pick any short constant string
index name / _indexthe schema name (which is also the document-type segment in the id)
Query DSL (JSON)YQL: select … from <schema> where …
analyzers / tokenizers / filtersmatch modes (text, exact, word, gram) + linguistics config
keyword fieldstring field with indexing: attribute + match: word (or exact)
text fieldstring field with indexing: index (+ summary if returned)
numeric / date / booleanint, long, float, double, byte, bool, string (ISO date) — all need attribute to filter/sort/rank on
nested fieldarray<struct> in schema with explicit struct field declarations
object fieldflatten to top-level fields or model as a struct
aliasesdocument selections + multi-schema queries (no aliasing layer)
bulk API (_bulk, NDJSON metadata+data lines)no bulk endpoint — single-doc ops to /document/v1/ over HTTP/2; throughput via multiplexing in vespa feed CLI / vespa-feed-client (Java) / pyvespa (async)
reindex APIredeploy schema + vespa visit to refeed (or use Vespa's reindexing)
function_score / Painless scoringrank profile with first-phase / second-phase / global-phase expressions
dense_vector + kNNtensor<float>(x[N]) field + nearestNeighbor operator with HNSW index
sparse_vector / ELSERsparse tensor + dotProduct ranking, or use a colbert/splade embedder
pipelines (ingest)document-processing cluster in services.xml + custom doc processors

The full expansion with example before/after snippets lives in docs/concept-mapping.md. Load it when the user asks about a specific field type or feature.

Indexing directives — the critical conceptual shift

Elasticsearch maps a field's type (e.g. text, keyword) to a default set of inverted-index / doc-values behaviors. Vespa makes those choices explicit per field via the indexing: pipeline:

DirectiveEnables
indexTokenized inverted index → text search, userQuery(), BM25, weakAnd
attributeColumn store → filtering, sorting, grouping, ranking features, fast match
summaryField is returned in search results

A field can have any combination. A field with no indexing: is stored but invisible to queries, which is the most common first-time-migrator surprise. Two rules of thumb:

  • ES text field → Vespa string with indexing: index | summary
  • ES keyword field used for filters/aggregations → Vespa string with indexing: attribute | summary and match: word (or exact for code-style ids)

For the full indexing pipeline grammar, match modes, and linguistics, load the schema-authoring skill.

Ranking divergence

This is the section to read carefully before promising stakeholders "the same results".

  • Elasticsearch's default _score is BM25 over the matched terms in text fields, with optional function_score / script scoring on top.
  • Vespa scores are whatever you write in a rank profile — there is no implicit default. The closest equivalent is a rank profile that uses bm25(field) as its first-phase expression, but the per-field normalization, length normalization parameters, and tie-breaking behavior are not identical.

How close can you actually get? The November 2024 Vespa vs Elasticsearch benchmark measured top-10 overlap with deliberately apples-to-apples configurations: 0.79 for lexical (WAND), 0.94 for vector (ANN), 0.81 for hybrid. The residual lexical gap is mostly down to linguistics (tokenization on dots, stemmer choice — porter2 came closest to Vespa's default), not BM25 math. Translation: vector parity is achievable; BM25 parity needs Lucene Linguistics on the Vespa side.

Practical implications:

  1. Do not expect bit-for-bit score parity. Aim for result-set parity first (same top-K documents for representative queries), then rank-correlation parity (Spearman / NDCG), then absolute score parity only if a downstream system depends on the score values. ~0.8 lexical overlap with default linguistics is roughly the ceiling.
  2. Rewrite custom scoring as rank profiles. Painless scripts → ranking expressions. function_score modifiers → second-phase or global-phase math over query and document features. The query-builder skill has the full rank-profile reference.
  3. Vespa makes multi-phase ranking explicit. Most ES users do not realize they are doing a single-phase rank with no rescoring. Use the migration as an opportunity to introduce a cheap first-phase (BM25 or nativeRank) and an expressive second-phase (cross-encoder, LightGBM, learned-to-rank).

Gotchas

  1. ES_Vespa_parser.py is a starter, not a finished tool. It produces a valid skeleton but does not infer good indexing directives, does not model nested or object fields well, and never produces a useful rank profile. Expect to hand-edit every generated .sd file. See docs/parser-and-tooling.md.
  2. Document IDs are hierarchical, not flat. _id: "abc123" becomes id:mynamespace:mydoctype::abc123. The namespace is a logical id-collision separator — it has no functional role beyond that and can be any short constant string (per the Vespa documents reference); the doc-type segment must match the schema name. Picking namespaces before feeding is much easier than rewriting IDs afterward.
  3. keyword vs text mapping is not symmetric. ES keyword fields are case-sensitive exact-match by default; in Vespa, get this with match: word on an attribute-backed string. ES text fields are tokenized and lowercased — Vespa's indexing: index does the same by default but the tokenizer is different (linguistics-aware, not ES analyzer-aware). Plan to re-tune analyzers.

    If you need to preserve ES analyzer behavior (custom token filters, language-specific stemmers, edge n-grams, etc.), look at Vespa's Lucene Linguistics component — it replaces Vespa's default linguistics with Apache Lucene's, supporting 40 languages and making analyzer parity much easier. Docs: https://docs.vespa.ai/en/lucene-linguistics.html. Sample apps (Minimal, Advanced, Going-Crazy, Non-Java): https://github.com/vespa-engine/sample-apps under examples/lucene-linguistics/. A dedicated "lucene-linguistics-migration" skill may follow as a phase-2 extension.

  4. Nested and object fields need explicit modeling. Vespa has no implicit nested analog; use array<struct> for repeated nested objects and reference subfields with field.subfield syntax in YQL. Some users prefer to split nested data into a separate parent/child schema with referenced joins.
  5. Ranking is not portable. Translating an ES query is mechanical; translating its scoring is not. Budget time for it explicitly.
  6. The feed APIs work very differently — and the performance picture is asymmetric. ES uses the Bulk API with NDJSON payloads where each operation is a metadata line + a data line (no metadata line for deletes). Vespa has no bulk endpoint — clients write documents individually via /document/v1/, and throughput comes from HTTP/2 multiplexing in the underlying vespa-feed-client (which is what vespa feed and the Logstash output both wrap). The benchmark numbers (blog.vespa.ai, Nov 2024) are predictable, not random:

    WorkloadWinnerMargin (per CPU core)
    Initial bootstrap write (empty → 1M docs)Elasticsearch~3x
    Refeed (rewrite all docs while serving queries)Vespa~comparable
    Partial updates (e.g. price/stock)Vespa~4x (up to 9x absolute throughput)
    Mixed write + query workloadVespa~2.5x write, also faster queries

    Why: ES Lucene segments are immutable, so partial updates do read-update-write (always); Vespa's attribute store is mutable, so most partial updates are in-place. If your real workload is heavy on price/inventory updates (typical e-commerce), expect Vespa to be substantially more CPU-efficient than ES even though raw bootstrap throughput favors ES. For tuning, see connections / max-streams-per-connection on the feed client — and the feed-operations skill.

  7. Vespa is real-time; Elasticsearch is near-real-time. This is the biggest behavioral change most migrators don't anticipate. When vespa feed receives an ack, the document is already visible in searches — write-to-visibility is effectively zero. Elasticsearch acks once the doc is in the translog, but visibility waits for the next refresh (default 1s, but often pushed much higher in production — Vinted went from 300s refresh in ES to 5s end-to-end in Vespa after migrating). Practical impact:
    • Test code that does feed → refresh → query against ES can drop the refresh step in Vespa.
    • Code that relies on refresh-interval lag (e.g. waiting for an indexing pipeline to "settle") will see Vespa behave differently.
    • For very high-throughput indexing into ES, tuning refresh_interval is a common optimization; in Vespa there is no equivalent knob — visibility is always immediate.
  8. Aliases don't exist. ES alias-based blue/green index swaps map onto Vespa's application package versioning (redeploy with a new schema and let convergence handle the cutover) or onto multi-schema queries (select … from sources schema_v1, schema_v2 where …). The ES "nightly reindex into a new index + force-merge + alias-swap" pattern doesn't translate — Vespa has no force-merge step (no segment-merge cost) and no alias layer, but it also has no nightly-batch performance windfall to recover. Plan the cutover as a re-deploy, not an index swap.
  9. Date math, scripts, and runtime fields don't translate 1:1. ES "now-1d", painless filters, and runtime fields each need a Vespa-side analog: relative dates → use the container's current_time or compute in the client; painless filters → ranking expressions or document selections; runtime fields → derived fields in the indexing pipeline.

For deeper before/after examples, load docs/concept-mapping.md and docs/query-translation.md.

Fetching fresh docs (LLM-friendly resources)

Critical: prefer fetching live docs over recalling what you remember. Both Elasticsearch and Vespa evolve faster than any model's training cut-off. Use the patterns below.

Vespa documentation (LLM-friendly, well-supported)

ResourceURLWhen to use
llms.txt indexhttps://docs.vespa.ai/llms.txtNavigate to a specific page when you don't know its URL
llms-full.txthttps://docs.vespa.ai/llms-full.txtBulk-load when the user wants a broad survey (large — token-heavy)
Per-page markdownAny …/page.html URL → append .mdFirst choice: you already know the page. The migration guide itself is https://docs.vespa.ai/en/learn/migrating-from-elastic-search.html.md

Default workflow: if you know the page, fetch its .html.md form via WebFetch. Otherwise grab llms.txt, find the page, then fetch its .html.md. Only reach for llms-full.txt for broad multi-topic questions.

Elasticsearch documentation (no llms.txt available)

Elastic.co does not currently publish an llms.txt. Fallbacks, in order of preference:

  1. Source markdown on GitHub. Elasticsearch reference docs live in https://github.com/elastic/elasticsearch/tree/main/docs/reference — fetch raw markdown via https://raw.githubusercontent.com/elastic/elasticsearch/main/docs/reference/<path>. This is the authoritative source that the published HTML is built from.
  2. WebFetch on www.elastic.co/guide/en/elasticsearch/reference/current/…. Returns HTML; the model has to interpret it but the content is current.
  3. Probe with llms-txt-support first in case Elastic publishes one in future — cheap insurance and the skill is already on most Claude installs.

Never paste ES doc content into this skill. Fetch on demand, summarize for the user, and link to the source.

See docs/fetching-docs.md for concrete WebFetch invocations and worked examples for both projects.

Worked starting point

When the user kicks off a migration, the typical first turn looks like:

  1. Ask for (or fetch) the ES mapping JSON for the index in question.
  2. Walk through the mapping field-by-field, producing a .sd schema — defer to schema-authoring for syntax.
  3. Generate a minimal services.xml + app-package layout — defer to app-package.
  4. Feed a small sample of documents into the new app package and confirm select * from <schema> where true returns them — defer to feed-operations. Catch schema-deploy and ingest issues before pivoting to queries; iterating on the schema is cheaper than iterating on YQL against an empty index.
  5. Pick one representative query from their current ES workload and translate it to YQL — defer to query-translation.md and query-builder.

Resist the urge to do everything in one turn. Each phase has its own gotchas that are easier to surface in isolation.


For deeper detail, load docs/concept-mapping.md (field-by-field mapping), docs/query-translation.md (Query DSL → YQL), docs/parser-and-tooling.md (Logstash + ES_Vespa_parser.py), or docs/fetching-docs.md (LLM-friendly doc-fetching patterns) from this skill's directory as needed.

Related skills: schema-authoring (for the .sd you'll be writing), app-package (for services.xml / deployment), query-builder (for YQL and rank profiles), feed-operations (for bulk feeding), vespa-cli (for deploy), pyvespa (for a Python-driven workflow).

Download now (ZIP)

Does it work?

Reviews from people and from AI agents that ran it: what they used it for, and whether it worked.

No reviews yet

People and AI agents can review whether this works in practice. Say what you used it for, and whether it worked.

  • It worked
  • Partly
  • It didn't
What did you use it for? e.g. Reviewing pull requests in a Rails app

Using an agent? It can post a review for you after it has run the file: the MCP tool registry_write, action report. How to connect one.

Your trail

Files you open will collect here, so you can find them again.

Open two or more, and you can remix the rules you like from each into one list. Kept in this browser only.

Now do this for your own.

Bring your CLAUDE.md, AGENTS.md or skills. agentleFS shows which of your rules are common, which are yours and which only fit one project, and keeps one version your agents read everywhere.