371 lines
		
	
	
		
			8.6 KiB
		
	
	
	
		
			Markdown
		
	
	
	
	
	
			
		
		
	
	
			371 lines
		
	
	
		
			8.6 KiB
		
	
	
	
		
			Markdown
		
	
	
	
	
	
# Search Migration Guide
 | 
						|
 | 
						|
## Overview
 | 
						|
 | 
						|
This guide helps you migrate from the old `SearchEvaluatorService` to the new `SearchOrchestratorService`.
 | 
						|
 | 
						|
## Why Migrate?
 | 
						|
 | 
						|
The new orchestrator provides:
 | 
						|
- ✅ **Correct filtering**: All field operators actually work
 | 
						|
- ✅ **Better highlighting**: Precise ranges instead of text matching
 | 
						|
- ✅ **More features**: Context lines, max results, match ranges
 | 
						|
- ✅ **Better performance**: Pre-calculated ranges, no rescanning
 | 
						|
 | 
						|
## Quick Migration
 | 
						|
 | 
						|
### Before (Old)
 | 
						|
```typescript
 | 
						|
import { SearchEvaluatorService } from './core/search/search-evaluator.service';
 | 
						|
 | 
						|
constructor(private evaluator: SearchEvaluatorService) {}
 | 
						|
 | 
						|
search(query: string) {
 | 
						|
  const results = this.evaluator.search(query, {
 | 
						|
    caseSensitive: false
 | 
						|
  });
 | 
						|
  
 | 
						|
  // Results don't include ranges
 | 
						|
  results.forEach(result => {
 | 
						|
    result.matches.forEach(match => {
 | 
						|
      // match.startOffset and match.endOffset are basic
 | 
						|
    });
 | 
						|
  });
 | 
						|
}
 | 
						|
```
 | 
						|
 | 
						|
### After (New)
 | 
						|
```typescript
 | 
						|
import { SearchOrchestratorService } from './core/search/search-orchestrator.service';
 | 
						|
 | 
						|
constructor(private orchestrator: SearchOrchestratorService) {}
 | 
						|
 | 
						|
search(query: string) {
 | 
						|
  const results = this.orchestrator.execute(query, {
 | 
						|
    caseSensitive: false,
 | 
						|
    contextLines: 5,      // NEW: Adjustable context
 | 
						|
    maxResults: 100       // NEW: Limit results
 | 
						|
  });
 | 
						|
  
 | 
						|
  // Results include precise ranges
 | 
						|
  results.forEach(result => {
 | 
						|
    result.matches.forEach(match => {
 | 
						|
      // match.ranges: MatchRange[] with start/end/line/context
 | 
						|
    });
 | 
						|
  });
 | 
						|
}
 | 
						|
```
 | 
						|
 | 
						|
## Step-by-Step Migration
 | 
						|
 | 
						|
### 1. Update Imports
 | 
						|
 | 
						|
```typescript
 | 
						|
// OLD
 | 
						|
import { SearchEvaluatorService, SearchResult } from './core/search/search-evaluator.service';
 | 
						|
 | 
						|
// NEW
 | 
						|
import { SearchOrchestratorService, SearchResult } from './core/search/search-orchestrator.service';
 | 
						|
```
 | 
						|
 | 
						|
### 2. Update Injection
 | 
						|
 | 
						|
```typescript
 | 
						|
// OLD
 | 
						|
constructor(private evaluator: SearchEvaluatorService) {}
 | 
						|
 | 
						|
// NEW
 | 
						|
constructor(private orchestrator: SearchOrchestratorService) {}
 | 
						|
```
 | 
						|
 | 
						|
### 3. Update Method Calls
 | 
						|
 | 
						|
```typescript
 | 
						|
// OLD
 | 
						|
const results = this.evaluator.search(query, options);
 | 
						|
 | 
						|
// NEW
 | 
						|
const results = this.orchestrator.execute(query, options);
 | 
						|
```
 | 
						|
 | 
						|
