@open-mercato/search 0.4.11-develop.2635.9f9e474720 → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/fulltext/drivers/meilisearch/index.js +2 -2
- package/dist/fulltext/drivers/meilisearch/index.js.map +1 -1
- package/dist/modules/search/cli.js +2 -2
- package/dist/modules/search/cli.js.map +1 -1
- package/package.json +20 -16
- package/src/__tests__/service.test.ts +9 -3
- package/src/fulltext/drivers/meilisearch/index.ts +4 -4
- package/src/modules/search/cli.ts +2 -2
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { Meilisearch } from "meilisearch";
|
|
2
2
|
import { resolveTimeoutMs } from "@open-mercato/shared/lib/http/fetchWithTimeout";
|
|
3
3
|
import { extractSearchableFields } from "../../../lib/field-policy.js";
|
|
4
4
|
const DEFAULT_MEILISEARCH_REQUEST_TIMEOUT_MS = 3e4;
|
|
@@ -21,7 +21,7 @@ function createMeilisearchDriver(options) {
|
|
|
21
21
|
const initializingIndexes = /* @__PURE__ */ new Map();
|
|
22
22
|
function getClient() {
|
|
23
23
|
if (!client) {
|
|
24
|
-
client = new
|
|
24
|
+
client = new Meilisearch({ host, apiKey, timeout: requestTimeoutMs });
|
|
25
25
|
}
|
|
26
26
|
return client;
|
|
27
27
|
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../../src/fulltext/drivers/meilisearch/index.ts"],
|
|
4
|
-
"sourcesContent": ["import { MeiliSearch } from 'meilisearch'\nimport type { EntityId } from '@open-mercato/shared/modules/entities'\nimport type { SearchFieldPolicy } from '@open-mercato/shared/modules/search'\nimport { resolveTimeoutMs } from '@open-mercato/shared/lib/http/fetchWithTimeout'\nimport type {\n FullTextSearchDriver,\n FullTextSearchDocument,\n FullTextSearchQuery,\n FullTextSearchHit,\n DocumentLookupKey,\n IndexStats,\n} from '../../types'\nimport { extractSearchableFields, type EncryptionMapEntry } from '../../../lib/field-policy'\n\n\nexport type MeilisearchDriverOptions = {\n host?: string\n apiKey?: string\n indexPrefix?: string\n defaultLimit?: number\n timeoutMs?: number\n encryptionMapResolver?: (entityId: EntityId) => Promise<EncryptionMapEntry[]>\n fieldPolicyResolver?: (entityId: EntityId) => SearchFieldPolicy | undefined\n}\n\nconst DEFAULT_MEILISEARCH_REQUEST_TIMEOUT_MS = 30_000\n\nfunction resolveMeilisearchTimeoutMs(explicit?: number): number {\n if (typeof explicit === 'number') return resolveTimeoutMs(explicit, DEFAULT_MEILISEARCH_REQUEST_TIMEOUT_MS)\n const raw = process.env.MEILISEARCH_REQUEST_TIMEOUT_MS\n const parsed = raw ? Number.parseInt(raw, 10) : undefined\n return resolveTimeoutMs(parsed, DEFAULT_MEILISEARCH_REQUEST_TIMEOUT_MS)\n}\n\nexport function createMeilisearchDriver(\n options?: MeilisearchDriverOptions\n): FullTextSearchDriver {\n const host = options?.host ?? process.env.MEILISEARCH_HOST ?? ''\n const apiKey = options?.apiKey ?? process.env.MEILISEARCH_API_KEY ?? ''\n const indexPrefix = options?.indexPrefix ?? process.env.MEILISEARCH_INDEX_PREFIX ?? 'om'\n const defaultLimit = options?.defaultLimit ?? 20\n const requestTimeoutMs = resolveMeilisearchTimeoutMs(options?.timeoutMs)\n const encryptionMapResolver = options?.encryptionMapResolver\n const fieldPolicyResolver = options?.fieldPolicyResolver\n\n let client: MeiliSearch | null = null\n const initializedIndexes = new Set<string>()\n const initializingIndexes = new Map<string, Promise<void>>()\n\n function getClient(): MeiliSearch {\n if (!client) {\n client = new MeiliSearch({ host, apiKey, timeout: requestTimeoutMs })\n }\n return client\n }\n\n function buildIndexName(tenantId: string): string {\n const sanitized = tenantId.replace(/[^a-zA-Z0-9_-]/g, '_')\n return `${indexPrefix}_${sanitized}`\n }\n\n function escapeFilterValue(value: string): string {\n return value.replace(/[\"\\\\]/g, '\\\\$&')\n }\n\n function buildFilters(options: FullTextSearchQuery): string[] {\n const filters: string[] = []\n\n if (options.organizationId) {\n filters.push(`_organizationId = \"${escapeFilterValue(options.organizationId)}\"`)\n }\n\n if (options.entityTypes?.length) {\n const entityFilter = options.entityTypes.map((t) => `\"${escapeFilterValue(t)}\"`).join(', ')\n filters.push(`_entityId IN [${entityFilter}]`)\n }\n\n return filters\n }\n\n async function doEnsureIndex(indexName: string): Promise<void> {\n const meiliClient = getClient()\n\n try {\n await meiliClient.createIndex(indexName, { primaryKey: '_id' })\n } catch (error: unknown) {\n const meilisearchError = error as { code?: string }\n if (meilisearchError.code !== 'index_already_exists') {\n throw error\n }\n }\n\n const index = meiliClient.index(indexName)\n await index.updateSettings({\n searchableAttributes: ['*'],\n filterableAttributes: ['_entityId', '_organizationId'],\n sortableAttributes: ['_indexedAt'],\n typoTolerance: {\n enabled: true,\n minWordSizeForTypos: {\n oneTypo: 4,\n twoTypos: 8,\n },\n },\n })\n\n initializedIndexes.add(indexName)\n }\n\n async function ensureIndex(indexName: string): Promise<void> {\n if (initializedIndexes.has(indexName)) {\n return\n }\n\n const existingPromise = initializingIndexes.get(indexName)\n if (existingPromise) {\n return existingPromise\n }\n\n const initPromise = doEnsureIndex(indexName)\n initializingIndexes.set(indexName, initPromise)\n\n try {\n await initPromise\n } finally {\n initializingIndexes.delete(indexName)\n }\n }\n\n async function prepareDocument(doc: FullTextSearchDocument): Promise<Record<string, unknown>> {\n // When encryptionMapResolver is provided, SEARCH_EXCLUDE_ENCRYPTED_FIELDS is enabled\n const excludeEncrypted = Boolean(encryptionMapResolver)\n const encryptedFields = encryptionMapResolver\n ? await encryptionMapResolver(doc.entityId)\n : []\n const fieldPolicy = fieldPolicyResolver?.(doc.entityId)\n\n const searchableFields = extractSearchableFields(doc.fields, {\n encryptedFields,\n fieldPolicy,\n })\n\n // When SEARCH_EXCLUDE_ENCRYPTED_FIELDS is enabled:\n // - Exclude sensitive parts of presenter (title, subtitle) - these are derived from encrypted fields\n // - Keep non-sensitive parts (icon, badge)\n // - Sanitize link labels (they often contain names derived from encrypted fields)\n // - Title/subtitle/link labels will be enriched at search time from the database\n let presenter = doc.presenter\n let links = doc.links\n if (excludeEncrypted) {\n if (presenter) {\n presenter = {\n ...presenter,\n title: '', // Will be enriched at search time\n subtitle: undefined, // Will be enriched at search time\n }\n }\n // Sanitize link labels - they often contain sensitive data (names, etc.)\n if (links && links.length > 0) {\n links = links.map((link) => ({\n ...link,\n label: link.kind === 'primary' ? 'Open' : 'View', // Generic labels\n }))\n }\n }\n\n return {\n _id: doc.recordId,\n _entityId: doc.entityId,\n _organizationId: doc.organizationId,\n _presenter: presenter,\n _url: doc.url,\n _links: links,\n _indexedAt: new Date().toISOString(),\n ...searchableFields,\n }\n }\n\n const driver: FullTextSearchDriver = {\n id: 'meilisearch',\n\n async ensureReady(): Promise<void> {\n // Client is lazily initialized\n },\n\n async isHealthy(): Promise<boolean> {\n if (!host) {\n return false\n }\n\n try {\n const meiliClient = getClient()\n await meiliClient.health()\n return true\n } catch {\n return false\n }\n },\n\n async search(query: string, options: FullTextSearchQuery): Promise<FullTextSearchHit[]> {\n const meiliClient = getClient()\n const indexName = buildIndexName(options.tenantId)\n\n try {\n const index = meiliClient.index(indexName)\n const filters = buildFilters(options)\n\n const response = await index.search(query, {\n limit: options.limit ?? defaultLimit,\n offset: options.offset,\n filter: filters.length > 0 ? filters.join(' AND ') : undefined,\n showRankingScore: true,\n })\n\n return response.hits.map((hit: Record<string, unknown>) => ({\n recordId: hit._id as string,\n entityId: hit._entityId as EntityId,\n score: (hit._rankingScore as number) ?? 0.5,\n presenter: hit._presenter as FullTextSearchHit['presenter'],\n url: hit._url as string | undefined,\n links: hit._links as FullTextSearchHit['links'],\n metadata: hit._metadata as Record<string, unknown> | undefined,\n }))\n } catch (error: unknown) {\n const meilisearchError = error as { code?: string }\n if (meilisearchError.code === 'index_not_found') {\n return []\n }\n throw error\n }\n },\n\n async index(doc: FullTextSearchDocument): Promise<void> {\n const meiliClient = getClient()\n const indexName = buildIndexName(doc.tenantId)\n\n await ensureIndex(indexName)\n\n const document = await prepareDocument(doc)\n\n const index = meiliClient.index(indexName)\n await index.addDocuments([document], { primaryKey: '_id' })\n },\n\n async delete(recordId: string, tenantId: string): Promise<void> {\n const meiliClient = getClient()\n const indexName = buildIndexName(tenantId)\n\n try {\n const index = meiliClient.index(indexName)\n await index.deleteDocument(recordId)\n } catch (error: unknown) {\n const meilisearchError = error as { code?: string }\n if (meilisearchError.code === 'index_not_found') {\n return\n }\n throw error\n }\n },\n\n async bulkIndex(docs: FullTextSearchDocument[]): Promise<void> {\n if (docs.length === 0) return\n\n // Group documents by tenant\n const byTenant = new Map<string, FullTextSearchDocument[]>()\n for (const doc of docs) {\n const list = byTenant.get(doc.tenantId) ?? []\n list.push(doc)\n byTenant.set(doc.tenantId, list)\n }\n\n const meiliClient = getClient()\n\n for (const [tenantId, tenantDocs] of byTenant) {\n const indexName = buildIndexName(tenantId)\n await ensureIndex(indexName)\n\n const documents = await Promise.all(tenantDocs.map(prepareDocument))\n\n const index = meiliClient.index(indexName)\n await index.addDocuments(documents, { primaryKey: '_id' })\n }\n },\n\n async purge(entityId: EntityId, tenantId: string): Promise<void> {\n const meiliClient = getClient()\n const indexName = buildIndexName(tenantId)\n\n try {\n const index = meiliClient.index(indexName)\n await index.deleteDocuments({\n filter: `_entityId = \"${entityId}\"`,\n })\n } catch (error: unknown) {\n const meilisearchError = error as { code?: string }\n if (meilisearchError.code === 'index_not_found') {\n return\n }\n throw error\n }\n },\n\n async clearIndex(tenantId: string): Promise<void> {\n const meiliClient = getClient()\n const indexName = buildIndexName(tenantId)\n\n try {\n const index = meiliClient.index(indexName)\n await index.deleteAllDocuments()\n } catch (error: unknown) {\n const meilisearchError = error as { code?: string }\n if (meilisearchError.code === 'index_not_found') {\n return\n }\n throw error\n }\n },\n\n async recreateIndex(tenantId: string): Promise<void> {\n const meiliClient = getClient()\n const indexName = buildIndexName(tenantId)\n\n initializedIndexes.delete(indexName)\n\n try {\n await meiliClient.deleteIndex(indexName)\n } catch (error: unknown) {\n const meilisearchError = error as { code?: string }\n if (meilisearchError.code !== 'index_not_found') {\n throw error\n }\n }\n\n await ensureIndex(indexName)\n },\n\n async getDocuments(\n ids: DocumentLookupKey[],\n tenantId: string\n ): Promise<Map<string, FullTextSearchHit>> {\n const result = new Map<string, FullTextSearchHit>()\n if (ids.length === 0) return result\n\n const meiliClient = getClient()\n const indexName = buildIndexName(tenantId)\n\n try {\n const index = meiliClient.index(indexName)\n\n const recordIds = ids.map((id) => id.recordId)\n const documents = await index.getDocuments({\n filter: `_id IN [${recordIds.map((id) => `\"${id}\"`).join(', ')}]`,\n limit: recordIds.length,\n })\n\n for (const doc of documents.results) {\n const hit = doc as Record<string, unknown>\n const key = `${hit._entityId}:${hit._id}`\n result.set(key, {\n recordId: hit._id as string,\n entityId: hit._entityId as EntityId,\n score: 0,\n presenter: hit._presenter as FullTextSearchHit['presenter'],\n url: hit._url as string | undefined,\n links: hit._links as FullTextSearchHit['links'],\n })\n }\n } catch {\n // Index not found or error, return empty map\n }\n\n return result\n },\n\n async getIndexStats(tenantId: string): Promise<IndexStats | null> {\n const meiliClient = getClient()\n const indexName = buildIndexName(tenantId)\n\n try {\n const index = meiliClient.index(indexName)\n const stats = await index.getStats()\n return {\n numberOfDocuments: stats.numberOfDocuments,\n isIndexing: stats.isIndexing,\n fieldDistribution: stats.fieldDistribution,\n }\n } catch (error: unknown) {\n const meilisearchError = error as { code?: string }\n if (meilisearchError.code === 'index_not_found') {\n return null\n }\n throw error\n }\n },\n\n async getEntityCounts(tenantId: string): Promise<Record<string, number> | null> {\n const meiliClient = getClient()\n const indexName = buildIndexName(tenantId)\n\n try {\n const index = meiliClient.index(indexName)\n const searchResult = await index.search('', {\n limit: 0,\n facets: ['_entityId'],\n })\n const facetDistribution = searchResult.facetDistribution?._entityId\n if (!facetDistribution) {\n return {}\n }\n return facetDistribution\n } catch (error: unknown) {\n const meilisearchError = error as { code?: string }\n if (meilisearchError.code === 'index_not_found') {\n return null\n }\n throw error\n }\n },\n }\n\n return driver\n}\n"],
|
|
4
|
+
"sourcesContent": ["import { Meilisearch } from 'meilisearch'\nimport type { EntityId } from '@open-mercato/shared/modules/entities'\nimport type { SearchFieldPolicy } from '@open-mercato/shared/modules/search'\nimport { resolveTimeoutMs } from '@open-mercato/shared/lib/http/fetchWithTimeout'\nimport type {\n FullTextSearchDriver,\n FullTextSearchDocument,\n FullTextSearchQuery,\n FullTextSearchHit,\n DocumentLookupKey,\n IndexStats,\n} from '../../types'\nimport { extractSearchableFields, type EncryptionMapEntry } from '../../../lib/field-policy'\n\n\nexport type MeilisearchDriverOptions = {\n host?: string\n apiKey?: string\n indexPrefix?: string\n defaultLimit?: number\n timeoutMs?: number\n encryptionMapResolver?: (entityId: EntityId) => Promise<EncryptionMapEntry[]>\n fieldPolicyResolver?: (entityId: EntityId) => SearchFieldPolicy | undefined\n}\n\nconst DEFAULT_MEILISEARCH_REQUEST_TIMEOUT_MS = 30_000\n\nfunction resolveMeilisearchTimeoutMs(explicit?: number): number {\n if (typeof explicit === 'number') return resolveTimeoutMs(explicit, DEFAULT_MEILISEARCH_REQUEST_TIMEOUT_MS)\n const raw = process.env.MEILISEARCH_REQUEST_TIMEOUT_MS\n const parsed = raw ? Number.parseInt(raw, 10) : undefined\n return resolveTimeoutMs(parsed, DEFAULT_MEILISEARCH_REQUEST_TIMEOUT_MS)\n}\n\nexport function createMeilisearchDriver(\n options?: MeilisearchDriverOptions\n): FullTextSearchDriver {\n const host = options?.host ?? process.env.MEILISEARCH_HOST ?? ''\n const apiKey = options?.apiKey ?? process.env.MEILISEARCH_API_KEY ?? ''\n const indexPrefix = options?.indexPrefix ?? process.env.MEILISEARCH_INDEX_PREFIX ?? 'om'\n const defaultLimit = options?.defaultLimit ?? 20\n const requestTimeoutMs = resolveMeilisearchTimeoutMs(options?.timeoutMs)\n const encryptionMapResolver = options?.encryptionMapResolver\n const fieldPolicyResolver = options?.fieldPolicyResolver\n\n let client: Meilisearch | null = null\n const initializedIndexes = new Set<string>()\n const initializingIndexes = new Map<string, Promise<void>>()\n\n function getClient(): Meilisearch {\n if (!client) {\n client = new Meilisearch({ host, apiKey, timeout: requestTimeoutMs })\n }\n return client\n }\n\n function buildIndexName(tenantId: string): string {\n const sanitized = tenantId.replace(/[^a-zA-Z0-9_-]/g, '_')\n return `${indexPrefix}_${sanitized}`\n }\n\n function escapeFilterValue(value: string): string {\n return value.replace(/[\"\\\\]/g, '\\\\$&')\n }\n\n function buildFilters(options: FullTextSearchQuery): string[] {\n const filters: string[] = []\n\n if (options.organizationId) {\n filters.push(`_organizationId = \"${escapeFilterValue(options.organizationId)}\"`)\n }\n\n if (options.entityTypes?.length) {\n const entityFilter = options.entityTypes.map((t) => `\"${escapeFilterValue(t)}\"`).join(', ')\n filters.push(`_entityId IN [${entityFilter}]`)\n }\n\n return filters\n }\n\n async function doEnsureIndex(indexName: string): Promise<void> {\n const meiliClient = getClient()\n\n try {\n await meiliClient.createIndex(indexName, { primaryKey: '_id' })\n } catch (error: unknown) {\n const meilisearchError = error as { code?: string }\n if (meilisearchError.code !== 'index_already_exists') {\n throw error\n }\n }\n\n const index = meiliClient.index(indexName)\n await index.updateSettings({\n searchableAttributes: ['*'],\n filterableAttributes: ['_entityId', '_organizationId'],\n sortableAttributes: ['_indexedAt'],\n typoTolerance: {\n enabled: true,\n minWordSizeForTypos: {\n oneTypo: 4,\n twoTypos: 8,\n },\n },\n })\n\n initializedIndexes.add(indexName)\n }\n\n async function ensureIndex(indexName: string): Promise<void> {\n if (initializedIndexes.has(indexName)) {\n return\n }\n\n const existingPromise = initializingIndexes.get(indexName)\n if (existingPromise) {\n return existingPromise\n }\n\n const initPromise = doEnsureIndex(indexName)\n initializingIndexes.set(indexName, initPromise)\n\n try {\n await initPromise\n } finally {\n initializingIndexes.delete(indexName)\n }\n }\n\n async function prepareDocument(doc: FullTextSearchDocument): Promise<Record<string, unknown>> {\n // When encryptionMapResolver is provided, SEARCH_EXCLUDE_ENCRYPTED_FIELDS is enabled\n const excludeEncrypted = Boolean(encryptionMapResolver)\n const encryptedFields = encryptionMapResolver\n ? await encryptionMapResolver(doc.entityId)\n : []\n const fieldPolicy = fieldPolicyResolver?.(doc.entityId)\n\n const searchableFields = extractSearchableFields(doc.fields, {\n encryptedFields,\n fieldPolicy,\n })\n\n // When SEARCH_EXCLUDE_ENCRYPTED_FIELDS is enabled:\n // - Exclude sensitive parts of presenter (title, subtitle) - these are derived from encrypted fields\n // - Keep non-sensitive parts (icon, badge)\n // - Sanitize link labels (they often contain names derived from encrypted fields)\n // - Title/subtitle/link labels will be enriched at search time from the database\n let presenter = doc.presenter\n let links = doc.links\n if (excludeEncrypted) {\n if (presenter) {\n presenter = {\n ...presenter,\n title: '', // Will be enriched at search time\n subtitle: undefined, // Will be enriched at search time\n }\n }\n // Sanitize link labels - they often contain sensitive data (names, etc.)\n if (links && links.length > 0) {\n links = links.map((link) => ({\n ...link,\n label: link.kind === 'primary' ? 'Open' : 'View', // Generic labels\n }))\n }\n }\n\n return {\n _id: doc.recordId,\n _entityId: doc.entityId,\n _organizationId: doc.organizationId,\n _presenter: presenter,\n _url: doc.url,\n _links: links,\n _indexedAt: new Date().toISOString(),\n ...searchableFields,\n }\n }\n\n const driver: FullTextSearchDriver = {\n id: 'meilisearch',\n\n async ensureReady(): Promise<void> {\n // Client is lazily initialized\n },\n\n async isHealthy(): Promise<boolean> {\n if (!host) {\n return false\n }\n\n try {\n const meiliClient = getClient()\n await meiliClient.health()\n return true\n } catch {\n return false\n }\n },\n\n async search(query: string, options: FullTextSearchQuery): Promise<FullTextSearchHit[]> {\n const meiliClient = getClient()\n const indexName = buildIndexName(options.tenantId)\n\n try {\n const index = meiliClient.index(indexName)\n const filters = buildFilters(options)\n\n const response = await index.search(query, {\n limit: options.limit ?? defaultLimit,\n offset: options.offset,\n filter: filters.length > 0 ? filters.join(' AND ') : undefined,\n showRankingScore: true,\n })\n\n return response.hits.map((hit: Record<string, unknown>) => ({\n recordId: hit._id as string,\n entityId: hit._entityId as EntityId,\n score: (hit._rankingScore as number) ?? 0.5,\n presenter: hit._presenter as FullTextSearchHit['presenter'],\n url: hit._url as string | undefined,\n links: hit._links as FullTextSearchHit['links'],\n metadata: hit._metadata as Record<string, unknown> | undefined,\n }))\n } catch (error: unknown) {\n const meilisearchError = error as { code?: string }\n if (meilisearchError.code === 'index_not_found') {\n return []\n }\n throw error\n }\n },\n\n async index(doc: FullTextSearchDocument): Promise<void> {\n const meiliClient = getClient()\n const indexName = buildIndexName(doc.tenantId)\n\n await ensureIndex(indexName)\n\n const document = await prepareDocument(doc)\n\n const index = meiliClient.index(indexName)\n await index.addDocuments([document], { primaryKey: '_id' })\n },\n\n async delete(recordId: string, tenantId: string): Promise<void> {\n const meiliClient = getClient()\n const indexName = buildIndexName(tenantId)\n\n try {\n const index = meiliClient.index(indexName)\n await index.deleteDocument(recordId)\n } catch (error: unknown) {\n const meilisearchError = error as { code?: string }\n if (meilisearchError.code === 'index_not_found') {\n return\n }\n throw error\n }\n },\n\n async bulkIndex(docs: FullTextSearchDocument[]): Promise<void> {\n if (docs.length === 0) return\n\n // Group documents by tenant\n const byTenant = new Map<string, FullTextSearchDocument[]>()\n for (const doc of docs) {\n const list = byTenant.get(doc.tenantId) ?? []\n list.push(doc)\n byTenant.set(doc.tenantId, list)\n }\n\n const meiliClient = getClient()\n\n for (const [tenantId, tenantDocs] of byTenant) {\n const indexName = buildIndexName(tenantId)\n await ensureIndex(indexName)\n\n const documents = await Promise.all(tenantDocs.map(prepareDocument))\n\n const index = meiliClient.index(indexName)\n await index.addDocuments(documents, { primaryKey: '_id' })\n }\n },\n\n async purge(entityId: EntityId, tenantId: string): Promise<void> {\n const meiliClient = getClient()\n const indexName = buildIndexName(tenantId)\n\n try {\n const index = meiliClient.index(indexName)\n await index.deleteDocuments({\n filter: `_entityId = \"${entityId}\"`,\n })\n } catch (error: unknown) {\n const meilisearchError = error as { code?: string }\n if (meilisearchError.code === 'index_not_found') {\n return\n }\n throw error\n }\n },\n\n async clearIndex(tenantId: string): Promise<void> {\n const meiliClient = getClient()\n const indexName = buildIndexName(tenantId)\n\n try {\n const index = meiliClient.index(indexName)\n await index.deleteAllDocuments()\n } catch (error: unknown) {\n const meilisearchError = error as { code?: string }\n if (meilisearchError.code === 'index_not_found') {\n return\n }\n throw error\n }\n },\n\n async recreateIndex(tenantId: string): Promise<void> {\n const meiliClient = getClient()\n const indexName = buildIndexName(tenantId)\n\n initializedIndexes.delete(indexName)\n\n try {\n await meiliClient.deleteIndex(indexName)\n } catch (error: unknown) {\n const meilisearchError = error as { code?: string }\n if (meilisearchError.code !== 'index_not_found') {\n throw error\n }\n }\n\n await ensureIndex(indexName)\n },\n\n async getDocuments(\n ids: DocumentLookupKey[],\n tenantId: string\n ): Promise<Map<string, FullTextSearchHit>> {\n const result = new Map<string, FullTextSearchHit>()\n if (ids.length === 0) return result\n\n const meiliClient = getClient()\n const indexName = buildIndexName(tenantId)\n\n try {\n const index = meiliClient.index(indexName)\n\n const recordIds = ids.map((id) => id.recordId)\n const documents = await index.getDocuments({\n filter: `_id IN [${recordIds.map((id) => `\"${id}\"`).join(', ')}]`,\n limit: recordIds.length,\n })\n\n for (const doc of documents.results) {\n const hit = doc as Record<string, unknown>\n const key = `${hit._entityId}:${hit._id}`\n result.set(key, {\n recordId: hit._id as string,\n entityId: hit._entityId as EntityId,\n score: 0,\n presenter: hit._presenter as FullTextSearchHit['presenter'],\n url: hit._url as string | undefined,\n links: hit._links as FullTextSearchHit['links'],\n })\n }\n } catch {\n // Index not found or error, return empty map\n }\n\n return result\n },\n\n async getIndexStats(tenantId: string): Promise<IndexStats | null> {\n const meiliClient = getClient()\n const indexName = buildIndexName(tenantId)\n\n try {\n const index = meiliClient.index(indexName)\n const stats = await index.getStats()\n return {\n numberOfDocuments: stats.numberOfDocuments,\n isIndexing: stats.isIndexing,\n fieldDistribution: stats.fieldDistribution,\n }\n } catch (error: unknown) {\n const meilisearchError = error as { code?: string }\n if (meilisearchError.code === 'index_not_found') {\n return null\n }\n throw error\n }\n },\n\n async getEntityCounts(tenantId: string): Promise<Record<string, number> | null> {\n const meiliClient = getClient()\n const indexName = buildIndexName(tenantId)\n\n try {\n const index = meiliClient.index(indexName)\n const searchResult = await index.search('', {\n limit: 0,\n facets: ['_entityId'],\n })\n const facetDistribution = searchResult.facetDistribution?._entityId\n if (!facetDistribution) {\n return {}\n }\n return facetDistribution\n } catch (error: unknown) {\n const meilisearchError = error as { code?: string }\n if (meilisearchError.code === 'index_not_found') {\n return null\n }\n throw error\n }\n },\n }\n\n return driver\n}\n"],
|
|
5
5
|
"mappings": "AAAA,SAAS,mBAAmB;AAG5B,SAAS,wBAAwB;AASjC,SAAS,+BAAwD;AAajE,MAAM,yCAAyC;AAE/C,SAAS,4BAA4B,UAA2B;AAC9D,MAAI,OAAO,aAAa,SAAU,QAAO,iBAAiB,UAAU,sCAAsC;AAC1G,QAAM,MAAM,QAAQ,IAAI;AACxB,QAAM,SAAS,MAAM,OAAO,SAAS,KAAK,EAAE,IAAI;AAChD,SAAO,iBAAiB,QAAQ,sCAAsC;AACxE;AAEO,SAAS,wBACd,SACsB;AACtB,QAAM,OAAO,SAAS,QAAQ,QAAQ,IAAI,oBAAoB;AAC9D,QAAM,SAAS,SAAS,UAAU,QAAQ,IAAI,uBAAuB;AACrE,QAAM,cAAc,SAAS,eAAe,QAAQ,IAAI,4BAA4B;AACpF,QAAM,eAAe,SAAS,gBAAgB;AAC9C,QAAM,mBAAmB,4BAA4B,SAAS,SAAS;AACvE,QAAM,wBAAwB,SAAS;AACvC,QAAM,sBAAsB,SAAS;AAErC,MAAI,SAA6B;AACjC,QAAM,qBAAqB,oBAAI,IAAY;AAC3C,QAAM,sBAAsB,oBAAI,IAA2B;AAE3D,WAAS,YAAyB;AAChC,QAAI,CAAC,QAAQ;AACX,eAAS,IAAI,YAAY,EAAE,MAAM,QAAQ,SAAS,iBAAiB,CAAC;AAAA,IACtE;AACA,WAAO;AAAA,EACT;AAEA,WAAS,eAAe,UAA0B;AAChD,UAAM,YAAY,SAAS,QAAQ,mBAAmB,GAAG;AACzD,WAAO,GAAG,WAAW,IAAI,SAAS;AAAA,EACpC;AAEA,WAAS,kBAAkB,OAAuB;AAChD,WAAO,MAAM,QAAQ,UAAU,MAAM;AAAA,EACvC;AAEA,WAAS,aAAaA,UAAwC;AAC5D,UAAM,UAAoB,CAAC;AAE3B,QAAIA,SAAQ,gBAAgB;AAC1B,cAAQ,KAAK,sBAAsB,kBAAkBA,SAAQ,cAAc,CAAC,GAAG;AAAA,IACjF;AAEA,QAAIA,SAAQ,aAAa,QAAQ;AAC/B,YAAM,eAAeA,SAAQ,YAAY,IAAI,CAAC,MAAM,IAAI,kBAAkB,CAAC,CAAC,GAAG,EAAE,KAAK,IAAI;AAC1F,cAAQ,KAAK,iBAAiB,YAAY,GAAG;AAAA,IAC/C;AAEA,WAAO;AAAA,EACT;AAEA,iBAAe,cAAc,WAAkC;AAC7D,UAAM,cAAc,UAAU;AAE9B,QAAI;AACF,YAAM,YAAY,YAAY,WAAW,EAAE,YAAY,MAAM,CAAC;AAAA,IAChE,SAAS,OAAgB;AACvB,YAAM,mBAAmB;AACzB,UAAI,iBAAiB,SAAS,wBAAwB;AACpD,cAAM;AAAA,MACR;AAAA,IACF;AAEA,UAAM,QAAQ,YAAY,MAAM,SAAS;AACzC,UAAM,MAAM,eAAe;AAAA,MACzB,sBAAsB,CAAC,GAAG;AAAA,MAC1B,sBAAsB,CAAC,aAAa,iBAAiB;AAAA,MACrD,oBAAoB,CAAC,YAAY;AAAA,MACjC,eAAe;AAAA,QACb,SAAS;AAAA,QACT,qBAAqB;AAAA,UACnB,SAAS;AAAA,UACT,UAAU;AAAA,QACZ;AAAA,MACF;AAAA,IACF,CAAC;AAED,uBAAmB,IAAI,SAAS;AAAA,EAClC;AAEA,iBAAe,YAAY,WAAkC;AAC3D,QAAI,mBAAmB,IAAI,SAAS,GAAG;AACrC;AAAA,IACF;AAEA,UAAM,kBAAkB,oBAAoB,IAAI,SAAS;AACzD,QAAI,iBAAiB;AACnB,aAAO;AAAA,IACT;AAEA,UAAM,cAAc,cAAc,SAAS;AAC3C,wBAAoB,IAAI,WAAW,WAAW;AAE9C,QAAI;AACF,YAAM;AAAA,IACR,UAAE;AACA,0BAAoB,OAAO,SAAS;AAAA,IACtC;AAAA,EACF;AAEA,iBAAe,gBAAgB,KAA+D;AAE5F,UAAM,mBAAmB,QAAQ,qBAAqB;AACtD,UAAM,kBAAkB,wBACpB,MAAM,sBAAsB,IAAI,QAAQ,IACxC,CAAC;AACL,UAAM,cAAc,sBAAsB,IAAI,QAAQ;AAEtD,UAAM,mBAAmB,wBAAwB,IAAI,QAAQ;AAAA,MAC3D;AAAA,MACA;AAAA,IACF,CAAC;AAOD,QAAI,YAAY,IAAI;AACpB,QAAI,QAAQ,IAAI;AAChB,QAAI,kBAAkB;AACpB,UAAI,WAAW;AACb,oBAAY;AAAA,UACV,GAAG;AAAA,UACH,OAAO;AAAA;AAAA,UACP,UAAU;AAAA;AAAA,QACZ;AAAA,MACF;AAEA,UAAI,SAAS,MAAM,SAAS,GAAG;AAC7B,gBAAQ,MAAM,IAAI,CAAC,UAAU;AAAA,UAC3B,GAAG;AAAA,UACH,OAAO,KAAK,SAAS,YAAY,SAAS;AAAA;AAAA,QAC5C,EAAE;AAAA,MACJ;AAAA,IACF;AAEA,WAAO;AAAA,MACL,KAAK,IAAI;AAAA,MACT,WAAW,IAAI;AAAA,MACf,iBAAiB,IAAI;AAAA,MACrB,YAAY;AAAA,MACZ,MAAM,IAAI;AAAA,MACV,QAAQ;AAAA,MACR,aAAY,oBAAI,KAAK,GAAE,YAAY;AAAA,MACnC,GAAG;AAAA,IACL;AAAA,EACF;AAEA,QAAM,SAA+B;AAAA,IACnC,IAAI;AAAA,IAEJ,MAAM,cAA6B;AAAA,IAEnC;AAAA,IAEA,MAAM,YAA8B;AAClC,UAAI,CAAC,MAAM;AACT,eAAO;AAAA,MACT;AAEA,UAAI;AACF,cAAM,cAAc,UAAU;AAC9B,cAAM,YAAY,OAAO;AACzB,eAAO;AAAA,MACT,QAAQ;AACN,eAAO;AAAA,MACT;AAAA,IACF;AAAA,IAEA,MAAM,OAAO,OAAeA,UAA4D;AACtF,YAAM,cAAc,UAAU;AAC9B,YAAM,YAAY,eAAeA,SAAQ,QAAQ;AAEjD,UAAI;AACF,cAAM,QAAQ,YAAY,MAAM,SAAS;AACzC,cAAM,UAAU,aAAaA,QAAO;AAEpC,cAAM,WAAW,MAAM,MAAM,OAAO,OAAO;AAAA,UACzC,OAAOA,SAAQ,SAAS;AAAA,UACxB,QAAQA,SAAQ;AAAA,UAChB,QAAQ,QAAQ,SAAS,IAAI,QAAQ,KAAK,OAAO,IAAI;AAAA,UACrD,kBAAkB;AAAA,QACpB,CAAC;AAED,eAAO,SAAS,KAAK,IAAI,CAAC,SAAkC;AAAA,UAC1D,UAAU,IAAI;AAAA,UACd,UAAU,IAAI;AAAA,UACd,OAAQ,IAAI,iBAA4B;AAAA,UACxC,WAAW,IAAI;AAAA,UACf,KAAK,IAAI;AAAA,UACT,OAAO,IAAI;AAAA,UACX,UAAU,IAAI;AAAA,QAChB,EAAE;AAAA,MACJ,SAAS,OAAgB;AACvB,cAAM,mBAAmB;AACzB,YAAI,iBAAiB,SAAS,mBAAmB;AAC/C,iBAAO,CAAC;AAAA,QACV;AACA,cAAM;AAAA,MACR;AAAA,IACF;AAAA,IAEA,MAAM,MAAM,KAA4C;AACtD,YAAM,cAAc,UAAU;AAC9B,YAAM,YAAY,eAAe,IAAI,QAAQ;AAE7C,YAAM,YAAY,SAAS;AAE3B,YAAM,WAAW,MAAM,gBAAgB,GAAG;AAE1C,YAAM,QAAQ,YAAY,MAAM,SAAS;AACzC,YAAM,MAAM,aAAa,CAAC,QAAQ,GAAG,EAAE,YAAY,MAAM,CAAC;AAAA,IAC5D;AAAA,IAEA,MAAM,OAAO,UAAkB,UAAiC;AAC9D,YAAM,cAAc,UAAU;AAC9B,YAAM,YAAY,eAAe,QAAQ;AAEzC,UAAI;AACF,cAAM,QAAQ,YAAY,MAAM,SAAS;AACzC,cAAM,MAAM,eAAe,QAAQ;AAAA,MACrC,SAAS,OAAgB;AACvB,cAAM,mBAAmB;AACzB,YAAI,iBAAiB,SAAS,mBAAmB;AAC/C;AAAA,QACF;AACA,cAAM;AAAA,MACR;AAAA,IACF;AAAA,IAEA,MAAM,UAAU,MAA+C;AAC7D,UAAI,KAAK,WAAW,EAAG;AAGvB,YAAM,WAAW,oBAAI,IAAsC;AAC3D,iBAAW,OAAO,MAAM;AACtB,cAAM,OAAO,SAAS,IAAI,IAAI,QAAQ,KAAK,CAAC;AAC5C,aAAK,KAAK,GAAG;AACb,iBAAS,IAAI,IAAI,UAAU,IAAI;AAAA,MACjC;AAEA,YAAM,cAAc,UAAU;AAE9B,iBAAW,CAAC,UAAU,UAAU,KAAK,UAAU;AAC7C,cAAM,YAAY,eAAe,QAAQ;AACzC,cAAM,YAAY,SAAS;AAE3B,cAAM,YAAY,MAAM,QAAQ,IAAI,WAAW,IAAI,eAAe,CAAC;AAEnE,cAAM,QAAQ,YAAY,MAAM,SAAS;AACzC,cAAM,MAAM,aAAa,WAAW,EAAE,YAAY,MAAM,CAAC;AAAA,MAC3D;AAAA,IACF;AAAA,IAEA,MAAM,MAAM,UAAoB,UAAiC;AAC/D,YAAM,cAAc,UAAU;AAC9B,YAAM,YAAY,eAAe,QAAQ;AAEzC,UAAI;AACF,cAAM,QAAQ,YAAY,MAAM,SAAS;AACzC,cAAM,MAAM,gBAAgB;AAAA,UAC1B,QAAQ,gBAAgB,QAAQ;AAAA,QAClC,CAAC;AAAA,MACH,SAAS,OAAgB;AACvB,cAAM,mBAAmB;AACzB,YAAI,iBAAiB,SAAS,mBAAmB;AAC/C;AAAA,QACF;AACA,cAAM;AAAA,MACR;AAAA,IACF;AAAA,IAEA,MAAM,WAAW,UAAiC;AAChD,YAAM,cAAc,UAAU;AAC9B,YAAM,YAAY,eAAe,QAAQ;AAEzC,UAAI;AACF,cAAM,QAAQ,YAAY,MAAM,SAAS;AACzC,cAAM,MAAM,mBAAmB;AAAA,MACjC,SAAS,OAAgB;AACvB,cAAM,mBAAmB;AACzB,YAAI,iBAAiB,SAAS,mBAAmB;AAC/C;AAAA,QACF;AACA,cAAM;AAAA,MACR;AAAA,IACF;AAAA,IAEA,MAAM,cAAc,UAAiC;AACnD,YAAM,cAAc,UAAU;AAC9B,YAAM,YAAY,eAAe,QAAQ;AAEzC,yBAAmB,OAAO,SAAS;AAEnC,UAAI;AACF,cAAM,YAAY,YAAY,SAAS;AAAA,MACzC,SAAS,OAAgB;AACvB,cAAM,mBAAmB;AACzB,YAAI,iBAAiB,SAAS,mBAAmB;AAC/C,gBAAM;AAAA,QACR;AAAA,MACF;AAEA,YAAM,YAAY,SAAS;AAAA,IAC7B;AAAA,IAEA,MAAM,aACJ,KACA,UACyC;AACzC,YAAM,SAAS,oBAAI,IAA+B;AAClD,UAAI,IAAI,WAAW,EAAG,QAAO;AAE7B,YAAM,cAAc,UAAU;AAC9B,YAAM,YAAY,eAAe,QAAQ;AAEzC,UAAI;AACF,cAAM,QAAQ,YAAY,MAAM,SAAS;AAEzC,cAAM,YAAY,IAAI,IAAI,CAAC,OAAO,GAAG,QAAQ;AAC7C,cAAM,YAAY,MAAM,MAAM,aAAa;AAAA,UACzC,QAAQ,WAAW,UAAU,IAAI,CAAC,OAAO,IAAI,EAAE,GAAG,EAAE,KAAK,IAAI,CAAC;AAAA,UAC9D,OAAO,UAAU;AAAA,QACnB,CAAC;AAED,mBAAW,OAAO,UAAU,SAAS;AACnC,gBAAM,MAAM;AACZ,gBAAM,MAAM,GAAG,IAAI,SAAS,IAAI,IAAI,GAAG;AACvC,iBAAO,IAAI,KAAK;AAAA,YACd,UAAU,IAAI;AAAA,YACd,UAAU,IAAI;AAAA,YACd,OAAO;AAAA,YACP,WAAW,IAAI;AAAA,YACf,KAAK,IAAI;AAAA,YACT,OAAO,IAAI;AAAA,UACb,CAAC;AAAA,QACH;AAAA,MACF,QAAQ;AAAA,MAER;AAEA,aAAO;AAAA,IACT;AAAA,IAEA,MAAM,cAAc,UAA8C;AAChE,YAAM,cAAc,UAAU;AAC9B,YAAM,YAAY,eAAe,QAAQ;AAEzC,UAAI;AACF,cAAM,QAAQ,YAAY,MAAM,SAAS;AACzC,cAAM,QAAQ,MAAM,MAAM,SAAS;AACnC,eAAO;AAAA,UACL,mBAAmB,MAAM;AAAA,UACzB,YAAY,MAAM;AAAA,UAClB,mBAAmB,MAAM;AAAA,QAC3B;AAAA,MACF,SAAS,OAAgB;AACvB,cAAM,mBAAmB;AACzB,YAAI,iBAAiB,SAAS,mBAAmB;AAC/C,iBAAO;AAAA,QACT;AACA,cAAM;AAAA,MACR;AAAA,IACF;AAAA,IAEA,MAAM,gBAAgB,UAA0D;AAC9E,YAAM,cAAc,UAAU;AAC9B,YAAM,YAAY,eAAe,QAAQ;AAEzC,UAAI;AACF,cAAM,QAAQ,YAAY,MAAM,SAAS;AACzC,cAAM,eAAe,MAAM,MAAM,OAAO,IAAI;AAAA,UAC1C,OAAO;AAAA,UACP,QAAQ,CAAC,WAAW;AAAA,QACtB,CAAC;AACD,cAAM,oBAAoB,aAAa,mBAAmB;AAC1D,YAAI,CAAC,mBAAmB;AACtB,iBAAO,CAAC;AAAA,QACV;AACA,eAAO;AAAA,MACT,SAAS,OAAgB;AACvB,cAAM,mBAAmB;AACzB,YAAI,iBAAiB,SAAS,mBAAmB;AAC/C,iBAAO;AAAA,QACT;AACA,cAAM;AAAA,MACR;AAAA,IACF;AAAA,EACF;AAEA,SAAO;AACT;",
|
|
6
6
|
"names": ["options"]
|
|
7
7
|
}
|
|
@@ -294,8 +294,8 @@ async function testMeilisearchCommand() {
|
|
|
294
294
|
console.log(`API Key: ${apiKey ? "(configured)" : "(not set)"}`);
|
|
295
295
|
console.log("");
|
|
296
296
|
try {
|
|
297
|
-
const {
|
|
298
|
-
const client = new
|
|
297
|
+
const { Meilisearch } = await import("meilisearch");
|
|
298
|
+
const client = new Meilisearch({ host, apiKey });
|
|
299
299
|
console.log("Testing connection...");
|
|
300
300
|
const health = await client.health();
|
|
301
301
|
console.log(`Health: ${health.status}`);
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../src/modules/search/cli.ts"],
|
|
4
|
-
"sourcesContent": ["import type { ModuleCli } from '@open-mercato/shared/modules/registry'\nimport { createRequestContainer } from '@open-mercato/shared/lib/di/container'\nimport { getRedisUrlOrThrow } from '@open-mercato/shared/lib/redis/connection'\nimport { recordIndexerError } from '@open-mercato/shared/lib/indexers/error-log'\nimport { recordIndexerLog } from '@open-mercato/shared/lib/indexers/status-log'\nimport { createProgressBar } from '@open-mercato/shared/lib/cli/progress'\nimport type { EntityManager } from '@mikro-orm/postgresql'\nimport { reindexEntity, DEFAULT_REINDEX_PARTITIONS } from '@open-mercato/core/modules/query_index/lib/reindexer'\nimport { writeCoverageCounts } from '@open-mercato/core/modules/query_index/lib/coverage'\nimport type { SearchService } from '../../service'\nimport type { SearchIndexer } from '../../indexer/search-indexer'\nimport { VECTOR_INDEXING_QUEUE_NAME, type VectorIndexJobPayload } from '../../queue/vector-indexing'\nimport { FULLTEXT_INDEXING_QUEUE_NAME, type FulltextIndexJobPayload } from '../../queue/fulltext-indexing'\nimport type { QueuedJob, JobContext } from '@open-mercato/queue'\nimport type { EntityId } from '@open-mercato/shared/modules/entities'\nimport { parseBooleanToken } from '@open-mercato/shared/lib/boolean'\n\ntype CliProgressBar = {\n update(completed: number): void\n complete(): void\n}\n\ntype ParsedArgs = Record<string, string | boolean>\n\ntype PartitionProgressInfo = { processed: number; total: number }\n\nfunction isIndexerVerbose(): boolean {\n const parsed = parseBooleanToken(process.env.OM_INDEXER_VERBOSE ?? '')\n return parsed === true\n}\n\nfunction createGroupedProgress(label: string, partitionTargets: number[]) {\n const totals = new Map<number, number>()\n const processed = new Map<number, number>()\n let bar: CliProgressBar | null = null\n\n const getTotals = () => {\n let total = 0\n let done = 0\n for (const value of totals.values()) total += value\n for (const value of processed.values()) done += value\n return { total, done }\n }\n\n const tryInitBar = () => {\n if (bar) return\n if (totals.size < partitionTargets.length) return\n const { total } = getTotals()\n if (total <= 0) return\n bar = createProgressBar(label, total)\n }\n\n return {\n onProgress(partition: number, info: PartitionProgressInfo) {\n processed.set(partition, info.processed)\n if (!totals.has(partition)) totals.set(partition, info.total)\n tryInitBar()\n if (!bar) return\n const { done } = getTotals()\n bar.update(done)\n },\n complete() {\n if (bar) bar.complete()\n },\n getTotals,\n }\n}\n\nfunction parseArgs(rest: string[]): ParsedArgs {\n const args: ParsedArgs = {}\n for (let i = 0; i < rest.length; i += 1) {\n const part = rest[i]\n if (!part?.startsWith('--')) continue\n const [rawKey, rawValue] = part.slice(2).split('=')\n if (!rawKey) continue\n if (rawValue !== undefined) {\n args[rawKey] = rawValue\n } else if (i + 1 < rest.length && !rest[i + 1]!.startsWith('--')) {\n args[rawKey] = rest[i + 1]!\n i += 1\n } else {\n args[rawKey] = true\n }\n }\n return args\n}\n\nfunction stringOpt(args: ParsedArgs, ...keys: string[]): string | undefined {\n for (const key of keys) {\n const raw = args[key]\n if (typeof raw !== 'string') continue\n const trimmed = raw.trim()\n if (trimmed.length > 0) return trimmed\n }\n return undefined\n}\n\nfunction numberOpt(args: ParsedArgs, ...keys: string[]): number | undefined {\n for (const key of keys) {\n const raw = args[key]\n if (typeof raw === 'number') return raw\n if (typeof raw === 'string') {\n const parsed = Number(raw)\n if (Number.isFinite(parsed)) return parsed\n }\n }\n return undefined\n}\n\nfunction flagOpt(args: ParsedArgs, ...keys: string[]): boolean | undefined {\n for (const key of keys) {\n const raw = args[key]\n if (raw === undefined) continue\n if (raw === true) return true\n if (raw === false) return false\n if (typeof raw === 'string') {\n const trimmed = raw.trim()\n if (!trimmed) return true\n const parsed = parseBooleanToken(trimmed)\n return parsed === null ? true : parsed\n }\n }\n return undefined\n}\n\nfunction toPositiveInt(value: number | undefined): number | undefined {\n if (value === undefined) return undefined\n const n = Math.floor(value)\n if (!Number.isFinite(n) || n <= 0) return undefined\n return n\n}\n\nfunction toNonNegativeInt(value: number | undefined, fallback = 0): number {\n if (value === undefined) return fallback\n const n = Math.floor(value)\n if (!Number.isFinite(n) || n < 0) return fallback\n return n\n}\n\n/**\n * Test search functionality with a query\n */\nasync function searchCommand(rest: string[]): Promise<void> {\n const args = parseArgs(rest)\n const query = stringOpt(args, 'query', 'q')\n const tenantId = stringOpt(args, 'tenant', 'tenantId')\n const organizationId = stringOpt(args, 'org', 'organizationId')\n const entityTypes = stringOpt(args, 'entity', 'entities')\n const strategies = stringOpt(args, 'strategy', 'strategies')\n const limit = numberOpt(args, 'limit') ?? 20\n\n if (!query) {\n console.error('Usage: yarn mercato search query --query \"search terms\" --tenant <id> [options]')\n console.error(' --query, -q Search query (required)')\n console.error(' --tenant Tenant ID (required)')\n console.error(' --org Organization ID (optional)')\n console.error(' --entity Entity types to search (comma-separated)')\n console.error(' --strategy Strategies to use (comma-separated: meilisearch,vector,tokens)')\n console.error(' --limit Max results (default: 20)')\n return\n }\n\n if (!tenantId) {\n console.error('Error: --tenant is required')\n return\n }\n\n const container = await createRequestContainer()\n\n try {\n const searchService = container.resolve('searchService') as SearchService | undefined\n\n if (!searchService) {\n console.error('Error: SearchService not available. Make sure the search module is registered.')\n return\n }\n\n console.log(`\\nSearching for: \"${query}\"`)\n console.log(`Tenant: ${tenantId}`)\n if (organizationId) console.log(`Organization: ${organizationId}`)\n console.log('---')\n\n const results = await searchService.search(query, {\n tenantId,\n organizationId,\n entityTypes: entityTypes?.split(',').map(s => s.trim()),\n strategies: strategies?.split(',').map(s => s.trim()) as any,\n limit,\n })\n\n if (results.length === 0) {\n console.log('No results found.')\n return\n }\n\n console.log(`\\nFound ${results.length} result(s):\\n`)\n\n for (let i = 0; i < results.length; i++) {\n const result = results[i]\n console.log(`${i + 1}. [${result.source}] ${result.entityId}`)\n console.log(` Record ID: ${result.recordId}`)\n console.log(` Score: ${result.score.toFixed(4)}`)\n if (result.presenter) {\n console.log(` Title: ${result.presenter.title}`)\n if (result.presenter.subtitle) console.log(` Subtitle: ${result.presenter.subtitle}`)\n }\n if (result.url) console.log(` URL: ${result.url}`)\n console.log('')\n }\n } finally {\n try {\n const em = container.resolve('em') as any\n await em?.getConnection?.()?.close?.()\n } catch {}\n }\n}\n\n/**\n * Show status of search strategies\n */\nasync function statusCommand(): Promise<void> {\n const container = await createRequestContainer()\n\n try {\n const searchService = container.resolve('searchService') as SearchService | undefined\n const strategies = container.resolve('searchStrategies') as any[] | undefined\n\n console.log('\\n=== Search Module Status ===\\n')\n\n if (!searchService) {\n console.log('SearchService: NOT REGISTERED')\n return\n }\n\n console.log('SearchService: ACTIVE')\n console.log('')\n\n if (!strategies || strategies.length === 0) {\n console.log('Strategies: NONE CONFIGURED')\n return\n }\n\n console.log('Strategies:')\n console.log('-----------')\n\n for (const strategy of strategies) {\n const available = await strategy.isAvailable?.() ?? true\n const status = available ? 'AVAILABLE' : 'UNAVAILABLE'\n const icon = available ? '\u2713' : '\u2717'\n console.log(` ${icon} ${strategy.name ?? strategy.id} (${strategy.id})`)\n console.log(` Status: ${status}`)\n console.log(` Priority: ${strategy.priority ?? 'N/A'}`)\n console.log('')\n }\n\n // Check environment variables\n console.log('Environment:')\n console.log('------------')\n console.log(` MEILISEARCH_HOST: ${process.env.MEILISEARCH_HOST ?? '(not set)'}`)\n console.log(` MEILISEARCH_API_KEY: ${process.env.MEILISEARCH_API_KEY ? '(set)' : '(not set)'}`)\n console.log(` OPENAI_API_KEY: ${process.env.OPENAI_API_KEY ? '(set)' : '(not set)'}`)\n console.log(` OM_SEARCH_ENABLED: ${process.env.OM_SEARCH_ENABLED ?? 'true (default)'}`)\n console.log('')\n } finally {\n try {\n const em = container.resolve('em') as any\n await em?.getConnection?.()?.close?.()\n } catch {}\n }\n}\n\n/**\n * Index a specific record for testing\n */\nasync function indexCommand(rest: string[]): Promise<void> {\n const args = parseArgs(rest)\n const entityId = stringOpt(args, 'entity', 'entityId')\n const recordId = stringOpt(args, 'record', 'recordId')\n const tenantId = stringOpt(args, 'tenant', 'tenantId')\n const organizationId = stringOpt(args, 'org', 'organizationId')\n\n if (!entityId || !recordId || !tenantId) {\n console.error('Usage: yarn mercato search index --entity <entityId> --record <recordId> --tenant <tenantId>')\n console.error(' --entity Entity ID (e.g., customers:customer_person_profile)')\n console.error(' --record Record ID')\n console.error(' --tenant Tenant ID')\n console.error(' --org Organization ID (optional)')\n return\n }\n\n const container = await createRequestContainer()\n\n try {\n const searchIndexer = container.resolve('searchIndexer') as SearchIndexer | undefined\n\n if (!searchIndexer) {\n console.error('Error: SearchIndexer not available.')\n return\n }\n\n // Load record from query engine\n const queryEngine = container.resolve('queryEngine') as any\n\n console.log(`\\nLoading record: ${entityId} / ${recordId}`)\n\n const result = await queryEngine.query(entityId, {\n tenantId,\n organizationId,\n filters: { id: recordId },\n includeCustomFields: true,\n page: { page: 1, pageSize: 1 },\n })\n\n const record = result.items[0]\n\n if (!record) {\n console.error('Error: Record not found')\n return\n }\n\n console.log('Record loaded, indexing...')\n\n // Extract custom fields\n const customFields: Record<string, unknown> = {}\n for (const [key, value] of Object.entries(record)) {\n if (key.startsWith('cf:') || key.startsWith('cf_')) {\n const cfKey = key.slice(3) // Remove 'cf:' or 'cf_' prefix (both are 3 chars)\n customFields[cfKey] = value\n }\n }\n\n await searchIndexer.indexRecord({\n entityId,\n recordId,\n tenantId,\n organizationId,\n record,\n customFields,\n })\n\n console.log('Record indexed successfully!')\n } finally {\n try {\n const em = container.resolve('em') as any\n await em?.getConnection?.()?.close?.()\n } catch {}\n }\n}\n\n/**\n * Test Meilisearch connection directly\n */\nasync function testMeilisearchCommand(): Promise<void> {\n const host = process.env.MEILISEARCH_HOST\n const apiKey = process.env.MEILISEARCH_API_KEY\n\n console.log('\\n=== Meilisearch Connection Test ===\\n')\n\n if (!host) {\n console.log('MEILISEARCH_HOST: NOT SET')\n console.log('\\nMeilisearch is not configured. Set MEILISEARCH_HOST in your .env file.')\n return\n }\n\n console.log(`Host: ${host}`)\n console.log(`API Key: ${apiKey ? '(configured)' : '(not set)'}`)\n console.log('')\n\n try {\n const { MeiliSearch } = await import('meilisearch')\n const client = new MeiliSearch({ host, apiKey })\n\n console.log('Testing connection...')\n const health = await client.health()\n console.log(`Health: ${health.status}`)\n\n console.log('\\nListing indexes...')\n const indexes = await client.getIndexes()\n\n if (indexes.results.length === 0) {\n console.log('No indexes found.')\n } else {\n console.log(`Found ${indexes.results.length} index(es):`)\n for (const index of indexes.results) {\n const stats = await client.index(index.uid).getStats()\n console.log(` - ${index.uid}: ${stats.numberOfDocuments} documents`)\n }\n }\n\n console.log('\\nMeilisearch connection successful!')\n } catch (error) {\n console.error('Connection failed:', error instanceof Error ? error.message : error)\n }\n}\n\nconst searchCli: ModuleCli = {\n command: 'query',\n async run(rest) {\n await searchCommand(rest)\n },\n}\n\nconst statusCli: ModuleCli = {\n command: 'status',\n async run() {\n await statusCommand()\n },\n}\n\nconst indexCli: ModuleCli = {\n command: 'index',\n async run(rest) {\n await indexCommand(rest)\n },\n}\n\nconst testMeilisearchCli: ModuleCli = {\n command: 'test-meilisearch',\n async run() {\n await testMeilisearchCommand()\n },\n}\n\nasync function resetVectorCoverageAfterPurge(\n em: EntityManager | null,\n entityId: string,\n tenantId: string | null,\n organizationId: string | null,\n): Promise<void> {\n if (!em || !entityId) return\n try {\n const scopes = new Set<string>()\n scopes.add('__null__')\n if (organizationId) scopes.add(organizationId)\n for (const scope of scopes) {\n const orgValue = scope === '__null__' ? null : scope\n await writeCoverageCounts(\n em,\n {\n entityType: entityId,\n tenantId,\n organizationId: orgValue,\n withDeleted: false,\n },\n { vectorCount: 0 },\n )\n }\n } catch (error) {\n console.warn('[search.cli] Failed to reset vector coverage after purge', error instanceof Error ? error.message : error)\n }\n}\n\nasync function reindexCommand(rest: string[]): Promise<void> {\n const args = parseArgs(rest)\n const tenantId = stringOpt(args, 'tenant', 'tenantId')\n const organizationId = stringOpt(args, 'org', 'orgId', 'organizationId')\n const entityId = stringOpt(args, 'entity', 'entityId')\n const force = flagOpt(args, 'force', 'full') === true\n const batchSize = toPositiveInt(numberOpt(args, 'batch', 'chunk', 'size'))\n const partitionsOption = toPositiveInt(numberOpt(args, 'partitions', 'partitionCount', 'parallel'))\n const partitionIndexRaw = numberOpt(args, 'partition', 'partitionIndex')\n const partitionIndexOption = partitionIndexRaw === undefined ? undefined : toNonNegativeInt(partitionIndexRaw, 0)\n const resetCoverageFlag = flagOpt(args, 'resetCoverage') === true\n const skipResetCoverageFlag = flagOpt(args, 'skipResetCoverage', 'noResetCoverage') === true\n const skipPurgeFlag = flagOpt(args, 'skipPurge', 'noPurge') === true\n const purgeFlag = flagOpt(args, 'purge', 'purgeFirst')\n\n const container = await createRequestContainer()\n let baseEm: EntityManager | null = null\n try {\n baseEm = (container.resolve('em') as EntityManager)\n } catch {\n baseEm = null\n }\n\n const disposeContainer = async () => {\n if (typeof (container as any)?.dispose === 'function') {\n await (container as any).dispose()\n }\n }\n\n const recordError = async (error: Error) => {\n await recordIndexerLog(\n { em: baseEm ?? undefined },\n {\n source: 'vector',\n handler: 'cli:search.reindex',\n level: 'warn',\n message: `Reindex failed${entityId ? ` for ${entityId}` : ''}`,\n entityType: entityId ?? null,\n tenantId: tenantId ?? null,\n organizationId: organizationId ?? null,\n details: { error: error.message },\n },\n ).catch(() => undefined)\n await recordIndexerError(\n { em: baseEm ?? undefined },\n {\n source: 'vector',\n handler: 'cli:search.reindex',\n error,\n entityType: entityId ?? null,\n tenantId: tenantId ?? null,\n organizationId: organizationId ?? null,\n payload: {\n args,\n force,\n batchSize,\n partitionsOption,\n partitionIndexOption,\n resetCoverageFlag,\n skipResetCoverageFlag,\n skipPurgeFlag,\n purgeFlag,\n },\n },\n )\n }\n\n try {\n const searchIndexer = container.resolve<SearchIndexer>('searchIndexer')\n const enabledEntities = new Set(searchIndexer.listEnabledEntities())\n const baseEventBus = (() => {\n try {\n return container.resolve('eventBus') as {\n emitEvent(event: string, payload: any, options?: any): Promise<void>\n }\n } catch {\n return null\n }\n })()\n if (!baseEventBus) {\n console.warn('[search.cli] eventBus unavailable; vector embeddings may not be refreshed. Run bootstrap or ensure event bus configuration.')\n }\n\n const partitionCount = Math.max(1, partitionsOption ?? DEFAULT_REINDEX_PARTITIONS)\n if (partitionIndexOption !== undefined && partitionIndexOption >= partitionCount) {\n console.error(`partitionIndex (${partitionIndexOption}) must be < partitionCount (${partitionCount})`)\n return\n }\n const partitionTargets =\n partitionIndexOption !== undefined\n ? [partitionIndexOption]\n : Array.from({ length: partitionCount }, (_, idx) => idx)\n\n const shouldResetCoverage = (partition: number): boolean => {\n if (resetCoverageFlag) return true\n if (skipResetCoverageFlag) return false\n if (partitionIndexOption !== undefined) return partitionIndexOption === 0\n return partition === partitionTargets[0]\n }\n\n const runReindex = async (entityType: string, purgeFirst: boolean) => {\n const scopeLabel = tenantId\n ? `tenant=${tenantId}${organizationId ? `, org=${organizationId}` : ''}`\n : 'all tenants'\n console.log(`Reindexing vectors for ${entityType} (${scopeLabel})${purgeFirst ? ' [purge]' : ''}`)\n await recordIndexerLog(\n { em: baseEm ?? undefined },\n {\n source: 'vector',\n handler: 'cli:search.reindex',\n message: `Reindex started for ${entityType}`,\n entityType,\n tenantId: tenantId ?? null,\n organizationId: organizationId ?? null,\n details: {\n purgeFirst,\n partitions: partitionTargets.length,\n partitionCount,\n partitionIndex: partitionIndexOption ?? null,\n batchSize,\n },\n },\n ).catch(() => undefined)\n\n if (purgeFirst && tenantId) {\n try {\n console.log(' -> purging existing vector index rows...')\n await searchIndexer.purgeEntity({ entityId: entityType as EntityId, tenantId })\n await resetVectorCoverageAfterPurge(baseEm, entityType, tenantId ?? null, organizationId ?? null)\n if (baseEventBus) {\n const scopes = new Set<string>()\n scopes.add('__null__')\n if (organizationId) scopes.add(organizationId)\n await Promise.all(\n Array.from(scopes).map((scope) => {\n const orgValue = scope === '__null__' ? null : scope\n return baseEventBus!\n .emitEvent(\n 'query_index.coverage.refresh',\n {\n entityType,\n tenantId: tenantId ?? null,\n organizationId: orgValue,\n delayMs: 0,\n },\n )\n .catch(() => undefined)\n }),\n )\n }\n } catch (err) {\n console.warn(' -> purge failed, continuing with reindex', err instanceof Error ? err.message : err)\n }\n } else if (purgeFirst && !tenantId) {\n console.warn(' -> skipping purge: tenant scope not provided')\n }\n\n const verbose = isIndexerVerbose()\n const progressState = verbose ? new Map<number, { last: number }>() : null\n const groupedProgress =\n !verbose && partitionTargets.length > 1\n ? createGroupedProgress(`Reindexing ${entityType}`, partitionTargets)\n : null\n const renderProgress = (part: number, info: PartitionProgressInfo) => {\n if (!progressState) return\n const state = progressState.get(part) ?? { last: 0 }\n const now = Date.now()\n if (now - state.last < 1000 && info.processed < info.total) return\n state.last = now\n progressState.set(part, state)\n const percent = info.total > 0 ? ((info.processed / info.total) * 100).toFixed(2) : '0.00'\n console.log(\n ` [${entityType}] partition ${part + 1}/${partitionCount}: ${info.processed.toLocaleString()} / ${info.total.toLocaleString()} (${percent}%)`,\n )\n }\n\n const processed = await Promise.all(\n partitionTargets.map(async (part, idx) => {\n const label = partitionTargets.length > 1 ? ` [partition ${part + 1}/${partitionCount}]` : ''\n if (partitionTargets.length === 1) {\n console.log(` -> processing${label}`)\n } else if (verbose && idx === 0) {\n console.log(` -> processing partitions in parallel (count=${partitionTargets.length})`)\n }\n\n const partitionContainer = await createRequestContainer()\n const partitionEm = partitionContainer.resolve<EntityManager>('em')\n try {\n let progressBar: CliProgressBar | null = null\n const useBar = partitionTargets.length === 1\n const stats = await reindexEntity(partitionEm, {\n entityType,\n tenantId: tenantId ?? undefined,\n organizationId: organizationId ?? undefined,\n force,\n batchSize,\n eventBus: baseEventBus ?? undefined,\n emitVectorizeEvents: true,\n partitionCount,\n partitionIndex: part,\n resetCoverage: shouldResetCoverage(part),\n onProgress(info) {\n if (useBar) {\n if (info.total > 0 && !progressBar) {\n progressBar = createProgressBar(`Reindexing ${entityType}${label}`, info.total)\n }\n progressBar?.update(info.processed)\n } else if (groupedProgress) {\n groupedProgress.onProgress(part, info)\n } else {\n renderProgress(part, info)\n }\n },\n })\n if (progressBar) (progressBar as CliProgressBar).complete()\n if (!useBar && groupedProgress) {\n groupedProgress.onProgress(part, { processed: stats.processed, total: stats.total })\n } else if (!useBar) {\n renderProgress(part, { processed: stats.processed, total: stats.total })\n } else {\n console.log(\n ` processed ${stats.processed} row(s)${stats.total ? ` (base ${stats.total})` : ''}`,\n )\n }\n return stats.processed\n } finally {\n if (typeof (partitionContainer as any)?.dispose === 'function') {\n await (partitionContainer as any).dispose()\n }\n }\n }),\n )\n\n groupedProgress?.complete()\n const totalProcessed = processed.reduce((acc, value) => acc + value, 0)\n console.log(`Finished ${entityType}: processed ${totalProcessed} row(s) across ${partitionTargets.length} partition(s)`)\n await recordIndexerLog(\n { em: baseEm ?? undefined },\n {\n source: 'vector',\n handler: 'cli:search.reindex',\n message: `Reindex completed for ${entityType}`,\n entityType,\n tenantId: tenantId ?? null,\n organizationId: organizationId ?? null,\n details: {\n processed: totalProcessed,\n partitions: partitionTargets.length,\n partitionCount,\n partitionIndex: partitionIndexOption ?? null,\n batchSize,\n },\n },\n ).catch(() => undefined)\n return totalProcessed\n }\n\n const defaultPurge = purgeFlag === true && !skipPurgeFlag\n\n if (entityId) {\n if (!enabledEntities.has(entityId)) {\n console.error(`Entity ${entityId} is not enabled for vector search.`)\n return\n }\n const purgeFirst = defaultPurge\n await runReindex(entityId, purgeFirst)\n console.log('Vector reindex completed.')\n return\n }\n\n const entityIds = searchIndexer.listEnabledEntities()\n if (!entityIds.length) {\n console.log('No entities enabled for vector search.')\n return\n }\n console.log(`Reindexing ${entityIds.length} vector-enabled entities...`)\n let processedOverall = 0\n for (let idx = 0; idx < entityIds.length; idx += 1) {\n const id = entityIds[idx]!\n console.log(`[${idx + 1}/${entityIds.length}] Preparing ${id}...`)\n processedOverall += await runReindex(id, defaultPurge)\n }\n console.log(`Vector reindex completed. Total processed rows: ${processedOverall.toLocaleString()}`)\n } catch (error) {\n const err = error instanceof Error ? error : new Error(String(error))\n console.error('[search.cli] Reindex failed:', err.stack ?? err.message)\n await recordError(err)\n throw err\n } finally {\n await disposeContainer()\n }\n}\n\nconst reindexCli: ModuleCli = {\n command: 'reindex',\n async run(rest) {\n await reindexCommand(rest)\n },\n}\n\nconst reindexHelpCli: ModuleCli = {\n command: 'reindex-help',\n async run() {\n console.log('Usage: yarn mercato search reindex [options]')\n console.log(' --tenant <id> Optional tenant scope (required for purge & coverage).')\n console.log(' --org <id> Optional organization scope (requires tenant).')\n console.log(' --entity <module:entity> Reindex a single entity (defaults to all enabled entities).')\n console.log(' --partitions <n> Number of partitions to process in parallel (default from query index).')\n console.log(' --partition <idx> Restrict to a specific partition index.')\n console.log(' --batch <n> Override batch size per chunk.')\n console.log(' --force Force reindex even if another job is running.')\n console.log(' --purgeFirst Purge vector rows before reindexing (defaults to skip).')\n console.log(' --skipPurge Explicitly skip purging vector rows.')\n console.log(' --skipResetCoverage Keep existing coverage snapshots.')\n },\n}\n\n/**\n * Start a queue worker for processing search indexing jobs.\n */\nasync function workerCommand(rest: string[]): Promise<void> {\n const queueName = rest[0]\n const args = parseArgs(rest)\n const concurrency = toPositiveInt(numberOpt(args, 'concurrency')) ?? 1\n\n const validQueues = [VECTOR_INDEXING_QUEUE_NAME, FULLTEXT_INDEXING_QUEUE_NAME]\n\n if (!queueName || !validQueues.includes(queueName)) {\n console.error('\\nUsage: yarn mercato search worker <queue-name> [options]\\n')\n console.error('Available queues:')\n console.error(` ${VECTOR_INDEXING_QUEUE_NAME} Process vector embedding indexing jobs`)\n console.error(` ${FULLTEXT_INDEXING_QUEUE_NAME} Process fulltext indexing jobs`)\n console.error('\\nOptions:')\n console.error(' --concurrency <n> Number of concurrent jobs to process (default: 1)')\n console.error('\\nExamples:')\n console.error(` yarn mercato search worker ${VECTOR_INDEXING_QUEUE_NAME} --concurrency=10`)\n console.error(` yarn mercato search worker ${FULLTEXT_INDEXING_QUEUE_NAME} --concurrency=5`)\n return\n }\n\n // Check if Redis is configured for async queue\n const queueStrategy = process.env.QUEUE_STRATEGY || 'local'\n if (queueStrategy !== 'async') {\n console.error('\\nError: Queue workers require QUEUE_STRATEGY=async')\n console.error('Set QUEUE_STRATEGY=async and configure REDIS_URL in your environment.\\n')\n return\n }\n\n const redisUrl = getRedisUrlOrThrow('QUEUE')\n\n // Dynamically import runWorker to avoid loading BullMQ unless needed\n const { runWorker } = await import('@open-mercato/queue/worker')\n\n console.log(`\\nStarting ${queueName} worker...`)\n console.log(` Concurrency: ${concurrency}`)\n console.log(` Redis: ${redisUrl.replace(/\\/\\/[^:]+:[^@]+@/, '//<credentials>@')}`)\n console.log('')\n\n if (queueName === VECTOR_INDEXING_QUEUE_NAME) {\n const { handleVectorIndexJob } = await import('./workers/vector-index.worker')\n const container = await createRequestContainer()\n\n await runWorker<VectorIndexJobPayload>({\n queueName: VECTOR_INDEXING_QUEUE_NAME,\n handler: async (job: QueuedJob<VectorIndexJobPayload>, ctx: JobContext) => {\n await handleVectorIndexJob(job, ctx, { resolve: container.resolve.bind(container) })\n },\n connection: { url: redisUrl },\n concurrency,\n })\n } else if (queueName === FULLTEXT_INDEXING_QUEUE_NAME) {\n const { handleFulltextIndexJob } = await import('./workers/fulltext-index.worker')\n const container = await createRequestContainer()\n\n await runWorker<FulltextIndexJobPayload>({\n queueName: FULLTEXT_INDEXING_QUEUE_NAME,\n handler: async (job: QueuedJob<FulltextIndexJobPayload>, ctx: JobContext) => {\n await handleFulltextIndexJob(job, ctx, { resolve: container.resolve.bind(container) })\n },\n connection: { url: redisUrl },\n concurrency,\n })\n }\n}\n\nconst workerCli: ModuleCli = {\n command: 'worker',\n async run(rest) {\n await workerCommand(rest)\n },\n}\n\nconst helpCli: ModuleCli = {\n command: 'help',\n async run() {\n console.log('\\nUsage: yarn mercato search <command> [options]\\n')\n console.log('Commands:')\n console.log(' status Show search module status and available strategies')\n console.log(' query Execute a search query')\n console.log(' index Index a specific record')\n console.log(' reindex Reindex vector embeddings for entities')\n console.log(' reindex-help Show reindex command options')\n console.log(' test-meilisearch Test Meilisearch connection')\n console.log(' worker Start a queue worker for search indexing')\n console.log(' help Show this help message')\n console.log('\\nExamples:')\n console.log(' yarn mercato search status')\n console.log(' yarn mercato search query --query \"john doe\" --tenant tenant-123')\n console.log(' yarn mercato search index --entity customers:customer_person_profile --record abc123 --tenant tenant-123')\n console.log(' yarn mercato search reindex --tenant tenant-123 --entity customers:customer_person_profile')\n console.log(' yarn mercato search test-meilisearch')\n console.log(` yarn mercato search worker ${VECTOR_INDEXING_QUEUE_NAME} --concurrency=10`)\n console.log(` yarn mercato search worker ${FULLTEXT_INDEXING_QUEUE_NAME} --concurrency=5`)\n },\n}\n\nexport default [searchCli, statusCli, indexCli, reindexCli, reindexHelpCli, testMeilisearchCli, workerCli, helpCli]\n"],
|
|
4
|
+
"sourcesContent": ["import type { ModuleCli } from '@open-mercato/shared/modules/registry'\nimport { createRequestContainer } from '@open-mercato/shared/lib/di/container'\nimport { getRedisUrlOrThrow } from '@open-mercato/shared/lib/redis/connection'\nimport { recordIndexerError } from '@open-mercato/shared/lib/indexers/error-log'\nimport { recordIndexerLog } from '@open-mercato/shared/lib/indexers/status-log'\nimport { createProgressBar } from '@open-mercato/shared/lib/cli/progress'\nimport type { EntityManager } from '@mikro-orm/postgresql'\nimport { reindexEntity, DEFAULT_REINDEX_PARTITIONS } from '@open-mercato/core/modules/query_index/lib/reindexer'\nimport { writeCoverageCounts } from '@open-mercato/core/modules/query_index/lib/coverage'\nimport type { SearchService } from '../../service'\nimport type { SearchIndexer } from '../../indexer/search-indexer'\nimport { VECTOR_INDEXING_QUEUE_NAME, type VectorIndexJobPayload } from '../../queue/vector-indexing'\nimport { FULLTEXT_INDEXING_QUEUE_NAME, type FulltextIndexJobPayload } from '../../queue/fulltext-indexing'\nimport type { QueuedJob, JobContext } from '@open-mercato/queue'\nimport type { EntityId } from '@open-mercato/shared/modules/entities'\nimport { parseBooleanToken } from '@open-mercato/shared/lib/boolean'\n\ntype CliProgressBar = {\n update(completed: number): void\n complete(): void\n}\n\ntype ParsedArgs = Record<string, string | boolean>\n\ntype PartitionProgressInfo = { processed: number; total: number }\n\nfunction isIndexerVerbose(): boolean {\n const parsed = parseBooleanToken(process.env.OM_INDEXER_VERBOSE ?? '')\n return parsed === true\n}\n\nfunction createGroupedProgress(label: string, partitionTargets: number[]) {\n const totals = new Map<number, number>()\n const processed = new Map<number, number>()\n let bar: CliProgressBar | null = null\n\n const getTotals = () => {\n let total = 0\n let done = 0\n for (const value of totals.values()) total += value\n for (const value of processed.values()) done += value\n return { total, done }\n }\n\n const tryInitBar = () => {\n if (bar) return\n if (totals.size < partitionTargets.length) return\n const { total } = getTotals()\n if (total <= 0) return\n bar = createProgressBar(label, total)\n }\n\n return {\n onProgress(partition: number, info: PartitionProgressInfo) {\n processed.set(partition, info.processed)\n if (!totals.has(partition)) totals.set(partition, info.total)\n tryInitBar()\n if (!bar) return\n const { done } = getTotals()\n bar.update(done)\n },\n complete() {\n if (bar) bar.complete()\n },\n getTotals,\n }\n}\n\nfunction parseArgs(rest: string[]): ParsedArgs {\n const args: ParsedArgs = {}\n for (let i = 0; i < rest.length; i += 1) {\n const part = rest[i]\n if (!part?.startsWith('--')) continue\n const [rawKey, rawValue] = part.slice(2).split('=')\n if (!rawKey) continue\n if (rawValue !== undefined) {\n args[rawKey] = rawValue\n } else if (i + 1 < rest.length && !rest[i + 1]!.startsWith('--')) {\n args[rawKey] = rest[i + 1]!\n i += 1\n } else {\n args[rawKey] = true\n }\n }\n return args\n}\n\nfunction stringOpt(args: ParsedArgs, ...keys: string[]): string | undefined {\n for (const key of keys) {\n const raw = args[key]\n if (typeof raw !== 'string') continue\n const trimmed = raw.trim()\n if (trimmed.length > 0) return trimmed\n }\n return undefined\n}\n\nfunction numberOpt(args: ParsedArgs, ...keys: string[]): number | undefined {\n for (const key of keys) {\n const raw = args[key]\n if (typeof raw === 'number') return raw\n if (typeof raw === 'string') {\n const parsed = Number(raw)\n if (Number.isFinite(parsed)) return parsed\n }\n }\n return undefined\n}\n\nfunction flagOpt(args: ParsedArgs, ...keys: string[]): boolean | undefined {\n for (const key of keys) {\n const raw = args[key]\n if (raw === undefined) continue\n if (raw === true) return true\n if (raw === false) return false\n if (typeof raw === 'string') {\n const trimmed = raw.trim()\n if (!trimmed) return true\n const parsed = parseBooleanToken(trimmed)\n return parsed === null ? true : parsed\n }\n }\n return undefined\n}\n\nfunction toPositiveInt(value: number | undefined): number | undefined {\n if (value === undefined) return undefined\n const n = Math.floor(value)\n if (!Number.isFinite(n) || n <= 0) return undefined\n return n\n}\n\nfunction toNonNegativeInt(value: number | undefined, fallback = 0): number {\n if (value === undefined) return fallback\n const n = Math.floor(value)\n if (!Number.isFinite(n) || n < 0) return fallback\n return n\n}\n\n/**\n * Test search functionality with a query\n */\nasync function searchCommand(rest: string[]): Promise<void> {\n const args = parseArgs(rest)\n const query = stringOpt(args, 'query', 'q')\n const tenantId = stringOpt(args, 'tenant', 'tenantId')\n const organizationId = stringOpt(args, 'org', 'organizationId')\n const entityTypes = stringOpt(args, 'entity', 'entities')\n const strategies = stringOpt(args, 'strategy', 'strategies')\n const limit = numberOpt(args, 'limit') ?? 20\n\n if (!query) {\n console.error('Usage: yarn mercato search query --query \"search terms\" --tenant <id> [options]')\n console.error(' --query, -q Search query (required)')\n console.error(' --tenant Tenant ID (required)')\n console.error(' --org Organization ID (optional)')\n console.error(' --entity Entity types to search (comma-separated)')\n console.error(' --strategy Strategies to use (comma-separated: meilisearch,vector,tokens)')\n console.error(' --limit Max results (default: 20)')\n return\n }\n\n if (!tenantId) {\n console.error('Error: --tenant is required')\n return\n }\n\n const container = await createRequestContainer()\n\n try {\n const searchService = container.resolve('searchService') as SearchService | undefined\n\n if (!searchService) {\n console.error('Error: SearchService not available. Make sure the search module is registered.')\n return\n }\n\n console.log(`\\nSearching for: \"${query}\"`)\n console.log(`Tenant: ${tenantId}`)\n if (organizationId) console.log(`Organization: ${organizationId}`)\n console.log('---')\n\n const results = await searchService.search(query, {\n tenantId,\n organizationId,\n entityTypes: entityTypes?.split(',').map(s => s.trim()),\n strategies: strategies?.split(',').map(s => s.trim()) as any,\n limit,\n })\n\n if (results.length === 0) {\n console.log('No results found.')\n return\n }\n\n console.log(`\\nFound ${results.length} result(s):\\n`)\n\n for (let i = 0; i < results.length; i++) {\n const result = results[i]\n console.log(`${i + 1}. [${result.source}] ${result.entityId}`)\n console.log(` Record ID: ${result.recordId}`)\n console.log(` Score: ${result.score.toFixed(4)}`)\n if (result.presenter) {\n console.log(` Title: ${result.presenter.title}`)\n if (result.presenter.subtitle) console.log(` Subtitle: ${result.presenter.subtitle}`)\n }\n if (result.url) console.log(` URL: ${result.url}`)\n console.log('')\n }\n } finally {\n try {\n const em = container.resolve('em') as any\n await em?.getConnection?.()?.close?.()\n } catch {}\n }\n}\n\n/**\n * Show status of search strategies\n */\nasync function statusCommand(): Promise<void> {\n const container = await createRequestContainer()\n\n try {\n const searchService = container.resolve('searchService') as SearchService | undefined\n const strategies = container.resolve('searchStrategies') as any[] | undefined\n\n console.log('\\n=== Search Module Status ===\\n')\n\n if (!searchService) {\n console.log('SearchService: NOT REGISTERED')\n return\n }\n\n console.log('SearchService: ACTIVE')\n console.log('')\n\n if (!strategies || strategies.length === 0) {\n console.log('Strategies: NONE CONFIGURED')\n return\n }\n\n console.log('Strategies:')\n console.log('-----------')\n\n for (const strategy of strategies) {\n const available = await strategy.isAvailable?.() ?? true\n const status = available ? 'AVAILABLE' : 'UNAVAILABLE'\n const icon = available ? '\u2713' : '\u2717'\n console.log(` ${icon} ${strategy.name ?? strategy.id} (${strategy.id})`)\n console.log(` Status: ${status}`)\n console.log(` Priority: ${strategy.priority ?? 'N/A'}`)\n console.log('')\n }\n\n // Check environment variables\n console.log('Environment:')\n console.log('------------')\n console.log(` MEILISEARCH_HOST: ${process.env.MEILISEARCH_HOST ?? '(not set)'}`)\n console.log(` MEILISEARCH_API_KEY: ${process.env.MEILISEARCH_API_KEY ? '(set)' : '(not set)'}`)\n console.log(` OPENAI_API_KEY: ${process.env.OPENAI_API_KEY ? '(set)' : '(not set)'}`)\n console.log(` OM_SEARCH_ENABLED: ${process.env.OM_SEARCH_ENABLED ?? 'true (default)'}`)\n console.log('')\n } finally {\n try {\n const em = container.resolve('em') as any\n await em?.getConnection?.()?.close?.()\n } catch {}\n }\n}\n\n/**\n * Index a specific record for testing\n */\nasync function indexCommand(rest: string[]): Promise<void> {\n const args = parseArgs(rest)\n const entityId = stringOpt(args, 'entity', 'entityId')\n const recordId = stringOpt(args, 'record', 'recordId')\n const tenantId = stringOpt(args, 'tenant', 'tenantId')\n const organizationId = stringOpt(args, 'org', 'organizationId')\n\n if (!entityId || !recordId || !tenantId) {\n console.error('Usage: yarn mercato search index --entity <entityId> --record <recordId> --tenant <tenantId>')\n console.error(' --entity Entity ID (e.g., customers:customer_person_profile)')\n console.error(' --record Record ID')\n console.error(' --tenant Tenant ID')\n console.error(' --org Organization ID (optional)')\n return\n }\n\n const container = await createRequestContainer()\n\n try {\n const searchIndexer = container.resolve('searchIndexer') as SearchIndexer | undefined\n\n if (!searchIndexer) {\n console.error('Error: SearchIndexer not available.')\n return\n }\n\n // Load record from query engine\n const queryEngine = container.resolve('queryEngine') as any\n\n console.log(`\\nLoading record: ${entityId} / ${recordId}`)\n\n const result = await queryEngine.query(entityId, {\n tenantId,\n organizationId,\n filters: { id: recordId },\n includeCustomFields: true,\n page: { page: 1, pageSize: 1 },\n })\n\n const record = result.items[0]\n\n if (!record) {\n console.error('Error: Record not found')\n return\n }\n\n console.log('Record loaded, indexing...')\n\n // Extract custom fields\n const customFields: Record<string, unknown> = {}\n for (const [key, value] of Object.entries(record)) {\n if (key.startsWith('cf:') || key.startsWith('cf_')) {\n const cfKey = key.slice(3) // Remove 'cf:' or 'cf_' prefix (both are 3 chars)\n customFields[cfKey] = value\n }\n }\n\n await searchIndexer.indexRecord({\n entityId,\n recordId,\n tenantId,\n organizationId,\n record,\n customFields,\n })\n\n console.log('Record indexed successfully!')\n } finally {\n try {\n const em = container.resolve('em') as any\n await em?.getConnection?.()?.close?.()\n } catch {}\n }\n}\n\n/**\n * Test Meilisearch connection directly\n */\nasync function testMeilisearchCommand(): Promise<void> {\n const host = process.env.MEILISEARCH_HOST\n const apiKey = process.env.MEILISEARCH_API_KEY\n\n console.log('\\n=== Meilisearch Connection Test ===\\n')\n\n if (!host) {\n console.log('MEILISEARCH_HOST: NOT SET')\n console.log('\\nMeilisearch is not configured. Set MEILISEARCH_HOST in your .env file.')\n return\n }\n\n console.log(`Host: ${host}`)\n console.log(`API Key: ${apiKey ? '(configured)' : '(not set)'}`)\n console.log('')\n\n try {\n const { Meilisearch } = await import('meilisearch')\n const client = new Meilisearch({ host, apiKey })\n\n console.log('Testing connection...')\n const health = await client.health()\n console.log(`Health: ${health.status}`)\n\n console.log('\\nListing indexes...')\n const indexes = await client.getIndexes()\n\n if (indexes.results.length === 0) {\n console.log('No indexes found.')\n } else {\n console.log(`Found ${indexes.results.length} index(es):`)\n for (const index of indexes.results) {\n const stats = await client.index(index.uid).getStats()\n console.log(` - ${index.uid}: ${stats.numberOfDocuments} documents`)\n }\n }\n\n console.log('\\nMeilisearch connection successful!')\n } catch (error) {\n console.error('Connection failed:', error instanceof Error ? error.message : error)\n }\n}\n\nconst searchCli: ModuleCli = {\n command: 'query',\n async run(rest) {\n await searchCommand(rest)\n },\n}\n\nconst statusCli: ModuleCli = {\n command: 'status',\n async run() {\n await statusCommand()\n },\n}\n\nconst indexCli: ModuleCli = {\n command: 'index',\n async run(rest) {\n await indexCommand(rest)\n },\n}\n\nconst testMeilisearchCli: ModuleCli = {\n command: 'test-meilisearch',\n async run() {\n await testMeilisearchCommand()\n },\n}\n\nasync function resetVectorCoverageAfterPurge(\n em: EntityManager | null,\n entityId: string,\n tenantId: string | null,\n organizationId: string | null,\n): Promise<void> {\n if (!em || !entityId) return\n try {\n const scopes = new Set<string>()\n scopes.add('__null__')\n if (organizationId) scopes.add(organizationId)\n for (const scope of scopes) {\n const orgValue = scope === '__null__' ? null : scope\n await writeCoverageCounts(\n em,\n {\n entityType: entityId,\n tenantId,\n organizationId: orgValue,\n withDeleted: false,\n },\n { vectorCount: 0 },\n )\n }\n } catch (error) {\n console.warn('[search.cli] Failed to reset vector coverage after purge', error instanceof Error ? error.message : error)\n }\n}\n\nasync function reindexCommand(rest: string[]): Promise<void> {\n const args = parseArgs(rest)\n const tenantId = stringOpt(args, 'tenant', 'tenantId')\n const organizationId = stringOpt(args, 'org', 'orgId', 'organizationId')\n const entityId = stringOpt(args, 'entity', 'entityId')\n const force = flagOpt(args, 'force', 'full') === true\n const batchSize = toPositiveInt(numberOpt(args, 'batch', 'chunk', 'size'))\n const partitionsOption = toPositiveInt(numberOpt(args, 'partitions', 'partitionCount', 'parallel'))\n const partitionIndexRaw = numberOpt(args, 'partition', 'partitionIndex')\n const partitionIndexOption = partitionIndexRaw === undefined ? undefined : toNonNegativeInt(partitionIndexRaw, 0)\n const resetCoverageFlag = flagOpt(args, 'resetCoverage') === true\n const skipResetCoverageFlag = flagOpt(args, 'skipResetCoverage', 'noResetCoverage') === true\n const skipPurgeFlag = flagOpt(args, 'skipPurge', 'noPurge') === true\n const purgeFlag = flagOpt(args, 'purge', 'purgeFirst')\n\n const container = await createRequestContainer()\n let baseEm: EntityManager | null = null\n try {\n baseEm = (container.resolve('em') as EntityManager)\n } catch {\n baseEm = null\n }\n\n const disposeContainer = async () => {\n if (typeof (container as any)?.dispose === 'function') {\n await (container as any).dispose()\n }\n }\n\n const recordError = async (error: Error) => {\n await recordIndexerLog(\n { em: baseEm ?? undefined },\n {\n source: 'vector',\n handler: 'cli:search.reindex',\n level: 'warn',\n message: `Reindex failed${entityId ? ` for ${entityId}` : ''}`,\n entityType: entityId ?? null,\n tenantId: tenantId ?? null,\n organizationId: organizationId ?? null,\n details: { error: error.message },\n },\n ).catch(() => undefined)\n await recordIndexerError(\n { em: baseEm ?? undefined },\n {\n source: 'vector',\n handler: 'cli:search.reindex',\n error,\n entityType: entityId ?? null,\n tenantId: tenantId ?? null,\n organizationId: organizationId ?? null,\n payload: {\n args,\n force,\n batchSize,\n partitionsOption,\n partitionIndexOption,\n resetCoverageFlag,\n skipResetCoverageFlag,\n skipPurgeFlag,\n purgeFlag,\n },\n },\n )\n }\n\n try {\n const searchIndexer = container.resolve<SearchIndexer>('searchIndexer')\n const enabledEntities = new Set(searchIndexer.listEnabledEntities())\n const baseEventBus = (() => {\n try {\n return container.resolve('eventBus') as {\n emitEvent(event: string, payload: any, options?: any): Promise<void>\n }\n } catch {\n return null\n }\n })()\n if (!baseEventBus) {\n console.warn('[search.cli] eventBus unavailable; vector embeddings may not be refreshed. Run bootstrap or ensure event bus configuration.')\n }\n\n const partitionCount = Math.max(1, partitionsOption ?? DEFAULT_REINDEX_PARTITIONS)\n if (partitionIndexOption !== undefined && partitionIndexOption >= partitionCount) {\n console.error(`partitionIndex (${partitionIndexOption}) must be < partitionCount (${partitionCount})`)\n return\n }\n const partitionTargets =\n partitionIndexOption !== undefined\n ? [partitionIndexOption]\n : Array.from({ length: partitionCount }, (_, idx) => idx)\n\n const shouldResetCoverage = (partition: number): boolean => {\n if (resetCoverageFlag) return true\n if (skipResetCoverageFlag) return false\n if (partitionIndexOption !== undefined) return partitionIndexOption === 0\n return partition === partitionTargets[0]\n }\n\n const runReindex = async (entityType: string, purgeFirst: boolean) => {\n const scopeLabel = tenantId\n ? `tenant=${tenantId}${organizationId ? `, org=${organizationId}` : ''}`\n : 'all tenants'\n console.log(`Reindexing vectors for ${entityType} (${scopeLabel})${purgeFirst ? ' [purge]' : ''}`)\n await recordIndexerLog(\n { em: baseEm ?? undefined },\n {\n source: 'vector',\n handler: 'cli:search.reindex',\n message: `Reindex started for ${entityType}`,\n entityType,\n tenantId: tenantId ?? null,\n organizationId: organizationId ?? null,\n details: {\n purgeFirst,\n partitions: partitionTargets.length,\n partitionCount,\n partitionIndex: partitionIndexOption ?? null,\n batchSize,\n },\n },\n ).catch(() => undefined)\n\n if (purgeFirst && tenantId) {\n try {\n console.log(' -> purging existing vector index rows...')\n await searchIndexer.purgeEntity({ entityId: entityType as EntityId, tenantId })\n await resetVectorCoverageAfterPurge(baseEm, entityType, tenantId ?? null, organizationId ?? null)\n if (baseEventBus) {\n const scopes = new Set<string>()\n scopes.add('__null__')\n if (organizationId) scopes.add(organizationId)\n await Promise.all(\n Array.from(scopes).map((scope) => {\n const orgValue = scope === '__null__' ? null : scope\n return baseEventBus!\n .emitEvent(\n 'query_index.coverage.refresh',\n {\n entityType,\n tenantId: tenantId ?? null,\n organizationId: orgValue,\n delayMs: 0,\n },\n )\n .catch(() => undefined)\n }),\n )\n }\n } catch (err) {\n console.warn(' -> purge failed, continuing with reindex', err instanceof Error ? err.message : err)\n }\n } else if (purgeFirst && !tenantId) {\n console.warn(' -> skipping purge: tenant scope not provided')\n }\n\n const verbose = isIndexerVerbose()\n const progressState = verbose ? new Map<number, { last: number }>() : null\n const groupedProgress =\n !verbose && partitionTargets.length > 1\n ? createGroupedProgress(`Reindexing ${entityType}`, partitionTargets)\n : null\n const renderProgress = (part: number, info: PartitionProgressInfo) => {\n if (!progressState) return\n const state = progressState.get(part) ?? { last: 0 }\n const now = Date.now()\n if (now - state.last < 1000 && info.processed < info.total) return\n state.last = now\n progressState.set(part, state)\n const percent = info.total > 0 ? ((info.processed / info.total) * 100).toFixed(2) : '0.00'\n console.log(\n ` [${entityType}] partition ${part + 1}/${partitionCount}: ${info.processed.toLocaleString()} / ${info.total.toLocaleString()} (${percent}%)`,\n )\n }\n\n const processed = await Promise.all(\n partitionTargets.map(async (part, idx) => {\n const label = partitionTargets.length > 1 ? ` [partition ${part + 1}/${partitionCount}]` : ''\n if (partitionTargets.length === 1) {\n console.log(` -> processing${label}`)\n } else if (verbose && idx === 0) {\n console.log(` -> processing partitions in parallel (count=${partitionTargets.length})`)\n }\n\n const partitionContainer = await createRequestContainer()\n const partitionEm = partitionContainer.resolve<EntityManager>('em')\n try {\n let progressBar: CliProgressBar | null = null\n const useBar = partitionTargets.length === 1\n const stats = await reindexEntity(partitionEm, {\n entityType,\n tenantId: tenantId ?? undefined,\n organizationId: organizationId ?? undefined,\n force,\n batchSize,\n eventBus: baseEventBus ?? undefined,\n emitVectorizeEvents: true,\n partitionCount,\n partitionIndex: part,\n resetCoverage: shouldResetCoverage(part),\n onProgress(info) {\n if (useBar) {\n if (info.total > 0 && !progressBar) {\n progressBar = createProgressBar(`Reindexing ${entityType}${label}`, info.total)\n }\n progressBar?.update(info.processed)\n } else if (groupedProgress) {\n groupedProgress.onProgress(part, info)\n } else {\n renderProgress(part, info)\n }\n },\n })\n if (progressBar) (progressBar as CliProgressBar).complete()\n if (!useBar && groupedProgress) {\n groupedProgress.onProgress(part, { processed: stats.processed, total: stats.total })\n } else if (!useBar) {\n renderProgress(part, { processed: stats.processed, total: stats.total })\n } else {\n console.log(\n ` processed ${stats.processed} row(s)${stats.total ? ` (base ${stats.total})` : ''}`,\n )\n }\n return stats.processed\n } finally {\n if (typeof (partitionContainer as any)?.dispose === 'function') {\n await (partitionContainer as any).dispose()\n }\n }\n }),\n )\n\n groupedProgress?.complete()\n const totalProcessed = processed.reduce((acc, value) => acc + value, 0)\n console.log(`Finished ${entityType}: processed ${totalProcessed} row(s) across ${partitionTargets.length} partition(s)`)\n await recordIndexerLog(\n { em: baseEm ?? undefined },\n {\n source: 'vector',\n handler: 'cli:search.reindex',\n message: `Reindex completed for ${entityType}`,\n entityType,\n tenantId: tenantId ?? null,\n organizationId: organizationId ?? null,\n details: {\n processed: totalProcessed,\n partitions: partitionTargets.length,\n partitionCount,\n partitionIndex: partitionIndexOption ?? null,\n batchSize,\n },\n },\n ).catch(() => undefined)\n return totalProcessed\n }\n\n const defaultPurge = purgeFlag === true && !skipPurgeFlag\n\n if (entityId) {\n if (!enabledEntities.has(entityId)) {\n console.error(`Entity ${entityId} is not enabled for vector search.`)\n return\n }\n const purgeFirst = defaultPurge\n await runReindex(entityId, purgeFirst)\n console.log('Vector reindex completed.')\n return\n }\n\n const entityIds = searchIndexer.listEnabledEntities()\n if (!entityIds.length) {\n console.log('No entities enabled for vector search.')\n return\n }\n console.log(`Reindexing ${entityIds.length} vector-enabled entities...`)\n let processedOverall = 0\n for (let idx = 0; idx < entityIds.length; idx += 1) {\n const id = entityIds[idx]!\n console.log(`[${idx + 1}/${entityIds.length}] Preparing ${id}...`)\n processedOverall += await runReindex(id, defaultPurge)\n }\n console.log(`Vector reindex completed. Total processed rows: ${processedOverall.toLocaleString()}`)\n } catch (error) {\n const err = error instanceof Error ? error : new Error(String(error))\n console.error('[search.cli] Reindex failed:', err.stack ?? err.message)\n await recordError(err)\n throw err\n } finally {\n await disposeContainer()\n }\n}\n\nconst reindexCli: ModuleCli = {\n command: 'reindex',\n async run(rest) {\n await reindexCommand(rest)\n },\n}\n\nconst reindexHelpCli: ModuleCli = {\n command: 'reindex-help',\n async run() {\n console.log('Usage: yarn mercato search reindex [options]')\n console.log(' --tenant <id> Optional tenant scope (required for purge & coverage).')\n console.log(' --org <id> Optional organization scope (requires tenant).')\n console.log(' --entity <module:entity> Reindex a single entity (defaults to all enabled entities).')\n console.log(' --partitions <n> Number of partitions to process in parallel (default from query index).')\n console.log(' --partition <idx> Restrict to a specific partition index.')\n console.log(' --batch <n> Override batch size per chunk.')\n console.log(' --force Force reindex even if another job is running.')\n console.log(' --purgeFirst Purge vector rows before reindexing (defaults to skip).')\n console.log(' --skipPurge Explicitly skip purging vector rows.')\n console.log(' --skipResetCoverage Keep existing coverage snapshots.')\n },\n}\n\n/**\n * Start a queue worker for processing search indexing jobs.\n */\nasync function workerCommand(rest: string[]): Promise<void> {\n const queueName = rest[0]\n const args = parseArgs(rest)\n const concurrency = toPositiveInt(numberOpt(args, 'concurrency')) ?? 1\n\n const validQueues = [VECTOR_INDEXING_QUEUE_NAME, FULLTEXT_INDEXING_QUEUE_NAME]\n\n if (!queueName || !validQueues.includes(queueName)) {\n console.error('\\nUsage: yarn mercato search worker <queue-name> [options]\\n')\n console.error('Available queues:')\n console.error(` ${VECTOR_INDEXING_QUEUE_NAME} Process vector embedding indexing jobs`)\n console.error(` ${FULLTEXT_INDEXING_QUEUE_NAME} Process fulltext indexing jobs`)\n console.error('\\nOptions:')\n console.error(' --concurrency <n> Number of concurrent jobs to process (default: 1)')\n console.error('\\nExamples:')\n console.error(` yarn mercato search worker ${VECTOR_INDEXING_QUEUE_NAME} --concurrency=10`)\n console.error(` yarn mercato search worker ${FULLTEXT_INDEXING_QUEUE_NAME} --concurrency=5`)\n return\n }\n\n // Check if Redis is configured for async queue\n const queueStrategy = process.env.QUEUE_STRATEGY || 'local'\n if (queueStrategy !== 'async') {\n console.error('\\nError: Queue workers require QUEUE_STRATEGY=async')\n console.error('Set QUEUE_STRATEGY=async and configure REDIS_URL in your environment.\\n')\n return\n }\n\n const redisUrl = getRedisUrlOrThrow('QUEUE')\n\n // Dynamically import runWorker to avoid loading BullMQ unless needed\n const { runWorker } = await import('@open-mercato/queue/worker')\n\n console.log(`\\nStarting ${queueName} worker...`)\n console.log(` Concurrency: ${concurrency}`)\n console.log(` Redis: ${redisUrl.replace(/\\/\\/[^:]+:[^@]+@/, '//<credentials>@')}`)\n console.log('')\n\n if (queueName === VECTOR_INDEXING_QUEUE_NAME) {\n const { handleVectorIndexJob } = await import('./workers/vector-index.worker')\n const container = await createRequestContainer()\n\n await runWorker<VectorIndexJobPayload>({\n queueName: VECTOR_INDEXING_QUEUE_NAME,\n handler: async (job: QueuedJob<VectorIndexJobPayload>, ctx: JobContext) => {\n await handleVectorIndexJob(job, ctx, { resolve: container.resolve.bind(container) })\n },\n connection: { url: redisUrl },\n concurrency,\n })\n } else if (queueName === FULLTEXT_INDEXING_QUEUE_NAME) {\n const { handleFulltextIndexJob } = await import('./workers/fulltext-index.worker')\n const container = await createRequestContainer()\n\n await runWorker<FulltextIndexJobPayload>({\n queueName: FULLTEXT_INDEXING_QUEUE_NAME,\n handler: async (job: QueuedJob<FulltextIndexJobPayload>, ctx: JobContext) => {\n await handleFulltextIndexJob(job, ctx, { resolve: container.resolve.bind(container) })\n },\n connection: { url: redisUrl },\n concurrency,\n })\n }\n}\n\nconst workerCli: ModuleCli = {\n command: 'worker',\n async run(rest) {\n await workerCommand(rest)\n },\n}\n\nconst helpCli: ModuleCli = {\n command: 'help',\n async run() {\n console.log('\\nUsage: yarn mercato search <command> [options]\\n')\n console.log('Commands:')\n console.log(' status Show search module status and available strategies')\n console.log(' query Execute a search query')\n console.log(' index Index a specific record')\n console.log(' reindex Reindex vector embeddings for entities')\n console.log(' reindex-help Show reindex command options')\n console.log(' test-meilisearch Test Meilisearch connection')\n console.log(' worker Start a queue worker for search indexing')\n console.log(' help Show this help message')\n console.log('\\nExamples:')\n console.log(' yarn mercato search status')\n console.log(' yarn mercato search query --query \"john doe\" --tenant tenant-123')\n console.log(' yarn mercato search index --entity customers:customer_person_profile --record abc123 --tenant tenant-123')\n console.log(' yarn mercato search reindex --tenant tenant-123 --entity customers:customer_person_profile')\n console.log(' yarn mercato search test-meilisearch')\n console.log(` yarn mercato search worker ${VECTOR_INDEXING_QUEUE_NAME} --concurrency=10`)\n console.log(` yarn mercato search worker ${FULLTEXT_INDEXING_QUEUE_NAME} --concurrency=5`)\n },\n}\n\nexport default [searchCli, statusCli, indexCli, reindexCli, reindexHelpCli, testMeilisearchCli, workerCli, helpCli]\n"],
|
|
5
5
|
"mappings": "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",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@open-mercato/search",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.5.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"main": "./dist/index.js",
|
|
6
6
|
"exports": {
|
|
@@ -114,29 +114,33 @@
|
|
|
114
114
|
"typecheck": "tsc --noEmit"
|
|
115
115
|
},
|
|
116
116
|
"dependencies": {
|
|
117
|
-
"@ai-sdk/amazon-bedrock": "^4.0.
|
|
118
|
-
"@ai-sdk/cohere": "^3.0.
|
|
119
|
-
"@ai-sdk/google": "^
|
|
120
|
-
"@ai-sdk/mistral": "^3.0.
|
|
121
|
-
"@ai-sdk/openai": "^3.0.
|
|
122
|
-
"ai": "^6.0.
|
|
123
|
-
"ai-sdk-ollama": "3.
|
|
124
|
-
"meilisearch": "^0.
|
|
117
|
+
"@ai-sdk/amazon-bedrock": "^4.0.96",
|
|
118
|
+
"@ai-sdk/cohere": "^3.0.30",
|
|
119
|
+
"@ai-sdk/google": "^3.0.64",
|
|
120
|
+
"@ai-sdk/mistral": "^3.0.30",
|
|
121
|
+
"@ai-sdk/openai": "^3.0.53",
|
|
122
|
+
"ai": "^6.0.168",
|
|
123
|
+
"ai-sdk-ollama": "3.8.3",
|
|
124
|
+
"meilisearch": "^0.57.0",
|
|
125
125
|
"pg": "8.20.0",
|
|
126
|
-
"zod": "^4.
|
|
126
|
+
"zod": "^4.3.6"
|
|
127
127
|
},
|
|
128
128
|
"peerDependencies": {
|
|
129
|
-
"@open-mercato/core": "0.
|
|
130
|
-
"@open-mercato/queue": "0.
|
|
131
|
-
"@open-mercato/shared": "0.
|
|
129
|
+
"@open-mercato/core": "0.5.0",
|
|
130
|
+
"@open-mercato/queue": "0.5.0",
|
|
131
|
+
"@open-mercato/shared": "0.5.0"
|
|
132
132
|
},
|
|
133
133
|
"devDependencies": {
|
|
134
134
|
"@types/jest": "^30.0.0",
|
|
135
|
-
"jest": "^30.
|
|
136
|
-
"ts-jest": "^29.4.
|
|
135
|
+
"jest": "^30.3.0",
|
|
136
|
+
"ts-jest": "^29.4.9"
|
|
137
137
|
},
|
|
138
138
|
"publishConfig": {
|
|
139
139
|
"access": "public"
|
|
140
140
|
},
|
|
141
|
-
"
|
|
141
|
+
"repository": {
|
|
142
|
+
"type": "git",
|
|
143
|
+
"url": "https://github.com/open-mercato/open-mercato",
|
|
144
|
+
"directory": "packages/search"
|
|
145
|
+
}
|
|
142
146
|
}
|
|
@@ -444,9 +444,15 @@ describe('SearchService', () => {
|
|
|
444
444
|
const start = Date.now()
|
|
445
445
|
await service.search('q', { tenantId: 't-1' })
|
|
446
446
|
const elapsed = Date.now() - start
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
447
|
+
const timings = Object.values(probeTimings)
|
|
448
|
+
const latestStart = Math.max(...timings.map((timing) => timing.start))
|
|
449
|
+
const earliestEnd = Math.min(...timings.map((timing) => timing.end))
|
|
450
|
+
|
|
451
|
+
// Sequential probes would not overlap. Parallel probes must overlap in time,
|
|
452
|
+
// and total elapsed time should stay well below a fully sequential run.
|
|
453
|
+
expect(timings).toHaveLength(3)
|
|
454
|
+
expect(latestStart).toBeLessThan(earliestEnd)
|
|
455
|
+
expect(elapsed).toBeLessThan(450)
|
|
450
456
|
expect(slowA.isAvailable).toHaveBeenCalledTimes(1)
|
|
451
457
|
expect(slowB.isAvailable).toHaveBeenCalledTimes(1)
|
|
452
458
|
expect(slowC.isAvailable).toHaveBeenCalledTimes(1)
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { Meilisearch } from 'meilisearch'
|
|
2
2
|
import type { EntityId } from '@open-mercato/shared/modules/entities'
|
|
3
3
|
import type { SearchFieldPolicy } from '@open-mercato/shared/modules/search'
|
|
4
4
|
import { resolveTimeoutMs } from '@open-mercato/shared/lib/http/fetchWithTimeout'
|
|
@@ -43,13 +43,13 @@ export function createMeilisearchDriver(
|
|
|
43
43
|
const encryptionMapResolver = options?.encryptionMapResolver
|
|
44
44
|
const fieldPolicyResolver = options?.fieldPolicyResolver
|
|
45
45
|
|
|
46
|
-
let client:
|
|
46
|
+
let client: Meilisearch | null = null
|
|
47
47
|
const initializedIndexes = new Set<string>()
|
|
48
48
|
const initializingIndexes = new Map<string, Promise<void>>()
|
|
49
49
|
|
|
50
|
-
function getClient():
|
|
50
|
+
function getClient(): Meilisearch {
|
|
51
51
|
if (!client) {
|
|
52
|
-
client = new
|
|
52
|
+
client = new Meilisearch({ host, apiKey, timeout: requestTimeoutMs })
|
|
53
53
|
}
|
|
54
54
|
return client
|
|
55
55
|
}
|
|
@@ -367,8 +367,8 @@ async function testMeilisearchCommand(): Promise<void> {
|
|
|
367
367
|
console.log('')
|
|
368
368
|
|
|
369
369
|
try {
|
|
370
|
-
const {
|
|
371
|
-
const client = new
|
|
370
|
+
const { Meilisearch } = await import('meilisearch')
|
|
371
|
+
const client = new Meilisearch({ host, apiKey })
|
|
372
372
|
|
|
373
373
|
console.log('Testing connection...')
|
|
374
374
|
const health = await client.health()
|