From 98bdca889e29faf17df2d8297185e3d577896e76 Mon Sep 17 00:00:00 2001 From: Alex Petenchea Date: Sat, 29 Aug 2026 16:32:17 +0800 Subject: [PATCH 1/7] Updated starter examples --- starter.sh | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/starter.sh b/starter.sh index 6ca977c7..d8f79b11 100755 --- a/starter.sh +++ b/starter.sh @@ -8,8 +8,8 @@ # ./starter.sh [single|cluster] [image[:tag]] # Example: # ./starter.sh cluster enterprise 3.12.4 -# ./starter.sh single enterprise-preview 4.0-nightly -# ./starter.sh single arangodb/enterprise-preview:4.0-nightly +# ./starter.sh single core-preview 4.0-nightly +# ./starter.sh single arangodb/enterprise-preview:3.12-nightly setup="${1:-single}" image="${2:-community}" From ad4b8d4ca4c28736e5ae9757ccece8d92b0e59ec Mon Sep 17 00:00:00 2001 From: Alex Petenchea Date: Sat, 29 Aug 2026 16:33:36 +0800 Subject: [PATCH 2/7] Added with_hidden parameter for Collection::indexes --- arango/collection.py | 10 ++++++-- arango/formatter.py | 2 ++ tests/test_index.py | 55 +++++++++++++++++++++++++++++++++----------- 3 files changed, 52 insertions(+), 15 deletions(-) diff --git a/arango/collection.py b/arango/collection.py index 70ca49b0..70bee723 100644 --- a/arango/collection.py +++ b/arango/collection.py @@ -1274,17 +1274,23 @@ def response_handler(resp: Response) -> Optional[Json]: # Index Management # #################### - def indexes(self) -> Result[Jsons]: + def indexes(self, with_hidden: bool = False) -> Result[Jsons]: """Return the collection indexes. + :param with_hidden: Include hidden index details. + :type with_hidden: bool :return: Collection indexes. :rtype: [dict] :raise arango.exceptions.IndexListError: If retrieval fails. """ + params: Params = {"collection": self.name} + if with_hidden: + params["withHidden"] = True + request = Request( method="get", endpoint="/_api/index", - params={"collection": self.name}, + params=params, ) def response_handler(resp: Response) -> Jsons: diff --git a/arango/formatter.py b/arango/formatter.py index e540fb40..7b6334cc 100644 --- a/arango/formatter.py +++ b/arango/formatter.py @@ -123,6 +123,8 @@ def format_index(body: Json, formatter: bool = True) -> Json: result["error_message"] = body["errorMessage"] if "trainingState" in body: result["training_state"] = body["trainingState"] + if "shards" in body: + result["shards"] = body["shards"] return verify_format(body, result) diff --git a/tests/test_index.py b/tests/test_index.py index 0dfd6add..6978cdcd 100644 --- a/tests/test_index.py +++ b/tests/test_index.py @@ -27,6 +27,25 @@ def test_list_indexes(icol, bad_col): assert err.value.error_code in {11, 1228} +def test_list_indexes_with_hidden(icol, monkeypatch): + requests = [] + + def execute(request, response_handler): + requests.append(request) + return [] + + monkeypatch.setattr(icol, "_execute", execute) + + assert icol.indexes() == [] + assert requests[-1].params == {"collection": icol.name} + + assert icol.indexes(with_hidden=True) == [] + assert requests[-1].params == { + "collection": icol.name, + "withHidden": "1", + } + + def test_get_index(icol, bad_col): indexes = icol.indexes() for index in indexes: @@ -253,24 +272,34 @@ def test_add_mdi_index(icol, db_version): icol.delete_index(result["id"]) -def test_add_vector_index(col): +def test_add_vector_index(col, db_version): docs = [] for i in range(100): docs.append({"_key": generate_doc_key(), "x": [1] * 128}) col.insert_many(docs) - result = col.add_index( - { - "type": "vector", - "fields": ["x"], - "name": "vector_index", - "params": { - "metric": "cosine", - "dimension": 128, - "nLists": 2, - }, - } - ) + index = { + "type": "vector", + "fields": ["x"], + "name": "vector_index", + "params": { + "metric": "cosine", + "dimension": 128, + "nLists": 2, + }, + } + + result = col.add_index(index) assert result["name"] == "vector_index" + + indexes = col.indexes(with_hidden=True) + + if db_version >= version.parse("3.12.10"): + details = next(item for item in indexes if item["id"] == result["id"]) + + assert details["shards"] is not None + for status in details["shards"].values(): + assert {"trainingState", "error", "resolvedNLists"} <= status.keys() + col.delete_index(result["id"]) From 3e0636237152ce986b47eaa73d5945c939f9f03f Mon Sep 17 00:00:00 2001 From: Alex Petenchea Date: Sat, 29 Aug 2026 17:48:56 +0800 Subject: [PATCH 3/7] Updated vector index test --- tests/test_index.py | 136 ++++++++++++++++++++++++++++++++++++++------ 1 file changed, 119 insertions(+), 17 deletions(-) diff --git a/tests/test_index.py b/tests/test_index.py index 6978cdcd..d68a6779 100644 --- a/tests/test_index.py +++ b/tests/test_index.py @@ -273,34 +273,136 @@ def test_add_mdi_index(icol, db_version): def test_add_vector_index(col, db_version): + # Insert vector data. docs = [] for i in range(100): - docs.append({"_key": generate_doc_key(), "x": [1] * 128}) + docs.append( + { + "_key": generate_doc_key(), + "x": [1] * 128, + "y": [1] * 128, + } + ) col.insert_many(docs) - index = { - "type": "vector", - "fields": ["x"], - "name": "vector_index", - "params": { - "metric": "cosine", - "dimension": 128, - "nLists": 2, + + # Test basic compatibility. + index_meta = [ + { + "type": "vector", + "fields": ["x"], + "name": "vector_index_1", + "params": { + "metric": "cosine", + "dimension": 128, + "nLists": 2, + }, }, - } + { + "type": "vector", + "fields": ["y"], + "name": "vector_index_2", + "params": { + "metric": "cosine", + "dimension": 128, + "nLists": 3, + }, + }, + ] - result = col.add_index(index) - assert result["name"] == "vector_index" + results = [col.add_index(index_meta[0]), col.add_index(index_meta[1])] + assert results[0]["name"] == "vector_index_1" + assert results[1]["name"] == "vector_index_2" + # Test hidden shard details. indexes = col.indexes(with_hidden=True) if db_version >= version.parse("3.12.10"): - details = next(item for item in indexes if item["id"] == result["id"]) + details = {item["id"]: item for item in indexes} + for result in results: + shards = details[result["id"]]["shards"] + assert shards is not None + for status in shards.values(): + assert {"trainingState", "error", "resolvedNLists"} <= status.keys() - assert details["shards"] is not None - for status in details["shards"].values(): - assert {"trainingState", "error", "resolvedNLists"} <= status.keys() + col.delete_index(results[0]["id"]) + col.delete_index(results[1]["id"]) - col.delete_index(result["id"]) + if db_version >= version.parse("3.12.10"): + # Test server-managed nLists. + default_index = { + "type": "vector", + "fields": ["x"], + "name": "vector_index_default", + "params": { + "metric": "cosine", + "dimension": 128, + }, + } + scaling_n_lists = { + "strategy": "autoSqrt", + "multiplier": 1, + "minNLists": 2, + "tiers": [], + } + scaling_index = { + "type": "vector", + "fields": ["y"], + "name": "vector_index_scaling", + "params": { + "metric": "cosine", + "dimension": 128, + "nLists": scaling_n_lists, + "numberOfDocsPerCentroid": 10, + "factory": "IVF{},Flat", + }, + } + + default_result = col.add_index(default_index) + scaling_result = col.add_index(scaling_index) + + default_n_lists = default_result["params"]["nLists"] + assert default_n_lists["strategy"] == "autoSqrt" + assert default_n_lists["multiplier"] == 4 + assert default_n_lists["minNLists"] == 2 + assert scaling_result["params"]["nLists"] == scaling_n_lists + assert scaling_result["params"]["numberOfDocsPerCentroid"] == 10 + assert scaling_result["params"]["factory"] == "IVF{},Flat" + + col.delete_index(default_result["id"]) + col.delete_index(scaling_result["id"]) + + # Test unusable creation. + unusable_index = { + "type": "vector", + "fields": ["x"], + "name": "vector_index_unusable", + "params": { + "metric": "cosine", + "dimension": 128, + "nLists": 2, + "factory": "IVF3,Flat", + }, + } + unusable_result = col.add_index(unusable_index) + assert unusable_result["trainingState"] == "unusable" + assert unusable_result["errorMessage"] + col.delete_index(unusable_result["id"]) + + # Test invalid request failure. + with assert_raises(IndexCreateError) as err: + col.add_index( + { + "type": "vector", + "fields": ["x"], + "name": "vector_index_invalid", + "params": { + "metric": "cosine", + "dimension": 128, + "nLists": 0, # must be greater than 0 + }, + } + ) + assert err.value.http_code == 400 def test_delete_index(icol, bad_col): From 70942f0249eb3e7d7dce54e000e37c36574e3383 Mon Sep 17 00:00:00 2001 From: Alex Petenchea Date: Sat, 29 Aug 2026 18:05:33 +0800 Subject: [PATCH 4/7] Updated index docs --- .github/workflows/docs.yaml | 2 +- docs/indexes.rst | 39 +++++++++++++++++++++++++++++++++++++ 2 files changed, 40 insertions(+), 1 deletion(-) diff --git a/.github/workflows/docs.yaml b/.github/workflows/docs.yaml index a775cd00..ef3f5049 100644 --- a/.github/workflows/docs.yaml +++ b/.github/workflows/docs.yaml @@ -19,7 +19,7 @@ jobs: - name: Create ArangoDB Docker container run: > docker create --name arango -p 8529:8529 -e ARANGO_ROOT_PASSWORD=passwd -v "$(pwd)/tests/static/":/tests/static - arangodb/arangodb:latest --server.jwt-secret-keyfile=/tests/static/keyfile + arangodb/arangodb:latest --server.jwt-secret-keyfile=/tests/static/keyfile --vector-index=true - name: Start ArangoDB Docker container run: docker start arango diff --git a/docs/indexes.rst b/docs/indexes.rst index 8df3048f..95e55c7f 100644 --- a/docs/indexes.rst +++ b/docs/indexes.rst @@ -62,4 +62,43 @@ on fields ``_from`` and ``_to``. For more information on indexes, refer to # Delete the last index from the collection. cities.delete_index(index['id']) + # Insert documents with vector embeddings. + cities.insert_many([ + { + '_key': f'city{i}', + 'continent': f'continent{i}', + 'country': f'country{i}', + 'population': i, + 'coordinates': [float(i % 180), float(i % 90)], + 'x': float(i), + 'y': float(i), + 'embedding': [float(i), float(i % 7), float(i % 11), 1.0], + } + for i in range(100) + ]) + + # Let ArangoDB determine the number of vector-index centroids. + vector_index = cities.add_index({ + 'type': 'vector', + 'fields': ['embedding'], + 'name': 'vector_index', + 'params': { + 'metric': 'cosine', + 'dimension': 4, + }, + }) + + # Index creation may succeed even if vector training fails. + if vector_index.get('trainingState') != 'ready': + raise RuntimeError( + vector_index.get('errorMessage', 'Vector index is not ready') + ) + +Omitted or scaling-object ``nLists``, ``numberOfDocsPerCentroid``, factory +placeholders such as ``IVF{},Flat``, and successful-but-unusable creation +behavior require ArangoDB 3.12.10 or later. A successful creation response +means that the index exists, but callers should check ``trainingState`` before +using it. If training fails permanently, the state is ``"unusable"`` and +``errorMessage`` describes the failure. + See :ref:`StandardCollection` for API specification. From e3d6fe604ca0e8c38ffc4acb164d13c11bc131b7 Mon Sep 17 00:00:00 2001 From: Alex Petenchea Date: Sat, 29 Aug 2026 18:17:04 +0800 Subject: [PATCH 5/7] Added with_stats parameter to `Collection::indexes` --- arango/collection.py | 10 ++++++++-- arango/formatter.py | 2 ++ tests/test_index.py | 18 +++++++++++++++++- 3 files changed, 27 insertions(+), 3 deletions(-) diff --git a/arango/collection.py b/arango/collection.py index 70bee723..5a2e7f8d 100644 --- a/arango/collection.py +++ b/arango/collection.py @@ -1274,16 +1274,22 @@ def response_handler(resp: Response) -> Optional[Json]: # Index Management # #################### - def indexes(self, with_hidden: bool = False) -> Result[Jsons]: + def indexes( + self, with_stats: bool = False, with_hidden: bool = False + ) -> Result[Jsons]: """Return the collection indexes. - :param with_hidden: Include hidden index details. + :param with_stats: Whether to include figures and estimates in the result. + :type with_stats: bool + :param with_hidden: Whether to include hidden indexes in the result. :type with_hidden: bool :return: Collection indexes. :rtype: [dict] :raise arango.exceptions.IndexListError: If retrieval fails. """ params: Params = {"collection": self.name} + if with_stats: + params["withStats"] = True if with_hidden: params["withHidden"] = True diff --git a/arango/formatter.py b/arango/formatter.py index 7b6334cc..d28e3c5e 100644 --- a/arango/formatter.py +++ b/arango/formatter.py @@ -73,6 +73,8 @@ def format_index(body: Json, formatter: bool = True) -> Json: result["cacheEnabled"] = body["cacheEnabled"] if "legacyPolygons" in body: result["legacyPolygons"] = body["legacyPolygons"] + if "figures" in body: + result["figures"] = body["figures"] if "estimates" in body: result["estimates"] = body["estimates"] if "analyzer" in body: diff --git a/tests/test_index.py b/tests/test_index.py index d68a6779..1e3759b3 100644 --- a/tests/test_index.py +++ b/tests/test_index.py @@ -22,12 +22,15 @@ def test_list_indexes(icol, bad_col): assert "sparse" in indexes[0] assert "unique" in indexes[0] + indexes = icol.indexes(with_stats=True) + assert "figures" in indexes[0] + with assert_raises(IndexListError) as err: bad_col.indexes() assert err.value.error_code in {11, 1228} -def test_list_indexes_with_hidden(icol, monkeypatch): +def test_list_indexes_options(icol, monkeypatch): requests = [] def execute(request, response_handler): @@ -45,6 +48,19 @@ def execute(request, response_handler): "withHidden": "1", } + assert icol.indexes(with_stats=True) == [] + assert requests[-1].params == { + "collection": icol.name, + "withStats": "1", + } + + assert icol.indexes(with_stats=True, with_hidden=True) == [] + assert requests[-1].params == { + "collection": icol.name, + "withStats": "1", + "withHidden": "1", + } + def test_get_index(icol, bad_col): indexes = icol.indexes() From cd45b8803134f7ac23007fd0b0fcb93708c3310e Mon Sep 17 00:00:00 2001 From: Alex Petenchea Date: Sat, 29 Aug 2026 18:23:32 +0800 Subject: [PATCH 6/7] Updated docs tests --- .github/workflows/docs.yaml | 13 ++++++++++++- docs/foxx.rst | 2 +- 2 files changed, 13 insertions(+), 2 deletions(-) diff --git a/.github/workflows/docs.yaml b/.github/workflows/docs.yaml index ef3f5049..97a1e1c5 100644 --- a/.github/workflows/docs.yaml +++ b/.github/workflows/docs.yaml @@ -19,11 +19,22 @@ jobs: - name: Create ArangoDB Docker container run: > docker create --name arango -p 8529:8529 -e ARANGO_ROOT_PASSWORD=passwd -v "$(pwd)/tests/static/":/tests/static - arangodb/arangodb:latest --server.jwt-secret-keyfile=/tests/static/keyfile --vector-index=true + arangodb/enterprise:latest --server.jwt-secret-keyfile=/tests/static/keyfile --vector-index=true - name: Start ArangoDB Docker container run: docker start arango + - name: Wait for ArangoDB + run: | + for _ in {1..30}; do + if curl --fail --silent --user root:passwd http://localhost:8529/_api/version > /dev/null; then + exit 0 + fi + sleep 1 + done + docker logs arango + exit 1 + - name: Set up Python uses: actions/setup-python@v7 with: diff --git a/docs/foxx.rst b/docs/foxx.rst index 0e48262a..43ecaa69 100644 --- a/docs/foxx.rst +++ b/docs/foxx.rst @@ -13,7 +13,7 @@ information, refer to `ArangoDB manual`_. **Example:** -.. testcode:: +.. code-block:: python from arango import ArangoClient From 2f553983ccac2f1aa0c44b09ddd3ffc9d9d112e1 Mon Sep 17 00:00:00 2001 From: Alex Petenchea Date: Sat, 29 Aug 2026 18:27:30 +0800 Subject: [PATCH 7/7] Removed unused file --- ci/conftest.py | 1 - 1 file changed, 1 deletion(-) delete mode 100644 ci/conftest.py diff --git a/ci/conftest.py b/ci/conftest.py deleted file mode 100644 index 133e3685..00000000 --- a/ci/conftest.py +++ /dev/null @@ -1 +0,0 @@ -from tests.conftest import * # noqa: F401,F403