logancyang_obsidian-copilot/src/search/SearchSystem.ts
Logan Yang e26561ba40
feat: introduce unified search system with legacy support (#1728)
* feat: introduce unified search system with legacy support

- Added a new `SearchSystem` interface and `SearchSystemFactory` to manage both legacy and v3 search implementations.
- Updated various components to utilize the new factory for creating retrievers and managing indexing operations.
- Introduced `useLegacySearch` setting to toggle between legacy and v3 search systems, enhancing user control over search functionality.
- Refactored existing search-related code to streamline indexing and retrieval processes, improving overall performance and maintainability.
- Updated documentation to reflect changes in search architecture and settings management.

* feat: enhance V3 indexing responses with document count

- Updated the V3SearchIndexer to return the document count after incremental and full indexing operations.
- Modified success messages to include the number of documents indexed, improving user feedback and clarity on indexing results.

* feat: improve error handling and user feedback for indexing operations

- Enhanced the indexing process by adding error handling for both incremental and full indexing operations, providing users with clear notifications in case of failures.
- Updated the `refreshVaultIndex` and `forceReindexVault` functions to log errors and display messages when indexing fails, improving overall user experience and transparency.
- Refactored the main indexing logic in `CopilotPlugin` to ensure proper loading of indexes based on the selected strategy, enhancing functionality and reliability.

* fix: update indexing logic for search system consistency

- Changed comments to clarify that the index loading occurs even when search is disabled.
- Refactored the index loading process to consistently use `SearchSystemFactory`, enhancing code clarity and maintainability.
- Updated the condition for enabling semantic search to account for the new `useLegacySearch` setting, improving search functionality control.
2025-08-17 16:32:21 -07:00

260 lines
7.7 KiB
TypeScript

import { logInfo, logWarn } from "@/logger";
import { getSettings } from "@/settings/model";
import { BaseRetriever } from "@langchain/core/retrievers";
import { App, Notice } from "obsidian";
import { HybridRetriever } from "./hybridRetriever";
import { TieredLexicalRetriever } from "./v3/TieredLexicalRetriever";
/**
* Unified interface for search operations (retrieval and indexing)
*/
export interface IndexingResult {
success: boolean;
documentCount?: number;
message?: string;
}
/**
* Interface for search indexing operations
*/
export interface SearchIndexer {
indexVaultIncremental(app: App): Promise<IndexingResult>;
indexVaultFull(app: App): Promise<IndexingResult>;
ensureLoaded(app: App): Promise<void>;
clearIndex(app: App): Promise<void>;
}
/**
* Retriever options interface
*/
export interface RetrieverOptions {
minSimilarityScore?: number;
maxK: number;
salientTerms: string[];
timeRange?: { startTime: number; endTime: number };
textWeight?: number;
returnAll?: boolean;
useRerankerThreshold?: number;
}
/**
* Abstract interface for search systems
*/
export interface SearchSystem {
readonly name: string;
readonly version: string;
readonly isLegacy: boolean;
createRetriever(app: App, options: RetrieverOptions): BaseRetriever;
getIndexer(): SearchIndexer;
isSemanticSearchEnabled(): boolean;
}
/**
* Legacy Orama-based search indexer implementation
*/
class LegacySearchIndexer implements SearchIndexer {
async indexVaultIncremental(app: App): Promise<IndexingResult> {
logInfo("LegacySearchIndexer: Incremental indexing");
try {
const VectorStoreManager = (await import("@/search/vectorStoreManager")).default;
const count = await VectorStoreManager.getInstance().indexVaultToVectorStore(false);
return { success: true, documentCount: count };
} catch (error) {
return { success: false, message: error.message };
}
}
async indexVaultFull(app: App): Promise<IndexingResult> {
logInfo("LegacySearchIndexer: Full indexing");
try {
const VectorStoreManager = (await import("@/search/vectorStoreManager")).default;
const count = await VectorStoreManager.getInstance().indexVaultToVectorStore(true);
return { success: true, documentCount: count };
} catch (error) {
return { success: false, message: error.message };
}
}
async ensureLoaded(app: App): Promise<void> {
logInfo("LegacySearchIndexer: Ensuring index is loaded");
const VectorStoreManager = (await import("@/search/vectorStoreManager")).default;
const isEmpty = await VectorStoreManager.getInstance().isIndexEmpty();
if (isEmpty) {
new Notice("Legacy Orama index is empty. Please rebuild the index.");
}
}
async clearIndex(app: App): Promise<void> {
logInfo("LegacySearchIndexer: Clearing index");
const VectorStoreManager = (await import("@/search/vectorStoreManager")).default;
await VectorStoreManager.getInstance().clearIndex();
}
}
/**
* V3 MemoryIndexManager-based search indexer implementation
*/
class V3SearchIndexer implements SearchIndexer {
async indexVaultIncremental(app: App): Promise<IndexingResult> {
logInfo("V3SearchIndexer: Incremental indexing");
try {
const { MemoryIndexManager } = await import("@/search/v3/MemoryIndexManager");
const documentCount = await MemoryIndexManager.getInstance(app).indexVaultIncremental();
await MemoryIndexManager.getInstance(app).ensureLoaded();
return {
success: true,
documentCount,
message: `V3 index updated with ${documentCount} documents`,
};
} catch (error) {
return { success: false, message: error.message };
}
}
async indexVaultFull(app: App): Promise<IndexingResult> {
logInfo("V3SearchIndexer: Full indexing");
try {
const { MemoryIndexManager } = await import("@/search/v3/MemoryIndexManager");
const documentCount = await MemoryIndexManager.getInstance(app).indexVault();
await MemoryIndexManager.getInstance(app).ensureLoaded();
return {
success: true,
documentCount,
message: `V3 index rebuilt with ${documentCount} documents`,
};
} catch (error) {
return { success: false, message: error.message };
}
}
async ensureLoaded(app: App): Promise<void> {
logInfo("V3SearchIndexer: Ensuring index is loaded");
const { MemoryIndexManager } = await import("@/search/v3/MemoryIndexManager");
await MemoryIndexManager.getInstance(app).ensureLoaded();
}
async clearIndex(app: App): Promise<void> {
logInfo("V3SearchIndexer: Clear not supported - index rebuilds on demand");
new Notice("V3 search rebuilds index automatically on demand.");
}
}
/**
* Legacy Orama-based search system implementation
* @deprecated Will be removed in v3.0.0
*/
class LegacySearchSystem implements SearchSystem {
readonly name = "Orama";
readonly version = "legacy";
readonly isLegacy = true;
private indexer = new LegacySearchIndexer();
createRetriever(app: App, options: RetrieverOptions): BaseRetriever {
return new HybridRetriever({
...options,
minSimilarityScore: options.minSimilarityScore ?? 0.1,
});
}
getIndexer(): SearchIndexer {
return this.indexer;
}
isSemanticSearchEnabled(): boolean {
// Legacy always has semantic capability via embeddings
return true;
}
}
/**
* V3 TieredLexical search system implementation
*/
class V3SearchSystem implements SearchSystem {
readonly name = "TieredLexical";
readonly version = "v3";
readonly isLegacy = false;
private indexer = new V3SearchIndexer();
createRetriever(app: App, options: RetrieverOptions): BaseRetriever {
return new TieredLexicalRetriever(app, options);
}
getIndexer(): SearchIndexer {
return this.indexer;
}
isSemanticSearchEnabled(): boolean {
return getSettings().enableSemanticSearchV3;
}
}
/**
* Factory for creating and managing search systems
*/
export class SearchSystemFactory {
private static searchSystem: SearchSystem | null = null;
/**
* Get the current search system based on settings
*/
static getSearchSystem(): SearchSystem {
const settings = getSettings();
// Check if we need to recreate due to settings change
if (this.searchSystem) {
const isCurrentlyLegacy = this.searchSystem.isLegacy;
const shouldBeLegacy = settings.useLegacySearch;
if (isCurrentlyLegacy !== shouldBeLegacy) {
this.searchSystem = null;
}
}
if (!this.searchSystem) {
if (settings.useLegacySearch) {
logWarn(
"DEPRECATED: Legacy Orama search is deprecated and will be removed in a future version. Please switch to v3 search in QA Settings."
);
this.searchSystem = new LegacySearchSystem();
} else {
this.searchSystem = new V3SearchSystem();
}
}
return this.searchSystem;
}
/**
* Reset the cached search system (use when settings change)
*/
static reset(): void {
this.searchSystem = null;
}
/**
* Create a retriever using the current search system
*/
static createRetriever(app: App, options: RetrieverOptions): BaseRetriever {
const searchSystem = this.getSearchSystem();
logInfo(`Creating retriever using ${searchSystem.name} (${searchSystem.version})`);
try {
return searchSystem.createRetriever(app, options);
} catch (error) {
// Fallback to v3 if legacy fails
if (searchSystem.isLegacy) {
logWarn("Failed to create legacy retriever, falling back to v3");
return new V3SearchSystem().createRetriever(app, options);
}
throw error;
}
}
/**
* Get the indexer for the current search system
*/
static getIndexer(): SearchIndexer {
return this.getSearchSystem().getIndexer();
}
}