This document defines the principles and workflow for creating and maintaining code examples in this repository.
You need the following installed locally:
- Python 3.x — runs the seed script, Python tests, and the spec directory generator
- Node.js 18+ and npm — runs JavaScript tests
- Docker — runs Ruby, PHP, and Java tests (via language-specific Docker images)
- Go 1.21+ — runs Go tests locally (no Docker)
- .NET 8 SDK — runs C# tests locally (no Docker)
After cloning, create the Python virtual environment and install JavaScript dependencies:
python3 -m venv .venv
source .venv/bin/activate
pip install elasticsearch==9.3.0
npm installThe Go, Ruby, PHP, and Java environments are handled automatically by
their test runner scripts (see Testing workflow below). Local artifacts
like go.mod, composer.json, and build output directories are
gitignored.
-
schema.json— the Elasticsearch request/response specification. This is the canonical list of endpoints. Each endpoint has aname, and anavailabilityobject withstackand/orserverlessentries, each containing an optionalvisibilityfield. This file is not checked in. You must provide it locally, e.g. by symlinking to a checkout of the elasticsearch-specification repository:ln -s /path/to/elasticsearch-specification/output/schema/schema.json schema.json
-
elastic-start-local/— a local Elasticsearch server managed by theelastic-start-localtool. Contains a.envfile with connection details. Key variables:ES_LOCAL_URL— server URL (default:http://localhost:9200)ES_LOCAL_PORT— HTTP portES_LOCAL_PASSWORD— password for theelasticuserES_LOCAL_API_KEY— base64-encoded API key
This directory is not checked in. You must provide it locally, e.g. by symlinking to an existing
elastic-start-localinstallation:ln -s /path/to/elastic-start-local elastic-start-local
Before testing, confirm Elasticsearch is reachable:
source elastic-start-local/.env
curl -s -u "elastic:$ES_LOCAL_PASSWORD" "$ES_LOCAL_URL"If the server is not running, start it from the elastic-start-local/
directory following its own README. The server runs in Docker.
Use the official Elasticsearch client docs to confirm idiomatic usage:
- Python: https://elasticsearch-py.readthedocs.io/
- JavaScript: https://www.elastic.co/docs/reference/elasticsearch/clients/javascript
- Ruby: https://www.elastic.co/docs/reference/elasticsearch/clients/ruby
- PHP: https://www.elastic.co/docs/reference/elasticsearch/clients/php
- Go: https://www.elastic.co/docs/reference/elasticsearch/clients/go
- Java: https://www.elastic.co/docs/reference/elasticsearch/clients/java
- .NET: https://www.elastic.co/docs/reference/elasticsearch/clients/dotnet
An endpoint is public if its stack visibility is "public" or
unset (public is the default). Only public endpoints get directories
under examples/.
Endpoints with stack.visibility set to "private" or "feature_flag"
are excluded. Serverless visibility is not considered.
To regenerate the directory structure from the spec:
import json, os, shutil
with open("schema.json") as f:
data = json.load(f)
for ep in data["endpoints"]:
vis = ep.get("availability", {}).get("stack", {}).get("visibility")
name = ep["name"]
path = os.path.join("examples", name)
if vis and vis != "public":
if os.path.isdir(path):
shutil.rmtree(path)
else:
os.makedirs(path, exist_ok=True)Some REST endpoints form a lifecycle pair (open/close, start/stop). When the close/clear/stop half only makes sense in context of the open/start half, collapse them into a single example directory named after the concept, not the verb:
| REST endpoints | Example directory |
|---|---|
open_point_in_time + close_point_in_time |
point_in_time |
scroll + clear_scroll |
scroll |
async_search.submit/get/status/delete |
async_search |
The endpoint field in front matter uses the directory name (e.g.
point_in_time), not the individual REST endpoint names.
{endpoint}-{es_version}-{language}.md
The endpoint name, Elasticsearch version, and language appear in that order so files sort naturally by endpoint, then version, then language.
Every file must start with YAML front matter containing exactly these four fields:
---
endpoint: bulk
lang: python
es_version: "9.3"
client: elasticsearch==9.3.0
---endpoint— must match the directory name (usually the spec'snamefield, but may differ for paired endpoints; see above)lang— lowercase language identifieres_version— quoted major.minor stringclient— package name and version as published to the language's registry (PyPI, npm, RubyGems, Packagist, Go modules)
The valid lang and client values for the current seven languages:
lang |
client |
|---|---|
python |
elasticsearch==9.3.0 |
javascript |
"@elastic/elasticsearch@9.3.0" |
ruby |
elasticsearch==9.3.0 |
php |
elasticsearch/elasticsearch==9.3.0 |
go |
github.com/elastic/go-elasticsearch/v9 |
java |
co.elastic.clients:elasticsearch-java:9.3.0 |
dotnet |
Elastic.Clients.Elasticsearch==9.3.0 |
# Elasticsearch {version} {endpoint} endpoint ({Language} example)
The {endpoint} in the title must match the directory name, not the
REST endpoint name. For paired endpoints this means using the concept
name: point_in_time, not open_point_in_time.
Every file begins with a main example, followed by additional sections that are consistent across all languages for a given endpoint. The sections should reflect the most important concerns for that endpoint.
-
Main example (no heading) — a complete, working snippet showing the primary use case. Preceded by a one- or two-sentence description of what API or helper is being used. Followed by brief prose noting any optional fields or key design choices.
-
Additional sections — two or three
##-level sections covering secondary concerns. These must use the same headings across all language variants of the same endpoint. Choose headings that fit the endpoint naturally.
For the bulk endpoint, the sections are:
## Handling errors## Large datasets
Other endpoints will have different sections. For example, search
might use ## Pagination and ## Filtering and aggregations. When
creating examples for a new endpoint, define the sections with the
first language file, then replicate them consistently.
Examples are consumed by developers who already have an application and a connected Elasticsearch client. They need a snippet to integrate, not a tutorial.
- Assume connectivity is established. Reference
clientas an already-connected instance. Do not show connection setup. - Minimal boilerplate. No unnecessary imports, no wrapper functions, no main entrypoints unless the language requires it (e.g., Go).
- Real-world data. Use realistic field names and values (e.g., a
product catalog), not placeholder data like
{"foo": "bar"}. - Screen-sized. Each file should fit roughly on one screen when rendered as HTML. Some may go longer; that's fine.
-
Idiomatic first, consistent second. A developer immersed in a given language's ecosystem should find the code completely natural. Idiomatic style for the target language always takes priority over cross-language consistency. Specifically:
-
Static languages must use typed models. Java, Go, and C# code must define proper types (records, structs, classes) for documents. Never use untyped maps or dictionaries (e.g.,
Map<String, Object>in Java) to hold document data — that is a dynamic-language pattern and looks alien in static-language code. If you find yourself reaching for a map/dictionary where the language has type definitions, you are writing Python in another language's syntax. -
Use each language's native idioms. Java uses records and the builder/fluent pattern. Go uses structs with JSON tags and explicit error handling. C# uses records,
async/await, pattern matching, and LINQ where appropriate. Ruby uses symbols and blocks. PHP uses associative arrays (which are idiomatic for PHP, unlike Java maps). -
Use the right helper for each client. Use
helpers.bulkin Python,client.helpers.bulkin JavaScript,esutil.BulkIndexerin Go,BulkIngesterin Java,BulkAllin .NET. Use the standard API when the client has no higher-level abstraction (e.g., Ruby, PHP).
A good test: show the snippet to a practitioner of that language. If they would rewrite it before putting it in their codebase, the example has failed.
-
-
Consistent across languages where it doesn't conflict with idiom. Use the same sample data, the same index name (
"products"), and the same document IDs ("prod-1", etc.) wherever possible. Section names, front matter fields, and prose structure must match across all language variants. But never sacrifice idiomatic code for the sake of making two files look similar. -
Tested. Every code snippet in the main example and error handling sections must be runnable against the local Elasticsearch server. Test before committing. For languages not installed locally, use Docker (see Testing workflow below).
These conventions are already established in the existing examples. Follow them so new files don't drift.
| Language | Quotes | Semicolons | Client variable | Notes |
|---|---|---|---|---|
| Python | Double | N/A | client |
|
| JavaScript | Double | Yes | client |
async/await, const over let |
| Ruby | Single (double for #{}) |
N/A | client |
Symbols for keys, puts over print |
| PHP | Single (double for $var) |
Yes | $client |
echo with \n |
| Go | Double (Go standard) | N/A | client |
Typed API, pointer helpers, log.Fatalf for errors |
| Java | Double (Java standard) | Yes | client |
Records for documents, var for locals, fluent builders |
| .NET | Double (C# standard) | Yes | client |
Records for documents, async/await, Console.WriteLine |
Embedded syntax and quote flipping. Strings containing JSON, Mustache templates, EQL, or ES|QL often embed double quotes as part of their content. In Python, Ruby, and PHP, use the other quote style to avoid escaping:
# Python: single quotes because the JSON content contains double quotes
source='{"query": {"match": {"{{field}}": "{{value}}"}}}'# Ruby: single quotes are already the default, so JSON content is natural
source: '{"query": {"match": {"{{field}}": "{{value}}"}}}'// PHP: single quotes are the default; JSON content contains double quotes
'source' => '{"query": {"match": {"{{field}}": "{{value}}"}}}'When the string has no embedded quotes, use the default for the language. The key rule: never escape quotes when flipping avoids it.
For PHP echo with concatenation, keep literal text in single quotes
and only use double quotes for escape sequences like "\n":
echo 'Quality score: ' . round($score, 3) . "\n";The examples/_connection/ directory is a special case. It is the
only example that intentionally shows connection setup — client
instantiation, authentication, and connection options. It also
includes instructions for installing the client library in each
language. Its title uses "connection management" instead of "endpoint":
# Elasticsearch 9.3 connection management ({Language} example)
All other examples assume client is already connected and must not
include setup code.
Each endpoint directory contains a README.md that lists and links
to all language variants. When adding a new endpoint or a new
language, update the relevant README.md files to include the new
entries.
- Do not include connection setup or client instantiation (except in
_connection). - Do not add comments that narrate what the code does.
- Do not use toy data or meaningless field names.
- Do not show
refreshor other operational parameters in the main example unless the endpoint specifically requires them. - Keep section structure parallel across languages for a given endpoint. The exception is client helper sections (e.g. "Using helpers" in scroll) — these only appear in languages whose client provides a higher-level abstraction and may be absent from others.
Every language has a test runner script in scripts/ that handles
credentials, virtual environments, and Docker boilerplate:
scripts/test-python.sh my_test.py
scripts/test-javascript.sh my_test.js
scripts/test-ruby.sh my_test.rb # Docker: ruby:3.3
scripts/test-php.sh my_test.php # Docker: composer:latest
scripts/test-go.sh my_test.go # local Go, temp module
scripts/test-java.sh MyTest.java # Docker: maven:3.9-eclipse-temurin-17
scripts/test-dotnet.sh Program.cs # local .NET SDK, temp projectBefore testing, seed the standard product data:
source .venv/bin/activate
python scripts/seed.py # seed 4 products into 'products' index
python scripts/seed.py --clean # delete all test indicesThis creates the products index with 4 documents (prod-1 through
prod-4) that all examples use. Run it before each test session. The
script reads credentials from elastic-start-local/.env automatically.
The product schema has these fields — reference them consistently in all examples:
| ID | name | brand | price | category | in_stock | rating |
|---|---|---|---|---|---|---|
prod-1 |
Espresso Machine Pro | BrewMaster | 899.99 | appliances | true | 4.7 |
prod-2 |
Noise-Cancelling Headphones | SoundCore | 249.00 | electronics | true | 4.5 |
prod-3 |
Ergonomic Standing Desk | DeskCraft | 599.00 | furniture | false | 4.8 |
prod-4 |
4K Webcam with Mic | StreamGear | 129.99 | electronics | true | 4.3 |
A reusable pom.xml lives in testing/java/. The test-java.sh
script uses it automatically. For manual testing, see
testing/java/README.md.
After creating or editing example files:
- Test the main example code against the running server
- Test the error handling code against the running server
- Verify front matter has exactly four fields with correct values
- Verify the title follows the format
- Verify section headings match across all languages for the endpoint
- Compare structure against existing examples in other languages
The Elasticsearch v9 clients vary significantly in their API surface. This section documents the tested, correct patterns for each language. These patterns were verified against Elasticsearch 9.3 and should be the starting point for all new examples.
Straightforward. Methods match endpoint names. Bodies are plain dicts.
from elasticsearch import Elasticsearch
client = Elasticsearch("http://localhost:9200", basic_auth=("elastic", "password"))
# CRUD
client.index(index="products", id="prod-1", document={...})
client.get(index="products", id="prod-1")
client.delete(index="products", id="prod-1")
client.update(index="products", id="prod-1", script={...})
# Multi-document
client.mget(index="products", ids=["prod-1", "prod-2"])
client.delete_by_query(index="products", query={...})
client.update_by_query(index="products", query={...}, script={...})
client.reindex(source={"index": "src"}, dest={"index": "dst"})
# Source filtering
client.mget(index="products", ids=[...], source_includes=["name", "price"])Async methods. camelCase names. Plain objects for bodies.
const { Client } = require('@elastic/elasticsearch')
const client = new Client({ node: 'http://localhost:9200', auth: {...} })
// CRUD
await client.index({ index: 'products', id: 'prod-1', document: {...} })
await client.get({ index: 'products', id: 'prod-1' })
await client.delete({ index: 'products', id: 'prod-1' })
// Multi-document
await client.mget({ index: 'products', ids: [...] })
await client.deleteByQuery({ index: 'products', query: {...} })
await client.updateByQuery({ index: 'products', query: {...}, script: {...} })
await client.reindex({ source: { index: 'src' }, dest: { index: 'dst' } })Symbol keys for parameters. String keys in response hashes. Body goes
in body: parameter.
client = Elasticsearch::Client.new(url: "...", user: "...", password: "...")
# CRUD
client.index(index: 'products', id: 'prod-1', body: {...})
client.get(index: 'products', id: 'prod-1')
# Multi-document — note body wrapping
client.mget(index: 'products', body: { ids: [...] })
client.delete_by_query(index: 'products', body: { query: {...} })
client.update_by_query(index: 'products', body: { query: {...}, script: {...} })
client.reindex(body: { source: { index: 'src' }, dest: { index: 'dst' } })Associative arrays for everything. Method names are camelCase.
$client = ClientBuilder::create()->setHosts([...])->build();
// CRUD
$client->index(['index' => 'products', 'id' => 'prod-1', 'body' => [...]]);
$client->get(['index' => 'products', 'id' => 'prod-1']);
// Multi-document
$client->mget(['index' => 'products', 'body' => ['ids' => [...]]]);
$client->deleteByQuery(['index' => 'products', 'body' => ['query' => [...]]]);
$client->reindex(['body' => ['source' => [...], 'dest' => [...]]]);
// Empty objects: use new \stdClass() for match_all etc.
$client->deleteByQuery([..., 'body' => ['query' => ['match_all' => new \stdClass()]]]);Uses the typed client with builder-style chaining. Pointer-heavy. Import paths are deep.
import (
"github.com/elastic/go-elasticsearch/v9"
"github.com/elastic/go-elasticsearch/v9/typedapi/core/mget"
"github.com/elastic/go-elasticsearch/v9/typedapi/types"
"github.com/elastic/go-elasticsearch/v9/typedapi/types/enums/conflicts"
)
client, _ := elasticsearch.NewTypedClient(elasticsearch.Config{...})
// CRUD
client.Index("products").Id("prod-1").Request(nil).Body(product).Do(ctx)
client.Get("products", "prod-1").Do(ctx)
client.Delete("products", "prod-1").Do(ctx)
// Multi-document
client.Mget().Index("products").Request(&mget.Request{
Ids: []string{"prod-1", "prod-2"},
}).Do(ctx)
// Queries use maps
query := &types.Query{
Term: map[string]types.TermQuery{
"category": {Value: "electronics"},
},
}
// Script params: use json.RawMessage, NOT map[string]interface{}
import "encoding/json"
params := map[string]json.RawMessage{
"discount": toRawMessage(0.9),
}
func toRawMessage(v interface{}) json.RawMessage {
b, _ := json.Marshal(v)
return b
}
// Float64 pointers (for range queries etc.)
lt := types.Float64(4.0)
types.NumberRangeQuery{Lt: <}
// Conflicts enum
client.DeleteByQuery("products").Conflicts(conflicts.Proceed)Fluent builder pattern. Records for typed documents. The v9 client API is significantly different from v7/v8.
// Connection — v9 uses ElasticsearchClient.of(), NOT RestClientTransport
var client = ElasticsearchClient.of(b -> b
.host("http://localhost:9200")
.usernameAndPassword("elastic", "password")
);
// Typed documents — use records, not Map<String, Object>
public record Product(String name, double price, String category) {}
// CRUD
client.index(i -> i.index("products").id("prod-1").document(product));
client.get(g -> g.index("products").id("prod-1"), Product.class);
client.delete(d -> d.index("products").id("prod-1"));
// Multi-document
client.mget(m -> m.index("products").ids(List.of("prod-1", "prod-2")), Product.class);
client.deleteByQuery(d -> d
.index("products")
.query(q -> q.term(t -> t.field("category").value("electronics")))
);
client.reindex(r -> r
.source(s -> s.index("products"))
.dest(d -> d.index("products-v2"))
);
// Scripts — v9 uses .source(src -> src.scriptString(...)), NOT .inline(...)
client.updateByQuery(u -> u
.index("products")
.query(q -> q.term(t -> t.field("category").value("electronics")))
.script(s -> s
.source(src -> src.scriptString("ctx._source.price *= params.discount"))
.params("discount", JsonData.of(0.9))
)
);Async/await. Fluent lambda builders. Records for typed documents.
// Connection
var client = new ElasticsearchClient(new ElasticsearchClientSettings(
new Uri("http://localhost:9200"))
.Authentication(new BasicAuthentication("elastic", "password"))
);
// Typed documents
public record Product(string Name, double Price, string Category);
// CRUD
await client.IndexAsync(product, i => i.Index("products").Id("prod-1"));
await client.GetAsync<Product>("prod-1", g => g.Index("products"));
await client.DeleteAsync("products", "prod-1");
// Multi-document
await client.MultiGetAsync<Product>(m => m
.Index("products")
.Ids(new Ids(new[] { "prod-1", "prod-2" }))
);
await client.DeleteByQueryAsync<object>("products", d => d
.Query(q => q.Term(t => t.Field("category").Value("electronics")))
);
await client.ReindexAsync(r => r
.Source(s => s.Index("products"))
.Dest(d => d.Index("products-v2"))
);
// Scripts
await client.UpdateByQueryAsync("products", u => u
.Query(q => q.Term(t => t.Field("category").Value("electronics")))
.Script(s => s
.Source("ctx._source.price *= params.discount")
.Params(p => p.Add("discount", 0.9))
)
);Hard-won lessons from building and testing examples across all seven languages. Read this before starting work.
- v9 client connection is completely different from v7/v8. Do not
use
RestClientTransport,RestHighLevelClient, or Apache HTTP client classes. The v9 pattern isElasticsearchClient.of(b -> b .host(...).usernameAndPassword(...)). - Scripts and templates use
.source(src -> src.scriptString(...)), not.source("string"). Thesource()method on Script, SearchTemplate, and RenderSearchTemplate builders takes aScriptSource, not a raw string. This is the most common compilation error. - The Maven artifact is
co.elastic.clients:elasticsearch-java. The latest 9.x version on Maven Central at time of writing is9.0.2(the version tracks the client library, not the server). - Java records need
@JsonPropertyfor snake_case fields. The seed data storesin_stockas snake_case, but Java records useinStock. Add@JsonProperty("in_stock")fromcom.fasterxml.jackson.annotation(needsjackson-annotationsas a compile dependency; already in the testingpom.xml). - Java tests via Docker are slow (~60s for compile + run). Budget
time accordingly. Use
timeout 120in CI to avoid hangs. - Java ES|QL uses
ObjectsEsqlAdapterandResultSetEsqlAdapter, not the rawquery()method. The raw method returns aBinaryResponse. TheObjectsEsqlAdapterlives atco.elastic.clients.elasticsearch._helpers.esql.objects. UseKEEPin ES|QL queries to avoid.keywordsubfields that won't map to record fields. - Some response objects implement
Mapdirectly. For example,GetAliasResponseandGetMappingResponseare themselvesMap<String, ...>— iterate withresponse.forEach(...)or access entries withresponse.get("indexName"). They do not have a.result()method.
- Script params must be
map[string]json.RawMessage, notmap[string]interface{}. This is a compile-time error that is easy to miss. Define a helper:func toRawMessage(v interface{}) json.RawMessage { b, _ := json.Marshal(v) return b }
types.Float64is a named type, not a pointer. For range queries, you need<wherelt := types.Float64(4.0). Writingtypes.Float64(4.0)directly where*types.Float64is expected will not compile.- Import paths are deep. Every typed API request type has its own
package:
typedapi/core/mget,typedapi/core/deletebyquery, etc. Enums likeconflicts.Proceedlive undertypedapi/types/enums/conflicts. Hit.Id_is*stringin search results. Dereference with*hit.Id_. Note thatGetResult.Id_(from mget) is plainstring.- The
Scroll()method onScrolltakesDurationVariant, not a string. UseRequest(&scroll.Request{ScrollId: id, Scroll: "1m"})instead of method chaining for the scroll follow-up call. - The typed API sometimes requires pointer fields. Use small helper
functions like
strPtrandintPtrrather than inline address-of expressions. - Methods like
Metric()take a single comma-separated string, not variadic arguments. WriteMetric("docs,store"), notMetric("docs", "store"). PutSettings()does not have anIndex()method on the builder chain. Pass the index viaRequest()or use the appropriate endpoint path.
- Method names don't always match the REST endpoint.
mgetisMultiGetAsync,delete_by_queryisDeleteByQueryAsync, etc. Idstakes a specific type, not a plain string array: usenew Ids(new[] { "prod-1", "prod-2" }).- Response property names don't follow a single pattern. The
property holding the main result varies by endpoint:
GetAliasResponse.Aliases,GetMappingResponse.Mappings,GetIndicesSettingsResponse.Settings,GetFieldMappingResponse.FieldMappings. Do not assume.Indicesor.Result— check the actual type. - Some method parameters require specific enum types. For example,
IndicesStatsRequest.Metric()takesCommonStatsFlagenum values (CommonStatsFlag.Docs,CommonStatsFlag.Store), not strings.ExistsAliasAsynctakes aNamesargument directly, not a lambda descriptor. Always check the method signature.
- Empty query objects (like
match_all) requirenew \stdClass(). Using an empty array[]serializes to[]in JSON, not{}.
formatin ES|QL is a query parameter, not a body field. Passformat: 'json'at the top level, not insidebody:. Python and JavaScript accept it in either position; Ruby and PHP require it as a query parameter.
- The client prints a warning ("unable to verify that the server is Elasticsearch") when connecting to a local server without the product header. This is cosmetic and can be ignored during testing.
- Query bodies go inside
body:, unlike Python/JS where they are top-level parameters.
- Always clean up test indices after tests. The test data seeder
(
scripts/seed.py --clean) handles the standard indices, but if your test creates additional indices (e.g.,products-v2,electronics), delete them explicitly. - Remember to
refreshbetween write and read operations in tests. Elasticsearch is near-real-time; a document indexed without a refresh may not appear in subsequent searches within the same test.
When choosing which endpoints to cover next, prefer this order:
- Core document APIs —
index,get,delete,update,mget,reindex(the CRUD fundamentals) - Search APIs —
search,msearch,scroll,count,field_caps(the most-used read path) - Index management —
indices.create,indices.delete,indices.get,indices.put_mapping,indices.put_settings - Aggregations and analytics — endpoints commonly used for dashboards and reporting
- Cluster and node APIs —
cluster.health,cluster.stats,cat.* - Everything else — remaining public endpoints
Within each group, complete all seven languages for one endpoint before moving to the next endpoint. This ensures every covered endpoint has full language coverage.
- Install or Docker-enable the language runtime
- Install the official Elasticsearch client for that language
- Add a test runner script to
scripts/ - Write the example following the conventions above
- Test all snippets against the running server
- Add the language to the table in
README.md - Update each endpoint's
README.mdto include the new language
- Create the example file in the existing
examples/{endpoint}/directory - Start with one language, test it, then replicate across all supported languages
- Choose realistic sample data that makes sense for the endpoint
- Follow the section structure conventions
- Create a
README.mdin the endpoint directory linking to all language variants
- Create new files alongside existing ones (e.g.,
bulk-9.4-python.mdnext tobulk-9.3-python.md) - Update code if the client API has changed
- Test against a server running the new version
To see which endpoints have examples and which are still empty:
for dir in examples/*/; do
count=$(find "$dir" -name "*.md" | wc -l)
if [ "$count" -gt 0 ]; then
echo " [done] $(basename "$dir") ($count files)"
fi
done
echo ""
echo "Endpoints without examples:"
for dir in examples/*/; do
count=$(find "$dir" -name "*.md" | wc -l)
if [ "$count" -eq 0 ]; then
echo " $(basename "$dir")"
fi
done | head -20
echo " ..."