Merge a4c74c1d95
into d4654dd7bd
This commit is contained in:
commit
7ef101849f
|
@ -17,6 +17,7 @@ import { CacheService } from '@/core/CacheService.js';
|
||||||
import { QueryService } from '@/core/QueryService.js';
|
import { QueryService } from '@/core/QueryService.js';
|
||||||
import { IdService } from '@/core/IdService.js';
|
import { IdService } from '@/core/IdService.js';
|
||||||
import { LoggerService } from '@/core/LoggerService.js';
|
import { LoggerService } from '@/core/LoggerService.js';
|
||||||
|
import Logger from '@/logger.js';
|
||||||
import type { Index, MeiliSearch } from 'meilisearch';
|
import type { Index, MeiliSearch } from 'meilisearch';
|
||||||
|
|
||||||
type K = string;
|
type K = string;
|
||||||
|
@ -46,6 +47,17 @@ export type SearchPagination = {
|
||||||
limit: number;
|
limit: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type ReIndexNotesResult = {
|
||||||
|
/** 再インデックスしたノートの数 */
|
||||||
|
fetchedCount: number;
|
||||||
|
/** 最後にインデックスしたノートのID */
|
||||||
|
lastNoteId?: MiNote['id'];
|
||||||
|
/** 最後にインデックスしたノートの投稿日時 */
|
||||||
|
lastNoteDate?: Date;
|
||||||
|
/** 再インデックスに失敗したノートのID */
|
||||||
|
errorNoteIds: MiNote['id'][];
|
||||||
|
};
|
||||||
|
|
||||||
function compileValue(value: V): string {
|
function compileValue(value: V): string {
|
||||||
if (typeof value === 'string') {
|
if (typeof value === 'string') {
|
||||||
return `'${value}'`; // TODO: escape
|
return `'${value}'`; // TODO: escape
|
||||||
|
@ -76,9 +88,13 @@ function compileQuery(q: Q): string {
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class SearchService {
|
export class SearchService {
|
||||||
|
public static MeilisearchNotActiveError = class extends Error {
|
||||||
|
};
|
||||||
|
|
||||||
private readonly meilisearchIndexScope: 'local' | 'global' | string[] = 'local';
|
private readonly meilisearchIndexScope: 'local' | 'global' | string[] = 'local';
|
||||||
private readonly meilisearchNoteIndex: Index | null = null;
|
private readonly meilisearchNoteIndex: Index | null = null;
|
||||||
private readonly provider: FulltextSearchProvider;
|
private readonly provider: FulltextSearchProvider;
|
||||||
|
private readonly logger: Logger;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
@Inject(DI.config)
|
@Inject(DI.config)
|
||||||
|
@ -126,7 +142,24 @@ export class SearchService {
|
||||||
}
|
}
|
||||||
|
|
||||||
this.provider = config.fulltextSearch?.provider ?? 'sqlLike';
|
this.provider = config.fulltextSearch?.provider ?? 'sqlLike';
|
||||||
this.loggerService.getLogger('SearchService').info(`-- Provider: ${this.provider}`);
|
this.logger = this.loggerService.getLogger('SearchService');
|
||||||
|
this.logger.info(`-- Provider: ${this.provider}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
@bindThis
|
||||||
|
private async addDocument(note: Pick<MiNote, 'id' | 'userId' | 'userHost' | 'channelId' | 'cw' | 'text' | 'tags'>) {
|
||||||
|
return this.meilisearchNoteIndex?.addDocuments([{
|
||||||
|
id: note.id,
|
||||||
|
createdAt: this.idService.parse(note.id).date.getTime(),
|
||||||
|
userId: note.userId,
|
||||||
|
userHost: note.userHost,
|
||||||
|
channelId: note.channelId,
|
||||||
|
cw: note.cw,
|
||||||
|
text: note.text,
|
||||||
|
tags: note.tags,
|
||||||
|
}], {
|
||||||
|
primaryKey: 'id',
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@bindThis
|
@bindThis
|
||||||
|
@ -150,18 +183,7 @@ export class SearchService {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
await this.meilisearchNoteIndex?.addDocuments([{
|
await this.addDocument(note);
|
||||||
id: note.id,
|
|
||||||
createdAt: this.idService.parse(note.id).date.getTime(),
|
|
||||||
userId: note.userId,
|
|
||||||
userHost: note.userHost,
|
|
||||||
channelId: note.channelId,
|
|
||||||
cw: note.cw,
|
|
||||||
text: note.text,
|
|
||||||
tags: note.tags,
|
|
||||||
}], {
|
|
||||||
primaryKey: 'id',
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@bindThis
|
@bindThis
|
||||||
|
@ -172,6 +194,106 @@ export class SearchService {
|
||||||
await this.meilisearchNoteIndex?.deleteDocument(note.id);
|
await this.meilisearchNoteIndex?.deleteDocument(note.id);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@bindThis
|
||||||
|
public async unindexNoteAll() {
|
||||||
|
await this.meilisearchNoteIndex?.deleteAllDocuments();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 一定期間の間に投稿されたノートをmeilisearchに再インデックスする.
|
||||||
|
*/
|
||||||
|
@bindThis
|
||||||
|
public async reIndexNotes(props: {
|
||||||
|
sinceDate?: number | null;
|
||||||
|
untilDate?: number | null;
|
||||||
|
}): Promise<ReIndexNotesResult> {
|
||||||
|
if (this.provider !== 'meilisearch' || !this.meilisearch || !this.meilisearchNoteIndex) {
|
||||||
|
throw new SearchService.MeilisearchNotActiveError();
|
||||||
|
}
|
||||||
|
|
||||||
|
const fetchNote = (sinceId?: MiNote['id'], untilId?: MiNote['id'], take?: number, limit?: number) => {
|
||||||
|
const query = this.notesRepository.createQueryBuilder('note')
|
||||||
|
// 速い条件だけ先に
|
||||||
|
.andWhere('note.visibility IN (:...visibilities)', { visibilities: ['home', 'public'] });
|
||||||
|
|
||||||
|
if (sinceId) {
|
||||||
|
query.andWhere('note.id > :sinceId', { sinceId });
|
||||||
|
}
|
||||||
|
|
||||||
|
if (untilId) {
|
||||||
|
query.andWhere('note.id < :untilId', { untilId });
|
||||||
|
}
|
||||||
|
|
||||||
|
query.andWhere('note.text IS NOT NULL OR note.cw IS NOT NULL');
|
||||||
|
|
||||||
|
switch (this.meilisearchIndexScope) {
|
||||||
|
case 'global': {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case 'local': {
|
||||||
|
query.andWhere('note.userHost IS NULL');
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
default: {
|
||||||
|
query.andWhere('note.userHost IN (:...userHosts)', { userHosts: this.meilisearchIndexScope });
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return query
|
||||||
|
.select([
|
||||||
|
'note.id',
|
||||||
|
'note.userId',
|
||||||
|
'note.userHost',
|
||||||
|
'note.channelId',
|
||||||
|
'note.cw',
|
||||||
|
'note.text',
|
||||||
|
'note.tags',
|
||||||
|
])
|
||||||
|
.orderBy('note.id', 'DESC')
|
||||||
|
.take(take)
|
||||||
|
.limit(limit)
|
||||||
|
.getMany();
|
||||||
|
};
|
||||||
|
|
||||||
|
this.logger.info('-- Start Re-indexing notes...');
|
||||||
|
|
||||||
|
const sinceId = props.sinceDate ? this.idService.gen(props.sinceDate) : undefined;
|
||||||
|
const untilId = props.untilDate ? this.idService.gen(props.untilDate) : undefined;
|
||||||
|
const errorNoteIds: MiNote['id'][] = [];
|
||||||
|
let lastNoteId: MiNote['id'] | undefined = undefined;
|
||||||
|
let fetchedCount = 0;
|
||||||
|
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
|
||||||
|
while (true) {
|
||||||
|
const notes = await fetchNote(sinceId, untilId, fetchedCount, 100);
|
||||||
|
if (notes.length === 0) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const note of notes) {
|
||||||
|
try {
|
||||||
|
await this.addDocument(note);
|
||||||
|
} catch (err) {
|
||||||
|
this.logger.error(`-- Failed to index note: ${note.id}`, err as Error);
|
||||||
|
errorNoteIds.push(note.id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
lastNoteId = notes[notes.length - 1].id;
|
||||||
|
fetchedCount += notes.length;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.logger.info(`-- Re-indexing finished. Total: ${fetchedCount}`);
|
||||||
|
|
||||||
|
return {
|
||||||
|
fetchedCount: fetchedCount,
|
||||||
|
lastNoteId: lastNoteId,
|
||||||
|
lastNoteDate: lastNoteId ? this.idService.parse(lastNoteId).date : undefined,
|
||||||
|
errorNoteIds: errorNoteIds,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
@bindThis
|
@bindThis
|
||||||
public async searchNote(
|
public async searchNote(
|
||||||
q: string,
|
q: string,
|
||||||
|
@ -222,7 +344,7 @@ export class SearchService {
|
||||||
if (this.config.fulltextSearch?.provider === 'sqlPgroonga') {
|
if (this.config.fulltextSearch?.provider === 'sqlPgroonga') {
|
||||||
query.andWhere('note.text &@~ :q', { q });
|
query.andWhere('note.text &@~ :q', { q });
|
||||||
} else {
|
} else {
|
||||||
query.andWhere('LOWER(note.text) LIKE :q', { q: `%${ sqlLikeEscape(q.toLowerCase()) }%` });
|
query.andWhere('LOWER(note.text) LIKE :q', { q: `%${sqlLikeEscape(q.toLowerCase())}%` });
|
||||||
}
|
}
|
||||||
|
|
||||||
if (opts.host) {
|
if (opts.host) {
|
||||||
|
|
Loading…
Reference in New Issue