Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
QDRANT_API_KEY=
QDRANT_URL=
23 changes: 20 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@

A lean, open-source Qdrant collection management tool focused on inspection, deduplication, vector repair, sparse-content detection, and targeted cleanup tasks.

Supports both **local Qdrant** (Docker/self-hosted) and **Qdrant Cloud**.

## Installation

```bash
Expand All @@ -24,17 +26,27 @@ The public package surface is intentionally small:

## Configuration

The tool reads `QDRANT_URL` from the environment and defaults to `http://localhost:6333`.
The tool reads `QDRANT_URL` and `QDRANT_API_KEY` from the environment.

**Local Qdrant (Docker/self-hosted):**
```bash
export QDRANT_URL=http://localhost:6333
python -m qdrant_manager
python __main__.py
```

**Qdrant Cloud:**
```bash
export QDRANT_URL=https://<your-cluster-id>.<region>.cloud.qdrant.io
export QDRANT_API_KEY=<your-api-key>
python __main__.py
```
Comment on lines +29 to 42

Copilot AI Apr 10, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The README now instructs running python __main__.py, but the rest of the README still presents this as an importable package (qdrant_manager.* surface and from qdrant_manager import QdrantCleaner). These two modes require different import structures; as written, the docs are internally inconsistent and will likely confuse users. Please align the run instructions with the supported execution mode (e.g. prefer python -m qdrant_manager for package usage, or update the package-surface/programmatic examples if the project is moving to a script-only layout).

Copilot uses AI. Check for mistakes.

Note: Qdrant Cloud URLs should **not** include `:6333`. Use the full HTTPS URL from your Qdrant Cloud dashboard.

## Launching the Cleaner CLI

```bash
python -m qdrant_manager
python __main__.py
```

