fromis_9/backend/services/meilisearch.js
caadiq 346d6529f2 Meilisearch 검색 엔진 도입
- Docker Compose에 Meilisearch 서비스 추가
- meilisearch.js 서비스 생성 (초기화, CRUD, 검색)
- 공개 일정 API에 Meilisearch 검색 통합
- 일정 생성/삭제 시 Meilisearch 자동 동기화
- YouTube 봇 일정 추가 시 Meilisearch 동기화
- sync-search API 추가 (기존 데이터 일괄 동기화)
- 다중 키워드, 오타 허용, 유사어 검색 지원
2026-01-06 08:22:43 +09:00

163 lines
4.3 KiB
JavaScript

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",
"description",
"member_names",
"category_name",
"source_name",
]);
// 필터링 가능한 필드 설정
await index.updateFilterableAttributes(["category_id", "date"]);
// 정렬 가능한 필드 설정
await index.updateSortableAttributes(["date", "time"]);
// 오타 허용 설정 (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}`;
}
// 정렬 (기본: 날짜 내림차순)
searchOptions.sort = options.sort || ["date:desc", "time:desc"];
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 };