feat(endpoint): 添加/reindex/:configId用于重建meilisearch索引
This commit is contained in:
@ -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 });
|
||||||
|
|
||||||
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user