Spunto Searchl’API Elasticsearch, en petit

L’API d’Elasticsearch,
en 4,3 mégaoctets.

C’est ce que télécharge un docker pull — 10,2 Mo une fois décompressée sur le disque — contre 669,1 Mo pour Elasticsearch 8.15.0, mesurés sur la même machine le 2 septembre 2026. Elasticsearch est excellent et, pour l’essentiel de ce qu’on lui fait faire, énorme : une JVM, 1,08 Go de mémoire au repos et 18,1 s de démarrage, pour indexer quelques centaines de milliers de documents et répondre à un bool suivi d’un tri. Spunto Search parle la même API HTTP — votre code client ne change pas d’une ligne — depuis un binaire Rust statique, dans un conteneur par ailleurs vide.

API ES 8.154,3 Mo à téléchargerrépond 172 ms après docker runun seul nœud
une commande · votre machine
$ curl -sSL https://github.com/spuntodotnet/search/releases/download/v0.7.0/ferrite-v0.7.0-x86_64-unknown-linux-musl.tar.gz | tar xz && ./ferrite

binaire musl statique, x86-64 ou arm64 — puis pointez un client Elasticsearch sur :9200

01 — Ce que vous obtenez

Votre client ne voit pas la différence.

Pas un nouveau SDK, pas un dialecte « en grande partie compatible » : les bibliothèques officielles d’Elasticsearch s’y connectent, la poignée de main qu’elles font à la connexion réussit, et les requêtes que vous envoyez déjà reviennent dans la forme qu’elles savent déjà lire.

L’index inversé lui-même n’est pas réinventé — c’est Tantivy, l’équivalent Rust de Lucene. Le travail est la couche de compatibilité au-dessus, et elle est vérifiée plutôt qu’affirmée : la suite de tests REST d’Elastic elle-même est rejouée contre un build qui tourne, cas par cas, et ce qu’elle a trouvé est le chiffre ci-dessous — ni un objectif, ni une promesse.

index_and_search.py
from elasticsearch import Elasticsearch   # the official client, unmodified

es = Elasticsearch("http://localhost:9200")

es.indices.create(index="books", mappings={"properties": {
    "title": {"type": "text"}, "author": {"type": "keyword"},
    "year": {"type": "integer"}}})

es.index(index="books", id="1", refresh=True,
         document={"title": "Bel-Ami", "author": "Maupassant", "year": 1885})

es.search(index="books", query={"match": {"title": "bel ami"}})
# {'took': 3, 'hits': {'total': {'value': 1, 'relation': 'eq'}, ...

conformité mesurée

mesurée le 1er septembre 2026 · il y a 58 jours

vieux de plus d’un mois

Cette mesure date de 58 jours — au-delà des 30 jours après lesquels cette page cesse de la présenter comme l’état actuel. Lisez-la comme un instantané pris le 1er septembre 2026, et consultez le rapport pour savoir où on en est.

82,7 %

172 / 208 cas — fidélité dans le périmètre déclaré

Parmi les cas qui portent sur quelque chose que cette page affirme, la part qui répond exactement comme Elasticsearch : 172 sur 208. Les cas qui échouent sur une capacité que le dépôt déclare refusée sortent de ce dénominateur — c’est le prix du périmètre, et la liste ci-dessous les nomme tous.

14,7 %

172 / 1 173 cas — de la suite d’Elasticsearch elle-même

Les mêmes 172 cas, rapportés à toute la suite que publie Elastic : 1 173. C’est la part d’Elasticsearch couverte tout court — une fraction, et le chiffre qui garde le premier honnête.

ce qui a tourné
La suite REST d’Elastic elle-même, version 7.10.2 (Apache-2.0), rejouée sans modification contre un build qui tourne — 1 173 cas répartis en 107 suites. Pas des tests écrits ici pour passer ici.
contre
ferrite-0.10.0, qui annonce la version d’API 8.15.0, mesuré le 1er septembre 2026.
déclaré
215 capacités dans compat.yaml : 103 supportées, 68 partielles, 44 refusées — les deux listes de cette page en sont générées.
rapport brut
docs/conformance.json — chaque cas, son verdict et sa raison. docs/compat.md en est l’équivalent pour les capacités.

