Skip to content

Commit 000e49e

Browse files
committed
feat: add SHOW COLLECTION command and diagnostics to documentation and implementation
1 parent 78523a6 commit 000e49e

12 files changed

Lines changed: 161 additions & 16 deletions

README.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -84,7 +84,7 @@ Full documentation lives in the [`docs/`](docs/) folder and at **[pavanjava.gith
8484
| [INSERT / INSERT BULK](docs/insert.md) | Adding documents, batch inserts, payload types |
8585
| [SEARCH / SELECT / SCROLL / RECOMMEND / Hybrid / RERANK](docs/search.md) | Semantic search, point retrieval, pagination, hybrid, reranking, recommendations |
8686
| [WHERE Filters](docs/filters.md) | Full SQL-style filter operators |
87-
| [Collections & Quantization](docs/collections.md) | CREATE, DROP, QUANTIZE (scalar/turbo/binary/product), CREATE INDEX |
87+
| [Collections & Quantization](docs/collections.md) | SHOW, CREATE, DROP, QUANTIZE (scalar/turbo/binary/product), CREATE INDEX |
8888
| [Scripts: EXECUTE / DUMP](docs/scripts.md) | Script files, collection backup/restore |
8989
| [Programmatic Usage](docs/programmatic.md) | Use QQL as a Python library |
9090
| [Reference: Models / Config / Errors](docs/reference.md) | Embedding models, config file, error reference |
@@ -125,6 +125,7 @@ CREATE COLLECTION articles QUANTIZE TURBO BITS 2
125125
CREATE COLLECTION articles QUANTIZE TURBO BITS 1.5 ALWAYS RAM
126126
CREATE INDEX ON COLLECTION articles FOR year TYPE integer
127127
SHOW COLLECTIONS
128+
SHOW COLLECTION articles
128129
DROP COLLECTION articles
129130

130131
-- Delete

docs/collections.md

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,64 @@ SHOW COLLECTIONS
2424

2525
---
2626

27+
## SHOW COLLECTION — inspect one collection
28+
29+
Returns collection diagnostics for a single collection using Qdrant's collection info.
30+
31+
**Syntax:**
32+
```sql
33+
SHOW COLLECTION <collection_name>
34+
```
35+
36+
**What it shows:**
37+
38+
- Point count
39+
- Indexed vector count
40+
- Segment count
41+
- Vector names, dimensions, and distance metrics
42+
- Dense vs hybrid topology
43+
- Sparse vector modifiers when present
44+
- Quantization mode
45+
- HNSW configuration
46+
- Payload indexes detected by Qdrant
47+
- Shard, replica, and write consistency settings
48+
49+
**Example:**
50+
```sql
51+
SHOW COLLECTION research_papers
52+
```
53+
54+
**Output:**
55+
```
56+
OK Collection 'research_papers' diagnostics
57+
Collection: research_papers
58+
Status : green
59+
Points : 12450
60+
Indexed vectors : 12450
61+
Segments : 3
62+
Topology : hybrid
63+
Vector 'dense' : 768 dims, Cosine distance
64+
Sparse 'sparse' : modifier=idf
65+
Quantization : scalar
66+
HNSW M : 16
67+
HNSW ef_construct : 100
68+
Payload indexes:
69+
category: keyword
70+
year: integer
71+
Shards : 1
72+
Replicas : 1
73+
Write consistency : 1
74+
```
75+
76+
**Notes:**
77+
78+
- `Topology` is `dense` for standard collections and `hybrid` when sparse vectors are configured alongside dense vectors.
79+
- Dense collections with named vectors still report their vector names and dimensions.
80+
- If no payload indexes exist, QQL prints `Payload indexes : none`.
81+
- Raises an error if the collection does not exist.
82+
83+
---
84+
2785
## CREATE COLLECTION — create a collection
2886

2987
Explicitly creates a new empty collection. Collections are also created automatically on the first INSERT, so this command is optional — use it when you want to pre-create a collection before inserting data.

docs/getting-started.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -144,6 +144,9 @@ SCROLL FROM notes LIMIT 10
144144
-- List all collections
145145
SHOW COLLECTIONS
146146

147+
-- Inspect one collection's diagnostics
148+
SHOW COLLECTION notes
149+
147150
-- Retrieve a point by ID
148151
SELECT * FROM notes WHERE id = 1
149152
```

docs/programmatic.md

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,15 @@ result = run_query(
8080
url="http://localhost:6333",
8181
)
8282
print(result.message) # "Deleted N point(s)"
83+
84+
# Inspect collection diagnostics
85+
result = run_query(
86+
"SHOW COLLECTION notes",
87+
url="http://localhost:6333",
88+
)
89+
print(result.data["topology"]) # "dense" or "hybrid"
90+
print(result.data["vectors"]) # {"": {...}} or {"dense": {...}, ...}
91+
print(result.data["payload_schema"]) # {"field": "keyword", ...} or None
8392
```
8493

8594
---
@@ -132,6 +141,7 @@ class ExecutionResult:
132141
| SCROLL | `{"points": [{"id": str, "payload": dict}, ...], "next_offset": str \| None}` |
133142
| RECOMMEND | `[{"id": str, "score": float, "payload": dict}, ...]` |
134143
| SHOW COLLECTIONS | `["name1", "name2", ...]` |
144+
| SHOW COLLECTION | `{"name": str, "status": str, "points_count": int \| None, "indexed_vectors_count": int \| None, "segments_count": int, "topology": str, "vectors": dict, "sparse_vectors": dict \| None, "quantization": str \| None, "hnsw_config": dict, "payload_schema": dict \| None, "sharding": dict}` |
135145
| CREATE COLLECTION | `None` |
136146
| CREATE INDEX | `None` |
137147
| DROP COLLECTION | `None` |

src/qql/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
"Executor",
2222
"Lexer",
2323
"Parser",
24+
"load_config",
2425
"run_query",
2526
]
2627

