feat(vector): add Qdrant/Milvus collection dimension display (#2396)

This commit is contained in:
lexmin0412 2026-07-02 12:08:30 +08:00 committed by GitHub
parent 0dc83f69b5
commit 24d952dc12
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
10 changed files with 171 additions and 0 deletions

View File

@ -578,6 +578,18 @@ async function toggle() {
const collectionRef = node.id.includes("__vector_collection:") ? node.id.split("__vector_collection:").pop() || node.label : node.label;
const tab = queryStore.createTab(node.connectionId, node.database || "default", node.label, "vector");
queryStore.updateSql(tab, collectionRef);
api
.vectorGetCollectionDetail(node.connectionId, node.database || "default", collectionRef)
.then((info) => {
if (info.dimension != null) {
if (node.meta) {
(node.meta as Record<string, unknown>).dimension = info.dimension;
} else {
node.meta = { dimension: info.dimension } as any;
}
}
})
.catch(() => {});
} else if (node.type === "database" && node.connectionId && hasTreeNodeDatabaseContext(node)) {
const config = connectionStore.getConfig(node.connectionId);
const effectiveDbType = effectiveDatabaseTypeForConnection(config);

View File

@ -375,6 +375,7 @@ export const documentListDatabases = forward("documentListDatabases");
export const mongoListDatabases = forward("mongoListDatabases");
export const documentListCollections = forward("documentListCollections");
export const mongoListCollections = forward("mongoListCollections");
export const vectorGetCollectionDetail = forward("vectorGetCollectionDetail");
export const mongoCreateDatabase = forward("mongoCreateDatabase");
export const mongoDropDatabase = forward("mongoDropDatabase");
export const mongoDropCollection = forward("mongoDropCollection");

View File

@ -1768,6 +1768,10 @@ export async function vectorListCollections(connectionId: string, database?: str
return documentListCollections(connectionId, database || "default");
}
export async function vectorGetCollectionDetail(connectionId: string, database: string, collection: string): Promise<CollectionInfo> {
return post("/api/mongo/vector-collection-detail", { connectionId, database, collection });
}
export async function mongoFindDocuments(connectionId: string, database: string, collection: string, skip: number, limit: number, filter?: string, projection?: string, sort?: string, executionId?: string): Promise<MongoDocumentResult> {
return documentFindDocuments(connectionId, database, collection, skip, limit, filter, projection, sort, executionId);
}

View File

@ -1480,6 +1480,10 @@ export async function mongoListCollections(connectionId: string, database: strin
return documentListCollections(connectionId, database);
}
export async function vectorGetCollectionDetail(connectionId: string, database: string, collection: string): Promise<CollectionInfo> {
return invoke("vector_collection_detail", { connectionId, database, collection });
}
export async function mongoCreateDatabase(connectionId: string, database: string): Promise<void> {
return invoke("mongo_create_database", { connectionId, database });
}

View File

@ -241,6 +241,102 @@ async fn list_chroma_collections(client: &VectorClient) -> Result<Vec<Collection
Ok(infos)
}
pub async fn get_collection_detail(
client: &VectorClient,
database: &str,
collection: &str,
) -> Result<CollectionInfo, String> {
match client.kind {
VectorDbKind::Qdrant => get_qdrant_collection_detail(client, collection).await,
VectorDbKind::Milvus => get_milvus_collection_detail(client, database, collection).await,
VectorDbKind::Weaviate => {
// Weaviate REST API does not expose vector dimension
Ok(CollectionInfo { name: collection.to_string(), id: collection.to_string(), dimension: None })
}
VectorDbKind::ChromaDb => get_chroma_collection_detail(client, collection).await,
}
}
async fn get_qdrant_collection_detail(client: &VectorClient, collection: &str) -> Result<CollectionInfo, String> {
let body = send_json(client.get(&format!("/collections/{}", path_segment(collection))), "Qdrant").await?;
let dim = body
.pointer("/result/config/params/vectors/size")
.and_then(Value::as_u64)
.or_else(|| {
body.pointer("/result/config/params/vectors")
.and_then(Value::as_object)
.and_then(|obj| obj.values().find_map(|v| v.get("size").and_then(|s| s.as_u64())))
})
.map(|d| d as u32);
Ok(CollectionInfo { name: collection.to_string(), id: collection.to_string(), dimension: dim })
}
fn milvus_vector_dim_from_field(field: &Value) -> Option<u32> {
if let Some(dim) = field.pointer("/params/dim").and_then(Value::as_u64) {
return Some(dim as u32);
}
if let Some(params) = field.get("params").and_then(Value::as_array) {
for param in params {
if param.get("key").and_then(Value::as_str) == Some("dim") {
if let Some(v) = param.get("value").and_then(Value::as_str) {
return v.parse().ok();
}
if let Some(v) = param.get("value").and_then(Value::as_u64) {
return Some(v as u32);
}
}
}
}
None
}
async fn get_milvus_collection_detail(
client: &VectorClient,
database: &str,
collection: &str,
) -> Result<CollectionInfo, String> {
let db_name = if database.is_empty() { "default" } else { database };
let body = send_json(
client
.post("/v2/vectordb/collections/describe")
.json(&serde_json::json!({ "dbName": db_name, "collectionName": collection })),
"Milvus",
)
.await?;
if body.get("code").and_then(Value::as_i64) != Some(0) {
let msg = body.get("message").and_then(Value::as_str).unwrap_or("unknown error");
return Err(format!("Milvus collection detail error: {msg}"));
}
let fields = body.pointer("/data/fields").and_then(Value::as_array);
let dim = fields
.and_then(|f| {
f.iter().find(|f| {
let t = f.get("type");
t.and_then(Value::as_str) == Some("FloatVector")
|| t.and_then(Value::as_str) == Some("BinaryVector")
|| t.and_then(Value::as_i64) == Some(101)
|| t.and_then(Value::as_i64) == Some(102)
})
})
.and_then(milvus_vector_dim_from_field);
Ok(CollectionInfo { name: collection.to_string(), id: collection.to_string(), dimension: dim })
}
async fn get_chroma_collection_detail(client: &VectorClient, collection: &str) -> Result<CollectionInfo, String> {
let body = send_json(
client.get(&format!(
"/api/v2/tenants/default_tenant/databases/default_database/collections/{}",
path_segment(collection)
)),
"ChromaDB",
)
.await?;
let name = body.get("name").and_then(Value::as_str).unwrap_or(collection);
let id = body.get("id").and_then(Value::as_str).unwrap_or(collection);
let dimension = body.get("dimension").and_then(|v| v.as_u64()).map(|d| d as u32);
Ok(CollectionInfo { name: name.to_string(), id: id.to_string(), dimension })
}
fn chroma_get_response_to_rows(body: &Value) -> Vec<Value> {
let ids = body.get("ids").and_then(Value::as_array).cloned().unwrap_or_default();
let docs = body.get("documents").and_then(Value::as_array).cloned().unwrap_or_default();

View File

@ -844,6 +844,25 @@ pub async fn list_vector_collections_core(
db::vector_driver::list_collections_with_db(&client, database).await
}
/// Get detailed metadata for a single vector collection (dimension, config, etc).
pub async fn get_vector_collection_detail_core(
state: &AppState,
connection_id: &str,
database: &str,
collection: &str,
) -> Result<db::vector_driver::CollectionInfo, String> {
let pool_key =
state.get_or_create_pool(connection_id, if database.is_empty() { None } else { Some(database) }).await?;
let client = {
let connections = state.connections.read().await;
match connections.get(&pool_key) {
Some(PoolKind::VectorDb(client)) => client.clone(),
_ => return Err("Not a vector database connection".to_string()),
}
};
db::vector_driver::get_collection_detail(&client, database, collection).await
}
pub async fn get_table_comment_core(
state: &AppState,
connection_id: &str,

View File

@ -442,6 +442,7 @@ async fn main() {
// MongoDB
.route("/mongo/list-databases", post(routes::mongo::list_databases))
.route("/mongo/list-collections", post(routes::mongo::list_collections))
.route("/mongo/vector-collection-detail", post(routes::mongo::vector_collection_detail))
.route("/mongo/create-database", post(routes::mongo::create_database))
.route("/mongo/drop-database", post(routes::mongo::drop_database))
.route("/mongo/drop-collection", post(routes::mongo::drop_collection))

View File

@ -196,6 +196,29 @@ pub async fn list_collections(
Ok(Json(result))
}
#[derive(Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct VectorCollectionDetailRequest {
pub connection_id: String,
pub database: String,
pub collection: String,
}
pub async fn vector_collection_detail(
State(state): State<Arc<WebState>>,
Json(req): Json<VectorCollectionDetailRequest>,
) -> Result<Json<dbx_core::db::vector_driver::CollectionInfo>, AppError> {
let result = dbx_core::schema::get_vector_collection_detail_core(
&state.app,
&req.connection_id,
&req.database,
&req.collection,
)
.await
.map_err(AppError)?;
Ok(Json(result))
}
pub async fn create_database(
State(state): State<Arc<WebState>>,
Json(req): Json<MongoCollectionRequest>,

View File

@ -40,6 +40,16 @@ pub async fn mongo_list_collections(
dbx_core::mongo_ops::mongo_list_collections_core(&state, &connection_id, &database).await
}
#[tauri::command]
pub async fn vector_collection_detail(
state: State<'_, Arc<AppState>>,
connection_id: String,
database: String,
collection: String,
) -> Result<dbx_core::db::vector_driver::CollectionInfo, String> {
dbx_core::schema::get_vector_collection_detail_core(&state, &connection_id, &database, &collection).await
}
#[tauri::command]
pub async fn mongo_create_database(
state: State<'_, Arc<AppState>>,

View File

@ -848,6 +848,7 @@ pub fn run() {
commands::sqlite_backup::backup_sqlite_database,
commands::mongo_cmd::mongo_list_databases,
commands::mongo_cmd::mongo_list_collections,
commands::mongo_cmd::vector_collection_detail,
commands::mongo_cmd::mongo_create_database,
commands::mongo_cmd::mongo_drop_database,
commands::mongo_cmd::mongo_drop_collection,