diff --git a/README.md b/README.md index 318a576..f1b4c6a 100644 --- a/README.md +++ b/README.md @@ -3,7 +3,7 @@ An on-device AI plugin for Joplin that semantically clusters notes, suggests tags and notebook structures, and detects stale/archivable notes. > [!NOTE] -> This plugin is under active development as part of GSoC 2026. The embedding pipeline, clustering (K-Means, K-Medoids, HDBSCAN), and an interactive UI panel are implemented. Settings customization and staleness analysis are upcoming features. +> This plugin is under active development as part of GSoC 2026. The embedding pipeline, clustering (K-Means, HDBSCAN), and an interactive UI panel are implemented. Settings customization and staleness analysis are upcoming features. --- @@ -29,7 +29,7 @@ The plugin implements a background-threaded embedding pipeline: - **Embedding Pipeline**: On-device embedding generation with WebGPU acceleration and WASM fallback - **Native AI Integration**: Automatically uses Joplin's built-in AI Search embeddings when available -- **Multi-Strategy Clustering**: Compare K-Means, K-Medoids, and HDBSCAN results side-by-side +- **Multi-Strategy Clustering**: Compare K-Means and HDBSCAN results side-by-side - **Interactive Panel**: Drag-and-drop notes between clusters, rename clusters, add custom categories - **Apply Categorization**: Organize notes into notebooks and/or tags based on clustering results - **Undo Support**: Revert any applied categorization with full change tracking diff --git a/src/pipeline/clustering/autoK.ts b/src/pipeline/clustering/autoK.ts index 80e61cf..a33ea82 100644 --- a/src/pipeline/clustering/autoK.ts +++ b/src/pipeline/clustering/autoK.ts @@ -1,5 +1,6 @@ import { DistanceFn, silhouetteScore } from './metrics'; import { kmeans } from './kmeans'; +// NOTE: kmedoids is not used in the default pipeline (too slow), but kept here for manual benchmarking import { kmedoids } from './kmedoids'; import { log } from '../../utils/logger'; @@ -87,7 +88,7 @@ export function computeKRange(n: number): [number, number] { * produces more useful note categories. * * @param vectors Input data points (N x D), already UMAP-reduced if applicable - * @param algorithm Which algorithm to use: 'kmeans' or 'kmedoids' + * @param algorithm Which algorithm to use: 'kmeans' or 'kmedoids' (note: kmedoids is not used in the default pipeline) * @param distFn Distance function (cosine or euclidean) * @param seed Seed for reproducible initialization * @returns The optimal K, its assignments, and its silhouette score diff --git a/src/pipeline/clustering/benchmark.ts b/src/pipeline/clustering/benchmark.ts index 8ec7f43..bbf368b 100644 --- a/src/pipeline/clustering/benchmark.ts +++ b/src/pipeline/clustering/benchmark.ts @@ -1,6 +1,7 @@ import { CategorizationConfig, BenchmarkResult, ClusteringStrategy } from '../../types/cluster'; import { DistanceFn, getDistanceFn, silhouetteScore, euclideanDistance } from './metrics'; import { kmeans } from './kmeans'; +// NOTE: kmedoids is not used in the default pipeline (too slow), but kept here for manual benchmarking import { kmedoids } from './kmedoids'; import { hdbscan } from './hdbscan'; import { findOptimalK } from './autoK'; @@ -25,7 +26,7 @@ export function runStrategy( switch (strategy.algorithm) { case 'kmeans': return kmeans(vectors, strategy.K ?? DEFAULT_K, distFn, seed); - case 'kmedoids': + case 'kmedoids': // NOTE: not used in default pipeline strategies (too slow) return kmedoids(vectors, strategy.K ?? DEFAULT_K, distFn, seed); case 'hdbscan': return hdbscan(vectors, strategy.minClusterSize ?? DEFAULT_MIN_CLUSTER_SIZE, strategy.minSamples, distFn); diff --git a/src/pipeline/clustering/kmedoids.ts b/src/pipeline/clustering/kmedoids.ts index 565b1a9..8b4a673 100644 --- a/src/pipeline/clustering/kmedoids.ts +++ b/src/pipeline/clustering/kmedoids.ts @@ -1,3 +1,10 @@ +/** + * NOTE: kmedoids is NOT currently used in the default pipeline strategies. + * It was removed due to prohibitively high runtime (~38s for ~500 notes) + * compared to kmeans and hdbscan. The implementation is retained here for + * potential future use or manual benchmarking. + */ + import { DistanceFn } from './metrics'; import { mulberry32 } from '../../utils/prng'; diff --git a/src/pipeline/pipelineConfig.ts b/src/pipeline/pipelineConfig.ts index b9f5a62..9262301 100644 --- a/src/pipeline/pipelineConfig.ts +++ b/src/pipeline/pipelineConfig.ts @@ -42,7 +42,6 @@ export function createAdaptiveConfig( intermediateNeighbors: adaptiveNeighbors(noteCount), strategies: [ { name: 'kmeans-auto', algorithm: 'kmeans', K: 'auto' }, - { name: 'kmedoids-auto', algorithm: 'kmedoids', K: 'auto' }, { name: 'hdbscan', algorithm: 'hdbscan', minClusterSize: 3, minSamples: 2 }, ], }; @@ -56,7 +55,6 @@ export function createPipelineConfig(metric: MetricType = 'cosine', seed = 42): intermediateNeighbors: 5, strategies: [ { name: 'kmeans-auto', algorithm: 'kmeans', K: 'auto' }, - { name: 'kmedoids-auto', algorithm: 'kmedoids', K: 'auto' }, { name: 'hdbscan', algorithm: 'hdbscan', minClusterSize: 3, minSamples: 2 }, ], }; diff --git a/src/types/cluster.ts b/src/types/cluster.ts index db1a982..3e828e8 100644 --- a/src/types/cluster.ts +++ b/src/types/cluster.ts @@ -1,10 +1,11 @@ +// NOTE: 'kmedoids' is kept in the type for compatibility but is not used in the default pipeline strategies (too slow) export type ClusteringAlgorithm = 'kmeans' | 'kmedoids' | 'hdbscan'; export interface ClusteringStrategy { /** Human-readable label for this run, e.g. 'kmeans-5' */ name: string; algorithm: ClusteringAlgorithm; - /** Number of clusters (kmeans / kmedoids). Use 'auto' for automatic selection via silhouette sweep. */ + /** Number of clusters (kmeans / kmedoids). Use 'auto' for automatic selection via silhouette sweep. Note: kmedoids is not active in the default pipeline. */ K?: number | 'auto'; /** Minimum points to form a cluster (hdbscan, default: 3) */ minClusterSize?: number; diff --git a/src/webview/components/StrategySection.tsx b/src/webview/components/StrategySection.tsx index c7099d6..5db07a2 100644 --- a/src/webview/components/StrategySection.tsx +++ b/src/webview/components/StrategySection.tsx @@ -14,8 +14,6 @@ function getStrategyDisplayName(name: string, isHighest: boolean): string { baseName = 'HDBSCAN'; } else if (name.startsWith('kmeans')) { baseName = 'K-Means (Testing)'; - } else if (name.startsWith('kmedoids')) { - baseName = 'K-Medoids (Testing)'; } if (isHighest) { @@ -63,7 +61,7 @@ export const StrategySection: React.FC = ({
{strategies .map((s, idx) => ({ s, idx })) - .filter(({ s }) => !s.strategyName.startsWith('kmeans') && !s.strategyName.startsWith('kmedoids')) + .filter(({ s }) => !s.strategyName.startsWith('kmeans')) .map(({ s, idx }) => ( {getStrategyDisplayName(s.strategyName, false)}: {s.silhouetteScore.toFixed(2)} diff --git a/src/webview/context/AppStateContext.tsx b/src/webview/context/AppStateContext.tsx index e94dff0..7fd502c 100644 --- a/src/webview/context/AppStateContext.tsx +++ b/src/webview/context/AppStateContext.tsx @@ -136,8 +136,7 @@ export const AppStateProvider: React.FC<{ children: React.ReactNode }> = ({ chil setStrategies(msg.strategies || []); setNotes(msg.notes || []); const nonTestingIdx = (msg.strategies || []).findIndex( - (s: BenchmarkResult) => - !s.strategyName.startsWith('kmeans') && !s.strategyName.startsWith('kmedoids'), + (s: BenchmarkResult) => !s.strategyName.startsWith('kmeans'), ); const fallbackIdx = nonTestingIdx !== -1 ? nonTestingIdx : 0; setSelectedStrategyIndex(msg.selectedStrategyIndex ?? fallbackIdx); diff --git a/test/pipeline/pipelineConfig.test.ts b/test/pipeline/pipelineConfig.test.ts index 16ac510..ff43d39 100644 --- a/test/pipeline/pipelineConfig.test.ts +++ b/test/pipeline/pipelineConfig.test.ts @@ -74,7 +74,7 @@ describe('adaptive scaling functions', () => { expect(config.metric).toBe('cosine'); expect(config.intermediateDim).toBe(19); expect(config.intermediateNeighbors).toBe(10); - expect(config.strategies.length).toBe(3); + expect(config.strategies.length).toBe(2); }); }); @@ -84,10 +84,9 @@ describe('DEFAULT_CONFIG', () => { expect(DEFAULT_CONFIG.seed).toBe(42); }); - it('includes kmeans, kmedoids, and hdbscan strategies', () => { + it('includes kmeans and hdbscan strategies', () => { const names = DEFAULT_CONFIG.strategies.map((s) => s.algorithm); expect(names).toContain('kmeans'); - expect(names).toContain('kmedoids'); expect(names).toContain('hdbscan'); }); });