Skip to content

Commit 74ac147

Browse files
authored
Feat/show collection diagnostics (#27)
* feat: add SHOW COLLECTION <name> diagnostics * feat: add SHOW COLLECTION command and diagnostics to documentation and implementation
1 parent 1708d1c commit 74ac147

14 files changed

Lines changed: 488 additions & 13 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/ast_nodes.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -180,6 +180,11 @@ class ShowCollectionsStmt:
180180
pass
181181

182182

183+
@dataclass(frozen=True)
184+
class ShowCollectionStmt:
185+
collection: str
186+
187+
183188
@dataclass(frozen=True)
184189
class SelectStmt:
185190
collection: str
@@ -240,6 +245,7 @@ class DeleteStmt:
240245
| CreateIndexStmt
241246
| DropCollectionStmt
242247
| ShowCollectionsStmt
248+
| ShowCollectionStmt
243249
| SelectStmt
244250
| ScrollStmt
245251
| SearchStmt

src/qql/cli.py

Lines changed: 67 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,11 @@
4949
[yellow]SHOW COLLECTIONS[/yellow]
5050
List all collections in the connected Qdrant instance.
5151
52+
[yellow]SHOW COLLECTION[/yellow] <name>
53+
Show detailed diagnostics for a single collection: point count, vector
54+
config, distance metric, quantization, HNSW parameters, payload indexes,
55+
and sharding info.
56+
5257
[yellow]SCROLL FROM[/yellow] <name> [yellow]LIMIT[/yellow] <n>
5358
Paginate points by ID order.
5459
Optional: [yellow]WHERE[/yellow] <filter>
@@ -136,7 +141,7 @@ def connect(url: str, secret: str | None) -> None:
136141

137142
cfg = QQLConfig(url=url, secret=secret)
138143
save_config(cfg)
139-
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")
140145
_launch_repl(cfg)
141146

142147

@@ -257,7 +262,7 @@ def dump(collection: str, output: str, batch_size: int) -> None:
257262
f"\n[bold green]Done.[/bold green] "
258263
f"{written} point(s) written"
259264
+ (f", [yellow]{skipped} skipped[/yellow] (no 'text' field)" if skipped else "")
260-
+ f"."
265+
+ "."
261266
)
262267

263268

@@ -362,6 +367,61 @@ def _launch_repl(cfg: QQLConfig) -> None:
362367
_run_and_print(executor, query)
363368

364369

370+
def _format_collection_diagnostics(data: dict) -> str:
371+
"""Format SHOW COLLECTION <name> diagnostics into a rich string."""
372+
lines = []
373+
374+
lines.append(f"[bold cyan]Collection:[/bold cyan] {data['name']}")
375+
lines.append(f" Status : {data['status']}")
376+
lines.append(f" Points : {data['points_count']}")
377+
lines.append(f" Indexed vectors : {data['indexed_vectors_count']}")
378+
lines.append(f" Segments : {data['segments_count']}")
379+
lines.append(f" Topology : {data['topology']}")
380+
381+
# Vectors
382+
vectors = data["vectors"]
383+
for vname, vconf in vectors.items():
384+
label = f" Vector '{vname}'" if vname else " Vector"
385+
lines.append(f"{label} : {vconf['size']} dims, {vconf['distance']} distance")
386+
387+
# Sparse vectors
388+
if data["sparse_vectors"]:
389+
for sname, sconf in data["sparse_vectors"].items():
390+
lines.append(f" Sparse '{sname}' : modifier={sconf['modifier']}")
391+
392+
lines.append(f" Quantization : {data['quantization'] or 'none'}")
393+
394+
# HNSW config
395+
hnsw = data["hnsw_config"]
396+
lines.append(f" HNSW M : {hnsw['m']}")
397+
lines.append(f" HNSW ef_construct : {hnsw['ef_construct']}")
398+
if hnsw.get("full_scan_threshold") is not None:
399+
lines.append(f" HNSW full_scan_thres : {hnsw['full_scan_threshold']}")
400+
if hnsw.get("max_indexing_threads") is not None:
401+
lines.append(f" HNSW max_idx_threads : {hnsw['max_indexing_threads']}")
402+
if hnsw.get("on_disk") is not None:
403+
lines.append(f" HNSW on_disk : {hnsw['on_disk']}")
404+
if hnsw.get("payload_m") is not None:
405+
lines.append(f" HNSW payload_m : {hnsw['payload_m']}")
406+
407+
# Payload schema
408+
schema = data["payload_schema"]
409+
if schema:
410+
lines.append(" Payload indexes:")
411+
for field, dtype in schema.items():
412+
lines.append(f" {field}: {dtype}")
413+
else:
414+
lines.append(" Payload indexes : none")
415+
416+
# Sharding
417+
sh = data["sharding"]
418+
lines.append(f" Shards : {sh['shard_number']}")
419+
lines.append(f" Replicas : {sh['replication_factor']}")
420+
lines.append(f" Write consistency : {sh['write_consistency_factor']}")
421+
422+
return "\n".join(lines)
423+
424+
365425
def _run_and_print(executor: Executor, query: str) -> None:
366426
try:
367427
tokens = Lexer().tokenize(query)
@@ -393,6 +453,11 @@ def _run_and_print(executor: Executor, query: str) -> None:
393453
console.print(table)
394454
return
395455