dans le périmètre

Généré depuis le compat.yaml du dépôt, une ligne par table : 103 capacités supportées et 68 partielles — les partielles sont marquées, parce qu’une capacité dont les bords manquent n’est pas une capacité sur laquelle on peut compter. Chacune est nommée comme le dépôt la nomme.

cluster
GET / · HEAD / (ping) · GET /_cluster/health, /_cluster/health/{index}partiel · GET /_cat/indices, /_cat/indices/{index}partiel · GET /_cat/healthpartiel · GET /_nodes, /_nodes/{spec}partiel · Content-Encoding: gzip / deflate sur le corps d'une requête
index
Création à l'écriture · Expressions d'index (a,b, logs-*, _all, exclusions, alias) · PUT /{index}partiel · DELETE /{index} · HEAD /{index} · GET /{index} · GET /{index}/_mapping · GET /{index}/_settingspartiel · PUT /{index}/_settingspartiel · PUT /{index}/_mappingpartiel · POST /{index}/_refresh · POST|GET /_analyze, /{index}/_analyzepartiel · Mapping dynamique · Alias · Templates d'index — _index_template et _templatepartiel · GET /{index}/_statspartiel
analyzers
standard (défaut) · simple · whitespace · keyword · stop · english · french · snowball · danish, dutch, german, hungarian, italian, norwegian, portuguese, romanian, russian, spanish, swedish, turkish · Analyzers sur mesure (settings.analysis)
analyse sur mesure
analysis.analyzer de type custom · Tokenizerspartiel · ngram et edge_ngram — tokenizer et filtre · Filtrespartiel
types de champ
text · keyword · byte, short, integer, long · float, doublepartiel · boolean · datepartiel · Tableaux de valeurs · null · object (sous-objet), déclaré ou deviné · nestedpartiel · join (parent/enfant)partiel · analyzerpartiel · search_analyzer · copy_to · storepartiel · Multi-fields (fields) · ignore_above · index
ingestion
PUT|POST /{index}/_doc/{id}partiel · POST /{index}/_doc · PUT|POST /{index}/_create/{id} · GET /{index}/_doc/{id} · HEAD /{index}/_doc/{id} · DELETE /{index}/_doc/{id} · POST|PUT /_bulk, /{index}/_bulkpartiel · refresh (true / false / wait_for) · POST /{index}/_update/{id}partiel · GET|POST /_mget, /{index}/_mget · GET|POST /{index}/_count · Versionnage optimiste if_seq_no / if_primary_term · POST /{index}/_delete_by_querypartiel · POST /{index}/_update_by_querypartiel
scroll
?scroll=1m sur _search · POST|GET /_search/scroll · POST|GET /_search/scroll/{scroll_id} · DELETE /_search/scroll, /_search/scroll/{scroll_id}
motifs d’index
catalogue · produits,marques · audits-2026.08.* · _all, *, URL sans index · audits-*,-audits-2026.07.* · ignore_unavailable · allow_no_indices · expand_wildcardspartiel
alias
POST /_aliasespartiel · PUT|POST /{index}/_alias/{nom} et ses six autres URL (/_alias/{nom}, /{index}/_alias, /_alias, _aliases pour chacune) · DELETE /{index}/_alias/{nom} · GET /_alias, /_alias/{nom}, /{index}/_alias, /{index}/_alias/{nom} · HEAD /_alias/{nom}, /{index}/_alias/{nom} · aliases dans PUT /{index} · is_write_index
réglages du cluster
GET|PUT /_cluster/settingspartiel · action.destructive_requires_name
query DSL
match_all · match_none · matchpartiel · multi_matchpartiel · match_phrasepartiel · match_phrase_prefixpartiel · existspartiel · term · ids · prefixpartiel · wildcardpartiel · regexppartiel · fuzzypartiel · les champs de métadonnées en clause (_id, _index, …)partiel · constant_score · dis_max · function_scorepartiel · boosting · termspartiel · rangepartiel · boolpartiel · _name (nommer une clause), matched_queries, include_named_queries_score · query_stringpartiel · simple_query_stringpartiel
recherche plein texte
lenient · type: best_fields (défaut) · type: most_fields · type: phrase · type: phrase_prefix · operator sous phrase / phrase_prefixpartiel
corps de recherche
POST|GET /{index}/_search, POST|GET /_search · query · from / size · sortpartiel · _source · fieldspartiel · docvalue_fieldspartiel · stored_fields · highlightpartiel · track_total_hitspartiel · Scoringpartiel · Format de réponse · preferencepartiel · aggs / aggregationspartiel · scroll · min_score · search_after · POST /{index}/_pit, DELETE /_pit, et le bloc pit du corps · timeout (paramètre et corps)partiel · ignore_unavailable, allow_no_indices, expand_wildcards · GET|POST /{index}/_explain/{id}, explain: truepartiel · GET|POST /{index}/_field_capspartiel · GET|POST /{index}/_validate/querypartiel
date math
now · now±<n><unité> · now/<unité> · <ancre>||<opérations> · date partielle (2026-03-15, 2026-03, 2026-03-15T12) · format sur range · dans term, terms, match, et sous un nested · time_zone
agrégations
min, max, sum, avg, value_count, stats · termspartiel · rangepartiel · histogrampartiel · date_histogrampartiel · Sous-agrégations · extended_statspartiel · percentilespartiel · top_hitspartiel · filterpartiel
nested
{"nested": {"path", "query"}} · Clauses internespartiel · score_modepartiel · Champs devinés sous un nested
join
{"type": "join", "relations": {...}} · Indexation · has_child, has_parentpartiel · parent_id · {"term": {"lien": "article"}} · routingpartiel
02 — Ce que ça coûte à faire tourner

