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.
In this repository7 other files it gives its agents.
- AGENTS.mdAGENTS.md
- app-packageSkill
- feed-operationsSkill
- pyvespaSkill
- query-builderSkill
- schema-authoringSkill
- vespa-cliSkill
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
- Distinctive: line 45
Treat each phase as a checkpoint.
- Very specific: line 134
Test code that does feed → refresh → query against ES can drop the refresh step in Vespa.
- Very specific: line 156
Default workflow: if you know the page, fetch its .html.md form via WebFetch.
- Distinctive: line 172
When the user kicks off a migration, the typical first turn looks like:
- Distinctive: line 174
Ask for (or fetch) the ES mapping JSON for the index in question.
≈ “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.
Files
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.sdschemas, indexing pipelines, field typesapp-package— services.xml, deployment.xml, embedders, app layoutquery-builder— YQL, rank profiles, grouping, ML model integrationfeed-operations— bulk feeding, partial updates, conditional writesvespa-cli— deploy, target config, auth, CI/CDpyvespa— 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).
| Phase | Goal | Tool / Skill |
| 1. Export | Pull ES mapping + documents | mapping: 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. Convert | Translate ES mapping → Vespa schema (.sd) + sample app package | ES_Vespa_parser.py (from Vespa sample-apps) as a starting point; manual review required → see schema-authoring |
| 3. Generate app package | Wire schemas into a deployable application package | app-package skill |
| 4. Deploy | Push the app package to Vespa (Cloud or self-hosted) | vespa-cli skill |
| 5. Feed + verify | Load docs into the new app, validate queries and ranking | vespa 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)
| Elasticsearch | Vespa |
| index | schema (.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 / _index | the schema name (which is also the document-type segment in the id) |
| Query DSL (JSON) | YQL: select … from <schema> where … |
| analyzers / tokenizers / filters | match modes (text, exact, word, gram) + linguistics config |
keyword field | string field with indexing: attribute + match: word (or exact) |
text field | string field with indexing: index (+ summary if returned) |
| numeric / date / boolean | int, long, float, double, byte, bool, string (ISO date) — all need attribute to filter/sort/rank on |
nested field | array<struct> in schema with explicit struct field declarations |
object field | flatten to top-level fields or model as a struct |
| aliases | document 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 API | redeploy schema + vespa visit to refeed (or use Vespa's reindexing) |
function_score / Painless scoring | rank profile with first-phase / second-phase / global-phase expressions |
| dense_vector + kNN | tensor<float>(x[N]) field + nearestNeighbor operator with HNSW index |
| sparse_vector / ELSER | sparse 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:
| Directive | Enables |
index | Tokenized inverted index → text search, userQuery(), BM25, weakAnd |
attribute | Column store → filtering, sorting, grouping, ranking features, fast match |
summary | Field 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
textfield → Vespastringwithindexing: index | summary - ES
keywordfield used for filters/aggregations → Vespastringwithindexing: attribute | summaryandmatch: word(orexactfor 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
_scoreis BM25 over the matched terms intextfields, with optionalfunction_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 itsfirst-phaseexpression, 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:
- 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.
- Rewrite custom scoring as rank profiles. Painless scripts → ranking expressions.
function_scoremodifiers → second-phase or global-phase math over query and document features. Thequery-builderskill has the full rank-profile reference. - 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 ornativeRank) and an expressivesecond-phase(cross-encoder, LightGBM, learned-to-rank).
Gotchas
ES_Vespa_parser.pyis a starter, not a finished tool. It produces a valid skeleton but does not infer good indexing directives, does not modelnestedorobjectfields well, and never produces a useful rank profile. Expect to hand-edit every generated.sdfile. Seedocs/parser-and-tooling.md.- Document IDs are hierarchical, not flat.
_id: "abc123"becomesid: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. keywordvstextmapping is not symmetric. ESkeywordfields are case-sensitive exact-match by default; in Vespa, get this withmatch: wordon anattribute-backed string. EStextfields are tokenized and lowercased — Vespa'sindexing: indexdoes 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.- Nested and object fields need explicit modeling. Vespa has no implicit
nestedanalog; usearray<struct>for repeated nested objects and reference subfields withfield.subfieldsyntax in YQL. Some users prefer to split nested data into a separate parent/child schema withreferencedjoins. - Ranking is not portable. Translating an ES query is mechanical; translating its scoring is not. Budget time for it explicitly.
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 whatvespa feedand the Logstash output both wrap). The benchmark numbers (blog.vespa.ai, Nov 2024) are predictable, not random:Workload Winner Margin (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 workload Vespa ~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-connectionon the feed client — and thefeed-operationsskill.- Vespa is real-time; Elasticsearch is near-real-time. This is the biggest behavioral change most migrators don't anticipate. When
vespa feedreceives 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 → queryagainst 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_intervalis a common optimization; in Vespa there is no equivalent knob — visibility is always immediate.
- Test code that does
- 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. - 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'scurrent_timeor 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)
| Resource | URL | When to use |
| llms.txt index | https://docs.vespa.ai/llms.txt | Navigate to a specific page when you don't know its URL |
| llms-full.txt | https://docs.vespa.ai/llms-full.txt | Bulk-load when the user wants a broad survey (large — token-heavy) |
| Per-page markdown | Any …/page.html URL → append .md | First 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:
- Source markdown on GitHub. Elasticsearch reference docs live in
https://github.com/elastic/elasticsearch/tree/main/docs/reference— fetch raw markdown viahttps://raw.githubusercontent.com/elastic/elasticsearch/main/docs/reference/<path>. This is the authoritative source that the published HTML is built from. - WebFetch on
www.elastic.co/guide/en/elasticsearch/reference/current/…. Returns HTML; the model has to interpret it but the content is current. - Probe with
llms-txt-supportfirst 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:
- Ask for (or fetch) the ES mapping JSON for the index in question.
- Walk through the mapping field-by-field, producing a
.sdschema — defer toschema-authoringfor syntax. - Generate a minimal
services.xml+app-packagelayout — defer toapp-package. - Feed a small sample of documents into the new app package and confirm
select * from <schema> where truereturns them — defer tofeed-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. - Pick one representative query from their current ES workload and translate it to YQL — defer to
query-translation.mdandquery-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), ordocs/fetching-docs.md(LLM-friendly doc-fetching patterns) from this skill's directory as needed.Related skills:
schema-authoring(for the.sdyou'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).
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
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.

