Skip to content

Commit 8de7a7e

Browse files
committed
new quantization implementation
1 parent aacdaa5 commit 8de7a7e

9 files changed

Lines changed: 281 additions & 18 deletions

File tree

README.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -111,6 +111,9 @@ RECOMMEND FROM articles POSITIVE IDS (1001, 1002) LIMIT 5
111111
CREATE COLLECTION articles
112112
CREATE COLLECTION articles HYBRID
113113
CREATE COLLECTION articles QUANTIZE SCALAR
114+
CREATE COLLECTION articles QUANTIZE TURBO
115+
CREATE COLLECTION articles QUANTIZE TURBO BITS 2
116+
CREATE COLLECTION articles QUANTIZE TURBO BITS 1.5 ALWAYS RAM
114117
CREATE INDEX ON COLLECTION articles FOR year TYPE integer
115118
SHOW COLLECTIONS
116119
DROP COLLECTION articles

docs/collections.md

Lines changed: 48 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -69,25 +69,33 @@ When `USING MODEL` is omitted, the collection uses the **default embedding model
6969

7070
Quantization reduces the memory footprint of vector collections and speeds up search at the cost of a small, controllable accuracy loss. QQL supports all three Qdrant quantization strategies via an optional `QUANTIZE` clause appended to `CREATE COLLECTION`.
7171

72-
**Three strategies:**
72+
**Four strategies:**
7373

74-
| Type | Compression | Accuracy Loss | Best For |
74+
| Type | Compression | Accuracy | Best For |
7575
|---|---|---|---|
76-
| `SCALAR` | 4× (float32 → int8) | < 1% | Most collections — best balance |
77-
| `BINARY` | 32× (float32 → 1-bit) | Higher | High-dimensional vectors (768+), speed priority |
76+
| `SCALAR` | 4× (float32 → int8) | < 1% loss | Most collections — best balance |
77+
| `TURBO` | 8–32× (4-bit to 1-bit) | Low–medium | Better recall than BINARY at same storage budget |
78+
| `BINARY` | 32× (float32 → 1-bit) | Higher loss | Speed priority; centered distributions only |
7879
| `PRODUCT` | 4× (configurable) | Variable | Memory-constrained deployments |
7980

8081
**Full syntax:**
8182
```
8283
CREATE COLLECTION <name> ... QUANTIZE SCALAR [QUANTILE <0.0–1.0>] [ALWAYS RAM]
84+
CREATE COLLECTION <name> ... QUANTIZE TURBO [BITS <1|1.5|2|4>] [ALWAYS RAM]
8385
CREATE COLLECTION <name> ... QUANTIZE BINARY [ALWAYS RAM]
8486
CREATE COLLECTION <name> ... QUANTIZE PRODUCT [ALWAYS RAM]
8587
```
8688

87-
- **`QUANTILE <float>`** — (scalar only) calibration quantile for the INT8 conversion; defaults to Qdrant's built-in default (0.99) when omitted.
88-
- **`ALWAYS RAM`** — keep the **quantized** vectors in RAM at all times, regardless of the collection's `on_disk` setting. Improves search throughput at the cost of higher RAM usage for the compressed index. The original full-precision vectors are stored and managed independently of this flag. Supported by all three quantization types.
89+
- **`QUANTILE <float>`** — (SCALAR only) calibration quantile for the INT8 conversion; defaults to Qdrant's built-in default (0.99) when omitted.
90+
- **`BITS <depth>`** — (TURBO only) bit depth controlling compression ratio:
91+
- `4` — 4-bit, **** compression (default when `BITS` is omitted)
92+
- `2` — 2-bit, **16×** compression
93+
- `1.5` — 1.5-bit, **24×** compression
94+
- `1` — 1-bit, **32×** compression (same ratio as BINARY, but better recall)
95+
- **`ALWAYS RAM`** — keep the **quantized** vectors in RAM at all times, regardless of the collection's `on_disk` setting. Improves search throughput at the cost of higher RAM usage for the compressed index. The original full-precision vectors are stored and managed independently of this flag. Supported by all four quantization types.
8996
- **`QUANTIZE`** always appears **after** all other clauses (`HYBRID`, `USING MODEL`, etc.).
9097
- For `PRODUCT`, the compression ratio is fixed at **** in this version.
98+
- For `TURBO`, all distance metrics are supported; `TURBO` fully supports Cosine, Dot, and Euclidean with SIMD-accelerated scoring.
9199
- When used with `HYBRID` collections, quantization applies only to the **dense** vector.
92100

