2_qdrant_demo.py:

# qdrant_demo_fixed.py
import json
from dataclasses import dataclass
from typing import Optional, List, Dict, Any
from qdrant_client import QdrantClient
from qdrant_client.http import models
from qdrant_client.http.exceptions import UnexpectedResponse
import uuid
import os


# ========== 数据模型定义 ==========
@dataclass
class ColumnConfig:
    name: str
    role: str
    description: str
    alias: List[str]
    sync: bool

    def to_dict(self):
        return {
            "name": self.name,
            "role": self.role,
            "description": self.description,
            "alias": self.alias,
            "sync": self.sync
        }

    @classmethod
    def from_dict(cls, data):
        return cls(
            name=data["name"],
            role=data["role"],
            description=data["description"],
            alias=data.get("alias", []),
            sync=data.get("sync", False)
        )


@dataclass
class TableConfig:
    name: str
    role: str
    description: str
    columns: List[ColumnConfig]

    def to_dict(self):
        return {
            "name": self.name,
            "role": self.role,
            "description": self.description,
            "columns": [col.to_dict() for col in self.columns]
        }

    @classmethod
    def from_dict(cls, data):
        return cls(
            name=data["name"],
            role=data["role"],
            description=data["description"],
            columns=[ColumnConfig.from_dict(col) for col in data.get("columns", [])]
        )


@dataclass
class MetricConfig:
    name: str
    description: str
    relevant_columns: List[str]
    alias: List[str]

    def to_dict(self):
        return {
            "name": self.name,
            "description": self.description,
            "relevant_columns": self.relevant_columns,
            "alias": self.alias
        }

    @classmethod
    def from_dict(cls, data):
        return cls(
            name=data["name"],
            description=data["description"],
            relevant_columns=data.get("relevant_columns", []),
            alias=data.get("alias", [])
        )


@dataclass
class MetaConfig:
    tables: Optional[List[TableConfig]] = None
    metrics: Optional[List[MetricConfig]] = None

    def to_dict(self):
        result = {}
        if self.tables:
            result["tables"] = [table.to_dict() for table in self.tables]
        if self.metrics:
            result["metrics"] = [metric.to_dict() for metric in self.metrics]
        return result

    @classmethod
    def from_dict(cls, data):
        tables = None
        metrics = None
        if "tables" in data:
            tables = [TableConfig.from_dict(table) for table in data["tables"]]
        if "metrics" in data:
            metrics = [MetricConfig.from_dict(metric) for metric in data["metrics"]]
        return cls(tables=tables, metrics=metrics)


from data_agent.v0.day04_embed.emb_cpu_2 import EmbeddingClientManager

embedding_client_manager = EmbeddingClientManager()

embedding_client_manager.init()

query = embedding_client_manager.embed_query("hello world")

# 这里的EmbeddingClientManager  可以模拟 一个 也可以


# ========== Qdrant 连接配置 ==========
class QdrantConfig:
    """Qdrant配置管理"""

    @staticmethod
    def get_config():
        """获取Qdrant连接配置"""
        return {
            'host': os.getenv('QDRANT_HOST', 'localhost'),
            'port': int(os.getenv('QDRANT_PORT', 6333)),
            'api_key': os.getenv('QDRANT_API_KEY', None),  # 如果有API密钥
            'timeout': 30,
            'check_compatibility': False  # 忽略版本兼容性警告
        }

    @staticmethod
    def test_connection():
        """测试Qdrant连接"""
        config = QdrantConfig.get_config()
        try:
            client = QdrantClient(**config)
            client.get_collections()
            print("✅ Qdrant连接成功!")
            return True
        except UnexpectedResponse as e:
            print(f"❌ Qdrant连接失败: {e}")
            print("\n💡 解决方法:")
            print("   1. 如果Qdrant需要API密钥,设置环境变量:")
            print("      export QDRANT_API_KEY=your-api-key")
            print("   2. 或使用无认证的Qdrant:")
            print("      docker run -d --name qdrant-meta -p 6333:6333 qdrant/qdrant")
            return False
        except Exception as e:
            print(f"❌ Qdrant连接失败: {e}")
            return False


