generated from ttipo/project-base
feat(backend): 메일 검색 색인의 바탕(표 · 맞추기 · 지난 메일 채우기 · 준비 상태)을 더한다
14단계 자체 검색 색인의 1차다. 검색 API 를 색인으로 바꾸는 일과 대화 키 잇기는 다음 차례다. - 마이그레이션 AddMailSearchIndex 하나: 표 mail_search_messages · mail_search_states 와 mail_settings.group_by_conversation 열(3차의 대화로 묶어 보기 설정 — 같은 배포에서 한 번에 만든다) - 접기 · 글자 두 개 묶음 · 맞은 곳 · 말머리 떼기 순수 함수와 표본 표(나중에 frontend 와 짝) - 엔진 포트 MAIL_INDEX_SOURCE + JMAP 어댑터: Email/changes 맞추기(실시간 · 5분 · 검색 전, 서버 전체 동시 4), 밤 창(KST 22:00~02:45 · 05:00~07:30) 지난 메일 채우기, 전체 대조 - GET /api/search-index, 로그인 · 새 비밀번호 때 상태 줄, 퇴사 마무리에 표 둘 - 로그 가림에 조건 검색 이름 넷, 개발 환경 DB 쿼리 로그 끔(메일 글이 DB 에 든다) 리뷰 반영: 시간 끝은 실패가 아니라 이어 하기, 본문 읽기 묶음의 메일 크기 상한(32MiB · 16MiB 넘으면 혼자), 겹친 id 빼기, 사내 도메인 주소만 색인(개발 서버 cron 이 e2e 줄을 건드리지 않게). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,79 @@
|
||||
import { MigrationInterface, QueryRunner } from 'typeorm';
|
||||
|
||||
/**
|
||||
* 메일 검색 색인 표 (14단계 — 자체 검색 색인 · 대화 묶기의 바탕, 설계 A).
|
||||
* - mail_search_messages: 직원 주소 + 엔진 메일 id 마다 한 줄 — 메일함 · 키워드 · 받은 시각 · 답장 관계 · 원문 글(NFC) · 접기 글(fold_*) ·
|
||||
* 글자 두 개 묶음(grams) · 대화 키. 색인: PK, (직원, 받은 시각, id) · (직원, Message-ID) · (직원, 대화 키),
|
||||
* GIN 셋(grams · mailbox_ids · refs — TypeORM 이 GIN 을 적지 못해 엔티티에는 synchronize: false 로 이름만 있다)
|
||||
* - mail_search_states: 직원마다 한 줄 — 상태(building · ready) · 엔진 Email state · 채우기 자리 · 멈춘 까닭 · 실패 수
|
||||
* - mail_settings.group_by_conversation: 목록을 대화로 묶어 보기(기본 끔 — API 는 14단계 3차)
|
||||
* 기존 표의 칸 · 행은 지우지 않는다 — 표를 새로 만들고 칸 하나를 더하기만 한다.
|
||||
* (자동 생성: npm run migration:generate — GIN 색인 셋만 손으로 더했다. 엔티티는 modules/mail-search/*.entity.ts ·
|
||||
* modules/mail-user-settings/mail-user-settings.entity.ts)
|
||||
*/
|
||||
export class AddMailSearchIndex1793873715423 implements MigrationInterface {
|
||||
name = 'AddMailSearchIndex1793873715423';
|
||||
|
||||
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(
|
||||
`CREATE TABLE "mail_search_messages" ("user_email" character varying(254) NOT NULL, "email_id" character varying(64) NOT NULL, "mailbox_ids" text array NOT NULL, "keywords" text array NOT NULL, "received_at" TIMESTAMP WITH TIME ZONE NOT NULL, "size" integer NOT NULL, "message_id" text, "refs" text array NOT NULL, "subject" text NOT NULL, "subject_base" text NOT NULL, "from_text" text NOT NULL, "to_text" text NOT NULL, "cc_text" text NOT NULL, "attachment_names" text NOT NULL, "has_attachment" boolean NOT NULL, "preview" text NOT NULL, "body_text" text NOT NULL, "fold_from" text NOT NULL, "fold_to" text NOT NULL, "fold_subject" text NOT NULL, "fold_attachment" text NOT NULL, "fold_body" text NOT NULL, "grams" text array NOT NULL, "thread_key" character varying(64) NOT NULL, "engine_thread_id" character varying(64), "indexed_at" TIMESTAMP WITH TIME ZONE NOT NULL, CONSTRAINT "PK_bdeada2a9bbda7dece469d81b6f" PRIMARY KEY ("user_email", "email_id"))`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`CREATE INDEX "IDX_mail_search_messages_thread_key" ON "mail_search_messages" ("user_email", "thread_key") `,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`CREATE INDEX "IDX_mail_search_messages_message_id" ON "mail_search_messages" ("user_email", "message_id") `,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`CREATE INDEX "IDX_mail_search_messages_received" ON "mail_search_messages" ("user_email", "received_at", "email_id") `,
|
||||
);
|
||||
// GIN — 찾는 말의 글자 두 개 묶음이 모두 든 줄(grams @> …) · 메일함 범위(mailbox_ids && …) · 늦게 온 원본이 자식을 찾기(refs @> …)
|
||||
await queryRunner.query(
|
||||
`CREATE INDEX "IDX_mail_search_messages_grams" ON "mail_search_messages" USING GIN ("grams")`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`CREATE INDEX "IDX_mail_search_messages_mailbox_ids" ON "mail_search_messages" USING GIN ("mailbox_ids")`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`CREATE INDEX "IDX_mail_search_messages_refs" ON "mail_search_messages" USING GIN ("refs")`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`CREATE TABLE "mail_search_states" ("user_email" character varying(254) NOT NULL, "status" character varying(32) NOT NULL, "email_state" text, "build_anchor" character varying(64), "build_done" integer NOT NULL DEFAULT '0', "build_total" integer, "build_started_at" TIMESTAMP WITH TIME ZONE, "ready_at" TIMESTAMP WITH TIME ZONE, "last_synced_at" TIMESTAMP WITH TIME ZONE, "paused_reason" character varying(32), "failure_count" integer NOT NULL DEFAULT '0', "created_at" TIMESTAMP WITH TIME ZONE NOT NULL, "updated_at" TIMESTAMP WITH TIME ZONE NOT NULL, CONSTRAINT "PK_e8aa94c380c49ff05919e5177ab" PRIMARY KEY ("user_email"))`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`CREATE INDEX "IDX_mail_search_states_status" ON "mail_search_states" ("status") `,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "mail_settings" ADD "group_by_conversation" boolean NOT NULL DEFAULT false`,
|
||||
);
|
||||
}
|
||||
|
||||
public async down(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "mail_settings" DROP COLUMN "group_by_conversation"`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`DROP INDEX "public"."IDX_mail_search_states_status"`,
|
||||
);
|
||||
await queryRunner.query(`DROP TABLE "mail_search_states"`);
|
||||
await queryRunner.query(
|
||||
`DROP INDEX "public"."IDX_mail_search_messages_refs"`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`DROP INDEX "public"."IDX_mail_search_messages_mailbox_ids"`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`DROP INDEX "public"."IDX_mail_search_messages_grams"`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`DROP INDEX "public"."IDX_mail_search_messages_received"`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`DROP INDEX "public"."IDX_mail_search_messages_message_id"`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`DROP INDEX "public"."IDX_mail_search_messages_thread_key"`,
|
||||
);
|
||||
await queryRunner.query(`DROP TABLE "mail_search_messages"`);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user