93101
**Examples:**
@@ -102,6 +110,26 @@ Scalar with explicit calibration and quantized vectors pinned to RAM:
102110
CREATE COLLECTION research_papers QUANTIZE SCALAR QUANTILE 0.95 ALWAYS RAM
103111
```
104112

113+
TurboQuant — default 4-bit (8× compression, good recall):
114+
```sql
115+
CREATE COLLECTION research_papers QUANTIZE TURBO
116+
```
117+
118+
TurboQuant — 2-bit (16× compression):
119+
```sql
120+
CREATE COLLECTION research_papers QUANTIZE TURBO BITS 2
121+
```
122+
123+
TurboQuant — 1.5-bit (24× compression) with quantized vectors pinned to RAM:
124+
```sql
125+
CREATE COLLECTION research_papers QUANTIZE TURBO BITS 1.5 ALWAYS RAM
126+
```
127+
128+
TurboQuant — 1-bit (32× compression, same ratio as BINARY but better recall):
129+
```sql
130+
CREATE COLLECTION research_papers QUANTIZE TURBO BITS 1
131+
```
132+
105133
Binary quantization for large high-dimensional embeddings:
106134
```sql
107135
CREATE COLLECTION research_papers QUANTIZE BINARY
@@ -115,22 +143,29 @@ CREATE COLLECTION research_papers QUANTIZE PRODUCT ALWAYS RAM
115143
Combined with hybrid collection:
116144
```sql
117145
CREATE COLLECTION research_papers HYBRID QUANTIZE SCALAR
146+
CREATE COLLECTION research_papers HYBRID QUANTIZE TURBO BITS 2
118147
```
119148

120149
Combined with a pinned model:
121150
```sql
122151
CREATE COLLECTION research_papers USING MODEL 'BAAI/bge-base-en-v1.5' QUANTIZE SCALAR QUANTILE 0.99
152+
CREATE COLLECTION research_papers USING MODEL 'BAAI/bge-base-en-v1.5' QUANTIZE TURBO BITS 2
153+
```
154+
155+
Combined with hybrid + dense model:
156+
```sql
157+
CREATE COLLECTION research_papers USING HYBRID DENSE MODEL 'BAAI/bge-base-en-v1.5' QUANTIZE TURBO
123158
```
124159

125160
**Valid combinations:**
126161

127-
| Base form | + QUANTIZE SCALAR | + QUANTIZE BINARY | + QUANTIZE PRODUCT |
128-
|---|---|---|---|
129-
| `CREATE COLLECTION name` ||||
130-
| `... HYBRID` ||||
131-
| `... USING MODEL 'x'` ||||
132-
| `... USING HYBRID` ||||
133-
| `... USING HYBRID DENSE MODEL 'x'` ||||
162+
| Base form | + SCALAR | + TURBO | + BINARY | + PRODUCT |
163+
|---|---|---|---|---|
164+
| `CREATE COLLECTION name` |||||
165+
| `... HYBRID` |||||
166+
| `... USING MODEL 'x'` |||||
167+
| `... USING HYBRID` |||||
168+
| `... USING HYBRID DENSE MODEL 'x'` |||||
134169

