Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 5 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -135,7 +135,11 @@ SELECT * FROM articles WHERE id = '3f2e1a4b-...'
-- Collections
CREATE COLLECTION articles
CREATE COLLECTION articles HYBRID
CREATE COLLECTION articles HNSW { payload_m: 16 }
CREATE COLLECTION articles WITH HNSW { payload_m: 16 }
CREATE COLLECTION articles WITH VECTORS { on_disk: true } WITH HNSW { full_scan_threshold: 10000 }
ALTER COLLECTION articles WITH OPTIMIZERS { indexing_threshold: 10000 }
ALTER COLLECTION articles WITH PARAMS { read_fan_out_factor: 4, on_disk_payload: false }
ALTER COLLECTION articles QUANTIZE DISABLED
CREATE COLLECTION articles QUANTIZE SCALAR
CREATE COLLECTION articles QUANTIZE TURBO
CREATE COLLECTION articles QUANTIZE TURBO BITS 2
Expand Down
36 changes: 27 additions & 9 deletions docs/collections.md
Original file line number Diff line number Diff line change
Expand Up @@ -93,10 +93,22 @@ CREATE COLLECTION <collection_name> HYBRID
CREATE COLLECTION <collection_name> USING MODEL '<model_name>'
CREATE COLLECTION <collection_name> USING HYBRID
CREATE COLLECTION <collection_name> USING HYBRID DENSE MODEL '<model>'
CREATE COLLECTION <collection_name> HNSW { payload_m: <int> }
```

Any of the above forms can be followed by an optional `QUANTIZE` clause and/or `HNSW { payload_m: <int> }`.
CREATE COLLECTION <collection_name> WITH VECTORS { on_disk: <bool> }
CREATE COLLECTION <collection_name> WITH HNSW { m, ef_construct, full_scan_threshold, max_indexing_threads, on_disk, payload_m, inline_storage }
CREATE COLLECTION <collection_name> WITH OPTIMIZERS { deleted_threshold, vacuum_min_vector_number, default_segment_number, max_segment_size, memmap_threshold, indexing_threshold, flush_interval_sec, max_optimization_threads, prevent_unoptimized }
CREATE COLLECTION <collection_name> WITH PARAMS { replication_factor, write_consistency_factor, on_disk_payload }
ALTER COLLECTION <collection_name> WITH HNSW { ... }
ALTER COLLECTION <collection_name> WITH VECTORS { ... }
ALTER COLLECTION <collection_name> WITH OPTIMIZERS { ... }
ALTER COLLECTION <collection_name> WITH PARAMS { ... }
ALTER COLLECTION <collection_name> QUANTIZE SCALAR [QUANTILE <0.0–1.0>] [ALWAYS RAM]
ALTER COLLECTION <collection_name> QUANTIZE BINARY [ALWAYS RAM]
ALTER COLLECTION <collection_name> QUANTIZE PRODUCT [ALWAYS RAM]
ALTER COLLECTION <collection_name> QUANTIZE TURBO [BITS <1|1.5|2|4>] [ALWAYS RAM]
ALTER COLLECTION <collection_name> QUANTIZE DISABLED
```

Any `CREATE COLLECTION` form can be followed by an optional `QUANTIZE` clause and one or more `WITH ... { ... }` config blocks.

**Examples:**

Expand All @@ -122,21 +134,27 @@ CREATE COLLECTION research_papers USING HYBRID DENSE MODEL 'BAAI/bge-base-en-v1.

Dense collection with payload-aware HNSW links:
```sql
CREATE COLLECTION research_papers HNSW {payload_m: 16}
CREATE COLLECTION research_papers WITH HNSW { payload_m: 16 }
```

When `USING MODEL` is omitted, the collection uses the **default embedding model's dimensions** (384 for `all-MiniLM-L6-v2`). If the collection already exists, the command succeeds with a message and does nothing.

### HNSW clause
### Collection config blocks

