Skip to main content

A SQL-like query language CLI wrapper for Qdrant vector database

Project description

QQL — Qdrant Query Language

A SQL-like CLI for Qdrant, a high-performance vector database. Instead of writing Python SDK calls, you write natural query statements to insert, search, manage, and delete vector data.

qql> INSERT INTO COLLECTION notes VALUES {'text': 'Qdrant is a vector database', 'author': 'alice'}
✓ Inserted 1 point [3f2e1a4b-8c91-4d0e-b123-abc123def456]

qql> SEARCH notes SIMILAR TO 'vector storage engines' LIMIT 3
✓ Found 2 result(s)
 Score  │ ID                                   │ Payload
────────┼──────────────────────────────────────┼──────────────────────────────────────
 0.8931 │ 3f2e1a4b-8c91-4d0e-b123-abc123def456 │ {'text': 'Qdrant is a ...', 'author': 'alice'}

Table of Contents


How It Works

QQL is a thin translation layer between a SQL-like query language and the Qdrant Python client. Every statement you type goes through three stages:

Your query string
      │
      ▼
  [ Lexer ]      — tokenizes the input into keywords, identifiers, literals
      │
      ▼
  [ Parser ]     — builds a typed AST node (e.g. InsertStmt, SearchStmt)
      │
      ▼
  [ Executor ]   — maps the AST node to a Qdrant client call
      │
      ▼
  Qdrant instance

When you run INSERT, the text field in your dictionary is automatically converted into a dense vector using Fastembed. The vector and the rest of your fields (stored as payload) are then upserted into Qdrant together. You never have to manage vectors manually.


Installation

Requirements: Python 3.12+, a running Qdrant instance.

From PyPI

pip install qql-cli

From source (development)

git clone <repo>
cd qql
pip install -e .

Or with uv:

uv sync

After installation the qql command is available globally in your terminal.


Connecting to Qdrant

Before running any queries you must connect to a Qdrant instance. The connection config is saved to ~/.qql/config.json and reused automatically in future sessions.

Local Qdrant (no API key)

qql connect --url http://localhost:6333

Qdrant Cloud (with API key)

qql connect --url https://<your-cluster>.qdrant.io --secret <your-api-key>

On success you will see:

Connecting to http://localhost:6333...
Connected. Config saved to ~/.qql/config.json

QQL Interactive Shell  •  http://localhost:6333
Type help for available commands or exit to quit.

qql>

Starting Qdrant locally with Docker

If you do not have a Qdrant instance running yet:

docker run -p 6333:6333 qdrant/qdrant

Disconnecting

To remove the saved connection config:

qql disconnect

The QQL Shell

Once connected, running qql alone (no arguments) reads the saved config and opens the interactive shell:

qql

Inside the shell:

Input Effect
A QQL statement Executes it and prints the result
help or ? or \h Prints a reference of all available commands
exit or quit or \q or :q Exits the shell
Empty line / Enter Ignored
Ctrl-D or Ctrl-C Exits the shell

All keywords are case-insensitiveINSERT, insert, and Insert all work.


All QQL Operations

INSERT — add a point

Inserts a new document into a collection. The text field is mandatory — it is automatically vectorized and stored as the point's vector. All other fields become searchable payload (metadata).

If the collection does not exist yet, it is created automatically with the correct vector dimensions.

Syntax:

INSERT INTO COLLECTION <collection_name> VALUES {<dict>}
INSERT INTO COLLECTION <collection_name> VALUES {<dict>} USING MODEL '<model_name>'

Examples:

Minimal insert (text only):

INSERT INTO COLLECTION articles VALUES {'text': 'Qdrant supports cosine similarity search'}

Insert with metadata:

INSERT INTO COLLECTION articles VALUES {
  'text': 'Neural networks learn representations from data',
  'author': 'alice',
  'category': 'ml',
  'year': 2024,
  'published': true
}

Insert with a specific embedding model:

