Skip to content

Commit 2f87a8b

Browse files
committed
fixed couple of issue for using the model while creating collection and sparse search
1 parent 97f7f1b commit 2f87a8b

7 files changed

Lines changed: 387 additions & 7 deletions

File tree

README.md

Lines changed: 32 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -252,6 +252,7 @@ SEARCH <collection_name> SIMILAR TO '<query_text>' LIMIT <n> USING MODEL '<model
252252
SEARCH <collection_name> SIMILAR TO '<query_text>' LIMIT <n> [USING MODEL '<model>'] WHERE <filter>
253253
SEARCH <collection_name> SIMILAR TO '<query_text>' LIMIT <n> USING HYBRID
254254
SEARCH <collection_name> SIMILAR TO '<query_text>' LIMIT <n> USING HYBRID [DENSE MODEL '<model>'] [SPARSE MODEL '<model>'] [WHERE <filter>]
255+
SEARCH <collection_name> SIMILAR TO '<query_text>' LIMIT <n> USING SPARSE [MODEL '<sparse_model>']
255256
SEARCH <collection_name> SIMILAR TO '<query_text>' LIMIT <n> EXACT
256257
SEARCH <collection_name> SIMILAR TO '<query_text>' LIMIT <n> [USING ...] [WHERE <filter>] [RERANK] WITH { hnsw_ef: <n>, exact: true|false, acorn: true|false }
257258
SEARCH <collection_name> SIMILAR TO '<query_text>' LIMIT <n> [USING ...] [WHERE <filter>] RERANK [MODEL '<reranker_model>']
@@ -284,6 +285,16 @@ Hybrid search with a WHERE filter:
284285
SEARCH articles SIMILAR TO 'transformers' LIMIT 10 USING HYBRID WHERE year >= 2020
285286
```
286287

288+
Sparse-only search (queries only the `sparse` named vector — useful for pure keyword retrieval):
289+
```sql
290+
SEARCH medical_knowledge SIMILAR TO 'beta blocker contraindications' LIMIT 5 USING SPARSE
291+
```
292+
293+
Sparse-only with a custom SPLADE model:
294+
```sql
295+
SEARCH medical_knowledge SIMILAR TO 'beta blocker contraindications' LIMIT 5 USING SPARSE MODEL 'prithivida/Splade_PP_en_v1'
296+
```
297+
287298
Exact search for recall debugging:
288299
```sql
289300
SEARCH articles SIMILAR TO 'attention mechanism' LIMIT 10 EXACT
@@ -498,7 +509,12 @@ Hybrid search combines **dense semantic vectors** and **sparse BM25 keyword vect
498509
A hybrid collection stores both a named dense vector (`"dense"`) and a named sparse vector (`"sparse"`):
499510

500511
```sql
512+
-- Shorthand (backward compatible)
501513
CREATE COLLECTION articles HYBRID
514+
515+
-- USING form — allows specifying a dense model
516+
CREATE COLLECTION articles USING HYBRID
517+
CREATE COLLECTION articles USING HYBRID DENSE MODEL 'BAAI/bge-base-en-v1.5'
502518
```
503519

504520
This is equivalent to calling Qdrant with:
@@ -695,21 +711,34 @@ Explicitly creates a new empty collection. Collections are also created automati
695711
```
696712
CREATE COLLECTION <collection_name>
697713
CREATE COLLECTION <collection_name> HYBRID
714+
CREATE COLLECTION <collection_name> USING MODEL '<model_name>'
715+
CREATE COLLECTION <collection_name> USING HYBRID
716+
CREATE COLLECTION <collection_name> USING HYBRID DENSE MODEL '<model>'
698717
```
699718

700719
**Examples:**
701720

702-
Dense-only collection (standard):
721+
Dense-only collection (standard, uses default model dimensions):
703722
```sql
704723
CREATE COLLECTION research_papers
705724
```
706725

707-
Hybrid collection (dense + sparse BM25):
726+
Dense-only collection pinned to a specific model (768-dimensional):
727+
```sql
728+
CREATE COLLECTION research_papers USING MODEL 'BAAI/bge-base-en-v1.5'
729+
```
730+
731+
Hybrid collection (dense + sparse BM25, default models):
708732
```sql
709733
CREATE COLLECTION research_papers HYBRID
710734
```
711735

