import MiniSearch, { type SearchResult } from 'minisearch';
type SearchableDoc = { id: string; text: string; };
type MiniSearchIndex = MiniSearch;
type MiniSearchOptions = { fuzzy?: number | boolean; prefix?: boolean; limit?: number; };
const DEFAULT_MINISEARCH_OPTIONS: Required<Pick<MiniSearchOptions, 'fuzzy' | 'prefix' | 'limit'>> = { fuzzy: 0.2, prefix: true, limit: 50, };
/**
- Converts a comma-delimited query string into MiniSearch format.
- Multi-word terms are wrapped in quotes for phrase matching.
- Example: "React Native, AWS, machine learning" -> ""React Native" AWS "machine learning""
*/
export function normalizeCommaDelimitedQuery(query: string): string {
return query
.split(',')
.map((term) => term.trim())
.filter((term) => term.length > 0)
.map((term) => (term.includes(' ') ?
"${term}": term)) .join(' '); }
export function createMiniSearchIndex(docs: SearchableDoc[], options?: MiniSearchOptions): MiniSearchIndex { const index = new MiniSearch({ fields: ['text'], storeFields: ['id', 'text'], searchOptions: { fuzzy: options?.fuzzy ?? DEFAULT_MINISEARCH_OPTIONS.fuzzy, prefix: options?.prefix ?? DEFAULT_MINISEARCH_OPTIONS.prefix, }, }); index.addAll(docs); return index; }
export function runMiniSearch( searcher: MiniSearchIndex, query: string, options?: MiniSearchOptions ): Array<{ id: string; score: number }> { const normalizedQuery = normalizeCommaDelimitedQuery(query); const results: SearchResult[] = searcher.search(normalizedQuery, { fuzzy: options?.fuzzy ?? DEFAULT_MINISEARCH_OPTIONS.fuzzy, prefix: options?.prefix ?? DEFAULT_MINISEARCH_OPTIONS.prefix, }); const limited = results.slice(0, options?.limit ?? DEFAULT_MINISEARCH_OPTIONS.limit); const maxScore = limited.length > 0 ? limited[0].score : 1; return limited.map((result) => ({ id: result.id, score: maxScore > 0 ? result.score / maxScore : 0, })); }
