This commit resolves several critical issues that prevented the plugin from working correctly with Qdrant and adds essential metadata to indexed chunks. **Settings & Configuration:** - Fix settings initialization using deep merge instead of shallow Object.assign - Prevents nested settings from being lost during load - Ensures all default values are properly preserved - Add orchestrator reinitialization when settings are saved - Ensures QdrantClient and embedding providers use updated settings - Fixes issue where plugin used localhost instead of saved HTTPS URL **UUID Generation:** - Fix generateDeterministicUUID() creating invalid UUIDs - Was generating 35-character UUIDs instead of proper 36-character format - Now correctly creates valid UUID v4 format: xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx - Properly generates segment 5 (12 hex chars) from combined hash data - Fixes segment 4 to start with 8/9/a/b per UUID spec - Resolves Qdrant API rejections: "value X is not a valid point ID" **Chunk Metadata:** - Add chunk_text field to ChunkMetadata type - Stores the actual text content of each chunk in Qdrant payload - Essential for displaying search results and content preview - Add model name to chunk metadata - Populates model field with embedding provider name (e.g., "nomic-embed-text") - Enables tracking which model generated each embedding - Supports future multi-model collections **Debug Logging:** - Add logging for settings loading and URL tracking - Add logging for QdrantClient initialization - Add logging for orchestrator creation with settings **Documentation:** - Add CLAUDE.md with comprehensive architecture documentation - Build commands and development workflow - Core components and data processing pipeline - Important implementation details and debugging guide 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
264 lines
7.5 KiB
TypeScript
264 lines
7.5 KiB
TypeScript
import { App, Notice, Plugin, TFile } from 'obsidian';
|
|
import { PluginSettings, IndexingProgress } from './src/types';
|
|
import { DEFAULT_SETTINGS, validateSettings } from './src/settings';
|
|
import { IndexingOrchestrator } from './src/indexing/orchestrator';
|
|
import { SearchModal } from './src/search/searchModal';
|
|
import { QdrantSettingsTab } from './src/ui/settingsTab';
|
|
|
|
export default class QdrantPlugin extends Plugin {
|
|
settings: PluginSettings;
|
|
private indexingOrchestrator: IndexingOrchestrator | null = null;
|
|
private statusBarItem: HTMLElement | null = null;
|
|
|
|
async onload() {
|
|
console.log('Qdrant Semantic Search plugin loading...');
|
|
|
|
try {
|
|
await this.loadSettings();
|
|
console.log('Settings loaded successfully');
|
|
|
|
// Validate settings
|
|
const errors = validateSettings(this.settings);
|
|
if (errors.length > 0) {
|
|
console.warn('Settings validation warnings:', errors);
|
|
new Notice('Qdrant: Please configure settings. Settings validation warnings: ' + errors.join(', '));
|
|
}
|
|
|
|
// Add status bar item first
|
|
this.setupStatusBar();
|
|
console.log('Status bar added');
|
|
|
|
// Add commands
|
|
this.addCommands();
|
|
console.log('Commands registered');
|
|
|
|
// Add settings tab
|
|
this.addSettingTab(new QdrantSettingsTab(this.app, this));
|
|
console.log('Settings tab added');
|
|
|
|
// Initialize indexing orchestrator (non-blocking)
|
|
this.initializeOrchestrator();
|
|
|
|
console.log('Qdrant Semantic Search plugin loaded successfully');
|
|
new Notice('Qdrant Semantic Search loaded! Configure settings before indexing.');
|
|
} catch (error) {
|
|
console.error('Failed to load Qdrant plugin:', error);
|
|
new Notice('Failed to load Qdrant plugin: ' + (error as Error).message);
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
private async initializeOrchestrator() {
|
|
try {
|
|
console.log('Initializing indexing orchestrator...');
|
|
console.log('Creating orchestrator with Qdrant URL:', this.settings.qdrant.url);
|
|
this.indexingOrchestrator = new IndexingOrchestrator(this.app, this.settings);
|
|
await this.indexingOrchestrator.initialize();
|
|
|
|
// Set up progress tracking
|
|
this.setupProgressTracking();
|
|
|
|
console.log('Indexing orchestrator initialized successfully');
|
|
this.updateStatusBar('Ready');
|
|
} catch (error) {
|
|
console.error('Failed to initialize indexing orchestrator:', error);
|
|
console.error('Stack trace:', (error as Error).stack);
|
|
new Notice('Qdrant: Indexing system not ready. Please check settings and connection.');
|
|
this.updateStatusBar('Not configured');
|
|
}
|
|
}
|
|
|
|
onunload() {
|
|
// Shutdown indexing orchestrator
|
|
if (this.indexingOrchestrator) {
|
|
this.indexingOrchestrator.shutdown();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Deep merge two objects, recursively merging nested objects
|
|
*/
|
|
private deepMerge(target: any, source: any): any {
|
|
const output = Object.assign({}, target);
|
|
if (this.isObject(target) && this.isObject(source)) {
|
|
Object.keys(source).forEach(key => {
|
|
if (this.isObject(source[key])) {
|
|
if (!(key in target)) {
|
|
Object.assign(output, { [key]: source[key] });
|
|
} else {
|
|
output[key] = this.deepMerge(target[key], source[key]);
|
|
}
|
|
} else {
|
|
Object.assign(output, { [key]: source[key] });
|
|
}
|
|
});
|
|
}
|
|
return output;
|
|
}
|
|
|
|
private isObject(item: any): boolean {
|
|
return item && typeof item === 'object' && !Array.isArray(item);
|
|
}
|
|
|
|
async loadSettings() {
|
|
const loadedData = await this.loadData();
|
|
console.log('Loading settings from data.json:', JSON.stringify(loadedData, null, 2));
|
|
|
|
// Use deep merge to properly combine defaults with saved settings
|
|
this.settings = this.deepMerge(DEFAULT_SETTINGS, loadedData || {});
|
|
|
|
console.log('Merged settings (Qdrant URL):', this.settings.qdrant.url);
|
|
}
|
|
|
|
async saveSettings() {
|
|
console.log('Saving settings (Qdrant URL):', this.settings.qdrant.url);
|
|
await this.saveData(this.settings);
|
|
|
|
// Reinitialize orchestrator with new settings
|
|
console.log('Reinitializing orchestrator with updated settings...');
|
|
if (this.indexingOrchestrator) {
|
|
await this.indexingOrchestrator.shutdown();
|
|
this.indexingOrchestrator = null;
|
|
}
|
|
|
|
// Reinitialize with new settings
|
|
await this.initializeOrchestrator();
|
|
}
|
|
|
|
private setupStatusBar() {
|
|
this.statusBarItem = this.addStatusBarItem();
|
|
this.updateStatusBar('Ready');
|
|
}
|
|
|
|
private addCommands() {
|
|
// Semantic search command
|
|
this.addCommand({
|
|
id: 'semantic-search',
|
|
name: 'Semantic search',
|
|
callback: () => {
|
|
if (!this.indexingOrchestrator?.isReady()) {
|
|
new Notice('Indexing system not ready. Please check your settings.');
|
|
return;
|
|
}
|
|
new SearchModal(this.app, this.settings).open();
|
|
}
|
|
});
|
|
|
|
// Index current file command
|
|
this.addCommand({
|
|
id: 'index-current-file',
|
|
name: 'Index current file',
|
|
checkCallback: (checking: boolean) => {
|
|
const activeFile = this.app.workspace.getActiveFile();
|
|
if (activeFile instanceof TFile) {
|
|
if (!checking) {
|
|
this.indexFile(activeFile);
|
|
}
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
});
|
|
|
|
// Full reindex command
|
|
this.addCommand({
|
|
id: 'full-reindex',
|
|
name: 'Full reindex vault',
|
|
callback: () => {
|
|
this.indexFullVault();
|
|
}
|
|
});
|
|
|
|
// Clear index command
|
|
this.addCommand({
|
|
id: 'clear-index',
|
|
name: 'Clear index',
|
|
callback: () => {
|
|
this.clearIndex();
|
|
}
|
|
});
|
|
|
|
// Open graph view command
|
|
if (this.settings.enableGraphView) {
|
|
this.addCommand({
|
|
id: 'open-graph-view',
|
|
name: 'Open graph view',
|
|
callback: () => {
|
|
// TODO: Implement graph view
|
|
new Notice('Graph view not yet implemented');
|
|
}
|
|
});
|
|
}
|
|
}
|
|
|
|
private setupProgressTracking() {
|
|
if (!this.indexingOrchestrator) return;
|
|
|
|
this.indexingOrchestrator.setProgressCallback((progress: IndexingProgress) => {
|
|
this.updateStatusBar(progress);
|
|
});
|
|
|
|
this.indexingOrchestrator.setErrorCallback((error: string) => {
|
|
new Notice('Indexing error: ' + error);
|
|
});
|
|
}
|
|
|
|
private updateStatusBar(progress: IndexingProgress | string) {
|
|
if (!this.statusBarItem) return;
|
|
|
|
if (typeof progress === 'string') {
|
|
this.statusBarItem.setText(`Qdrant: ${progress}`);
|
|
return;
|
|
}
|
|
|
|
if (progress.isRunning) {
|
|
const percentage = progress.totalFiles > 0
|
|
? Math.round((progress.processedFiles / progress.totalFiles) * 100)
|
|
: 0;
|
|
this.statusBarItem.setText(`Qdrant: Indexing ${percentage}% (${progress.processedFiles}/${progress.totalFiles})`);
|
|
} else {
|
|
this.statusBarItem.setText('Qdrant: Ready');
|
|
}
|
|
}
|
|
|
|
// Public methods for settings tab
|
|
async testQdrantConnection(): Promise<boolean> {
|
|
if (!this.indexingOrchestrator) return false;
|
|
const connections = await this.indexingOrchestrator.testConnections();
|
|
return connections.qdrant;
|
|
}
|
|
|
|
async testOllamaConnection(): Promise<boolean> {
|
|
if (!this.indexingOrchestrator) return false;
|
|
const connections = await this.indexingOrchestrator.testConnections();
|
|
return connections.embedding;
|
|
}
|
|
|
|
async indexFullVault(): Promise<void> {
|
|
if (!this.indexingOrchestrator?.isReady()) {
|
|
throw new Error('Indexing system not ready');
|
|
}
|
|
await this.indexingOrchestrator.indexFullVault();
|
|
}
|
|
|
|
async indexFile(file: TFile): Promise<void> {
|
|
if (!this.indexingOrchestrator?.isReady()) {
|
|
throw new Error('Indexing system not ready');
|
|
}
|
|
await this.indexingOrchestrator.indexFile(file);
|
|
}
|
|
|
|
async clearIndex(): Promise<void> {
|
|
if (!this.indexingOrchestrator?.isReady()) {
|
|
throw new Error('Indexing system not ready');
|
|
}
|
|
await this.indexingOrchestrator.clearIndex();
|
|
}
|
|
|
|
async getIndexStats(): Promise<any> {
|
|
if (!this.indexingOrchestrator?.isReady()) {
|
|
throw new Error('Indexing system not ready');
|
|
}
|
|
return await this.indexingOrchestrator.getIndexStats();
|
|
}
|
|
} |