File size: 8,651 Bytes
ede84b4 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 | """
qdrant_alias.py — ตั้ง/ดู alias ของ collection Modutech (28 ต.ค.)
บอทเรียก collection ข้อความของ Modutech ผ่าน alias modutech_live (brands/modutech/brand.yaml)
ตั้ง alias ให้ชี้ collection จริง แล้วสลับเวอร์ชัน / ย้อนกลับได้ด้วยคำสั่งเดียว ไม่ต้องแก้โค้ด
& $py qdrant_alias.py show
& $py qdrant_alias.py set --to modutech_belt_chunks # ครั้งแรก: ชี้ collection เดิม (ไม่คัดลอก ไม่สร้างใหม่)
& $py qdrant_alias.py set --to modutech_belt_chunks_v31 # วันหน้า: สลับไปเวอร์ชันใหม่ที่ build แยกไว้แล้ว
& $py qdrant_alias.py set --to modutech_belt_chunks # ย้อนกลับ
กติกา
· ปิดบอทก่อนรัน (Qdrant แบบไฟล์ล็อกโฟลเดอร์)
· แตะเฉพาะ alias — ไม่สร้าง ไม่ลบ ไม่เขียนจุดใน collection ไหนเลย
· ห้ามใช้กับ ./qdrant_db ของ Movex และห้ามชื่อที่ขึ้นต้นด้วย movex (สคริปต์ปฏิเสธเอง)
· สำรอง meta.json เป็น meta.json.bak-<เวลา> ก่อนแก้ทุกครั้ง แล้วเปิดใหม่ตรวจผลหลังแก้
· collection ปลายทางต้องมีจุด และชื่อ/ขนาดเวกเตอร์ต้องตรงกับตัวที่ alias ชี้อยู่ (ไม่ตรง = ไม่สลับ)
หลังตั้งเสร็จ meta.json ในโฟลเดอร์เปลี่ยน (ไฟล์อยู่ใน Git LFS) → commit แล้วอัปโหลดโฟลเดอร์ขึ้น Space
"""
from __future__ import annotations
import argparse
import os
import shutil
import sys
import time
BASE = os.path.dirname(os.path.abspath(__file__))
DEFAULT_PATH = os.path.join(BASE, "qdrant_modutech_db")
DEFAULT_ALIAS = "modutech_live"
def _refuse_movex(path: str, *names: str) -> str | None:
real = os.path.realpath(path)
if os.path.basename(real.rstrip("\\/")) == "qdrant_db" or real == os.path.realpath(os.path.join(BASE, "qdrant_db")):
return "นี่คือฐานข้อมูลของ Movex — ห้ามแตะ (กฎโปรเจกต์)"
for n in names:
if n and n.lower().startswith("movex"):
return f"ชื่อ {n} เป็นของ Movex — ห้ามแตะ"
return None
def _vectors(c, name: str) -> dict:
"""ชื่อเวกเตอร์ → (ขนาด, distance) เทียบโครงสร้างก่อนสลับ"""
vc = c.get_collection(name).config.params.vectors
if isinstance(vc, dict):
return {k: (v.size, str(v.distance)) for k, v in vc.items()}
return {"": (vc.size, str(vc.distance))}
def _open(path: str):
from qdrant_client import QdrantClient
if not os.path.isfile(os.path.join(path, "meta.json")):
raise SystemExit(f"❌ ไม่พบ {os.path.join(path, 'meta.json')}")
try:
return QdrantClient(path=path)
except Exception as e: # noqa: BLE001 — ส่วนมากคือบอทเปิดค้างอยู่
raise SystemExit(f"❌ เปิด {path} ไม่ได้ ({type(e).__name__}: {e}) — ปิดบอทก่อนแล้วรันใหม่")
def show(path: str) -> dict:
c = _open(path)
try:
aliases = {a.alias_name: a.collection_name for a in c.get_aliases().aliases}
print(f"โฟลเดอร์: {path}")
for col in sorted(x.name for x in c.get_collections().collections):
n = c.get_collection(col).points_count
via = [a for a, t in aliases.items() if t == col]
print(f" collection {col}: {n} จุด · เวกเตอร์ {_vectors(c, col)}"
+ (f" ← alias {', '.join(via)}" if via else ""))
if not aliases:
print(" (ยังไม่มี alias — บอทใช้ชื่อสำรองใน brand.yaml)")
return aliases
finally:
c.close()
def set_alias(path: str, alias: str, to: str, dry: bool = False) -> int:
from qdrant_client import models as M
why = _refuse_movex(path, alias, to)
if why:
print(f"❌ {why}")
return 2
c = _open(path)
try:
names = {x.name for x in c.get_collections().collections}
aliases = {a.alias_name: a.collection_name for a in c.get_aliases().aliases}
if to not in names:
print(f"❌ ไม่มี collection {to} (มี: {', '.join(sorted(names))})")
return 2
if alias in names:
print(f"❌ {alias} เป็นชื่อ collection อยู่แล้ว ตั้งเป็น alias ซ้ำไม่ได้")
return 2
n = c.get_collection(to).points_count or 0
if n <= 0:
print(f"❌ {to} ไม่มีจุด — ไม่สลับ")
return 2
old = aliases.get(alias)
if old == to:
print(f"✅ {alias} ชี้ {to} อยู่แล้ว ไม่ต้องทำอะไร")
return 0
if old:
a, b = _vectors(c, old), _vectors(c, to)
if a != b:
print(f"❌ โครงสร้างเวกเตอร์ไม่ตรงกัน {old} {a} ≠ {to} {b} — retriever จะค้นไม่ได้ ไม่สลับ")
return 2
print(f"{alias}: {old or '(ยังไม่มี)'} → {to} ({n} จุด)")
if dry:
print("(--dry-run ไม่ได้เขียน)")
return 0
finally:
c.close()
del c
meta = os.path.join(path, "meta.json")
bak = f"{meta}.bak-{time.strftime('%Y%m%d-%H%M%S')}"
shutil.copy2(meta, bak) # shadow copy ก่อนเขียน
print(f"สำรอง → {os.path.basename(bak)}")
c = _open(path)
try:
ops = []
if old:
ops.append(M.DeleteAliasOperation(delete_alias=M.DeleteAlias(alias_name=alias)))
ops.append(M.CreateAliasOperation(create_alias=M.CreateAlias(collection_name=to, alias_name=alias)))
c.update_collection_aliases(change_aliases_operations=ops)
finally:
c.close()
del c
c = _open(path) # เปิดใหม่ตรวจว่าบันทึกลงไฟล์จริง
try:
got = {a.alias_name: a.collection_name for a in c.get_aliases().aliases}.get(alias)
cnt = c.get_collection(alias).points_count if got else 0
finally:
c.close()
del c
if got != to or cnt != n:
print(f"❌ ตรวจหลังเขียนไม่ผ่าน (alias → {got}, {cnt} จุด) — คืนไฟล์จาก {os.path.basename(bak)}")
shutil.copy2(bak, meta)
return 1
print(f"✅ {alias} → {to} · ค้นผ่าน alias ได้ {cnt} จุด · commit qdrant_modutech_db/meta.json แล้วอัปโหลดขึ้น Space")
return 0
def main() -> int:
ap = argparse.ArgumentParser()
ap.add_argument("cmd", choices=["show", "set"])
ap.add_argument("--path", default=DEFAULT_PATH)
ap.add_argument("--alias", default=DEFAULT_ALIAS)
ap.add_argument("--to", help="collection ปลายทาง (คำสั่ง set)")
ap.add_argument("--dry-run", action="store_true")
a = ap.parse_args()
why = _refuse_movex(a.path)
if why:
print(f"❌ {why}")
return 2
if a.cmd == "show":
show(a.path)
return 0
if not a.to:
print("❌ ต้องระบุ --to <collection>")
return 2
return set_alias(a.path, a.alias, a.to, a.dry_run)
if __name__ == "__main__":
sys.exit(main())
|