infomankit 0.3.23__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- infoman/__init__.py +1 -0
- infoman/cli/README.md +378 -0
- infoman/cli/__init__.py +7 -0
- infoman/cli/commands/__init__.py +3 -0
- infoman/cli/commands/init.py +312 -0
- infoman/cli/scaffold.py +634 -0
- infoman/cli/templates/Makefile.template +132 -0
- infoman/cli/templates/app/__init__.py.template +3 -0
- infoman/cli/templates/app/app.py.template +4 -0
- infoman/cli/templates/app/models_base.py.template +18 -0
- infoman/cli/templates/app/models_entity_init.py.template +11 -0
- infoman/cli/templates/app/models_schemas_init.py.template +11 -0
- infoman/cli/templates/app/repository_init.py.template +11 -0
- infoman/cli/templates/app/routers_init.py.template +15 -0
- infoman/cli/templates/app/services_init.py.template +11 -0
- infoman/cli/templates/app/static_index.html.template +39 -0
- infoman/cli/templates/app/static_main.js.template +31 -0
- infoman/cli/templates/app/static_style.css.template +111 -0
- infoman/cli/templates/app/utils_init.py.template +11 -0
- infoman/cli/templates/config/.env.dev.template +43 -0
- infoman/cli/templates/config/.env.prod.template +43 -0
- infoman/cli/templates/config/README.md.template +28 -0
- infoman/cli/templates/docker/.dockerignore.template +60 -0
- infoman/cli/templates/docker/Dockerfile.template +47 -0
- infoman/cli/templates/docker/README.md.template +240 -0
- infoman/cli/templates/docker/docker-compose.yml.template +81 -0
- infoman/cli/templates/docker/mysql_custom.cnf.template +42 -0
- infoman/cli/templates/docker/mysql_init.sql.template +15 -0
- infoman/cli/templates/project/.env.example.template +1 -0
- infoman/cli/templates/project/.gitignore.template +60 -0
- infoman/cli/templates/project/Makefile.template +38 -0
- infoman/cli/templates/project/README.md.template +137 -0
- infoman/cli/templates/project/deploy.sh.template +97 -0
- infoman/cli/templates/project/main.py.template +10 -0
- infoman/cli/templates/project/manage.sh.template +97 -0
- infoman/cli/templates/project/pyproject.toml.template +47 -0
- infoman/cli/templates/project/service.sh.template +203 -0
- infoman/config/__init__.py +25 -0
- infoman/config/base.py +67 -0
- infoman/config/db_cache.py +237 -0
- infoman/config/db_relation.py +181 -0
- infoman/config/db_vector.py +39 -0
- infoman/config/jwt.py +16 -0
- infoman/config/llm.py +16 -0
- infoman/config/log.py +627 -0
- infoman/config/mq.py +26 -0
- infoman/config/settings.py +65 -0
- infoman/llm/__init__.py +0 -0
- infoman/llm/llm.py +297 -0
- infoman/logger/__init__.py +57 -0
- infoman/logger/context.py +191 -0
- infoman/logger/core.py +358 -0
- infoman/logger/filters.py +157 -0
- infoman/logger/formatters.py +138 -0
- infoman/logger/handlers.py +276 -0
- infoman/logger/metrics.py +160 -0
- infoman/performance/README.md +583 -0
- infoman/performance/__init__.py +19 -0
- infoman/performance/cli.py +215 -0
- infoman/performance/config.py +166 -0
- infoman/performance/reporter.py +519 -0
- infoman/performance/runner.py +303 -0
- infoman/performance/standards.py +222 -0
- infoman/service/__init__.py +8 -0
- infoman/service/app.py +67 -0
- infoman/service/core/__init__.py +0 -0
- infoman/service/core/auth.py +105 -0
- infoman/service/core/lifespan.py +132 -0
- infoman/service/core/monitor.py +57 -0
- infoman/service/core/response.py +37 -0
- infoman/service/exception/__init__.py +7 -0
- infoman/service/exception/error.py +274 -0
- infoman/service/exception/exception.py +25 -0
- infoman/service/exception/handler.py +238 -0
- infoman/service/infrastructure/__init__.py +8 -0
- infoman/service/infrastructure/base.py +212 -0
- infoman/service/infrastructure/db_cache/__init__.py +8 -0
- infoman/service/infrastructure/db_cache/manager.py +194 -0
- infoman/service/infrastructure/db_relation/__init__.py +41 -0
- infoman/service/infrastructure/db_relation/manager.py +300 -0
- infoman/service/infrastructure/db_relation/manager_pro.py +408 -0
- infoman/service/infrastructure/db_relation/mysql.py +52 -0
- infoman/service/infrastructure/db_relation/pgsql.py +54 -0
- infoman/service/infrastructure/db_relation/sqllite.py +25 -0
- infoman/service/infrastructure/db_vector/__init__.py +40 -0
- infoman/service/infrastructure/db_vector/manager.py +201 -0
- infoman/service/infrastructure/db_vector/qdrant.py +322 -0
- infoman/service/infrastructure/mq/__init__.py +15 -0
- infoman/service/infrastructure/mq/manager.py +178 -0
- infoman/service/infrastructure/mq/nats/__init__.py +0 -0
- infoman/service/infrastructure/mq/nats/nats_client.py +57 -0
- infoman/service/infrastructure/mq/nats/nats_event_router.py +25 -0
- infoman/service/launch.py +284 -0
- infoman/service/middleware/__init__.py +7 -0
- infoman/service/middleware/base.py +41 -0
- infoman/service/middleware/logging.py +51 -0
- infoman/service/middleware/rate_limit.py +301 -0
- infoman/service/middleware/request_id.py +21 -0
- infoman/service/middleware/white_list.py +24 -0
- infoman/service/models/__init__.py +8 -0
- infoman/service/models/base.py +441 -0
- infoman/service/models/type/embed.py +70 -0
- infoman/service/routers/__init__.py +18 -0
- infoman/service/routers/health_router.py +311 -0
- infoman/service/routers/monitor_router.py +44 -0
- infoman/service/utils/__init__.py +8 -0
- infoman/service/utils/cache/__init__.py +0 -0
- infoman/service/utils/cache/cache.py +192 -0
- infoman/service/utils/module_loader.py +10 -0
- infoman/service/utils/parse.py +10 -0
- infoman/service/utils/resolver/__init__.py +8 -0
- infoman/service/utils/resolver/base.py +47 -0
- infoman/service/utils/resolver/resp.py +102 -0
- infoman/service/vector/__init__.py +20 -0
- infoman/service/vector/base.py +56 -0
- infoman/service/vector/qdrant.py +125 -0
- infoman/service/vector/service.py +67 -0
- infoman/utils/__init__.py +2 -0
- infoman/utils/decorators/__init__.py +8 -0
- infoman/utils/decorators/cache.py +137 -0
- infoman/utils/decorators/retry.py +99 -0
- infoman/utils/decorators/safe_execute.py +99 -0
- infoman/utils/decorators/timing.py +99 -0
- infoman/utils/encryption/__init__.py +8 -0
- infoman/utils/encryption/aes.py +66 -0
- infoman/utils/encryption/ecc.py +108 -0
- infoman/utils/encryption/rsa.py +112 -0
- infoman/utils/file/__init__.py +0 -0
- infoman/utils/file/handler.py +22 -0
- infoman/utils/hash/__init__.py +0 -0
- infoman/utils/hash/hash.py +61 -0
- infoman/utils/http/__init__.py +8 -0
- infoman/utils/http/client.py +62 -0
- infoman/utils/http/info.py +94 -0
- infoman/utils/http/result.py +19 -0
- infoman/utils/notification/__init__.py +8 -0
- infoman/utils/notification/feishu.py +35 -0
- infoman/utils/text/__init__.py +8 -0
- infoman/utils/text/extractor.py +111 -0
- infomankit-0.3.23.dist-info/METADATA +632 -0
- infomankit-0.3.23.dist-info/RECORD +143 -0
- infomankit-0.3.23.dist-info/WHEEL +4 -0
- infomankit-0.3.23.dist-info/entry_points.txt +5 -0
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
# database/backends.py
|
|
2
|
+
"""
|
|
3
|
+
数据库后端实现
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from typing import Dict, Any
|
|
7
|
+
from infoman.config import DatabaseInstanceConfig
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class MySQLBackend:
|
|
11
|
+
"""MySQL 后端"""
|
|
12
|
+
|
|
13
|
+
@staticmethod
|
|
14
|
+
def get_engine() -> str:
|
|
15
|
+
return "tortoise.backends.mysql"
|
|
16
|
+
|
|
17
|
+
@staticmethod
|
|
18
|
+
def get_credentials(config: DatabaseInstanceConfig) -> Dict[str, Any]:
|
|
19
|
+
"""生成 MySQL 连接凭证"""
|
|
20
|
+
credentials = {
|
|
21
|
+
"host": config.host,
|
|
22
|
+
"port": config.port,
|
|
23
|
+
"user": config.user,
|
|
24
|
+
"password": config.password,
|
|
25
|
+
"database": config.database,
|
|
26
|
+
"charset": config.charset,
|
|
27
|
+
# 连接池
|
|
28
|
+
"minsize": config.pool_min_size,
|
|
29
|
+
"maxsize": config.pool_max_size,
|
|
30
|
+
"pool_recycle": config.pool_recycle,
|
|
31
|
+
"echo": config.echo,
|
|
32
|
+
# 超时
|
|
33
|
+
"connect_timeout": config.connect_timeout,
|
|
34
|
+
# 其他
|
|
35
|
+
"autocommit": True,
|
|
36
|
+
"init_command": "SET sql_mode='STRICT_TRANS_TABLES'",
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
# SSL
|
|
40
|
+
if config.ssl_enabled:
|
|
41
|
+
ssl_config = {}
|
|
42
|
+
if config.ssl_ca:
|
|
43
|
+
ssl_config["ca"] = config.ssl_ca
|
|
44
|
+
if config.ssl_cert:
|
|
45
|
+
ssl_config["cert"] = config.ssl_cert
|
|
46
|
+
if config.ssl_key:
|
|
47
|
+
ssl_config["key"] = config.ssl_key
|
|
48
|
+
|
|
49
|
+
if ssl_config:
|
|
50
|
+
credentials["ssl"] = ssl_config
|
|
51
|
+
|
|
52
|
+
return credentials
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
# !/usr/bin/env python
|
|
2
|
+
# -*-coding:utf-8 -*-
|
|
3
|
+
|
|
4
|
+
"""
|
|
5
|
+
# Time :2025/12/22 21:10
|
|
6
|
+
# Author :Maxwell
|
|
7
|
+
# Description:
|
|
8
|
+
"""
|
|
9
|
+
from typing import Dict, Any
|
|
10
|
+
from infoman.config import DatabaseInstanceConfig
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class PostgreSQLBackend:
|
|
14
|
+
"""PostgreSQL 后端"""
|
|
15
|
+
|
|
16
|
+
@staticmethod
|
|
17
|
+
def get_engine() -> str:
|
|
18
|
+
return "tortoise.backends.asyncpg"
|
|
19
|
+
|
|
20
|
+
@staticmethod
|
|
21
|
+
def get_credentials(config: DatabaseInstanceConfig) -> Dict[str, Any]:
|
|
22
|
+
"""生成 PostgreSQL 连接凭证"""
|
|
23
|
+
credentials = {
|
|
24
|
+
"host": config.host,
|
|
25
|
+
"port": config.port,
|
|
26
|
+
"user": config.user,
|
|
27
|
+
"password": config.password,
|
|
28
|
+
"database": config.database,
|
|
29
|
+
# 连接池(注意:asyncpg 参数名不同)
|
|
30
|
+
"min_size": config.pool_min_size,
|
|
31
|
+
"max_size": config.pool_max_size,
|
|
32
|
+
"max_queries": 50000,
|
|
33
|
+
"max_inactive_connection_lifetime": config.pool_recycle,
|
|
34
|
+
# 超时
|
|
35
|
+
"timeout": config.connect_timeout,
|
|
36
|
+
"command_timeout": config.command_timeout,
|
|
37
|
+
# PostgreSQL 特有
|
|
38
|
+
"schema": config.schema_name,
|
|
39
|
+
"server_settings": {
|
|
40
|
+
"application_name": "infoman",
|
|
41
|
+
"jit": "off",
|
|
42
|
+
},
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
# SSL
|
|
46
|
+
if config.ssl_enabled:
|
|
47
|
+
import ssl
|
|
48
|
+
|
|
49
|
+
ssl_context = ssl.create_default_context()
|
|
50
|
+
if config.ssl_ca:
|
|
51
|
+
ssl_context.load_verify_locations(cafile=config.ssl_ca)
|
|
52
|
+
credentials["ssl"] = ssl_context
|
|
53
|
+
|
|
54
|
+
return credentials
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
# !/usr/bin/env python
|
|
2
|
+
# -*-coding:utf-8 -*-
|
|
3
|
+
|
|
4
|
+
"""
|
|
5
|
+
# Time :2025/12/22 21:11
|
|
6
|
+
# Author :Maxwell
|
|
7
|
+
# Description:
|
|
8
|
+
"""
|
|
9
|
+
from typing import Dict, Any
|
|
10
|
+
from infoman.config import DatabaseConfig
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class SQLiteBackend:
|
|
14
|
+
"""SQLite 后端"""
|
|
15
|
+
|
|
16
|
+
@staticmethod
|
|
17
|
+
def get_engine() -> str:
|
|
18
|
+
return "tortoise.backends.sqlite"
|
|
19
|
+
|
|
20
|
+
@staticmethod
|
|
21
|
+
def get_credentials(config: DatabaseConfig) -> Dict[str, Any]:
|
|
22
|
+
"""生成 SQLite 连接凭证"""
|
|
23
|
+
return {
|
|
24
|
+
"file_path": f"{config.database}.db",
|
|
25
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
# !/usr/bin/env python
|
|
2
|
+
# -*-coding:utf-8 -*-
|
|
3
|
+
|
|
4
|
+
"""
|
|
5
|
+
向量数据库基础设施
|
|
6
|
+
|
|
7
|
+
支持:
|
|
8
|
+
- Qdrant: 高性能向量搜索引擎
|
|
9
|
+
- Milvus: 待实现
|
|
10
|
+
|
|
11
|
+
Usage:
|
|
12
|
+
# 方式 1: 通过 lifespan 自动初始化
|
|
13
|
+
from infoman.service.app import application
|
|
14
|
+
# VectorDBManager 会在应用启动时自动初始化
|
|
15
|
+
|
|
16
|
+
# 方式 2: 手动使用
|
|
17
|
+
from infoman.service.infrastructure.db_vector import VectorDBManager
|
|
18
|
+
|
|
19
|
+
manager = VectorDBManager()
|
|
20
|
+
await manager.startup()
|
|
21
|
+
|
|
22
|
+
# 使用 Qdrant
|
|
23
|
+
client = manager.qdrant.client
|
|
24
|
+
await client.create_collection(...)
|
|
25
|
+
|
|
26
|
+
# 便捷方法
|
|
27
|
+
await manager.create_collection("my_collection", vector_size=768)
|
|
28
|
+
results = await manager.search("my_collection", query_vector=[...])
|
|
29
|
+
|
|
30
|
+
# 关闭
|
|
31
|
+
await manager.shutdown()
|
|
32
|
+
"""
|
|
33
|
+
|
|
34
|
+
from .manager import VectorDBManager
|
|
35
|
+
from .qdrant import QdrantBackend
|
|
36
|
+
|
|
37
|
+
__all__ = [
|
|
38
|
+
"VectorDBManager",
|
|
39
|
+
"QdrantBackend",
|
|
40
|
+
]
|
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
# !/usr/bin/env python
|
|
2
|
+
# -*-coding:utf-8 -*-
|
|
3
|
+
|
|
4
|
+
"""
|
|
5
|
+
向量数据库管理器(支持延迟导入)
|
|
6
|
+
|
|
7
|
+
支持:
|
|
8
|
+
- Qdrant
|
|
9
|
+
- Milvus (待实现)
|
|
10
|
+
- 统一的管理接口
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
from typing import Optional, Dict, Any, TYPE_CHECKING
|
|
14
|
+
from fastapi import FastAPI
|
|
15
|
+
from loguru import logger
|
|
16
|
+
|
|
17
|
+
from infoman.config import settings
|
|
18
|
+
|
|
19
|
+
if TYPE_CHECKING:
|
|
20
|
+
from infoman.service.infrastructure.db_vector.qdrant import QdrantBackend
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class VectorDBManager:
|
|
24
|
+
"""向量数据库管理器"""
|
|
25
|
+
|
|
26
|
+
def __init__(self):
|
|
27
|
+
self.qdrant: Optional[Any] = None
|
|
28
|
+
self.initialized = False
|
|
29
|
+
|
|
30
|
+
@property
|
|
31
|
+
def is_available(self) -> bool:
|
|
32
|
+
"""是否有可用的向量数据库"""
|
|
33
|
+
return (
|
|
34
|
+
(self.qdrant and self.qdrant.is_available) or
|
|
35
|
+
False # 其他数据库
|
|
36
|
+
)
|
|
37
|
+
|
|
38
|
+
@property
|
|
39
|
+
def client(self):
|
|
40
|
+
"""
|
|
41
|
+
获取默认客户端
|
|
42
|
+
|
|
43
|
+
优先级:Qdrant > Milvus
|
|
44
|
+
"""
|
|
45
|
+
if self.qdrant and self.qdrant.is_available:
|
|
46
|
+
return self.qdrant.client
|
|
47
|
+
|
|
48
|
+
return None
|
|
49
|
+
|
|
50
|
+
async def startup(self, app: Optional[FastAPI] = None) -> bool:
|
|
51
|
+
if self.initialized:
|
|
52
|
+
logger.warning("⚠️ VectorDBManager 已初始化,跳过重复初始化")
|
|
53
|
+
return True
|
|
54
|
+
|
|
55
|
+
success_count = 0
|
|
56
|
+
|
|
57
|
+
# ========== 启动 Qdrant ==========
|
|
58
|
+
if settings.qdrant_configured:
|
|
59
|
+
# 延迟导入 QdrantBackend
|
|
60
|
+
try:
|
|
61
|
+
from infoman.service.infrastructure.db_vector.qdrant import QdrantBackend
|
|
62
|
+
except ImportError as e:
|
|
63
|
+
logger.error(f"❌ Qdrant 依赖未安装: {e}")
|
|
64
|
+
logger.error("请运行: pip install infomankit[vector]")
|
|
65
|
+
return False
|
|
66
|
+
|
|
67
|
+
self.qdrant = QdrantBackend(settings)
|
|
68
|
+
|
|
69
|
+
if await self.qdrant.startup():
|
|
70
|
+
success_count += 1
|
|
71
|
+
|
|
72
|
+
if app:
|
|
73
|
+
app.state.qdrant_client = self.qdrant.client
|
|
74
|
+
logger.debug("✅ Qdrant 客户端已挂载到 app.state")
|
|
75
|
+
|
|
76
|
+
# ========== 初始化完成 ==========
|
|
77
|
+
if success_count > 0:
|
|
78
|
+
self.initialized = True
|
|
79
|
+
logger.success(f"✅ 向量数据库初始化完成({success_count} 个)")
|
|
80
|
+
return True
|
|
81
|
+
else:
|
|
82
|
+
logger.info("⏭️ 向量数据库未配置,跳过初始化")
|
|
83
|
+
return False
|
|
84
|
+
|
|
85
|
+
async def shutdown(self):
|
|
86
|
+
if not self.initialized:
|
|
87
|
+
return
|
|
88
|
+
|
|
89
|
+
logger.info("⏹️ 关闭向量数据库连接...")
|
|
90
|
+
|
|
91
|
+
if self.qdrant:
|
|
92
|
+
await self.qdrant.shutdown()
|
|
93
|
+
|
|
94
|
+
self.initialized = False
|
|
95
|
+
logger.success("✅ 所有向量数据库已关闭")
|
|
96
|
+
|
|
97
|
+
async def health_check(self) -> Dict[str, Any]:
|
|
98
|
+
"""
|
|
99
|
+
健康检查
|
|
100
|
+
|
|
101
|
+
Returns:
|
|
102
|
+
{
|
|
103
|
+
"qdrant": {"status": "healthy", ...},
|
|
104
|
+
"milvus": {"status": "not_configured", ...},
|
|
105
|
+
}
|
|
106
|
+
"""
|
|
107
|
+
health = {}
|
|
108
|
+
|
|
109
|
+
# Qdrant
|
|
110
|
+
if self.qdrant:
|
|
111
|
+
health["qdrant"] = await self.qdrant.health_check()
|
|
112
|
+
else:
|
|
113
|
+
health["qdrant"] = {
|
|
114
|
+
"status": "not_configured",
|
|
115
|
+
"name": "qdrant",
|
|
116
|
+
"details": {"enabled": False}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
# Milvus
|
|
120
|
+
# if self.milvus:
|
|
121
|
+
# health["milvus"] = await self.milvus.health_check()
|
|
122
|
+
|
|
123
|
+
return health
|
|
124
|
+
|
|
125
|
+
async def get_stats(self) -> Dict[str, Any]:
|
|
126
|
+
"""
|
|
127
|
+
获取统计信息
|
|
128
|
+
|
|
129
|
+
Returns:
|
|
130
|
+
{
|
|
131
|
+
"qdrant": {...},
|
|
132
|
+
"milvus": {...},
|
|
133
|
+
}
|
|
134
|
+
"""
|
|
135
|
+
stats = {}
|
|
136
|
+
|
|
137
|
+
if self.qdrant and self.qdrant.is_available:
|
|
138
|
+
stats["qdrant"] = await self.qdrant.get_stats()
|
|
139
|
+
|
|
140
|
+
# if self.milvus and self.milvus.is_available:
|
|
141
|
+
# stats["milvus"] = await self.milvus.get_stats()
|
|
142
|
+
|
|
143
|
+
return stats
|
|
144
|
+
|
|
145
|
+
# ========== 便捷方法 ==========
|
|
146
|
+
|
|
147
|
+
async def create_collection(
|
|
148
|
+
self,
|
|
149
|
+
collection_name: str,
|
|
150
|
+
vector_size: int,
|
|
151
|
+
backend: str = "qdrant",
|
|
152
|
+
**kwargs
|
|
153
|
+
) -> bool:
|
|
154
|
+
"""
|
|
155
|
+
创建集合
|
|
156
|
+
|
|
157
|
+
Args:
|
|
158
|
+
collection_name: 集合名称
|
|
159
|
+
vector_size: 向量维度
|
|
160
|
+
backend: 使用的后端(qdrant/milvus)
|
|
161
|
+
**kwargs: 其他参数
|
|
162
|
+
|
|
163
|
+
Returns:
|
|
164
|
+
是否创建成功
|
|
165
|
+
"""
|
|
166
|
+
if backend == "qdrant" and self.qdrant:
|
|
167
|
+
return await self.qdrant.create_collection(
|
|
168
|
+
collection_name, vector_size, **kwargs
|
|
169
|
+
)
|
|
170
|
+
|
|
171
|
+
logger.error(f"后端 {backend} 不可用")
|
|
172
|
+
return False
|
|
173
|
+
|
|
174
|
+
async def search(
|
|
175
|
+
self,
|
|
176
|
+
collection_name: str,
|
|
177
|
+
query_vector: list,
|
|
178
|
+
limit: int = 10,
|
|
179
|
+
backend: str = "qdrant",
|
|
180
|
+
**kwargs
|
|
181
|
+
):
|
|
182
|
+
"""
|
|
183
|
+
向量搜索
|
|
184
|
+
|
|
185
|
+
Args:
|
|
186
|
+
collection_name: 集合名称
|
|
187
|
+
query_vector: 查询向量
|
|
188
|
+
limit: 返回结果数量
|
|
189
|
+
backend: 使用的后端
|
|
190
|
+
**kwargs: 其他参数
|
|
191
|
+
|
|
192
|
+
Returns:
|
|
193
|
+
搜索结果
|
|
194
|
+
"""
|
|
195
|
+
if backend == "qdrant" and self.qdrant:
|
|
196
|
+
return await self.qdrant.search(
|
|
197
|
+
collection_name, query_vector, limit, **kwargs
|
|
198
|
+
)
|
|
199
|
+
|
|
200
|
+
logger.error(f"后端 {backend} 不可用")
|
|
201
|
+
return []
|
|
@@ -0,0 +1,322 @@
|
|
|
1
|
+
# !/usr/bin/env python
|
|
2
|
+
# -*-coding:utf-8 -*-
|
|
3
|
+
|
|
4
|
+
"""
|
|
5
|
+
Qdrant 向量数据库后端
|
|
6
|
+
|
|
7
|
+
功能:
|
|
8
|
+
- 异步连接管理
|
|
9
|
+
- 健康检查
|
|
10
|
+
- 集合管理
|
|
11
|
+
- 向量操作
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
from typing import Optional, Dict, Any, List
|
|
15
|
+
from qdrant_client import AsyncQdrantClient
|
|
16
|
+
from qdrant_client.models import Distance, VectorParams, PointStruct
|
|
17
|
+
from loguru import logger
|
|
18
|
+
|
|
19
|
+
from infoman.config import VectorDBConfig
|
|
20
|
+
from infoman.service.infrastructure.base import (
|
|
21
|
+
BaseInfrastructureComponent,
|
|
22
|
+
ComponentType,
|
|
23
|
+
ComponentStatus,
|
|
24
|
+
)
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class QdrantBackend(BaseInfrastructureComponent):
|
|
28
|
+
"""Qdrant 向量数据库后端"""
|
|
29
|
+
|
|
30
|
+
def __init__(self, config: VectorDBConfig):
|
|
31
|
+
super().__init__(
|
|
32
|
+
component_type=ComponentType.OTHER, # 向量数据库可以归类为 OTHER
|
|
33
|
+
name="qdrant",
|
|
34
|
+
enabled=config.qdrant_configured,
|
|
35
|
+
)
|
|
36
|
+
self.config = config
|
|
37
|
+
self._client: Optional[AsyncQdrantClient] = None
|
|
38
|
+
|
|
39
|
+
async def startup(self) -> bool:
|
|
40
|
+
"""启动 Qdrant 连接"""
|
|
41
|
+
if not self.enabled:
|
|
42
|
+
logger.info("⏭️ Qdrant 未启用,跳过初始化")
|
|
43
|
+
self._set_status(ComponentStatus.NOT_CONFIGURED)
|
|
44
|
+
return False
|
|
45
|
+
|
|
46
|
+
try:
|
|
47
|
+
self._set_status(ComponentStatus.INITIALIZING)
|
|
48
|
+
logger.info(f"🔌 正在连接 Qdrant [{self.config.QDRANT_HOST}]...")
|
|
49
|
+
|
|
50
|
+
# 创建 Qdrant 客户端
|
|
51
|
+
self._client = AsyncQdrantClient(
|
|
52
|
+
host=self.config.QDRANT_HOST,
|
|
53
|
+
port=self.config.QDRANT_HTTP_PORT,
|
|
54
|
+
grpc_port=self.config.QDRANT_GRPC_PORT,
|
|
55
|
+
api_key=self.config.QDRANT_API_KEY,
|
|
56
|
+
timeout=self.config.QDRANT_TIMEOUT,
|
|
57
|
+
prefer_grpc=True, # 优先使用 gRPC(性能更好)
|
|
58
|
+
)
|
|
59
|
+
|
|
60
|
+
# 测试连接
|
|
61
|
+
await self._test_connection()
|
|
62
|
+
|
|
63
|
+
self._set_status(ComponentStatus.HEALTHY)
|
|
64
|
+
self._log_startup(
|
|
65
|
+
True,
|
|
66
|
+
f"{self.config.QDRANT_HOST}:{self.config.QDRANT_HTTP_PORT}"
|
|
67
|
+
)
|
|
68
|
+
|
|
69
|
+
return True
|
|
70
|
+
|
|
71
|
+
except Exception as e:
|
|
72
|
+
self._set_status(ComponentStatus.UNHEALTHY)
|
|
73
|
+
self._log_startup(False, str(e))
|
|
74
|
+
return False
|
|
75
|
+
|
|
76
|
+
async def shutdown(self):
|
|
77
|
+
"""关闭 Qdrant 连接"""
|
|
78
|
+
if not self._client:
|
|
79
|
+
return
|
|
80
|
+
|
|
81
|
+
try:
|
|
82
|
+
logger.info("⏹️ 正在关闭 Qdrant 连接...")
|
|
83
|
+
|
|
84
|
+
# Qdrant 客户端会自动清理连接
|
|
85
|
+
# 如果需要显式关闭,可以添加逻辑
|
|
86
|
+
self._client = None
|
|
87
|
+
|
|
88
|
+
self._set_status(ComponentStatus.STOPPED)
|
|
89
|
+
self._log_shutdown(True)
|
|
90
|
+
|
|
91
|
+
except Exception as e:
|
|
92
|
+
self._log_shutdown(False, str(e))
|
|
93
|
+
|
|
94
|
+
async def health_check(self) -> Dict[str, Any]:
|
|
95
|
+
"""健康检查"""
|
|
96
|
+
if not self.enabled:
|
|
97
|
+
return {
|
|
98
|
+
"status": "not_configured",
|
|
99
|
+
"component_type": self.component_type.value,
|
|
100
|
+
"name": self.name,
|
|
101
|
+
"details": {"enabled": False}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
if not self._client:
|
|
105
|
+
return {
|
|
106
|
+
"status": "unhealthy",
|
|
107
|
+
"component_type": self.component_type.value,
|
|
108
|
+
"name": self.name,
|
|
109
|
+
"details": {"error": "客户端未初始化"}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
try:
|
|
113
|
+
# 尝试获取集合列表
|
|
114
|
+
collections = await self._client.get_collections()
|
|
115
|
+
|
|
116
|
+
return {
|
|
117
|
+
"status": "healthy",
|
|
118
|
+
"component_type": self.component_type.value,
|
|
119
|
+
"name": self.name,
|
|
120
|
+
"details": {
|
|
121
|
+
"host": self.config.QDRANT_HOST,
|
|
122
|
+
"http_port": self.config.QDRANT_HTTP_PORT,
|
|
123
|
+
"grpc_port": self.config.QDRANT_GRPC_PORT,
|
|
124
|
+
"collections_count": len(collections.collections),
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
except Exception as e:
|
|
129
|
+
logger.error(f"Qdrant 健康检查失败: {e}")
|
|
130
|
+
self._set_status(ComponentStatus.UNHEALTHY)
|
|
131
|
+
|
|
132
|
+
return {
|
|
133
|
+
"status": "unhealthy",
|
|
134
|
+
"component_type": self.component_type.value,
|
|
135
|
+
"name": self.name,
|
|
136
|
+
"details": {"error": str(e)}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
async def _test_connection(self):
|
|
140
|
+
"""测试连接"""
|
|
141
|
+
try:
|
|
142
|
+
# 尝试获取集合列表
|
|
143
|
+
await self._client.get_collections()
|
|
144
|
+
logger.success("✅ Qdrant 连接测试成功")
|
|
145
|
+
|
|
146
|
+
except Exception as e:
|
|
147
|
+
logger.error(f"❌ Qdrant 连接测试失败: {e}")
|
|
148
|
+
raise
|
|
149
|
+
|
|
150
|
+
# ========== 便捷方法 ==========
|
|
151
|
+
|
|
152
|
+
async def create_collection(
|
|
153
|
+
self,
|
|
154
|
+
collection_name: str,
|
|
155
|
+
vector_size: int,
|
|
156
|
+
distance: Distance = Distance.COSINE,
|
|
157
|
+
**kwargs
|
|
158
|
+
) -> bool:
|
|
159
|
+
"""
|
|
160
|
+
创建集合
|
|
161
|
+
|
|
162
|
+
Args:
|
|
163
|
+
collection_name: 集合名称
|
|
164
|
+
vector_size: 向量维度
|
|
165
|
+
distance: 距离度量(COSINE/EUCLID/DOT)
|
|
166
|
+
**kwargs: 其他参数
|
|
167
|
+
|
|
168
|
+
Returns:
|
|
169
|
+
是否创建成功
|
|
170
|
+
"""
|
|
171
|
+
if not self._client:
|
|
172
|
+
logger.error("Qdrant 客户端未初始化")
|
|
173
|
+
return False
|
|
174
|
+
|
|
175
|
+
try:
|
|
176
|
+
# 检查集合是否存在
|
|
177
|
+
collections = await self._client.get_collections()
|
|
178
|
+
collection_names = [c.name for c in collections.collections]
|
|
179
|
+
|
|
180
|
+
if collection_name in collection_names:
|
|
181
|
+
logger.warning(f"集合 {collection_name} 已存在")
|
|
182
|
+
return True
|
|
183
|
+
|
|
184
|
+
# 创建集合
|
|
185
|
+
await self._client.create_collection(
|
|
186
|
+
collection_name=collection_name,
|
|
187
|
+
vectors_config=VectorParams(
|
|
188
|
+
size=vector_size,
|
|
189
|
+
distance=distance,
|
|
190
|
+
),
|
|
191
|
+
**kwargs
|
|
192
|
+
)
|
|
193
|
+
|
|
194
|
+
logger.success(f"✅ 创建集合成功: {collection_name}")
|
|
195
|
+
return True
|
|
196
|
+
|
|
197
|
+
except Exception as e:
|
|
198
|
+
logger.error(f"创建集合失败: {e}")
|
|
199
|
+
return False
|
|
200
|
+
|
|
201
|
+
async def delete_collection(self, collection_name: str) -> bool:
|
|
202
|
+
"""删除集合"""
|
|
203
|
+
if not self._client:
|
|
204
|
+
return False
|
|
205
|
+
|
|
206
|
+
try:
|
|
207
|
+
await self._client.delete_collection(collection_name)
|
|
208
|
+
logger.success(f"✅ 删除集合成功: {collection_name}")
|
|
209
|
+
return True
|
|
210
|
+
|
|
211
|
+
except Exception as e:
|
|
212
|
+
logger.error(f"删除集合失败: {e}")
|
|
213
|
+
return False
|
|
214
|
+
|
|
215
|
+
async def upsert_points(
|
|
216
|
+
self,
|
|
217
|
+
collection_name: str,
|
|
218
|
+
points: List[PointStruct],
|
|
219
|
+
) -> bool:
|
|
220
|
+
"""插入或更新向量点"""
|
|
221
|
+
if not self._client:
|
|
222
|
+
return False
|
|
223
|
+
|
|
224
|
+
try:
|
|
225
|
+
await self._client.upsert(
|
|
226
|
+
collection_name=collection_name,
|
|
227
|
+
points=points,
|
|
228
|
+
)
|
|
229
|
+
logger.debug(f"✅ 插入/更新 {len(points)} 个点到 {collection_name}")
|
|
230
|
+
return True
|
|
231
|
+
|
|
232
|
+
except Exception as e:
|
|
233
|
+
logger.error(f"插入向量失败: {e}")
|
|
234
|
+
return False
|
|
235
|
+
|
|
236
|
+
async def search(
|
|
237
|
+
self,
|
|
238
|
+
collection_name: str,
|
|
239
|
+
query_vector: List[float],
|
|
240
|
+
limit: int = 10,
|
|
241
|
+
score_threshold: Optional[float] = None,
|
|
242
|
+
**kwargs
|
|
243
|
+
) -> List[Dict[str, Any]]:
|
|
244
|
+
"""
|
|
245
|
+
向量搜索
|
|
246
|
+
|
|
247
|
+
Args:
|
|
248
|
+
collection_name: 集合名称
|
|
249
|
+
query_vector: 查询向量
|
|
250
|
+
limit: 返回结果数量
|
|
251
|
+
score_threshold: 相似度阈值
|
|
252
|
+
**kwargs: 其他搜索参数
|
|
253
|
+
|
|
254
|
+
Returns:
|
|
255
|
+
搜索结果列表
|
|
256
|
+
"""
|
|
257
|
+
if not self._client:
|
|
258
|
+
return []
|
|
259
|
+
|
|
260
|
+
try:
|
|
261
|
+
results = await self._client.search(
|
|
262
|
+
collection_name=collection_name,
|
|
263
|
+
query_vector=query_vector,
|
|
264
|
+
limit=limit,
|
|
265
|
+
score_threshold=score_threshold,
|
|
266
|
+
**kwargs
|
|
267
|
+
)
|
|
268
|
+
|
|
269
|
+
return [
|
|
270
|
+
{
|
|
271
|
+
"id": r.id,
|
|
272
|
+
"score": r.score,
|
|
273
|
+
"payload": r.payload,
|
|
274
|
+
}
|
|
275
|
+
for r in results
|
|
276
|
+
]
|
|
277
|
+
|
|
278
|
+
except Exception as e:
|
|
279
|
+
logger.error(f"向量搜索失败: {e}")
|
|
280
|
+
return []
|
|
281
|
+
|
|
282
|
+
async def get_collection_info(self, collection_name: str) -> Optional[Dict[str, Any]]:
|
|
283
|
+
"""获取集合信息"""
|
|
284
|
+
if not self._client:
|
|
285
|
+
return None
|
|
286
|
+
|
|
287
|
+
try:
|
|
288
|
+
info = await self._client.get_collection(collection_name)
|
|
289
|
+
return {
|
|
290
|
+
"name": collection_name,
|
|
291
|
+
"vectors_count": info.vectors_count,
|
|
292
|
+
"points_count": info.points_count,
|
|
293
|
+
"status": info.status,
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
except Exception as e:
|
|
297
|
+
logger.error(f"获取集合信息失败: {e}")
|
|
298
|
+
return None
|
|
299
|
+
|
|
300
|
+
async def get_stats(self) -> Dict[str, Any]:
|
|
301
|
+
"""获取统计信息"""
|
|
302
|
+
if not self._client or not self.is_available:
|
|
303
|
+
return {}
|
|
304
|
+
|
|
305
|
+
try:
|
|
306
|
+
collections = await self._client.get_collections()
|
|
307
|
+
|
|
308
|
+
stats = {
|
|
309
|
+
"collections_count": len(collections.collections),
|
|
310
|
+
"collections": []
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
for collection in collections.collections:
|
|
314
|
+
info = await self.get_collection_info(collection.name)
|
|
315
|
+
if info:
|
|
316
|
+
stats["collections"].append(info)
|
|
317
|
+
|
|
318
|
+
return stats
|
|
319
|
+
|
|
320
|
+
except Exception as e:
|
|
321
|
+
logger.error(f"获取统计信息失败: {e}")
|
|
322
|
+
return {}
|