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

@@ -14,6 +14,8 @@ describe('connectionDriverType', () => {
expect(normalizeDriverType('elastic')).toBe('elasticsearch');
expect(normalizeDriverType('chromadb')).toBe('chroma');
expect(normalizeDriverType('chroma-db')).toBe('chroma');
expect(normalizeDriverType('qdrantdb')).toBe('qdrant');
expect(normalizeDriverType('qdrant-db')).toBe('qdrant');
expect(normalizeDriverType('doris')).toBe('diros');
expect(normalizeDriverType('open-gauss')).toBe('opengauss');
expect(normalizeDriverType('InterSystemsIRIS')).toBe('iris');

View File

@@ -16,6 +16,7 @@ export const normalizeDriverType = (value: string): string => {
if (normalized === 'postgresql' || normalized === 'pg' || normalized === 'pq' || normalized === 'pgx') return 'postgres';
if (normalized === 'elastic') return 'elasticsearch';
if (normalized === 'chromadb' || normalized === 'chroma-db') return 'chroma';
if (normalized === 'qdrantdb' || normalized === 'qdrant-db') return 'qdrant';
if (normalized === 'doris') return 'diros';
if (
normalized === 'open_gauss' ||

View File

@@ -88,6 +88,7 @@ describe('connectionModalPresentation', () => {
'mongodb',
'elasticsearch',
'chroma',
'qdrant',
'redis',
'tdengine',
'custom',
@@ -165,6 +166,14 @@ describe('connectionModalPresentation', () => {
'credentials',
'databaseScope',
]);
expect(resolveConnectionConfigLayout('qdrant').sections).toEqual([
'identity',
'uri',
'target',
'service',
'credentials',
'databaseScope',
]);
});
it('uses localized labels for layout kinds shown in the modal', () => {

View File

@@ -252,7 +252,7 @@ export const resolveConnectionConfigLayout = (
],
};
}
if (type === 'chroma') {
if (type === 'chroma' || type === 'qdrant') {
return {
kind: 'vector',
sections: [

View File

@@ -18,6 +18,7 @@ describe('connectionTypeCapabilities', () => {
expect(singleHostUriSchemesByType.dameng).toEqual(['dameng', 'dm']);
expect(singleHostUriSchemesByType.elasticsearch).toEqual(['http', 'https']);
expect(singleHostUriSchemesByType.chroma).toEqual(['http', 'https', 'chroma']);
expect(singleHostUriSchemesByType.qdrant).toEqual(['http', 'https', 'qdrant']);
expect(singleHostUriSchemesByType.redis).toEqual(['redis']);
});
@@ -26,6 +27,7 @@ describe('connectionTypeCapabilities', () => {
expect(supportsSSLForType('MongoDB')).toBe(true);
expect(supportsSSLForType('elasticsearch')).toBe(true);
expect(supportsSSLForType('chroma')).toBe(true);
expect(supportsSSLForType('qdrant')).toBe(true);
expect(supportsSSLForType('tdengine')).toBe(true);
expect(supportsSSLForType('dameng')).toBe(true);
expect(supportsSSLForType('sqlite')).toBe(false);
@@ -40,6 +42,8 @@ describe('connectionTypeCapabilities', () => {
expect(supportsSSLClientCertificateForType('redis')).toBe(true);
expect(supportsSSLCAPathForType('chroma')).toBe(true);
expect(supportsSSLClientCertificateForType('chroma')).toBe(false);
expect(supportsSSLCAPathForType('qdrant')).toBe(true);
expect(supportsSSLClientCertificateForType('qdrant')).toBe(false);
});
it('detects postgres-compatible SSL parameter dialects', () => {
@@ -68,6 +72,7 @@ describe('connectionTypeCapabilities', () => {
expect(supportsConnectionParamsForType('tdengine')).toBe(true);
expect(supportsConnectionParamsForType('elasticsearch')).toBe(true);
expect(supportsConnectionParamsForType('chroma')).toBe(true);
expect(supportsConnectionParamsForType('qdrant')).toBe(true);
expect(supportsConnectionParamsForType('redis')).toBe(false);
expect(supportsConnectionParamsForType('sqlite')).toBe(false);
expect(supportsConnectionParamsForType('jvm')).toBe(false);

View File

@@ -13,6 +13,7 @@ export const singleHostUriSchemesByType: Record<string, string[]> = {
vastbase: ["vastbase"],
elasticsearch: ["http", "https"],
chroma: ["http", "https", "chroma"],
qdrant: ["http", "https", "qdrant"],
};
const normalizeConnectionType = (type: string) =>
@@ -42,6 +43,7 @@ const sslSupportedTypes = new Set([
"tdengine",
"elasticsearch",
"chroma",
"qdrant",
]);
export const supportsSSLForType = (type: string) =>
@@ -65,6 +67,7 @@ const sslCAPathSupportedTypes = new Set([
"redis",
"elasticsearch",
"chroma",
"qdrant",
]);
const sslClientCertificateSupportedTypes = new Set([
@@ -127,4 +130,5 @@ export const supportsConnectionParamsForType = (type: string) =>
type === "dameng" ||
type === "tdengine" ||
type === "elasticsearch" ||
type === "chroma";
type === "chroma" ||
type === "qdrant";

View File

@@ -25,6 +25,7 @@ describe('connectionTypeCatalog', () => {
expect(keys).toContain('redis');
expect(keys).toContain('elasticsearch');
expect(keys).toContain('chroma');
expect(keys).toContain('qdrant');
expect(keys).toContain('jvm');
expect(keys).toContain('custom');
expect(new Set(keys).size).toBe(keys.length);
@@ -40,6 +41,7 @@ describe('connectionTypeCatalog', () => {
expect(getConnectionTypeDefaultPort('mongodb')).toBe(27017);
expect(getConnectionTypeDefaultPort('elasticsearch')).toBe(9200);
expect(getConnectionTypeDefaultPort('chroma')).toBe(8000);
expect(getConnectionTypeDefaultPort('qdrant')).toBe(6333);
expect(getConnectionTypeDefaultPort('sqlite')).toBe(0);
expect(getConnectionTypeDefaultPort('duckdb')).toBe(0);
expect(getConnectionTypeDefaultPort('unknown')).toBe(3306);
@@ -50,6 +52,7 @@ describe('connectionTypeCatalog', () => {
expect(getConnectionTypeHint('mongodb')).toBe('单机 / 副本集');
expect(getConnectionTypeHint('elasticsearch')).toContain('Mapping');
expect(getConnectionTypeHint('chroma')).toContain('向量');
expect(getConnectionTypeHint('qdrant')).toContain('Payload');
expect(getConnectionTypeHint('oceanbase')).toBe('MySQL / Oracle 租户');
expect(getConnectionTypeHint('duckdb')).toBe('本地文件连接');
expect(getConnectionTypeHint('mysql')).toBe('标准连接配置');

View File

@@ -49,6 +49,7 @@ export const CONNECTION_TYPE_GROUPS: ConnectionTypeCatalogGroup[] = [
label: '向量数据库',
items: [
{ key: 'chroma', name: 'Chroma' },
{ key: 'qdrant', name: 'Qdrant' },
],
},
{
@@ -105,6 +106,8 @@ export const getConnectionTypeDefaultPort = (type: string): number => {
return 9200;
case 'chroma':
return 8000;
case 'qdrant':
return 6333;
case 'highgo':
return 5866;
case 'mariadb':
@@ -133,6 +136,8 @@ export const getConnectionTypeHint = (type: string): string => {
return '支持索引浏览、Mapping 检查、JSON DSL 和 query_string 查询';
case 'chroma':
return 'Collection 浏览、向量检索和元数据过滤';
case 'qdrant':
return 'Collection 浏览、向量搜索和 Payload 过滤';
case 'oceanbase':
return 'MySQL / Oracle 租户';
case 'sqlite':

View File

@@ -90,6 +90,24 @@ describe('dataSourceCapabilities', () => {
});
});
it('treats Qdrant as a queryable vector datasource without SQL export actions', () => {
expect(getDataSourceCapabilities({ type: 'qdrant' })).toMatchObject({
type: 'qdrant',
supportsQueryEditor: true,
supportsSqlQueryExport: false,
supportsCopyInsert: false,
supportsCreateDatabase: false,
supportsRenameDatabase: false,
supportsDropDatabase: false,
forceReadOnlyQueryResult: false,
});
expect(getDataSourceCapabilities({ type: 'custom', driver: 'qdrantdb' })).toMatchObject({
type: 'qdrant',
supportsQueryEditor: true,
supportsCopyInsert: false,
});
});
it('treats OceanBase Oracle protocol as Oracle capabilities', () => {
expect(getDataSourceCapabilities({
type: 'oceanbase',

View File

@@ -24,6 +24,9 @@ const normalizeDataSourceToken = (raw: string): string => {
case 'chromadb':
case 'chroma-db':
return 'chroma';
case 'qdrantdb':
case 'qdrant-db':
return 'qdrant';
case 'intersystems':
case 'intersystemsiris':
case 'inter-systems':

View File

@@ -32,6 +32,8 @@ describe('sqlDialect', () => {
expect(resolveSqlDialect('custom', 'elastic')).toBe('elasticsearch');
expect(resolveSqlDialect('ChromaDB')).toBe('chroma');
expect(resolveSqlDialect('custom', 'chroma-db')).toBe('chroma');
expect(resolveSqlDialect('QdrantDB')).toBe('qdrant');
expect(resolveSqlDialect('custom', 'qdrant-db')).toBe('qdrant');
expect(resolveSqlDialect('OceanBase', '', { oceanBaseProtocol: 'oracle' })).toBe('oracle');
expect(resolveSqlDialect('custom', 'oceanbase', { oceanBaseProtocol: 'oracle' })).toBe('oracle');
expect(isMysqlFamilyDialect('mariadb')).toBe(true);

View File

@@ -31,6 +31,7 @@ export type SqlDialect =
| 'redis'
| 'elasticsearch'
| 'chroma'
| 'qdrant'
| 'unknown'
| string;
@@ -120,6 +121,10 @@ export const resolveSqlDialect = (
case 'chroma-db':
case 'chroma':
return 'chroma';
case 'qdrantdb':
case 'qdrant-db':
case 'qdrant':
return 'qdrant';
default:
break;
}
@@ -146,6 +151,7 @@ export const resolveSqlDialect = (
if (source.includes('iris') || source.includes('intersystems')) return 'iris';
if (source.includes('elastic')) return 'elasticsearch';
if (source.includes('chroma')) return 'chroma';
if (source.includes('qdrant')) return 'qdrant';
return source;
};