135170
> INSERT and SEARCH on quantized collections work exactly the same as on non-quantized ones — no changes to INSERT or SEARCH syntax are needed.
136171

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ classifiers = [
3737
"Topic :: Text Processing :: Indexing",
3838
]
3939
dependencies = [
40-
"qdrant-client[fastembed]>=1.13.0",
40+
"qdrant-client[fastembed]>=1.18.0",
4141
"click>=8.1.0",
4242
"rich>=13.0.0",
4343
"prompt_toolkit>=3.0.0",

src/qql/ast_nodes.py

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,14 +9,16 @@ class QuantizationType(Enum):
99
SCALAR = "scalar"
1010
BINARY = "binary"
1111
PRODUCT = "product"
12+
TURBO = "turbo"
1213

1314

1415
@dataclass(frozen=True)
1516
class QuantizationConfig:
1617
"""Quantization settings parsed from a QUANTIZE clause."""
1718
type: QuantizationType
18-
quantile: float | None = None # SCALAR only; None → Qdrant default (0.99)
19-
always_ram: bool = False # all types; default False
19+
quantile: float | None = None # SCALAR only; None → Qdrant default (0.99)
20+
always_ram: bool = False # all types; default False
21+
turbo_bits: float | None = None # TURBO only; None → bits4 (Qdrant default 4-bit, 8×)
2022

2123

2224
@dataclass(frozen=True)

src/qql/executor.py

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,9 @@
4141
ScalarQuantization,
4242
ScalarQuantizationConfig,
4343
ScalarType,
44+
TurboQuantBitSize,
45+
TurboQuantization,
46+
TurboQuantQuantizationConfig,
4447
SearchParams,
4548
SparseVector,
4649
SparseVectorParams,
@@ -846,7 +849,7 @@ def _wrap_as_filter(self, qdrant_expr: Any) -> Filter:
846849

847850
def _build_quantization_config(
848851
self, qc: QuantizationConfig
849-
) -> ScalarQuantization | BinaryQuantization | ProductQuantization:
852+
) -> ScalarQuantization | BinaryQuantization | ProductQuantization | TurboQuantization:
850853
"""Convert a parsed QuantizationConfig to a Qdrant SDK quantization object."""
851854
if qc.type == QuantizationType.SCALAR:
852855
return ScalarQuantization(
@@ -867,6 +870,20 @@ def _build_quantization_config(
867870
always_ram=qc.always_ram,
868871
)
869872
)
873+
if qc.type == QuantizationType.TURBO:
874+
_BITS_MAP: dict[float, TurboQuantBitSize] = {
875+
4.0: TurboQuantBitSize.BITS4,
876+
2.0: TurboQuantBitSize.BITS2,
877+
1.5: TurboQuantBitSize.BITS1_5,
878+
1.0: TurboQuantBitSize.BITS1,
879+
}
880+
bits_enum = _BITS_MAP.get(qc.turbo_bits or 4.0, TurboQuantBitSize.BITS4)
881+
return TurboQuantization(
882+
turbo=TurboQuantQuantizationConfig(
883+
bits=bits_enum,
884+
always_ram=qc.always_ram,
885+
)
886+
)
870887
raise QQLRuntimeError(f"Unknown quantization type: {qc.type}")
871888

872889
def _collection_is_hybrid(self, name: str) -> bool:

src/qql/lexer.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,8 @@ class TokenKind(Enum):
2727
QUANTILE = auto()
2828
ALWAYS = auto()
2929
RAM = auto()
30+
TURBO = auto()
31+
BITS = auto()
3032
CREATE = auto()
3133
INDEX = auto()
3234
ON = auto()
@@ -113,6 +115,8 @@ class TokenKind(Enum):
113115
"QUANTILE": TokenKind.QUANTILE,
114116
"ALWAYS": TokenKind.ALWAYS,
115117
"RAM": TokenKind.RAM,
118+
"TURBO": TokenKind.TURBO,
119+
"BITS": TokenKind.BITS,
116120
"CREATE": TokenKind.CREATE,
117121
"INDEX": TokenKind.INDEX,
118122
"ON": TokenKind.ON,

src/qql/parser.py

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -248,8 +248,32 @@ def _parse_quantize_clause(self) -> QuantizationConfig:
248248
always_ram = True
249249
return QuantizationConfig(type=QuantizationType.PRODUCT, always_ram=always_ram)
250250

251+
if tok.kind == TokenKind.TURBO:
252+
self._advance()
253+
turbo_bits: float | None = None
254+
always_ram = False
255+
if self._peek().kind == TokenKind.BITS:
256+
self._advance()
257+
bits_tok = self._peek()
258+
raw = float(self._parse_number())
259+
if raw not in (1.0, 1.5, 2.0, 4.0):
260+
raise QQLSyntaxError(
261+
f"BITS must be one of 1, 1.5, 2, or 4 for TURBO quantization, got {raw}",
262+
bits_tok.pos,
263+
)
264+
turbo_bits = raw
265+
if self._peek().kind == TokenKind.ALWAYS:
266+
self._advance()
267+
self._expect(TokenKind.RAM)
268+
always_ram = True
269+
return QuantizationConfig(
270+
type=QuantizationType.TURBO,
271+
turbo_bits=turbo_bits,
272+
always_ram=always_ram,
273+
)
274+
251275
raise QQLSyntaxError(
252-
f"Expected SCALAR, BINARY, or PRODUCT after QUANTIZE, got '{tok.value}'",
276+
f"Expected SCALAR, BINARY, PRODUCT, or TURBO after QUANTIZE, got '{tok.value}'",
253277
tok.pos,
254278
)
255279

