Skip to content

Commit 9cf7b3b

Browse files
author
srimon12
committed
feat: enhance collection creation and update vector commands with explicit vector names
1 parent cf77e7d commit 9cf7b3b

6 files changed

Lines changed: 37 additions & 23 deletions

File tree

docs/scripts.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -120,7 +120,7 @@ Done. 41 point(s) written.
120120
-- configured model (see: qql connect).
121121
-- ============================================================
122122

123-
CREATE COLLECTION medical_records HYBRID
123+
CREATE COLLECTION medical_records USING HYBRID DENSE VECTOR 'dense' SPARSE VECTOR 'sparse'
124124

125125
-- Batch 1 / 1 (records 1–41)
126126
INSERT BULK INTO COLLECTION medical_records VALUES [
@@ -132,7 +132,7 @@ INSERT BULK INTO COLLECTION medical_records VALUES [
132132
'peer_reviewed': true
133133
},
134134
...
135-
] USING HYBRID
135+
] USING HYBRID DENSE VECTOR 'dense' SPARSE VECTOR 'sparse'
136136

137137
-- ============================================================
138138
-- End of dump

src/qql/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
__all__ = [
1616
"__version__",
1717
"Connection",
18+
"DEFAULT_MODEL",
1819
"QQLConfig",
1920
"QQLError",
2021
"QQLRuntimeError",

src/qql/cli.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -110,7 +110,8 @@
110110
[yellow]DELETE FROM[/yellow] <name> [yellow]WHERE id =[/yellow] '<id>'
111111
Delete a point by its ID.
112112
113-
[yellow]UPDATE[/yellow] <name> [yellow]SET VECTOR[/yellow] ['<dense_vector>'] [yellow]WHERE id =[/yellow] '<id>'|<int> [<vector>]
113+
[yellow]UPDATE[/yellow] <name> [yellow]SET VECTOR[/yellow] [yellow]WHERE id =[/yellow] '<id>'|<int> [<vector>]
114+
[yellow]UPDATE[/yellow] <name> [yellow]SET VECTOR[/yellow] '<dense_vector_name>' [yellow]WHERE id =[/yellow] '<id>'|<int> [<vector>]
114115
Replace the dense vector for a single point by ID.
115116
The point must already exist. Vector is a float array: [0.1, 0.2, ..., 0.N]
116117

src/qql/executor.py

Lines changed: 7 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -74,7 +74,6 @@
7474
AlterCollectionStmt,
7575
AndExpr,
7676
BetweenExpr,
77-
CollectionParamsConfig,
7877
CollectionConfig,
7978
CompareExpr,
8079
CreateCollectionStmt,
@@ -94,7 +93,6 @@
9493
MatchTextExpr,
9594
NotExpr,
9695
NotInExpr,
97-
OptimizersRuntimeConfig,
9896
OrExpr,
9997
QuantizationUpdate,
10098
QuantizationConfig,
@@ -108,8 +106,6 @@
108106
ShowCollectionsStmt,
109107
UpdateVectorStmt,
110108
UpdatePayloadStmt,
111-
VectorsConfig,
112-
HnswRuntimeConfig,
113109
)
114110
from .config import QQLConfig
115111
from .embedder import CrossEncoderEmbedder, Embedder, SparseEmbedder
@@ -384,10 +380,13 @@ def _execute_insert_bulk(self, node: InsertBulkStmt) -> ExecutionResult:
384380
dense_name = topology.dense_using(node.dense_vector) or dense_name
385381
sparse_name = topology.sparse_using(node.sparse_vector)
386382

383+
first_dense_vector: list[float] | None = None
387384
points: list[PointStruct] = []
388385
for vals in node.values_list:
389386
point_id, payload = self._extract_point_id_and_payload(vals)
390387
dense_vector = dense_embedder.embed(vals["text"])
388+
if first_dense_vector is None:
389+
first_dense_vector = dense_vector
391390
sparse_obj = sparse_embedder.embed(vals["text"])
392391
sparse_vector = SparseVector(
393392
indices=sparse_obj["indices"], values=sparse_obj["values"]
@@ -401,11 +400,11 @@ def _execute_insert_bulk(self, node: InsertBulkStmt) -> ExecutionResult:
401400
)
402401

