sdpy-kit 0.1.0__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.
- kit/__init__.py +68 -0
- kit/ai/__init__.py +47 -0
- kit/ai/base_service.py +94 -0
- kit/ai/chat_service.py +110 -0
- kit/ai/embedding_service.py +133 -0
- kit/ai/exceptions.py +57 -0
- kit/ai/failover.py +244 -0
- kit/ai/image_service.py +99 -0
- kit/ai/model_builder.py +119 -0
- kit/ai/model_client.py +214 -0
- kit/ai/pool.py +411 -0
- kit/ai/profiles.py +373 -0
- kit/ai/rerank_service.py +93 -0
- kit/ai/runner.py +580 -0
- kit/ai/types.py +28 -0
- kit/common_kit.py +221 -0
- kit/config/__init__.py +44 -0
- kit/config/kit_config.py +147 -0
- kit/config/log_config.py +134 -0
- kit/db/__init__.py +70 -0
- kit/db/base_db_kit.py +406 -0
- kit/db/crud.py +361 -0
- kit/db/engine.py +230 -0
- kit/db/exceptions.py +34 -0
- kit/db/models/__init__.py +33 -0
- kit/db/models/base.py +56 -0
- kit/db/mysql_kit.py +89 -0
- kit/db/pg_kit.py +87 -0
- kit/db/session.py +85 -0
- kit/db/sqlite_kit.py +284 -0
- kit/db/transaction.py +109 -0
- kit/doc_kit.py +1502 -0
- kit/file_kit.py +429 -0
- kit/mcp_kit.py +547 -0
- kit/mineru_kit.py +918 -0
- kit/redis_kit.py +392 -0
- kit/zvec_kit.py +671 -0
- sdpy_kit-0.1.0.dist-info/METADATA +128 -0
- sdpy_kit-0.1.0.dist-info/RECORD +40 -0
- sdpy_kit-0.1.0.dist-info/WHEEL +4 -0
kit/db/models/base.py
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Copyright (c) 2021-2026 Clark Chang. All Rights Reserved.
|
|
3
|
+
|
|
4
|
+
Licensed under the Apache License, Version 2.0 (the "License");
|
|
5
|
+
you may not use this file except in compliance with the License.
|
|
6
|
+
You may obtain a copy of the License at
|
|
7
|
+
|
|
8
|
+
http://www.apache.org/licenses/LICENSE-2.0
|
|
9
|
+
|
|
10
|
+
Unless required by applicable law or agreed to in writing, software
|
|
11
|
+
distributed under the License is distributed on an "AS IS" BASIS,
|
|
12
|
+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
13
|
+
See the License for the specific language governing permissions and
|
|
14
|
+
limitations under the License.
|
|
15
|
+
|
|
16
|
+
Project: sdpy
|
|
17
|
+
Author: Clark Chang
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
# date: 2026-08-23
|
|
21
|
+
"""SQLModel 基础模型混入类
|
|
22
|
+
|
|
23
|
+
提供 TimestampMixin(时间戳)、SoftDeleteMixin(软删除)、AuditMixin(审计字段)。
|
|
24
|
+
default_factory 统一使用 datetime.now(UTC),避免废弃的 utcnow()。
|
|
25
|
+
"""
|
|
26
|
+
|
|
27
|
+
from datetime import UTC, datetime
|
|
28
|
+
from typing import Optional
|
|
29
|
+
|
|
30
|
+
from sqlmodel import Field, SQLModel
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def _utcnow() -> datetime:
|
|
34
|
+
"""获取当前 UTC 时间"""
|
|
35
|
+
return datetime.now(UTC)
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
class TimestampMixin(SQLModel):
|
|
39
|
+
"""时间戳混入:自动添加 created_at 和 updated_at 字段"""
|
|
40
|
+
|
|
41
|
+
created_at: datetime = Field(default_factory=_utcnow, nullable=False)
|
|
42
|
+
updated_at: Optional[datetime] = Field(default=None, nullable=True)
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
class SoftDeleteMixin(SQLModel):
|
|
46
|
+
"""软删除混入:添加 is_deleted 和 deleted_at 字段"""
|
|
47
|
+
|
|
48
|
+
is_deleted: bool = Field(default=False, nullable=False)
|
|
49
|
+
deleted_at: Optional[datetime] = Field(default=None, nullable=True)
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
class AuditMixin(TimestampMixin):
|
|
53
|
+
"""审计混入:在 TimestampMixin 基础上添加操作人字段"""
|
|
54
|
+
|
|
55
|
+
created_by: Optional[str] = Field(default=None, max_length=100)
|
|
56
|
+
updated_by: Optional[str] = Field(default=None, max_length=100)
|
kit/db/mysql_kit.py
ADDED
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Copyright (c) 2021-2026 Clark Chang. All Rights Reserved.
|
|
3
|
+
|
|
4
|
+
Licensed under the Apache License, Version 2.0 (the "License");
|
|
5
|
+
you may not use this file except in compliance with the License.
|
|
6
|
+
You may obtain a copy of the License at
|
|
7
|
+
|
|
8
|
+
http://www.apache.org/licenses/LICENSE-2.0
|
|
9
|
+
|
|
10
|
+
Unless required by applicable law or agreed to in writing, software
|
|
11
|
+
distributed under the License is distributed on an "AS IS" BASIS,
|
|
12
|
+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
13
|
+
See the License for the specific language governing permissions and
|
|
14
|
+
limitations under the License.
|
|
15
|
+
|
|
16
|
+
Project: sdpy
|
|
17
|
+
Author: Clark Chang
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
from __future__ import annotations
|
|
21
|
+
|
|
22
|
+
# date: 2026-08-22
|
|
23
|
+
"""MySQL 数据库工具类
|
|
24
|
+
|
|
25
|
+
基于 kit.db 统一数据库层封装,提供开箱即用的服务方法。
|
|
26
|
+
业务代码无需管理 session/transaction,直接调用即可。
|
|
27
|
+
|
|
28
|
+
本类继承 BaseDbKit,复用全部共享实现(连接管理 / ORM / 原生 SQL / 事务 /
|
|
29
|
+
表操作),仅声明 DB_TYPE 并覆写 MySQL 专属的 get_table_info。
|
|
30
|
+
|
|
31
|
+
使用方式::
|
|
32
|
+
|
|
33
|
+
# 单条操作(自动管理事务)
|
|
34
|
+
user = MySqlKit.create(UserModel, {"name": "张三", "email": "zs@example.com"})
|
|
35
|
+
users = MySqlKit.list(UserModel, skip=0, limit=10)
|
|
36
|
+
MySqlKit.update(UserModel, user.id, {"name": "李四"})
|
|
37
|
+
|
|
38
|
+
# 原生 SQL 操作
|
|
39
|
+
rows = MySqlKit.query("SELECT * FROM users WHERE id = :id", {"id": 1})
|
|
40
|
+
|
|
41
|
+
# 多操作事务
|
|
42
|
+
with MySqlKit.transaction() as session:
|
|
43
|
+
crud = CRUDBase(UserModel)
|
|
44
|
+
crud.create(session, {"name": "test"})
|
|
45
|
+
# 退出 with 时统一提交
|
|
46
|
+
"""
|
|
47
|
+
|
|
48
|
+
from kit.config.log_config import get_default_logger
|
|
49
|
+
from kit.db.base_db_kit import BaseDbKit
|
|
50
|
+
from kit.db.engine import MYSQL
|
|
51
|
+
|
|
52
|
+
logger = get_default_logger(__name__)
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
class MySqlKit(BaseDbKit):
|
|
56
|
+
"""MySQL 数据库工具类
|
|
57
|
+
|
|
58
|
+
基于 kit.db 统一数据库层,封装为业务可直调的服务方法。
|
|
59
|
+
所有 ORM 方法内部使用 session_scope 管理事务(自动提交/回滚),
|
|
60
|
+
业务代码无需手动管理 session。
|
|
61
|
+
|
|
62
|
+
对于多操作事务场景,请使用 transaction() 上下文管理器。
|
|
63
|
+
"""
|
|
64
|
+
|
|
65
|
+
DB_TYPE = MYSQL
|
|
66
|
+
|
|
67
|
+
@classmethod
|
|
68
|
+
def get_table_info(cls, table_name: str) -> list[dict[str, object]]:
|
|
69
|
+
"""获取表的列信息
|
|
70
|
+
|
|
71
|
+
Args:
|
|
72
|
+
table_name: 表名
|
|
73
|
+
|
|
74
|
+
Returns:
|
|
75
|
+
列信息字典列表(column_name, data_type, is_nullable 等)
|
|
76
|
+
"""
|
|
77
|
+
sql = """
|
|
78
|
+
SELECT
|
|
79
|
+
COLUMN_NAME as column_name,
|
|
80
|
+
DATA_TYPE as data_type,
|
|
81
|
+
IS_NULLABLE as is_nullable,
|
|
82
|
+
COLUMN_DEFAULT as column_default,
|
|
83
|
+
COLUMN_COMMENT as column_comment
|
|
84
|
+
FROM INFORMATION_SCHEMA.COLUMNS
|
|
85
|
+
WHERE TABLE_NAME = :table_name
|
|
86
|
+
AND TABLE_SCHEMA = DATABASE()
|
|
87
|
+
ORDER BY ORDINAL_POSITION
|
|
88
|
+
"""
|
|
89
|
+
return cls.query(sql, {"table_name": table_name})
|
kit/db/pg_kit.py
ADDED
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Copyright (c) 2021-2026 Clark Chang. All Rights Reserved.
|
|
3
|
+
|
|
4
|
+
Licensed under the Apache License, Version 2.0 (the "License");
|
|
5
|
+
you may not use this file except in compliance with the License.
|
|
6
|
+
You may obtain a copy of the License at
|
|
7
|
+
|
|
8
|
+
http://www.apache.org/licenses/LICENSE-2.0
|
|
9
|
+
|
|
10
|
+
Unless required by applicable law or agreed to in writing, software
|
|
11
|
+
distributed under the License is distributed on an "AS IS" BASIS,
|
|
12
|
+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
13
|
+
See the License for the specific language governing permissions and
|
|
14
|
+
limitations under the License.
|
|
15
|
+
|
|
16
|
+
Project: sdpy
|
|
17
|
+
Author: Clark Chang
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
from __future__ import annotations
|
|
21
|
+
|
|
22
|
+
# date: 2026-08-22
|
|
23
|
+
"""PostgreSQL 数据库工具类
|
|
24
|
+
|
|
25
|
+
基于 kit.db 统一数据库层封装,提供开箱即用的服务方法。
|
|
26
|
+
业务代码无需管理 session/transaction,直接调用即可。
|
|
27
|
+
|
|
28
|
+
本类继承 BaseDbKit,复用全部共享实现(连接管理 / ORM / 原生 SQL / 事务 /
|
|
29
|
+
表操作),仅声明 DB_TYPE 并覆写 PostgreSQL 专属的 get_table_info。
|
|
30
|
+
|
|
31
|
+
使用方式::
|
|
32
|
+
|
|
33
|
+
# 单条操作(自动管理事务)
|
|
34
|
+
user = PgKit.create(UserModel, {"name": "张三", "email": "zs@example.com"})
|
|
35
|
+
users = PgKit.list(UserModel, skip=0, limit=10)
|
|
36
|
+
PgKit.update(UserModel, user.id, {"name": "李四"})
|
|
37
|
+
|
|
38
|
+
# 原生 SQL 操作
|
|
39
|
+
rows = PgKit.query("SELECT * FROM users WHERE active = :active", {"active": True})
|
|
40
|
+
|
|
41
|
+
# 多操作事务
|
|
42
|
+
with PgKit.transaction() as session:
|
|
43
|
+
crud = CRUDBase(UserModel)
|
|
44
|
+
crud.create(session, {"name": "test"})
|
|
45
|
+
# 退出 with 时统一提交
|
|
46
|
+
"""
|
|
47
|
+
|
|
48
|
+
from kit.config.log_config import get_default_logger
|
|
49
|
+
from kit.db.base_db_kit import BaseDbKit
|
|
50
|
+
from kit.db.engine import POSTGRESQL
|
|
51
|
+
|
|
52
|
+
logger = get_default_logger(__name__)
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
class PgKit(BaseDbKit):
|
|
56
|
+
"""PostgreSQL 数据库工具类
|
|
57
|
+
|
|
58
|
+
基于 kit.db 统一数据库层,封装为业务可直调的服务方法。
|
|
59
|
+
所有 ORM 方法内部使用 session_scope 管理事务(自动提交/回滚),
|
|
60
|
+
业务代码无需手动管理 session。
|
|
61
|
+
|
|
62
|
+
对于多操作事务场景,请使用 transaction() 上下文管理器。
|
|
63
|
+
"""
|
|
64
|
+
|
|
65
|
+
DB_TYPE = POSTGRESQL
|
|
66
|
+
|
|
67
|
+
@classmethod
|
|
68
|
+
def get_table_info(cls, table_name: str) -> list[dict[str, object]]:
|
|
69
|
+
"""获取表的列信息
|
|
70
|
+
|
|
71
|
+
Args:
|
|
72
|
+
table_name: 表名
|
|
73
|
+
|
|
74
|
+
Returns:
|
|
75
|
+
列信息字典列表(column_name, data_type, is_nullable, column_default)
|
|
76
|
+
"""
|
|
77
|
+
sql = """
|
|
78
|
+
SELECT
|
|
79
|
+
column_name,
|
|
80
|
+
data_type,
|
|
81
|
+
is_nullable,
|
|
82
|
+
column_default
|
|
83
|
+
FROM information_schema.columns
|
|
84
|
+
WHERE table_name = :table_name
|
|
85
|
+
ORDER BY ordinal_position
|
|
86
|
+
"""
|
|
87
|
+
return cls.query(sql, {"table_name": table_name})
|
kit/db/session.py
ADDED
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Copyright (c) 2021-2026 Clark Chang. All Rights Reserved.
|
|
3
|
+
|
|
4
|
+
Licensed under the Apache License, Version 2.0 (the "License");
|
|
5
|
+
you may not use this file except in compliance with the License.
|
|
6
|
+
You may obtain a copy of the License at
|
|
7
|
+
|
|
8
|
+
http://www.apache.org/licenses/LICENSE-2.0
|
|
9
|
+
|
|
10
|
+
Unless required by applicable law or agreed to in writing, software
|
|
11
|
+
distributed under the License is distributed on an "AS IS" BASIS,
|
|
12
|
+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
13
|
+
See the License for the specific language governing permissions and
|
|
14
|
+
limitations under the License.
|
|
15
|
+
|
|
16
|
+
Project: sdpy
|
|
17
|
+
Author: Clark Chang
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
# date: 2025-04-19
|
|
21
|
+
"""统一的会话管理模块
|
|
22
|
+
|
|
23
|
+
提供唯一的会话入口 session_scope,自动管理提交/回滚/关闭。
|
|
24
|
+
CRUD 操作不应在内部调用 commit/rollback,由 session_scope 统一管理事务边界。
|
|
25
|
+
"""
|
|
26
|
+
|
|
27
|
+
from contextlib import contextmanager
|
|
28
|
+
from typing import Generator
|
|
29
|
+
|
|
30
|
+
from sqlmodel import Session
|
|
31
|
+
|
|
32
|
+
from kit.config.log_config import get_default_logger
|
|
33
|
+
from kit.db.engine import get_engine
|
|
34
|
+
|
|
35
|
+
logger = get_default_logger(__name__)
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
@contextmanager
|
|
39
|
+
def session_scope(
|
|
40
|
+
db_type: str | None = None,
|
|
41
|
+
*,
|
|
42
|
+
autocommit: bool = True,
|
|
43
|
+
) -> Generator[Session, None, None]:
|
|
44
|
+
"""会话作用域上下文管理器(唯一的会话入口)
|
|
45
|
+
|
|
46
|
+
正常退出时自动提交(autocommit=True),异常时自动回滚,最终关闭会话。
|
|
47
|
+
|
|
48
|
+
Args:
|
|
49
|
+
db_type: 数据库类型(postgresql/mysql/sqlite),None 时使用配置默认值
|
|
50
|
+
autocommit: 是否在正常退出时自动提交事务
|
|
51
|
+
|
|
52
|
+
Yields:
|
|
53
|
+
SQLModel Session 实例
|
|
54
|
+
|
|
55
|
+
用法::
|
|
56
|
+
|
|
57
|
+
with session_scope() as session:
|
|
58
|
+
user = User(name="test")
|
|
59
|
+
session.add(user)
|
|
60
|
+
# 退出时自动提交
|
|
61
|
+
"""
|
|
62
|
+
engine = get_engine(db_type)
|
|
63
|
+
session = Session(engine)
|
|
64
|
+
db_name = db_type or "默认"
|
|
65
|
+
|
|
66
|
+
try:
|
|
67
|
+
yield session
|
|
68
|
+
if autocommit:
|
|
69
|
+
session.commit()
|
|
70
|
+
logger.info(f"✅ [{db_name}] 会话事务已提交")
|
|
71
|
+
except Exception as e:
|
|
72
|
+
session.rollback()
|
|
73
|
+
logger.warning(f"⚠️ [{db_name}] 会话事务已回滚,原因: {e}")
|
|
74
|
+
raise
|
|
75
|
+
finally:
|
|
76
|
+
session.close()
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def get_session(db_type: str | None = None) -> Session:
|
|
80
|
+
"""获取裸 Session(调用者需自行管理生命周期)
|
|
81
|
+
|
|
82
|
+
建议优先使用 session_scope 上下文管理器。
|
|
83
|
+
"""
|
|
84
|
+
engine = get_engine(db_type)
|
|
85
|
+
return Session(engine)
|
kit/db/sqlite_kit.py
ADDED
|
@@ -0,0 +1,284 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Copyright (c) 2021-2026 Clark Chang. All Rights Reserved.
|
|
3
|
+
|
|
4
|
+
Licensed under the Apache License, Version 2.0 (the "License");
|
|
5
|
+
you may not use this file except in compliance with the License.
|
|
6
|
+
You may obtain a copy of the License at
|
|
7
|
+
|
|
8
|
+
http://www.apache.org/licenses/LICENSE-2.0
|
|
9
|
+
|
|
10
|
+
Unless required by applicable law or agreed to in writing, software
|
|
11
|
+
distributed under the License is distributed on an "AS IS" BASIS,
|
|
12
|
+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
13
|
+
See the License for the specific language governing permissions and
|
|
14
|
+
limitations under the License.
|
|
15
|
+
|
|
16
|
+
Project: sdpy
|
|
17
|
+
Author: Clark Chang
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
from __future__ import annotations
|
|
21
|
+
|
|
22
|
+
# date: 2026-08-22
|
|
23
|
+
"""SQLite 数据库工具类
|
|
24
|
+
|
|
25
|
+
基于 kit.db 统一数据库层封装,提供开箱即用的服务方法。
|
|
26
|
+
业务代码无需管理 session/transaction,直接调用即可。
|
|
27
|
+
|
|
28
|
+
本类继承 BaseDbKit,复用全部共享实现(连接管理 / ORM / 原生 SQL / 事务 /
|
|
29
|
+
表操作),并额外提供 SQLite 专属的向量操作(基于 sqlite-vector 扩展)。
|
|
30
|
+
|
|
31
|
+
使用方式::
|
|
32
|
+
|
|
33
|
+
# 单条操作(自动管理事务)
|
|
34
|
+
user = SQLiteKit.create(UserModel, {"name": "张三"})
|
|
35
|
+
users = SQLiteKit.list(UserModel, skip=0, limit=10)
|
|
36
|
+
SQLiteKit.update(UserModel, user.id, {"name": "李四"})
|
|
37
|
+
|
|
38
|
+
# 向量操作
|
|
39
|
+
SQLiteKit.create_vector_table("images", dims=384)
|
|
40
|
+
SQLiteKit.insert_vectors("images", [[0.1, 0.2, ...], ...])
|
|
41
|
+
rows = SQLiteKit.search_vectors("images", [0.1, 0.2, ...], top_k=5)
|
|
42
|
+
"""
|
|
43
|
+
|
|
44
|
+
import numpy as np
|
|
45
|
+
from sqlalchemy import text
|
|
46
|
+
|
|
47
|
+
from kit.config.log_config import get_default_logger
|
|
48
|
+
from kit.db.base_db_kit import BaseDbKit
|
|
49
|
+
from kit.db.engine import SQLITE, get_engine
|
|
50
|
+
from kit.db.session import session_scope
|
|
51
|
+
|
|
52
|
+
logger = get_default_logger(__name__)
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
class SQLiteKit(BaseDbKit):
|
|
56
|
+
"""SQLite 数据库工具类
|
|
57
|
+
|
|
58
|
+
基于 kit.db 统一数据库层,封装为业务可直调的服务方法。
|
|
59
|
+
所有 ORM 方法内部使用 session_scope 管理事务(自动提交/回滚),
|
|
60
|
+
业务代码无需手动管理 session。
|
|
61
|
+
|
|
62
|
+
对于多操作事务场景,请使用 transaction() 上下文管理器。
|
|
63
|
+
"""
|
|
64
|
+
|
|
65
|
+
DB_TYPE = SQLITE
|
|
66
|
+
|
|
67
|
+
@classmethod
|
|
68
|
+
def raw_connection(cls):
|
|
69
|
+
"""获取原生 DBAPI 连接(sqlite-vector 扩展已由引擎自动加载)
|
|
70
|
+
|
|
71
|
+
用于执行 sqlite-vector 特殊函数(vector_init / vector_quantize /
|
|
72
|
+
vector_quantize_preload 等)与静态 DDL。调用方负责关闭连接。
|
|
73
|
+
"""
|
|
74
|
+
return get_engine(cls.DB_TYPE).raw_connection()
|
|
75
|
+
|
|
76
|
+
@classmethod
|
|
77
|
+
def create_raw_table(
|
|
78
|
+
cls, table_name: str, columns: dict[str, str], if_not_exists: bool = True
|
|
79
|
+
) -> None:
|
|
80
|
+
"""根据「列名 → 列定义」字典创建普通表
|
|
81
|
+
|
|
82
|
+
Args:
|
|
83
|
+
table_name: 表名
|
|
84
|
+
columns: 列定义字典,如 {"id": "TEXT PRIMARY KEY", "name": "TEXT NOT NULL"}
|
|
85
|
+
if_not_exists: 是否附加 IF NOT EXISTS(默认 True)
|
|
86
|
+
"""
|
|
87
|
+
column_defs = ", ".join(f"{name} {definition}" for name, definition in columns.items())
|
|
88
|
+
if_not_exists_sql = "IF NOT EXISTS " if if_not_exists else ""
|
|
89
|
+
sql = f"CREATE TABLE {if_not_exists_sql}{table_name} ({column_defs})"
|
|
90
|
+
with get_engine(cls.DB_TYPE).connect() as conn:
|
|
91
|
+
conn.execute(text(sql))
|
|
92
|
+
conn.commit()
|
|
93
|
+
logger.info(f"✅ 创建表 {table_name}")
|
|
94
|
+
|
|
95
|
+
@classmethod
|
|
96
|
+
def get_table_info(cls, table_name: str) -> list[dict[str, object]]:
|
|
97
|
+
"""获取表的列信息
|
|
98
|
+
|
|
99
|
+
Args:
|
|
100
|
+
table_name: 表名
|
|
101
|
+
|
|
102
|
+
Returns:
|
|
103
|
+
列信息字典列表(column_name, data_type, is_nullable, column_default)
|
|
104
|
+
"""
|
|
105
|
+
sql = """
|
|
106
|
+
SELECT
|
|
107
|
+
name as column_name,
|
|
108
|
+
type as data_type,
|
|
109
|
+
CASE WHEN "notnull" = 0 THEN 'YES' ELSE 'NO' END as is_nullable,
|
|
110
|
+
dflt_value as column_default
|
|
111
|
+
FROM pragma_table_info(:table_name)
|
|
112
|
+
ORDER BY cid
|
|
113
|
+
"""
|
|
114
|
+
return cls.query(sql, {"table_name": table_name})
|
|
115
|
+
|
|
116
|
+
# ==================== 向量操作 ====================
|
|
117
|
+
|
|
118
|
+
@classmethod
|
|
119
|
+
def create_vector_table(
|
|
120
|
+
cls,
|
|
121
|
+
table_name: str,
|
|
122
|
+
id_col: str = "id",
|
|
123
|
+
dims: int = 384,
|
|
124
|
+
extra_cols: list[dict[str, str]] | None = None,
|
|
125
|
+
) -> None:
|
|
126
|
+
"""创建包含向量列的表
|
|
127
|
+
|
|
128
|
+
Args:
|
|
129
|
+
table_name: 表名
|
|
130
|
+
id_col: 主键列名(INTEGER PRIMARY KEY)
|
|
131
|
+
dims: 向量维度(默认 384)
|
|
132
|
+
extra_cols: 额外列定义,每项为 {"name": str, "type": str},
|
|
133
|
+
如 [{"name": "label", "type": "TEXT"}]
|
|
134
|
+
|
|
135
|
+
表结构示例::
|
|
136
|
+
|
|
137
|
+
CREATE TABLE images (
|
|
138
|
+
id INTEGER PRIMARY KEY,
|
|
139
|
+
embedding BLOB, -- FLOAT32 向量
|
|
140
|
+
label TEXT
|
|
141
|
+
);
|
|
142
|
+
"""
|
|
143
|
+
extra_defs = ""
|
|
144
|
+
if extra_cols:
|
|
145
|
+
extra_defs = ", " + ", ".join(
|
|
146
|
+
f"{c['name']} {c['type']}" for c in extra_cols
|
|
147
|
+
)
|
|
148
|
+
|
|
149
|
+
sql = f"""
|
|
150
|
+
CREATE TABLE IF NOT EXISTS {table_name} (
|
|
151
|
+
{id_col} INTEGER PRIMARY KEY,
|
|
152
|
+
embedding BLOB{extra_defs}
|
|
153
|
+
)
|
|
154
|
+
"""
|
|
155
|
+
with session_scope(cls.DB_TYPE, autocommit=False) as session:
|
|
156
|
+
session.execute(text(sql))
|
|
157
|
+
logger.info(f"✅ 创建向量表 {table_name}(维度={dims})")
|
|
158
|
+
|
|
159
|
+
@classmethod
|
|
160
|
+
def insert_vectors(
|
|
161
|
+
cls,
|
|
162
|
+
table_name: str,
|
|
163
|
+
vectors: list[list[float]],
|
|
164
|
+
ids: list[int] | None = None,
|
|
165
|
+
extra_data: list[dict[str, object]] | None = None,
|
|
166
|
+
) -> int:
|
|
167
|
+
"""批量插入向量数据
|
|
168
|
+
|
|
169
|
+
Args:
|
|
170
|
+
table_name: 表名
|
|
171
|
+
vectors: 向量列表,每个向量为 float 列表
|
|
172
|
+
ids: 可选的 ID 列表(长度需与 vectors 一致),None 时自动递增
|
|
173
|
+
extra_data: 额外列数据列表(长度需与 vectors 一致)
|
|
174
|
+
|
|
175
|
+
Returns:
|
|
176
|
+
插入的行数
|
|
177
|
+
"""
|
|
178
|
+
if ids is not None and len(ids) != len(vectors):
|
|
179
|
+
raise ValueError("ids 长度必须与 vectors 一致")
|
|
180
|
+
if extra_data is not None and len(extra_data) != len(vectors):
|
|
181
|
+
raise ValueError("extra_data 长度必须与 vectors 一致")
|
|
182
|
+
|
|
183
|
+
engine = get_engine(cls.DB_TYPE)
|
|
184
|
+
with engine.connect() as conn:
|
|
185
|
+
for i, vec in enumerate(vectors):
|
|
186
|
+
vec_bytes = np.array(vec, dtype=np.float32).tobytes()
|
|
187
|
+
if ids is not None:
|
|
188
|
+
_id = ids[i]
|
|
189
|
+
sql = f"INSERT INTO {table_name} (id, embedding) VALUES (:id, :vec)"
|
|
190
|
+
params: dict[str, object] = {"id": _id, "vec": vec_bytes}
|
|
191
|
+
else:
|
|
192
|
+
sql = f"INSERT INTO {table_name} (embedding) VALUES (:vec)"
|
|
193
|
+
params = {"vec": vec_bytes}
|
|
194
|
+
|
|
195
|
+
if extra_data is not None:
|
|
196
|
+
col_names = ", ".join(k for k in extra_data[i].keys())
|
|
197
|
+
col_vals = ", ".join(f":{k}" for k in extra_data[i].keys())
|
|
198
|
+
if ids is not None:
|
|
199
|
+
sql = (
|
|
200
|
+
f"INSERT INTO {table_name} "
|
|
201
|
+
f"(id, embedding, {col_names}) VALUES (:id, :vec, {col_vals})" # noqa: E501
|
|
202
|
+
)
|
|
203
|
+
params.update(extra_data[i])
|
|
204
|
+
else:
|
|
205
|
+
sql = (
|
|
206
|
+
f"INSERT INTO {table_name} "
|
|
207
|
+
f"(embedding, {col_names}) VALUES (:vec, {col_vals})"
|
|
208
|
+
)
|
|
209
|
+
params = {"vec": vec_bytes, **extra_data[i]}
|
|
210
|
+
|
|
211
|
+
conn.execute(text(sql), params)
|
|
212
|
+
conn.commit()
|
|
213
|
+
logger.info(f"✅ 插入 {len(vectors)} 条向量到 {table_name}")
|
|
214
|
+
return len(vectors)
|
|
215
|
+
|
|
216
|
+
@classmethod
|
|
217
|
+
def search_vectors(
|
|
218
|
+
cls,
|
|
219
|
+
table_name: str,
|
|
220
|
+
query_vector: list[float],
|
|
221
|
+
top_k: int = 10,
|
|
222
|
+
distance_type: str = "COSINE",
|
|
223
|
+
filter_sql: str | None = None,
|
|
224
|
+
) -> list[dict[str, object]]:
|
|
225
|
+
"""搜索最近邻向量
|
|
226
|
+
|
|
227
|
+
Args:
|
|
228
|
+
table_name: 表名
|
|
229
|
+
query_vector: 查询向量(float 列表)
|
|
230
|
+
top_k: 返回结果数
|
|
231
|
+
distance_type: 距离类型(COSINE / L2 / L1 / DOT / SQUARED_L2)
|
|
232
|
+
filter_sql: 可选的过滤条件 WHERE 子句(不含 WHERE 关键字),
|
|
233
|
+
如 "label = 'cat'"
|
|
234
|
+
|
|
235
|
+
Returns:
|
|
236
|
+
按距离升序排列的结果列表,每项为 {"id": int, "distance": float, ...额外列}
|
|
237
|
+
"""
|
|
238
|
+
engine = get_engine(cls.DB_TYPE)
|
|
239
|
+
query_bytes = np.array(query_vector, dtype=np.float32).tobytes()
|
|
240
|
+
|
|
241
|
+
with engine.connect() as conn:
|
|
242
|
+
# 初始化向量索引
|
|
243
|
+
conn.execute(
|
|
244
|
+
text(
|
|
245
|
+
"SELECT vector_init("
|
|
246
|
+
f"'{table_name}', 'embedding', 'type=FLOAT32,dimension={len(query_vector)}')" # noqa: E501
|
|
247
|
+
)
|
|
248
|
+
)
|
|
249
|
+
|
|
250
|
+
# 量化
|
|
251
|
+
conn.execute(text(f"SELECT vector_quantize('{table_name}', 'embedding')"))
|
|
252
|
+
|
|
253
|
+
where_clause = f"WHERE {filter_sql}" if filter_sql else ""
|
|
254
|
+
|
|
255
|
+
sql = f"""
|
|
256
|
+
SELECT e.id, v.distance
|
|
257
|
+
FROM {table_name} AS e
|
|
258
|
+
JOIN vector_quantize_scan('{table_name}', 'embedding', :query, {top_k}) AS v
|
|
259
|
+
ON e.id = v.rowid
|
|
260
|
+
{where_clause}
|
|
261
|
+
ORDER BY v.distance ASC
|
|
262
|
+
"""
|
|
263
|
+
result = conn.execute(text(sql), {"query": query_bytes})
|
|
264
|
+
rows = result.fetchall()
|
|
265
|
+
columns = result.keys()
|
|
266
|
+
data = [dict(zip(columns, row)) for row in rows]
|
|
267
|
+
logger.info(f"🔍 向量搜索 {table_name} → 返回 {len(data)} 条")
|
|
268
|
+
return data
|
|
269
|
+
|
|
270
|
+
@classmethod
|
|
271
|
+
def vector_version(cls) -> str:
|
|
272
|
+
"""获取 sqlite-vector 扩展版本
|
|
273
|
+
|
|
274
|
+
Returns:
|
|
275
|
+
版本号字符串,加载失败时返回 "unavailable"
|
|
276
|
+
"""
|
|
277
|
+
try:
|
|
278
|
+
engine = get_engine(cls.DB_TYPE)
|
|
279
|
+
with engine.connect() as conn:
|
|
280
|
+
row = conn.execute(text("SELECT vector_version()")).fetchone()
|
|
281
|
+
return str(row[0]) if row else "unknown"
|
|
282
|
+
except Exception as e:
|
|
283
|
+
logger.warning(f"⚠️ 获取 vector_version 失败: {e}")
|
|
284
|
+
return "unavailable"
|