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())