add meili search

This commit is contained in:
liuyi 2025-06-01 21:24:43 +08:00
parent 7af6efc642
commit 02ccf58457
6 changed files with 40 additions and 5 deletions

View File

@ -6,6 +6,7 @@ import * as controllers from '@/modules/content/controllers';
import * as entities from '@/modules/content/entities'; import * as entities from '@/modules/content/entities';
import * as repositories from '@/modules/content/repositories'; import * as repositories from '@/modules/content/repositories';
import * as services from '@/modules/content/services'; import * as services from '@/modules/content/services';
import { SearchService } from '@/modules/content/services';
import { SanitizeService } from '@/modules/content/services/SanitizeService'; import { SanitizeService } from '@/modules/content/services/SanitizeService';
import { PostService } from '@/modules/content/services/post.service'; import { PostService } from '@/modules/content/services/post.service';
@ -31,23 +32,29 @@ export class ContentModule {
repositories.CategoryRepository, repositories.CategoryRepository,
repositories.TagRepository, repositories.TagRepository,
services.CategoryService, services.CategoryService,
{ token: services.SearchService, optional: true },
], ],
useFactory( useFactory(
postRepository: repositories.PostRepository, postRepository: repositories.PostRepository,
categoryRepository: repositories.CategoryRepository, categoryRepository: repositories.CategoryRepository,
tagRepository: repositories.TagRepository, tagRepository: repositories.TagRepository,
categoryService: services.CategoryService, categoryService: services.CategoryService,
searchService: SearchService,
) { ) {
return new PostService( return new PostService(
postRepository, postRepository,
categoryRepository, categoryRepository,
categoryService, categoryService,
tagRepository, tagRepository,
searchService,
config.SearchType, config.SearchType,
); );
}, },
}, },
]; ];
if (config.SearchType === 'meili') {
providers.push(services.SearchService);
}
return { return {
module: ContentModule, module: ContentModule,
imports: [ imports: [

View File

@ -32,6 +32,9 @@ export class QueryPostDto implements PaginateOptions {
@IsOptional() @IsOptional()
isPublished?: boolean; isPublished?: boolean;
@IsOptional()
search?: string;
@IsEnum(PostOrder, { @IsEnum(PostOrder, {
message: `The sorting rule must be one of ${Object.values(PostOrder).join(',')}`, message: `The sorting rule must be one of ${Object.values(PostOrder).join(',')}`,
}) })

View File

@ -1,3 +1,4 @@
export * from './category.service'; export * from './category.service';
export * from './tag.service'; export * from './tag.service';
export * from './comment.service'; export * from './comment.service';
export * from './search.service';

View File

@ -1,7 +1,7 @@
import { Injectable } from '@nestjs/common'; import { Injectable } from '@nestjs/common';
import { isNil } from '@nestjs/common/utils/shared.utils'; import { isNil } from '@nestjs/common/utils/shared.utils';
import { isArray, isFunction, omit } from 'lodash'; import { isArray, isFunction, omit, pick } from 'lodash';
import { EntityNotFoundError, In, IsNull, Not, SelectQueryBuilder } from 'typeorm'; import { EntityNotFoundError, In, IsNull, Not, SelectQueryBuilder } from 'typeorm';
import { PostOrder } from '@/modules/content/constants'; import { PostOrder } from '@/modules/content/constants';
@ -9,6 +9,7 @@ import { CreatePostDto, QueryPostDto, UpdatePostDto } from '@/modules/content/dt
import { PostEntity } from '@/modules/content/entities/post.entity'; import { PostEntity } from '@/modules/content/entities/post.entity';
import { CategoryRepository } from '@/modules/content/repositories'; import { CategoryRepository } from '@/modules/content/repositories';
import { PostRepository } from '@/modules/content/repositories/post.repository'; import { PostRepository } from '@/modules/content/repositories/post.repository';
import { SearchService } from '@/modules/content/services/search.service';
import { SearchType } from '@/modules/content/types'; import { SearchType } from '@/modules/content/types';
import { SelectTrashMode } from '@/modules/database/constants'; import { SelectTrashMode } from '@/modules/database/constants';
import { QueryHook } from '@/modules/database/types'; import { QueryHook } from '@/modules/database/types';
@ -29,10 +30,17 @@ export class PostService {
protected categoryRepository: CategoryRepository, protected categoryRepository: CategoryRepository,
protected categoryService: CategoryService, protected categoryService: CategoryService,
protected tagRepository: TagRepository, protected tagRepository: TagRepository,
protected searchService?: SearchService,
protected searchType: SearchType = 'mysql', protected searchType: SearchType = 'mysql',
) {} ) {}
async paginate(options: QueryPostDto, callback?: QueryHook<PostEntity>) { async paginate(options: QueryPostDto, callback?: QueryHook<PostEntity>) {
if (!isNil(this.searchService) && !isNil(options.search) && this.searchType === 'meili') {
return this.searchService.search(
options.search,
pick(options, ['trashed', 'page', 'limit']),
);
}
const qb = await this.buildListQuery(this.repository.buildBaseQB(), options, callback); const qb = await this.buildListQuery(this.repository.buildBaseQB(), options, callback);
return paginate(qb, options); return paginate(qb, options);
} }
@ -62,7 +70,11 @@ export class PostService {
publishedAt, publishedAt,
}; };
const item = await this.repository.save(createPostDto); const item = await this.repository.save(createPostDto);
return this.detail(item.id); const result = await this.detail(item.id);
if (!isNil(this.searchService)) {
await this.searchService.create(result);
}
return result;
} }
async update(data: UpdatePostDto) { async update(data: UpdatePostDto) {
@ -88,7 +100,11 @@ export class PostService {
...omit(data, ['id', 'publish', 'tags', 'category']), ...omit(data, ['id', 'publish', 'tags', 'category']),
publishedAt, publishedAt,
}); });
return this.detail(data.id); const result = await this.detail(data.id);
if (!isNil(this.searchService)) {
await this.searchService.update([result]);
}
return result;
} }
async delete(ids: string[], trash?: boolean) { async delete(ids: string[], trash?: boolean) {
@ -105,8 +121,15 @@ export class PostService {
...(await this.repository.remove(directs)), ...(await this.repository.remove(directs)),
...(await this.repository.softRemove(softs)), ...(await this.repository.softRemove(softs)),
]; ];
if (!isNil(this.searchService)) {
await this.searchService.delete(directs.map((item) => item.id));
await this.searchService.update(softs);
}
} else { } else {
result = await this.repository.remove(items); result = await this.repository.remove(items);
if (!isNil(this.searchService)) {
await this.searchService.delete(ids);
}
} }
return result; return result;
} }
@ -118,6 +141,7 @@ export class PostService {
.withDeleted() .withDeleted()
.getMany(); .getMany();
const trashes = items.filter((item) => !isNil(item.deleteAt)); const trashes = items.filter((item) => !isNil(item.deleteAt));
await this.searchService.update(trashes);
const trashedIds = trashes.map((item) => item.id); const trashedIds = trashes.map((item) => item.id);
if (trashedIds.length < 1) { if (trashedIds.length < 1) {
return []; return [];

View File

@ -14,7 +14,7 @@ import { SelectTrashMode } from '@/modules/database/constants';
import { MeiliService } from '@/modules/meilisearch/meili.service'; import { MeiliService } from '@/modules/meilisearch/meili.service';
export class SearchService implements OnModuleInit { export class SearchService implements OnModuleInit {
index = 'content'; private index = 'content';
protected client: MeiliSearch; protected client: MeiliSearch;

View File

@ -1,6 +1,6 @@
import { SelectTrashMode } from '@/modules/database/constants'; import { SelectTrashMode } from '@/modules/database/constants';
export type SearchType = 'mysql'; export type SearchType = 'mysql' | 'meili';
export interface ContentConfig { export interface ContentConfig {
SearchType?: SearchType; SearchType?: SearchType;