QQL currently supports one explicit HNSW knob during collection creation:
QQL supports the same config blocks on both `CREATE COLLECTION` and `ALTER COLLECTION`:

- `payload_m` — enables payload-aware HNSW connectivity used by Qdrant for filtered / tenant-aware workloads
- `WITH VECTORS { on_disk }`
- `WITH HNSW { m, ef_construct, full_scan_threshold, max_indexing_threads, on_disk, payload_m, inline_storage }`
- `WITH OPTIMIZERS { deleted_threshold, vacuum_min_vector_number, default_segment_number, max_segment_size, memmap_threshold, indexing_threshold, flush_interval_sec, max_optimization_threads, prevent_unoptimized }`
- `WITH PARAMS { replication_factor, write_consistency_factor, on_disk_payload }` on create
- `WITH PARAMS { replication_factor, write_consistency_factor, read_fan_out_factor, read_fan_out_delay_ms, on_disk_payload }` on alter
- `ALTER COLLECTION ... QUANTIZE ...` supports the same quantization forms as create, plus `QUANTIZE DISABLED`

Example:

```sql
CREATE COLLECTION tenant_docs USING HYBRID HNSW {payload_m: 16}
CREATE COLLECTION tenant_docs USING HYBRID WITH HNSW { payload_m: 16, m: 32 }
ALTER COLLECTION tenant_docs WITH OPTIMIZERS { indexing_threshold: 10000 }
```

---
Expand Down
2 changes: 1 addition & 1 deletion docs/scripts.md
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,7 @@ Done. 3/3 statement(s) succeeded.

Export every point in a collection to a `.qql` script file. The generated file is valid QQL that re-creates the collection and re-inserts all payload data. Points are written in batches of 50 as `INSERT BULK` statements.

> **Scope of a dump:** The generated script preserves collection topology (dense vs hybrid) and all point payloads. It does **not** preserve quantization config, pinned model / vector dimensions, or payload indexes — those must be re-applied manually after import if needed.
> **Scope of a dump:** The generated script preserves collection topology (dense vs hybrid), collection config blocks, quantization config, and all point payloads. It does **not** preserve pinned model / vector dimensions or payload indexes — those must be re-applied manually after import if needed.