src/qql/cli.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -141,7 +141,7 @@ def connect(url: str, secret: str | None) -> None:
141141

142142
cfg = QQLConfig(url=url, secret=secret)
143143
save_config(cfg)
144-
console.print(f"[bold green]Connected.[/bold green] Config saved to ~/.qql/config.json\n")
144+
console.print("[bold green]Connected.[/bold green] Config saved to ~/.qql/config.json\n")
145145
_launch_repl(cfg)
146146

147147

@@ -262,7 +262,7 @@ def dump(collection: str, output: str, batch_size: int) -> None:
262262
f"\n[bold green]Done.[/bold green] "
263263
f"{written} point(s) written"
264264
+ (f", [yellow]{skipped} skipped[/yellow] (no 'text' field)" if skipped else "")
265-
+ f"."
265+
+ "."
266266
)
267267

268268

@@ -416,7 +416,7 @@ def _format_collection_diagnostics(data: dict) -> str:
416416
# Sharding
417417
sh = data["sharding"]
418418
lines.append(f" Shards : {sh['shard_number']}")
419-
lines.append(f" Replicas : {sh['replication_factor']}")
419+
lines.append(f" Replicas : {sh['replication_factor']}")
420420
lines.append(f" Write consistency : {sh['write_consistency_factor']}")
421421

422422
return "\n".join(lines)

src/qql/config.py

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
from __future__ import annotations
22

33
import json
4-
import os
54
from dataclasses import asdict, dataclass
65
from pathlib import Path
76

src/qql/executor.py

Lines changed: 10 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -85,12 +85,12 @@
8585
)
8686
from .config import QQLConfig
8787
from .embedder import CrossEncoderEmbedder, Embedder, SparseEmbedder
88+
from .exceptions import QQLRuntimeError
8889

8990
_RERANK_FETCH_MULTIPLIER = 4
9091
_HYBRID_PREFETCH_MULTIPLIER = 4
9192
_COLLECTION_VISIBILITY_TIMEOUT_SECONDS = 5.0
9293
_COLLECTION_VISIBILITY_POLL_SECONDS = 0.05
93-
from .exceptions import QQLRuntimeError
9494

9595

9696
@dataclass
@@ -431,27 +431,31 @@ def _execute_show_collection(self, node: ShowCollectionStmt) -> ExecutionResult:
431431

432432
# ── Vector topology ────────────────────────────────────────────────
433433
vectors = params.vectors # type: ignore[union-attr]
434+
sparse_vector_params = params.sparse_vectors or {}
434435
if isinstance(vectors, dict):
435-
topology = "hybrid"
436436
vector_details = {}
437437
for vname, vconfig in vectors.items():
438438
vector_details[vname] = {
439439
"size": vconfig.size,
440440
"distance": str(vconfig.distance) if vconfig.distance else None,
441441
}
442+
elif vectors is None:
443+
raise QQLRuntimeError(
444+
f"Collection '{node.collection}' has no vector configuration"
445+
)
442446
else:
443-
topology = "dense"
444447
vector_details = {
445448
"": {
446449
"size": vectors.size,
447450
"distance": str(vectors.distance) if vectors.distance else None,
448451
}
449452
}
453+
topology = "hybrid" if sparse_vector_params else "dense"
450454

451455
# ── Sparse vector config ───────────────────────────────────────────
452456
sparse_vectors = {}
453-
if params.sparse_vectors:
454-
for sname, sconfig in params.sparse_vectors.items():
457+
if sparse_vector_params:
458+
for sname, sconfig in sparse_vector_params.items():
455459
sparse_vectors[sname] = {
456460
"modifier": str(sconfig.modifier) if sconfig.modifier else None,
457461
}
@@ -488,7 +492,7 @@ def _execute_show_collection(self, node: ShowCollectionStmt) -> ExecutionResult:
488492

489493
# ── Payload schema / indexes ───────────────────────────────────────
490494
payload_indexes = {}
491-
for field_name, idx_info in info.payload_schema.items():
495+
for field_name, idx_info in (info.payload_schema or {}).items():
492496
payload_indexes[field_name] = str(idx_info.data_type)
493497

