
Elasticsearch Onboarding
- 2 installs
- 31 repo stars
- Updated May 28, 2026
- elastic/cursor-plugins
elasticsearch-onboarding skill documents Help developers new to Elasticsearch get from zero to a working search experience.
About
elasticsearch-onboarding skill documents Help developers new to Elasticsearch get from zero to a working search experience. Guide them through understanding their intent, mapping their data, and building a search experience with best practices baked in. Use this when developers are new to Elasticsearch and need help getting started with th. name: elasticsearch-onboarding description: >
- Help developers new to Elasticsearch get from zero to a working search experience.
- Platform-specific setup patterns for elasticsearch-onboarding.
- Evidence-backed steps from upstream SKILL.md.
- When-to-use criteria for elasticsearch-onboarding versus alternatives.
Elasticsearch Onboarding by the numbers
- 2 all-time installs (skills.sh)
- Ranked #1,786 of 2,203 Security skills by installs in the Skillselion catalog
- Data as of Jul 24, 2026 (Skillselion catalog sync)
elasticsearch-onboarding capabilities & compatibility
- Capabilities
- elasticsearch onboarding quick start · elasticsearch onboarding when to use guidance · elasticsearch onboarding integration patterns
- Works with
- elasticsearch
- Use cases
- security audit
What elasticsearch-onboarding says it does
Help developers new to Elasticsearch get from zero to a working search experience.
Guide them through understanding their intent, mapping their data, and building
npx skills add https://github.com/elastic/cursor-plugins --skill elasticsearch-onboardingAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 2 |
|---|---|
| repo stars | ★ 31 |
| Last updated | May 28, 2026 |
| Repository | elastic/cursor-plugins ↗ |
How do I use elasticsearch-onboarding correctly?
Help developers new to Elasticsearch get from zero to a working search experience. Guide them through understanding their intent, mapping their data, and building a search experience with best practic
Who is it for?
Teams implementing elasticsearch-onboarding workflows from the catalog.
Skip if: Skip when requirements clearly match a different specialized stack.
When should I use this skill?
User asks about elasticsearch-onboarding, help developers new to elasticsearch get from zero to a working search experience. guide t.
What you get
Working elasticsearch-onboarding setup with validated configuration and next steps.
Files
Elastic Developer Guide
You are an Elasticsearch solutions architect working alongside the developer. Your job is to guide developers from "I want search" to a working search experience — understanding their intent, recommending the right approach, and generating tested, production-ready code. Use the conversation playbook in references/elasticsearch-onboarding-playbook.md to structure the conversation. Always ask one question at a time, listen for signals, and adapt your recommendations to their specific use case and data shape.
Examples
Example user intents that should trigger this skill:
- "I want to build a search experience for my e-commerce site"
- "How do I get started with Elasticsearch?"
- "What are the best practices for building a search experience?"
- "Can you help me understand how to model my data for search?"
- "How do I build a vector database?"
Guidelines
- Ask one question at a time, then wait.
- Only generate code once the user confirms the approach and the mapping.
- Use the Synonyms API for synonym management, not a custom-built solution.
- Always use a versioned index name + alias (e.g.
products_v1+products_current) and explain why. - Explain decisions briefly, assume the user does not understand Elasticsearch yet.
- Always go through the mapping walkthrough — it's the most expensive thing to change later.
- Ask what programming language the user wants to use, don't assume.
- Avoid generating code with deprecated APIs. If you must use a deprecated API for some reason, explain why and warn
about future compatibility issues.
Catalog / E-Commerce Search Guide
Guide developers through building product catalog and e-commerce search with Elasticsearch. Use this guide when they need product search with filtering, faceting, autocomplete, boosting by attributes, and shopping-oriented relevance.
1. When to Use This Guide
Apply this guide when the developer signals:
- Product search — search across a product catalog with titles, descriptions, categories
- Faceted navigation — filter by brand, category, price range, rating, with counts
- Autocomplete / typeahead — suggest products as the user types
- "Did you mean" — spelling correction and suggestions
- Merchandising / boosting — promote certain products (new arrivals, on sale, high margin)
- Multi-attribute filtering — size, color, availability, shipping options
Do not use this guide when: the developer only needs document search without structured attributes — point them to keyword or hybrid search. If they need meaning-based "find similar products," combine this with the vector-hybrid-search guide.
2. Index Mapping
E-commerce indices need text fields for search, keyword fields for filtering/faceting, numeric fields for sorting/range filters, and nested fields for variants.
PUT /products
{
"settings": {
"analysis": {
"analyzer": {
"autocomplete_analyzer": {
"type": "custom",
"tokenizer": "standard",
"filter": ["lowercase", "autocomplete_filter"]
},
"synonym_analyzer": {
"type": "custom",
"tokenizer": "standard",
"filter": ["lowercase", "product_synonyms"]
}
},
"filter": {
"autocomplete_filter": {
"type": "edge_ngram",
"min_gram": 2,
"max_gram": 15
},
"product_synonyms": {
"type": "synonym",
"synonyms": [
"laptop, notebook => laptop",
"phone, mobile, cell phone => phone",
"tv, television => tv",
"headphones, earphones, earbuds => headphones"
]
}
}
}
},
"mappings": {
"properties": {
"title": {
"type": "text",
"analyzer": "synonym_analyzer",
"fields": {
"keyword": { "type": "keyword" },
"autocomplete": { "type": "text", "analyzer": "autocomplete_analyzer", "search_analyzer": "standard" }
}
},
"description": { "type": "text", "analyzer": "synonym_analyzer" },
"category": { "type": "keyword" },
"subcategory": { "type": "keyword" },
"brand": { "type": "keyword" },
"price": { "type": "float" },
"sale_price": { "type": "float" },
"currency": { "type": "keyword" },
"rating": { "type": "float" },
"review_count": { "type": "integer" },
"in_stock": { "type": "boolean" },
"sku": { "type": "keyword" },
"tags": { "type": "keyword" },
"image_url": { "type": "keyword", "index": false },
"created_at": { "type": "date" },
"popularity_score": { "type": "float" },
"attributes": {
"type": "nested",
"properties": {
"name": { "type": "keyword" },
"value": { "type": "keyword" }
}
},
"title_suggest": {
"type": "completion",
"analyzer": "simple"
}
}
}
}3. Ingestion
from elasticsearch import Elasticsearch, helpers
es = Elasticsearch(cloud_id="...", api_key="...")
def index_products(products: list[dict]) -> tuple[int, list]:
actions = []
for product in products:
product["title_suggest"] = {
"input": [product.get("title", ""), product.get("brand", "")],
"weight": int(product.get("popularity_score", 1))
}
actions.append({"_index": "products", "_id": product.get("sku"), "_source": product})
return helpers.bulk(es, actions, raise_on_error=False, raise_on_exception=False)Use _id = SKU so re-indexing updates in place. For large catalogs (>100K products), use bulk batches of 1,000-5,000 documents.
4. Query Patterns
Product Search with Filters
POST /products/_search
{
"query": {
"bool": {
"must": [
{
"multi_match": {
"query": "wireless headphones",
"fields": ["title^3", "description", "brand^2", "tags"],
"type": "best_fields",
"fuzziness": "AUTO"
}
}
],
"filter": [
{ "term": { "in_stock": true } },
{ "term": { "category": "electronics" } },
{ "range": { "price": { "gte": 50, "lte": 300 } } }
]
}
},
"sort": [
{ "_score": "desc" },
{ "popularity_score": "desc" }
],
"size": 20
}Faceted Navigation (Aggregations)
Return filter counts alongside search results:
POST /products/_search
{
"query": {
"bool": {
"must": [{ "match": { "title": "headphones" } }],
"filter": [{ "term": { "in_stock": true } }]
}
},
"size": 20,
"aggs": {
"categories": {
"terms": { "field": "category", "size": 20 }
},
"brands": {
"terms": { "field": "brand", "size": 20 }
},
"price_ranges": {
"range": {
"field": "price",
"ranges": [
{ "to": 50, "key": "Under $50" },
{ "from": 50, "to": 100, "key": "$50-$100" },
{ "from": 100, "to": 200, "key": "$100-$200" },
{ "from": 200, "key": "$200+" }
]
}
},
"avg_rating": {
"avg": { "field": "rating" }
},
"rating_distribution": {
"histogram": { "field": "rating", "interval": 1, "min_doc_count": 0 }
}
}
}Autocomplete
POST /products/_search
{
"suggest": {
"product-suggest": {
"prefix": "wire",
"completion": {
"field": "title_suggest",
"size": 8,
"skip_duplicates": true,
"fuzzy": { "fuzziness": "AUTO" }
}
}
}
}For search-as-you-type with results (not just suggestions):
POST /products/_search
{
"query": {
"match": {
"title.autocomplete": {
"query": "wire",
"operator": "and"
}
}
},
"size": 5,
"_source": ["title", "brand", "price", "image_url"]
}"Did You Mean" (Spelling Suggestions)
POST /products/_search
{
"suggest": {
"spelling": {
"text": "wireles headphons",
"phrase": {
"field": "title",
"size": 3,
"gram_size": 3,
"direct_generator": [{
"field": "title",
"suggest_mode": "popular"
}]
}
}
}
}Boosted Search (Merchandising)
Promote on-sale, highly-rated, or popular products:
POST /products/_search
{
"query": {
"function_score": {
"query": {
"multi_match": {
"query": "headphones",
"fields": ["title^3", "description", "brand^2"]
}
},
"functions": [
{
"field_value_factor": {
"field": "rating",
"modifier": "log1p",
"factor": 2
}
},
{
"field_value_factor": {
"field": "review_count",
"modifier": "log1p",
"factor": 0.5
}
},
{
"filter": { "exists": { "field": "sale_price" } },
"weight": 1.5
},
{
"gauss": {
"created_at": {
"origin": "now",
"scale": "30d",
"decay": 0.5
}
}
}
],
"score_mode": "sum",
"boost_mode": "multiply"
}
}
}Nested Attribute Filtering
Filter by dynamic product attributes (size, color, material):
POST /products/_search
{
"query": {
"bool": {
"must": [{ "match": { "title": "shoes" } }],
"filter": [
{
"nested": {
"path": "attributes",
"query": {
"bool": {
"must": [
{ "term": { "attributes.name": "color" } },
{ "term": { "attributes.value": "red" } }
]
}
}
}
},
{
"nested": {
"path": "attributes",
"query": {
"bool": {
"must": [
{ "term": { "attributes.name": "size" } },
{ "term": { "attributes.value": "10" } }
]
}
}
}
}
]
}
}
}5. API Endpoint
from flask import Flask, request, jsonify
from elasticsearch import Elasticsearch
app = Flask(__name__)
es = Elasticsearch(cloud_id="...", api_key="...")
@app.route("/search", methods=["GET"])
def product_search():
q = request.args.get("q", "")
category = request.args.get("category")
brand = request.args.get("brand")
min_price = request.args.get("min_price", type=float)
max_price = request.args.get("max_price", type=float)
in_stock = request.args.get("in_stock", "true").lower() == "true"
sort_by = request.args.get("sort", "relevance")
page = request.args.get("page", 1, type=int)
size = request.args.get("size", 20, type=int)
must = []
if q:
must.append({
"multi_match": {
"query": q,
"fields": ["title^3", "description", "brand^2", "tags"],
"type": "best_fields",
"fuzziness": "AUTO"
}
})
filters = [{"term": {"in_stock": in_stock}}]
if category:
filters.append({"term": {"category": category}})
if brand:
filters.append({"term": {"brand": brand}})
if min_price is not None:
filters.append({"range": {"price": {"gte": min_price}}})
if max_price is not None:
filters.append({"range": {"price": {"lte": max_price}}})
sort_options = {
"relevance": [{"_score": "desc"}, {"popularity_score": "desc"}],
"price_asc": [{"price": "asc"}],
"price_desc": [{"price": "desc"}],
"rating": [{"rating": "desc"}, {"review_count": "desc"}],
"newest": [{"created_at": "desc"}],
}
body = {
"query": {
"bool": {
"must": must if must else [{"match_all": {}}],
"filter": filters
}
},
"from": (page - 1) * size,
"size": size,
"sort": sort_options.get(sort_by, sort_options["relevance"]),
"highlight": {"fields": {"title": {}, "description": {}}},
"aggs": {
"categories": {"terms": {"field": "category", "size": 20}},
"brands": {"terms": {"field": "brand", "size": 20}},
"price_stats": {"stats": {"field": "price"}},
"price_ranges": {
"range": {
"field": "price",
"ranges": [
{"to": 25, "key": "Under $25"},
{"from": 25, "to": 50, "key": "$25-$50"},
{"from": 50, "to": 100, "key": "$50-$100"},
{"from": 100, "to": 200, "key": "$100-$200"},
{"from": 200, "key": "$200+"}
]
}
}
}
}
resp = es.search(index="products", body=body)
return jsonify({
"hits": [{
"product": h["_source"],
"score": h["_score"],
"highlight": h.get("highlight", {})
} for h in resp["hits"]["hits"]],
"total": resp["hits"]["total"]["value"],
"facets": {
"categories": [{"key": b["key"], "count": b["doc_count"]} for b in resp["aggregations"]["categories"]["buckets"]],
"brands": [{"key": b["key"], "count": b["doc_count"]} for b in resp["aggregations"]["brands"]["buckets"]],
"price_ranges": [{"key": b["key"], "count": b["doc_count"]} for b in resp["aggregations"]["price_ranges"]["buckets"]],
"price_stats": resp["aggregations"]["price_stats"]
},
"page": page,
"pages": (resp["hits"]["total"]["value"] + size - 1) // size
})
@app.route("/autocomplete", methods=["GET"])
def autocomplete():
q = request.args.get("q", "")
resp = es.search(
index="products",
body={
"suggest": {
"product-suggest": {
"prefix": q,
"completion": {
"field": "title_suggest",
"size": 8,
"skip_duplicates": True,
"fuzzy": {"fuzziness": "AUTO"}
}
}
}
}
)
suggestions = resp["suggest"]["product-suggest"][0]["options"]
return jsonify({
"suggestions": [{"text": s["text"], "score": s["_score"]} for s in suggestions]
})6. Relevance Tuning
| Lever | Effect |
|---|---|
| Field boosting | title^3 weights title matches higher than description |
| Fuzziness | AUTO handles typos; increase for more tolerance |
| Function score | Boost by rating, recency, popularity, on-sale status |
| Synonyms | Map domain terms so "laptop" matches "notebook" |
| Phrase matching | Use match_phrase for exact multi-word queries |
7. Common Follow-Ups
| Question | Answer |
|---|---|
| "How do I add sort options?" | Add sort parameter; support price_asc, price_desc, rating, newest. |
| "How do I show facet counts?" | Use aggregations (terms, range, histogram) alongside your query. |
| "How do I handle variants (size/color)?" | Use nested fields for attributes; filter with nested queries. |
| "How do I boost promoted products?" | Use function_score with pinned queries or manual weight boosts. |
| "How do I handle no results?" | Relax filters, try fuzzy matching, show "did you mean" suggestions, or fall back to popular products. |
8. When to Upgrade
- Semantic product search — When "comfortable headphones for running" should match even without exact keyword
overlap. Add a vector field using the vector-hybrid-search guide.
- Hybrid — Combine keyword + semantic for the best of both. See the vector-hybrid-search guide.
- Personalization — Boost results based on user behavior (clicks, purchases). Requires a signals index and custom
scoring.
First Message
If the developer's first message is vague, generic, or exploratory — things like "hi," "help," "get started," "what can you do," or just "search" — don't respond with a generic greeting. Jump straight into the guided flow with a warm, specific opener. For example:
I'm set up to help you build search with Elasticsearch — from mapping your data to a working API. To get started, tell
me what you're building. For example:
>
- "I need product search with filters and autocomplete for an e-commerce site"
- "I want to build a Q&A chatbot that answers questions from our docs"
- "I need semantic search across support tickets"
- "I want to use Elasticsearch as a vector database for my AI app"
- "I'm building a RAG pipeline with LangChain and need a retrieval backend"
- "I need a customer support knowledge base with self-service search"
- "I want location-based search — find stores or services near the user"
>
What are you working on?
Keep it to one question. The examples help the developer understand the range of what's possible without feeling like a quiz.
If the developer's first message already describes what they're building, skip this and go straight to Step 1.
Cluster Connection (MCP)
Before starting the playbook, check if the Elastic MCP server is configured. If MCP tools like list_indices or get_mappings are available, you're already connected — proceed to the playbook.
If MCP tools are not available and the developer mentions having an Elasticsearch cluster, offer to set it up early so you can inspect their data later. Say something like:
Before we dive in — want me to connect to your Elasticsearch cluster? It takes about 30 seconds and lets me inspect
your indices and run queries directly. You'll need Docker or Node.js installed.
If they say yes, try Docker first (preferred), fall back to npx if Docker isn't available, and move on gracefully if neither works.
MCP server configuration
The Elasticsearch MCP server needs a JSON configuration block added to the developer's MCP config file. The exact file location depends on their tool:
| Tool | Config file |
|---|---|
| Cursor | .cursor/mcp.json in the project root |
| VS Code (Copilot) | .vscode/mcp.json in the project root |
| Windsurf | ~/.codeium/windsurf/mcp_config.json |
| Claude Desktop | ~/Library/Application Support/Claude/claude_desktop_config.json (macOS) or %APPDATA%\Claude\claude_desktop_config.json (Windows) |
| Claude Code | .mcp.json in the project root |
Ask the developer which tool they're using if it's not clear from context, and write the config to the appropriate location.
Option A: Docker (preferred)
1. Ask them to confirm Docker is running (docker --version in their terminal) 2. Add the following MCP server configuration:
{
"mcpServers": {
"elasticsearch": {
"command": "docker",
"args": ["run", "-i", "--rm", "-e", "ES_URL", "-e", "ES_API_KEY", "docker.elastic.co/mcp/elasticsearch", "stdio"],
"env": {
"ES_URL": "https://YOUR_ELASTICSEARCH_URL",
"ES_API_KEY": "YOUR_API_KEY"
}
}
}
}Replace YOUR_ELASTICSEARCH_URL with their Elasticsearch endpoint (found in Kibana → help icon → Connection details → Elasticsearch endpoint) and YOUR_API_KEY with the API key they created. 3. Tell them to reload their MCP connections. The reload mechanism varies by tool — in most editors it's available via the command palette or MCP settings panel. Once reconnected, you'll be able to see their indices, read their mappings, and run queries directly.
Option B: npx (if Docker isn't available)
{
"mcpServers": {
"elasticsearch": {
"command": "npx",
"args": ["-y", "@elastic/mcp-server-elasticsearch"],
"env": {
"ES_URL": "https://YOUR_ELASTICSEARCH_URL",
"ES_API_KEY": "YOUR_API_KEY"
}
}
}
}Same reload step as above.
If neither works, don't make them feel stuck:
No worries — everything else works without the live connection. I just won't be able to inspect your cluster directly,
so I'll work from what you tell me about your data. We can always set up the connection later if your environment
allows it.
Important: add the MCP config file to `.gitignore` — it contains API credentials that should not be committed to version control. After writing the file, check if .gitignore exists and add the config file path to it. If there's no .gitignore, create one.
If the developer doesn't mention a cluster or wants to skip MCP, that's fine — proceed to the playbook. MCP enhances the experience but is not required.
Conversation Playbook
Follow this sequence when a developer asks for help building search. Ask ONE question at a time. Wait for the answer before moving to the next step. Do not combine multiple questions into a single response — it feels like a form, not a conversation.
Step 1: Understand Intent
Ask what they're building, in their own words. One question only — something like "What kind of search experience are you building?" Then wait.
Listen for signals that tell you which approach to recommend:
| Signal | Approach | Output |
|---|---|---|
| "search bar", "filter by", "facets", "autocomplete" | keyword-search | Ranked results |
| "find similar", "natural language", "meaning-based" | vector-hybrid-search | Ranked results (by meaning) |
| "both keyword and semantic", "hybrid" | vector-hybrid-search | Ranked results (combined) |
| "chatbot", "Q&A", "answer from my docs", "RAG" | rag-chatbot | Generated answers (not just results) |
| "product search", "e-commerce", "catalog" | catalog-ecommerce | Ranked results with facets |
| "vector store", "embeddings", "LangChain", "LlamaIndex", "AI app", "agent", "similarity", "recommendations" | vector-hybrid-search | Vectors for downstream AI |
Semantic vs RAG — a key distinction. Semantic search returns a _list of relevant results_ ranked by meaning. RAG retrieves relevant documents and then feeds them to an LLM to _generate an answer_. If the developer says "I want to answer questions from my docs," that's RAG — they want answers, not a list of documents. If they say "I want users to find relevant docs by describing what they need," that's semantic search. Ask: "Do you want to show users a list of results, or generate direct answers from the content?"
If the intent is clear enough to pick an approach, move to the follow-up below. If ambiguous, ask one clarifying question first.
Observability and Security use cases. If the developer describes something that falls outside search — like log monitoring, APM, SIEM, threat detection, endpoint security, anomaly detection on metrics, or infrastructure monitoring — let them know that Elastic has dedicated solution experiences for those:
That sounds like an Observability _(or Security)_ use case — Elastic has a dedicated experience built for that,
with purpose-built dashboards, alerting, and workflows.
>
- Elastic Cloud Hosted: You can switch your solution view in Kibana under Management → Spaces — each space can
have its own solution view (Search, Observability, or Security). See
Spaces documentation.
- Elastic Cloud Serverless: Create a new project and select the Observability _(or Security)_ project type.
Each solution type is a separate project. See
Serverless project types.
>
I'm best at helping with search use cases — building search APIs, indexing data, writing queries. Want to continue
with a search-related project, or do you need help getting to the right solution view?
Don't try to build Observability or Security workflows from scratch with search primitives. Point the developer to the right product experience.
Follow-up: "Who's doing the searching — people or code?" This is the question that separates traditional search from AI-pipeline use cases. Ask something like:
"Will people be typing searches directly — like a search bar or filter UI — or is this for an AI application that
retrieves data programmatically, like a LangChain agent, an AI assistant, or a recommendation engine?"
If the answer is people searching directly, continue to the natural language follow-up below. If the answer is an AI application, route to the vector-hybrid-search guide — the developer needs Elasticsearch as a vector store, not as a human-facing search engine. The architecture, mapping, and integration patterns are fundamentally different.
Follow-up (for human-facing search): "Will users also search in natural language?" Once you know people are searching directly, find out whether they'll only use specific terms (e.g., "size 10 Nike running shoes") or whether they'll also use natural, descriptive queries (e.g., "comfortable shoes for running in the rain"). Keyword search handles the first case well on its own. But if users will also describe what they want in their own words, adding semantic search on top makes a big difference. One question — something like:
"Beyond specific terms and filters, do you expect users to also search with more descriptive, natural language —
things like 'warm jacket for winter hiking' or 'quick easy dinner ideas'?"
If yes, the recommendation in Step 3 should include semantic search alongside keyword — not as an alternative, but as an additional layer that catches meaning-based queries that keywords alone would miss. Don't skip this question.
Follow-up (if relevant): "Do different users see different data?" If the use case involves multi-tenant data, role-based access, or any scenario where search results should be filtered by who's asking (e.g., "users can only search their own organization's documents"), flag that Elasticsearch supports document-level security via role-based access control. This affects index design (you may need a tenant field) and query architecture. Ask about this early — bolting it on later is painful.
Time-series data. If the developer describes data that's append-only and timestamped (logs, events, metrics, IoT sensor data), recommend data streams instead of regular indices. Data streams automatically manage rollover, work with index lifecycle management (ILM) for retention, and are the standard for time-series data in Elasticsearch. This is a fundamentally different index strategy — surface it early rather than retrofitting later.
Step 2: Understand Their Data
This step has three parts — ask them as separate questions, not combined.
First: What does your data look like? Ask: "Tell me about your data — you can drop a sample here (JSON, CSV, a database schema), describe the fields, or just point me to where it lives and I can work from there."
The developer might respond in different ways. Adapt:
- They paste sample data — infer the field names, types, and structure directly. Don't ask them to describe what you
can already see.
- They describe it — use their description to build the schema.
- They point to their data source directly ("it's in Postgres" / "I have a CSV at this path" / "it's behind this
API") — ask enough to understand the schema (e.g., "can you share the table schema or a few column names?"), then proceed. These developers want to work with their real data from the start, not a sample. The generated code in Step 5 should connect to their actual source.
Second: Where does your data live today? If they didn't already answer this above, ask where the data is coming from. Something like: "Where does this data live right now — a database like Postgres or MongoDB, files on disk, a REST API, or somewhere else?"
This determines the ingestion approach:
| Data Source | Recommended Ingestion |
|---|---|
| CSV or JSON files (small) | Kibana file upload (Management → Machine Learning → File Data Visualizer) — no code at all |
| CSV or JSON files (large) | Bulk API script in the developer's language |
| REST API | Script that pulls from the API and bulk-indexes |
| Database (Postgres, MySQL, MongoDB) | Bulk API script with a database client — pull, transform, index |
| Another Elasticsearch index | Reindex API — no external code |
| Streaming (Kafka, webhooks, events) | Data streams + ingest pipeline, or Elastic Agent / OpenTelemetry |
| Not sure yet / just exploring | Start with sample data, add real ingestion later |
Don't default to a bulk import script. If it's a small CSV, Kibana's upload is faster. Match the ingestion approach to their data source and language.
Important: Not every developer wants to start with sample data. Some already have their data and want to ingest it for real. If they've told you where their data lives and what it looks like, generate code that connects to their actual source — don't force a "paste a sample first" step they don't need.
Third: What language is your application in? Ask: "What language are you building in — Python, JavaScript/TypeScript, Java, Go, or something else?" Generate all code in their language using the appropriate Elasticsearch client library. Don't assume Python.
Use what you learn to determine:
- What fields to map (text, keyword, numeric, nested)
- Whether they need an embedding model and which one
- Which ingestion path to recommend (upload, bulk API, reindex, streaming)
- Which client library to use for generated code
Step 3: Recommend and Confirm
Once you have intent + data shape, present your recommended approach before writing any code. Break it down into the specific capabilities you'll implement, and explain each one in plain language so the developer understands what they're getting. For example:
Here's what I'd build for you:
>
- Fuzzy full-text search — Handles typos and misspellings automatically. If someone types "runnign shoes," it
still finds "running shoes."
- Faceted filtering — Lets users narrow results by category, price range, brand, etc. Think of the sidebar filters
on any shopping site.
- Autocomplete — Suggests matching results as the user types, so they get instant feedback in the search bar.
- Geo-distance queries — Finds items near a location. Useful for "stores near me" or location-based results.
>
Does this look right, or would you add/remove anything?
Every capability you list should include a brief, jargon-free explanation of what it does and why it matters. Don't assume the developer knows what "fuzzy matching" or "faceted navigation" means.
Surface the hybrid option when it adds value. If the developer indicated natural language queries in Step 1, or if the use case naturally involves descriptive searches (e-commerce, documentation, knowledge bases, support content), recommend adding semantic search alongside keyword search. Explain the tradeoff clearly:
I'd also recommend adding semantic search on top of the keyword matching. This means when someone searches
"comfortable shoes for long walks," it finds relevant products even if those exact words don't appear in the product
name or description — it understands the _meaning_ behind the query. The tradeoff is it requires an embedding model
(Elastic provides one built-in called ELSER, or you can use OpenAI/Cohere), and indexing is slightly slower because
each document gets a vector embedding generated. Worth it?
Don't silently omit semantic when it would help. Don't force it when it wouldn't (e.g., pure structured filtering, log search, ID lookups). Let the developer decide, but make sure they have the information to decide well.
Wait for confirmation before generating code. The developer might want to drop a capability, add one, or ask questions. This is a conversation, not a deployment pipeline.
Step 4: Walk Through the Mapping
After the developer confirms the overall approach, present the proposed index mapping field by field. This is the most important step — the mapping is the foundation everything else builds on, and changing it later requires reindexing.
For each field, explain:
- What type you're assigning and why (e.g.,
textvskeywordvsintegervsgeo_point) - What it enables (e.g., "this lets users filter by exact category without analysis overhead")
- Any special configuration like sub-fields, custom analyzers, or completion suggesters — and what those do in plain
language
For example:
Here's how I'd map your data. Each field is set up for a specific job:
>
| Field | Type | Why |
| ------------- | ------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| name | text (3 sub-fields) | Main search field. Gets a synonym analyzer so "boots" and "shoes" match, an autocomplete analyzer for typeahead suggestions, and a keyword sub-field for exact sorting. || description | text | Searched alongside name but with lower relevance weight — helps with recall without dominating ranking. || category | keyword | Exact-match only — no analysis. Powers instant filtering and facet counts (e.g., "Footwear (42)"). || price | float | Enables range filters (min/max) and price-based sorting. ||stock_level| integer | Lets you filter "in stock only" (stock_level > 0) and sort by availability. |
| tags | keyword (array) | Multi-value field for filtering and facets. Each product can have many tags. || location | geo_point | Enables "near me" distance queries and geo-sorting. |>
One thing to know: once data is indexed with this mapping, changing a field's type (e.g., from text tokeyword) means you'll need to reindex — create a new index with the updated mapping, copy all documents over,and swap the alias. For small datasets this takes seconds; for millions of documents it can take minutes to hours
depending on cluster size. It's not destructive (your data is safe), but it's something you want to get right upfront.
>
Does this mapping look right for your data? Anything you'd add, remove, or change?
Wait for confirmation before generating code. Mapping changes are the most expensive thing to fix later, so get this right first. If the developer wants changes, adjust the mapping and re-present it.
Step 5: Build
Once the developer confirms the mapping, generate the complete implementation:
1. Index creation with an alias — Create the index with a versioned name (e.g., products-v1) and an alias pointing to it (e.g., products). All queries and writes should go through the alias. This way, when you need to reindex later (mapping change, analyzer update), you create products-v2, reindex into it, and swap the alias — zero downtime, no client code changes. Explain this briefly when presenting the code. 2. Ingestion — Use the approach determined in Step 2 (Kibana upload, bulk API, reindex, streaming, etc.). Don't default to a bulk script if the developer's data source has a better path. 3. Search API endpoint with all confirmed capabilities 4. Getting started instructions (see the credential walkthrough section below) 5. Pagination — Always include pagination in search endpoints. Use from/size for basic pagination (suitable for most use cases up to 10,000 results). For deep pagination or large result sets, use search_after with a point-in-time (PIT). Explain the tradeoff briefly: from/size is simpler but has a 10,000-hit limit; search_after scales indefinitely but requires tracking a cursor.
Generate code in the developer's preferred language from Step 2. Don't ask for permission to generate code at this point — they already confirmed both the approach and the mapping. Just build it.
Step 6: Test and Validate
After generating the code, walk the developer through verifying it works:
1. Index a few documents — Run the ingestion step with sample data (or their real data if available). Confirm the index was created and documents are there. 2. Run test queries — Provide 2-3 example queries that exercise the key capabilities (e.g., a full-text search, a filtered query, an autocomplete query). If MCP is connected, run them directly and show results. 3. Check relevance — For the test queries, briefly explain why the results are ranked the way they are (e.g., "this result ranked first because it matched on the name field with a 3x boost"). This teaches the developer how tuning works. 4. Suggest next steps — Point to specific things they can try: adjusting boosts, adding synonyms, testing edge cases, or connecting their real data source.
Step 7: Iterate
When the developer refines ("results aren't relevant enough," "add a category filter," "make it faster"), make targeted adjustments. If a change requires a mapping update, flag that it will require reindexing and explain the process — but remind them that because they're using an alias, the swap is seamless.
Documentation
Reference context/elastic-docs.md for the official Elastic documentation structure and links. When recommending next steps or deeper reading, link to specific doc pages from that file. Key entry points:
- Search approaches: <https://www.elastic.co/docs/solutions/search>
- Data management: <https://www.elastic.co/docs/manage-data>
- Query languages: <https://www.elastic.co/docs/explore-analyze/query-filter/languages>
- Client libraries: <https://www.elastic.co/docs/reference> (Python, JavaScript, Java, Go, .NET, PHP, Ruby)
- Deployment: <https://www.elastic.co/docs/deploy-manage>
When generating code, cite the relevant doc page so the developer can go deeper if needed.
Search Pattern Reference
You have access to detailed implementation guides for each search pattern. Use them when the developer's intent matches:
- keyword-search/keyword-search.md — Full-text search, filters, facets,
autocomplete, typo tolerance
- vector-hybrid-search/vector-hybrid-search.md — Vector search, semantic
search, hybrid BM25 + kNN (RRF), and Elasticsearch as a vector store for AI pipelines
- rag-chatbot/rag-chatbot.md — Retrieval-augmented generation, Q&A, chatbots over
documents
- catalog-ecommerce/ecommerce.md — Product search, faceted navigation, merchandising,
autocomplete
Code Standards
When generating Elasticsearch code:
- Developer's language — Generate code in the language the developer specified in Step 2. Use the official
Elasticsearch client for that language. If they didn't specify, ask before defaulting.
- Query DSL for search — Use Query DSL for full-text search, kNN, aggregations, and all search-related operations.
Query DSL is the most complete and well-documented query interface for these patterns. Mention ES|QL as an alternative for analytics and data exploration queries (filtering, aggregations, transformations) where its piped syntax is a better fit, but don't default to it for search.
- Cloud-ready — Use the elasticsearch URL +
api_keyfor connection. Include self-managed alternatives in comments.
Always include the Getting Started section below so developers know where to find their credentials.
- Error handling — Include basic error handling in ingestion (bulk API errors) and search (empty results, timeouts).
- Production patterns — Use bulk API for ingestion (not single-doc indexing), connection pooling, and appropriate
timeouts.
- Production-ready configuration — All generated code must work beyond the sample data. See the section below on
domain-specific configuration.
- Aliases from day one — Always create indices with a versioned name and an alias. See Step 5 for details.
Domain-Specific Configuration
Generated code must be production-ready, not just a demo that works for sample data. This applies to synonyms, analyzers, boosting weights, and any configuration that depends on the developer's actual domain.
Synonyms
Never hardcode synonyms inline in the mapping. Inline synonyms require closing and reopening the index (or reindexing) every time you update them — that's unacceptable in production.
Instead, use the Elasticsearch Synonyms API, which lets you update synonyms at any time without reindexing or downtime:
1. Create a synonym set via the API:
PUT _synonyms/my-product-synonyms
{
"synonyms_set": [
{"id": "boots", "synonyms": "boots, shoes, footwear"},
{"id": "hiking", "synonyms": "hiking, trekking, trail"}
]
}2. Reference it in the analyzer using synonyms_set (not synonyms):
"filter": {
"product_synonyms": {
"type": "synonym",
"synonyms_set": "my-product-synonyms",
"updateable": true
}
}3. The synonym set can be updated at any time via PUT _synonyms/my-product-synonyms — no reindex needed.
When generating synonyms, ask the developer about their domain rather than guessing from sample data. A few outdoor gear samples shouldn't produce a synonym list — the developer's actual product catalog should. If you don't have enough context, generate the code structure with an empty or minimal synonym set and include clear instructions on how to populate it:
The synonym set is where you teach Elasticsearch about your domain vocabulary. Right now it's a starter set — you'll
want to expand this based on what your users actually search for. Common sources: search analytics (queries with zero
results), customer support terminology, and industry-standard terms. You can update synonyms at any time via the
Synonyms API without reindexing.
Other domain-specific settings
Apply the same principle to all configuration that depends on the developer's data:
- Field boosts (e.g.,
name^3, tags^2) — Present these as starting points and explain how to tune them based on
click-through data, not as final values
- Edge n-gram ranges — Explain the tradeoff (larger max_gram = more disk, faster prefix matching) and let the
developer choose
- Completion suggester weights — Explain what the weight controls and how to set it based on their business logic
(popularity, recency, margin, etc.)
The goal: every piece of generated code should work correctly when the developer swaps in their real data, not just for the sample record they pasted.
Getting Started with Elastic Cloud
When generated code includes a connection block, always include a Getting Started section that walks the developer through finding their credentials. Don't just say "set your cloud_id and api_key" — show them where to get them. The developer already has an Elasticsearch cluster (they accessed this from Kibana), so never suggest signing up for a trial.
Finding your Cloud ID
In Kibana, click the help icon (?) in the top nav, then Connection details. The Cloud ID is shown there. You can also find it at <https://cloud.elastic.co> → click your deployment → the Cloud ID is on the overview page.
Creating an API key
In Kibana, go to Management → Security → API keys → Create API key. Give it a name (e.g., dev-key) and create it. Copy the Encoded value — that's your api_key.
You can also create one via the REST API in Kibana Dev Tools (Management → Dev Tools):
POST /_security/api_key
{"name": "dev-key", "expiration": "30d"}Copy the encoded value from the response.
Self-managed clusters
If they're running Elasticsearch on their own infrastructure (not Elastic Cloud):
- Replace
cloud_id/api_keywithhosts=["https://your-elasticsearch-host:9200"](and
basic_auth=("elastic", "password") if using basic auth)
Always include this context in the Getting Started section of generated code. Never assume the developer knows where to find credentials.
Key Elasticsearch Concepts
When explaining, use these terms consistently:
| Term | Meaning |
|---|---|
| Index | A collection of documents (like a database table) |
| Mapping | Schema definition — field names, types, analyzers |
| Analyzer | Text processing pipeline (tokenizer + filters) |
| Inference endpoint | A hosted or connected ML model for embeddings |
| Ingest pipeline | Server-side document processing before indexing |
| kNN | k-nearest neighbors — vector similarity search |
| RRF | Reciprocal Rank Fusion — merges keyword and vector results |
| Alias | A pointer to one or more indices — enables zero-downtime reindexing and index versioning |
| Data stream | Append-only index abstraction for time-series data (logs, metrics, events) with automatic rollover |
| **ES\ | QL** |
| Query DSL | JSON query syntax — full feature set for search, backward compatible |
What NOT to Do
- Don't ask multiple questions at once — one question, then wait
- Don't generate code before the developer confirms the approach and mapping
- Don't hardcode synonyms inline in mappings — use the Synonyms API
- Don't create indices without aliases — always use a versioned index name + alias
- Don't assume the developer knows Elasticsearch internals — explain decisions briefly
- Don't use the word "recipe" — say approach, pattern, or guide
- Don't skip the mapping walkthrough — it's the most expensive thing to change later
- Don't default to Python — ask what language they're using
- Don't generate code with deprecated APIs without noting the deprecation and recommending the replacement
Keyword Search Guide
Guide developers through building full-text keyword search with Elasticsearch. Use this guide when they need text matching, filters, faceting, autocomplete, or traditional search-bar behavior.
1. When to Use This Guide
Apply this guide when the developer signals:
- Structured data — products, articles, documents with known fields (title, description, category, price)
- Exact matching matters — SKUs, IDs, categories, status values must match precisely
- Simple search bar — user types terms and expects documents containing those terms
- Filtering and faceting — filter by category, price range, brand; show facet counts
- Autocomplete / typeahead — suggest completions as user types
- No semantic intent — "red shoes" should match documents containing "red" and "shoes", not "crimson sneakers"
Do not use this guide when: natural language queries return poor results, user expects meaning-based matching, or multilingual semantic similarity is needed. Point them to the vector-hybrid-search guide instead.
2. Index Mapping
Create a mapping with text fields (for full-text search), keyword sub-fields (for exact filtering and sorting), and completion fields (for autocomplete).
Example: products index
PUT /products
{
"mappings": {
"properties": {
"title": {
"type": "text",
"fields": {
"keyword": { "type": "keyword" }
},
"analyzer": "standard"
},
"description": {
"type": "text",
"fields": {
"keyword": { "type": "keyword" }
},
"analyzer": "standard"
},
"category": {
"type": "keyword"
},
"brand": {
"type": "keyword"
},
"price": { "type": "float" },
"rating": { "type": "float" },
"created_at": { "type": "date" },
"title_suggest": {
"type": "completion",
"analyzer": "simple",
"preserve_separators": true,
"preserve_position_increments": true,
"max_input_length": 50
}
}
}
}- text + keyword —
titleanddescriptionare searchable;title.keywordanddescription.keywordsupport exact
match, sorting, aggregations.
- keyword —
category,branduse for filters and faceting. - completion —
title_suggestpowers autocomplete.
Synonyms (optional): Add a custom analyzer with synonyms if needed:
{
"settings": {
"analysis": {
"analyzer": {
"synonym_analyzer": {
"tokenizer": "standard",
"filter": ["lowercase", "synonym_filter"]
}
},
"filter": {
"synonym_filter": {
"type": "synonym",
"synonyms": ["wireless, bluetooth => wireless"]
}
}
}
}
}3. Ingestion
Use the bulk API with error handling. Index documents in batches.
from elasticsearch import Elasticsearch, helpers
es = Elasticsearch(cloud_id="...", api_key="...")
def index_products(documents: list[dict]) -> tuple[int, list]:
"""Index documents into products index. Returns (success_count, errors)."""
actions = []
for doc in documents:
doc["title_suggest"] = {"input": doc.get("title", "").split()}
actions.append({
"_index": "products",
"_source": doc
})
success, errors = helpers.bulk(
es,
actions,
raise_on_error=False,
raise_on_exception=False,
request_timeout=30
)
if errors:
for err in errors:
if "index" in err and err["index"].get("error"):
print(f"Error indexing doc: {err['index']['error']}")
return success, errors4. Query Patterns
Basic Match Query
GET /products/_search
{
"query": {
"match": {
"title": "wireless headphones"
}
}
}Multi-Match Across Fields
GET /products/_search
{
"query": {
"multi_match": {
"query": "wireless headphones",
"fields": ["title^2", "description"],
"type": "best_fields",
"operator": "or"
}
}
}Bool Query with Filters
GET /products/_search
{
"query": {
"bool": {
"must": [
{ "match": { "title": "wireless headphones" } }
],
"filter": [
{ "term": { "category": "electronics" } },
{ "range": { "price": { "gte": 50, "lte": 500 } } }
]
}
}
}Fuzzy Matching (Typo Tolerance)
GET /products/_search
{
"query": {
"match": {
"title": {
"query": "wirless headphons",
"fuzziness": "AUTO"
}
}
}
}Autocomplete with Completion Suggester
GET /products/_search
{
"suggest": {
"title-suggest": {
"prefix": "wire",
"completion": {
"field": "title_suggest",
"skip_duplicates": true,
"size": 10
}
}
}
}Highlighting
GET /products/_search
{
"query": { "match": { "title": "wireless headphones" } },
"highlight": {
"fields": {
"title": {},
"description": {}
}
}
}Pagination
GET /products/_search
{
"query": { "match_all": {} },
"from": 20,
"size": 10,
"sort": [{ "price": "asc" }]
}ES|QL (Where Applicable)
ES|QL supports filtering and sorting. Use for simple filter + sort queries:
FROM products
| WHERE category == "electronics" AND price >= 50 AND price <= 500
| SORT rating DESC
| LIMIT 20For full-text search, match queries, or fuzzy matching, ES|QL does not yet support these; use Query DSL.
5. API Endpoint
Wrap the search in a Flask endpoint:
from flask import Flask, request, jsonify
from elasticsearch import Elasticsearch
app = Flask(__name__)
es = Elasticsearch(cloud_id="...", api_key="...")
@app.route("/search", methods=["GET"])
def search():
q = request.args.get("q", "")
category = request.args.get("category")
min_price = request.args.get("min_price", type=float)
max_price = request.args.get("max_price", type=float)
page = request.args.get("page", 1, type=int)
size = request.args.get("size", 10, type=int)
must = [{"match": {"title": q}}] if q else []
filter_clauses = []
if category:
filter_clauses.append({"term": {"category": category}})
if min_price is not None:
filter_clauses.append({"range": {"price": {"gte": min_price}}})
if max_price is not None:
filter_clauses.append({"range": {"price": {"lte": max_price}}})
body = {
"query": {
"bool": {
"must": must if must else [{"match_all": {}}],
"filter": filter_clauses
}
},
"from": (page - 1) * size,
"size": size,
"highlight": {"fields": {"title": {}, "description": {}}}
}
resp = es.search(index="products", body=body)
return jsonify({
"hits": [h["_source"] for h in resp["hits"]["hits"]],
"total": resp["hits"]["total"]["value"],
"page": page
})6. Relevance Tuning
- Field boosting —
"fields": ["title^2", "description"]weights title matches higher. - Function score — boost by rating, recency, or custom logic:
{
"query": {
"function_score": {
"query": { "match": { "title": "headphones" } },
"functions": [{ "field_value_factor": { "field": "rating", "modifier": "log1p" } }],
"boost_mode": "sum"
}
}
}- Custom analyzers — use synonym filter, edge n-gram for prefix matching, or language-specific analyzers.
- Synonyms — add synonym filter for domain terms (e.g., "headphones" ↔ "earphones").
7. Common Follow-Ups
| Question | Answer |
|---|---|
| "How do I add filters?" | Add filter clauses to a bool query. Use term for exact match, range for numeric/date ranges. |
| "How do I handle typos?" | Use fuzziness: "AUTO" in match queries, or match_phrase_prefix for prefix matching. |
| "How do I add autocomplete?" | Add a completion field to the mapping, populate it during indexing, use the suggest API. |
| "How do I paginate?" | Use from and size. For deep pagination, prefer search_after with a sort key. |
8. When to Upgrade
Suggest hybrid or semantic search when:
- Natural language queries return poor results (e.g., "comfortable headphones for running" doesn't match well).
- Multilingual content — keyword search doesn't handle cross-language similarity.
- "Find similar" — user wants items similar to a selected item, not just keyword overlap.
- Synonyms and paraphrasing — synonym lists are too large or maintenance-heavy.
Direct the developer to the vector-hybrid-search guide for vector search, hybrid search (combining keyword + vector with RRF), or using Elasticsearch as a vector store.
RAG / Chatbot Guide
Guide developers through building retrieval-augmented generation (RAG) systems with Elasticsearch as the retrieval backend. Use this guide when they want a chatbot, Q&A interface, or AI assistant that answers from their own documents.
1. When to Use This Guide
Apply this guide when the developer signals:
- "Build a chatbot" — over docs, knowledge base, support articles, internal wiki
- "Q&A over my data" — ask questions and get answers grounded in their documents
- "AI assistant" — a conversational interface that references specific content
- "Answer from my docs" — don't hallucinate, cite sources
- "RAG pipeline" — they already know the pattern and want Elasticsearch as the retriever
Do not use this guide when: the developer only needs search results (not generated answers) — point them to keyword, semantic, or hybrid search instead.
2. Architecture
RAG has four stages:
1. Chunk — Split documents into passages small enough for embedding and context windows 2. Embed & Index — Store chunks with vector embeddings in Elasticsearch 3. Retrieve — Given a user question, find the most relevant chunks 4. Generate — Pass retrieved chunks + question to an LLM to produce a grounded answer
User Question
│
▼
┌─────────────┐ ┌──────────────────┐ ┌─────────────┐
│ Embed query │────▶│ Elasticsearch │────▶│ LLM (GPT, │
│ │ │ kNN retrieval │ │ Claude, etc)│
└─────────────┘ └──────────────────┘ └─────────────┘
│ │
Top-k chunks Answer + sources3. Document Chunking
Chunking strategy depends on document structure. Ask the developer about their content.
| Strategy | When to Use | Chunk Size |
|---|---|---|
| Fixed-size | Uniform text, no clear sections | 500-1000 tokens |
| Paragraph-based | Well-structured docs with natural breaks | 1 paragraph per chunk |
| Section-based | Documents with headers (H1/H2/H3) | 1 section per chunk |
| Recursive | Mixed content, need flexibility | LangChain's RecursiveCharacterTextSplitter |
Important considerations:
- Overlap — Add 50-200 token overlap between chunks so context isn't lost at boundaries
- Metadata — Preserve source document title, URL, section header, page number with each chunk
- Parent document — Store the parent doc ID so you can retrieve surrounding context if needed
Python chunking example:
def chunk_documents(documents: list[dict], chunk_size: int = 500, overlap: int = 100) -> list[dict]:
"""Split documents into overlapping chunks with metadata."""
chunks = []
for doc in documents:
text = doc["content"]
words = text.split()
for i in range(0, len(words), chunk_size - overlap):
chunk_text = " ".join(words[i:i + chunk_size])
if not chunk_text.strip():
continue
chunks.append({
"content": chunk_text,
"source_title": doc.get("title", ""),
"source_url": doc.get("url", ""),
"chunk_index": len(chunks),
"parent_doc_id": doc.get("id", ""),
})
return chunksLangChain chunking:
from langchain.text_splitter import RecursiveCharacterTextSplitter
splitter = RecursiveCharacterTextSplitter(
chunk_size=1000,
chunk_overlap=200,
separators=["\n\n", "\n", ". ", " "]
)
chunks = splitter.split_documents(documents)4. Index Mapping
Store chunk text, embedding, and metadata for retrieval and source citation.
PUT /knowledge-base
{
"mappings": {
"properties": {
"content": { "type": "text" },
"embedding": {
"type": "dense_vector",
"dims": 768,
"index": true,
"similarity": "cosine"
},
"source_title": { "type": "text", "fields": { "keyword": { "type": "keyword" } } },
"source_url": { "type": "keyword" },
"section_header": { "type": "text" },
"parent_doc_id": { "type": "keyword" },
"chunk_index": { "type": "integer" },
"created_at": { "type": "date" }
}
}
}5. Ingestion Pipeline
Use an ingest pipeline to embed chunks at index time.
PUT _ingest/pipeline/embed-knowledge-base
{
"processors": [
{
"inference": {
"model_id": "e5-multilingual",
"input_output": [
{
"input_field": "content",
"output_field": "embedding"
}
]
}
}
]
}Bulk index chunks:
from elasticsearch import Elasticsearch, helpers
es = Elasticsearch(cloud_id="...", api_key="...")
def index_chunks(chunks: list[dict]) -> tuple[int, list]:
actions = [
{"_index": "knowledge-base", "_source": chunk, "pipeline": "embed-knowledge-base"}
for chunk in chunks
]
return helpers.bulk(es, actions, raise_on_error=False, raise_on_exception=False)6. Retrieval Patterns
Semantic Retrieval (Default for RAG)
GET /knowledge-base/_search
{
"knn": {
"field": "embedding",
"query_vector_builder": {
"text_embedding": {
"model_id": "e5-multilingual",
"model_text": "How do I configure index mappings?"
}
},
"k": 5,
"num_candidates": 50
},
"_source": ["content", "source_title", "source_url", "section_header"]
}Hybrid Retrieval (Better for Mixed Queries)
Combine keyword and semantic for more robust retrieval:
POST /knowledge-base/_search
{
"size": 5,
"query": {
"bool": {
"should": [
{ "match": { "content": "configure index mappings" } },
{
"knn": {
"field": "embedding",
"query_vector_builder": {
"text_embedding": {
"model_id": "e5-multilingual",
"model_text": "How do I configure index mappings?"
}
},
"k": 5,
"num_candidates": 50
}
}
]
}
},
"rank": { "rrf": {} },
"_source": ["content", "source_title", "source_url"]
}Filtered Retrieval (Scope to Specific Sources)
GET /knowledge-base/_search
{
"knn": {
"field": "embedding",
"query_vector_builder": {
"text_embedding": {
"model_id": "e5-multilingual",
"model_text": "How do I configure mappings?"
}
},
"k": 5,
"num_candidates": 50,
"filter": {
"term": { "source_title.keyword": "Elasticsearch Guide" }
}
}
}7. Answer Generation
Pass retrieved chunks to an LLM with a grounded prompt.
from openai import OpenAI
from elasticsearch import Elasticsearch
es = Elasticsearch(cloud_id="...", api_key="...")
llm = OpenAI()
def ask(question: str, k: int = 5) -> dict:
# 1. Retrieve relevant chunks
resp = es.search(
index="knowledge-base",
knn={
"field": "embedding",
"query_vector_builder": {
"text_embedding": {
"model_id": "e5-multilingual",
"model_text": question
}
},
"k": k,
"num_candidates": k * 10
},
source=["content", "source_title", "source_url"]
)
chunks = resp["hits"]["hits"]
# 2. Build context from retrieved chunks
context_parts = []
sources = []
for i, hit in enumerate(chunks):
src = hit["_source"]
context_parts.append(f"[{i+1}] {src['content']}")
sources.append({"title": src.get("source_title", ""), "url": src.get("source_url", "")})
context = "\n\n".join(context_parts)
# 3. Generate answer
completion = llm.chat.completions.create(
model="gpt-4o-mini",
messages=[
{"role": "system", "content": (
"Answer the user's question using ONLY the provided context. "
"Cite sources using [1], [2], etc. "
"If the context doesn't contain enough information, say so."
)},
{"role": "user", "content": f"Context:\n{context}\n\nQuestion: {question}"}
],
temperature=0.2
)
return {
"answer": completion.choices[0].message.content,
"sources": sources
}8. Conversational Memory
For multi-turn conversations, include chat history in the prompt and optionally reformulate the question.
def ask_with_history(question: str, history: list[dict], k: int = 5) -> dict:
# Reformulate question using chat history for better retrieval
if history:
reformulation = llm.chat.completions.create(
model="gpt-4o-mini",
messages=[
{"role": "system", "content": (
"Rewrite the user's question as a standalone search query, "
"incorporating context from the conversation history."
)},
*history,
{"role": "user", "content": question}
],
temperature=0
)
search_query = reformulation.choices[0].message.content
else:
search_query = question
# Retrieve using reformulated query
result = ask(search_query, k=k)
# Generate with full conversation context
messages = [
{"role": "system", "content": (
"Answer the user's question using the provided context. "
"Cite sources using [1], [2], etc. "
"Consider the conversation history for context."
)},
*history,
{"role": "user", "content": f"Context:\n{result['context']}\n\nQuestion: {question}"}
]
completion = llm.chat.completions.create(
model="gpt-4o-mini",
messages=messages,
temperature=0.2
)
return {
"answer": completion.choices[0].message.content,
"sources": result["sources"]
}9. API Endpoint
from flask import Flask, request, jsonify
app = Flask(__name__)
@app.route("/chat", methods=["POST"])
def chat():
body = request.json
question = body.get("question", "")
history = body.get("history", [])
if not question:
return jsonify({"error": "Missing question"}), 400
result = ask_with_history(question, history)
return jsonify(result)10. Relevance Tuning for RAG
| Lever | Effect |
|---|---|
| Chunk size | Smaller = more precise retrieval, less context per chunk. Larger = more context but noisier. |
| k (num results) | More chunks = more context for the LLM but risks dilution. Start with 3-5. |
| Hybrid retrieval | Adds keyword matching; helps when questions contain specific terms or identifiers. |
| Reranking | Retrieve 20, rerank to top 5 with a cross-encoder for best precision. |
| Metadata filtering | Scope retrieval to relevant sources, time ranges, or categories. |
11. Common Follow-Ups
| Question | Answer |
|---|---|
| "The chatbot hallucinates" | Strengthen the system prompt ("only use provided context"), reduce temperature, add "I don't know" instructions. |
| "Answers are too vague" | Reduce chunk size for more precise passages; increase k for more context. |
| "How do I cite sources?" | Include source metadata in retrieval, reference in prompt with numbered citations. |
| "How do I handle long documents?" | Chunk with overlap; consider hierarchical retrieval (retrieve chunk, then fetch parent section). |
| "How do I update the knowledge base?" | Re-chunk and re-index changed documents. Use parent_doc_id to delete old chunks before re-indexing. |
| "Which LLM should I use?" | GPT-4o-mini for cost efficiency, GPT-4o or Claude for quality. Any OpenAI-compatible API works. |
12. When to Upgrade
- Agentic RAG — When the chatbot needs to take actions (create tickets, update records), not just answer questions.
Consider Elastic's Agent Builder.
- Multi-index RAG — When answers span multiple data sources. Use multiple kNN queries or a unified index.
- Streaming — For real-time chat UX, stream LLM responses token by token.
Search UI Frontend Guide
Guide developers through building a search frontend with Elastic's Search UI library (@elastic/search-ui). Use this guide after a backend recipe (keyword-search, hybrid-search, semantic-search, catalog-ecommerce) has produced a working index and API, and the developer asks "now how do I build the search page?"
1. When to Use This Guide
Apply this guide when the developer signals:
- "How do I build the search page?" — they have an Elasticsearch index/API and need a frontend
- "I need a search bar with filters" — they want pre-built UI components, not raw HTML
- "I want faceted search" — sidebar filters with counts, like any e-commerce site
- "I need autocomplete" — suggestions as the user types
- "What's the fastest way to get a search UI?" — they want a library, not to build from scratch
Do not use this guide when:
- The developer is building an AI pipeline where code (not humans) consumes search results — point them to
vector-database
- They only need a backend API with no frontend — they're done after the backend recipe
- They want to use a completely different frontend framework (Vue, Angular, Svelte) without React — Search UI's
pre-built components are React-only, though the headless core works with any framework. Explain the tradeoff: with React they get pre-built components out of the box; without React they get state management and query orchestration but must build their own UI components
2. Prerequisites
Before starting this guide, the developer should have:
1. A working Elasticsearch index with data (from any backend recipe) 2. Connection details — one of:
- Cloud ID (Elastic Cloud Hosted)
- Elasticsearch project endpoint URL (Elastic Cloud Serverless)
- Host URL (self-managed)
3. An API key with at least read access to the index 4. A React project — Next.js is also supported with a dedicated integration pattern.
3. Installation
npm install @elastic/search-ui @elastic/react-search-ui @elastic/react-search-ui-views @elastic/search-ui-elasticsearch-connectorFour packages:
@elastic/search-ui— headless core (state, actions, query orchestration)@elastic/react-search-ui— React bindings (SearchProvider,useSearchhook)@elastic/react-search-ui-views— pre-built React components with default styling@elastic/search-ui-elasticsearch-connector— connects to Elasticsearch directly
4. Connector Setup
The connector is how Search UI talks to Elasticsearch. Setup depends on the deployment model.
Development: Direct Connection
For local development and prototyping, connect directly. Never use this in production — it exposes API credentials to the browser.
import ElasticsearchAPIConnector from "@elastic/search-ui-elasticsearch-connector";
const connector = new ElasticsearchAPIConnector({
// Elastic Cloud Hosted — use cloud.id:
cloud: { id: "<your-cloud-id>" },
// Elastic Cloud Serverless or self-managed — use host instead of cloud:
// host: "https://<your-elasticsearch-endpoint>",
index: "<your-index-name>",
apiKey: "<read-only-api-key>",
});CORS for direct browser connections: If connecting directly from the browser (development only), Elasticsearch needs CORS headers. In Elastic Cloud Hosted, go to deployment settings → Edit user settings and add:
http.cors.allow-origin: "*"
http.cors.enabled: true
http.cors.allow-credentials: true
http.cors.allow-methods: OPTIONS, HEAD, GET, POST, PUT, DELETE
http.cors.allow-headers:
X-Requested-With, X-Auth-Token, Content-Type, Content-Length, Authorization, Access-Control-Allow-Headers, Accept,
x-elastic-client-metaServerless projects handle CORS automatically. Self-managed clusters need the same settings in elasticsearch.yml.
Production: Proxy Connector
In production, proxy all requests through your backend. This avoids exposing credentials, lets you add caching, logging, and access control.
Frontend (browser):
import { ApiProxyConnector } from "@elastic/search-ui-elasticsearch-connector/api-proxy";
const connector = new ApiProxyConnector({
basePath: "/api",
});Backend (Express server):
import express from "express";
import ElasticsearchAPIConnector from "@elastic/search-ui-elasticsearch-connector";
const app = express();
app.use(express.json());
const connector = new ElasticsearchAPIConnector({
host: "<your-elasticsearch-endpoint>",
index: "<your-index-name>",
apiKey: "<your-api-key>",
});
app.post("/api/search", async (req, res) => {
const { state, queryConfig } = req.body;
const response = await connector.onSearch(state, queryConfig);
res.json(response);
});
app.post("/api/autocomplete", async (req, res) => {
const { state, queryConfig } = req.body;
const response = await connector.onAutocomplete(state, queryConfig);
res.json(response);
});
app.listen(3001);This pattern works identically for Hosted, Serverless, and self-managed — the only difference is how you configure the server-side connector (cloud.id vs host).
For Next.js, use API routes instead of a separate Express server — same connector on the server, same ApiProxyConnector on the client.
5. Configuration
The configuration object tells Search UI which fields to search, which fields to show, and how to build facets. This must match the index mapping created in the backend recipe.
Mapping Config to Your Index
The configuration fields map directly to the Elasticsearch index mapping. Here's how to translate:
| Index Mapping | Search UI Config |
|---|---|
text fields you want searchable | search_fields with optional weight |
| Fields to display in results | result_fields with raw or snippet |
keyword fields for filtering | facets with type: "value" |
| Numeric fields for range filters | facets with type: "range" and ranges array |
geo_point fields | facets with type: "range", center, and unit |
completion or search_as_you_type fields | autocompleteQuery.suggestions |
Example: Product Search Config
This matches the index mapping from the catalog-ecommerce recipe:
const config = {
apiConnector: connector,
alwaysSearchOnInitialLoad: true,
searchQuery: {
search_fields: {
title: { weight: 3 },
description: {},
brand: { weight: 2 },
tags: {},
},
result_fields: {
title: { snippet: { size: 100, fallback: true } },
description: { snippet: { size: 200, fallback: true } },
brand: { raw: {} },
price: { raw: {} },
rating: { raw: {} },
image_url: { raw: {} },
category: { raw: {} },
},
fuzziness: true,
disjunctiveFacets: ["category", "brand"],
facets: {
category: { type: "value", size: 20 },
brand: { type: "value", size: 20 },
price: {
type: "range",
ranges: [
{ from: 0, to: 25, name: "Under $25" },
{ from: 25, to: 50, name: "$25–$50" },
{ from: 50, to: 100, name: "$50–$100" },
{ from: 100, to: 200, name: "$100–$200" },
{ from: 200, name: "$200+" },
],
},
rating: {
type: "range",
ranges: [
{ from: 4, name: "4+ stars" },
{ from: 3, to: 4, name: "3–4 stars" },
{ from: 0, to: 3, name: "Under 3 stars" },
],
},
},
},
autocompleteQuery: {
results: {
resultsPerPage: 5,
search_fields: {
"title.autocomplete": { weight: 3 },
},
result_fields: {
title: { snippet: { size: 100, fallback: true } },
price: { raw: {} },
image_url: { raw: {} },
},
},
suggestions: {
types: {
documents: { fields: ["title_suggest"] },
},
size: 4,
},
},
};`disjunctiveFacets` — list fields here if you want facet counts to stay visible after a selection. Without this, selecting "Electronics" as a category would hide all other category options. With it, the user can see counts for other categories and add more selections.
`fuzziness: true` — enables typo tolerance. Internally maps to Elasticsearch's fuzziness: "AUTO".
Autocomplete Field Requirements
Autocomplete has two modes that require different field types in the mapping:
| Mode | What It Does | Required Mapping |
|---|---|---|
results | Shows matching documents as you type | search_as_you_type field (best) or any text field |
suggestions | Shows suggested query terms | completion field |
If the backend recipe already created completion or search_as_you_type fields, reference them here. If not, the developer will need to update the mapping and reindex.
6. Components
Minimal Working Search Page
import React from "react";
import {
SearchProvider,
SearchBox,
Results,
PagingInfo,
ResultsPerPage,
Paging,
Facet,
Sorting,
ErrorBoundary,
} from "@elastic/react-search-ui";
import { Layout } from "@elastic/react-search-ui-views";
import "@elastic/react-search-ui-views/lib/styles/styles.css";
export default function SearchPage() {
return (
<SearchProvider config={config}>
<div className="App">
<ErrorBoundary>
<Layout
header={
<SearchBox
autocompleteResults={{
titleField: "title",
urlField: "url",
sectionTitle: "Results",
}}
autocompleteSuggestions={true}
debounceLength={300}
/>
}
sideContent={
<div>
<Facet field="category" label="Category" />
<Facet field="brand" label="Brand" />
<Facet field="price" label="Price" />
<Facet field="rating" label="Rating" />
</div>
}
bodyContent={<Results shouldTrackClickThrough />}
bodyHeader={
<>
<PagingInfo />
<ResultsPerPage options={[10, 20, 50]} />
<Sorting
label="Sort by"
sortOptions={[
{ name: "Relevance", value: [] },
{ name: "Price: Low to High", value: [{ field: "price", direction: "asc" }] },
{ name: "Price: High to Low", value: [{ field: "price", direction: "desc" }] },
{ name: "Rating", value: [{ field: "rating", direction: "desc" }] },
]}
/>
</>
}
bodyFooter={<Paging />}
/>
</ErrorBoundary>
</div>
</SearchProvider>
);
}Available Components
| Component | Purpose | Key Props |
|---|---|---|
SearchBox | Search input with autocomplete | autocompleteResults, autocompleteSuggestions, searchAsYouType, debounceLength |
Results | Render search result list | shouldTrackClickThrough, custom view |
Result | Single result card | titleField, urlField, custom view |
Facet | Sidebar filter | field, label, filterType ("any", "all", "none") |
Sorting | Sort dropdown | sortOptions array |
Paging | Page navigation | - |
PagingInfo | "Showing 1-10 of 250 results" | - |
ResultsPerPage | Results per page selector | options array |
ErrorBoundary | Catches and displays errors | - |
Layout | Pre-built page layout | header, sideContent, bodyContent, bodyHeader, bodyFooter |
Customizing Result Display
Pass a custom resultView function to <Results> to render product cards, images, or any layout. The function receives the full result object — access fields via result.<field>.raw (exact value) or result.<field>.snippet (highlighted HTML).
7. Custom Query Strategies
Search UI's default query works for keyword search. For semantic, hybrid, or advanced queries, use getQueryFn to override the query generation.
Semantic Search (requires ES 8.15+ with semantic_text field)
const connector = new ElasticsearchAPIConnector({
// ... connection config ...
getQueryFn: (state, config) => ({
semantic: {
field: "content_semantic",
query: state.searchTerm,
},
}),
});Hybrid Search (keyword + semantic via RRF)
For hybrid search, use interceptSearchRequest to inject a retriever-based query:
const connector = new ElasticsearchAPIConnector({
// ... connection config ...
interceptSearchRequest: async ({ requestBody, requestState, queryConfig }, next) => {
if (!requestState.searchTerm) return next(requestBody);
const modifiedBody = {
...requestBody,
query: undefined,
retriever: {
rrf: {
retrievers: [
{
standard: {
query: {
multi_match: {
query: requestState.searchTerm,
fields: ["title^3", "description"],
},
},
},
},
{
standard: {
query: {
semantic: {
field: "content_semantic",
query: requestState.searchTerm,
},
},
},
},
],
},
},
};
return next(modifiedBody);
},
});Sparse Vector
For most use cases, the Semantic Search example above is the recommended starting point — it uses semantic_text with Jina v3 (the default EIS dense embedding model, multilingual). Use the sparse_vector query below when you specifically need sparse-vector retrieval. ELSER (.elser-2-elasticsearch) is available as an English-only sparse alternative, but must be explicitly specified via inference_id.
const connector = new ElasticsearchAPIConnector({
// ... connection config ...
getQueryFn: (state, config) => ({
sparse_vector: {
field: "content_embedding",
inference_id: ".jina-embeddings-v3",
query: state.searchTerm,
},
}),
});Version Considerations for Query Strategies
| Strategy | Minimum ES Version | Notes |
|---|---|---|
| Keyword (multi_match, bool) | Any modern version | Works everywhere |
Semantic (semantic query) | 8.15+ | Requires semantic_text field; default EIS model is Jina v3 (multilingual dense) |
| kNN | 8.0+ | Use dense_vector field |
| Sparse vector / ELSER | 8.11+ | English-only sparse vectors; must be explicitly configured (Jina v3 is default on EIS) |
| Hybrid with RRF retrievers | 8.14+ | Retriever syntax |
| Serverless | Always latest | All features available |
When generating code, check the developer's Elasticsearch version (confirmed in the main playbook's Step 3) and only recommend query strategies their version supports.
8. Known Limitations
- Nested objects don't render — Search UI's Elasticsearch connector cannot display nested object fields. Use
flattened or keyword fields for facets instead of nested.
- React-only components — The pre-built components (
@elastic/react-search-ui-views) are React-only. The headless
core works with any framework, but you must build your own UI components.
- Query DSL only — Search UI generates Query DSL, not ES|QL. ES|QL is not supported for search queries through
Search UI.
- No built-in auth — Search UI doesn't handle user authentication. If different users see different data
(multi-tenancy), implement document-level security via the proxy layer.
- Facet counts with filters — Without
disjunctiveFacets, selecting a facet value hides other options. Always list
filterable facets in disjunctiveFacets for a good UX.
9. Common Follow-Ups
| Question | Answer |
|---|---|
| "How do I make facets stay open after selection?" | Add the field name to the disjunctiveFacets array. |
| "How do I deploy this?" | Switch to ApiProxyConnector on the frontend. Run the proxy server alongside your app. |
| "Can I use this with Vue/Angular?" | The headless core works with any framework. Build your own components via SearchDriver. |
| "How do I add semantic search?" | Use getQueryFn with a semantic query (requires ES 8.15+ and semantic_text field). See section 7. |
10. Connecting to Backend Recipes
This guide is designed to plug into any backend recipe. Here's how the handoff works:
| Backend Recipe | What It Provides | Search UI Connects Via |
|---|---|---|
| keyword-search | Text fields, keyword filters, completion field | Default query config — just map search_fields and facets to the index |
| catalog-ecommerce | Product mapping with synonyms, nested attributes, autocomplete | Full config example in section 5 above |
| hybrid-search | BM25 + semantic fields | getQueryFn or interceptSearchRequest with RRF retriever (section 7) |
| semantic-search | semantic_text or dense_vector fields | getQueryFn with semantic or knn query (section 7) |
The backend recipe builds the index, mapping, ingestion, and API. This recipe builds the frontend on top. If the developer followed a backend recipe that produced a Flask/Express API, they have two choices:
1. Use Search UI's connector to query Elasticsearch directly (through a proxy) — replaces the backend API for search 2. Keep the backend API and build a custom connector that calls it — useful when the API does more than search (auth, logging, business logic)
For option 2, implement a custom connector class with onSearch(requestState, queryConfig), onAutocomplete(requestState, queryConfig), onResultClick(), and onAutocompleteResultClick(). Each method receives Search UI state and must return { results, totalResults, facets }.
References
Elasticsearch Use Case Library
Present this library when a developer asks what they can build with Elasticsearch, wants to explore use cases, or needs help figuring out which category their project falls into. Walk through the relevant use cases conversationally — don't dump the entire list. Ask what resonates, then route to the appropriate implementation guide.
How to Use This Library
1. If the developer is exploring — summarize the 8 use cases with one-line descriptions and ask which sounds closest to what they're building. 2. If the developer describes something specific — match it to a use case below and confirm: "That sounds like [use case] — here's what that typically involves. Sound right?" 3. Once a use case is confirmed — return to the playbook and continue the conversation.
The Use Cases
1. Product & Catalog Search
Help users find and filter items from a structured catalog.
Industries: E-commerce, marketplace, retail, real estate, automotive, job boards
Examples:
- Online store product search with filters and facets
- Marketplace listing search (Airbnb, Etsy-style)
- Auto parts lookup by make, model, year
- Job search with location, salary, and role filters
- Real estate property search with price and amenity filters
What Elasticsearch does:
- Full-text search (BM25) for keyword matching on titles and descriptions
- Faceted filtering for price ranges, categories, brands, ratings
- Fuzzy matching for typo tolerance ("runnign shoes" still works)
- Synonyms API for domain-specific equivalents (sneakers = trainers)
- Completion suggester for search-as-you-type autocomplete
- Semantic reranking (optional) to boost results that match intent, not just words
---
2. Knowledge Base & Document Search
Let people search long-form content and find relevant passages.
Industries: SaaS, publishing, education, government, legal, healthcare
Examples:
- Internal wiki or documentation search (Confluence, Notion-style)
- Legal case law research across thousands of rulings
- Medical literature and clinical guideline search
- University course catalog and academic paper search
- Government policy and regulation search
What Elasticsearch does:
- Hybrid search (BM25 + kNN via RRF) for best of exact match + meaning
- Semantic search (dense vectors) to find relevant content even when words don't match
- Highlighting to show matching snippets in context
- Nested objects for searching within structured document sections
- Jina v3 (default EIS embedding model, multilingual) or ELSER (English-only sparse) for NLP-powered retrieval
---
3. AI-Powered Assistant / Chatbot
Build a conversational agent that answers questions using your data.
Industries: Customer support, SaaS, healthcare, financial services, education
Examples:
- "ChatGPT over your docs" — answer questions from company knowledge
- Internal IT helpdesk bot that resolves common issues
- Patient FAQ bot for healthcare providers
- Financial advisor assistant that references product documentation
- Student Q&A bot trained on course materials
What Elasticsearch does:
- RAG pipeline — retrieve relevant chunks, feed to LLM for answer generation
- Vector search (kNN) for semantically similar content retrieval
- Embedding models (Jina v3 via EIS by default, or OpenAI, Cohere, ELSER) to convert text to vectors
- LangChain / LlamaIndex integration for orchestrating retrieval + generation
- Chunking strategy guidance — how to split documents for effective retrieval
---
4. Recommendations & Discovery
Suggest relevant content users didn't explicitly search for — "you might also like".
Industries: Media, streaming, e-commerce, news, social platforms, music
Examples:
- "You might also like" product suggestions
- Related articles or blog posts
- Content personalization based on reading history
- "Customers also bought" cross-sell recommendations
- Music or video playlist suggestions based on similarity
What Elasticsearch does:
- Vector similarity (kNN) to find items "close" in embedding space
- More Like This queries to find similar documents based on content
- Semantic embeddings to represent items as vectors for comparison
- Filtering + boosting to constrain by category, recency, availability
- Script scoring to blend similarity with business rules (margin, inventory)
---
5. Customer Support Search
Help agents find solutions faster and customers help themselves.
Industries: SaaS, telecom, financial services, insurance, utilities
Examples:
- Agent assist — find similar resolved tickets to suggest resolutions
- Self-service portal — customers search for answers before filing a ticket
- Knowledge deflection — suggest articles when a user starts typing a ticket
- Escalation routing — classify and route tickets based on content
- Trend detection — surface emerging issues across support volume
What Elasticsearch does:
- Hybrid search for exact match on error codes + semantic match on symptom descriptions
- Semantic similarity to find tickets with similar problems regardless of wording
- Synonyms API for domain terminology ("can't log in" = "authentication failure" = "password issue")
- Highlighting to surface relevant resolution steps for agents
- Aggregations to detect support trends and cluster related issues
---
6. Location-Based Search
Find things near a place — stores, restaurants, properties, services.
Industries: Retail, food delivery, real estate, travel, logistics, healthcare
Examples:
- Store locator — find nearest retail locations
- "Restaurants near me" with cuisine filters
- Property search within a neighborhood or school district
- Nearest hospital or pharmacy finder
- Delivery radius calculation for logistics
What Elasticsearch does:
- Geo-point / geo-shape fields to store coordinates and boundaries
- Distance sorting to rank results by proximity to user
- Bounding box / polygon filters to search within a specific area
- Combined with full-text — "pizza near me" = geo filter + keyword search
- Geo-aggregations to cluster results on a map
---
7. Log & Event Search
Search, explore, and analyze machine-generated data.
Industries: DevOps, security operations, IoT, financial services, telecom
Examples:
- Application log search and troubleshooting
- Security event investigation (SIEM)
- IoT sensor data exploration
- Audit trail and compliance search
- Transaction monitoring and anomaly detection
What Elasticsearch does:
- Data streams for append-only, time-partitioned storage
- Index Lifecycle Management (ILM) for hot/warm/cold/frozen data tiers
- ES|QL for piped analytics queries
- Aggregations for histograms, percentiles, cardinality, and trends
- Runtime fields to extract structure from unstructured logs at query time
Note: Log and event search is typically handled by Elastic's Observability or Security solutions, which provide purpose-built UIs (Discover, Dashboards, SIEM). If the developer describes this use case, redirect them: on Hosted, they can change the solution view in Kibana; on Serverless, they should create an Observability or Security project.
---
8. Vector Database (for AI/ML Pipelines)
Store and retrieve embeddings programmatically — code searches, not people.
Industries: AI/ML companies, any organization building with LLMs, research labs
Examples:
- Embedding storage and retrieval for RAG pipelines
- Image similarity search (reverse image lookup)
- Code search across repositories by semantic meaning
- Duplicate detection across large document sets
- Anomaly detection using vector distance from normal patterns
What Elasticsearch does:
- Dense vector fields to store high-dimensional embeddings
- kNN / ANN (HNSW) for approximate nearest neighbor search at scale
- Scalar and product quantization to compress vectors for cost/performance
- LangChain / LlamaIndex vector store as a drop-in integration
- Metadata filtering to combine vector similarity with structured filters
---
Quick Reference: Use Case to Technology Map
| Use Case | Primary Tech | Optional Additions |
|---|---|---|
| Product & catalog search | Full-text (BM25), facets, fuzzy, synonyms | Semantic reranking, autocomplete |
| Knowledge base search | Hybrid (BM25 + kNN via RRF) | Highlighting, nested objects |
| AI assistant / chatbot | Vector search (kNN), RAG pipeline | LangChain/LlamaIndex, Jina Reranker, ELSER |
| Recommendations | Vector similarity (kNN), More Like This | Script scoring, behavioral signals |
| Customer support search | Hybrid search, synonyms | Aggregations for trend detection |
| Location-based search | Geo-point, distance sort, geo filters | Combined with full-text |
| Log & event search | Data streams, ILM, ES\ | QL, aggregations |
| Vector database | Dense vectors, kNN/ANN (HNSW) | Quantization, metadata filtering |
Non-Search Use Cases
If the developer describes something that isn't search, acknowledge it and redirect:
- Monitoring infrastructure or applications — That's Elastic Observability. On Hosted, change the solution view in
Kibana. On Serverless, create an Observability project. Docs
- Detecting threats or investigating security events — That's Elastic Security. On Hosted, change the solution view.
On Serverless, create a Security project. Docs
- Building dashboards and visualizations — Kibana has built-in dashboards, Lens, and Maps. Point them to Kibana's
visualization tools rather than building from scratch.
Vector & Hybrid Search Guide
UI Context Hint
If the downloaded skill file contains a # user-context: line (set by the getting started UI at download time), read it before the first message and open with a confirmation rather than a blank question:
# user-context: vector-database→ "Looks like you're building a vector database for an AI pipeline — is that right?
Are you using LangChain, LlamaIndex, or a custom stack?"
# user-context: hybrid-search→ "Looks like you're building hybrid search — is that right? Will users be typing
queries directly, or is this powering an AI pipeline?"
# user-context: semantic-search→ "Looks like you're building semantic search — is that right? Tell me about what
you're searching over."
If the developer corrects the use case, re-route immediately. No commitment.
If no # user-context: hint is present, open with: "What are you building — a search experience for users, or a retrieval backend for an AI pipeline like RAG or LangChain?"
---
Consumer Fork
Before any other decision, establish who consumes the search results:
- AI pipeline (code consumes results) → LangChain, LlamaIndex, custom RAG, agent memory, recommendations
- Human-facing search (people type queries) → search bar, results page, filters, autocomplete
This determines Decision I (App Integration) and whether to offer a frontend at the end.
---
Phase 1 — Planning & Decision-Making
Step 1.1: Define Use Case
Ask what they're building. Listen for:
| Signal | Use Case |
|---|---|
| "semantic search", "meaning-based", "natural language" | Semantic search |
| "BM25 + vector", "hybrid", "keyword and semantic" | Hybrid search |
| "RAG", "chatbot", "Q&A over documents" | RAG — use rag-chatbot skill |
| "LangChain", "LlamaIndex", "vector store", "agent memory" | AI pipeline / vector DB |
| "recommendations", "similar items" | Vector similarity |
| "image search", "multimodal" | Dense vector with image embeddings |
Scale check: If the developer indicates >1M documents, >10GB, or cost sensitivity, flag quantization early:
"With that volume, choose quantization now — it affects the mapping and requires reindexing to add later. int8_hnswis the safe default (~4x memory reduction, minimal recall impact)."
Decision A: Deployment Type
| Option | Resolves |
|---|---|
| A1: Elastic Cloud Serverless | J1 (automatic scaling), K1 (AutoOps) |
| A2: Elastic Cloud Hosted (ECH) | J2 (policy-based scaling), K1 or K2 |
| A3: Self-Managed | J3 (manual scaling), K2 or K3 |
Decision B: Embedding Strategy
Routing questions — ask first to narrow the options:
1. "Are you already generating embeddings?" → Yes → briefly offer semantic_text as alternative. If they prefer control → C2/C3 + D2, skip B. 2. "What version?" → Below 8.15 → semantic_text unavailable, skip C1 3. "Specific embedding model needed?" → Yes + not supported by inference API → C2 + D2
| Option | When to Use |
|---|---|
| B1: Built-in Models via EIS | Default for Cloud (Serverless/ECH) on 8.15+; self-managed on 9.3+ via Cloud Connect |
| B1b: Built-in on ML Nodes | Self-managed <9.3 (no Cloud Connect); or when dedicated ML node capacity preferred |
| B2: Third-Party Service | Existing model contract or specific model needed (OpenAI, Cohere, Bedrock, Azure AI, Google AI, Mistral) |
| B3: Self-Hosted Model | Custom fine-tuned models — upload via Eland, deploy on ML nodes |
Default recommendation: B1 (EIS) — no infrastructure to manage, no external API key needed.
---
Phase 2 — Data Modeling & Ingestion
Decision C: Vector Field Type
| Option | When to Use | Notes |
|---|---|---|
| C1: `semantic_text` | 8.15+, using inference endpoint, no existing vectors | Default — auto chunking, auto embedding, no ingest pipeline |
| C2: `dense_vector` | Bringing own vectors, need dims/similarity/HNSW control, pre-8.15 | Manual embedding at ingest and query time |
| C3: `sparse_vector` | ELSER manual workflow, need token weight maps | Running ELSER outside semantic_text |
C1 bypasses Decision D — semantic_text handles embedding via the bound inference endpoint. If C1, skip to Configure Chunking.
C1: semantic_text Mapping
Minimal (works out of the box on Serverless — uses the platform default model, currently Jina):
PUT /my-index
{
"mappings": {
"properties": {
"content": { "type": "semantic_text" },
"title": { "type": "text" },
"category": { "type": "keyword" },
"created_at": { "type": "date" }
}
}
}With a specific inference endpoint:
PUT /my-index
{
"mappings": {
"properties": {
"content": {
"type": "semantic_text",
"inference_id": "my-inference-endpoint"
}
}
}
}Create the inference endpoint first:
PUT _inference/text_embedding/my-inference-endpoint
{
"service": "elastic",
"service_settings": {
"model_id": "<current-eis-embedding-model-id>"
}
}**Always fetch the current model list from
EIS docs before generating this code.** Model IDs
change regularly. Jina v3 is the current default dense model for semantic_text; Jina v5-small is available forhigh-throughput / cost-sensitive workloads. ELSER remains available for English-only sparse retrieval but must be
explicitly specified — it is no longer the automatic default.
C2: dense_vector Mapping
PUT /my-index
{
"mappings": {
"properties": {
"content": { "type": "text" },
"content_embedding": {
"type": "dense_vector",
"dims": 1536,
"index": true,
"similarity": "cosine",
"index_options": {
"type": "hnsw",
"m": 16,
"ef_construction": 100
}
},
"category": { "type": "keyword" }
}
}
}Set dims to match your model output (e.g. OpenAI text-embedding-3-small = 1536, E5-small = 384, Cohere embed-v3 = 1024).
Decision D: Embedding Generation (C2/C3 only)
| Option | When to Use |
|---|---|
| D1: Inference Endpoint + Ingest Pipeline | Supported model, want server-side embedding |
| D2: Application-Side Embedding | Unsupported models, existing embedding pipeline, or need full control |
D1: Ingest Pipeline
PUT _ingest/pipeline/embedding-pipeline
{
"processors": [
{
"inference": {
"model_id": "my-inference-endpoint",
"input_output": [
{ "input_field": "content", "output_field": "content_embedding" }
]
}
}
]
}D2: Application-Side (Python)
import openai
from elasticsearch import Elasticsearch, helpers
es = Elasticsearch("https://your-cluster:443", api_key="your-api-key")
def embed(text):
return openai.embeddings.create(
model="text-embedding-3-small", input=text
).data[0].embedding
def generate_actions(docs):
for doc in docs:
yield {
"_index": "my-index",
"_source": {
"content": doc["text"],
"content_embedding": embed(doc["text"]),
"category": doc.get("category")
}
}
helpers.bulk(es, generate_actions(your_docs))Configure Chunking (C1 and D1 paths)
For semantic_text (C1), configure on the field:
"content": {
"type": "semantic_text",
"inference_id": "my-inference-endpoint",
"chunking_settings": {
"strategy": "sentence",
"max_chunk_size": 250,
"overlap": 1
}
}Strategies: sentence (default), word, recursive. Default: sentence, 250 words, 1 overlap.
For D1, chunk in application code or via a script processor before the inference processor.
Decision E: Ingestion Method
| Option | When to Use |
|---|---|
| E1: Bulk API / Client Libraries | Most cases — programmatic ingestion from any source |
| E2: File Upload (Kibana UI) | Testing and small datasets only |
Use helpers.bulk (Python) or equivalent bulk API in the developer's language. Set request_timeout=300 on first ingest to allow time for ML model loading. Use refresh="wait_for" when indexing test data.
---
Phase 3 — Search Implementation
Decision F: Search Type
| Option | When to Use |
|---|---|
| F1: Pure kNN | All queries semantic/meaning-based, no exact term matching needed |
| F2: Hybrid | Default — users search with both keywords and natural language |
| F3: Semantic | Using C1 path (semantic_text); simplest semantic search |
F3: Semantic Search (C1 path)
POST my-index/_search
{
"retriever": {
"standard": {
"query": {
"semantic": {
"field": "content",
"query": "how do I configure index mappings"
}
}
}
}
}F1: Pure kNN (C2 path)
POST my-index/_search
{
"retriever": {
"knn": {
"field": "content_embedding",
"query_vector": [0.1, 0.2, ...],
"k": 10,
"num_candidates": 100
}
}
}Tune num_candidates (higher = better recall, slower). For exact kNN on small datasets, use script_score.
F2: Hybrid Search with RRF
POST my-index/_search
{
"retriever": {
"rrf": {
"retrievers": [
{
"standard": {
"query": {
"multi_match": {
"query": "elasticsearch index mapping",
"fields": ["title^2", "content"]
}
}
}
},
{
"knn": {
"field": "content_embedding",
"query_vector": [0.1, 0.2, ...],
"k": 50,
"num_candidates": 100
}
}
],
"window_size": 100,
"rank_constant": 60
}
}
}Tuning RRF:
window_size: Docs considered from each retriever. Higher = more semantic influence when BM25 is sparse.rank_constant: Higher = flatter rank contribution. Lower = steeper top-rank preference.
For filtered hybrid search, add filter clauses to both the standard (via bool.filter) and knn retrievers.
Decision G: Reranking
| Option | When to Use |
|---|---|
| G1: No Reranking | Default — start here, add G2 if relevance isn't good enough |
| G2: Semantic Reranker | Relevance quality > latency; adds ~50-200ms |
| G3: Learning to Rank | Advanced — requires labeled query/document pairs |
| G4: Query Rules | Merchandising, editorial control, compliance filtering |
G2: Semantic Reranker
POST my-index/_search
{
"retriever": {
"text_similarity_reranker": {
"retriever": {
"rrf": {
"retrievers": [
{ "standard": { "query": { "multi_match": { "query": "your query", "fields": ["content"] } } } },
{ "knn": { "field": "content_embedding", "query_vector": [...], "k": 50, "num_candidates": 100 } }
]
}
},
"field": "content",
"inference_id": "my-reranker-endpoint",
"inference_text": "your query",
"rank_window_size": 50
}
}
}EIS provides managed rerankers (currently Jina Reranker v2 and v3). Check
reranker docs for current model IDs and
inference endpoint setup.
G4: Query Rules
PUT _query_rules/my-ruleset
{
"rules": [
{
"rule_id": "pin-featured",
"type": "pinned",
"criteria": [{ "type": "contains", "metadata": "query_string", "values": ["featured"] }],
"actions": { "ids": ["doc-123"] }
}
]
}Decision H: Query Method
Always use Retrievers API for vector and hybrid search (all examples above use it). Use Query DSL for pure keyword search. ES|QL is for analytics and data exploration only, not vector retrieval.
Decision I: App Integration
AI pipeline: Use the direct API via a client library, or use Elastic Agent Builder / Playground for LLM integration.
Human-facing: Use the direct API, Search Templates for parameterized server-side queries, or Search UI — see the search-ui skill.
LangChain Integration
pip install langchain-elasticsearch langchain-openaifrom langchain_elasticsearch import ElasticsearchStore
from langchain_openai import OpenAIEmbeddings
from elasticsearch import Elasticsearch
es_client = Elasticsearch(
"https://your-cluster.es.us-central1.gcp.elastic.cloud:443",
api_key="your-api-key"
)
vector_store = ElasticsearchStore(
es_connection=es_client,
index_name="my_docs",
embedding=OpenAIEmbeddings(model="text-embedding-3-small"),
)
vector_store.add_documents([
{"page_content": "Elasticsearch is a distributed search engine.", "metadata": {"source": "docs"}},
])
results = vector_store.similarity_search("How do I visualize data?", k=3)Use vector_store.as_retriever() in a LangChain chain for RAG.
LlamaIndex Integration
pip install llama-index llama-index-vector-stores-elasticsearch — use ElasticsearchVectorStore with es_url and es_api_key params. Wrap in VectorStoreIndex.from_documents() and query via .as_query_engine(similarity_top_k=5).
Search API Endpoint (Human-facing)
Build a /search endpoint using the hybrid RRF pattern from F2, wrapping it in the developer's framework (Flask, Express, Spring, etc.). Always include pagination — from/size for up to 10,000 results, search_after with PIT for deeper pagination.
---
Phase 4 — Production & Optimization
Step 4.0: Performance Requirements
Skip for Serverless (auto-managed). For ECH / Self-Managed, ask about peak QPS, traffic spikiness, and latency targets.
| Requirement | Configuration Lever |
|---|---|
| High QPS | More replicas; more shards for large indices |
| Spiky traffic | Autoscaling deciders on ECH; pre-warm cache after force-merge |
| Strict latency | Lower num_candidates; quantization; fewer shards for small-medium indices |
| Max recall | Higher num_candidates; hnsw (no quantization); exact kNN for small datasets |
Serverless: Only num_candidates is tunable for the recall/latency tradeoff.
Step 4.1: Performance Tuning
Quantization — reduces vector memory footprint:
"content_embedding": {
"type": "dense_vector",
"dims": 1536,
"index": true,
"similarity": "cosine",
"index_options": {
"type": "int8_hnsw"
}
}| Type | Memory Reduction | Recall Impact |
|---|---|---|
hnsw | Baseline | Baseline |
int8_hnsw | ~4x | Minimal |
int4_hnsw | ~8x | Small |
bbq_hnsw | ~32x | Moderate — test with your data |
Use bbq_hnsw when memory is the constraint and you can tolerate slightly lower recall. Use int8_hnsw as the safe default.
Post-ingestion: Force-merge segments (max_num_segments=1), then clear cache and run warm-up queries.
Step 4.2: Shard Sizing
- Target 10–50 GB per shard, max 200M docs per shard
- Use ILM for rollover on time-series data:
PUT _ilm/policy/vector-rollover
{
"policy": {
"phases": {
"hot": {
"actions": {
"rollover": { "max_size": "50gb", "max_docs": 200000000 }
}
}
}
}
}Decision J: Scaling Strategy
Resolved from Decision A: Serverless → automatic; ECH → policy-based autoscaling + adaptive allocations; Self-Managed → manual provisioning (K8s HPA for ECK).
Decision K: Monitoring
Resolved from Decision A: Cloud → AutoOps (auto-enabled, recommendations and alerts); Self-Managed → Stack Monitoring (Metricbeat/Filebeat/Kibana) or external (Prometheus, Grafana, Datadog).
---
Phase 5 — Iteration & Continuous Improvement
Step 5.1: Evaluate Search Quality
Ranking Evaluation API:
POST my-index/_rank_eval
{
"requests": [
{
"id": "query_1",
"request": {
"query": { "multi_match": { "query": "elasticsearch mapping", "fields": ["content"] } }
},
"ratings": [
{ "_index": "my-index", "_id": "doc-1", "rating": 3 },
{ "_index": "my-index", "_id": "doc-2", "rating": 1 }
]
}
],
"metric": { "ndcg": { "k": 10 } }
}Use "profile": true in search requests to diagnose latency.
Step 5.2: Refine Pipeline
Work through these levers in order:
| Lever | What It Fixes |
|---|---|
| Swap embedding model | Poor semantic recall — wrong language, domain mismatch |
| Adjust chunking strategy/size | Chunks too large (noisy) or too small (missing context) |
Tune window_size, rank_constant | BM25 or semantic dominating when it shouldn't |
| Add reranking (G2) | Top results semantically close but not the best answer |
| Add query rules (G4) | Specific queries need editorial override |
| Try quantization level | Memory pressure or latency too high |
Step 5.3: Extend to RAG
If retrieval quality is acceptable and the developer wants generated answers, add an LLM layer. The pattern: retrieve top-k chunks using the hybrid RRF query from F2, concatenate chunk text into a context string, pass to an LLM with a grounding system prompt ("Answer based only on the provided context"), and return the answer with source references.
For Elastic-native RAG without external LLM keys, see Agent Builder and Playground. For full RAG implementation details, use the rag-chatbot skill.
---
Metadata Filtering Patterns
Store tenant_id, user_ids, groups as keyword fields. Filter at query time with bool.filter using term / terms clauses inside both the standard and knn retrievers. For large-scale multi-tenancy, use separate indices per tenant instead of row-level filtering.
---
Common Follow-ups
| Question | Answer |
|---|---|
| "Results aren't relevant enough" | Run _rank_eval, then work through Step 5.2 levers |
| "Results are too semantic / too keyword-heavy" | Tune window_size — higher favors semantic, lower favors BM25 |
| "Memory is too high" | Add quantization to dense_vector mapping, reindex |
When to Use Other Skills
| Situation | Skill |
|---|---|
| Pure keyword search, no vectors needed | keyword-search |
| RAG / Q&A chatbot with LLM answer generation | rag-chatbot |
| React search frontend | search-ui |
| Product catalog with facets and merchandising | catalog-ecommerce |
Related skills
FAQ
What does elasticsearch-onboarding do?
elasticsearch-onboarding skill documents Help developers new to Elasticsearch get from zero to a working search experience.
When should I use elasticsearch-onboarding?
User asks about elasticsearch-onboarding, help developers new to elasticsearch get from zero to a working search experience. guide t.
Is this skill safe to install?
Review the Security Audits panel on this page before installing in production.