### 4. Update Result Handling
 | 
						|
 | 
						|
```typescript
 | 
						|
// OLD
 | 
						|
results.forEach(result => {
 | 
						|
  const { noteId, matches, score } = result;
 | 
						|
  // matches[].startOffset, matches[].endOffset
 | 
						|
});
 | 
						|
 | 
						|
// NEW
 | 
						|
results.forEach(result => {
 | 
						|
  const { noteId, matches, score, allRanges } = result;
 | 
						|
  // matches[].ranges: MatchRange[]
 | 
						|
  // allRanges: MatchRange[] (all ranges in note)
 | 
						|
});
 | 
						|
```
 | 
						|
 | 
						|
## Highlighting Migration
 | 
						|
 | 
						|
### Before (Manual)
 | 
						|
```typescript
 | 
						|
highlightMatch(context: string, matchText: string): string {
 | 
						|
  const regex = new RegExp(`(${matchText})`, 'gi');
 | 
						|
  return context.replace(regex, '<mark>$1</mark>');
 | 
						|
}
 | 
						|
```
 | 
						|
 | 
						|
### After (Service)
 | 
						|
```typescript
 | 
						|
import { SearchHighlighterService } from './core/search/search-highlighter.service';
 | 
						|
 | 
						|
constructor(private highlighter: SearchHighlighterService) {}
 | 
						|
 | 
						|
highlightMatch(match: SearchMatch): string {
 | 
						|
  // Use ranges for precise highlighting
 | 
						|
  if (match.ranges && match.ranges.length > 0) {
 | 
						|
    return this.highlighter.highlightWithRanges(match.context, match.ranges);
 | 
						|
  }
 | 
						|
  
 | 
						|
  // Fallback to text-based
 | 
						|
  return this.highlighter.highlightMatches(match.context, [match.text], false);
 | 
						|
}
 | 
						|
```
 | 
						|
 | 
						|
## Preferences Migration
 | 
						|
 | 
						|
### Before (Manual State)
 | 
						|
```typescript
 | 
						|
export class MyComponent {
 | 
						|
  collapseResults = false;
 | 
						|
  showMoreContext = false;
 | 
						|
  
 | 
						|
  // Manual localStorage
 | 
						|
  ngOnInit() {
 | 
						|
    const saved = localStorage.getItem('my-prefs');
 | 
						|
    if (saved) {
 | 
						|
      const prefs = JSON.parse(saved);
 | 
						|
      this.collapseResults = prefs.collapse;
 | 
						|
    }
 | 
						|
  }
 | 
						|
  
 | 
						|
  savePrefs() {
 | 
						|
    localStorage.setItem('my-prefs', JSON.stringify({
 | 
						|
      collapse: this.collapseResults
 | 
						|
    }));
 | 
						|
  }
 | 
						|
}
 | 
						|
```
 | 
						|
 | 
						|
### After (Service)
 | 
						|
```typescript
 | 
						|
import { SearchPreferencesService } from './core/search/search-preferences.service';
 | 
						|
 | 
						|
export class MyComponent {
 | 
						|
  constructor(private preferences: SearchPreferencesService) {}
 | 
						|
  
 | 
						|
  collapseResults = false;
 | 
						|
  showMoreContext = false;
 | 
						|
  
 | 
						|
  ngOnInit() {
 | 
						|
    // Auto-load preferences
 | 
						|
    const prefs = this.preferences.getPreferences('my-context');
 | 
						|
    this.collapseResults = prefs.collapseResults;
 | 
						|
    this.showMoreContext = prefs.showMoreContext;
 | 
						|
  }
 | 
						|
  
 | 
						|
  onToggleCollapse() {
 | 
						|
    // Auto-save preferences
 | 
						|
    this.preferences.updatePreferences('my-context', {
 | 
						|
      collapseResults: this.collapseResults
 | 
						|
    });
 | 
						|
  }
 | 
						|
}
 | 
						|
```
 | 
						|
 | 
						|
