2026-01-06 08:22:43 +09:00
|
|
|
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);
|
|
|
|
|
|
2026-01-06 08:46:10 +09:00
|
|
|
// 검색 가능한 필드 설정 (순서가 우선순위 결정)
|
2026-01-06 08:22:43 +09:00
|
|
|
await index.updateSearchableAttributes([
|
|
|
|
|
"title",
|
|
|
|
|
"member_names",
|
2026-01-06 08:46:10 +09:00
|
|
|
"description",
|
2026-01-06 08:22:43 +09:00
|
|
|
"source_name",
|
2026-01-06 08:46:10 +09:00
|
|
|
"category_name",
|
2026-01-06 08:22:43 +09:00
|
|
|
]);
|
|
|
|
|
|
|
|
|
|
// 필터링 가능한 필드 설정
|
|
|
|
|
await index.updateFilterableAttributes(["category_id", "date"]);
|
|
|
|
|
|
|
|
|
|
// 정렬 가능한 필드 설정
|
|
|
|
|
await index.updateSortableAttributes(["date", "time"]);
|
|
|
|
|
|
2026-01-06 08:46:10 +09:00
|
|
|
// 랭킹 규칙 설정 (동일 유사도일 때 최신 날짜 우선)
|
|
|
|
|
await index.updateRankingRules([
|
|
|
|
|
"words", // 검색어 포함 개수
|
|
|
|
|
"typo", // 오타 수
|
|
|
|
|
"proximity", // 검색어 간 거리
|
|
|
|
|
"attribute", // 필드 우선순위
|
|
|
|
|
"exactness", // 정확도
|
|
|
|
|
"date:desc", // 동일 유사도 시 최신 날짜 우선
|
|
|
|
|
]);
|
|
|
|
|
|
2026-01-06 08:22:43 +09:00
|
|
|
// 오타 허용 설정 (typo tolerance)
|
|
|
|
|
await index.updateTypoTolerance({
|
|
|
|
|
enabled: true,
|
|
|
|
|
minWordSizeForTypos: {
|
|
|
|
|
oneTypo: 2,
|
|
|
|
|
twoTypos: 4,
|
|
|
|
|
},
|
|
|
|
|
});
|
|
|
|
|
|
2026-01-06 19:48:43 +09:00
|
|
|
// 페이징 설정 (기본 1000개 제한 해제)
|
|
|
|
|
await index.updatePagination({
|
|
|
|
|
maxTotalHits: 10000, // 최대 10000개까지 조회 가능
|
|
|
|
|
});
|
|
|
|
|
|
2026-01-06 08:22:43 +09:00
|
|
|
console.log("[Meilisearch] 인덱스 초기화 완료");
|
|
|
|
|
} catch (error) {
|
|
|
|
|
console.error("[Meilisearch] 초기화 오류:", error.message);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 일정 문서 추가/업데이트
|
|
|
|
|
*/
|
|
|
|
|
export async function addOrUpdateSchedule(schedule) {
|
|
|
|
|
try {
|
|
|
|
|
const index = client.index(SCHEDULE_INDEX);
|
|
|
|
|
|
2026-01-06 08:46:10 +09:00
|
|
|
// 멤버 이름을 쉼표로 구분하여 저장
|
2026-01-06 08:22:43 +09:00
|
|
|
const memberNames = schedule.members
|
2026-01-06 08:46:10 +09:00
|
|
|
? schedule.members.map((m) => m.name).join(",")
|
2026-01-06 08:22:43 +09:00
|
|
|
: "";
|
|
|
|
|
|
|
|
|
|
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);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
2026-01-06 19:48:43 +09:00
|
|
|
* 일정 검색 (페이징 지원)
|
2026-01-06 08:22:43 +09:00
|
|
|
*/
|
|
|
|
|
export async function searchSchedules(query, options = {}) {
|
|
|
|
|
try {
|
|
|
|
|
const index = client.index(SCHEDULE_INDEX);
|
|
|
|
|
|
|
|
|
|
const searchOptions = {
|
2026-01-06 19:48:43 +09:00
|
|
|
limit: options.limit || 1000, // 기본 1000개 (Meilisearch 최대)
|
|
|
|
|
offset: options.offset || 0, // 페이징용 offset
|
2026-01-06 08:22:43 +09:00
|
|
|
attributesToRetrieve: ["*"],
|
2026-01-10 19:06:49 +09:00
|
|
|
showRankingScore: true, // 유사도 점수 포함
|
2026-01-06 08:22:43 +09:00
|
|
|
};
|
|
|
|
|
|
|
|
|
|
// 카테고리 필터
|
|
|
|
|
if (options.categoryId) {
|
|
|
|
|
searchOptions.filter = `category_id = ${options.categoryId}`;
|
|
|
|
|
}
|
|
|
|
|
|
2026-01-06 08:46:10 +09:00
|
|
|
// 정렬 지정 시에만 적용 (기본은 유사도순)
|
|
|
|
|
if (options.sort) {
|
|
|
|
|
searchOptions.sort = options.sort;
|
|
|
|
|
}
|
2026-01-06 08:22:43 +09:00
|
|
|
|
|
|
|
|
const results = await index.search(query, searchOptions);
|
|
|
|
|
|
2026-01-10 19:06:49 +09:00
|
|
|
// 유사도 0.5 미만인 결과 필터링
|
|
|
|
|
const filteredHits = results.hits.filter((hit) => hit._rankingScore >= 0.5);
|
|
|
|
|
|
2026-01-06 19:48:43 +09:00
|
|
|
// 페이징 정보 포함 반환
|
|
|
|
|
return {
|
2026-01-10 19:06:49 +09:00
|
|
|
hits: filteredHits,
|
|
|
|
|
total: filteredHits.length, // 필터링 후 결과 수
|
2026-01-06 19:48:43 +09:00
|
|
|
offset: searchOptions.offset,
|
|
|
|
|
limit: searchOptions.limit,
|
|
|
|
|
};
|
2026-01-06 08:22:43 +09:00
|
|
|
} catch (error) {
|
|
|
|
|
console.error("[Meilisearch] 검색 오류:", error.message);
|
2026-01-06 19:48:43 +09:00
|
|
|
return { hits: [], total: 0, offset: 0, limit: 0 };
|
2026-01-06 08:22:43 +09:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 모든 일정 동기화 (초기 데이터 로드용)
|
|
|
|
|
*/
|
|
|
|
|
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 };
|