Skip to content

Commit e8a33bb

Browse files
committed
Enhance collection and index creation with HNSW support and advanced options
- Added HNSW clause with payload_m parameter for collection creation. - Updated CREATE INDEX to support advanced options for keyword, uuid, and text types. - Enhanced error handling for unknown HNSW parameters and validation for payload_m. - Updated documentation to reflect new features and usage examples. - Added tests for HNSW and advanced index options.
1 parent 73c1b82 commit e8a33bb

10 files changed

Lines changed: 425 additions & 17 deletions

File tree

README.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -123,11 +123,15 @@ SELECT * FROM articles WHERE id = '3f2e1a4b-...'
123123
-- Collections
124124
CREATE COLLECTION articles
125125
CREATE COLLECTION articles HYBRID
126+
CREATE COLLECTION articles HNSW { payload_m: 16 }
126127
CREATE COLLECTION articles QUANTIZE SCALAR
127128
CREATE COLLECTION articles QUANTIZE TURBO
128129
CREATE COLLECTION articles QUANTIZE TURBO BITS 2
129130
CREATE COLLECTION articles QUANTIZE TURBO BITS 1.5 ALWAYS RAM
130131
CREATE INDEX ON COLLECTION articles FOR year TYPE integer
132+
CREATE INDEX ON COLLECTION articles FOR tenant_id TYPE keyword WITH { is_tenant: true, on_disk: true }
133+
CREATE INDEX ON COLLECTION articles FOR doc_id TYPE uuid
134+
CREATE INDEX ON COLLECTION articles FOR title TYPE text WITH { tokenizer: 'word', min_token_len: 2, lowercase: true }
131135
SHOW COLLECTIONS
132136
SHOW COLLECTION articles
133137
DROP COLLECTION articles

docs/collections.md

Lines changed: 42 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -93,9 +93,10 @@ CREATE COLLECTION <collection_name> HYBRID
9393
CREATE COLLECTION <collection_name> USING MODEL '<model_name>'
9494
CREATE COLLECTION <collection_name> USING HYBRID
9595
CREATE COLLECTION <collection_name> USING HYBRID DENSE MODEL '<model>'
96+
CREATE COLLECTION <collection_name> HNSW { payload_m: <int> }
9697
```
9798

98-
Any of the above forms can be followed by an optional `QUANTIZE` clause — see [Quantization](#quantization--quantize-clause) below.
99+
Any of the above forms can be followed by an optional `QUANTIZE` clause and/or `HNSW { payload_m: <int> }`.
99100

100101
**Examples:**
101102

@@ -119,8 +120,25 @@ Hybrid collection with a custom dense model:
119120
CREATE COLLECTION research_papers USING HYBRID DENSE MODEL 'BAAI/bge-base-en-v1.5'
120121
```
121122

123+
Dense collection with payload-aware HNSW links:
124+
```sql
125+
CREATE COLLECTION research_papers HNSW {payload_m: 16}
126+
```
127+
122128
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.
123129

130+
### HNSW clause
131+
132+
QQL currently supports one explicit HNSW knob during collection creation:
133+
134+
- `payload_m` — enables payload-aware HNSW connectivity used by Qdrant for filtered / tenant-aware workloads
135+
136+
Example:
137+
138+
```sql
139+
CREATE COLLECTION tenant_docs USING HYBRID HNSW {payload_m: 16}
140+
```
141+
124142
---
125143

126144
## Quantization — QUANTIZE clause
@@ -239,6 +257,7 @@ Creates a payload index on a collection field. Payload indexes speed up `WHERE`
239257
**Syntax:**
240258
```
241259
CREATE INDEX ON COLLECTION <collection_name> FOR <field_name> TYPE <schema_type>
260+
CREATE INDEX ON COLLECTION <collection_name> FOR <field_name> TYPE <schema_type> WITH { ... }
242261
```
243262

244263
**Supported schema types:**
@@ -252,19 +271,41 @@ CREATE INDEX ON COLLECTION <collection_name> FOR <field_name> TYPE <schema_type>
252271
| `text` | Full-text search (enables `MATCH` operators) |
253272
| `geo` | Geospatial coordinates |
254273
| `datetime` | Date/time values |
274+
| `uuid` | UUID payload values |
255275

256276
**Examples:**
257277

258278
```sql
259279
CREATE INDEX ON COLLECTION articles FOR category TYPE keyword
280+
CREATE INDEX ON COLLECTION articles FOR tenant_id TYPE keyword WITH {is_tenant: true, on_disk: true, enable_hnsw: true}
260281
CREATE INDEX ON COLLECTION articles FOR year TYPE integer
282+
CREATE INDEX ON COLLECTION articles FOR doc_id TYPE uuid
261283
CREATE INDEX ON COLLECTION articles FOR title TYPE text
284+
CREATE INDEX ON COLLECTION articles FOR title TYPE text WITH {tokenizer: 'word', min_token_len: 2, max_token_len: 20, lowercase: true, phrase_matching: true}
262285
CREATE INDEX ON COLLECTION articles FOR meta.author TYPE keyword
263286
```
264287