INSERT INTO COLLECTION articles VALUES {'text': 'hello world'} USING MODEL 'BAAI/bge-small-en-v1.5'

Insert with nested metadata:

INSERT INTO COLLECTION articles VALUES {
  'text': 'Attention is all you need',
  'meta': {'venue': 'NeurIPS', 'citations': 50000},
  'tags': ['transformers', 'attention', 'nlp']
}

What happens internally:

  1. The text value is embedded into a dense vector using the configured model.
  2. A UUID is auto-generated as the point ID.
  3. All fields (including text) are stored in the payload.
  4. The point is upserted into Qdrant.

Rules:

  • text is always required. Omitting it raises an error.
  • A point ID (UUID) is generated automatically — you do not provide one.
  • If the collection already exists with a different vector size (from a different model), an error is raised with a clear message.

SEARCH — find similar points

Performs a semantic similarity search: your query text is embedded with the same model used during insert, then Qdrant finds the nearest vectors by cosine distance.

Syntax:

SEARCH <collection_name> SIMILAR TO '<query_text>' LIMIT <n>
SEARCH <collection_name> SIMILAR TO '<query_text>' LIMIT <n> USING MODEL '<model_name>'

Examples:

Basic search, return top 5 results:

SEARCH articles SIMILAR TO 'machine learning algorithms' LIMIT 5

Search with a specific model:

SEARCH articles SIMILAR TO 'deep learning' LIMIT 10 USING MODEL 'BAAI/bge-small-en-v1.5'

Output:

Results are displayed as a table with three columns:

 Score  │ ID                                   │ Payload
────────┼──────────────────────────────────────┼──────────────────────────────────
 0.9241 │ 3f2e1a4b-...                          │ {'text': 'Neural networks...', 'author': 'alice'}
 0.8817 │ 7a1b2c3d-...                          │ {'text': 'Attention is all...', 'tags': [...]}
  • Score — cosine similarity score between 0 and 1. Higher is more similar.
  • ID — the UUID of the matching point.
  • Payload — all fields stored alongside the vector.

Important: Use the same model for SEARCH as you used for INSERT. Mixing models produces meaningless scores because the vectors live in different spaces.


SHOW COLLECTIONS — list collections

Lists all collections in the connected Qdrant instance.

Syntax:

SHOW COLLECTIONS

Example:

SHOW COLLECTIONS

Output:

✓ 3 collection(s) found
┌──────────────────┐
│ Collection       │
├──────────────────┤
│ articles         │
│ notes            │
│ products         │
└──────────────────┘

CREATE COLLECTION — create a collection

Explicitly creates a new empty collection. Collections are also created automatically on the first INSERT, so this command is optional — use it when you want to pre-create a collection before inserting data.

Syntax:

CREATE COLLECTION <collection_name>

Example:

CREATE COLLECTION research_papers

The collection is created using the default embedding model's dimensions (384 for all-MiniLM-L6-v2) with cosine distance.

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


DROP COLLECTION — delete a collection

Permanently deletes a collection and all points inside it. This operation is irreversible.

Syntax:

DROP COLLECTION <collection_name>

Example:

DROP COLLECTION old_experiments

Raises an error if the collection does not exist.


DELETE — remove a point

Deletes a single point from a collection by its ID. The point ID is the UUID returned by INSERT.

Syntax:

DELETE FROM <collection_name> WHERE id = '<point_id>'
DELETE FROM <collection_name> WHERE id = <integer_id>

Examples:

Delete by UUID string:

DELETE FROM articles WHERE id = '3f2e1a4b-8c91-4d0e-b123-abc123def456'

Delete by integer ID:

DELETE FROM articles WHERE id = 42

To find a point's ID, run a SEARCH first and copy the ID from the results table.


Embedding Models

QQL uses Fastembed to convert text into vectors locally — no external API call is needed.

Default model

sentence-transformers/all-MiniLM-L6-v2
  • Vector dimensions: 384
  • Size: ~90 MB (downloaded on first use, cached locally)
  • Good balance of speed and quality for English text

