add meili search
This commit is contained in:
parent
996f887d73
commit
7af6efc642
@ -1,4 +1,4 @@
|
||||
import { pick, unset } from 'lodash';
|
||||
import { isNil, pick, unset } from 'lodash';
|
||||
import { FindOptionsUtils, FindTreeOptions, TreeRepository, TreeRepositoryUtils } from 'typeorm';
|
||||
|
||||
import { CategoryEntity } from '@/modules/content/entities/category.entity';
|
||||
@ -111,4 +111,17 @@ export class CategoryRepository extends TreeRepository<CategoryEntity> {
|
||||
}
|
||||
return data as CategoryEntity[];
|
||||
}
|
||||
|
||||
async flatAncestorsTree(item: CategoryEntity) {
|
||||
let data: Omit<CategoryEntity, 'children'>[] = [];
|
||||
const category = await this.findAncestorsTree(item);
|
||||
const { parent } = category;
|
||||
unset(category, 'children');
|
||||
unset(category, 'item');
|
||||
data.push(item);
|
||||
if (!isNil(parent)) {
|
||||
data = [...(await this.flatAncestorsTree(parent)), ...data];
|
||||
}
|
||||
return data as CategoryEntity[];
|
||||
}
|
||||
}
|
||||
|
94
src/modules/content/services/search.service.ts
Normal file
94
src/modules/content/services/search.service.ts
Normal file
@ -0,0 +1,94 @@
|
||||
import { ForbiddenException, OnModuleInit } from '@nestjs/common';
|
||||
import { isNil, omit } from 'lodash';
|
||||
import { MeiliSearch } from 'meilisearch';
|
||||
|
||||
import { PostEntity } from '@/modules/content/entities';
|
||||
import {
|
||||
CategoryRepository,
|
||||
CommentRepository,
|
||||
PostRepository,
|
||||
} from '@/modules/content/repositories';
|
||||
import { SearchOption } from '@/modules/content/types';
|
||||
import { getSearchData, getSearchItem } from '@/modules/content/utils';
|
||||
import { SelectTrashMode } from '@/modules/database/constants';
|
||||
import { MeiliService } from '@/modules/meilisearch/meili.service';
|
||||
|
||||
export class SearchService implements OnModuleInit {
|
||||
index = 'content';
|
||||
|
||||
protected client: MeiliSearch;
|
||||
|
||||
constructor(
|
||||
protected meiliService: MeiliService,
|
||||
protected categoryRepository: CategoryRepository,
|
||||
protected commentRepository: CommentRepository,
|
||||
protected postRepository: PostRepository,
|
||||
) {
|
||||
this.client = this.meiliService.getClient();
|
||||
}
|
||||
|
||||
async onModuleInit(): Promise<any> {
|
||||
await this.client.deleteIndex(this.index);
|
||||
this.client.index(this.index).updateFilterableAttributes(['deleteAt', 'publishedAt']);
|
||||
this.client.index(this.index).updateSortableAttributes(['updatedAt', 'commentCount']);
|
||||
const posts = await this.postRepository.buildBaseQB().withDeleted().getMany();
|
||||
await this.client
|
||||
.index(this.index)
|
||||
.addDocuments(
|
||||
await getSearchData(posts, this.categoryRepository, this.commentRepository),
|
||||
);
|
||||
}
|
||||
|
||||
getClient() {
|
||||
if (isNil(this.client)) {
|
||||
throw new ForbiddenException('Has no meili search client!');
|
||||
}
|
||||
return this.client;
|
||||
}
|
||||
|
||||
async search(text: string, param: SearchOption = {}) {
|
||||
const option = { page: 1, limit: 10, trashed: SelectTrashMode.NONE, ...param };
|
||||
const limit = isNil(option.limit) || option.limit < 1 ? 1 : option.limit;
|
||||
const page = isNil(option.page) || option.page < 1 ? 1 : option.page;
|
||||
let filter = ['deletedAt IS NULL'];
|
||||
if (option.trashed === SelectTrashMode.ALL) {
|
||||
filter = [];
|
||||
} else if (option.trashed === SelectTrashMode.ONLY) {
|
||||
filter = ['deletedAt IS NOT NULL'];
|
||||
}
|
||||
if (option.isPublished) {
|
||||
filter.push('publishedAt IS NOT NULL');
|
||||
}
|
||||
const result = await this.client
|
||||
.index(this.index)
|
||||
.search(text, { page, limit, sort: ['updatedAt:desc', 'commentCount:desc'], filter });
|
||||
return {
|
||||
item: result.hits,
|
||||
currentPage: result.page,
|
||||
perPage: result.hitsPerPage,
|
||||
totalItems: result.estimatedTotalHits,
|
||||
itemCount: result.totalHits,
|
||||
...omit(result, ['hits', 'page', 'hitsPerPage', 'estimatedTotalHits', 'totalHits']),
|
||||
};
|
||||
}
|
||||
|
||||
async create(post: PostEntity) {
|
||||
return this.getClient()
|
||||
.index(this.index)
|
||||
.addDocuments(
|
||||
await getSearchItem(this.categoryRepository, this.commentRepository, post),
|
||||
);
|
||||
}
|
||||
|
||||
async update(posts: PostEntity[]) {
|
||||
return this.getClient()
|
||||
.index(this.index)
|
||||
.updateDocuments(
|
||||
await getSearchData(posts, this.categoryRepository, this.commentRepository),
|
||||
);
|
||||
}
|
||||
|
||||
async delete(ids: string[]) {
|
||||
return this.getClient().index(this.index).deleteDocuments(ids);
|
||||
}
|
||||
}
|
@ -1,5 +1,14 @@
|
||||
import { SelectTrashMode } from '@/modules/database/constants';
|
||||
|
||||
export type SearchType = 'mysql';
|
||||
|
||||
export interface ContentConfig {
|
||||
SearchType?: SearchType;
|
||||
}
|
||||
|
||||
export interface SearchOption {
|
||||
trashed?: SelectTrashMode;
|
||||
isPublished?: boolean;
|
||||
page?: number;
|
||||
limit?: number;
|
||||
}
|
||||
|
53
src/modules/content/utils.ts
Normal file
53
src/modules/content/utils.ts
Normal file
@ -0,0 +1,53 @@
|
||||
import { instanceToPlain } from 'class-transformer';
|
||||
import { isNil, pick } from 'lodash';
|
||||
|
||||
import { PostEntity } from '@/modules/content/entities';
|
||||
import { CategoryRepository, CommentRepository } from '@/modules/content/repositories';
|
||||
|
||||
export async function getSearchItem(
|
||||
categoryRepository: CategoryRepository,
|
||||
commentRepository: CommentRepository,
|
||||
post: PostEntity,
|
||||
) {
|
||||
const categories = isNil(post.category)
|
||||
? []
|
||||
: (await categoryRepository.flatAncestorsTree(post.category)).map((item) => ({
|
||||
id: item.id,
|
||||
name: item.name,
|
||||
}));
|
||||
const comments = (
|
||||
await commentRepository.find({
|
||||
relations: ['post'],
|
||||
where: { post: { id: post.id } },
|
||||
})
|
||||
).map((item) => ({ id: item.id, name: item.body }));
|
||||
return [
|
||||
{
|
||||
...pick(instanceToPlain(post), [
|
||||
'id',
|
||||
'title',
|
||||
'body',
|
||||
'summary',
|
||||
'commentCount',
|
||||
'deleteAt',
|
||||
'publishedAt',
|
||||
'createdAt',
|
||||
'updatedAt',
|
||||
]),
|
||||
categories,
|
||||
comments,
|
||||
tags: post.tags.map((item) => ({ id: item.id, name: item.name })),
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
export const getSearchData = async (
|
||||
posts: PostEntity[],
|
||||
categoryRepository: CategoryRepository,
|
||||
commentRepository: CommentRepository,
|
||||
) =>
|
||||
(
|
||||
await Promise.all(
|
||||
posts.map((post) => getSearchItem(categoryRepository, commentRepository, post)),
|
||||
)
|
||||
).reduce((o, n) => [...o, ...n], []);
|
Loading…
Reference in New Issue
Block a user