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 { FindOptionsUtils, FindTreeOptions, TreeRepository, TreeRepositoryUtils } from 'typeorm';
|
||||||
|
|
||||||
import { CategoryEntity } from '@/modules/content/entities/category.entity';
|
import { CategoryEntity } from '@/modules/content/entities/category.entity';
|
||||||
@ -111,4 +111,17 @@ export class CategoryRepository extends TreeRepository<CategoryEntity> {
|
|||||||
}
|
}
|
||||||
return data as 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 type SearchType = 'mysql';
|
||||||
|
|
||||||
export interface ContentConfig {
|
export interface ContentConfig {
|
||||||
SearchType?: SearchType;
|
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