Un conteneur qu’on oublie.

L’image finale est un scratch qui contient un binaire lié statiquement — 1 couche contre 10. Rien d’autre : pas de libc, pas de shell, pas de JVM, rien à patcher un mardi.

coût mesuré

mesurée le 2 septembre 2026 · il y a 58 jours

vieille de plus d’un mois

Cette mesure date de 58 jours — au-delà des 30 jours après lesquels cette page cesse de la présenter comme l’état actuel. Le binaire grossit : entre les deux dernières campagnes du dépôt, l’image a bougé et le démarrage aussi, et c’est précisément pour cela que ce tableau est lu plutôt que tapé. Consultez la mesure pour savoir où on en est. Le dépôt signale que cette campagne a tourné sur un arbre modifié : l’image mesurée n’est donc peut-être pas celle que construit c2b4743 — Vrai si un des fichiers dont l'image est faite (src/, Cargo.toml, Cargo.lock, Dockerfile) differait de mesure.ferrite_sha pendant la campagne — donc si l'image mesuree pourrait ne pas etre celle que ce commit produit. Un README modifie au meme moment ne compte pas : il ne change pas un octet du binaire.

Elasticsearch 8.15.0Spunto Search 0.11.0
image, telle qu’un registre la sert669,1 Mo4,3 Mo

La somme des blobs qu'un registre sert pour cette image — le manifeste, la configuration et les couches telles qu'elles sont poussees. C'est ce qu'un docker pull telecharge, et le seul chiffre que ce depot publie. null quand l'archive porte des couches nues : la taille qu'un registre servirait n'en est alors pas deductible, et un nombre plausible mis a sa place serait la taille decompressee sous le nom de l'autre.

image, décompressée1,27 Go10,2 Mo

La somme des couches une fois depliees, c'est-a-dire ce que le systeme de fichiers de l'image occupe une fois l'image tiree.

mémoire au repos1,08 Go4,9 Mo

La somme des VmRSS de tous les processus du cgroup du conteneur, en kilo-octets, relevee 2 s apres le premier GET / servi et avant qu'aucun document ne soit indexe. Lire le seul PID 1 rendrait le RSS du shell qui lance la JVM d'Elasticsearch : deux ordres de grandeur en dessous, et du bon cote pour nous.