456+
# Pretty-print SHOW COLLECTION <name> diagnostics
457+
if isinstance(result.data, dict) and "topology" in result.data:
458+
console.print(_format_collection_diagnostics(result.data))
459+
return
460+
396461
# Pretty-print search results
397462
if isinstance(result.data, list) and result.data and isinstance(result.data[0], dict) and "score" in result.data[0]:
398463
table = Table(show_header=True, header_style="bold cyan")

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: 106 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -80,16 +80,17 @@
8080
ScrollStmt,
8181
SearchStmt,
8282
SearchWith,
83+
ShowCollectionStmt,
8384
ShowCollectionsStmt,
8485
)
8586
from .config import QQLConfig
8687
from .embedder import CrossEncoderEmbedder, Embedder, SparseEmbedder
88+
from .exceptions import QQLRuntimeError
8789

8890
_RERANK_FETCH_MULTIPLIER = 4
8991
_HYBRID_PREFETCH_MULTIPLIER = 4
9092
_COLLECTION_VISIBILITY_TIMEOUT_SECONDS = 5.0
9193
_COLLECTION_VISIBILITY_POLL_SECONDS = 0.05
92-
from .exceptions import QQLRuntimeError
9394

9495

9596
@dataclass
@@ -117,6 +118,8 @@ def execute(self, node: ASTNode) -> ExecutionResult:
117118
return self._execute_drop(node)
118119
if isinstance(node, ShowCollectionsStmt):
119120
return self._execute_show(node)
121+
if isinstance(node, ShowCollectionStmt):
122+
return self._execute_show_collection(node)
120123
if isinstance(node, ScrollStmt):
121124
return self._execute_scroll(node)
122125
if isinstance(node, SelectStmt):
@@ -418,6 +421,108 @@ def _execute_show(self, node: ShowCollectionsStmt) -> ExecutionResult:
418421
data=names,
419422
)
420423