Specifying a different model

Add USING MODEL '<model_name>' to INSERT or SEARCH:

INSERT INTO docs VALUES {'text': 'hello'} USING MODEL 'BAAI/bge-small-en-v1.5'
SEARCH docs SIMILAR TO 'hello' LIMIT 5 USING MODEL 'BAAI/bge-small-en-v1.5'

Commonly available Fastembed models

Model Dimensions Notes
sentence-transformers/all-MiniLM-L6-v2 384 Default. Fast, good general quality
BAAI/bge-small-en-v1.5 384 Strong English retrieval
BAAI/bge-base-en-v1.5 768 Higher quality, larger size
BAAI/bge-large-en-v1.5 1024 Best quality, slowest
sentence-transformers/all-mpnet-base-v2 768 Strong semantic similarity

Models are downloaded automatically on first use and cached by Fastembed. Loading a new model for the first time takes a few seconds.

Model consistency rule

Every collection is created with a fixed vector size determined by the model used on first INSERT (or CREATE COLLECTION). If you try to INSERT into an existing collection using a different model that produces different dimensions, QQL will raise an error:

Error: Vector dimension mismatch: collection 'docs' expects 384 dims,
but model produces 768 dims. Specify a compatible model with USING MODEL '<model>'.

Value Types in Dictionaries

The VALUES dictionary (and nested dicts) supports these types:

Type Example Notes
String 'hello' or "hello" Single or double quotes
Integer 42, -7 Whole numbers, negative allowed
Float 3.14, -0.5 Decimal numbers
Boolean true, false Case-insensitive
Null null Case-insensitive
Nested dict {'key': 'val'} Arbitrary nesting
List ['a', 'b', 1] Mixed types allowed

Examples of each:

INSERT INTO demo VALUES {
  'text':    'example document',
  'count':   42,
  'score':   0.95,
  'active':  true,
  'deleted': false,
  'ref':     null,
  'meta':    {'source': 'web', 'lang': 'en'},
  'tags':    ['ai', 'nlp', 'search']
}

Trailing commas in dicts and lists are allowed:

INSERT INTO demo VALUES {'text': 'hi', 'x': 1,}

Configuration File

The connection config is stored at ~/.qql/config.json:

{
  "url": "http://localhost:6333",
  "secret": null,
  "default_model": "sentence-transformers/all-MiniLM-L6-v2"
}
Field Description
url Qdrant instance URL
secret API key (null if not required)
default_model Embedding model used when no USING MODEL clause is given

You can edit this file directly to change the default model without reconnecting:

{
  "url": "http://localhost:6333",
  "secret": null,
  "default_model": "BAAI/bge-small-en-v1.5"
}

Programmatic Usage

QQL can also be used as a Python library without the CLI:

from qql import run_query

# Single query
result = run_query(
    "INSERT INTO COLLECTION notes VALUES {'text': 'hello world', 'author': 'alice'}",
    url="http://localhost:6333",
)
print(result.message)   # "Inserted 1 point [<uuid>]"
print(result.data)      # {"id": "...", "collection": "notes"}

# Search
result = run_query(
    "SEARCH notes SIMILAR TO 'hello' LIMIT 5",
    url="http://localhost:6333",
)
for hit in result.data:
    print(hit["score"], hit["id"], hit["payload"])

Or use the pipeline directly for more control:

from qdrant_client import QdrantClient
from qql.lexer import Lexer
from qql.parser import Parser
from qql.executor import Executor
from qql.config import QQLConfig

client = QdrantClient(url="http://localhost:6333")
config = QQLConfig(url="http://localhost:6333")
executor = Executor(client, config)

query = "SHOW COLLECTIONS"
tokens = Lexer().tokenize(query)
node = Parser(tokens).parse()
result = executor.execute(node)

print(result.data)   # ["notes", "articles", ...]

ExecutionResult

All operations return an ExecutionResult:

