Skip to content

When the index says two documents and the query says one

Nested elements are stored as documents of their own, so the panel and the editor legitimately disagree, and one panel asks the cluster nothing at all.

An index panel reports two documents. You run SELECT COUNT(*) against the same index and get one. Nothing is stale, nothing is cached wrong, and no refresh will reconcile them. The gap is a fact about how the engine stores a nested field, and both numbers are correct answers to two different questions.

This provider was verified against Elasticsearch 9.1.4, image docker.elastic.co/elasticsearch/elasticsearch:9.1.4, default build flavour, subscription tier basic, security disabled, measured 2026-08-19. The two-against-one pair itself was measured on the sibling search product, which runs this same implementation, against a probe index whose items field is mapped nested: _cat reported 2 documents and SELECT COUNT(*) answered 1.

Where each monitoring figure comes from

The monitoring surface for this engine makes four kinds of read, all of them plain HTTP: /, _cluster/health, _cluster/stats and _cat/indices. There is no driver in the path at all - every statement and every stats call is a JSON body carried by the runtime’s own fetch, and the query surface itself is Elasticsearch SQL over POST /_sql?format=json.

So a per-index row in the monitoring panels is _cat/indices and nothing else:

{ "index": "probe_shapes", "docs.count": "2" }

rowCount is docs.count. The per-index size is pri.store.size, which is primaries only. The cluster-wide size beside it comes from _cluster/stats as indices.store.size_in_bytes, which includes replicas. Those two figures are deliberately not the same measurement, so they do not sum, and on the measured single node with one replica requested they happened to be equal - which is exactly why the choice had to be made deliberately rather than discovered later by someone reconciling a total that stopped matching.

Two more counts read differently from what their names suggest. tableCount counts indices, because an index is the table on this surface, and it counts the user’s indices only. indexCount is 0 and stays 0: every mapped field is inverted-indexed as a property of being mapped, so there is no secondary-index object anybody declared and nothing to name.

Why a nested element is its own document

A nested field is not sugar over an array of objects. The engine stores each element of that array as a document of its own, hidden under the parent, so that a query can match one element’s fields together rather than matching across a flattened bag of values. That is the entire reason the mapping type exists.

docs.count counts documents. All of them. A single logical record with one nested element is two documents in that number. SELECT COUNT(*) runs through the SQL endpoint and counts the rows that endpoint produces, which is the parent only. Neither number is a rounding of the other, and the gap grows with the size of the nested array.

This is also why the schema tree for this engine reads the index mapping through GET /<index>/_mapping rather than asking SQL what the columns are. Measured, an index that maps a flattened and a nested field answers SELECT * with {"columns":[],"rows":[[]]} - no columns at all. The mapping is the only honest source for structure, and the index listing is where the cluster’s own document count comes from.

Reading the count the panel actually reports

The panel is reporting the cluster’s document count. It is not reporting how many rows a query would return, and it was never derived from one.

The two numbers, side by side

Where you look What it counts probe_shapes
Monitoring panel rowCount _cat/indices docs.count - every stored document 2
Editor SELECT COUNT(*) rows the SQL endpoint produces - parents only 1

Deriving the panel figure from SQL instead would mean issuing a statement per index, on a surface whose grammar the schema tree must not depend on, to answer a question the panel is not asking. So an index with nested fields always reads higher in the panels than in the editor, and the honest fix is to know which number you are reading rather than to bend one of them toward the other.

The same rule about not fabricating a figure shows up one row over. A closed index has rowCount and the total-size fields forced to zero, because those are required numbers with nowhere else to go - but the optional per-index size is left absent rather than set to 0, since a zero there would be an invented measurement rather than a forced one. And one closed index takes the cluster’s Data figure away entirely: the Storage tab shows N/A for every open index beside it rather than summing the ones that did answer, because a partial sum presented as a cluster total is the same lie, one digit larger.

The panel that sends no request at all

getPerformanceMetrics() returns {} for this engine, and it asks the cluster nothing. There is no request. The Performance panel is empty because the seam declined to invent its contents, not because a call failed.

Empty was chosen over zero for a measurable reason. Cache hit ratio is scored with a below-direction threshold and a critical floor of 80, so a “neutral” 0 paints a red critical cache fault on every healthy cluster; an absent ratio defaults to a healthy 100 instead. Every other metric would read as a measurement of zero, which is a different and false claim - and the tabs did read them that way. Measured 2026-08-19 on the sibling search provider, which is this same code path, the Overview showed Buffer Pool 0% and Deadlocks 0 for two fields the payload simply omits. Both cards now read N/A beside Not measured.

Performance metrics return nothing and ask the cluster nothing, uptime reads N/A, the active connection count is absent rather than zero, and there are no active sessions or slow queries to read. Uptime is N/A because neither the health payload nor the version payload carries one, and a 0s would claim the cluster booted this instant. Active connections are absent because the number the cluster does publish is not one of this seam’s calls, and the shard and node counts that are here would be a different number wearing that field’s name. Sessions are empty structurally: one statement is one HTTP request, so there is no session and no connection catalog. Slow queries are empty for a plainer reason - the slow log is written to the node’s own log file, which no API returns.

Of those, only the performance metrics are reachable. The numbers exist on this product’s stats endpoints, so widening the seam by one call is a recorded follow-up rather than an impossibility. The rest are properties of the engine.

Absent rather than zero, across this dashboard

One more absence works the same way. _cluster/stats is heavier and more privileged than _cluster/health, so a cluster that answers health and refuses stats is an ordinary configuration, not a fault. That one read catches its own failure and returns unknown, because losing the health status over a missing byte count would blank a panel that already had the important number. The unknown then propagates honestly: the size reads N/A, the byte key is omitted rather than set to 0, and the Storage tab returns no cluster row at all rather than a row claiming zero bytes stored. A cluster that really stores nothing publishes a real 0 and keeps it.

The inverse encoding appears on the same screen and is also correct: maxConnections is 0, because for a ceiling, zero and absence are the same fact, and the Connections card reads a zero maximum as “no limit published” rather than dividing by it.

None of this is specific to search engines. It is the rule the whole capability model runs on: a figure that cannot be answered on the connected engine is absent with the reason written where it would have been, not rendered as a confident zero. The engine pages publish those boundaries before you connect.

Two more, in the same spirit. The SQL surface here has no writes of any kind, so row editing is not offered and then failed. And Agent AUTO mode does not run on this connection: the tool-using run needs a database-native read-only profile, which exists on PostgreSQL, SQLite and DuckDB only, so an auto run here ends engine-unsupported. Agent PLAN mode does open, toolless, and drafts a statement for you to run yourself.