import * as plugins from './smartfuzzy.plugins.js'; /** * Type for the search result returned by ArticleSearch */ export type IArticleSearchResult = { /** The matched article */ item: plugins.tsclass.content.IArticle; /** The index of the article in the original array */ refIndex: number; /** The match score (lower is better) */ score?: number; /** Information about where matches were found in the article */ matches?: ReadonlyArray<{ indices: ReadonlyArray; key?: string; value?: string; refIndex?: number; }>; } /** * Specialized search engine for articles with weighted field searching * * This class provides fuzzy searching against article content, with different weights * assigned to different parts of the article (title, tags, content) to provide * more relevant results. * * @example * ```typescript * const articles = [ * { * title: 'Getting Started with TypeScript', * content: 'TypeScript is a superset of JavaScript that adds static typing...', * tags: ['typescript', 'javascript', 'programming'], * author: 'John Doe', * timestamp: Date.now(), * featuredImageUrl: null, * url: 'https://example.com/typescript-intro' * } * ]; * * const articleSearch = new ArticleSearch(articles); * const results = await articleSearch.search('typescript'); * ``` */ export class ArticleSearch { /** * Collection of articles to search through */ public articles: plugins.tsclass.content.IArticle[] = []; /** * Flag indicating whether the search index needs to be updated */ public needsUpdate: boolean = false; /** * Promise manager for async operations */ private readyDeferred = plugins.smartpromise.defer(); /** * Fuse.js instance for searching */ private fuse: plugins.fuseJs; /** * Creates a new ArticleSearch instance * * @param articleArrayArg - Optional array of articles to initialize with */ constructor(articleArrayArg?: plugins.tsclass.content.IArticle[]) { // Validate input if provided if (articleArrayArg !== undefined && !Array.isArray(articleArrayArg)) { throw new Error('Article array must be an array'); } this.fuse = new plugins.fuseJs(this.articles); this.readyDeferred.resolve(); if (articleArrayArg) { for (const article of articleArrayArg) { // Validate each article has required fields if (!article || typeof article !== 'object') { throw new Error('Each article must be a valid object'); } // Require at least title field if (!article.title || typeof article.title !== 'string') { throw new Error('Each article must have a title string'); } this.addArticle(article); } } } /** * Adds an article to the collection and marks the index for updating * * @param articleArg - The article to add to the search collection * @returns void * * @example * ```typescript * articleSearch.addArticle({ * title: 'Advanced TypeScript Features', * content: 'This article covers advanced TypeScript concepts...', * tags: ['typescript', 'advanced'], * author: 'Jane Smith', * timestamp: Date.now(), * featuredImageUrl: null, * url: 'https://example.com/advanced-typescript' * }); * ``` */ public addArticle(articleArg: plugins.tsclass.content.IArticle): void { if (!articleArg || typeof articleArg !== 'object') { throw new Error('Article must be a valid object'); } // Require at least title field if (!articleArg.title || typeof articleArg.title !== 'string') { throw new Error('Article must have a title string'); } // Validate tags if present if (articleArg.tags !== undefined && !Array.isArray(articleArg.tags)) { throw new Error('Article tags must be an array of strings'); } this.articles.push(articleArg); this.needsUpdate = true; } /** * Performs a weighted fuzzy search across all articles * * The search uses the following weighting: * - Title: 3x importance * - Tags: 2x importance * - Content: 1x importance * * @param searchStringArg - The search query string * @returns Array of articles matched with their relevance score and match details * * @example * ```typescript * // Search for articles about TypeScript * const results = await articleSearch.search('typescript'); * * // Access the first (most relevant) result * if (results.length > 0) { * console.log(results[0].item.title); * * // See where the match was found * console.log(results[0].matches); * } * ``` */ public async search(searchStringArg: string): Promise { if (typeof searchStringArg !== 'string') { throw new Error('Search string must be a string'); } // Empty article collection should return empty results if (this.articles.length === 0) { return []; } if (this.needsUpdate) { const oldDeferred = this.readyDeferred; this.readyDeferred = plugins.smartpromise.defer(); this.needsUpdate = false; if (oldDeferred.status !== 'fulfilled') { this.readyDeferred.promise.then(oldDeferred.resolve); } this.fuse = new plugins.fuseJs(this.articles, { keys: [ { name: 'title', weight: 3, }, { name: 'tags', weight: 2, }, { name: 'content', weight: 1, }, ], includeMatches: true, }); this.readyDeferred.resolve(); } else { await this.readyDeferred.promise; } return this.fuse.search(searchStringArg); } }