403402
if not topology.exists:
404-
first_dense = dense_embedder.embed(node.values_list[0]["text"])
403+
assert first_dense_vector is not None
405404
self._create_collection_and_wait(
406405
collection_name=node.collection,
407406
vectors_config={
408-
dense_name: VectorParams(size=len(first_dense), distance=Distance.COSINE)
407+
dense_name: VectorParams(size=len(first_dense_vector), distance=Distance.COSINE)
409408
},
410409
sparse_vectors_config={
411410
sparse_name: SparseVectorParams(modifier=Modifier.IDF)
@@ -557,7 +556,7 @@ def _execute_alter_collection(self, node: AlterCollectionStmt) -> ExecutionResul
557556
topology = self._resolve_topology(node.collection)
558557

559558
update_kwargs: dict[str, Any] = {"collection_name": node.collection}
560-
vectors_config = self._build_vectors_config_diff(node.collection, node.config)
559+
vectors_config = self._build_vectors_config_diff(topology, node.config)
561560
hnsw_config = self._build_hnsw_config(node.config)
562561
optimizers_config = self._build_optimizers_config(node.config)
563562
collection_params = self._build_collection_params_diff(node.config)
@@ -1328,12 +1327,11 @@ def _build_collection_params_diff(
13281327

13291328
def _build_vectors_config_diff(
13301329
self,
1331-
collection_name: str,
1330+
topology: CollectionTopology,
13321331
config: CollectionConfig | None,
13331332
) -> dict[str, VectorParamsDiff] | None:
13341333
if config is None or config.vectors is None:
13351334
return None
1336-
topology = self._resolve_topology(collection_name)
13371335
vector_name = topology.dense_config_key()
13381336
return {
13391337
vector_name: VectorParamsDiff(on_disk=config.vectors.on_disk),

tests/test_executor.py

Lines changed: 25 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1936,6 +1936,28 @@ def test_dense_search_explicit_vector_resolves_ambiguous_collection(
19361936
executor.execute(node)
19371937
assert mock_client.query_points.call_args.kwargs["using"] == "body"
19381938

1939+
def test_dense_search_explicit_vector_unknown_name_raises(
1940+
self, executor, mock_client
1941+
):
1942+
from qdrant_client.models import Distance, VectorParams
1943+
1944+
mock_client.collection_exists.return_value = True
1945+
mock_client.get_collection.return_value.config.params.vectors = {
1946+
"title": VectorParams(size=384, distance=Distance.COSINE),
1947+
"body": VectorParams(size=384, distance=Distance.COSINE),
1948+
}
1949+
mock_client.get_collection.return_value.config.params.sparse_vectors = None
1950+
1951+
node = SearchStmt(
1952+
collection="col",
1953+
query_text="q",
1954+
limit=5,
1955+
model=None,
1956+
dense_vector="missing_name",
1957+
)
1958+
with pytest.raises(QQLRuntimeError, match="no dense vector named"):
1959+
executor.execute(node)
1960+
19391961
def test_hybrid_search_forwards_search_params_to_prefetch(
19401962
self, executor, mock_client, mock_sparse_embedder, mocker
19411963
):
@@ -2922,11 +2944,12 @@ class TestUpdateVectorVectorShape:
29222944
"""Gaps 12 & 13 — verify exact vector shape sent to Qdrant for named/unnamed collections."""
29232945

29242946
def test_update_vector_unnamed_collection_sends_plain_list(self, executor, mock_client):
2947+
from qdrant_client.models import Distance, VectorParams
29252948
from qql.ast_nodes import UpdateVectorStmt
2949+
29262950
mock_client.collection_exists.return_value = True
2927-
# Unnamed collection: get_collection returns non-dict vectors
29282951
info = mock_client.get_collection.return_value
2929-
info.config.params.vectors = [None] # list → not a dict → unnamed
2952+
info.config.params.vectors = VectorParams(size=3, distance=Distance.COSINE)
29302953

29312954
node = UpdateVectorStmt(collection="articles", point_id=1, vector=(0.1, 0.2, 0.3))
29322955
executor.execute(node)

tests/test_parser.py

Lines changed: 0 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,6 @@
2727
OrExpr,
2828
QuantizationUpdate,
2929
QuantizationType,
30-
QuantizationSearchWith,
3130
RecommendStmt,
3231
SelectStmt,
3332
ScrollStmt,
@@ -1122,14 +1121,6 @@ def test_with_trailing_comma(self):
11221121
node = parse("SEARCH col SIMILAR TO 'q' LIMIT 5 WITH { hnsw_ef: 256, }")
11231122
assert node.with_clause.hnsw_ef == 256
11241123

1125-
def test_with_quantization_unknown_key_raises(self):
1126-
with pytest.raises(QQLSyntaxError):
1127-
parse(
1128-
"SEARCH col SIMILAR TO 'q' LIMIT 5 "
1129-
"WITH { quantization: { unknown: true } }"
1130-
)
1131-
1132-
11331124
class TestSparseOnlySearch:
11341125
def test_using_sparse_sets_flag(self):
11351126
node = parse("SEARCH col SIMILAR TO 'q' LIMIT 5 USING SPARSE")

0 commit comments

Comments
 (0)