ObsiViewer/docs/SEARCH/SEARCH_MIGRATION_GUIDE.md

8.6 KiB

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)

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)

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

// OLD
import { SearchEvaluatorService, SearchResult } from './core/search/search-evaluator.service';

// NEW
import { SearchOrchestratorService, SearchResult } from './core/search/search-orchestrator.service';

2. Update Injection

// OLD
constructor(private evaluator: SearchEvaluatorService) {}

// NEW
constructor(private orchestrator: SearchOrchestratorService) {}

3. Update Method Calls

// OLD
const results = this.evaluator.search(query, options);

// NEW
const results = this.orchestrator.execute(query, options);

4. Update Result Handling

// 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)

highlightMatch(context: string, matchText: string): string {
  const regex = new RegExp(`(${matchText})`, 'gi');
  return context.replace(regex, '<mark>$1</mark>');
}

After (Service)

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)

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)

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

<app-search-results
  [results]="results()"
  (noteOpen)="onNoteOpen($event)"
/>

After

<app-search-results
  [results]="results()"
  [collapseAll]="collapseResults"
  [showMoreContext]="showMoreContext"
  [contextLines]="contextLines()"
  (noteOpen)="onNoteOpen($event)"
/>

SearchPanelComponent

No changes needed! The component now includes toggles automatically.

<app-search-panel
  placeholder="Search in vault..."
  context="vault"
  (noteOpen)="openNote($event)"
/>

Common Patterns

Pattern 1: Search with Context

// 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

// 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

// 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

it('should search', () => {
  const results = evaluator.search('test');
  expect(results.length).toBeGreaterThan(0);
});

After

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:

// 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:

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:

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