tests/test_executor.py

Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1640,3 +1640,101 @@ def test_result_message_no_quantization_suffix_when_absent(self, executor, mock_
16401640
node = CreateCollectionStmt(collection="articles")
16411641
result = executor.execute(node)
16421642
assert "quantization" not in result.message
1643+
1644+
1645+
class TestTurboQuantCreate:
1646+
"""Executor tests for QUANTIZE TURBO — verifies correct SDK objects are built."""
1647+
1648+
@pytest.fixture
1649+
def executor(self, cfg, mock_client):
1650+
return Executor(mock_client, cfg)
1651+
1652+
# ── TurboQuantization object is produced ──────────────────────────────
1653+
1654+
def test_turbo_passes_turbo_quantization(self, executor, mock_client):
1655+
from qdrant_client.models import TurboQuantization
1656+
node = CreateCollectionStmt(
1657+
collection="articles",
1658+
quantization=QuantizationConfig(type=QuantizationType.TURBO),
1659+
)
1660+
executor.execute(node)
1661+
kw = mock_client.create_collection.call_args.kwargs
1662+
assert isinstance(kw.get("quantization_config"), TurboQuantization)
1663+
1664+
def test_turbo_default_bits_is_bits4(self, executor, mock_client):
1665+
from qdrant_client.models import TurboQuantBitSize
1666+
node = CreateCollectionStmt(
1667+
collection="articles",
1668+
quantization=QuantizationConfig(type=QuantizationType.TURBO),
1669+
)
1670+
executor.execute(node)
1671+
kw = mock_client.create_collection.call_args.kwargs
1672+
assert kw["quantization_config"].turbo.bits == TurboQuantBitSize.BITS4
1673+
1674+
def test_turbo_bits2(self, executor, mock_client):
1675+
from qdrant_client.models import TurboQuantBitSize
1676+
node = CreateCollectionStmt(
1677+
collection="articles",
1678+
quantization=QuantizationConfig(type=QuantizationType.TURBO, turbo_bits=2.0),
1679+
)
1680+
executor.execute(node)
1681+
kw = mock_client.create_collection.call_args.kwargs
1682+
assert kw["quantization_config"].turbo.bits == TurboQuantBitSize.BITS2
1683+
1684+
def test_turbo_bits1_5(self, executor, mock_client):
1685+
from qdrant_client.models import TurboQuantBitSize
1686+
node = CreateCollectionStmt(
1687+
collection="articles",
1688+
quantization=QuantizationConfig(type=QuantizationType.TURBO, turbo_bits=1.5),
1689+
)
1690+
executor.execute(node)
1691+
kw = mock_client.create_collection.call_args.kwargs
1692+
assert kw["quantization_config"].turbo.bits == TurboQuantBitSize.BITS1_5
1693+
1694+
def test_turbo_bits1(self, executor, mock_client):
1695+
from qdrant_client.models import TurboQuantBitSize
1696+
node = CreateCollectionStmt(
1697+
collection="articles",
1698+
quantization=QuantizationConfig(type=QuantizationType.TURBO, turbo_bits=1.0),
1699+
)
1700+
executor.execute(node)
1701+
kw = mock_client.create_collection.call_args.kwargs
1702+
assert kw["quantization_config"].turbo.bits == TurboQuantBitSize.BITS1
1703+
1704+
def test_turbo_always_ram_true(self, executor, mock_client):
1705+
node = CreateCollectionStmt(
1706+
collection="articles",
1707+
quantization=QuantizationConfig(type=QuantizationType.TURBO, always_ram=True),
1708+
)
1709+
executor.execute(node)
1710+
kw = mock_client.create_collection.call_args.kwargs
1711+
assert kw["quantization_config"].turbo.always_ram is True
1712+
1713+
def test_turbo_always_ram_false_by_default(self, executor, mock_client):
1714+
node = CreateCollectionStmt(
1715+
collection="articles",
1716+
quantization=QuantizationConfig(type=QuantizationType.TURBO),
1717+
)
1718+
executor.execute(node)
1719+
kw = mock_client.create_collection.call_args.kwargs
1720+
assert kw["quantization_config"].turbo.always_ram is False
1721+
1722+
def test_turbo_hybrid_collection_has_both_configs(self, executor, mock_client):
1723+
from qdrant_client.models import TurboQuantization
1724+
node = CreateCollectionStmt(
1725+
collection="articles",
1726+
hybrid=True,
1727+
quantization=QuantizationConfig(type=QuantizationType.TURBO),
1728+
)
1729+
executor.execute(node)
1730+
kw = mock_client.create_collection.call_args.kwargs
1731+
assert isinstance(kw.get("quantization_config"), TurboQuantization)
1732+
assert "sparse_vectors_config" in kw
1733+
1734+
def test_turbo_result_message_includes_turbo(self, executor, mock_client):
1735+
node = CreateCollectionStmt(
1736+
collection="articles",
1737+
quantization=QuantizationConfig(type=QuantizationType.TURBO),
1738+
)
1739+
result = executor.execute(node)
1740+
assert "turbo" in result.message

0 commit comments

Comments
 (0)