## Component Migration
 | 
						|
 | 
						|
### SearchResultsComponent
 | 
						|
 | 
						|
#### Before
 | 
						|
```typescript
 | 
						|
<app-search-results
 | 
						|
  [results]="results()"
 | 
						|
  (noteOpen)="onNoteOpen($event)"
 | 
						|
/>
 | 
						|
```
 | 
						|
 | 
						|
#### After
 | 
						|
```typescript
 | 
						|
<app-search-results
 | 
						|
  [results]="results()"
 | 
						|
  [collapseAll]="collapseResults"
 | 
						|
  [showMoreContext]="showMoreContext"
 | 
						|
  [contextLines]="contextLines()"
 | 
						|
  (noteOpen)="onNoteOpen($event)"
 | 
						|
/>
 | 
						|
```
 | 
						|
 | 
						|
### SearchPanelComponent
 | 
						|
 | 
						|
No changes needed! The component now includes toggles automatically.
 | 
						|
 | 
						|
```typescript
 | 
						|
<app-search-panel
 | 
						|
  placeholder="Search in vault..."
 | 
						|
  context="vault"
 | 
						|
  (noteOpen)="openNote($event)"
 | 
						|
/>
 | 
						|
```
 | 
						|
 | 
						|
## Common Patterns
 | 
						|
 | 
						|
### Pattern 1: Search with Context
 | 
						|
 | 
						|
```typescript
 | 
						|
// OLD: Fixed context
 | 
						|
const results = this.evaluator.search(query);
 | 
						|
 | 
						|
// NEW: Adjustable context
 | 
						|
const results = this.orchestrator.execute(query, {
 | 
						|
  contextLines: this.showMoreContext ? 5 : 2
 | 
						|
});
 | 
						|
```
 | 
						|
 | 
						|
### Pattern 2: Limit Results
 | 
						|
 | 
						|
```typescript
 | 
						|
// OLD: Manual slicing
 | 
						|
const results = this.evaluator.search(query).slice(0, 100);
 | 
						|
 | 
						|
// NEW: Built-in limit
 | 
						|
const results = this.orchestrator.execute(query, {
 | 
						|
  maxResults: 100
 | 
						|
});
 | 
						|
```
 | 
						|
 | 
						|
### Pattern 3: Highlighting
 | 
						|
 | 
						|
```typescript
 | 
						|
// OLD: Manual regex
 | 
						|
const highlighted = text.replace(
 | 
						|
  new RegExp(`(${term})`, 'gi'),
 | 
						|
  '<mark>$1</mark>'
 | 
						|
);
 | 
						|
 | 
						|
// NEW: Service with XSS protection
 | 
						|
const highlighted = this.highlighter.highlightMatches(
 | 
						|
  text,
 | 
						|
  [term],
 | 
						|
  caseSensitive
 | 
						|
);
 | 
						|
```
 | 
						|
 | 
						|
## Testing Migration
 | 
						|
 | 
						|
### Before
 | 
						|
```typescript
 | 
						|
it('should search', () => {
 | 
						|
  const results = evaluator.search('test');
 | 
						|
  expect(results.length).toBeGreaterThan(0);
 | 
						|
});
 | 
						|
```
 | 
						|
 | 
						|
### After
 | 
						|
```typescript
 | 
						|
it('should search with orchestrator', () => {
 | 
						|
  const results = orchestrator.execute('test');
 | 
						|
  expect(results.length).toBeGreaterThan(0);
 | 
						|
  expect(results[0].allRanges).toBeDefined();
 | 
						|
  expect(results[0].matches[0].ranges).toBeDefined();
 | 
						|
});
 | 
						|
```
 | 
						|
 | 
						|
## Backward Compatibility
 | 
						|
 | 
						|
