From 038a2ccca33893c22542ce900cadbf9fd49ae72e Mon Sep 17 00:00:00 2001 From: Thebinary110 Date: Sun, 4 Jan 2026 13:39:14 +0530 Subject: [PATCH] Add frontend support for HNSW-based visualization --- src/components/VisualizeChart/renderBy.js | 76 ++++++++++++++------ src/components/VisualizeChart/requestData.js | 35 +++++++-- 2 files changed, 82 insertions(+), 29 deletions(-) diff --git a/src/components/VisualizeChart/renderBy.js b/src/components/VisualizeChart/renderBy.js index 6323cc30b..71fcde865 100644 --- a/src/components/VisualizeChart/renderBy.js +++ b/src/components/VisualizeChart/renderBy.js @@ -26,50 +26,48 @@ const PALLETE = [ '#651067', ]; -// const SELECTED_BORDER_COLOR = '#881177'; - function colorByPayload(payloadValue, colored) { + if (payloadValue === undefined || payloadValue === null) { + return BACKGROUND_COLOR; + } + if (colored[payloadValue]) { return colored[payloadValue]; } const nextColorIndex = Object.keys(colored).length % PALLETE.length; - colored[payloadValue] = PALLETE[nextColorIndex]; return PALLETE[nextColorIndex]; } -// This function generates an array of colors for each point in the chart. -// There are following options available for colorBy: -// -// - None: all points will have the same color -// - typeof = "string": color points based on the source field -// - {"payload": "field_name"}: color points based on the payload field -// - {"discover_score": { ... } }: color points based on the discover score -// - {"query": { ... }}: color points based on the query score +// -------------------------------------------------- +// COLOR GENERATION +// -------------------------------------------------- export function generateColorBy(points, colorBy = null) { - // Points example: - // [ - // { id: 0, payload: { field_name: 1 }, score: 0.5, vector: [0.1, 0.2, ....] }, - // { id: 1, payload: { field_name: 2 }, score: 0.6, vector: [0.3, 0.4, ....] }, - // ... - // ] + if (!points || points.length === 0) { + return []; + } + // Default: single color if (!colorBy) { - return Array.from({ length: points.length }, () => BACKGROUND_COLOR); // Default color + return Array.from({ length: points.length }, () => BACKGROUND_COLOR); } - // If `colorBy` is a string, interpret as a field name + // If `colorBy` is a string → payload field if (typeof colorBy === 'string') { colorBy = { payload: colorBy }; } function getNestedValue(obj, path) { + if (!obj) return undefined; return path.split('.').reduce((acc, part) => acc && acc[part], obj); } + // ------------------------------- + // Payload-based coloring + // ------------------------------- if (colorBy.payload) { const valuesToColor = {}; @@ -79,20 +77,52 @@ export function generateColorBy(points, colorBy = null) { }); } + // ------------------------------- + // Score-based coloring + // (query / discover / HNSW-safe) + // ------------------------------- if (colorBy.query) { - const scores = points.map((point) => point.score); + const scores = points + .map((point) => point.score) + .filter((score) => typeof score === 'number'); + + if (scores.length === 0) { + return Array.from({ length: points.length }, () => BACKGROUND_COLOR); + } + const minScore = Math.min(...scores); const maxScore = Math.max(...scores); + // Avoid division by zero + if (minScore === maxScore) { + return Array.from({ length: points.length }, () => SCORE_GRADIENT_COLORS[1]); + } + const colorScale = chroma.scale(SCORE_GRADIENT_COLORS); - return scores.map((score) => { - const normalizedScore = (score - minScore) / (maxScore - minScore); + + return points.map((point) => { + if (typeof point.score !== 'number') { + return BACKGROUND_COLOR; + } + + const normalizedScore = (point.score - minScore) / (maxScore - minScore); return colorScale(normalizedScore).hex(); }); } + + // Fallback + return Array.from({ length: points.length }, () => BACKGROUND_COLOR); } +// -------------------------------------------------- +// SIZE GENERATION +// -------------------------------------------------- + export function generateSizeBy(points) { - // ToDo: Intoroduce size differentiation later + if (!points || points.length === 0) { + return []; + } + + // HNSW-safe default return points.map(() => 3); } diff --git a/src/components/VisualizeChart/requestData.js b/src/components/VisualizeChart/requestData.js index af5ab63de..51ad1897b 100644 --- a/src/components/VisualizeChart/requestData.js +++ b/src/components/VisualizeChart/requestData.js @@ -1,9 +1,31 @@ /* eslint-disable camelcase */ -export function requestData(qdrantClient, collectionName, { limit, filter = null, using = null, color_by = null }) { - // Based on the input parameters, we need to decide what kind of request we need to send - // By default we should do scroll request - // But if we have color_by field which uses query, it should be used instead +export function requestData( + qdrantClient, + collectionName, + { limit, filter = null, using = null, color_by = null, method = null } +) { + // -------------------------------------------------- + // HNSW-based visualization request + // -------------------------------------------------- + if (method === 'hnsw') { + /** + * HNSW visualization does NOT require vectors. + * Backend is expected to return precomputed 2D points. + */ + return qdrantClient.http.post( + `/collections/${collectionName}/visualize`, + { + method: 'hnsw', + limit, + filter, + } + ); + } + + // -------------------------------------------------- + // Query-based coloring (query / discover) + // -------------------------------------------------- if (color_by?.query) { const query = { query: color_by.query, @@ -17,8 +39,9 @@ export function requestData(qdrantClient, collectionName, { limit, filter = null return qdrantClient.query(collectionName, query); } - // It it's not a query, we should do a scroll request - + // -------------------------------------------------- + // Default: scroll request + // -------------------------------------------------- const scrollQuery = { limit: limit, filter: filter,