docker run → première réponse18,1 s172 ms

Le temps entre docker run -d et le premier GET / servi en 200, mediane de mesure.tours_demarrage demarrages successifs (conteneur recree a chaque tour). Pour ferrite l'essentiel est la creation du conteneur par Docker.

ce que le conteneur exécute/bin/tini/ferrite — 10,2 Mo

La taille du fichier que l'ENTRYPOINT du conteneur execute. null quand l'ENTRYPOINT n'est pas un binaire — celui d'Elasticsearch est un lien vers un script qui lance une JVM, et un nombre qui ne se compare a rien vaut moins que son absence.

Les deux colonnes sont mesurées par tests/compat/measure_container.sh sur la même machine, dans les mêmes conditions, contre un vrai Elasticsearch 8.15.0 tiré tel qu’Elastic le publie — jamais contre une idée de ce que coûte Elasticsearch. Les 172 ms sont l’aller-retour complet d’un docker run jusqu’à la première requête servie, et la médiane de 5 démarrages consécutifs : 263 ms · 170 ms · 174 ms · 169 ms · 172 ms. Pour ce binaire, l’essentiel de ce temps est Docker qui crée le conteneur.

ce qui a été mesuré
ferrite:0.11.0, construite ici depuis le Dockerfile du dépôt à c2b4743, contre docker.elastic.co/elasticsearch/elasticsearch:8.15.0.
comment
./tests/compat/measure_container.sh --json docs/container.json sur linux/amd64, Docker 29.7.2 (overlayfs). La version du serveur Docker et son magasin d'images ne changent pas ce que cet outil mesure — il compte les octets de l'archive — mais ils changent ce que les AUTRES outils repondent a la meme question : docker image inspect --format '{{.Size}}' rendait la taille decompressee jusqu'a Docker 28 et rend la compressee depuis la 29, et la colonne DISK USAGE de docker images en additionne encore une troisieme.
docker run
-p 9299:9200 pour Spunto Search, -p 9299:9200 -e discovery.type=single-node -e xpack.security.enabled=false -e ES_JAVA_OPTS=-Xms512m -Xmx512m pour Elasticsearch — cela fait partie de la définition des lignes mémoire et démarrage, ce n’est pas un détail.
mesure brute
docs/container.json — chaque chiffre ci-dessus, sa définition et le digest du manifeste des octets exacts comptés.

ce que ça change

Pas la requête — le déploiement. Un moteur de recherche qui démarre instantanément et tient en quelques mégaoctets cesse d’être un morceau d’infrastructure avec son propre plan de capacité et devient un détail : un sidecar à côté de l’app, un service dans un job de CI qui ne vaut pas la peine d’être mis en cache, un conteneur dans un environnement de dev déjà prêt avant que vous ayez changé de fenêtre.

C’est un seul nœud qui porte un seul shard sur un seul volume. Tout le compromis est là : pas de réplication, pas de bascule, pas de rééquilibrage — et rien de la machinerie qui leur donne le coût qu’ils ont.

docker-compose.yml
services:
  search:
    build: https://github.com/spuntodotnet/search.git
    ports:
      - "9200:9200"
    volumes:
      - search-data:/data

volumes:
  search-data:

Aucune image n’est encore publiée : ceci construit depuis le dépôt — la même image scratch que mesure le tableau. Pas de Docker du tout ? Les releases fournissent un binaire musl statique pour x86-64 et arm64.

03 — Ce qu’il refuse

Rien n’échoue en silence.

Le mode d’échec qui compte dans une couche de compatibilité n’est pas une fonctionnalité manquante, c’est une fonctionnalité manquante qui répond 0 hits. Chaque clause, type de champ et route non implémentés renvoie une erreur au format d’Elasticsearch, avec un type — not_implemented_in_ferrite_exception — qui veut dire précisément « ES sait le faire, nous pas encore ».

Certains refus sont définitifs et voulus, et ce sont les plus intéressants : cardinality et slop sont refusés parce que Tantivy et Lucene ne s’accordent pas sur ce qu’ils doivent renvoyer, et les deux auraient été faciles à accepter et faux en silence. La raison de chaque refus est dans le dépôt, et elle est reproduite ci-dessous plutôt que résumée.

