diff --git a/src/meilisearch_endpoint/index.ts b/src/meilisearch_endpoint/index.ts index a1962e5..84686dc 100644 --- a/src/meilisearch_endpoint/index.ts +++ b/src/meilisearch_endpoint/index.ts @@ -1,9 +1,18 @@ 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({ id: 'meilisearch', handler: (router, context) => { + const logger = createLogger('meilisearch_endpoint'); + router.get('/', (_req, res) => res.send('Hello, MeiliSearch!')); + + // 获取可用的集合列表 router.get('/collections', async (_req, res) => { const { services, getSchema } = context; const { CollectionsService } = services; @@ -11,18 +20,39 @@ export default defineEndpoint({ const collSvc = new CollectionsService({ 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, note: col.meta?.note || '', 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); }); + + // 测试连接 MeiliSearch 服务器 router.post('/connection-test', async (req, res) => { const { host, apiKey } = req.body; console.log(req.body); @@ -46,5 +76,97 @@ export default defineEndpoint({ 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('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('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 }); + + }); } });