712-
The collection is created using the **default embedding model's dimensions** (384 for `all-MiniLM-L6-v2`) with **cosine distance**.
736+
Hybrid collection with a custom dense model:
737+
```sql
738+
CREATE COLLECTION research_papers USING HYBRID DENSE MODEL 'BAAI/bge-base-en-v1.5'
739+
```
740+
741+
When `USING MODEL` is omitted, the collection uses the **default embedding model's dimensions** (384 for `all-MiniLM-L6-v2`). Specify `USING MODEL` to pin the collection to a specific model's output size — this must match the model you use in INSERT and SEARCH.
713742

714743
If the collection already exists, the command succeeds with a message and does nothing.
715744

src/qql/ast_nodes.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -133,6 +133,7 @@ class InsertStmt:
133133
class CreateCollectionStmt:
134134
collection: str
135135
hybrid: bool = False # if True, create with dense + sparse named vectors
136+
model: str | None = None # dense model; None → use config default
136137

137138

138139
@dataclass(frozen=True)
@@ -152,7 +153,8 @@ class SearchStmt:
152153
limit: int
153154
model: str | None # dense model; None → use config default
154155
hybrid: bool = False # if True, use prefetch+RRF hybrid search
155-
sparse_model: str | None = None # sparse model for hybrid; None → SparseEmbedder.DEFAULT_MODEL
156+
sparse_only: bool = False # if True, query only the sparse vector (no dense)
157+
sparse_model: str | None = None # sparse model for hybrid/sparse-only; None → SparseEmbedder.DEFAULT_MODEL
156158
query_filter: FilterExpr | None = None # optional WHERE clause; default keeps existing tests valid
157159
rerank: bool = False # if True, apply cross-encoder reranking post-Qdrant
158160
rerank_model: str | None = None # cross-encoder model; None → CrossEncoderEmbedder.DEFAULT_MODEL

src/qql/cli.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,8 @@
3030
3131
[yellow]CREATE COLLECTION[/yellow] <name> [[yellow]HYBRID[/yellow]]
3232
Create a new collection. Add HYBRID for dense+sparse BM25 vectors.
33+
Optional: [yellow]USING MODEL[/yellow] '<model>'
34+
Optional: [yellow]USING HYBRID[/yellow] [DENSE MODEL '<model>']
3335
3436
[yellow]DROP COLLECTION[/yellow] <name>
3537
Delete a collection and all its points.
@@ -41,6 +43,7 @@
4143
Semantic search by vector similarity.
4244
Optional: [yellow]USING MODEL[/yellow] '<model>'
4345
Optional: [yellow]USING HYBRID[/yellow] [DENSE MODEL '<model>'] [SPARSE MODEL '<model>']
46+
Optional: [yellow]USING SPARSE[/yellow] [MODEL '<model>'] sparse-vector-only search
4447
Optional: [yellow]WHERE[/yellow] <filter> (e.g. WHERE year > 2020 AND status = 'ok')
4548
Optional: [yellow]RERANK[/yellow] [MODEL '<model>'] rerank results with a cross-encoder
4649
Optional: [yellow]EXACT[/yellow] bypass HNSW and perform exact search

src/qql/executor.py

Lines changed: 44 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -177,9 +177,11 @@ def _execute_create(self, node: CreateCollectionStmt) -> ExecutionResult:
177177
message=f"Collection '{node.collection}' already exists",
178178
)
179179

180+
dense_model_name = node.model or self._config.default_model
181+
180182
# ── Hybrid collection: named dense + sparse vectors ────────────────
181183
if node.hybrid:
182-
embedder = Embedder(self._config.default_model)
184+
embedder = Embedder(dense_model_name)
183185
dims = embedder.dimensions
184186
self._client.create_collection(
185187
collection_name=node.collection,
@@ -199,7 +201,7 @@ def _execute_create(self, node: CreateCollectionStmt) -> ExecutionResult:
199201
)
200202

201203
# ── Standard dense-only collection ─────────────────────────────────
202-
embedder = Embedder(self._config.default_model)
204+
embedder = Embedder(dense_model_name)
203205
dims = embedder.dimensions
204206
self._client.create_collection(
205207
collection_name=node.collection,
@@ -302,6 +304,46 @@ def _execute_search(self, node: SearchStmt) -> ExecutionResult:
302304
data=results,
303305
)
304306

