import { MeiliSearch } from "meilisearch"; // Meilisearch 클라이언트 초기화 const client = new MeiliSearch({ host: "http://fromis9-meilisearch:7700", apiKey: process.env.MEILI_MASTER_KEY, }); const SCHEDULE_INDEX = "schedules"; /** * 인덱스 초기화 및 설정 */ export async function initMeilisearch() { try { // 인덱스 생성 (이미 존재하면 무시) await client.createIndex(SCHEDULE_INDEX, { primaryKey: "id" }); // 인덱스 설정 const index = client.index(SCHEDULE_INDEX); // 검색 가능한 필드 설정 (순서가 우선순위 결정) await index.updateSearchableAttributes([ "title", "member_names", "description", "source_name", "category_name", ]); // 필터링 가능한 필드 설정 await index.updateFilterableAttributes(["category_id", "date"]); // 정렬 가능한 필드 설정 await index.updateSortableAttributes(["date", "time"]); // 랭킹 규칙 설정 (동일 유사도일 때 최신 날짜 우선) await index.updateRankingRules([ "words", // 검색어 포함 개수 "typo", // 오타 수 "proximity", // 검색어 간 거리 "attribute", // 필드 우선순위 "exactness", // 정확도 "date:desc", // 동일 유사도 시 최신 날짜 우선 ]); // 오타 허용 설정 (typo tolerance) await index.updateTypoTolerance({ enabled: true, minWordSizeForTypos: { oneTypo: 2, twoTypos: 4, }, }); console.log("[Meilisearch] 인덱스 초기화 완료"); } catch (error) { console.error("[Meilisearch] 초기화 오류:", error.message); } } /** * 일정 문서 추가/업데이트 */ export async function addOrUpdateSchedule(schedule) { try { const index = client.index(SCHEDULE_INDEX); // 멤버 이름을 쉼표로 구분하여 저장 const memberNames = schedule.members ? schedule.members.map((m) => m.name).join(",") : ""; const document = { id: schedule.id, title: schedule.title, description: schedule.description || "", date: schedule.date, time: schedule.time || "", category_id: schedule.category_id, category_name: schedule.category_name || "", category_color: schedule.category_color || "", source_name: schedule.source_name || "", source_url: schedule.source_url || "", member_names: memberNames, }; await index.addDocuments([document]); console.log(`[Meilisearch] 일정 추가/업데이트: ${schedule.id}`); } catch (error) { console.error("[Meilisearch] 문서 추가 오류:", error.message); } } /** * 일정 문서 삭제 */ export async function deleteSchedule(scheduleId) { try { const index = client.index(SCHEDULE_INDEX); await index.deleteDocument(scheduleId); console.log(`[Meilisearch] 일정 삭제: ${scheduleId}`); } catch (error) { console.error("[Meilisearch] 문서 삭제 오류:", error.message); } } /** * 일정 검색 */ export async function searchSchedules(query, options = {}) { try { const index = client.index(SCHEDULE_INDEX); const searchOptions = { limit: options.limit || 50, attributesToRetrieve: ["*"], }; // 카테고리 필터 if (options.categoryId) { searchOptions.filter = `category_id = ${options.categoryId}`; } // 정렬 지정 시에만 적용 (기본은 유사도순) if (options.sort) { searchOptions.sort = options.sort; } const results = await index.search(query, searchOptions); return results.hits; } catch (error) { console.error("[Meilisearch] 검색 오류:", error.message); return []; } } /** * 모든 일정 동기화 (초기 데이터 로드용) */ export async function syncAllSchedules(schedules) { try { const index = client.index(SCHEDULE_INDEX); // 기존 문서 모두 삭제 await index.deleteAllDocuments(); // 문서 변환 const documents = schedules.map((schedule) => ({ id: schedule.id, title: schedule.title, description: schedule.description || "", date: schedule.date, time: schedule.time || "", category_id: schedule.category_id, category_name: schedule.category_name || "", category_color: schedule.category_color || "", source_name: schedule.source_name || "", source_url: schedule.source_url || "", member_names: schedule.member_names || "", })); // 일괄 추가 await index.addDocuments(documents); console.log(`[Meilisearch] ${documents.length}개 일정 동기화 완료`); return documents.length; } catch (error) { console.error("[Meilisearch] 동기화 오류:", error.message); return 0; } } export { client };