Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
92 changes: 92 additions & 0 deletions src/chttpd/src/chttpd_db.erl
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
handle_changes_req/2,
update_doc_result_to_json/1, update_doc_result_to_json/2,
handle_design_info_req/3,
handle_index_info_req/2,
handle_view_cleanup_req/2,
update_doc/4,
http_code_from_status/1,
Expand Down Expand Up @@ -464,6 +465,65 @@ handle_design_info_req(#httpd{method = 'GET'} = Req, Db, #doc{} = DDoc) ->
handle_design_info_req(Req, _Db, _DDoc) ->
send_method_not_allowed(Req, "GET").

% $db/_index_info: build status of indexes in a db
handle_index_info_req(#httpd{method = 'GET', path_parts = [_, _]} = Req, Db) ->
Types = parse_types(Req),
DbName = couch_db:name(Db),
{ok, {CopiesExpected, Indexes}} = fabric:get_index_info(DbName, Types),
IndexesEjson = {index_info_to_json(Indexes)},
Ejson = {[{name, DbName}, {copies_expected, CopiesExpected}, {indexes, IndexesEjson}]},
send_json(Req, 200, Ejson);
handle_index_info_req(#httpd{method = 'GET'} = Req, _Db) ->
chttpd:send_error(Req, not_found);
handle_index_info_req(Req, _Db) ->
send_method_not_allowed(Req, "GET").

% _index_info types: one or more index types. ex: ?type=nouveau,search
parse_types(Req) ->
TypeArgs = [V || {K, V} <- chttpd:qs(Req), K == "type"],
case TypeArgs of
[] ->
[view, search, nouveau];
[_ | _] ->
Tokens = lists:flatmap(fun(V) -> string:tokens(V, ",") end, TypeArgs),
case lists:usort(lists:map(fun parse_type/1, Tokens)) of
[] -> throw({query_parse_error, <<"`type` must not be empty">>});
Types -> [T || T <- [view, search, nouveau], lists:member(T, Types)]
end
end.

parse_type("view") ->
view;
parse_type("search") ->
search;
parse_type("nouveau") ->
nouveau;
parse_type(Other) ->
Msg = io_lib:format("Invalid index type: ~s. Must be view, search or nouveau", [Other]),
throw({query_parse_error, ?l2b(Msg)}).

index_info_to_json(Indexes) ->
[{DDocId, index_info(Res)} || {DDocId, Res} <- Indexes].

index_info({error, Error}) ->
errobj(Error);
index_info({Sections}) ->
{[{Sect, index_section_json(Sect, Val)} || {Sect, Val} <- Sections]}.

index_section_json(view_index, Res) ->
res_to_json(Res);
index_section_json(_Named, {Idxs}) ->
{[{Name, res_to_json(Res)} || {Name, Res} <- Idxs]}.

res_to_json({error, Error}) ->
errobj(Error);
res_to_json(Info) ->
Info.

errobj(Error) ->
{_Code, ErrorStr, ReasonStr} = chttpd:error_info(Error),
{[{error, ErrorStr}, {reason, ReasonStr}]}.

create_db_req(#httpd{} = Req, DbName) ->
couch_httpd:verify_is_server_admin(Req),
ShardsOpt = parse_shards_opt(Req),
Expand Down Expand Up @@ -2660,6 +2720,38 @@ monitor_attachments_test_() ->
?_assertEqual([], monitor_attachments(Atts))
end}.

index_info_to_json_test_() ->
% {error, Reason} turn into {"error":..., "reason":...}
Unavailable = {service_unavailable, <<"Search is not available">>},
Indexes = [
{<<"_design/err">>, {error, not_found}},
{<<"_design/ok">>,
{[
{view_index, {[{update_seq, 1}]}},
{search_indexes,
{[{<<"i">>, {[{doc_count, 2}]}}, {<<"j">>, {error, Unavailable}}]}},
{nouveau_indexes, {[{<<"n">>, #{num_docs => 3}}]}}
]}}
],
Expected = [
{<<"_design/err">>, {[{error, <<"not_found">>}, {reason, <<"missing">>}]}},
{<<"_design/ok">>,
{[
{view_index, {[{update_seq, 1}]}},
{search_indexes,
{[
{<<"i">>, {[{doc_count, 2}]}},
{<<"j">>,
{[
{error, <<"service unavailable">>},
{reason, <<"Search is not available">>}
]}}
]}},
{nouveau_indexes, {[{<<"n">>, #{num_docs => 3}}]}}
]}}
],
?_assertEqual(Expected, index_info_to_json(Indexes)).

parse_partitioned_opt_test_() ->
{
foreach,
Expand Down
1 change: 1 addition & 0 deletions src/chttpd/src/chttpd_httpd_handlers.erl
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ url_handler(<<"_up">>) -> fun chttpd_misc:handle_up_req/1;
url_handler(_) -> no_match.

db_handler(<<"_view_cleanup">>) -> fun chttpd_db:handle_view_cleanup_req/2;
db_handler(<<"_index_info">>) -> fun chttpd_db:handle_index_info_req/2;
db_handler(<<"_compact">>) -> fun chttpd_db:handle_compact_req/2;
db_handler(<<"_design">>) -> fun chttpd_db:handle_design_req/2;
db_handler(<<"_partition">>) -> fun chttpd_db:handle_partition_req/2;
Expand Down
1 change: 1 addition & 0 deletions src/docs/src/api/database/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -44,5 +44,6 @@ Where ``{db}`` is the name of any database.
changes
compact
cleanup
index_info
security
misc
224 changes: 224 additions & 0 deletions src/docs/src/api/database/index_info.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,224 @@
.. Licensed under the Apache License, Version 2.0 (the "License"); you may not
.. use this file except in compliance with the License. You may obtain a copy of
.. the License at
..
.. http://www.apache.org/licenses/LICENSE-2.0
..
.. Unless required by applicable law or agreed to in writing, software
.. distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
.. WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
.. License for the specific language governing permissions and limitations under
.. the License.

.. _api/db/index_info:

=====================
``/{db}/_index_info``
=====================

.. versionadded:: 3.6

.. http:get:: /{db}/_index_info
:synopsis: Returns status all the indexes in the database

Index info objects have the same fields as the ones returned by
:get:`/{db}/_design/{ddoc}/_info`,
:get:`/{db}/_design/{ddoc}/_search_info/{index}` and
:get:`/{db}/_design/{ddoc}/_nouveau_info/{index}`, with an
``updates_pending`` object with the index build pending bounds across all
copies of the database. See :ref:`api/db/index_info/structure`.

This endpoint may be used when one needs to know whether a database's
indexes are fully built, for example before switching traffic to a replica.

:param db: Database name
:query string type: Filter response to the given index types. Can be one of
these values: ``view``, ``search`` and ``nouveau``, comma separated.
*Optional*, default: all types.
:<header Accept: - :mimetype:`application/json`
- :mimetype:`text/plain`
:>header Content-Type: - :mimetype:`application/json`
- :mimetype:`text/plain; charset=utf-8`
:>json string name: Database name
:>json number copies_expected: The number of shard copies of the database
(Q*N) Compare with the ``copies`` count of each ``updates_pending``
object.
:>json object indexes: Index information keyed by design doc ID.
See :ref:`api/db/index_info/structure`.
:code 200: Request completed successfully
:code 400: Invalid database name or ``type`` value
:code 401: Unauthorized request to a protected API
:code 403: Insufficient permissions / :ref:`Too many requests with invalid credentials<error/403>`
:code 404: Database doesn't exist

**Request**:

.. code-block:: http

GET /recipes/_index_info HTTP/1.1
Accept: application/json
Host: localhost:5984

**Response**:

.. code-block:: http

HTTP/1.1 200 OK
Cache-Control: must-revalidate
Content-Type: application/json
Date: Mon, 01 Sep 2025 15:42:11 GMT
Server: CouchDB (Erlang/OTP)

{
"name": "recipes",
"copies_expected": 6,
"indexes": {
"_design/cookbook": {
"view_index": {
"collator_versions": [
"153.136"
],
"compact_running": false,
"language": "javascript",
"purge_seq": 0,
"signature": "a6d97b0199e54a1eb56e4becb1322587",
"sizes": {
"active": 1825,
"external": 1355,
"file": 16750
},
"update_seq": 54,
"updater_running": false,
"waiting_clients": 0,
"waiting_commit": false,
"updates_pending": {
"minimum": 0,
"maximum": 0,
"copies": 6
}
},
"search_indexes": {
"ingredients": {
"committed_seq": 54,
"disk_size": 3960,
"doc_count": 50,
"doc_del_count": 0,
"pending_seq": 54,
"signature": "0b4ba635d5eb4fcbb2f6c9c2247460ec",
"updates_pending": {
"minimum": 0,
"maximum": 0,
"copies": 6
}
}
},
"nouveau_indexes": {
"ingredients": {
"disk_size": 6324,
"num_docs": 50,
"purge_seq": 0,
"signature": "ea87fe8f9517403691850f51d0a1ce3e5afaf89347204dd79430252a0591e503",
"update_seq": 54,
"updates_pending": {
"minimum": 0,
"maximum": 0,
"copies": 6
}
}
}
},
"_design/8c2a4caf8ea1b581ac43a062fd43a876dee1382d": {
"view_index": {
"collator_versions": [
"153.136"
],
"compact_running": false,
"language": "query",
"purge_seq": 0,
"signature": "b77547252cb8b19ff12831973b576c0f",
"sizes": {
"active": 0,
"external": 10,
"file": 102
},
"update_seq": 0,
"updater_running": false,
"waiting_clients": 0,
"waiting_commit": false,
"updates_pending": {
"minimum": 54,
"maximum": 54,
"copies": 6
}
}
}
}
}

In this example the ``cookbook`` design document has a view group, a
search index and a nouveau index, all fully built on all six copies. The
second design document is a Mango index which has not been built on
any copy yet: every copy is 54 updates behind.

.. _api/db/index_info/structure:

Index Information
=================

The ``indexes`` object of the :get:`/{db}/_index_info` response has one entry
per design document with at least one index, keyed by design doc ID. Each entry
may contain:

* **view_index** (*object*): The design doc's view group, with the fields
described in :ref:`api/ddoc/view_index_info` and ``updates_pending``. A design
document has one view group for all of its views.

* **search_indexes** (*object*): One entry per search index of the design
document, keyed by index name, with the fields of the ``search_index`` object
returned by :get:`/{db}/_design/{ddoc}/_search_info/{index}` plus
``updates_pending``.
* **nouveau_indexes** (*object*): One entry per nouveau index of the design
document, keyed by index name, with the fields of the ``search_index`` object
returned by :get:`/{db}/_design/{ddoc}/_nouveau_info/{index}` plus
``updates_pending``.

Pending stats
--------------

Every index object contains an ``updates_pending`` object which shows the
number of pending changes for that index.

* **minimum** (*number*): Backlog of the most up to date copy of each
range. ``0`` means at least one fully built copy of the index exists for
every range.
* **maximum** (*number*): Backlog of the least up to date copy of each range.
``0`` means the index is fully built on all returned shard copies.
* **copies** (*number*): The number of shard copies with returned a response.
Copies which are unreachable, in maintenance mode, or which did not answer
before the request timed out are skipped. When this is much smaller than
``copies_expected`` the bounds are partial and ``maximum`` may underestimate
the true pending backlog.

Errors
------

If an index cannot be inspected it will return an error for that index only.
For example is Closeau is not available search indexes will return:

.. code-block:: javascript

"search_indexes": {
"ingredients": {
"error": "service unavailable",
"reason": "Search is not available"
}
}
Comment on lines +210 to +215

@ricellis ricellis Sep 7, 2026

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this example correct?
It suggests that the API will list the same error for each search index in defined in the ddoc if search is unavailable. I suppose this is covering the case of other possible errors too? I'm trying to understand if the idea is to present the error/reason at the top-level or for each defined index.

To my mind the problem with this approach to errors is that it makes reusing existing models of the search (or view or query) index information impossible because it introduces new error and reason fields into those models (that I think are invalid in the existing case).

@nickva nickva Sep 7, 2026

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's for the case when we couldn't get any information for any copy of the index on any shards. It would be either a cluster is very unhealthy (really partitioned) or an index type is disabled. If we get at least some copies as a response we'll return those stats with a copies values.

For the search case or nouveau case it makes sense to consider those services disabled as a real possibility. And yeah, it would list that error for any search index and for every nouveau index if we have those defined but index service is not available.

I guess we could hide those index types if the services are disabled but then we'd be hiding indexes form users and that could be confusing, too. If we do hide them if index types are disabled we'd still have to see what to return if no copies return an info object. We could crash the whole response, or maybe remove all the fields except "copies": 0 perhaps?

"ingredients": {"updates_pending": {"copies": 0}}

But that seems a kind of odd too...

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thinking about it a bit more another pain point with the proposed approach is that the key for view indexes is dynamic (the ddoc name) which makes it harder to model the response type vs the static keys for search_indexes and nouveau_indexes.

How about something like:

{
    "db_name": "recipes", // instead of "name" to make it clear what name this is
    "copies_expected": 6,
    "indexes" : {
        "view_indexes": { // add this to better match the search peers and indicate the type of the nested structure
            "_design/cookbook": {
                "ok" : true, // false for error cases (presence of ok is Couch convention, omission is more normal for couch failure cases, but I think a false is preferable)
                "error" : "omit if ok",
                "reason": "omit if ok",
                "view_index": {
                    // existing model used in `GET /{db}/_design/{ddoc}/_info`
                }
            }
        },
        "search_indexes": {
            "ingredients": {
                "ok" : true,
                "error" : "omit if ok",
                "reason": "omit if ok",
                "search_index": { // add this to separate the error info from the index info and align with existing `GET /{db}/_design/{ddoc}/_search_info/{index}_search_info` naming and the view structure in this proposed endpoint
                }
            }
        },
        "nouveau_indexes": {
            "ingredients": {
                "ok" : true,
                "error" : "omit if ok",
                "reason": "omit if ok",
                "search_index": { // same idea as for search, my understanding from https://docs.couchdb.org/en/stable/api/ddoc/nouveau.html#db-design-ddoc-nouveau-info-index is that this is still called search_index for nouveau
                }
            }
        }
    }
}

I think this kind of structure would make the types from the existing info endpoints reusable under the view_index and search_index keys as well as making room for success/error information.

The existing info responses are objects pairing e.g. a name and view_index so potentially even better alignment to those would be possible by using arrays rather than dictionaries e.g.

        "view_indexes": [
            {
                "name": "_design/cookbook",
                "view_index": {
                    // existing view_index model from _info
                }
            }
        ], //etc

but that gets us back to the place where those existing info responses don't have ok/error/reason. Another way around that would be splitting success/error at a higher level e.g.

{
    "db_name": "recipes", // instead of "name" to make it clear what name this is
    "copies_expected": 6,
    "indexes" : {
        "view_indexes": [
            {
                "name": "_design/cookbook",
                "view_index": {
                    // existing view_index model from _info
                }
            }
        ],
        "search_indexes": [...],
        "nouveau_indexes": [...]
    },
    "error_indexes" : {
        "view_indexes": [
            {
                "name" : "_design/recipebook",
                "error" : "foo",
                "reason": "bar",
            }
        ],
        "search_indexes": [...],
        "nouveau_indexes": [...]
    },

Food for thought anyway.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh I should also have said re-using the existing structures depends on updates_pending matching in both places, the existing one for view_index has minimum, preferred, total- IIUC maximum and copies are new and not renames of these existing values, but is there any reason not to include them in the existing endpoints so that the schema is the same between the *_info and this bulk version?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah I like using arrays rather than dictionaries a bit better. Excellent point @ricellis

Oh I should also have said re-using the existing structures depends on updates_pending matching in both places, the existing one for view_index has minimum, preferred, total- IIUC maximum and copies are new and not renames of these existing values, but is there any reason not to include them in the existing endpoints so that the schema is the same between the *_info and this bulk version?

The issue is with the data they return today. It's simply broken. We don't actually wait for all the shards in those endpoints to properly report min/max values. We could fix those I suppose, too but that's a slightly bigger change.

I'll try to get a list of objects shape first going and we can see how it looks


Invalid ``type`` values return a ``400 Bad Request``:

.. code-block:: javascript

{
"error": "query_parse_error",
"reason": "Invalid index type: foo. Must be view, search or nouveau"
}
4 changes: 4 additions & 0 deletions src/docs/src/api/ddoc/common.rst
Original file line number Diff line number Diff line change
Expand Up @@ -148,6 +148,10 @@
index, index size and current status of the design document and associated
index information.

To obtain this information for every design document of a database,
including search and nouveau indexes, in a single request see
:get:`/{db}/_index_info`.

:param db: Database name
:param ddoc: Design document name
:<header Accept: - :mimetype:`application/json`
Expand Down
10 changes: 9 additions & 1 deletion src/docs/src/api/ddoc/nouveau.rst
Original file line number Diff line number Diff line change
Expand Up @@ -143,6 +143,14 @@
"search_index": {
"num_docs": 1000,
"update_seq": 5000,
"disk_size": 1048576
"disk_size": 1048576,
"pending_updates": 100,
"signature": "ef30790cdaf74ad78daebee9597a17393d875db085184cf688aa0e8abcb972f2"
}
}

.. versionadded:: 3.6
``pending_updates`` is the number of database updates the index still
has to process, summed over shard ranges. ``0`` means the index is up to
date with the database. To obtain it for every index of a database in one
request see :get:`/{db}/_index_info`.
10 changes: 9 additions & 1 deletion src/docs/src/api/ddoc/search.rst
Original file line number Diff line number Diff line change
Expand Up @@ -169,6 +169,14 @@
"doc_del_count": 129180,
"doc_count": 1066173,
"disk_size": 728305827,
"committed_seq": 7125496
"committed_seq": 7125496,
"pending_updates": 0,
"signature": "13083011f9554446a4ac093927e75d07"
}
}

.. versionadded:: 3.6
``pending_updates`` is the number of database updates the index still
has to process, summed over shard ranges. ``0`` means the index is up to
date with the database. To obtain it for every index of a database in one
request see :get:`/{db}/_index_info`.
Loading