# ========== Qdrant 仓储实现 ==========
class QdrantMetaRepository:
    def __init__(self, host: str = "localhost", port: int = 6333,
                 collection_name: str = "demo_meta_configs",
                 api_key: str = "happy123",
                 check_compatibility: bool = False):

        # 构建客户端配置
        client_config = {
            'host': host,
            'port': port,
            'timeout': 30,
            'check_compatibility': check_compatibility,
            # TODO ?
             'https': False  # ✅ 强制使用 HTTP 协议
        }

        # 如果有API密钥,添加到配置中
        if api_key:
            client_config['api_key'] = api_key

        try:
            self.client = QdrantClient(**client_config)
            print(f"✅ Qdrant客户端初始化成功 (host={host}, port={port})")
        except Exception as e:
            print(f"❌ Qdrant客户端初始化失败: {e}")
            raise

        self.collection_name = collection_name
        self._init_collection()

    def _init_collection(self):
        """初始化集合"""
        try:
            # 检查集合是否存在
            collections = self.client.get_collections().collections
            collection_exists = any(c.name == self.collection_name for c in collections)

            if not collection_exists:
                # 创建集合
                self.client.create_collection(
                    collection_name=self.collection_name,
                    vectors_config=models.VectorParams(
                        # size=384,  # 使用all-MiniLM-L6-v2的维度
                        size=1024,  # ✅ 修改为 1024,与你的模型输出保持一致
                        distance=models.Distance.COSINE
                    )
                )
                print(f"✅ Qdrant集合 '{self.collection_name}' 创建成功")
            else:
                print(f"✅ Qdrant集合 '{self.collection_name}' 已存在")
        except UnexpectedResponse as e:
            print(f"❌ 集合操作失败: {e}")
            print("\n💡 可能的原因:")
            print("   1. Qdrant服务器未启动")
            print("   2. API密钥错误或未提供")
            print("   3. 端口或主机配置错误")
            raise
        except Exception as e:
            print(f"❌ 集合操作失败: {e}")
            raise

    def _generate_id(self) -> str:
        return str(uuid.uuid4())

    def _entity_to_vector(self, entity: MetaConfig) -> List[float]:
        """将实体转换为向量"""
        try:
            # 将配置转换为文本
            text = json.dumps(entity.to_dict(), ensure_ascii=False)
            # vector = embedding_client.embed_query(text)
            vector = embedding_client_manager.embed_query(text)
            return vector
        except Exception as e:
            print(f"⚠️ 向量生成失败: {e}")
            # 使用随机向量
            import random
            return [random.random() for _ in range(384)]

    def _prepare_payload(self, entity: MetaConfig) -> Dict[str, Any]:
        """准备payload"""
        return {
            "config_data": json.dumps(entity.to_dict(), ensure_ascii=False),
            "table_names": [t.name for t in entity.tables] if entity.tables else [],
            "metric_names": [m.name for m in entity.metrics] if entity.metrics else [],
            "has_tables": bool(entity.tables),
            "has_metrics": bool(entity.metrics)
        }

    # ========== CRUD 操作 ==========
    def create(self, entity: MetaConfig) -> str:
        """创建配置,返回ID"""
        point_id = self._generate_id()
        vector = self._entity_to_vector(entity)
        payload = self._prepare_payload(entity)

        try:
            self.client.upsert(
                collection_name=self.collection_name,
                points=[
                    models.PointStruct(
                        id=point_id,
                        vector=vector,
                        payload=payload
                    )
                ]
            )
            print(f"✅ Qdrant创建成功,ID: {point_id}")
            return point_id
        except Exception as e:
            print(f"❌ Qdrant创建失败: {e}")
            raise

    def get_by_id(self, id: str) -> Optional[MetaConfig]:
        """根据ID获取配置"""
        try:
            result = self.client.retrieve(
                collection_name=self.collection_name,
                ids=[id]
            )

            if result and len(result) > 0:
                payload = result[0].payload
                if payload and "config_data" in payload:
                    data = json.loads(payload["config_data"])
                    return MetaConfig.from_dict(data)
        except Exception as e:
            print(f"❌ 查询失败: {e}")
        return None

    def get_all(self, limit: int = 100) -> List[MetaConfig]:
        """获取所有配置"""
        try:
            results = self.client.scroll(
                collection_name=self.collection_name,
                limit=limit
            )

            configs = []
            for point in results[0]:
                if point.payload and "config_data" in point.payload:
                    data = json.loads(point.payload["config_data"])
                    configs.append(MetaConfig.from_dict(data))
            return configs
        except Exception as e:
            print(f"❌ 获取所有配置失败: {e}")
            return []

    def update(self, id: str, entity: MetaConfig) -> bool:
        """更新配置"""
        try:
            # 检查是否存在
            existing = self.get_by_id(id)
            if not existing:
                print(f"❌ ID {id} 不存在")
                return False

            vector = self._entity_to_vector(entity)
            payload = self._prepare_payload(entity)

            self.client.upsert(
                collection_name=self.collection_name,
                points=[
                    models.PointStruct(
                        id=id,
                        vector=vector,
                        payload=payload
                    )
                ]
            )
            print(f"✅ Qdrant更新成功,ID: {id}")
            return True
        except Exception as e:
            print(f"❌ Qdrant更新失败: {e}")
            return False

    def delete(self, id: str) -> bool:
        """删除配置"""
        try:
            result = self.client.delete(
                collection_name=self.collection_name,
                points_selector=models.PointIdsList(points=[id])
            )
            success = result.status == "completed"
            if success:
                print(f"✅ Qdrant删除成功,ID: {id}")
            else:
                print(f"❌ Qdrant删除失败")
            return success
        except Exception as e:
            print(f"❌ Qdrant删除失败: {e}")
            return False

    def search(self, query: str, limit: int = 10, score_threshold: float = 0.5) -> List[tuple]:
        """向量相似度搜索,返回 (配置, 相似度分数)"""
        try:
            # 生成查询向量
            query_vector = embedding_client_manager.embed_query(query)

            results = self.client.search(
                collection_name=self.collection_name,
                query_vector=query_vector,
                limit=limit,
                score_threshold=score_threshold
            )

            configs = []
            for scored_point in results:
                if scored_point.payload and "config_data" in scored_point.payload:
                    data = json.loads(scored_point.payload["config_data"])
                    config = MetaConfig.from_dict(data)
                    configs.append((config, scored_point.score))
            return configs
        except Exception as e:
            print(f"❌ 向量搜索失败: {e}")
            return []

    def search_by_keyword(self, query: str) -> List[MetaConfig]:
        """关键词搜索"""
        try:
            scroll_filter = models.Filter(
                should=[
                    models.FieldCondition(
                        key="table_names",
                        match=models.MatchText(text=query)
                    ),
                    models.FieldCondition(
                        key="metric_names",
                        match=models.MatchText(text=query)
                    )
                ]
            )

            results = self.client.scroll(
                collection_name=self.collection_name,
                scroll_filter=scroll_filter,
                limit=100
            )

            configs = []
            for point in results[0]:
                if point.payload and "config_data" in point.payload:
                    data = json.loads(point.payload["config_data"])
                    configs.append(MetaConfig.from_dict(data))
            return configs
        except Exception as e:
            print(f"❌ 关键词搜索失败: {e}")
            return []