hors périmètre

Les 44 refus déclarés dans compat.yaml, par famille — les volontaires d’abord, « pas encore écrit » en dernier.

hors périmètre, volontairement
Cluster distribué — _cluster/state, _cluster/stats, _cluster/reroute, _cluster/allocation/explain, _cluster/pending_tasks, _remote/info, _nodes/stats, _tasks · Snapshots, dépôts, restauration — _snapshot/* · Flux de données — _data_stream, data_stream dans un template · Tout ce qu'Elastic ajoute autour du moteur — sécurité et rôles (_security), machine learning (_ml), cycle de vie (_ilm, _slm), _watcher, _transform, _enrich, _eql, _sql, _esql, _inference, connecteurs, *search applications*, _rollup, _ccr, _graph, licence, _monitoring · Scripting — _scripts, _search/template, _render/template, Painless · script_fields, runtime_mappings · routing, filter_path, typed_keys · search_type, max_concurrent_shard_requests, pre_filter_shard_size, batched_reduce_size
divergence de moteur
finnish · Noms de champ pointés (a.b) ou réservés · type: cross_fields, bool_prefix · slop · histogram, date_histogram, range sur un champ multivalué · cardinality · Champ text dans une clause interne · Tri et agrégations sur un sous-champ de nested, depuis la racine
refusé par Elasticsearch aussi
L'API typée — un {type} dans l'URL, un _type dans la réponse, include_type_name · from avec scroll · Contexte expiré, fermé, ou jamais ouvert · DELETE /{alias} · à l'indexation
pas encore écrit
Les _cat/* autres que _cat/indices et _cat/health · Cycle de vie d'un index — _close, _open, _forcemerge, _shrink, _split, _clone, _rollover, _recovery, _segments, _flush, _upgrade, _cache/clear, _shard_stores, _resolve/index · Les autres routes de recherche — _search_shards, _termvectors, _mtermvectors, _suggest, _source · GET /{index}/_mapping/field/{champs} · _component_template, _simulate_index_template, _simulate · arabic, czech, greek, thai et les autres langues · char_filter · Un analyzer de type autre que custom (french, standard paramétré…) · Tout autre type (geo_point, ip, binary…) · Autres paramètres de champ (null_value, doc_values, norms…) · version / version_type externes · _reindex, pipelines d'ingestion · filter, routing, index_routing, search_routing sur un alias · GET /_cat/aliases · remove_index et add d'un alias du même nom dans le même POST /_aliases · GET /{index}/_alias sur un index fermé · intervals, terms_set, script, span_*… · collapse, knn, seq_no_primary_term, post_filter, suggest, rescore, track_scores, q, terminate_after, version, indices_boost, profile, slice, stats, ext, retriever · rest_total_hits_as_int · _msearch, _search/template · composite, filters, nested, significant_terms, date_range, ip_range, percentile_ranks, median_absolute_deviation… · nested dans un nested · inner_hits, ignore_unmapped
la raison de chaque refus — 44 entrées, dans les mots du dépôt

Cité mot pour mot depuis compat.yaml.

hors périmètre, volontairement

Cluster distribué — _cluster/state, _cluster/stats, _cluster/reroute, _cluster/allocation/explain, _cluster/pending_tasks, _remote/info, _nodes/stats, _tasks
ferrite est mono-nœud et mono-shard : il n'y a ni allocation, ni réallocation, ni tâche distribuée à rapporter, et une réponse plausible inventée pour ces routes serait un mensonge sur la nature du serveur.
Snapshots, dépôts, restauration — _snapshot/*
la sauvegarde d'un ferrite est la copie de son répertoire de données, ou l'export par scroll ; un dépôt de snapshots répliqué est de la mécanique de cluster.
Flux de données — _data_stream, data_stream dans un template
un flux de données est une suite d'index gérée par _rollover et l'ILM, donc de l'exploitation de cluster posée au-dessus du moteur. Le refus est déclaré ici parce qu'il est exercé : helpers.reindex du client officiel demande GET /_data_stream/{cible} avant de copier, sur toute cible — c'est la suite serveur du client Python qui l'a montré, et sans cette ligne l'appel comptait indeterminé, donc contre nous.
Tout ce qu'Elastic ajoute autour du moteur — sécurité et rôles (_security), machine learning (_ml), cycle de vie (_ilm, _slm), _watcher, _transform, _enrich, _eql, _sql, _esql, _inference, connecteurs, *search applications*, _rollup, _ccr, _graph, licence, _monitoring
ce sont des produits posés sur le moteur, pas le moteur : chacun a son propre modèle de données et sa propre API, et aucun n'est ce qu'un client Elasticsearch existant appelle pour chercher des documents. C'est le corpus d'usage qui a montré qu'aucune capacité ne les réclamait — donc que 19 % de ses requêtes tombaient dans un trou de la déclaration, ni servies ni refusées.
Scripting — _scripts, _search/template, _render/template, Painless
un moteur de script est un langage à embarquer, à isoler et à maintenir : c'est exactement le genre de poids que ferrite existe pour ne pas porter.
script_fields, runtime_mappings
les deux définissent des champs calculés par un script Painless, que ferrite n'exécute pas. La mesure le confirme plutôt que la supposition : sur les 444 requêtes du corpus qui portent runtime_mappings, 425 l'envoient vide (des gabarits de tracks Rally), et sur les 19 non vides 18 portent un script. L'objet vide est donc accepté — il ne définit aucun champ, donc ne demande rien, et ES rend la même réponse avec ou sans (mesuré) ; un objet non vide est refusé explicitement.
routing, filter_path, typed_keys
ferrite est mono-shard, routing n'a rien à choisir ; les deux autres changent la forme de la réponse, et une forme qui dépend d'un paramètre est une seconde API à mesurer.
search_type, max_concurrent_shard_requests, pre_filter_shard_size, batched_reduce_size
ils reglent la façon dont une recherche se distribue entre shards ; il n'y en a qu'un, donc rien à distribuer et rien à régler.

divergence de moteur

finnish
le stemmer finnois de rust-stemmers s'écarte de celui de Lucene sur 13 mots des 84 399 du vocabulaire finnois de Snowball (0,015 %) : il coupe la voyelle finale d'un emprunt à diacritique étranger là où l'algorithme la garde (garcía rend garcí chez lui et garcía chez ES ; de même bundesstraße, españa, musée). Un analyzer n'est jamais livré sous le nom d'ES tant qu'il n'est pas mesuré identique — un écart de 0,015 % rendu en 200 est le pire résultat possible ici.
Noms de champ pointés (a.b) ou réservés
un point est le séparateur de chemin d'un objet : le porter dans un nom rendrait ambigu ce qu'un client.ville désigne. Le préfixe _, lui, n'est pas réservé — seuls le sont les champs de métadonnées d'ES (_id, _source, _seq_no…) et les racines des colonnes internes de ferrite (_elem, _nelem, _join_parent), qu'un champ utilisateur écraserait.
type: cross_fields, bool_prefix
le premier demande des statistiques de termes fusionnées entre champs, le second un scoring de suggestion : les approcher rendrait un autre classement que celui qu'un client attend.
slop
refusé quel que soit le type, pour la raison qui le fait refuser dans match_phrase (divergence n° 2).
histogram, date_histogram, range sur un champ multivalué
l'agrégation de tantivy compte les valeurs, Elasticsearch compte les documents : un document dont le champ vaut [1, 2, 3] tombe trois fois dans le bucket qui les contient (mesuré : doc_count de 4 là où ES en compte 2). Le refus n'est prononcé que si la colonne est réellement multivaluée — un champ à une valeur par document, le cas courant, reste servi et exact. terms, value_count et stats ne sont pas concernés : leurs comptes coïncident avec ceux d'ES.
cardinality
l'estimation de tantivy diffère de celle d'ES (mesuré : 582 valeurs distinctes annoncées là où ES en compte 598), y compris sous le seuil où ES est exact — un compte approché sous le nom d'ES serait faux sans le dire.
Champ text dans une clause interne
les colonnes portent la valeur, pas les termes analysés : interroger son multi-field .keyword, ou sortir la clause du nested.
Tri et agrégations sur un sous-champ de nested, depuis la racine
ils porteraient sur les valeurs à plat, donc sur autre chose que ce que la requête a filtré. Chez Elasticsearch ces valeurs vivent dans des documents cachés : une agrégation n'en voit aucune et rend son résultat vide (null, 0.0, buckets: []), un tri est refusé (it is mandatory to set the [nested] context on the nested sort field). ferrite les porte sur le document parent : il rendait donc une moyenne de 7.0 là où ES rend null, et un ordre en 200 là où ES rend 400. Un chiffre plausible et faux est le pire des résultats — le refus est explicite, et c'est le même que pour la requête équivalente (divergence n° 10).

refusé par Elasticsearch aussi

L'API typée — un {type} dans l'URL, un _type dans la réponse, include_type_name
elle a disparu en 8.x, la version que ferrite annonce : un vrai Elasticsearch 8 échoue au même endroit, et la rendre reviendrait à annoncer une version qu'on ne sert pas.
from avec scroll
comme ES : action_request_validation_exception.
Contexte expiré, fermé, ou jamais ouvert
404 search_phase_execution_exception, cause search_context_missing_exception — la forme exacte d'ES, celle que les clients reconnaissent.
DELETE /{alias}
effacer des index que le client n'a pas nommés n'est pas une suppression, c'est un accident ; ES 8 refuse de la même façon (« The provided expression [x] matches an alias, specify the corresponding concrete indices instead »).
à l'indexation
{"d": "now"} est refusé, comme chez ES : le document porterait une date qui dépend de l'instant où il a été écrit.

pas encore écrit

Les _cat/* autres que _cat/indices et _cat/health
ce sont des vues de texte sur un état que ferrite a pour la plupart ; elles ne sont pas écrites, et leurs tests exigent en plus les colonnes h, s et help que les deux _cat existants n'ont pas non plus.
Cycle de vie d'un index — _close, _open, _forcemerge, _shrink, _split, _clone, _rollover, _recovery, _segments, _flush, _upgrade, _cache/clear, _shard_stores, _resolve/index
la moitié de ces routes n'a pas de sens sans shards (_shrink, _split, _recovery, _shard_stores) ; l'autre moitié suppose un index qu'on peut arrêter de servir sans le supprimer (_close / _open), état que ferrite n'a pas.
Les autres routes de recherche — _search_shards, _termvectors, _mtermvectors, _suggest, _source
aucune n'est demandée par le code client qu'on cherche à servir.
GET /{index}/_mapping/field/{champs}
route absente (no handler found) : ferrite a pourtant le mapping, c'est un manque et pas une impossibilité — 15 cas de la suite d'Elastic tombent dessus.
_component_template, _simulate_index_template, _simulate
un template de composants est un template qu'on cite depuis un autre : tant qu'il n'est pas lu, composed_of est refusé à la pose plutôt qu'appliqué à moitié. La simulation, elle, rend l'index qu'on obtiendrait — utile, et sans client qui la demande.
arabic, czech, greek, thai et les autres langues
leur chaîne demande des filtres de normalisation qui ne sont pas portés (et, pour plusieurs, un stemmer absent de rust-stemmers). Rien n'y est mesuré : les douze langues servies le sont mot à mot, et celles-ci ne le seraient pas.
char_filter
aucun mapping venu d'une instance réelle n'en a encore demandé ; c'est une brique à écrire, pas un obstacle.
Un analyzer de type autre que custom (french, standard paramétré…)
paramétrer un analyzer intégré (stopwords, stem_exclusion) demande de reproduire sa composition interne exacte, qui n'est mesurée que dans sa forme par défaut.
Tout autre type (geo_point, ip, binary…)
chacun demande son propre encodage et ses propres requêtes ; aucun n'est apparu dans les mappings des instances reprises jusqu'ici.
Autres paramètres de champ (null_value, doc_values, norms…)
les paramètres acceptés sont type, analyzer, search_analyzer, fields, ignore_above, format, index, copy_to et store ; les autres sont refusés plutôt qu'acceptés sans effet, faute de quoi un doc_values: false laisserait croire qu'un champ n'est pas triable.
version / version_type externes
la version d'un document est gérée par ferrite ; l'imposer de l'extérieur demande de tenir un ordre que le serveur ne contrôle plus, et rien ne l'a encore réclamé.
_reindex, pipelines d'ingestion
_reindex copie d'un index vers un autre, et son intérêt est justement ce que ferrite n'a pas : une source distante, un pipeline, un script. Rien ne s'y oppose, ce n'est pas écrit — et scroll + _bulk le font aujourd'hui côté client, ce que ni _delete_by_query ni _update_by_query ne permettaient.
filter, routing, index_routing, search_routing sur un alias
un alias filtré dont le filtre n'est pas appliqué rendrait précisément les documents qu'il est censé cacher ; le routage, lui, n'a rien à choisir sur un mono-shard.
GET /_cat/aliases
route absente (no handler found) ; ses tests exigent en plus h=, s= et help sur les _cat, que ferrite n'a pas — 10 cas de la suite d'Elastic tombent dessus.
remove_index et add d'un alias du même nom dans le même POST /_aliases
refusé en 400 (« an index or data stream exists with the same name as the alias ») : ferrite applique les alias avant les suppressions, là où ES calcule tout l'état puis l'applique — c'est pourtant l'usage même de remove_index.
GET /{index}/_alias sur un index fermé
_close / _open sont hors périmètre, donc un index fermé n'existe pas ici.
intervals, terms_set, script, span_*…
parsing_exception: unknown query [...], avec la liste des clauses connues.
collapse, knn, seq_no_primary_term, post_filter, suggest, rescore, track_scores, q, terminate_after, version, indices_boost, profile, slice, stats, ext, retriever
aucun n'est un obstacle de moteur. search_after et pit, qui manquaient le plus, sont servis depuis la carte 08.
rest_total_hits_as_int
il change la forme de hits.total (nombre au lieu d'objet) ; ES 8 l'accepte encore et du code venu de la 6.x/7.x s'en sert, voir [compat-es7.md](compat-es7.md).
_msearch, _search/template
_msearch est le plus regretté : un tableau de bord qui pose six facettes fait six appels au lieu d'un.
composite, filters, nested, significant_terms, date_range, ip_range, percentile_ranks, median_absolute_deviation…
filters (la sœur plurielle de filter) et composite sont les deux qui manquent le plus ; aucune n'est un obstacle de moteur.
nested dans un nested
il faudrait un indice d'élément par niveau.
inner_hits, ignore_unmapped
inner_hits demande de rendre l'élément qui a correspondu : ferrite le sait (c'est la colonne jumelle), il ne l'expose simplement pas.

à lire

C’est la version 0.10.0, un seul nœud, sans authentification. Quiconque atteint le port 9200 peut lire et écrire chaque index, et il n’y a de réplica nulle part — la durabilité de vos données est celle de ce seul volume. Traitez-le comme un index de recherche que vous pouvez reconstruire depuis votre source de vérité, sur un réseau privé, et il a exactement la bonne taille. Traitez-le comme une base de données et il vous décevra.

04 — Là où il se rend utile

Le champ de recherche, pas le cluster de recherche.

La plupart des produits qui ont besoin de recherche ont besoin d’un index, de quelques centaines de milliers de documents et d’une facette ou deux — et finissent par faire tourner un système distribué conçu pour des pétaoctets, parce que c’est à lui que parle la bibliothèque cliente. Ceci parle à la même bibliothèque.

sidecar
dans le même fichier compose que l’app, sur le même volume, sans plan de capacité
CI
un conteneur de service qui démarre plus vite que l’étape de checkout qui le précède
environnements de dev
une sandbox Spunto qui arrive avec la recherche qui répond déjà
en bordure d’un produit
de la recherche dans une appliance, une installation sur site, un Raspberry Pi

↳ aussi chez Spunto