diff --git a/apps/desktop/src/components/meilisearch/MeilisearchIndexView.vue b/apps/desktop/src/components/meilisearch/MeilisearchIndexView.vue
index 013d83cd03..df6f2a3ead 100644
--- a/apps/desktop/src/components/meilisearch/MeilisearchIndexView.vue
+++ b/apps/desktop/src/components/meilisearch/MeilisearchIndexView.vue
@@ -100,8 +100,17 @@ onMounted(() => {
{{ t("meilisearch.documentCountLabel") }}
{{ overview ? overview.numberOfDocuments : "-" }}
-
-
{{ t("meilisearch.databaseSize") }}
+
+
{{ t("meilisearch.documentSize") }}
+
{{ formatBytes(overview.documentSize) }}
+
+
+
{{ t("meilisearch.avgDocumentSize") }}
+
{{ formatBytes(overview.avgDocumentSize) }}
+
+
+
+
{{ t("meilisearch.instanceDatabaseSize") }}
{{ formatBytes(overview.databaseSize) }}
diff --git a/apps/desktop/src/components/meilisearch/__tests__/MeilisearchIndexView.spec.ts b/apps/desktop/src/components/meilisearch/__tests__/MeilisearchIndexView.spec.ts
new file mode 100644
index 0000000000..56f591f761
--- /dev/null
+++ b/apps/desktop/src/components/meilisearch/__tests__/MeilisearchIndexView.spec.ts
@@ -0,0 +1,107 @@
+// @vitest-environment happy-dom
+
+import { createApp, defineComponent, h, nextTick, ref, type Component } from "vue";
+import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
+
+const mocks = vi.hoisted(() => ({ getIndexOverview: vi.fn() }));
+
+function passthrough(tag: string): Component {
+ return defineComponent({
+ inheritAttrs: false,
+ setup(_, { attrs, slots }) {
+ return () => h(tag, attrs, slots.default?.());
+ },
+ });
+}
+
+vi.mock("vue-i18n", () => ({
+ useI18n: () => ({ locale: ref("zh-CN"), t: (key: string) => key }),
+}));
+vi.mock("@lucide/vue", () => ({
+ Copy: passthrough("span"),
+ FileText: passthrough("span"),
+ ListChecks: passthrough("span"),
+ Settings: passthrough("span"),
+}));
+vi.mock("@/lib/backend/api", () => ({ meilisearchGetIndexOverview: mocks.getIndexOverview }));
+vi.mock("@/composables/useToast", () => ({ useToast: () => ({ toast: vi.fn() }) }));
+vi.mock("@/lib/tabs/tabUiState", () => ({ useTabUiState: () => ({ initialState: {}, track: () => {} }) }));
+vi.mock("@/components/meilisearch/MeilisearchDocumentsPage.vue", () => ({ default: passthrough("div") }));
+vi.mock("@/components/meilisearch/MeilisearchSettingsPage.vue", () => ({ default: passthrough("div") }));
+vi.mock("@/components/meilisearch/MeilisearchTasksPage.vue", () => ({ default: passthrough("div") }));
+
+import MeilisearchIndexView from "@/components/meilisearch/MeilisearchIndexView.vue";
+
+let app: ReturnType | undefined;
+let root: HTMLDivElement | undefined;
+
+async function mountIndexView() {
+ root = document.createElement("div");
+ document.body.append(root);
+ app = createApp(MeilisearchIndexView, { connectionId: "c1", index: "report_template" });
+ app.mount(root);
+ await vi.waitFor(() => expect(mocks.getIndexOverview).toHaveBeenCalledTimes(1));
+ await nextTick();
+ return root;
+}
+
+beforeEach(() => {
+ mocks.getIndexOverview.mockReset();
+});
+
+afterEach(() => {
+ app?.unmount();
+ root?.remove();
+ app = undefined;
+ root = undefined;
+ vi.clearAllMocks();
+});
+
+describe("MeilisearchIndexView storage rows", () => {
+ // Meilisearch >= 1.14 reports the index's own sizes, so the sidebar must show
+ // them. Reporting the instance-wide `databaseSize` here made every index look
+ // like it owned the whole instance (issue #9880).
+ it("shows the index's own size instead of the instance-wide number", async () => {
+ mocks.getIndexOverview.mockResolvedValue({
+ uid: "report_template",
+ primaryKey: "id",
+ createdAt: "2026-09-21T06:30:00Z",
+ updatedAt: "2026-09-21T06:30:46Z",
+ numberOfDocuments: 256,
+ isIndexing: false,
+ documentSize: 309_329_920,
+ avgDocumentSize: 1_208_320,
+ databaseSize: 4_194_304_000,
+ });
+
+ const mounted = await mountIndexView();
+ const text = mounted.textContent ?? "";
+
+ expect(text).toContain("meilisearch.documentSize");
+ expect(text).toContain("meilisearch.avgDocumentSize");
+ expect(text).not.toContain("meilisearch.instanceDatabaseSize");
+ expect(text).toContain("295.0 MB");
+ expect(text).not.toContain("3.9 GB");
+ });
+
+ it("labels the instance-wide size as such on servers without per-index sizes", async () => {
+ mocks.getIndexOverview.mockResolvedValue({
+ uid: "report_template",
+ primaryKey: "id",
+ createdAt: null,
+ updatedAt: null,
+ numberOfDocuments: 256,
+ isIndexing: false,
+ documentSize: null,
+ avgDocumentSize: null,
+ databaseSize: 26_935_296,
+ });
+
+ const mounted = await mountIndexView();
+ const text = mounted.textContent ?? "";
+
+ expect(text).toContain("meilisearch.instanceDatabaseSize");
+ expect(text).not.toContain("meilisearch.documentSize");
+ expect(text).toContain("25.7 MB");
+ });
+});
diff --git a/apps/desktop/src/i18n/locales/az.ts b/apps/desktop/src/i18n/locales/az.ts
index 652f108743..b6e9f9a929 100644
--- a/apps/desktop/src/i18n/locales/az.ts
+++ b/apps/desktop/src/i18n/locales/az.ts
@@ -5610,6 +5610,9 @@ export default withEnglishFallback({
primaryKey: "Əsas açar",
documentCountLabel: "Sənədlər",
databaseSize: "Verilənlər bazasının ölçüsü",
+ documentSize: "Sənəd ölçüsü",
+ avgDocumentSize: "Orta sənəd ölçüsü",
+ instanceDatabaseSize: "İnstansiya verilənlər bazasının ölçüsü",
copyDocument: "JSON-u kopyala",
copied: "Kopyalandı",
autoRefresh: "Avtomatik yeniləmə",
diff --git a/apps/desktop/src/i18n/locales/en.ts b/apps/desktop/src/i18n/locales/en.ts
index 2ddfc9f260..294f3c660c 100644
--- a/apps/desktop/src/i18n/locales/en.ts
+++ b/apps/desktop/src/i18n/locales/en.ts
@@ -6029,6 +6029,9 @@ export default {
primaryKey: "Primary key",
documentCountLabel: "Documents",
databaseSize: "Database size",
+ documentSize: "Document size",
+ avgDocumentSize: "Average document size",
+ instanceDatabaseSize: "Instance database size",
copyDocument: "Copy JSON",
copied: "Copied",
autoRefresh: "Auto refresh",
diff --git a/apps/desktop/src/i18n/locales/es.ts b/apps/desktop/src/i18n/locales/es.ts
index f5da14c118..f3bcf9800c 100644
--- a/apps/desktop/src/i18n/locales/es.ts
+++ b/apps/desktop/src/i18n/locales/es.ts
@@ -5571,6 +5571,9 @@ export default withEnglishFallback({
primaryKey: "Clave primaria",
documentCountLabel: "Documentos",
databaseSize: "Tamaño de la base de datos",
+ documentSize: "Tamaño del documento",
+ avgDocumentSize: "Tamaño medio del documento",
+ instanceDatabaseSize: "Tamaño de la base de datos de la instancia",
copyDocument: "Copiar JSON",
copied: "Copiado",
autoRefresh: "Actualización automática",
diff --git a/apps/desktop/src/i18n/locales/it.ts b/apps/desktop/src/i18n/locales/it.ts
index 369d28d0ae..3a45613782 100644
--- a/apps/desktop/src/i18n/locales/it.ts
+++ b/apps/desktop/src/i18n/locales/it.ts
@@ -5568,6 +5568,9 @@ export default withEnglishFallback({
primaryKey: "Chiave primaria",
documentCountLabel: "Documenti",
databaseSize: "Dimensione del database",
+ documentSize: "Dimensione del documento",
+ avgDocumentSize: "Dimensione media del documento",
+ instanceDatabaseSize: "Dimensione del database dell'istanza",
copyDocument: "Copia JSON",
copied: "Copiato",
autoRefresh: "Aggiornamento automatico",
diff --git a/apps/desktop/src/i18n/locales/ja.ts b/apps/desktop/src/i18n/locales/ja.ts
index a016314fa9..dfb3b2451d 100644
--- a/apps/desktop/src/i18n/locales/ja.ts
+++ b/apps/desktop/src/i18n/locales/ja.ts
@@ -5596,6 +5596,9 @@ export default withEnglishFallback({
primaryKey: "主キー",
documentCountLabel: "ドキュメント数",
databaseSize: "データベースサイズ",
+ documentSize: "ドキュメントサイズ",
+ avgDocumentSize: "平均ドキュメントサイズ",
+ instanceDatabaseSize: "インスタンスのデータベースサイズ",
copyDocument: "JSON をコピー",
copied: "コピーしました",
autoRefresh: "自動更新",
diff --git a/apps/desktop/src/i18n/locales/ko.ts b/apps/desktop/src/i18n/locales/ko.ts
index 714721e356..1e15bf7a05 100644
--- a/apps/desktop/src/i18n/locales/ko.ts
+++ b/apps/desktop/src/i18n/locales/ko.ts
@@ -5215,6 +5215,9 @@ export default withEnglishFallback({
primaryKey: "기본 키",
documentCountLabel: "문서 수",
databaseSize: "데이터베이스 크기",
+ documentSize: "문서 크기",
+ avgDocumentSize: "평균 문서 크기",
+ instanceDatabaseSize: "인스턴스 데이터베이스 크기",
copyDocument: "JSON 복사",
copied: "복사됨",
autoRefresh: "자동 새로고침",
diff --git a/apps/desktop/src/i18n/locales/pt-BR.ts b/apps/desktop/src/i18n/locales/pt-BR.ts
index dbc14da35a..624661440d 100644
--- a/apps/desktop/src/i18n/locales/pt-BR.ts
+++ b/apps/desktop/src/i18n/locales/pt-BR.ts
@@ -5570,6 +5570,9 @@ export default withEnglishFallback({
primaryKey: "Chave primária",
documentCountLabel: "Documentos",
databaseSize: "Tamanho do banco de dados",
+ documentSize: "Tamanho do documento",
+ avgDocumentSize: "Tamanho médio do documento",
+ instanceDatabaseSize: "Tamanho do banco de dados da instância",
copyDocument: "Copiar JSON",
copied: "Copiado",
autoRefresh: "Atualização automática",
diff --git a/apps/desktop/src/i18n/locales/tr.ts b/apps/desktop/src/i18n/locales/tr.ts
index d20465e484..e502af9afa 100644
--- a/apps/desktop/src/i18n/locales/tr.ts
+++ b/apps/desktop/src/i18n/locales/tr.ts
@@ -5504,6 +5504,9 @@ export default withEnglishFallback({
primaryKey: "Birincil anahtar",
documentCountLabel: "Belgeler",
databaseSize: "Veritabanı boyutu",
+ documentSize: "Belge boyutu",
+ avgDocumentSize: "Ortalama belge boyutu",
+ instanceDatabaseSize: "Örnek veritabanı boyutu",
copyDocument: "JSON Kopyala",
copied: "Kopyalandı",
autoRefresh: "Otomatik yenile",
diff --git a/apps/desktop/src/i18n/locales/zh-CN.ts b/apps/desktop/src/i18n/locales/zh-CN.ts
index cb4ee58d49..9a33737e8d 100644
--- a/apps/desktop/src/i18n/locales/zh-CN.ts
+++ b/apps/desktop/src/i18n/locales/zh-CN.ts
@@ -6002,6 +6002,9 @@ export default withEnglishFallback({
primaryKey: "主键",
documentCountLabel: "文档数",
databaseSize: "数据库大小",
+ documentSize: "文档大小",
+ avgDocumentSize: "平均文档大小",
+ instanceDatabaseSize: "实例数据库大小",
copyDocument: "复制 JSON",
copied: "已复制",
autoRefresh: "自动刷新",
diff --git a/apps/desktop/src/i18n/locales/zh-TW.ts b/apps/desktop/src/i18n/locales/zh-TW.ts
index f26c554b64..974982a7fb 100644
--- a/apps/desktop/src/i18n/locales/zh-TW.ts
+++ b/apps/desktop/src/i18n/locales/zh-TW.ts
@@ -4885,6 +4885,9 @@ export default withEnglishFallback({
primaryKey: "主鍵",
documentCountLabel: "文件數",
databaseSize: "資料庫大小",
+ documentSize: "文件大小",
+ avgDocumentSize: "平均文件大小",
+ instanceDatabaseSize: "實例資料庫大小",
copyDocument: "複製 JSON",
copied: "已複製",
autoRefresh: "自動重新整理",
diff --git a/apps/desktop/src/lib/backend/tauri.ts b/apps/desktop/src/lib/backend/tauri.ts
index 76e639c572..0606ebf22c 100644
--- a/apps/desktop/src/lib/backend/tauri.ts
+++ b/apps/desktop/src/lib/backend/tauri.ts
@@ -4739,6 +4739,11 @@ export interface MeilisearchIndexOverview {
updatedAt: string | null;
numberOfDocuments: number;
isIndexing: boolean;
+ /** Raw document store size of this index (Meilisearch >= 1.14); null on older servers. */
+ documentSize: number | null;
+ /** Average document size of this index (Meilisearch >= 1.14); null on older servers. */
+ avgDocumentSize: number | null;
+ /** Instance-wide database size; every index shares it, so it is only a fallback. */
databaseSize: number | null;
}
diff --git a/crates/dbx-drivers/src/db/meilisearch_driver.rs b/crates/dbx-drivers/src/db/meilisearch_driver.rs
index 9e17e94f8f..d0b99add62 100644
--- a/crates/dbx-drivers/src/db/meilisearch_driver.rs
+++ b/crates/dbx-drivers/src/db/meilisearch_driver.rs
@@ -1093,10 +1093,39 @@ pub struct MeilisearchIndexOverview {
pub updated_at: Option,
pub number_of_documents: u64,
pub is_indexing: bool,
+ /// Raw document store size of *this* index in bytes (`rawDocumentDbSize`,
+ /// Meilisearch >= 1.14). `None` on servers that do not report per-index sizes.
+ pub document_size: Option,
+ /// Average size of one document in this index in bytes (`avgDocumentSize`,
+ /// Meilisearch >= 1.14). `None` on older servers.
+ pub avg_document_size: Option,
/// Instance-wide database size in bytes; `None` when the API key cannot read instance stats.
+ /// It counts every index of the instance, so the UI only uses it as a fallback.
pub database_size: Option,
}
+/// Builds the index overview from the three responses the tab needs. Kept pure
+/// so the per-index size fields (Meilisearch >= 1.14) and the older-server
+/// fallback can be covered without a live server.
+fn parse_index_overview(
+ index: &str,
+ info: &Value,
+ stats: &Value,
+ instance_stats: Option<&Value>,
+) -> MeilisearchIndexOverview {
+ MeilisearchIndexOverview {
+ uid: info.get("uid").and_then(Value::as_str).unwrap_or(index).to_string(),
+ primary_key: info.get("primaryKey").and_then(Value::as_str).map(str::to_string),
+ created_at: info.get("createdAt").and_then(Value::as_str).map(str::to_string),
+ updated_at: info.get("updatedAt").and_then(Value::as_str).map(str::to_string),
+ number_of_documents: stats.get("numberOfDocuments").and_then(Value::as_u64).unwrap_or(0),
+ is_indexing: stats.get("isIndexing").and_then(Value::as_bool).unwrap_or(false),
+ document_size: stats.get("rawDocumentDbSize").and_then(Value::as_u64),
+ avg_document_size: stats.get("avgDocumentSize").and_then(Value::as_u64),
+ database_size: instance_stats.and_then(|value| value.get("databaseSize")).and_then(Value::as_u64),
+ }
+}
+
pub async fn get_index_overview(client: &MeilisearchClient, index: &str) -> Result {
let info = response_json(
client
@@ -1109,22 +1138,11 @@ pub async fn get_index_overview(client: &MeilisearchClient, index: &str) -> Resu
.await?;
let stats = get_index_stats(client, index).await?;
// Instance stats require broader key permissions, so keep them best-effort.
- let database_size = match client.get("/stats").send().await {
- Ok(response) => response_json(response, "instance stats lookup")
- .await
- .ok()
- .and_then(|value| value.get("databaseSize").and_then(Value::as_u64)),
+ let instance_stats = match client.get("/stats").send().await {
+ Ok(response) => response_json(response, "instance stats lookup").await.ok(),
Err(_) => None,
};
- Ok(MeilisearchIndexOverview {
- uid: info.get("uid").and_then(Value::as_str).unwrap_or(index).to_string(),
- primary_key: info.get("primaryKey").and_then(Value::as_str).map(str::to_string),
- created_at: info.get("createdAt").and_then(Value::as_str).map(str::to_string),
- updated_at: info.get("updatedAt").and_then(Value::as_str).map(str::to_string),
- number_of_documents: stats.get("numberOfDocuments").and_then(Value::as_u64).unwrap_or(0),
- is_indexing: stats.get("isIndexing").and_then(Value::as_bool).unwrap_or(false),
- database_size,
- })
+ Ok(parse_index_overview(index, &info, &stats, instance_stats.as_ref()))
}
pub async fn delete_index(client: &MeilisearchClient, index: &str) -> Result<(), String> {
@@ -1904,8 +1922,9 @@ mod tests {
use super::{
decode_bounded_rest_body, decoded_identity, ensure_task_index, filter_expression, identity_path,
meilisearch_base_url, meilisearch_filter_from_request, meilisearch_search_body, meilisearch_sort_from_request,
- parse_key, parse_meilisearch_search_response, parse_rest_request_line, parse_system_stats, task_selector_query,
- MeilisearchClient, MeilisearchOverviewSection, MeilisearchTask, MeilisearchTaskSelector,
+ parse_index_overview, parse_key, parse_meilisearch_search_response, parse_rest_request_line,
+ parse_system_stats, task_selector_query, MeilisearchClient, MeilisearchOverviewSection, MeilisearchTask,
+ MeilisearchTaskSelector,
};
use reqwest::Method;
use serde_json::{json, Value};
@@ -2037,6 +2056,57 @@ mod tests {
assert!(indexless.contains("not associated with an index"));
}
+ #[test]
+ fn index_overview_prefers_the_per_index_size_when_the_server_reports_one() {
+ // Meilisearch >= 1.14: /indexes/{uid}/stats carries the index's own sizes.
+ let overview = parse_index_overview(
+ "movies",
+ &json!({ "uid": "movies", "primaryKey": "id", "createdAt": "2026-08-01T00:00:00Z" }),
+ &json!({
+ "numberOfDocuments": 256,
+ "indexSize": 90_000_000,
+ "rawDocumentDbSize": 26_935_296,
+ "avgDocumentSize": 105_216,
+ "isIndexing": false
+ }),
+ Some(&json!({ "databaseSize": 999_999_999 })),
+ );
+
+ assert_eq!(overview.document_size, Some(26_935_296));
+ assert_eq!(overview.avg_document_size, Some(105_216));
+ // The instance-wide number stays available, but the UI must not present it as this index's size.
+ assert_eq!(overview.database_size, Some(999_999_999));
+ assert_eq!(overview.number_of_documents, 256);
+ }
+
+ #[test]
+ fn index_overview_leaves_per_index_sizes_unset_on_older_servers() {
+ // Meilisearch <= 1.13 answers index stats without rawDocumentDbSize / avgDocumentSize.
+ let overview = parse_index_overview(
+ "movies",
+ &json!({ "uid": "movies" }),
+ &json!({ "numberOfDocuments": 12, "isIndexing": true, "fieldDistribution": { "id": 12 } }),
+ Some(&json!({ "databaseSize": 4096 })),
+ );
+
+ assert_eq!(overview.document_size, None);
+ assert_eq!(overview.avg_document_size, None);
+ assert_eq!(overview.database_size, Some(4096));
+ assert_eq!(overview.number_of_documents, 12);
+ assert!(overview.is_indexing);
+ }
+
+ #[test]
+ fn index_overview_survives_a_forbidden_instance_stats_lookup() {
+ let overview = parse_index_overview("movies", &json!({}), &json!({ "numberOfDocuments": 1 }), None);
+
+ assert_eq!(overview.database_size, None);
+ assert_eq!(overview.document_size, None);
+ assert_eq!(overview.uid, "movies");
+ assert_eq!(overview.number_of_documents, 1);
+ assert!(!overview.is_indexing);
+ }
+
#[test]
fn system_stats_compute_totals_and_top_ten_deterministically() {
let indexes = (0..12)