# ========== 演示函数 ==========
def qdrant_demo():
    print("=" * 60)
    print("🔷 Qdrant 元数据管理演示")
    print("=" * 60)

    # 1. 检查并启动Qdrant
    print("\n📦 检查Qdrant服务...")

    try:
        repo = QdrantMetaRepository(
            host="localhost",
            port=6333,
            collection_name="demo_meta_configs",
            api_key="happy123",  # 无认证
            check_compatibility=False  # 忽略版本检查
        )
    except Exception as e:
        print(f"❌ 初始化失败: {e}")
        return

    # 4. 准备测试数据
    print("\n📦 准备测试数据...")

    columns = [
        ColumnConfig(
            name="user_id",
            role="primary_key",
            description="用户唯一标识",
            alias=["用户ID", "UID"],
            sync=False
        ),
        ColumnConfig(
            name="user_name",
            role="dimension",
            description="用户姓名",
            alias=["姓名", "用户名"],
            sync=True
        ),
        ColumnConfig(
            name="user_level",
            role="dimension",
            description="用户等级,如VIP、普通等",
            alias=["等级", "用户等级"],
            sync=True
        )
    ]

    tables = [
        TableConfig(
            name="dim_user",
            role="dim",
            description="用户维度表,存储用户基本信息",
            columns=columns
        )
    ]

    metrics = [
        MetricConfig(
            name="用户活跃度",
            description="用户在一定时间内的活跃程度评分",
            relevant_columns=["fact_user_activity.login_count", "fact_user_activity.action_count"],
            alias=["活跃度", "用户活跃分"]
        )
    ]

    meta_config = MetaConfig(tables=tables, metrics=metrics)

    # 5. 创建
    print("\n📝 创建配置...")
    try:
        config_id = repo.create(meta_config)
    except Exception as e:
        print(f"❌ 创建失败: {e}")
        return

    # 6. 查询
    print("\n🔍 根据ID查询...")
    retrieved = repo.get_by_id(config_id)
    if retrieved:
        print(f"  查询成功!")
        print(f"  表数量: {len(retrieved.tables) if retrieved.tables else 0}")
        print(f"  指标数量: {len(retrieved.metrics) if retrieved.metrics else 0}")
        if retrieved.tables:
            print(f"  表名: {[t.name for t in retrieved.tables]}")
        if retrieved.metrics:
            print(f"  指标名: {[m.name for m in retrieved.metrics]}")

    # 7. 向量搜索
    print("\n🔍 向量搜索 '用户相关配置'...")
    search_results = repo.search("用户相关配置", limit=3)
    if search_results:
        for config, score in search_results:
            table_names = [t.name for t in config.tables] if config.tables else []
            metric_names = [m.name for m in config.metrics] if config.metrics else []
            print(f"  相似度: {score:.4f}, 表: {table_names}, 指标: {metric_names}")
    else:
        print("  没有找到结果")

    # 8. 关键词搜索
    print("\n🔍 关键词搜索 'dim_user'...")
    keyword_results = repo.search_by_keyword("dim_user")
    print(f"  找到 {len(keyword_results)} 个配置")

    # 9. 更新
    print("\n📝 更新配置...")
    new_columns = columns + [
        ColumnConfig(
            name="email",
            role="dimension",
            description="用户邮箱",
            alias=["邮箱", "电子邮件"],
            sync=True
        )
    ]
    new_tables = [
        TableConfig(
            name="dim_user",
            role="dim",
            description="用户维度表,存储用户基本信息和联系方式",
            columns=new_columns
        )
    ]
    updated_config = MetaConfig(tables=new_tables, metrics=metrics)
    if repo.update(config_id, updated_config):
        print("  更新成功!")

    # 10. 获取所有
    print("\n📋 获取所有配置...")
    all_configs = repo.get_all()
    print(f"  共有 {len(all_configs)} 个配置")

    # 11. 删除
    print("\n🗑️ 删除配置...")
    repo.delete(config_id)
    print("  删除成功!")

    print("\n" + "=" * 60)
    print("✅ Qdrant演示完成!")
    print("=" * 60)


if __name__ == "__main__":
    qdrant_demo()

'''
Qdrant 自带了一个非常直观的 Web 管理界面,你可以直接在浏览器里访问,查看集合、数据和执行搜索。
🌐 访问 Qdrant Web UI
如果你的 Qdrant 服务正在本地运行,通常可以通过以下地址访问:
http://localhost:6333/dashboard
打开这个链接,你应该能看到一个图形化界面。
💻 在界面中查看数据
进入 Dashboard 后,你可以这样操作:
找到集合:在页面中找到并点击你的集合名称,也就是 demo_meta_configs。
查看数据点:进入集合详情页后,点击 "Points" 标签页。
浏览内容:这里会列出所有存入的数据点(Points)。你可以点击某个具体的 ID,查看它的:
Payload: 这里存储了你代码中的 MetaConfig 数据,比如表名、指标名等,是 JSON 格式。
Vector: 这里就是那个 1024 维的向量,通常是一串数字。
通过这个界面,你可以很方便地验证数据是否成功写入,以及检查数据的具体内容。
'''

Logo

Agent 垂直技术社区,欢迎活跃、内容共建。

更多推荐