/** * Meilisearch indexer for search functionality */ import meilisearch from '../config/meilisearch.js'; import { Service } from '../models/Service.js'; import { Analysis } from '../models/Analysis.js'; const INDEX_NAME = 'services'; export class SearchIndexer { static index = null; /** * Initialize the search index */ static async init() { try { console.log('Initializing Meilisearch...'); // Check if index exists const indexes = await meilisearch.getIndexes(); const indexExists = indexes.results.some(idx => idx.uid === INDEX_NAME); if (!indexExists) { console.log('Creating search index...'); await meilisearch.createIndex(INDEX_NAME, { primaryKey: 'id' }); } this.index = meilisearch.index(INDEX_NAME); // Configure searchable attributes await this.index.updateSettings({ searchableAttributes: [ 'name', 'findings.positive.title', 'findings.positive.description', 'findings.negative.title', 'findings.negative.description', 'data_types_collected', 'third_parties.name' ], filterableAttributes: ['grade', 'overall_score'], sortableAttributes: ['name', 'created_at', 'overall_score'], rankingRules: [ 'words', 'typo', 'proximity', 'attribute', 'sort', 'exactness' ] }); console.log('Meilisearch initialized successfully'); } catch (error) { console.error('Meilisearch initialization error:', error); throw error; } } /** * Index a single service * @param {Object} service - Service with analysis */ static async indexService(service) { try { if (!this.index) { await this.init(); } const document = { id: service.id, name: service.name, url: service.url, logo_url: service.logo_url, grade: service.grade, overall_score: service.overall_score, findings: service.findings || { positive: [], negative: [], neutral: [] }, data_types_collected: service.data_types_collected || [], third_parties: service.third_parties || [], last_analyzed: service.last_analyzed, created_at: service.created_at }; await this.index.addDocuments([document]); console.log(`Indexed service: ${service.name}`); } catch (error) { console.error(`Failed to index service ${service.name}:`, error); throw error; } } /** * Index all services */ static async indexAll() { try { console.log('Indexing all services...'); const services = await Service.findAllWithLatestAnalysis(); const documents = services.map(service => ({ id: service.id, name: service.name, url: service.url, logo_url: service.logo_url, grade: service.grade, overall_score: service.overall_score, findings: service.findings || { positive: [], negative: [], neutral: [] }, data_types_collected: service.data_types_collected || [], third_parties: service.third_parties || [], last_analyzed: service.last_analyzed, created_at: service.created_at })); if (documents.length > 0) { await this.index.addDocuments(documents); console.log(`Indexed ${documents.length} services`); } return { indexed: documents.length }; } catch (error) { console.error('Bulk indexing error:', error); throw error; } } /** * Search services * @param {string} query - Search query * @param {Object} options - Search options */ static async search(query, options = {}) { try { if (!this.index) { await this.init(); } const searchOptions = { limit: options.limit || 25, offset: options.offset || 0, ...options }; const results = await this.index.search(query, searchOptions); return results; } catch (error) { console.error('Search error:', error); throw error; } } /** * Delete a service from the index * @param {number} serviceId - Service ID */ static async deleteService(serviceId) { try { if (!this.index) { await this.init(); } await this.index.deleteDocument(serviceId); console.log(`Deleted service ${serviceId} from index`); } catch (error) { console.error(`Failed to delete service ${serviceId}:`, error); throw error; } } /** * Get index stats */ static async getStats() { try { if (!this.index) { await this.init(); } const stats = await this.index.getStats(); return stats; } catch (error) { console.error('Failed to get stats:', error); throw error; } } }