424+
def _execute_show_collection(self, node: ShowCollectionStmt) -> ExecutionResult:
425+
if not self._client.collection_exists(node.collection):
426+
raise QQLRuntimeError(f"Collection '{node.collection}' does not exist")
427+
428+
info = self._client.get_collection(node.collection)
429+
config = info.config
430+
params = config.params
431+
432+
# ── Vector topology ────────────────────────────────────────────────
433+
vectors = params.vectors # type: ignore[union-attr]
434+
sparse_vector_params = params.sparse_vectors or {}
435+
if isinstance(vectors, dict):
436+
vector_details = {}
437+
for vname, vconfig in vectors.items():
438+
vector_details[vname] = {
439+
"size": vconfig.size,
440+
"distance": str(vconfig.distance) if vconfig.distance else None,
441+
}
442+
elif vectors is None:
443+
raise QQLRuntimeError(
444+
f"Collection '{node.collection}' has no vector configuration"
445+
)
446+
else:
447+
vector_details = {
448+
"": {
449+
"size": vectors.size,
450+
"distance": str(vectors.distance) if vectors.distance else None,
451+
}
452+
}
453+
topology = "hybrid" if sparse_vector_params else "dense"
454+
455+
# ── Sparse vector config ───────────────────────────────────────────
456+
sparse_vectors = {}
457+
if sparse_vector_params:
458+
for sname, sconfig in sparse_vector_params.items():
459+
sparse_vectors[sname] = {
460+
"modifier": str(sconfig.modifier) if sconfig.modifier else None,
461+
}
462+
463+
# ── Quantization ───────────────────────────────────────────────────
464+
quant_config = config.quantization_config
465+
quantization = None
466+
if quant_config is not None:
467+
qtype = type(quant_config).__name__
468+
if hasattr(quant_config, "scalar"):
469+
quantization = "scalar"
470+
elif hasattr(quant_config, "binary"):
471+
quantization = "binary"
472+
elif hasattr(quant_config, "product"):
473+
quantization = "product"
474+
elif hasattr(quant_config, "turbo"):
475+
quantization = "turbo"
476+
else:
477+
quantization = qtype
478+
479+
# ── HNSW config ────────────────────────────────────────────────────
480+
hnsw = {
481+
"m": config.hnsw_config.m,
482+
"ef_construct": config.hnsw_config.ef_construct,
483+
}
484+
if config.hnsw_config.full_scan_threshold is not None:
485+
hnsw["full_scan_threshold"] = config.hnsw_config.full_scan_threshold
486+
if config.hnsw_config.max_indexing_threads is not None:
487+
hnsw["max_indexing_threads"] = config.hnsw_config.max_indexing_threads
488+
if config.hnsw_config.on_disk is not None:
489+
hnsw["on_disk"] = config.hnsw_config.on_disk
490+
if config.hnsw_config.payload_m is not None:
491+
hnsw["payload_m"] = config.hnsw_config.payload_m
492+
493+
# ── Payload schema / indexes ───────────────────────────────────────
494+
payload_indexes = {}
495+
for field_name, idx_info in (info.payload_schema or {}).items():
496+
payload_indexes[field_name] = str(idx_info.data_type)
497+
498+
# ── Sharding / replication ─────────────────────────────────────────
499+
sharding = {
500+
"shard_number": params.shard_number,
501+
"replication_factor": params.replication_factor,
502+
"write_consistency_factor": params.write_consistency_factor,
503+
}
504+
505+
data = {
506+
"name": node.collection,
507+
"status": str(info.status),
508+
"points_count": info.points_count,
509+
"indexed_vectors_count": info.indexed_vectors_count,
510+
"segments_count": info.segments_count,
511+
"topology": topology,
512+
"vectors": vector_details,
513+
"sparse_vectors": sparse_vectors or None,
514+
"quantization": quantization,
515+
"hnsw_config": hnsw,
516+
"payload_schema": payload_indexes or None,
517+
"sharding": sharding,
518+
}
519+
520+
return ExecutionResult(
521+
success=True,
522+
message=f"Collection '{node.collection}' diagnostics",
523+
data=data,
524+
)
525+
421526
def _execute_scroll(self, node: ScrollStmt) -> ExecutionResult:
422527
if not self._client.collection_exists(node.collection):
423528
raise QQLRuntimeError(f"Collection '{node.collection}' does not exist")

0 commit comments

Comments
 (0)