Skip to content

Commit 644e85b

Browse files
committed
feat: add SELECT statement for retrieving a point by ID
1 parent 9e70cfc commit 644e85b

17 files changed

Lines changed: 198 additions & 11 deletions

README.md

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
[![MIT License](https://img.shields.io/badge/license-MIT-green)](LICENSE)
88
[![Tests](https://img.shields.io/badge/tests-405%20passing-brightgreen)](tests/)
99

10-
Write `INSERT`, `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`, `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.
1111

1212
```
1313
qql> INSERT INTO COLLECTION notes VALUES {'text': 'Qdrant is a vector database', 'author': 'alice', 'year': 2024}
@@ -82,7 +82,7 @@ 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 / SCROLL / RECOMMEND / Hybrid / RERANK](docs/search.md) | Semantic search, pagination, hybrid, reranking, recommendations |
85+
| [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 |
8787
| [Collections & Quantization](docs/collections.md) | CREATE, DROP, QUANTIZE (scalar/turbo/binary/product), CREATE INDEX |
8888
| [Scripts: EXECUTE / DUMP](docs/scripts.md) | Script files, collection backup/restore |
@@ -113,6 +113,9 @@ SCROLL FROM articles AFTER 'cursor-id' LIMIT 50
113113
-- Recommend
114114
RECOMMEND FROM articles POSITIVE IDS (1001, 1002) LIMIT 5
115115

116+
-- Select (retrieve a point by ID)
117+
SELECT * FROM articles WHERE id = '3f2e1a4b-...'
118+
116119
-- Collections
117120
CREATE COLLECTION articles
118121
CREATE COLLECTION articles HYBRID

docs/getting-started.md

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -143,14 +143,17 @@ SCROLL FROM notes LIMIT 10
143143

144144
-- List all collections
145145
SHOW COLLECTIONS
146+
147+
-- Retrieve a point by ID
148+
SELECT * FROM notes WHERE id = 1
146149
```
147150

148151
---
149152

150153
## Next Steps
151154

152155
- [INSERT / INSERT BULK](insert.md) — adding documents
153-
- [SEARCH / SCROLL / RECOMMEND / Hybrid / RERANK](search.md) — querying
156+
- [SEARCH / SELECT / SCROLL / RECOMMEND / Hybrid / RERANK](search.md) — querying
154157
- [WHERE Filters](filters.md) — payload filtering
155158
- [Collections & Quantization](collections.md) — managing collections
156159
- [Scripts: EXECUTE / DUMP](scripts.md) — automating with script files

docs/index.html

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -148,8 +148,8 @@ <h3>INSERT / INSERT BULK</h3>
148148
<p>Adding documents, batch inserts, payload types</p>
149149
</a>
150150
<a class="card" href="search">
151-
<h3>SEARCH / SCROLL / RECOMMEND</h3>
152-
<p>Semantic search, pagination, hybrid search, reranking, recommendations</p>
151+
<h3>SEARCH / SELECT / SCROLL / RECOMMEND</h3>
152+
<p>Semantic search, point retrieval, pagination, hybrid search, reranking, recommendations</p>
153153
</a>
154154
<a class="card" href="filters">
155155
<h3>WHERE Filters</h3>

docs/programmatic.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,13 @@ result = run_query(
6767
for hit in result.data:
6868
print(hit["score"], hit["payload"])
6969

70+
# Retrieve a point by ID
71+
result = run_query(
72+
"SELECT * FROM notes WHERE id = 1",
73+
url="http://localhost:6333",
74+
)
75+
print(result.data) # {"id": "1", "payload": {...}}
76+
7077
# Delete by filter
7178
result = run_query(
7279
"DELETE FROM notes WHERE year < 2023",
@@ -120,6 +127,7 @@ class ExecutionResult:
120127
| INSERT (dense) | `{"id": int \| "<uuid>", "collection": "<name>"}` |
121128
| INSERT (hybrid) | `{"id": int \| "<uuid>", "collection": "<name>"}` |
122129
| INSERT BULK | `None` (count in `result.message`) |
130+
| SELECT | `{"id": str, "payload": dict}` or `None` when not found |
123131
| SEARCH | `[{"id": str, "score": float, "payload": dict}, ...]` |
124132
| SCROLL | `{"points": [{"id": str, "payload": dict}, ...], "next_offset": str \| None}` |
125133
| RECOMMEND | `[{"id": str, "score": float, "payload": dict}, ...]` |

docs/reference.md

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -174,8 +174,9 @@ 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 / DROP / DELETE on a non-existent collection | Check name spelling or run `SHOW COLLECTIONS` |
178-
| `Unexpected token '...'; expected a QQL statement keyword` | Unrecognized statement | Check the query syntax; QQL does not support SQL SELECT |
177+
| `Collection '...' does not exist` | SEARCH / SCROLL / SELECT / DROP / DELETE on a non-existent collection | Check name spelling or run `SHOW COLLECTIONS` |
178+
| `Unexpected token '...'; expected a QQL statement keyword` | Unrecognized statement | Check the query syntax and supported statement list |
179+
| `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 |
179180
| `Unterminated string literal (at position N)` | A string is missing its closing quote | Close the string with a matching `'` or `"` |
180181
| `Unexpected character '@' (at position N)` | A character not part of QQL syntax | Remove or quote the offending character |
181182
| `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` |

docs/search.md

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
# SEARCH, SCROLL, RECOMMEND, Hybrid Search & Reranking
1+
# SEARCH, SELECT, SCROLL, RECOMMEND, Hybrid Search & Reranking
22

33
---
44

@@ -70,6 +70,28 @@ Results are displayed as a table with three columns:
7070

7171
---
7272

73+
## SELECT — retrieve a point by ID
74+
75+
Fetches a single point payload by exact point ID.
76+
77+
**Syntax:**
78+
```sql
79+
SELECT * FROM <collection_name> WHERE id = '<point_id>'
80+
SELECT * FROM <collection_name> WHERE id = <integer_id>
81+
```
82+
83+
**Examples:**
84+
```sql
85+
SELECT * FROM articles WHERE id = '3f2e1a4b-8c91-4d0e-b123-abc123def456'
86+
SELECT * FROM articles WHERE id = 42
87+
```
88+
89+
`SELECT` in this version is intentionally strict:
90+
- only `*` projection is supported
91+
- only `WHERE id = ...` is supported
92+
93+
---
94+
7395
## Query-Time Search Params (`EXACT`, `WITH`)
7496

7597
Use these when you want to debug retrieval quality or tune recall without changing collection-level settings.

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[project]
22
name = "qql-cli"
3-
version = "2.1.0"
3+
version = "2.2.0"
44
description = "QQL is a SQL-like query language and CLI for Qdrant vector database. Write INSERT, SEARCH, 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), WHERE clause filters, script execution, and collection dump/restore."
55
readme = "README.md"
66
license = { file = "LICENSE" }

src/qql/ast_nodes.py

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

182182

183+
@dataclass(frozen=True)
184+
class SelectStmt:
185+
collection: str
186+
point_id: str | int
187+
188+
183189
@dataclass(frozen=True)
184190
class ScrollStmt:
185191
collection: str
@@ -234,6 +240,7 @@ class DeleteStmt:
234240
| CreateIndexStmt
235241
| DropCollectionStmt
236242
| ShowCollectionsStmt
243+
| SelectStmt
237244
| ScrollStmt
238245
| SearchStmt
239246
| RecommendStmt

src/qql/cli.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,9 @@
5454
Optional: [yellow]WHERE[/yellow] <filter>
5555
Optional: [yellow]AFTER[/yellow] '<id>'|<int>
5656
57+
[yellow]SELECT * FROM[/yellow] <name> [yellow]WHERE id =[/yellow] '<id>'|<int>
58+
Retrieve a single point by its ID and return its payload.
59+
5760
[yellow]SEARCH[/yellow] <name> [yellow]SIMILAR TO[/yellow] '<text>' [yellow]LIMIT[/yellow] <n>
5861
Semantic search by vector similarity.
5962
Optional: [yellow]USING MODEL[/yellow] '<model>'
@@ -419,5 +422,14 @@ def _run_and_print(executor: Executor, query: str) -> None:
419422
console.print(f"[dim]next_offset: {result.data['next_offset']}[/dim]")
420423
return
421424

425+
# Pretty-print SELECT result
426+
if isinstance(result.data, dict) and "id" in result.data and "payload" in result.data:
427+
table = Table(show_header=True, header_style="bold cyan")
428+
table.add_column("ID")
429+
table.add_column("Payload")
430+
table.add_row(str(result.data["id"]), str(result.data["payload"]))
431+
console.print(table)
432+
return
433+
422434
# Fallback: print data as-is
423435
console.print(result.data)

src/qql/executor.py

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,7 @@
7676
QuantizationConfig,
7777
QuantizationType,
7878
RecommendStmt,
79+
SelectStmt,
7980
ScrollStmt,
8081
SearchStmt,
8182
SearchWith,
@@ -118,6 +119,8 @@ def execute(self, node: ASTNode) -> ExecutionResult:
118119
return self._execute_show(node)
119120
if isinstance(node, ScrollStmt):
120121
return self._execute_scroll(node)
122+
if isinstance(node, SelectStmt):
123+
return self._execute_select(node)
121124
if isinstance(node, SearchStmt):
122125
return self._execute_search(node)
123126
if isinstance(node, RecommendStmt):
@@ -447,6 +450,33 @@ def _execute_scroll(self, node: ScrollStmt) -> ExecutionResult:
447450
data={"points": points, "next_offset": None if next_offset is None else str(next_offset)},
448451
)
449452

453+
def _execute_select(self, node: SelectStmt) -> ExecutionResult:
454+
if not self._client.collection_exists(node.collection):
455+
raise QQLRuntimeError(f"Collection '{node.collection}' does not exist")
456+
457+
try:
458+
records = self._client.retrieve(
459+
collection_name=node.collection,
460+
ids=[node.point_id],
461+
with_payload=True,
462+
with_vectors=False,
463+
)
464+
except UnexpectedResponse as e:
465+
raise QQLRuntimeError(f"Qdrant error during SELECT: {e}") from e
466+
467+
if not records:
468+
return ExecutionResult(
469+
success=True,
470+
message=f"Point '{node.point_id}' not found in '{node.collection}'",
471+
)
472+
473+
record = records[0]
474+
return ExecutionResult(
475+
success=True,
476+
message=f"Retrieved point '{node.point_id}' from '{node.collection}'",
477+
data={"id": str(record.id), "payload": record.payload or {}},
478+
)
479+
450480
def _execute_search(self, node: SearchStmt) -> ExecutionResult:
451481
if not self._client.collection_exists(node.collection):
452482
raise QQLRuntimeError(f"Collection '{node.collection}' does not exist")

0 commit comments

Comments
 (0)