diff --git a/src/components/Collections/CollectionsList.jsx b/src/components/Collections/CollectionsList.jsx
index bed120e1c..1bb5ab638 100644
--- a/src/components/Collections/CollectionsList.jsx
+++ b/src/components/Collections/CollectionsList.jsx
@@ -1,7 +1,7 @@
import React, { useState } from 'react';
import PropTypes from 'prop-types';
import { Link } from 'react-router';
-import { Checkbox, MenuItem, TableCell, TableRow, Typography, Table } from '@mui/material';
+import { Box, Checkbox, MenuItem, TableCell, TableRow, Tooltip, Typography, Table } from '@mui/material';
import { useTheme } from '@mui/material/styles';
import {
StyledTableBody,
@@ -74,6 +74,44 @@ CollectionNameCell.propTypes = {
collection: PropTypes.object.isRequired,
};
+// With custom sharding `shard_number` is only the *default* number of shards per shard key:
+// every shard key can be created with its own `shards_number`. So the total number of shards
+// comes from the collection cluster info, and is never derived from `shard_number`.
+const ShardsCell = ({ collectionParams, shardCount, shardKeysCount }) => {
+ const shardNumber = collectionParams.shard_number;
+
+ if (collectionParams.sharding_method !== 'custom') {
+ return {shardNumber};
+ }
+
+ if (shardCount == null) {
+ return (
+
+ {`${shardNumber} / key`}
+
+ );
+ }
+
+ const keysLabel = shardKeysCount === 1 ? '1 shard key' : `${shardKeysCount} shard keys`;
+
+ return (
+
+
+ {shardCount}
+
+ {keysLabel}
+
+
+
+ );
+};
+
+ShardsCell.propTypes = {
+ collectionParams: PropTypes.object.isRequired,
+ shardCount: PropTypes.number,
+ shardKeysCount: PropTypes.number,
+};
+
const CollectionTableRow = ({
collection,
getCollectionsCall,
@@ -145,7 +183,11 @@ const CollectionTableRow = ({
{collection.segments_count}
- {collection.config.params.shard_number}
+
diff --git a/src/components/Collections/collectionList.test.jsx b/src/components/Collections/collectionList.test.jsx
index a1d1507bc..a2806c8c6 100644
--- a/src/components/Collections/collectionList.test.jsx
+++ b/src/components/Collections/collectionList.test.jsx
@@ -60,6 +60,65 @@ const COLLECTIONS = [
},
];
+const CUSTOM_SHARDED_COLLECTIONS = [
+ {
+ name: 'Custom sharded',
+ status: 'green',
+ points_count: 100,
+ segments_count: 7,
+ config: {
+ params: {
+ shard_number: 2,
+ sharding_method: 'custom',
+ vectors: {
+ size: 128,
+ distance: 'cosine',
+ },
+ },
+ },
+ shard_count: 6,
+ shard_keys_count: 3,
+ aliases: [],
+ },
+ {
+ name: 'Custom sharded without cluster info',
+ status: 'green',
+ points_count: 200,
+ segments_count: 8,
+ config: {
+ params: {
+ shard_number: 4,
+ sharding_method: 'custom',
+ vectors: {
+ size: 128,
+ distance: 'cosine',
+ },
+ },
+ },
+ aliases: [],
+ },
+ {
+ // shard keys created with their own `shards_number`: 2 + 2 + 1 shards
+ name: 'Custom sharded with uneven shard keys',
+ status: 'green',
+ points_count: 300,
+ segments_count: 9,
+ config: {
+ params: {
+ shard_number: 2,
+ sharding_method: 'custom',
+ vectors: {
+ size: 128,
+ distance: 'cosine',
+ },
+ },
+ },
+ shard_count: 5,
+ shard_keys_count: 3,
+ aliases: [],
+ },
+];
+
const DEFAULT_SELECTION_PROPS = {
selectedCollections: new Set(),
handleToggleSelect: vi.fn(),
@@ -222,4 +281,69 @@ describe('CollectionsList', () => {
expect(btn.closest('li')).toHaveAttribute('aria-disabled', 'true');
});
});
+ it('should render shard_number as-is for automatic sharding', () => {
+ render(
+
+ {}}
+ refreshCollection={vi.fn()}
+ isRefreshing={false}
+ {...DEFAULT_SELECTION_PROPS}
+ />
+
+ );
+ expect(screen.getByText('2')).toBeInTheDocument();
+ expect(screen.queryByText(/shard keys/)).not.toBeInTheDocument();
+ });
+
+ it('should render the total number of shards for custom sharding', () => {
+ render(
+
+ {}}
+ refreshCollection={vi.fn()}
+ isRefreshing={false}
+ {...DEFAULT_SELECTION_PROPS}
+ />
+
+ );
+ // 6 shards in total (3 shard keys x 2 shards per key), not the `shard_number` of 2
+ expect(screen.getByText('6')).toBeInTheDocument();
+ expect(screen.getAllByText('3 shard keys')).toHaveLength(2);
+ expect(screen.queryByText('2')).not.toBeInTheDocument();
+ });
+
+ it('should mark shard_number as per-key when the total is unknown for custom sharding', () => {
+ render(
+
+ {}}
+ refreshCollection={vi.fn()}
+ isRefreshing={false}
+ {...DEFAULT_SELECTION_PROPS}
+ />
+
+ );
+ expect(screen.getByText('4 / key')).toBeInTheDocument();
+ });
+
+ it('should not derive the total from shard_number when shard keys have different shard counts', () => {
+ render(
+
+ {}}
+ refreshCollection={vi.fn()}
+ isRefreshing={false}
+ {...DEFAULT_SELECTION_PROPS}
+ />
+
+ );
+ // 3 shard keys with 2 + 2 + 1 shards, which is not `shard_keys_count` * `shard_number`
+ expect(screen.getByText('5')).toBeInTheDocument();
+ expect(screen.getByLabelText(/5 shard\(s\) across 3 shard keys/)).toBeInTheDocument();
+ });
});
diff --git a/src/pages/Collections.jsx b/src/pages/Collections.jsx
index b60b84ba5..9a9ff3be6 100644
--- a/src/pages/Collections.jsx
+++ b/src/pages/Collections.jsx
@@ -27,7 +27,7 @@ function Collections() {
const [searchQuery, setSearchQuery] = useState('');
const [errorMessage, setErrorMessage] = useState(null);
const [isRefreshing, setIsRefreshing] = useState(false);
- const { client: qdrantClient } = useClient();
+ const { client: qdrantClient, isRestricted } = useClient();
const [currentPage, setCurrentPage] = useState(1);
const PAGE_SIZE = 5;
const [selectedCollections, setSelectedCollections] = useState(new Set());
@@ -44,6 +44,35 @@ function Collections() {
[qdrantClient]
);
+ // `shard_number` is only the *default* number of shards per shard key for custom sharding
+ // (each shard key can be created with its own `shards_number`), so the actual total shard
+ // count has to be read from the collection cluster info.
+ const getShardInfo = useCallback(
+ async (collectionName, collectionData) => {
+ if (isRestricted || collectionData.config?.params?.sharding_method !== 'custom') {
+ return {};
+ }
+ try {
+ const res = await qdrantClient.api('cluster').collectionClusterInfo({ collection_name: collectionName });
+ const clusterInfo = res.data?.result ?? {};
+ const shards = [...(clusterInfo.local_shards ?? []), ...(clusterInfo.remote_shards ?? [])];
+ const shardKeys = new Set(shards.filter((shard) => shard.shard_key != null).map((shard) => shard.shard_key));
+ const shardCount = Number(clusterInfo.shard_count);
+ if (!Number.isFinite(shardCount)) {
+ return {};
+ }
+ return {
+ shard_count: shardCount,
+ shard_keys_count: shardKeys.size,
+ };
+ } catch {
+ // Cluster info is unavailable (e.g. insufficient permissions), fall back to `shard_number`
+ return {};
+ }
+ },
+ [qdrantClient, isRestricted]
+ );
+
const getCollectionsCall = useCallback(
async (page = 1) => {
try {
@@ -61,9 +90,11 @@ function Collections() {
.map((alias) => alias.alias_name);
try {
const collectionData = await qdrantClient.getCollection(collection.name);
+ const shardInfo = await getShardInfo(collection.name, collectionData);
return {
name: collection.name,
...collectionData,
+ ...shardInfo,
aliases: [...collectionAliases],
};
} catch (error) {
@@ -84,7 +115,7 @@ function Collections() {
setRawCollections(null);
}
},
- [qdrantClient, getErrorMessageWithApiKey]
+ [qdrantClient, getErrorMessageWithApiKey, getShardInfo]
);
const getFilteredCollectionsCall = useCallback(
@@ -97,9 +128,11 @@ function Collections() {
filtered.map(async (collection) => {
try {
const collectionData = await qdrantClient.getCollection(collection.name);
+ const shardInfo = await getShardInfo(collection.name, collectionData);
return {
name: collection.name,
...collectionData,
+ ...shardInfo,
};
} catch (error) {
return {
@@ -118,7 +151,7 @@ function Collections() {
setRawCollections(null);
}
},
- [collections, qdrantClient, getErrorMessageWithApiKey]
+ [collections, qdrantClient, getErrorMessageWithApiKey, getShardInfo]
);
useEffect(() => {
@@ -150,8 +183,9 @@ function Collections() {
setIsRefreshing(true);
try {
const collectionData = await qdrantClient.getCollection(collectionName);
+ const shardInfo = await getShardInfo(collectionName, collectionData);
setRawCollections((prev) =>
- prev.map((c) => (c.name === collectionName ? { ...c, ...collectionData, error: null } : c))
+ prev.map((c) => (c.name === collectionName ? { ...c, ...collectionData, ...shardInfo, error: null } : c))
);
setErrorMessage(null);
} catch (error) {
@@ -161,7 +195,7 @@ function Collections() {
setIsRefreshing(false);
}
},
- [qdrantClient, getErrorMessageWithApiKey]
+ [qdrantClient, getErrorMessageWithApiKey, getShardInfo]
);
const handlePageChange = (event, value) => {