The old `SearchEvaluatorService` still works as a **wrapper**:
 | 
						|
 | 
						|
```typescript
 | 
						|
// This still works (delegates to orchestrator)
 | 
						|
const results = this.evaluator.search(query, options);
 | 
						|
 | 
						|
// But you won't get the new features:
 | 
						|
// - No contextLines option
 | 
						|
// - No maxResults option
 | 
						|
// - No allRanges in results
 | 
						|
// - matches[].ranges are converted from allRanges[0]
 | 
						|
```
 | 
						|
 | 
						|
## Breaking Changes
 | 
						|
 | 
						|
**None!** All existing code continues to work.
 | 
						|
 | 
						|
## Deprecation Timeline
 | 
						|
 | 
						|
- **Now**: `SearchEvaluatorService` marked as `@deprecated`
 | 
						|
- **v2.0**: `SearchEvaluatorService` will be removed
 | 
						|
- **Migration window**: ~6 months
 | 
						|
 | 
						|
## Checklist
 | 
						|
 | 
						|
- [ ] Update imports to `SearchOrchestratorService`
 | 
						|
- [ ] Update injection in constructors
 | 
						|
- [ ] Replace `.search()` with `.execute()`
 | 
						|
- [ ] Add `SearchHighlighterService` for highlighting
 | 
						|
- [ ] Add `SearchPreferencesService` for preferences
 | 
						|
- [ ] Update component inputs (collapseAll, showMoreContext, contextLines)
 | 
						|
- [ ] Update tests to check for ranges
 | 
						|
- [ ] Remove manual localStorage code
 | 
						|
- [ ] Test all search scenarios
 | 
						|
 | 
						|
## Need Help?
 | 
						|
 | 
						|
- **Documentation**: `src/core/search/README.md`
 | 
						|
- **Examples**: `src/components/search-panel/search-panel.component.ts`
 | 
						|
- **Tests**: `src/core/search/*.spec.ts`
 | 
						|
- **Issues**: Create a GitHub issue with `[search]` prefix
 | 
						|
 | 
						|
## FAQ
 | 
						|
 | 
						|
### Q: Do I have to migrate immediately?
 | 
						|
**A:** No, the old service still works. But you won't get the bug fixes and new features.
 | 
						|
 | 
						|
### Q: Will my existing code break?
 | 
						|
**A:** No, backward compatibility is maintained.
 | 
						|
 | 
						|
### Q: What if I only want highlighting?
 | 
						|
**A:** You can use `SearchHighlighterService` independently:
 | 
						|
 | 
						|
```typescript
 | 
						|
import { SearchHighlighterService } from './core/search/search-highlighter.service';
 | 
						|
 | 
						|
constructor(private highlighter: SearchHighlighterService) {}
 | 
						|
 | 
						|
highlight(text: string, terms: string[]) {
 | 
						|
  return this.highlighter.highlightMatches(text, terms, false);
 | 
						|
}
 | 
						|
```
 | 
						|
 | 
						|
### Q: What if I only want preferences?
 | 
						|
**A:** You can use `SearchPreferencesService` independently:
 | 
						|
 | 
						|
```typescript
 | 
						|
import { SearchPreferencesService } from './core/search/search-preferences.service';
 | 
						|
 | 
						|
constructor(private preferences: SearchPreferencesService) {}
 | 
						|
 | 
						|
loadPrefs() {
 | 
						|
  return this.preferences.getPreferences('my-context');
 | 
						|
}
 | 
						|
```
 | 
						|
 | 
						|
### Q: Can I mix old and new?
 | 
						|
**A:** Yes, but not recommended. Stick to one approach per component.
 | 
						|
 | 
						|
## Examples
 | 
						|
 | 
						|
See complete examples in:
 | 
						|
- `src/components/search-panel/search-panel.component.ts`
 | 
						|
- `src/components/search-results/search-results.component.ts`
 | 
						|
- `src/core/search/*.spec.ts`
 |