On startup you will be prompted for a Qdrant URL (pre-filled from `QDRANT_URL` or `http://localhost:6333`). The CLI checks connectivity immediately and shows a green **Connected** or red **Unreachable** status. If the instance is not reachable you can enter a different URL or continue anyway — the main menu stays up and reports errors per-action rather than crashing.
Expand All @@ -44,7 +56,12 @@ On startup you will be prompted for a Qdrant URL (pre-filled from `QDRANT_URL` o
```python
from qdrant_manager import QdrantCleaner

# Local Qdrant
cleaner = QdrantCleaner(host="localhost", port=6333)

# Qdrant Cloud (with API key)
cleaner = QdrantCleaner(host="https://<cluster-id>.<region>.cloud.qdrant.io", api_key="<your-api-key>")

cleaner.set_collection("my_collection")

summary = cleaner.analyze_collection(progress_callback=print)
Expand Down
4 changes: 2 additions & 2 deletions __init__.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
"""Public package surface for the standalone Qdrant cleaner."""
from .cleaner import QdrantCleaner
from .cleaner_cli import QdrantCleanerCLI
from cleaner import QdrantCleaner
from cleaner_cli import QdrantCleanerCLI
Comment on lines +2 to +3

Copilot AI Apr 10, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These imports changed to from cleaner ... / from cleaner_cli .... If this is a package (as implied by __init__.py and the README’s qdrant_manager.* examples), this will raise ModuleNotFoundError because cleaner/cleaner_cli aren’t top-level modules— they’re package modules. Use relative imports (e.g. from .cleaner import ...) or otherwise ensure package-qualified imports so import qdrant_manager works.

Suggested change
from cleaner import QdrantCleaner
from cleaner_cli import QdrantCleanerCLI
from .cleaner import QdrantCleaner
from .cleaner_cli import QdrantCleanerCLI

Copilot uses AI. Check for mistakes.

__all__ = ["QdrantCleaner", "QdrantCleanerCLI"]
2 changes: 1 addition & 1 deletion __main__.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

from __future__ import annotations

from .cleaner_cli import QdrantCleanerCLI
from cleaner_cli import QdrantCleanerCLI

Copilot AI Apr 10, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

from cleaner_cli import QdrantCleanerCLI will fail when running as a package entry point (e.g. python -m qdrant_manager), because it imports a top-level cleaner_cli module instead of qdrant_manager.cleaner_cli. Use a package-relative import (or a dual-mode fallback) so both module execution and script execution work reliably.

Suggested change
from cleaner_cli import QdrantCleanerCLI
try:
from .cleaner_cli import QdrantCleanerCLI
except ImportError:
from cleaner_cli import QdrantCleanerCLI

Copilot uses AI. Check for mistakes.


def main() -> None:
Expand Down
29 changes: 19 additions & 10 deletions cleaner.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,8 @@
from typing import Any, Dict, List, Optional, Tuple
from urllib.parse import urlparse

from .config import CANONICAL_TEXT_FIELDS
from .dependencies import (
from config import CANONICAL_TEXT_FIELDS, QDRANT_API_KEY
from dependencies import (
Comment on lines +13 to +14

Copilot AI Apr 10, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These top-level imports switched to bare-module imports (from config ..., from dependencies ...). If this directory is used as a package (README advertises qdrant_manager.* and there are still relative imports in this file, e.g. from .dependencies import qdrant_models later), import qdrant_manager / python -m qdrant_manager will fail because config/dependencies won’t resolve as top-level modules. Please standardize the import strategy (e.g., keep explicit relative imports within the package, or use a dual-mode try/except import pattern consistently across the file).

Copilot uses AI. Check for mistakes.
CuMLDBSCAN,
GPU_CLUSTERING_AVAILABLE,
IsolationForest,
Expand All @@ -31,16 +31,25 @@
class QdrantCleaner:
"""Collection cleaning utilities"""

def __init__(self, host="localhost", port=6333):
def __init__(self, host="localhost", port=6333, api_key=None):
if QdrantClient is None:
raise ImportError("qdrant-client required")

if host.startswith("http://") or host.startswith("https://"):
parsed = urlparse(host)
host = parsed.hostname
port = parsed.port or port

self.client = QdrantClient(host=host, port=int(port))

api_key = api_key or QDRANT_API_KEY

# Use 'url' parameter for HTTPS/cloud, 'host'+'port' for local HTTP
if host.startswith("https://"):
client_kwargs = {"url": host}
if api_key:
client_kwargs["api_key"] = api_key
elif host.startswith("http://"):
client_kwargs = {"url": host}
else:
client_kwargs = {"host": host, "port": int(port)}
if api_key:
client_kwargs["api_key"] = api_key

self.client = QdrantClient(**client_kwargs)
self.collection_name = None
self.has_optimize_method = hasattr(self.client, 'optimize_collection')

Expand Down
9 changes: 5 additions & 4 deletions cleaner_cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,8 @@
from typing import Any, Dict, List, Optional
from urllib.parse import urlparse

from .cleaner import QdrantCleaner
from .config import QDRANT_DIRECT
from cleaner import QdrantCleaner
from config import QDRANT_DIRECT, QDRANT_API_KEY
Comment on lines +8 to +9

Copilot AI Apr 10, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

from cleaner import ... / from config import ... will break when this code is executed as an installed package/module (the README still references qdrant_manager.cleaner_cli and from qdrant_manager import ...). Prefer package-relative imports (or a consistent dual-mode import fallback) so both python -m qdrant_manager and programmatic imports keep working.

Suggested change
from cleaner import QdrantCleaner
from config import QDRANT_DIRECT, QDRANT_API_KEY
try:
from .cleaner import QdrantCleaner
from .config import QDRANT_DIRECT, QDRANT_API_KEY
except ImportError:
from cleaner import QdrantCleaner
from config import QDRANT_DIRECT, QDRANT_API_KEY

Copilot uses AI. Check for mistakes.


_COLOUR = sys.stdout.isatty()
Expand Down Expand Up @@ -77,13 +77,14 @@ def __init__(self, *, qdrant_url: str = QDRANT_DIRECT) -> None:
self.collections: List[Dict[str, Any]] = []
self._set_qdrant_url(qdrant_url)

def _set_qdrant_url(self, qdrant_url: str) -> None:
def _set_qdrant_url(self, qdrant_url: str, api_key: str = None) -> None:
qdrant_url = (qdrant_url or "").strip() or QDRANT_DIRECT
parsed = urlparse(qdrant_url)
host = parsed.hostname or "localhost"
port = parsed.port or 6333
Comment on lines 83 to 84

Copilot AI Apr 10, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

_set_qdrant_url() parses the URL but then passes only parsed.hostname into QdrantCleaner. For https://...cloud.qdrant.io this strips the scheme, so QdrantCleaner won’t take the HTTPS/cloud path and will instead try host=<domain>, port=6333, which is incorrect for Qdrant Cloud. Pass the full URL (including scheme) through to QdrantCleaner, or preserve the scheme when building the endpoint so HTTPS + API key works.

Suggested change
host = parsed.hostname or "localhost"
port = parsed.port or 6333
if parsed.scheme and parsed.hostname:
host = qdrant_url
port = parsed.port or 6333
else:
host = parsed.hostname or "localhost"
port = parsed.port or 6333

Copilot uses AI. Check for mistakes.
api_key = api_key or QDRANT_API_KEY
self.qdrant_url = qdrant_url
self.cleaner = QdrantCleaner(host=host, port=port)
self.cleaner = QdrantCleaner(host=host, port=port, api_key=api_key)
self.collections = []

def _progress(self, message: str) -> None:
Expand Down
1 change: 1 addition & 0 deletions config.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
import os

QDRANT_URL = os.environ.get("QDRANT_URL", "http://localhost:6333")
QDRANT_API_KEY = os.environ.get("QDRANT_API_KEY")
QDRANT_DIRECT = QDRANT_URL
CANONICAL_TEXT_FIELDS = ["text", "content", "information"]
CANONICAL_ROOT_FIELDS: list[str] = []
Loading