494498
# ── Sharding / replication ─────────────────────────────────────────

tests/test_dumper.py

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,6 @@
77
from qql.dumper import (
88
_DEFAULT_DUMP_BATCH_SIZE,
99
_is_hybrid,
10-
_serialize_dict,
1110
_serialize_value,
1211
dump_collection,
1312
)

tests/test_executor.py

Lines changed: 74 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -454,6 +454,45 @@ def test_show_collection_hybrid(self, executor, mock_client, mocker):
454454
assert data["vectors"]["dense"]["size"] == 768
455455
assert data["sparse_vectors"]["sparse"]["modifier"] == "idf"
456456

457+
def test_show_collection_named_dense_is_not_reported_as_hybrid(self, executor, mock_client, mocker):
458+
from qdrant_client.models import (
459+
CollectionStatus,
460+
Distance,
461+
VectorParams,
462+
)
463+
464+
mock_client.collection_exists.return_value = True
465+
466+
mock_info = mocker.MagicMock()
467+
mock_info.status = CollectionStatus.GREEN
468+
mock_info.points_count = 3
469+
mock_info.indexed_vectors_count = 3
470+
mock_info.segments_count = 1
471+
mock_info.config.params.vectors = {
472+
"body": VectorParams(size=384, distance=Distance.COSINE),
473+
"title": VectorParams(size=128, distance=Distance.DOT),
474+
}
475+
mock_info.config.params.sparse_vectors = None
476+
mock_info.config.params.shard_number = 1
477+
mock_info.config.params.replication_factor = 1
478+
mock_info.config.params.write_consistency_factor = 1
479+
mock_info.config.hnsw_config.m = 16
480+
mock_info.config.hnsw_config.ef_construct = 100
481+
mock_info.config.hnsw_config.full_scan_threshold = None
482+
mock_info.config.hnsw_config.max_indexing_threads = None
483+
mock_info.config.hnsw_config.on_disk = None
484+
mock_info.config.hnsw_config.payload_m = None
485+
mock_info.config.quantization_config = None
486+
mock_info.payload_schema = {}
487+
488+
mock_client.get_collection.return_value = mock_info
489+
490+
result = executor.execute(ShowCollectionStmt(collection="named_dense"))
491+
492+
assert result.success is True
493+
assert result.data["topology"] == "dense"
494+
assert result.data["sparse_vectors"] is None
495+
457496
def test_show_collection_with_payload_schema(self, executor, mock_client, mocker):
458497
from qdrant_client.models import (
459498
CollectionStatus,
@@ -494,6 +533,41 @@ def test_show_collection_with_payload_schema(self, executor, mock_client, mocker
494533
assert result.success is True
495534
assert result.data["payload_schema"] == {"category": "keyword"}
496535

536+
def test_show_collection_handles_missing_payload_schema(self, executor, mock_client, mocker):
537+
from qdrant_client.models import (
538+
CollectionStatus,
539+
Distance,
540+
VectorParams,
541+
)
542+
543+
mock_client.collection_exists.return_value = True
544+
545+
mock_info = mocker.MagicMock()
546+
mock_info.status = CollectionStatus.GREEN
547+
mock_info.points_count = 0
548+
mock_info.indexed_vectors_count = 0
549+
mock_info.segments_count = 0
550+
mock_info.config.params.vectors = VectorParams(size=384, distance=Distance.COSINE)
551+
mock_info.config.params.shard_number = 1
552+
mock_info.config.params.replication_factor = 1
553+
mock_info.config.params.write_consistency_factor = 1
554+
mock_info.config.params.sparse_vectors = None
555+
mock_info.config.hnsw_config.m = 16
556+
mock_info.config.hnsw_config.ef_construct = 100
557+
mock_info.config.hnsw_config.full_scan_threshold = None
558+
mock_info.config.hnsw_config.max_indexing_threads = None
559+
mock_info.config.hnsw_config.on_disk = None
560+
mock_info.config.hnsw_config.payload_m = None
561+
mock_info.config.quantization_config = None
562+
mock_info.payload_schema = None
563+
564+
mock_client.get_collection.return_value = mock_info
565+
566+
result = executor.execute(ShowCollectionStmt(collection="docs"))
567+
568+
assert result.success is True
569+
assert result.data["payload_schema"] is None
570+
497571
def test_show_collection_nonexistent_raises(self, executor, mock_client):
498572
mock_client.collection_exists.return_value = False
499573
node = ShowCollectionStmt(collection="ghost")
@@ -1227,7 +1301,6 @@ def test_hybrid_insert_uses_custom_dense_model(
12271301
)
12281302
executor.execute(node)
12291303
# Embedder should have been called with the custom dense model name
1230-
call_args = mocker.patch.object # already patched by mock_embedder fixture
12311304
# Verify through the dense vector in the upsert call
12321305
point = mock_client.upsert.call_args.kwargs["points"][0]
12331306
assert "dense" in point.vector

0 commit comments

Comments
 (0)