307+
# ── Sparse-only SEARCH: query the "sparse" named vector directly ─────
308+
if node.sparse_only:
309+
sparse_model_name = node.sparse_model or SparseEmbedder.DEFAULT_MODEL
310+
sparse_embedder = SparseEmbedder(sparse_model_name)
311+
sparse_obj = sparse_embedder.query_embed(node.query_text)
312+
sparse_vector = SparseVector(
313+
indices=sparse_obj["indices"],
314+
values=sparse_obj["values"],
315+
)
316+
317+
try:
318+
response = self._client.query_points(
319+
collection_name=node.collection,
320+
query=sparse_vector,
321+
using="sparse",
322+
limit=fetch_limit,
323+
query_filter=qdrant_filter,
324+
)
325+
except UnexpectedResponse as e:
326+
raise QQLRuntimeError(f"Qdrant error during SEARCH: {e}") from e
327+
328+
results = [
329+
{"id": str(h.id), "score": round(h.score, 4), "payload": h.payload}
330+
for h in response.points
331+
]
332+
333+
if node.rerank:
334+
results = self._apply_reranking(node.query_text, results, node.limit, node.rerank_model)
335+
return ExecutionResult(
336+
success=True,
337+
message=f"Found {len(results)} result(s) (sparse, reranked)",
338+
data=results,
339+
)
340+
341+
return ExecutionResult(
342+
success=True,
343+
message=f"Found {len(results)} result(s) (sparse)",
344+
data=results,
345+
)
346+
305347
# ── Standard dense-only SEARCH ─────────────────────────────────────
306348
model_name = node.model or self._config.default_model
307349
embedder = Embedder(model_name)

src/qql/parser.py

Lines changed: 30 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -107,10 +107,31 @@ def _parse_create(self) -> CreateCollectionStmt:
107107
self._expect(TokenKind.COLLECTION)
108108
collection = self._parse_identifier()
109109
hybrid: bool = False
110+
model: str | None = None
111+
110112
if self._peek().kind == TokenKind.HYBRID:
113+
# Bare HYBRID shorthand — backward compat
111114
self._advance()
112115
hybrid = True
113-
return CreateCollectionStmt(collection=collection, hybrid=hybrid)
116+
elif self._peek().kind == TokenKind.USING:
117+
self._advance() # consume USING
118+
if self._peek().kind == TokenKind.HYBRID:
119+
self._advance() # consume HYBRID
120+
hybrid = True
121+
# Optional DENSE MODEL sub-clause
122+
if self._peek().kind == TokenKind.DENSE:
123+
self._advance() # consume DENSE
124+
self._expect(TokenKind.MODEL)
125+
model = self._expect(TokenKind.STRING).value
126+
else:
127+
self._expect(TokenKind.MODEL)
128+
model = self._expect(TokenKind.STRING).value
129+
130+
return CreateCollectionStmt(
131+
collection=collection,
132+
hybrid=hybrid,
133+
model=model,
134+
)
114135

115136
def _parse_drop(self) -> DropCollectionStmt:
116137
self._expect(TokenKind.DROP)
@@ -139,6 +160,7 @@ def _parse_search(self) -> SearchStmt:
139160

140161
model: str | None = None
141162
hybrid: bool = False
163+
sparse_only: bool = False
142164
sparse_model: str | None = None
143165
if self._peek().kind == TokenKind.USING:
144166
self._advance() # consume USING
@@ -154,6 +176,12 @@ def _parse_search(self) -> SearchStmt:
154176
model = m
155177
else:
156178
sparse_model = m
179+
elif self._peek().kind == TokenKind.SPARSE:
180+
self._advance() # consume SPARSE
181+
sparse_only = True
182+
if self._peek().kind == TokenKind.MODEL:
183+
self._advance() # consume MODEL
184+
sparse_model = self._expect(TokenKind.STRING).value
157185
else:
158186
self._expect(TokenKind.MODEL)
159187
model = self._expect(TokenKind.STRING).value
@@ -196,6 +224,7 @@ def _parse_search(self) -> SearchStmt:
196224
limit=limit,
197225
model=model,
198226
hybrid=hybrid,
227+
sparse_only=sparse_only,
199228
sparse_model=sparse_model,
200229
query_filter=query_filter,
201230
rerank=rerank,

0 commit comments

Comments
 (0)