@dataclass
class ExecutionResult:
    success: bool       # True if operation succeeded
    message: str        # Human-readable summary
    data: Any           # Operation-specific payload (see below)
Operation result.data type
INSERT {"id": "<uuid>", "collection": "<name>"}
SEARCH [{"id": str, "score": float, "payload": dict}, ...]
SHOW COLLECTIONS ["name1", "name2", ...]
CREATE COLLECTION None
DROP COLLECTION None
DELETE None

Project Structure

qql/
├── pyproject.toml          # Package config; installs the `qql` CLI command
├── src/
│   └── qql/
│       ├── __init__.py     # Public API: run_query()
│       ├── cli.py          # CLI entry point: connect, disconnect, REPL
│       ├── config.py       # QQLConfig dataclass + ~/.qql/config.json I/O
│       ├── exceptions.py   # QQLError, QQLSyntaxError, QQLRuntimeError
│       ├── lexer.py        # Tokenizer: string → List[Token]
│       ├── ast_nodes.py    # Frozen dataclasses for each statement type
│       ├── parser.py       # Recursive descent parser: tokens → AST node
│       ├── embedder.py     # Fastembed wrapper with per-model cache
│       └── executor.py     # AST node → Qdrant client call
└── tests/
    ├── test_lexer.py       # Tokenizer unit tests
    ├── test_parser.py      # Parser unit tests (all 6 statement types)
    └── test_executor.py    # Executor unit tests (mocked Qdrant client)

Running Tests

Tests do not require a running Qdrant instance — the Qdrant client is mocked.

pytest tests/ -v

Expected output: 54 tests passing.


Error Reference

Error Cause Fix
Not connected. Run: qql connect --url <url> No ~/.qql/config.json found Run qql connect --url <url> first
Connection failed: ... Qdrant unreachable at given URL Check that Qdrant is running and the URL is correct
INSERT requires a 'text' field in VALUES text key missing from the VALUES dict Add 'text': '...' to your dict
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
Collection '...' does not exist SEARCH / DROP / DELETE on a non-existent collection Check name spelling or run SHOW COLLECTIONS
Unexpected token '...'; expected a QQL statement keyword Unrecognized statement Check the query syntax; QQL does not support SQL SELECT
Unterminated string literal (at position N) A string is missing its closing quote Close the string with a matching ' or "
Unexpected character '@' (at position N) A character not part of QQL syntax Remove or quote the offending character

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

qql_cli-0.1.0.tar.gz (20.4 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

qql_cli-0.1.0-py3-none-any.whl (18.9 kB view details)

Uploaded Python 3

File details

Details for the file qql_cli-0.1.0.tar.gz.

File metadata

  • Download URL: qql_cli-0.1.0.tar.gz
  • Upload date:
  • Size: 20.4 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.2.0 CPython/3.12.13

File hashes

Hashes for qql_cli-0.1.0.tar.gz
Algorithm Hash digest
SHA256 8289d061354267c8ff36e0948c10c7e4143ec08b745c891818ae9177c336e9de
MD5 7f0384b0bd5cca4a383488ef67c221be
BLAKE2b-256 fa4ee2f24f6a15e4ece020d65af5f439e5a2e4722123e4138175510e24ef6c0b

See more details on using hashes here.

File details

Details for the file qql_cli-0.1.0-py3-none-any.whl.

File metadata

  • Download URL: qql_cli-0.1.0-py3-none-any.whl
  • Upload date:
  • Size: 18.9 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.2.0 CPython/3.12.13

File hashes

Hashes for qql_cli-0.1.0-py3-none-any.whl
Algorithm Hash digest
SHA256 56e6e21bf5eb1ae0c04b9a155baba8ed21780e74a9d9d365cd1d447311405530
MD5 d9cf58621dc7610d90342349c2781daa
BLAKE2b-256 5d5c0e5de51b365354e59a0cb56752699f250665cce9bcce465f49c5190d6b5a

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page