**CLI usage:**
```bash
Expand Down
62 changes: 61 additions & 1 deletion src/qql/ast_nodes.py
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,58 @@ class QuantizationSearchWith:
oversampling: float | None = None


@dataclass(frozen=True)
class VectorsConfig:
on_disk: bool | None = None


@dataclass(frozen=True)
class HnswRuntimeConfig:
m: int | None = None
ef_construct: int | None = None
full_scan_threshold: int | None = None
max_indexing_threads: int | None = None
on_disk: bool | None = None
payload_m: int | None = None
inline_storage: bool | None = None


@dataclass(frozen=True)
class OptimizersRuntimeConfig:
deleted_threshold: float | None = None
vacuum_min_vector_number: int | None = None
default_segment_number: int | None = None
max_segment_size: int | None = None
memmap_threshold: int | None = None
indexing_threshold: int | None = None
flush_interval_sec: int | None = None
max_optimization_threads: int | str | None = None
prevent_unoptimized: bool | None = None


@dataclass(frozen=True)
class CollectionParamsConfig:
replication_factor: int | None = None
write_consistency_factor: int | None = None
read_fan_out_factor: int | None = None
read_fan_out_delay_ms: int | None = None
on_disk_payload: bool | None = None


@dataclass(frozen=True)
class CollectionConfig:
vectors: VectorsConfig | None = None
hnsw: HnswRuntimeConfig | None = None
optimizers: OptimizersRuntimeConfig | None = None
params: CollectionParamsConfig | None = None


@dataclass(frozen=True)
class QuantizationUpdate:
config: QuantizationConfig | None = None
disabled: bool = False


# ── Filter expression leaf nodes ──────────────────────────────────────────────

@dataclass(frozen=True)
Expand Down Expand Up @@ -172,7 +224,14 @@ class CreateCollectionStmt:
hybrid: bool = False # if True, create with dense + sparse named vectors
model: str | None = None # dense model; None → use config default
quantization: QuantizationConfig | None = None # optional QUANTIZE clause
payload_m: int | None = None # optional HNSW { payload_m: N } clause
config: CollectionConfig | None = None


@dataclass(frozen=True)
class AlterCollectionStmt:
collection: str
config: CollectionConfig | None = None
quantization: QuantizationUpdate | None = None


@dataclass(frozen=True)
Expand Down Expand Up @@ -274,6 +333,7 @@ class UpdatePayloadStmt:
InsertStmt
| InsertBulkStmt
| CreateCollectionStmt
| AlterCollectionStmt
| CreateIndexStmt
| DropCollectionStmt
| ShowCollectionsStmt
Expand Down
39 changes: 35 additions & 4 deletions src/qql/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,12 +38,28 @@
Create a new collection. Add HYBRID for dense+sparse BM25 vectors.
Optional: [yellow]USING MODEL[/yellow] '<model>'
Optional: [yellow]USING HYBRID[/yellow] [DENSE MODEL '<model>']
Optional: [yellow]HNSW[/yellow] { payload_m: <int> }
Optional: [yellow]WITH VECTORS[/yellow] { on_disk: <bool> }
Optional: [yellow]WITH HNSW[/yellow] { m, ef_construct, full_scan_threshold, max_indexing_threads, on_disk, payload_m, inline_storage }
Optional: [yellow]WITH OPTIMIZERS[/yellow] { deleted_threshold, vacuum_min_vector_number, default_segment_number, max_segment_size, memmap_threshold, indexing_threshold, flush_interval_sec, max_optimization_threads, prevent_unoptimized }
Optional: [yellow]WITH PARAMS[/yellow] { replication_factor, write_consistency_factor, on_disk_payload }
Optional: [yellow]QUANTIZE SCALAR[/yellow] [QUANTILE <0.0–1.0>] [ALWAYS RAM]
Optional: [yellow]QUANTIZE BINARY[/yellow] [ALWAYS RAM]
Optional: [yellow]QUANTIZE PRODUCT[/yellow] [ALWAYS RAM] (4× compression)
Optional: [yellow]QUANTIZE TURBO[/yellow] [BITS <1|1.5|2|4>] [ALWAYS RAM]
QUANTIZE may be combined with any HYBRID or MODEL clause.

[yellow]ALTER COLLECTION[/yellow] <name>
Update runtime collection config without recreating the collection.
Optional: [yellow]WITH VECTORS[/yellow] { on_disk: <bool> }
Optional: [yellow]WITH HNSW[/yellow] { m, ef_construct, full_scan_threshold, max_indexing_threads, on_disk, payload_m, inline_storage }
Optional: [yellow]WITH OPTIMIZERS[/yellow] { deleted_threshold, vacuum_min_vector_number, default_segment_number, max_segment_size, memmap_threshold, indexing_threshold, flush_interval_sec, max_optimization_threads, prevent_unoptimized }
Optional: [yellow]WITH PARAMS[/yellow] { replication_factor, write_consistency_factor, read_fan_out_factor, read_fan_out_delay_ms, on_disk_payload }
Optional: [yellow]QUANTIZE SCALAR[/yellow] [QUANTILE <0.0–1.0>] [ALWAYS RAM]
Optional: [yellow]QUANTIZE BINARY[/yellow] [ALWAYS RAM]
Optional: [yellow]QUANTIZE PRODUCT[/yellow] [ALWAYS RAM]
Optional: [yellow]QUANTIZE TURBO[/yellow] [BITS <1|1.5|2|4>] [ALWAYS RAM]
Optional: [yellow]QUANTIZE DISABLED[/yellow]

[yellow]DROP COLLECTION[/yellow] <name>
Delete a collection and all its points.

Expand Down Expand Up @@ -400,7 +416,10 @@ def _format_collection_diagnostics(data: dict) -> str:
vectors = data["vectors"]
for vname, vconf in vectors.items():
label = f" Vector '{vname}'" if vname else " Vector"
lines.append(f"{label} : {vconf['size']} dims, {vconf['distance']} distance")
suffix = ""
if vconf.get("on_disk") is not None:
suffix = f", on_disk={vconf['on_disk']}"
lines.append(f"{label} : {vconf['size']} dims, {vconf['distance']} distance{suffix}")

# Sparse vectors
if data["sparse_vectors"]:
Expand All @@ -421,6 +440,8 @@ def _format_collection_diagnostics(data: dict) -> str:
lines.append(f" HNSW on_disk : {hnsw['on_disk']}")
if hnsw.get("payload_m") is not None:
lines.append(f" HNSW payload_m : {hnsw['payload_m']}")
if hnsw.get("inline_storage") is not None:
lines.append(f" HNSW inline_storage : {hnsw['inline_storage']}")

# Payload schema
schema = data["payload_schema"]
Expand All @@ -439,11 +460,21 @@ def _format_collection_diagnostics(data: dict) -> str:
else:
lines.append(" Payload indexes : none")

sharding = data["sharding"]
if sharding.get("replication_factor") is not None:
lines.append(f" Replication factor : {sharding['replication_factor']}")
if sharding.get("write_consistency_factor") is not None:
lines.append(f" Write consistency : {sharding['write_consistency_factor']}")
if sharding.get("read_fan_out_factor") is not None:
lines.append(f" Read fan-out : {sharding['read_fan_out_factor']}")
if sharding.get("read_fan_out_delay_ms") is not None:
lines.append(f" Read fan-out delay : {sharding['read_fan_out_delay_ms']} ms")
if sharding.get("on_disk_payload") is not None:
lines.append(f" Payload on_disk : {sharding['on_disk_payload']}")

# Sharding
sh = data["sharding"]
lines.append(f" Shards : {sh['shard_number']}")
lines.append(f" Replicas : {sh['replication_factor']}")
lines.append(f" Write consistency : {sh['write_consistency_factor']}")

return "\n".join(lines)

Expand Down
127 changes: 121 additions & 6 deletions src/qql/dumper.py
Original file line number Diff line number Diff line change
Expand Up @@ -66,11 +66,120 @@ def _serialize_dict(d: dict[str, Any], indent: int = 4) -> str:
# ── Collection type detection ─────────────────────────────────────────────────


def _collection_info(collection: str, client: QdrantClient) -> Any:
return client.get_collection(collection)


def _is_hybrid(collection: str, client: QdrantClient) -> bool:
"""Return True if the collection uses named vectors (dense + sparse)."""
info = client.get_collection(collection)
vectors = info.config.params.vectors # type: ignore[union-attr]
return isinstance(vectors, dict)
"""Return True only when sparse vectors are configured."""
info = _collection_info(collection, client)
sparse_vectors = info.config.params.sparse_vectors
return isinstance(sparse_vectors, dict) and bool(sparse_vectors)


def _quantization_clause(info: Any) -> str:
quant = info.config.quantization_config
if quant is None:
return ""
if hasattr(quant, "scalar"):
clause = " QUANTIZE SCALAR"
if quant.scalar.quantile is not None:
clause += f" QUANTILE {quant.scalar.quantile}"
if quant.scalar.always_ram:
clause += " ALWAYS RAM"
return clause
if hasattr(quant, "binary"):
clause = " QUANTIZE BINARY"
if quant.binary.always_ram:
clause += " ALWAYS RAM"
return clause
if hasattr(quant, "product"):
clause = " QUANTIZE PRODUCT"
if quant.product.always_ram:
clause += " ALWAYS RAM"
return clause
if hasattr(quant, "turbo"):
clause = " QUANTIZE TURBO"
bits = quant.turbo.bits
if bits is not None:
bit_map = {
"BITS4": "4",
"BITS2": "2",
"BITS1_5": "1.5",
"BITS1": "1",
}
clause += f" BITS {bit_map.get(getattr(bits, 'name', ''), str(bits))}"
if quant.turbo.always_ram:
clause += " ALWAYS RAM"
return clause
return ""


def _config_clauses(info: Any) -> str:
clauses: list[str] = []
params = info.config.params
vectors = params.vectors # type: ignore[union-attr]
dense_vectors = vectors.get("dense") if isinstance(vectors, dict) else vectors
if dense_vectors is not None and getattr(dense_vectors, "on_disk", None) is not None:
clauses.append(f"WITH VECTORS {{ on_disk: {'true' if dense_vectors.on_disk else 'false'} }}")

hnsw = info.config.hnsw_config
hnsw_items: list[str] = []
for key in (
"m",
"ef_construct",
"full_scan_threshold",
"max_indexing_threads",
"payload_m",
):
value = getattr(hnsw, key, None)
if value is not None:
hnsw_items.append(f"{key}: {value}")
for key in ("on_disk", "inline_storage"):
value = getattr(hnsw, key, None)
if value is not None:
hnsw_items.append(f"{key}: {'true' if value else 'false'}")
if hnsw_items:
clauses.append(f"WITH HNSW {{ {', '.join(hnsw_items)} }}")

optimizers = getattr(info.config, "optimizer_config", None) or getattr(info.config, "optimizers_config", None)
optimizer_items: list[str] = []
if optimizers is not None:
for key in (
"deleted_threshold",
"vacuum_min_vector_number",
"default_segment_number",
"max_segment_size",
"memmap_threshold",
"indexing_threshold",
"flush_interval_sec",
):
value = getattr(optimizers, key, None)
if value is not None:
optimizer_items.append(f"{key}: {value}")
max_opt_threads = getattr(optimizers, "max_optimization_threads", None)
if max_opt_threads is not None:
value = getattr(max_opt_threads, "value", max_opt_threads)
optimizer_items.append(f"max_optimization_threads: {value}")
prevent_unoptimized = getattr(optimizers, "prevent_unoptimized", None)
if prevent_unoptimized is not None:
optimizer_items.append(
f"prevent_unoptimized: {'true' if prevent_unoptimized else 'false'}"
)
if optimizer_items:
clauses.append(f"WITH OPTIMIZERS {{ {', '.join(optimizer_items)} }}")

param_items: list[str] = []
for key in ("replication_factor", "write_consistency_factor", "on_disk_payload"):
value = getattr(params, key, None)
if value is not None:
if isinstance(value, bool):
param_items.append(f"{key}: {'true' if value else 'false'}")
else:
param_items.append(f"{key}: {value}")
if param_items:
clauses.append(f"WITH PARAMS {{ {', '.join(param_items)} }}")
return (" " + " ".join(clauses)) if clauses else ""


# ── Main entry point ──────────────────────────────────────────────────────────
Expand Down Expand Up @@ -98,7 +207,9 @@ def dump_collection(
)
return 0, 0

hybrid = _is_hybrid(collection, client)
info = _collection_info(collection, client)
sparse_vectors = info.config.params.sparse_vectors
hybrid = isinstance(sparse_vectors, dict) and bool(sparse_vectors)
col_type = "hybrid (dense + sparse)" if hybrid else "dense"
using_clause = " USING HYBRID" if hybrid else ""

Expand Down Expand Up @@ -138,7 +249,11 @@ def dump_collection(

# ── CREATE statement ──────────────────────────────────────────────
hybrid_suffix = " HYBRID" if hybrid else ""
f.write(f"CREATE COLLECTION {collection}{hybrid_suffix}\n\n")
config_suffix = _config_clauses(info)
quantization_suffix = _quantization_clause(info)
f.write(
f"CREATE COLLECTION {collection}{hybrid_suffix}{config_suffix}{quantization_suffix}\n\n"
)

# ── Paginate and write INSERT BULK batches ────────────────────────
offset = None
Expand Down
Loading
Loading