feat(endpoint): 添加/reindex/:configId用于重建meilisearch索引

This commit is contained in:
2025-10-23 08:39:40 +00:00
parent 445ee3fd92
commit 76b83a4dd8

View File

@ -1,9 +1,18 @@
import { defineEndpoint } from '@directus/extensions-sdk'; import { defineEndpoint } from '@directus/extensions-sdk';
import { MeiliSearch } from 'meilisearch';
import { MeiliDocs, MeiliIndexConfig, MeiliSearchConfig } from '../types/meilisearch';
import { getNestedProperty } from '../helper/nest';
import { createLogger } from '../logger';
import { buildQueryFields, filterTranslations } from '../helper/collection';
export default defineEndpoint({ export default defineEndpoint({
id: 'meilisearch', id: 'meilisearch',
handler: (router, context) => { handler: (router, context) => {
const logger = createLogger('meilisearch_endpoint');
router.get('/', (_req, res) => res.send('Hello, MeiliSearch!')); router.get('/', (_req, res) => res.send('Hello, MeiliSearch!'));
// 获取可用的集合列表
router.get('/collections', async (_req, res) => { router.get('/collections', async (_req, res) => {
const { services, getSchema } = context; const { services, getSchema } = context;
const { CollectionsService } = services; const { CollectionsService } = services;
@ -11,18 +20,39 @@ export default defineEndpoint({
const collSvc = new CollectionsService({ const collSvc = new CollectionsService({
schema, schema,
}) })
const collections = await collSvc.readByQuery(); const allCollections = await collSvc.readByQuery();
const visible = collections.filter(col => !col.meta?.hidden && !col.meta?.system && col.schema); const availableCollections = allCollections.filter((col) => {
const isVisible = !col.meta?.hidden;
const isUserDefined = !col.meta?.system;
const hasSchema = !!col.schema;
const isNotMeiliConfig = col.collection !== 'meili_index_configs' && col.collection !== 'meili_search_config';
const result = visible.map(col => ({ return isVisible && isUserDefined && hasSchema && isNotMeiliConfig;
});
const result = availableCollections.map(col => ({
collection: col.collection, collection: col.collection,
note: col.meta?.note || '', note: col.meta?.note || '',
icon: col.meta?.icon || '', icon: col.meta?.icon || '',
})); }));
const { ItemsService } = services;
const meiliIndexConfigsSvc = new ItemsService('meili_index_configs', {
schema,
});
// 读取已有配置
const existingConfigs = await meiliIndexConfigsSvc.readByQuery({ limit: -1 });
const existingMap = new Map(existingConfigs.map((config) => [config.collection_name, config]));
console.log('Existing MeiliSearch index configs:', existingConfigs);
console.log('Available collections for MeiliSearch indexing:', existingMap);
res.json(result); res.json(result);
}); });
// 测试连接 MeiliSearch 服务器
router.post('/connection-test', async (req, res) => { router.post('/connection-test', async (req, res) => {
const { host, apiKey } = req.body; const { host, apiKey } = req.body;
console.log(req.body); console.log(req.body);
@ -46,5 +76,97 @@ export default defineEndpoint({
return res.json({ success: false, message: `连接失败:${(error as Error).message}` }); return res.json({ success: false, message: `连接失败:${(error as Error).message}` });
} }
}); });
// 重建索引
router.post('/reindex/:configId?', async (req, res) => {
const { ItemsService } = context.services;
const schema = await context.getSchema();
// 读取Meilisearch索引配置
let configs = [];
const configService = new ItemsService<MeiliIndexConfig>('meili_index_configs', { schema });
const configId = req.params.configId;
if (configId) {
const cfg = await configService.readOne(configId);
if (!cfg) {
return res.status(404).json({ success: false, message: '配置未找到' });
}
if (!cfg.enabled) {
return res.status(400).json({ success: false, message: '该配置未启用' });
}
configs = [cfg];
} else {
// 读取所有启用的配置
const resp = await configService.readByQuery({ filter: { enabled: { _eq: true } }, limit: -1 });
configs = resp;
}
// 获取可用语言选项
let availableLanguages: string[] = [];
try {
const langService = new ItemsService('languages', { schema });
const languages = await langService.readByQuery({ limit: -1 });
availableLanguages = languages.map(lang => lang.code);
} catch (error) {
logger.error('Error fetching languages:', error);
}
// 读取 MeiliSearch 全局配置
const meiliService = new ItemsService<MeiliSearchConfig>('meili_search_config', { schema });
const meiliConfigs = await meiliService.readByQuery({ limit: 1 });
if (meiliConfigs.length === 0) {
return res.status(500).json({ success: false, message: 'MeiliSearch 全局配置未设置' });
}
const { host } = meiliConfigs[0] || { host: '', apiKey: '' };
const client = new MeiliSearch({
host,
})
const result: any[] = [];
for (const cfg of configs) {
const fields = cfg.fields;
const queryFields = buildQueryFields(fields);
const itemService = new ItemsService(cfg.collection_name, { schema });
const items = await itemService.readByQuery({ fields: queryFields, limit: -1 });
// 为每种语言重建索引
for (const lang of availableLanguages) {
const filteredItems = filterTranslations(items, lang);
const docs = filteredItems.map(item => {
const doc: MeiliDocs = { id: item.id };
for (const [key, value] of Object.entries(cfg.fields)) {
const fieldValue = getNestedProperty(item, value);
logger.info(`Mapping field ${key} to value: ${fieldValue}`);
doc[key] = fieldValue;
}
return doc;
});
const index = client.index(`${cfg.index_name}_${lang}`);
// 删除所有索引
await index.deleteAllDocuments();
try {
const enqueueRes = await index.addDocuments(docs);
result.push({ config: cfg.collection_name, enqueued: enqueueRes });
} catch (error) {
console.error(`Error reindexing collection ${cfg.collection_name}:`, error);
result.push({ config: cfg.collection_name, error: (error as Error).message });
}
// 处理结果
if (cfg.enabled) {
console.log(`Reindexing triggered for collection: ${cfg.collection_name}`);
} else {
console.log(`Skipping disabled config for collection: ${cfg.collection_name}`);
}
}
}
return res.json({ success: true, result });
});
} }
}); });