Skip to content

Commit 573f850

Browse files
committed
2 new concepts 1. Update and 2.Group By
1 parent 74ac147 commit 573f850

11 files changed

Lines changed: 844 additions & 7 deletions

File tree

README.md

Lines changed: 16 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,9 @@
55
[![PyPI version](https://img.shields.io/pypi/v/qql-cli?color=blue&label=PyPI)](https://pypi.org/project/qql-cli/)
66
[![Python 3.12+](https://img.shields.io/pypi/pyversions/qql-cli)](https://pypi.org/project/qql-cli/)
77
[![MIT License](https://img.shields.io/badge/license-MIT-green)](LICENSE)
8-
[![Tests](https://img.shields.io/badge/tests-405%20passing-brightgreen)](tests/)
8+
[![Tests](https://img.shields.io/badge/tests-485%20passing-brightgreen)](tests/)
99

10-
Write `INSERT`, `SELECT`, `SEARCH`, `SCROLL`, `RECOMMEND`, `DELETE`, and `CREATE COLLECTION` statements instead of Python SDK calls. Supports hybrid dense+sparse vector search, cross-encoder reranking, quantization (scalar, turbo, binary, product), SQL-style `WHERE` filters, script execution, and collection dump/restore.
10+
Write `INSERT`, `SELECT`, `SEARCH`, `SCROLL`, `RECOMMEND`, `UPDATE`, `DELETE`, and `CREATE COLLECTION` statements instead of Python SDK calls. Supports hybrid dense+sparse vector search, grouped search (GROUP BY), cross-encoder reranking, quantization (scalar, turbo, binary, product), SQL-style `WHERE` filters, script execution, and collection dump/restore.
1111

1212
```
1313
qql> INSERT INTO COLLECTION notes VALUES {'text': 'Qdrant is a vector database', 'author': 'alice', 'year': 2024}
@@ -82,9 +82,9 @@ Full documentation lives in the [`docs/`](docs/) folder and at **[pavanjava.gith
8282
|---|---|
8383
| [Getting Started](docs/getting-started.md) | Installation, connecting, first queries |
8484
| [INSERT / INSERT BULK](docs/insert.md) | Adding documents, batch inserts, payload types |
85-
| [SEARCH / SELECT / SCROLL / RECOMMEND / Hybrid / RERANK](docs/search.md) | Semantic search, point retrieval, pagination, hybrid, reranking, recommendations |
85+
| [SEARCH / SELECT / SCROLL / RECOMMEND / Hybrid / GROUP BY / RERANK](docs/search.md) | Semantic search, grouped search, point retrieval, pagination, hybrid, reranking, recommendations |
8686
| [WHERE Filters](docs/filters.md) | Full SQL-style filter operators |
87-
| [Collections & Quantization](docs/collections.md) | SHOW, 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, UPDATE VECTOR, UPDATE PAYLOAD |
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 |
@@ -128,6 +128,17 @@ SHOW COLLECTIONS
128128
SHOW COLLECTION articles
129129
DROP COLLECTION articles
130130

131+
-- Search with grouping
132+
SEARCH articles SIMILAR TO 'query' LIMIT 5 GROUP BY category
133+
SEARCH articles SIMILAR TO 'query' LIMIT 5 GROUP BY category GROUP_SIZE 3
134+
SEARCH articles SIMILAR TO 'query' LIMIT 5 WHERE year >= 2020 GROUP BY category GROUP_SIZE 2
135+
SEARCH articles SIMILAR TO 'query' LIMIT 5 USING HYBRID GROUP BY category
136+
137+
-- Update
138+
UPDATE articles SET VECTOR WHERE id = '3f2e1a4b-...' [0.1, 0.2, 0.3, 0.4]
139+
UPDATE articles SET PAYLOAD WHERE id = '3f2e1a4b-...' {'year': 2025, 'status': 'active'}
140+
UPDATE articles SET PAYLOAD WHERE category = 'draft' {'status': 'published'}
141+
131142
-- Delete
132143
DELETE FROM articles WHERE id = '3f2e1a4b-...'
133144
DELETE FROM articles WHERE year < 2020
@@ -147,7 +158,7 @@ Tests do not require a running Qdrant instance — the Qdrant client is mocked.
147158
pytest tests/ -v
148159
```
149160

150-
Expected: **405 tests passing**.
161+
Expected: **485 tests passing**.
151162

152163
---
153164

docs/collections.md

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -310,3 +310,68 @@ DELETE FROM articles WHERE year < 2020 AND status = 'draft'
310310
**Notes:**
311311
- If no points match the filter or ID, the operation succeeds silently with a count of 0.
312312
- The collection itself must exist; deleting from a non-existent collection raises an error.
313+
314+
---
315+
316+
## UPDATE SET VECTOR — replace a point's dense vector
317+
318+
Replaces the stored dense vector for a **single point** identified by its ID. The point must already exist in the collection. Use this when you want to refresh an embedding without changing the payload.
319+
320+
**Syntax:**
321+
```
322+
UPDATE <collection> SET VECTOR WHERE id = '<point_id>' [<vector>]
323+
UPDATE <collection> SET VECTOR WHERE id = <integer_id> [<vector>]
324+
```
325+
326+
The vector is provided as a JSON-style float array `[v1, v2, ..., vN]`. The array length must match the collection's configured vector dimensions.
327+
328+
**Examples:**
329+
330+
```sql
331+
-- Replace vector by UUID
332+
UPDATE articles SET VECTOR WHERE id = '3f2e1a4b-8c91-4d0e-b123-abc123def456' [0.1, 0.2, 0.3, 0.4]
333+
334+
-- Replace vector by integer ID
335+
UPDATE articles SET VECTOR WHERE id = 42 [0.1, 0.2, 0.3, 0.4]
336+
```
337+
338+
**Notes:**
339+
- Only single-point updates are supported (by ID). Bulk or filter-based vector updates are not supported.
340+
- The point must already exist; this operation does not create new points.
341+
- The collection must exist; updating from a non-existent collection raises an error.
342+
- For hybrid collections, the dense vector named `"dense"` is updated. Sparse vectors are managed separately.
343+
344+
---
345+
346+
## UPDATE SET PAYLOAD — merge fields into a point's payload
347+
348+
Merges new key/value pairs into the payload of one or more points. **Existing fields not mentioned in the update are preserved** (additive merge, not a full replace). Use a `WHERE` filter to update multiple points at once.
349+
350+
**Syntax:**
351+
```
352+
UPDATE <collection> SET PAYLOAD WHERE id = '<point_id>' {<payload>}
353+
UPDATE <collection> SET PAYLOAD WHERE id = <integer_id> {<payload>}
354+
UPDATE <collection> SET PAYLOAD WHERE <filter> {<payload>}
355+
```
356+
357+
**Examples:**
358+
359+
```sql
360+
-- Update a single point by UUID
361+
UPDATE articles SET PAYLOAD WHERE id = '3f2e1a4b-8c91-4d0e-b123-abc123def456' {'year': 2025, 'status': 'active'}
362+
363+
-- Update a single point by integer ID
364+
UPDATE articles SET PAYLOAD WHERE id = 42 {'category': 'tech'}
365+
366+
-- Update all points matching a filter
367+
UPDATE articles SET PAYLOAD WHERE category = 'draft' {'status': 'published'}
368+
369+
-- Compound filter update
370+
UPDATE articles SET PAYLOAD WHERE year < 2020 AND status = 'draft' {'archived': true}
371+
```
372+
373+
**Notes:**
374+
- **Merge semantics:** only the fields in `{…}` are written; all other existing payload fields are preserved.
375+
- If no points match the filter, the operation succeeds silently with no changes.
376+
- The collection must exist; updating from a non-existent collection raises an error.
377+
- All `WHERE` filter operators supported by `DELETE` are also supported here (see [WHERE Filters](filters.md)).

docs/reference.md

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -162,7 +162,7 @@ Tests do not require a running Qdrant instance — the Qdrant client is mocked.
162162
pytest tests/ -v
163163
```
164164

165-
Expected output: **405 tests passing**.
165+
Expected output: **485 tests passing**.
166166

167167
---
168168

@@ -174,14 +174,20 @@ Expected output: **405 tests passing**.
174174
| `Connection failed: ...` | Qdrant unreachable at given URL | Check that Qdrant is running and the URL is correct |
175175
| `INSERT requires a 'text' field in VALUES` | `text` key missing from the VALUES dict | Add `'text': '...'` to your dict |
176176
| `Vector dimension mismatch: collection '...' expects X dims, but model produces Y dims` | Model used in INSERT differs from the one used to create the collection | Use `USING MODEL` to specify the same model as the collection was created with |
177-
| `Collection '...' does not exist` | SEARCH / SCROLL / SELECT / DROP / DELETE on a non-existent collection | Check name spelling or run `SHOW COLLECTIONS` |
177+
| `Collection '...' does not exist` | SEARCH / SCROLL / SELECT / DROP / DELETE / UPDATE on a non-existent collection | Check name spelling or run `SHOW COLLECTIONS` |
178178
| `Unexpected token '...'; expected a QQL statement keyword` | Unrecognized statement | Check the query syntax and supported statement list |
179179
| `SELECT requires a string or integer point id, got '...'` | `SELECT` used with a non-ID filter value | Use `SELECT * FROM <collection> WHERE id = '<id>'` or an integer ID |
180180
| `Unterminated string literal (at position N)` | A string is missing its closing quote | Close the string with a matching `'` or `"` |
181181
| `Unexpected character '@' (at position N)` | A character not part of QQL syntax | Remove or quote the offending character |
182182
| `Expected a filter operator after field '...'` | Unknown operator in WHERE clause | Use one of: `=`, `!=`, `>`, `>=`, `<`, `<=`, `IN`, `NOT IN`, `BETWEEN`, `IS NULL`, `IS NOT NULL`, `IS EMPTY`, `IS NOT EMPTY`, `MATCH` |
183183
| `Expected ')' ...` | Unclosed parenthesis in WHERE clause | Add the missing `)` to close the group |
184184
| `Qdrant error during SEARCH: ...` | Hybrid search on a non-hybrid collection, or wrong vector names | Ensure the collection was created with `HYBRID` before using `USING HYBRID` in INSERT/SEARCH |
185+
| `Qdrant error during GROUP BY SEARCH: ...` | GROUP BY on an unindexed field, or unsupported field type | Ensure the group-by field is indexed as `keyword` or `integer` via `CREATE INDEX` |
186+
| `GROUP BY and RERANK cannot be combined ...` | Both GROUP BY and RERANK specified in the same SEARCH | Remove one of the two clauses |
187+
| `Expected VECTOR or PAYLOAD after SET, got '...'` | Unknown keyword after SET in UPDATE | Use `UPDATE ... SET VECTOR ...` or `UPDATE ... SET PAYLOAD ...` |
188+
| `Expected a vector list [...] after point ID in UPDATE SET VECTOR` | UPDATE SET VECTOR missing the `[...]` float array | Add the vector array: `UPDATE ... SET VECTOR WHERE id = '...' [0.1, 0.2, ...]` |
189+
| `Qdrant error during UPDATE VECTOR: ...` | Point does not exist, or vector dimensions mismatch | Verify the point ID exists and the vector length matches the collection's dimensions |
190+
| `Qdrant error during UPDATE PAYLOAD: ...` | Qdrant rejected the payload update | Check field values and collection state |
185191
| `Qdrant error during SCROLL: ...` | Qdrant rejected scroll request | Verify collection state, filter, and cursor (`AFTER`) value |
186192
| `Unknown index type '...'` | Invalid schema type in CREATE INDEX | Use one of: `keyword`, `integer`, `float`, `bool`, `text`, `geo`, `datetime` |
187193
| `Qdrant error during CREATE INDEX: ...` | Qdrant rejected the index creation | Check field name and collection state |

docs/search.md

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -343,3 +343,68 @@ SEARCH articles SIMILAR TO 'semantic search' LIMIT 5
343343
| Large collections with keyword-heavy queries | `USING HYBRID RERANK` |
344344

345345
> **Note on scores:** After reranking, the `score` column shows the cross-encoder's raw logit (can be any real number, unbounded). Do not compare reranked scores to non-reranked cosine similarity scores.
346+
347+
---
348+
349+
## SEARCH … GROUP BY — grouped results
350+
351+
Returns the top-scoring points **grouped by a payload field value**. Instead of a single flat ranked list, results are organised into groups — each group contains the top-scoring points that share the same value for the specified field.
352+
353+
Useful for **result diversification**: e.g. "return the 3 best articles from each category", or "show the top 2 papers per author".
354+
355+
**Syntax:**
356+
```
357+
SEARCH <collection> SIMILAR TO '<query>' LIMIT <n> GROUP BY <field>
358+
SEARCH <collection> SIMILAR TO '<query>' LIMIT <n> GROUP BY <field> GROUP_SIZE <m>
359+
SEARCH <collection> SIMILAR TO '<query>' LIMIT <n> [WHERE <filter>] GROUP BY <field> [GROUP_SIZE <m>]
360+
SEARCH <collection> SIMILAR TO '<query>' LIMIT <n> USING HYBRID GROUP BY <field> [GROUP_SIZE <m>]
361+
```
362+
363+
- **`LIMIT <n>`** — maximum number of **groups** to return.
364+
- **`GROUP_SIZE <m>`** — maximum number of points per group (default: **3**).
365+
- **`GROUP BY <field>`** — the payload field whose values define the groups. Dot-notation is supported (e.g. `meta.author`). The field should be indexed as `keyword` or `integer` for best performance.
366+
- `WHERE` filters, `USING HYBRID`, and `USING MODEL` are all compatible with GROUP BY.
367+
- **`GROUP BY` and `RERANK` cannot be combined** in the same statement — this raises a syntax error.
368+
369+
**Examples:**
370+
371+
Top 5 categories, up to 3 articles each (default group_size):
372+
```sql
373+
SEARCH articles SIMILAR TO 'machine learning' LIMIT 5 GROUP BY category
374+
```
375+
376+
Top 3 authors, up to 2 papers each:
377+
```sql
378+
SEARCH papers SIMILAR TO 'neural networks' LIMIT 3 GROUP BY author GROUP_SIZE 2
379+
```
380+
381+
Grouped search with a payload filter:
382+
```sql
383+
SEARCH articles SIMILAR TO 'deep learning' LIMIT 5 WHERE year >= 2022 GROUP BY category GROUP_SIZE 4
384+
```
385+
386+
Grouped hybrid search:
387+
```sql
388+
SEARCH articles SIMILAR TO 'vector databases' LIMIT 4 USING HYBRID GROUP BY category GROUP_SIZE 3
389+
```
390+
391+
**Output:**
392+
393+
```
394+
✓ Found 3 group(s) by 'category' (grouped)
395+
Group: machine-learning
396+
Score │ ID │ Payload
397+
────────┼──────────────────────────────────────┼────────────────────────────────────────
398+
0.9312 │ 3f2e1a4b-8c91-4d0e-b123-abc123def456 │ {'text': '...', 'category': 'machine-learning'}
399+
0.8845 │ 9a1b2c3d-4e5f-6789-abcd-ef0123456789 │ {'text': '...', 'category': 'machine-learning'}
400+
401+
Group: nlp
402+
Score │ ID │ Payload
403+
────────┼──────────────────────────────────────┼────────────────────────────────────────
404+
0.9100 │ 1a2b3c4d-5e6f-7890-bcde-f01234567890 │ {'text': '...', 'category': 'nlp'}
405+
```
406+
407+
> **Tip:** For GROUP BY to work efficiently, create a payload index on the grouping field first:
408+
> ```sql
409+
> CREATE INDEX ON COLLECTION articles FOR category TYPE keyword
410+
> ```

src/qql/ast_nodes.py

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -213,6 +213,8 @@ class SearchStmt:
213213
rerank: bool = False # if True, apply cross-encoder reranking post-Qdrant
214214
rerank_model: str | None = None # cross-encoder model; None → CrossEncoderEmbedder.DEFAULT_MODEL
215215
with_clause: SearchWith | None = None
216+
group_by: str | None = None # GROUP BY field name; None → normal flat search
217+
group_size: int = 3 # max points per group (ignored when group_by is None)
216218

217219

218220
@dataclass(frozen=True)
@@ -237,6 +239,23 @@ class DeleteStmt:
237239
query_filter: FilterExpr | None = None
238240

239241

242+
@dataclass(frozen=True)
243+
class UpdateVectorStmt:
244+
"""UPDATE <collection> SET VECTOR WHERE id = <id> [vector...]"""
245+
collection: str
246+
point_id: str | int
247+
vector: tuple[float, ...] # dense vector as immutable tuple (frozen=True compatible)
248+
249+
250+
@dataclass(frozen=True)
251+
class UpdatePayloadStmt:
252+
"""UPDATE <collection> SET PAYLOAD WHERE <filter|id> {payload}"""
253+
collection: str
254+
payload: dict[str, Any]
255+
point_id: str | int | None = None # mutually exclusive with query_filter
256+
query_filter: FilterExpr | None = None
257+
258+
240259
# Union type for all top-level statement nodes
241260
ASTNode = (
242261
InsertStmt
@@ -251,4 +270,6 @@ class DeleteStmt:
251270
| SearchStmt
252271
| RecommendStmt
253272
| DeleteStmt
273+
| UpdateVectorStmt
274+
| UpdatePayloadStmt
254275
)

0 commit comments

Comments
 (0)