mirror of
https://github.com/qdrant/qdrant.git
synced 2026-07-26 04:31:02 -05:00
* refactor(edge): split EdgeShard load into new() and load() - new(path, config): create edge shard only when path has no existing segments - load(path, config?): load from existing files; config optional (load from edge_config.json or infer from segments) - Helpers: has_existing_segments, ensure_dirs_and_open_wal, resolve_initial_config, load_segments, ensure_appendable_segment - Python: EdgeShard.create_new(path, config); __init__ still calls load() - Examples use new() for creation and load(path, None) for reopen - Error instead of panic on config mismatch; explicit load/create in Python - fix: use OptimizerThresholds.deferred_internal_id for dev compatibility Made-with: Cursor * rollback threshold changes * fix(edge): address dancixx review comments - Persist inferred config in load() when edge_config.json does not exist (SaveOnDisk::new only saves when file exists) - Python: add #[new] delegating to load() for backward compatibility - qdrant_edge.pyi: Optional return types for deleted_threshold, vacuum_min_vector_number, default_segment_number; add __init__ doc Made-with: Cursor * Revert "fix(edge): address dancixx review comments" This reverts commit 370e21a6c74c41210e1658f89814395cb86ed81c. * fix: optional returns * fix: save config it is not exist * fix: save data on disk always * fix: python examples * chore: remove edge.close() * fix: wal lock * fix: rename of EdgeShardConfig -> EdgeConfig --------- Co-authored-by: Cursor Agent <agent@cursor.com> Co-authored-by: Andrey Vasnetsov <andrey@vasnetsov.com> Co-authored-by: Daniel Boros <dancixx@gmail.com>
236 lines
7.2 KiB
Python
236 lines
7.2 KiB
Python
#!/usr/bin/env python3
|
|
"""
|
|
Test edge optimize by preparing data in a running local Qdrant, downloading a
|
|
shard snapshot, and optimizing the restored edge shard.
|
|
|
|
Requires a running Qdrant on localhost:6333. The example creates and drops its
|
|
own temporary collection, including all points needed for the demo.
|
|
"""
|
|
|
|
import os
|
|
import shutil
|
|
|
|
import requests
|
|
from common import DATA_DIR
|
|
|
|
from qdrant_edge import *
|
|
|
|
COLLECTION = "edge_optimize_example"
|
|
QDRANT_URL = "http://localhost:6333"
|
|
SNAPSHOT_DIR = os.path.join(DATA_DIR, "snapshots")
|
|
SNAPSHOT_PATH = os.path.join(SNAPSHOT_DIR, "optimize.snapshot")
|
|
SHARD_DIR = os.path.join(DATA_DIR, "optimize_test_shard")
|
|
VECTOR_SIZE = 384
|
|
POINT_COUNT = 1000
|
|
DELETED_COUNT = 250
|
|
BATCH_SIZE = 100
|
|
SEARCH_POINT_ID = POINT_COUNT - 1
|
|
|
|
|
|
def request_json(method: str, path: str, **kwargs):
|
|
response = requests.request(method, f"{QDRANT_URL}{path}", timeout=120, **kwargs)
|
|
response.raise_for_status()
|
|
if response.content:
|
|
return response.json()
|
|
return None
|
|
|
|
|
|
def ensure_qdrant_available():
|
|
try:
|
|
request_json("GET", "/collections")
|
|
except requests.RequestException as err:
|
|
raise SystemExit(
|
|
"Qdrant is not reachable at http://localhost:6333.\n"
|
|
"Start one locally, for example with:\n"
|
|
"docker run -d --name qdrant-local -p 6333:6333 -p 6334:6334 qdrant/qdrant:latest"
|
|
) from err
|
|
|
|
|
|
def drop_collection():
|
|
response = requests.delete(
|
|
f"{QDRANT_URL}/collections/{COLLECTION}?wait=true",
|
|
timeout=120,
|
|
)
|
|
if response.status_code not in (200, 202, 404):
|
|
response.raise_for_status()
|
|
|
|
|
|
def build_vector(point_id: int) -> list[float]:
|
|
return [
|
|
((point_id * 17 + dimension * 13) % 1000) / 1000.0
|
|
for dimension in range(VECTOR_SIZE)
|
|
]
|
|
|
|
|
|
def build_point(point_id: int) -> dict:
|
|
return {
|
|
"id": point_id,
|
|
"vector": build_vector(point_id),
|
|
"payload": {
|
|
"group": f"group-{point_id % 5}",
|
|
"bucket": point_id % 10,
|
|
"city": f"city-{point_id % 17}",
|
|
},
|
|
}
|
|
|
|
|
|
def prepare_collection():
|
|
print(f"Preparing collection '{COLLECTION}' with {POINT_COUNT} points...")
|
|
drop_collection()
|
|
|
|
request_json(
|
|
"PUT",
|
|
f"/collections/{COLLECTION}",
|
|
json={
|
|
"vectors": {
|
|
"size": VECTOR_SIZE,
|
|
"distance": "Dot",
|
|
}
|
|
},
|
|
)
|
|
|
|
for start in range(1, POINT_COUNT + 1, BATCH_SIZE):
|
|
end = min(start + BATCH_SIZE - 1, POINT_COUNT)
|
|
print(f" Uploading points {start}..{end}")
|
|
request_json(
|
|
"PUT",
|
|
f"/collections/{COLLECTION}/points?wait=true",
|
|
json={
|
|
"points": [build_point(point_id) for point_id in range(start, end + 1)]
|
|
},
|
|
)
|
|
|
|
remote_count = request_json(
|
|
"POST",
|
|
f"/collections/{COLLECTION}/points/count",
|
|
json={"exact": True},
|
|
)["result"]["count"]
|
|
assert remote_count == POINT_COUNT, (
|
|
f"Unexpected point count in Qdrant: expected {POINT_COUNT}, got {remote_count}"
|
|
)
|
|
print(f"Collection ready, exact count: {remote_count}")
|
|
|
|
|
|
def create_shard_snapshot() -> str:
|
|
prepare_collection()
|
|
|
|
print(f"Creating shard snapshot for collection '{COLLECTION}' shard 0...")
|
|
response = request_json("POST", f"/collections/{COLLECTION}/shards/0/snapshots")
|
|
snapshot_name = response["result"]["name"]
|
|
print(f"Snapshot created: {snapshot_name}")
|
|
|
|
os.makedirs(SNAPSHOT_DIR, exist_ok=True)
|
|
if os.path.exists(SNAPSHOT_PATH):
|
|
os.remove(SNAPSHOT_PATH)
|
|
|
|
print("Downloading snapshot...")
|
|
download = requests.get(
|
|
f"{QDRANT_URL}/collections/{COLLECTION}/shards/0/snapshots/{snapshot_name}",
|
|
stream=True,
|
|
timeout=120,
|
|
)
|
|
download.raise_for_status()
|
|
|
|
with open(SNAPSHOT_PATH, "wb") as file_handle:
|
|
for chunk in download.iter_content(chunk_size=8192):
|
|
file_handle.write(chunk)
|
|
|
|
print(f"Downloaded to {SNAPSHOT_PATH}")
|
|
return SNAPSHOT_PATH
|
|
|
|
|
|
def load_edge_shard() -> EdgeShard:
|
|
return EdgeShard.load(
|
|
SHARD_DIR,
|
|
EdgeConfig(
|
|
vectors=EdgeVectorParams(size=VECTOR_SIZE, distance=Distance.Dot),
|
|
optimizers=EdgeOptimizersConfig(
|
|
deleted_threshold=0.2,
|
|
vacuum_min_vector_number=100,
|
|
default_segment_number=2,
|
|
),
|
|
),
|
|
)
|
|
|
|
|
|
def main():
|
|
ensure_qdrant_available()
|
|
|
|
try:
|
|
snapshot_path = create_shard_snapshot()
|
|
|
|
if os.path.exists(SHARD_DIR):
|
|
shutil.rmtree(SHARD_DIR)
|
|
|
|
print(f"\nUnpacking snapshot to {SHARD_DIR}...")
|
|
EdgeShard.unpack_snapshot(snapshot_path, SHARD_DIR)
|
|
|
|
print("Loading edge shard...")
|
|
shard = load_edge_shard()
|
|
|
|
initial_count = shard.count(CountRequest(exact=True))
|
|
print(f"\nInitial exact point count: {initial_count}")
|
|
assert initial_count == POINT_COUNT, (
|
|
f"Unexpected initial point count: expected {POINT_COUNT}, got {initial_count}"
|
|
)
|
|
|
|
deleted_ids = list(range(4, POINT_COUNT + 1, 4))
|
|
print(f"Deleting {len(deleted_ids)} points to create a vacuum candidate...")
|
|
shard.update(UpdateOperation.delete_points(deleted_ids))
|
|
|
|
info_before = shard.info()
|
|
count_before = shard.count(CountRequest(exact=True))
|
|
expected_count = POINT_COUNT - DELETED_COUNT
|
|
print("\nBefore optimize:")
|
|
print(f" {info_before}")
|
|
print(f" Exact point count: {count_before}")
|
|
assert count_before == expected_count, (
|
|
f"Unexpected count before optimize: expected {expected_count}, got {count_before}"
|
|
)
|
|
|
|
print("\nRunning optimize...")
|
|
optimized = shard.optimize()
|
|
print(f" Optimized: {optimized}")
|
|
assert optimized, "Expected optimize() to rebuild the vacuum candidate segment"
|
|
|
|
info_after = shard.info()
|
|
count_after = shard.count(CountRequest(exact=True))
|
|
print("\nAfter optimize:")
|
|
print(f" {info_after}")
|
|
print(f" Exact point count: {count_after}")
|
|
|
|
assert count_after == count_before, (
|
|
f"Point count changed after optimize: {count_before} -> {count_after}"
|
|
)
|
|
print(f"\nPoint count preserved after optimize: {count_after}")
|
|
|
|
print("\nTesting search after optimize...")
|
|
results = shard.search(
|
|
SearchRequest(
|
|
query=Query.Nearest(build_vector(SEARCH_POINT_ID)),
|
|
limit=5,
|
|
with_payload=True,
|
|
)
|
|
)
|
|
print(f" Search returned {len(results)} results")
|
|
for point in results:
|
|
print(f" {point}")
|
|
|
|
assert any(point.id == SEARCH_POINT_ID for point in results), (
|
|
f"Expected point {SEARCH_POINT_ID} in the search results"
|
|
)
|
|
|
|
print("\nRunning optimize again (should be no-op)...")
|
|
optimized_again = shard.optimize()
|
|
print(f" Optimized: {optimized_again}")
|
|
assert not optimized_again, "Second optimize() run should be a no-op"
|
|
|
|
print("\nAll checks passed!")
|
|
finally:
|
|
print(f"\nDropping temporary collection '{COLLECTION}'...")
|
|
drop_collection()
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|