feat(qdrant): 新增 Qdrant 向量库连接支持

- 后端新增 Qdrant REST 连接、collection 元数据、scroll/search 查询与 upsert/delete/payload 更新

- 前端新增 Qdrant 类型、连接配置、图标、方言和能力矩阵

- 测试覆盖 mock REST、真实服务 smoke 和前端配置

Refs #555
This commit is contained in:
Syngnat
2026-06-13 17:03:20 +08:00
parent 56126e22f2
commit c805b16fcd
21 changed files with 1434 additions and 11 deletions

View File

@@ -33,10 +33,10 @@ describe('ConnectionModal data source registry', () => {
expect(source).toContain('type === "elasticsearch"');
expect(source).toContain("return '支持索引浏览、Mapping 检查、JSON DSL 和 query_string 查询';");
expect(source).toContain(
'type === "clickhouse" ? "default" : (type === "redis" || type === "elasticsearch" || type === "chroma") ? "" : "root";',
'type === "clickhouse" ? "default" : (type === "redis" || type === "elasticsearch" || type === "chroma" || type === "qdrant") ? "" : "root";',
);
expect(source).toContain(
'placeholder={(dbType === "elasticsearch" || dbType === "chroma") ? "未开启认证可留空" : undefined}',
'placeholder={(dbType === "elasticsearch" || dbType === "chroma" || dbType === "qdrant") ? "未开启认证可留空" : undefined}',
);
expect(source).toContain('label="显示数据库 (留空显示全部)"');
});
@@ -52,6 +52,18 @@ describe('ConnectionModal data source registry', () => {
expect(source).toContain('return "http://127.0.0.1:8000/default_database?tenant=default_tenant";');
expect(source).toContain('return "tenant=default_tenant&apiKey=...";');
});
it('exposes Qdrant in the create-connection picker with vector defaults', () => {
expect(source).toContain("case 'qdrant':");
expect(source).toContain('return 6333;');
expect(source).toContain('qdrant: ["http", "https", "qdrant"]');
expect(source).toContain("key: 'qdrant'");
expect(source).toContain("name: 'Qdrant'");
expect(source).toContain('type === "qdrant"');
expect(source).toContain("return 'Collection 浏览、向量搜索和 Payload 过滤';");
expect(source).toContain('return "http://127.0.0.1:6333";');
expect(source).toContain('return "apiKey=...";');
});
});
describe('ConnectionModal Redis Sentinel configuration', () => {

View File

@@ -1789,7 +1789,7 @@ const ConnectionModal: React.FC<{
parsedValues.useSSL = false;
parsedValues.sslMode = "disable";
}
} else if (type === "chroma") {
} else if (type === "chroma" || type === "qdrant") {
const tls = String(
parsed.params.get("tls") ||
parsed.params.get("ssl") ||
@@ -1870,6 +1870,9 @@ const ConnectionModal: React.FC<{
if (dbType === "chroma") {
return "http://127.0.0.1:8000/default_database?tenant=default_tenant";
}
if (dbType === "qdrant") {
return "http://127.0.0.1:6333";
}
if (dbType === "redis") {
return "redis://:pass@127.0.0.1:6379,127.0.0.2:6379/0?topology=cluster 或 redis://:pass@10.0.0.1:26379,10.0.0.2:26379/0?topology=sentinel&master=mymaster";
}
@@ -1913,6 +1916,8 @@ const ConnectionModal: React.FC<{
return "retryWrites=true&readPreference=secondaryPreferred";
case "chroma":
return "tenant=default_tenant&apiKey=...";
case "qdrant":
return "apiKey=...";
case "dameng":
return "schema=SYSDBA";
case "tdengine":
@@ -2054,7 +2059,7 @@ const ConnectionModal: React.FC<{
const scheme =
type === "postgres"
? "postgresql"
: type === "chroma"
: type === "chroma" || type === "qdrant"
? values.useSSL
? "https"
: "http"
@@ -2108,7 +2113,7 @@ const ConnectionModal: React.FC<{
if (mode === "skip-verify" || mode === "preferred") {
params.set("skip_verify", "true");
}
} else if (type === "chroma") {
} else if (type === "chroma" || type === "qdrant") {
if (mode === "skip-verify" || mode === "preferred") {
params.set("skip_verify", "true");
}
@@ -3705,7 +3710,7 @@ const ConnectionModal: React.FC<{
});
} else if (type !== "custom") {
const defaultUser =
type === "clickhouse" ? "default" : (type === "redis" || type === "elasticsearch" || type === "chroma") ? "" : "root";
type === "clickhouse" ? "default" : (type === "redis" || type === "elasticsearch" || type === "chroma" || type === "qdrant") ? "" : "root";
const sslCapableType = supportsSSLForType(type);
setUseSSL(false);
setUseHttpTunnel(false);
@@ -5001,13 +5006,13 @@ const ConnectionModal: React.FC<{
name="user"
label="用户名"
rules={
(dbType === "mongodb" || dbType === "elasticsearch" || dbType === "chroma")
(dbType === "mongodb" || dbType === "elasticsearch" || dbType === "chroma" || dbType === "qdrant")
? []
: [createUriAwareRequiredRule("请输入用户名")]
}
style={{ marginBottom: 0 }}
>
<Input {...noAutoCapInputProps} placeholder={(dbType === "elasticsearch" || dbType === "chroma") ? "未开启认证可留空" : undefined} />
<Input {...noAutoCapInputProps} placeholder={(dbType === "elasticsearch" || dbType === "chroma" || dbType === "qdrant") ? "未开启认证可留空" : undefined} />
</Form.Item>
<Form.Item
name="password"

View File

@@ -25,6 +25,13 @@ describe('DatabaseIcons', () => {
expect(markup).toContain('>Ch</text>');
});
it('includes Qdrant in the selectable database icons', () => {
expect(DB_ICON_TYPES).toContain('qdrant');
expect(getDbIconLabel('qdrant')).toBe('Qdrant');
const markup = renderToStaticMarkup(<>{getDbIcon('qdrant', undefined, 22)}</>);
expect(markup).toContain('>Qd</text>');
});
it('wraps database icons in a consistent frame for sidebar sizing', () => {
const mysqlMarkup = renderToStaticMarkup(<>{getDbIcon('mysql', undefined, 22)}</>);
const jvmMarkup = renderToStaticMarkup(<>{getDbIcon('jvm', undefined, 22)}</>);

View File

@@ -50,6 +50,7 @@ const DB_DEFAULT_COLORS: Record<string, string> = {
iris: '#1F6FEB',
tdengine: '#2962FF',
chroma: '#7C3AED',
qdrant: '#DC244C',
diros: '#0050B3',
starrocks: '#00A6A6',
sphinx: '#2F5D62',
@@ -182,6 +183,9 @@ const TDengineIcon: React.FC<DbIconProps> = ({ size = 16, color }) => (
const ChromaIcon: React.FC<DbIconProps> = ({ size = 16, color }) => (
<ColorBadge size={size} color={color || DB_DEFAULT_COLORS.chroma} label="Ch" />
);
const QdrantIcon: React.FC<DbIconProps> = ({ size = 16, color }) => (
<ColorBadge size={size} color={color || DB_DEFAULT_COLORS.qdrant} label="Qd" />
);
const JVMIcon: React.FC<DbIconProps> = ({ size = 16, color }) => (
<ColorBadge size={size} color={color || DB_DEFAULT_COLORS.jvm} label="JVM" />
);
@@ -236,6 +240,7 @@ const DB_ICON_MAP: Record<string, React.FC<DbIconProps>> = {
iris: IrisIcon,
tdengine: TDengineIcon,
chroma: ChromaIcon,
qdrant: QdrantIcon,
elasticsearch: ElasticsearchIcon,
custom: CustomIcon,
};
@@ -244,7 +249,7 @@ const DB_ICON_MAP: Record<string, React.FC<DbIconProps>> = {
export const DB_ICON_TYPES: string[] = [
'mysql', 'mariadb', 'oceanbase', 'postgres', 'redis', 'mongodb', 'jvm',
'oracle', 'sqlserver', 'sqlite', 'duckdb', 'clickhouse', 'starrocks',
'kingbase', 'dameng', 'vastbase', 'opengauss', 'highgo', 'iris', 'tdengine', 'chroma', 'elasticsearch', 'custom',
'kingbase', 'dameng', 'vastbase', 'opengauss', 'highgo', 'iris', 'tdengine', 'chroma', 'qdrant', 'elasticsearch', 'custom',
];
/** 该类型是否有品牌 SVG 文件 */
@@ -268,6 +273,7 @@ export const getDbIconLabel = (type: string): string => {
duckdb: 'DuckDB', kingbase: '金仓', dameng: '达梦',
vastbase: 'VastBase', opengauss: 'OpenGauss', highgo: '瀚高', iris: 'InterSystems IRIS', tdengine: 'TDengine',
chroma: 'Chroma',
qdrant: 'Qdrant',
elasticsearch: 'Elasticsearch',
custom: '自定义',
};