288+
**Advanced options currently supported:**
289+
290+
- `keyword` / `uuid`
291+
- `is_tenant: true|false`
292+
- `on_disk: true|false`
293+
- `enable_hnsw: true|false`
294+
- `text`
295+
- `tokenizer: 'prefix'|'whitespace'|'word'|'multilingual'`
296+
- `min_token_len: <int>`
297+
- `max_token_len: <int>`
298+
- `lowercase: true|false`
299+
- `ascii_folding: true|false`
300+
- `phrase_matching: true|false`
301+
- `stopwords: 'english'` or `stopwords: ['a', 'the']`
302+
- `on_disk: true|false`
303+
- `enable_hnsw: true|false`
304+
265305
**Rules:**
266306
- The collection must already exist. Raises an error otherwise.
267307
- Indexes are idempotent — creating the same index twice succeeds silently.
308+
- Advanced `WITH { ... }` options are currently supported only for `keyword`, `uuid`, and `text`.
268309

269310
---
270311

docs/programmatic.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -88,7 +88,7 @@ result = run_query(
8888
)
8989
print(result.data["topology"]) # "dense" or "hybrid"
9090
print(result.data["vectors"]) # {"": {...}} or {"dense": {...}, ...}
91-
print(result.data["payload_schema"]) # {"field": "keyword", ...} or None
91+
print(result.data["payload_schema"]) # {"field": {"type": "keyword", ...}, ...} or None
9292
```
9393

9494
---

docs/reference.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -192,5 +192,6 @@ Expected output: **500 tests passing**.
192192
| `Vector elements must be numeric; got invalid value: ...` | A non-numeric value (string or null) was present in the vector array for `UPDATE SET VECTOR` | Ensure all vector elements are floats: `UPDATE … [0.1, 0.2, …, 0.N]` |
193193
| `GROUP_SIZE must be a positive integer, got N` | `GROUP_SIZE 0` or a negative value was specified | Use a positive integer: `GROUP_SIZE 3` |
194194
| `Qdrant error during SCROLL: ...` | Qdrant rejected scroll request | Verify collection state, filter, and cursor (`AFTER`) value |
195-
| `Unknown index type '...'` | Invalid schema type in CREATE INDEX | Use one of: `keyword`, `integer`, `float`, `bool`, `text`, `geo`, `datetime` |
195+
| `Unknown index type '...'` | Invalid schema type in CREATE INDEX | Use one of: `keyword`, `integer`, `float`, `bool`, `text`, `geo`, `datetime`, `uuid` |
196+
| `Unknown CREATE INDEX option '...'` | Unsupported advanced option for the chosen payload index type | Check which `WITH { ... }` keys are supported for `keyword`, `uuid`, or `text` |
196197
| `Qdrant error during CREATE INDEX: ...` | Qdrant rejected the index creation | Check field name and collection state |

src/qql/ast_nodes.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -172,13 +172,15 @@ class CreateCollectionStmt:
172172
hybrid: bool = False # if True, create with dense + sparse named vectors
173173
model: str | None = None # dense model; None → use config default
174174
quantization: QuantizationConfig | None = None # optional QUANTIZE clause
175+
payload_m: int | None = None # optional HNSW { payload_m: N } clause
175176

176177

177178
@dataclass(frozen=True)
178179
class CreateIndexStmt:
179180
collection: str
180181
field_name: str
181182
schema: str
183+
options: dict[str, Any] | None = None
182184

183185

184186
@dataclass(frozen=True)

src/qql/cli.py

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@
3838
Create a new collection. Add HYBRID for dense+sparse BM25 vectors.
3939
Optional: [yellow]USING MODEL[/yellow] '<model>'
4040
Optional: [yellow]USING HYBRID[/yellow] [DENSE MODEL '<model>']
41+
Optional: [yellow]HNSW[/yellow] { payload_m: <int> }
4142
Optional: [yellow]QUANTIZE SCALAR[/yellow] [QUANTILE <0.0–1.0>] [ALWAYS RAM]
4243
Optional: [yellow]QUANTIZE BINARY[/yellow] [ALWAYS RAM]
4344
Optional: [yellow]QUANTIZE PRODUCT[/yellow] [ALWAYS RAM] (4× compression)
@@ -46,6 +47,11 @@
4647
[yellow]DROP COLLECTION[/yellow] <name>
4748
Delete a collection and all its points.
4849
50+
[yellow]CREATE INDEX ON COLLECTION[/yellow] <name> [yellow]FOR[/yellow] <field> [yellow]TYPE[/yellow] <schema>
51+
Create a payload index for filtering or text search.
52+
Optional: [yellow]WITH[/yellow] { is_tenant, on_disk, enable_hnsw } for keyword/uuid
53+
Optional: [yellow]WITH[/yellow] { tokenizer, min_token_len, max_token_len, lowercase, ascii_folding, phrase_matching, stopwords, on_disk, enable_hnsw } for text
54+
4955
[yellow]SHOW COLLECTIONS[/yellow]
5056
List all collections in the connected Qdrant instance.
5157
@@ -420,8 +426,16 @@ def _format_collection_diagnostics(data: dict) -> str:
420426
schema = data["payload_schema"]
421427
if schema:
422428
lines.append(" Payload indexes:")
423-
for field, dtype in schema.items():
424-
lines.append(f" {field}: {dtype}")
429+
for field, index_info in schema.items():
430+
if isinstance(index_info, dict):
431+
line = f" {field}: {index_info.get('type')}"
432+
params = index_info.get("params")
433+
if params:
434+
rendered = ", ".join(f"{k}={v}" for k, v in params.items())
435+
line += f" ({rendered})"
436+
lines.append(line)
437+
else:
438+
lines.append(f" {field}: {index_info}")
425439
else:
426440
lines.append(" Payload indexes : none")
427441

0 commit comments

Comments
 (0)