@open-mercato/search 0.4.2-canary-c02407ff85

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.
Files changed (237) hide show
  1. package/AGENTS.md +678 -0
  2. package/build.mjs +92 -0
  3. package/dist/di.js +157 -0
  4. package/dist/di.js.map +7 -0
  5. package/dist/fulltext/drivers/index.js +21 -0
  6. package/dist/fulltext/drivers/index.js.map +7 -0
  7. package/dist/fulltext/drivers/meilisearch/index.js +320 -0
  8. package/dist/fulltext/drivers/meilisearch/index.js.map +7 -0
  9. package/dist/fulltext/index.js +7 -0
  10. package/dist/fulltext/index.js.map +7 -0
  11. package/dist/fulltext/types.js +1 -0
  12. package/dist/fulltext/types.js.map +7 -0
  13. package/dist/index.js +12 -0
  14. package/dist/index.js.map +7 -0
  15. package/dist/indexer/index.js +8 -0
  16. package/dist/indexer/index.js.map +7 -0
  17. package/dist/indexer/search-indexer.js +848 -0
  18. package/dist/indexer/search-indexer.js.map +7 -0
  19. package/dist/indexer/subscribers/delete.js +41 -0
  20. package/dist/indexer/subscribers/delete.js.map +7 -0
  21. package/dist/lib/debug.js +34 -0
  22. package/dist/lib/debug.js.map +7 -0
  23. package/dist/lib/fallback-presenter.js +107 -0
  24. package/dist/lib/fallback-presenter.js.map +7 -0
  25. package/dist/lib/field-policy.js +75 -0
  26. package/dist/lib/field-policy.js.map +7 -0
  27. package/dist/lib/index.js +19 -0
  28. package/dist/lib/index.js.map +7 -0
  29. package/dist/lib/merger.js +93 -0
  30. package/dist/lib/merger.js.map +7 -0
  31. package/dist/lib/presenter-enricher.js +192 -0
  32. package/dist/lib/presenter-enricher.js.map +7 -0
  33. package/dist/modules/search/acl.js +14 -0
  34. package/dist/modules/search/acl.js.map +7 -0
  35. package/dist/modules/search/ai-tools.js +284 -0
  36. package/dist/modules/search/ai-tools.js.map +7 -0
  37. package/dist/modules/search/api/embeddings/reindex/cancel/route.js +65 -0
  38. package/dist/modules/search/api/embeddings/reindex/cancel/route.js.map +7 -0
  39. package/dist/modules/search/api/embeddings/reindex/route.js +165 -0
  40. package/dist/modules/search/api/embeddings/reindex/route.js.map +7 -0
  41. package/dist/modules/search/api/embeddings/route.js +246 -0
  42. package/dist/modules/search/api/embeddings/route.js.map +7 -0
  43. package/dist/modules/search/api/index/route.js +245 -0
  44. package/dist/modules/search/api/index/route.js.map +7 -0
  45. package/dist/modules/search/api/reindex/cancel/route.js +65 -0
  46. package/dist/modules/search/api/reindex/cancel/route.js.map +7 -0
  47. package/dist/modules/search/api/reindex/route.js +332 -0
  48. package/dist/modules/search/api/reindex/route.js.map +7 -0
  49. package/dist/modules/search/api/search/global/route.js +100 -0
  50. package/dist/modules/search/api/search/global/route.js.map +7 -0
  51. package/dist/modules/search/api/search/route.js +101 -0
  52. package/dist/modules/search/api/search/route.js.map +7 -0
  53. package/dist/modules/search/api/settings/fulltext/route.js +55 -0
  54. package/dist/modules/search/api/settings/fulltext/route.js.map +7 -0
  55. package/dist/modules/search/api/settings/global-search/route.js +80 -0
  56. package/dist/modules/search/api/settings/global-search/route.js.map +7 -0
  57. package/dist/modules/search/api/settings/route.js +118 -0
  58. package/dist/modules/search/api/settings/route.js.map +7 -0
  59. package/dist/modules/search/api/settings/vector-store/route.js +77 -0
  60. package/dist/modules/search/api/settings/vector-store/route.js.map +7 -0
  61. package/dist/modules/search/backend/config/search/page.js +10 -0
  62. package/dist/modules/search/backend/config/search/page.js.map +7 -0
  63. package/dist/modules/search/backend/config/search/page.meta.js +24 -0
  64. package/dist/modules/search/backend/config/search/page.meta.js.map +7 -0
  65. package/dist/modules/search/cli.js +698 -0
  66. package/dist/modules/search/cli.js.map +7 -0
  67. package/dist/modules/search/di.js +32 -0
  68. package/dist/modules/search/di.js.map +7 -0
  69. package/dist/modules/search/frontend/components/GlobalSearchDialog.js +357 -0
  70. package/dist/modules/search/frontend/components/GlobalSearchDialog.js.map +7 -0
  71. package/dist/modules/search/frontend/components/HybridSearchTable.js +343 -0
  72. package/dist/modules/search/frontend/components/HybridSearchTable.js.map +7 -0
  73. package/dist/modules/search/frontend/components/SearchSettingsPageClient.js +303 -0
  74. package/dist/modules/search/frontend/components/SearchSettingsPageClient.js.map +7 -0
  75. package/dist/modules/search/frontend/components/sections/FulltextSearchSection.js +360 -0
  76. package/dist/modules/search/frontend/components/sections/FulltextSearchSection.js.map +7 -0
  77. package/dist/modules/search/frontend/components/sections/GlobalSearchSection.js +101 -0
  78. package/dist/modules/search/frontend/components/sections/GlobalSearchSection.js.map +7 -0
  79. package/dist/modules/search/frontend/components/sections/VectorSearchSection.js +608 -0
  80. package/dist/modules/search/frontend/components/sections/VectorSearchSection.js.map +7 -0
  81. package/dist/modules/search/frontend/index.js +9 -0
  82. package/dist/modules/search/frontend/index.js.map +7 -0
  83. package/dist/modules/search/frontend/utils.js +41 -0
  84. package/dist/modules/search/frontend/utils.js.map +7 -0
  85. package/dist/modules/search/i18n/de.json +61 -0
  86. package/dist/modules/search/i18n/en.json +72 -0
  87. package/dist/modules/search/i18n/es.json +61 -0
  88. package/dist/modules/search/i18n/pl.json +61 -0
  89. package/dist/modules/search/index.js +11 -0
  90. package/dist/modules/search/index.js.map +7 -0
  91. package/dist/modules/search/lib/auto-indexing.js +29 -0
  92. package/dist/modules/search/lib/auto-indexing.js.map +7 -0
  93. package/dist/modules/search/lib/embedding-config.js +131 -0
  94. package/dist/modules/search/lib/embedding-config.js.map +7 -0
  95. package/dist/modules/search/lib/global-search-config.js +45 -0
  96. package/dist/modules/search/lib/global-search-config.js.map +7 -0
  97. package/dist/modules/search/lib/reindex-lock.js +99 -0
  98. package/dist/modules/search/lib/reindex-lock.js.map +7 -0
  99. package/dist/modules/search/subscribers/fulltext_upsert.js +64 -0
  100. package/dist/modules/search/subscribers/fulltext_upsert.js.map +7 -0
  101. package/dist/modules/search/subscribers/vector_delete.js +58 -0
  102. package/dist/modules/search/subscribers/vector_delete.js.map +7 -0
  103. package/dist/modules/search/subscribers/vector_purge.js +142 -0
  104. package/dist/modules/search/subscribers/vector_purge.js.map +7 -0
  105. package/dist/modules/search/subscribers/vector_upsert.js +58 -0
  106. package/dist/modules/search/subscribers/vector_upsert.js.map +7 -0
  107. package/dist/modules/search/workers/fulltext-index.worker.js +240 -0
  108. package/dist/modules/search/workers/fulltext-index.worker.js.map +7 -0
  109. package/dist/modules/search/workers/vector-index.worker.js +234 -0
  110. package/dist/modules/search/workers/vector-index.worker.js.map +7 -0
  111. package/dist/queue/fulltext-indexing.js +15 -0
  112. package/dist/queue/fulltext-indexing.js.map +7 -0
  113. package/dist/queue/index.js +3 -0
  114. package/dist/queue/index.js.map +7 -0
  115. package/dist/queue/vector-indexing.js +15 -0
  116. package/dist/queue/vector-indexing.js.map +7 -0
  117. package/dist/service.js +286 -0
  118. package/dist/service.js.map +7 -0
  119. package/dist/strategies/fulltext.strategy.js +116 -0
  120. package/dist/strategies/fulltext.strategy.js.map +7 -0
  121. package/dist/strategies/index.js +12 -0
  122. package/dist/strategies/index.js.map +7 -0
  123. package/dist/strategies/token.strategy.js +80 -0
  124. package/dist/strategies/token.strategy.js.map +7 -0
  125. package/dist/strategies/vector.strategy.js +137 -0
  126. package/dist/strategies/vector.strategy.js.map +7 -0
  127. package/dist/types.js +1 -0
  128. package/dist/types.js.map +7 -0
  129. package/dist/vector/drivers/chromadb/index.js +44 -0
  130. package/dist/vector/drivers/chromadb/index.js.map +7 -0
  131. package/dist/vector/drivers/index.js +9 -0
  132. package/dist/vector/drivers/index.js.map +7 -0
  133. package/dist/vector/drivers/pgvector/index.js +509 -0
  134. package/dist/vector/drivers/pgvector/index.js.map +7 -0
  135. package/dist/vector/drivers/qdrant/index.js +44 -0
  136. package/dist/vector/drivers/qdrant/index.js.map +7 -0
  137. package/dist/vector/index.js +4 -0
  138. package/dist/vector/index.js.map +7 -0
  139. package/dist/vector/lib/vector-logs.js +33 -0
  140. package/dist/vector/lib/vector-logs.js.map +7 -0
  141. package/dist/vector/services/checksum.js +20 -0
  142. package/dist/vector/services/checksum.js.map +7 -0
  143. package/dist/vector/services/embedding.js +222 -0
  144. package/dist/vector/services/embedding.js.map +7 -0
  145. package/dist/vector/services/index.js +4 -0
  146. package/dist/vector/services/index.js.map +7 -0
  147. package/dist/vector/services/vector-index.service.js +960 -0
  148. package/dist/vector/services/vector-index.service.js.map +7 -0
  149. package/dist/vector/types/pg.d.js +1 -0
  150. package/dist/vector/types/pg.d.js.map +7 -0
  151. package/dist/vector/types.js +75 -0
  152. package/dist/vector/types.js.map +7 -0
  153. package/jest.config.cjs +19 -0
  154. package/package.json +142 -0
  155. package/src/__tests__/queue.test.ts +148 -0
  156. package/src/__tests__/service.test.ts +345 -0
  157. package/src/__tests__/workers.test.ts +319 -0
  158. package/src/di.ts +291 -0
  159. package/src/fulltext/drivers/index.ts +41 -0
  160. package/src/fulltext/drivers/meilisearch/index.ts +410 -0
  161. package/src/fulltext/index.ts +13 -0
  162. package/src/fulltext/types.ts +115 -0
  163. package/src/index.ts +36 -0
  164. package/src/indexer/index.ts +13 -0
  165. package/src/indexer/search-indexer.ts +1141 -0
  166. package/src/indexer/subscribers/delete.ts +49 -0
  167. package/src/lib/debug.ts +46 -0
  168. package/src/lib/fallback-presenter.ts +106 -0
  169. package/src/lib/field-policy.ts +169 -0
  170. package/src/lib/index.ts +13 -0
  171. package/src/lib/merger.ts +159 -0
  172. package/src/lib/presenter-enricher.ts +323 -0
  173. package/src/modules/search/README.md +694 -0
  174. package/src/modules/search/acl.ts +10 -0
  175. package/src/modules/search/ai-tools.ts +467 -0
  176. package/src/modules/search/api/embeddings/reindex/cancel/route.ts +77 -0
  177. package/src/modules/search/api/embeddings/reindex/route.ts +197 -0
  178. package/src/modules/search/api/embeddings/route.ts +304 -0
  179. package/src/modules/search/api/index/route.ts +297 -0
  180. package/src/modules/search/api/reindex/cancel/route.ts +77 -0
  181. package/src/modules/search/api/reindex/route.ts +419 -0
  182. package/src/modules/search/api/search/global/route.ts +120 -0
  183. package/src/modules/search/api/search/route.ts +121 -0
  184. package/src/modules/search/api/settings/fulltext/route.ts +82 -0
  185. package/src/modules/search/api/settings/global-search/route.ts +91 -0
  186. package/src/modules/search/api/settings/route.ts +187 -0
  187. package/src/modules/search/api/settings/vector-store/route.ts +105 -0
  188. package/src/modules/search/backend/config/search/page.meta.ts +22 -0
  189. package/src/modules/search/backend/config/search/page.tsx +12 -0
  190. package/src/modules/search/cli.ts +818 -0
  191. package/src/modules/search/di.ts +50 -0
  192. package/src/modules/search/frontend/components/GlobalSearchDialog.tsx +436 -0
  193. package/src/modules/search/frontend/components/HybridSearchTable.tsx +418 -0
  194. package/src/modules/search/frontend/components/SearchSettingsPageClient.tsx +476 -0
  195. package/src/modules/search/frontend/components/sections/FulltextSearchSection.tsx +624 -0
  196. package/src/modules/search/frontend/components/sections/GlobalSearchSection.tsx +124 -0
  197. package/src/modules/search/frontend/components/sections/VectorSearchSection.tsx +943 -0
  198. package/src/modules/search/frontend/index.ts +3 -0
  199. package/src/modules/search/frontend/utils.ts +82 -0
  200. package/src/modules/search/i18n/de.json +61 -0
  201. package/src/modules/search/i18n/en.json +72 -0
  202. package/src/modules/search/i18n/es.json +61 -0
  203. package/src/modules/search/i18n/pl.json +61 -0
  204. package/src/modules/search/index.ts +9 -0
  205. package/src/modules/search/lib/auto-indexing.ts +35 -0
  206. package/src/modules/search/lib/embedding-config.ts +161 -0
  207. package/src/modules/search/lib/global-search-config.ts +69 -0
  208. package/src/modules/search/lib/reindex-lock.ts +201 -0
  209. package/src/modules/search/subscribers/fulltext_upsert.ts +83 -0
  210. package/src/modules/search/subscribers/vector_delete.ts +75 -0
  211. package/src/modules/search/subscribers/vector_purge.ts +161 -0
  212. package/src/modules/search/subscribers/vector_upsert.ts +75 -0
  213. package/src/modules/search/workers/fulltext-index.worker.ts +318 -0
  214. package/src/modules/search/workers/vector-index.worker.ts +292 -0
  215. package/src/queue/fulltext-indexing.ts +87 -0
  216. package/src/queue/index.ts +2 -0
  217. package/src/queue/vector-indexing.ts +66 -0
  218. package/src/service.ts +397 -0
  219. package/src/strategies/fulltext.strategy.ts +155 -0
  220. package/src/strategies/index.ts +17 -0
  221. package/src/strategies/token.strategy.ts +153 -0
  222. package/src/strategies/vector.strategy.ts +234 -0
  223. package/src/types.ts +38 -0
  224. package/src/vector/drivers/chromadb/index.ts +49 -0
  225. package/src/vector/drivers/index.ts +4 -0
  226. package/src/vector/drivers/pgvector/index.ts +627 -0
  227. package/src/vector/drivers/qdrant/index.ts +49 -0
  228. package/src/vector/index.ts +3 -0
  229. package/src/vector/lib/vector-logs.ts +46 -0
  230. package/src/vector/services/checksum.ts +18 -0
  231. package/src/vector/services/embedding.ts +275 -0
  232. package/src/vector/services/index.ts +3 -0
  233. package/src/vector/services/vector-index.service.ts +1234 -0
  234. package/src/vector/types/pg.d.ts +1 -0
  235. package/src/vector/types.ts +220 -0
  236. package/tsconfig.json +9 -0
  237. package/watch.mjs +6 -0
@@ -0,0 +1,818 @@
1
+ import type { ModuleCli } from '@open-mercato/shared/modules/registry'
2
+ import { createRequestContainer } from '@open-mercato/shared/lib/di/container'
3
+ import { recordIndexerError } from '@open-mercato/shared/lib/indexers/error-log'
4
+ import { recordIndexerLog } from '@open-mercato/shared/lib/indexers/status-log'
5
+ import { createProgressBar } from '@open-mercato/shared/lib/cli/progress'
6
+ import type { EntityManager } from '@mikro-orm/postgresql'
7
+ import { reindexEntity, DEFAULT_REINDEX_PARTITIONS } from '@open-mercato/core/modules/query_index/lib/reindexer'
8
+ import { writeCoverageCounts } from '@open-mercato/core/modules/query_index/lib/coverage'
9
+ import type { SearchService } from '../../service'
10
+ import type { SearchIndexer } from '../../indexer/search-indexer'
11
+ import { VECTOR_INDEXING_QUEUE_NAME, type VectorIndexJobPayload } from '../../queue/vector-indexing'
12
+ import { FULLTEXT_INDEXING_QUEUE_NAME, type FulltextIndexJobPayload } from '../../queue/fulltext-indexing'
13
+ import type { QueuedJob, JobContext } from '@open-mercato/queue'
14
+ import type { EntityId } from '@open-mercato/shared/modules/entities'
15
+ import { parseBooleanToken } from '@open-mercato/shared/lib/boolean'
16
+
17
+ type CliProgressBar = {
18
+ update(completed: number): void
19
+ complete(): void
20
+ }
21
+
22
+ type ParsedArgs = Record<string, string | boolean>
23
+
24
+ function parseArgs(rest: string[]): ParsedArgs {
25
+ const args: ParsedArgs = {}
26
+ for (let i = 0; i < rest.length; i += 1) {
27
+ const part = rest[i]
28
+ if (!part?.startsWith('--')) continue
29
+ const [rawKey, rawValue] = part.slice(2).split('=')
30
+ if (!rawKey) continue
31
+ if (rawValue !== undefined) {
32
+ args[rawKey] = rawValue
33
+ } else if (i + 1 < rest.length && !rest[i + 1]!.startsWith('--')) {
34
+ args[rawKey] = rest[i + 1]!
35
+ i += 1
36
+ } else {
37
+ args[rawKey] = true
38
+ }
39
+ }
40
+ return args
41
+ }
42
+
43
+ function stringOpt(args: ParsedArgs, ...keys: string[]): string | undefined {
44
+ for (const key of keys) {
45
+ const raw = args[key]
46
+ if (typeof raw !== 'string') continue
47
+ const trimmed = raw.trim()
48
+ if (trimmed.length > 0) return trimmed
49
+ }
50
+ return undefined
51
+ }
52
+
53
+ function numberOpt(args: ParsedArgs, ...keys: string[]): number | undefined {
54
+ for (const key of keys) {
55
+ const raw = args[key]
56
+ if (typeof raw === 'number') return raw
57
+ if (typeof raw === 'string') {
58
+ const parsed = Number(raw)
59
+ if (Number.isFinite(parsed)) return parsed
60
+ }
61
+ }
62
+ return undefined
63
+ }
64
+
65
+ function flagOpt(args: ParsedArgs, ...keys: string[]): boolean | undefined {
66
+ for (const key of keys) {
67
+ const raw = args[key]
68
+ if (raw === undefined) continue
69
+ if (raw === true) return true
70
+ if (raw === false) return false
71
+ if (typeof raw === 'string') {
72
+ const trimmed = raw.trim()
73
+ if (!trimmed) return true
74
+ const parsed = parseBooleanToken(trimmed)
75
+ return parsed === null ? true : parsed
76
+ }
77
+ }
78
+ return undefined
79
+ }
80
+
81
+ function toPositiveInt(value: number | undefined): number | undefined {
82
+ if (value === undefined) return undefined
83
+ const n = Math.floor(value)
84
+ if (!Number.isFinite(n) || n <= 0) return undefined
85
+ return n
86
+ }
87
+
88
+ function toNonNegativeInt(value: number | undefined, fallback = 0): number {
89
+ if (value === undefined) return fallback
90
+ const n = Math.floor(value)
91
+ if (!Number.isFinite(n) || n < 0) return fallback
92
+ return n
93
+ }
94
+
95
+ /**
96
+ * Test search functionality with a query
97
+ */
98
+ async function searchCommand(rest: string[]): Promise<void> {
99
+ const args = parseArgs(rest)
100
+ const query = stringOpt(args, 'query', 'q')
101
+ const tenantId = stringOpt(args, 'tenant', 'tenantId')
102
+ const organizationId = stringOpt(args, 'org', 'organizationId')
103
+ const entityTypes = stringOpt(args, 'entity', 'entities')
104
+ const strategies = stringOpt(args, 'strategy', 'strategies')
105
+ const limit = numberOpt(args, 'limit') ?? 20
106
+
107
+ if (!query) {
108
+ console.error('Usage: yarn mercato search query --query "search terms" --tenant <id> [options]')
109
+ console.error(' --query, -q Search query (required)')
110
+ console.error(' --tenant Tenant ID (required)')
111
+ console.error(' --org Organization ID (optional)')
112
+ console.error(' --entity Entity types to search (comma-separated)')
113
+ console.error(' --strategy Strategies to use (comma-separated: meilisearch,vector,tokens)')
114
+ console.error(' --limit Max results (default: 20)')
115
+ return
116
+ }
117
+
118
+ if (!tenantId) {
119
+ console.error('Error: --tenant is required')
120
+ return
121
+ }
122
+
123
+ const container = await createRequestContainer()
124
+
125
+ try {
126
+ const searchService = container.resolve('searchService') as SearchService | undefined
127
+
128
+ if (!searchService) {
129
+ console.error('Error: SearchService not available. Make sure the search module is registered.')
130
+ return
131
+ }
132
+
133
+ console.log(`\nSearching for: "${query}"`)
134
+ console.log(`Tenant: ${tenantId}`)
135
+ if (organizationId) console.log(`Organization: ${organizationId}`)
136
+ console.log('---')
137
+
138
+ const results = await searchService.search(query, {
139
+ tenantId,
140
+ organizationId,
141
+ entityTypes: entityTypes?.split(',').map(s => s.trim()),
142
+ strategies: strategies?.split(',').map(s => s.trim()) as any,
143
+ limit,
144
+ })
145
+
146
+ if (results.length === 0) {
147
+ console.log('No results found.')
148
+ return
149
+ }
150
+
151
+ console.log(`\nFound ${results.length} result(s):\n`)
152
+
153
+ for (let i = 0; i < results.length; i++) {
154
+ const result = results[i]
155
+ console.log(`${i + 1}. [${result.source}] ${result.entityId}`)
156
+ console.log(` Record ID: ${result.recordId}`)
157
+ console.log(` Score: ${result.score.toFixed(4)}`)
158
+ if (result.presenter) {
159
+ console.log(` Title: ${result.presenter.title}`)
160
+ if (result.presenter.subtitle) console.log(` Subtitle: ${result.presenter.subtitle}`)
161
+ }
162
+ if (result.url) console.log(` URL: ${result.url}`)
163
+ console.log('')
164
+ }
165
+ } finally {
166
+ try {
167
+ const em = container.resolve('em') as any
168
+ await em?.getConnection?.()?.close?.()
169
+ } catch {}
170
+ }
171
+ }
172
+
173
+ /**
174
+ * Show status of search strategies
175
+ */
176
+ async function statusCommand(): Promise<void> {
177
+ const container = await createRequestContainer()
178
+
179
+ try {
180
+ const searchService = container.resolve('searchService') as SearchService | undefined
181
+ const strategies = container.resolve('searchStrategies') as any[] | undefined
182
+
183
+ console.log('\n=== Search Module Status ===\n')
184
+
185
+ if (!searchService) {
186
+ console.log('SearchService: NOT REGISTERED')
187
+ return
188
+ }
189
+
190
+ console.log('SearchService: ACTIVE')
191
+ console.log('')
192
+
193
+ if (!strategies || strategies.length === 0) {
194
+ console.log('Strategies: NONE CONFIGURED')
195
+ return
196
+ }
197
+
198
+ console.log('Strategies:')
199
+ console.log('-----------')
200
+
201
+ for (const strategy of strategies) {
202
+ const available = await strategy.isAvailable?.() ?? true
203
+ const status = available ? 'AVAILABLE' : 'UNAVAILABLE'
204
+ const icon = available ? '✓' : '✗'
205
+ console.log(` ${icon} ${strategy.name ?? strategy.id} (${strategy.id})`)
206
+ console.log(` Status: ${status}`)
207
+ console.log(` Priority: ${strategy.priority ?? 'N/A'}`)
208
+ console.log('')
209
+ }
210
+
211
+ // Check environment variables
212
+ console.log('Environment:')
213
+ console.log('------------')
214
+ console.log(` MEILISEARCH_HOST: ${process.env.MEILISEARCH_HOST ?? '(not set)'}`)
215
+ console.log(` MEILISEARCH_API_KEY: ${process.env.MEILISEARCH_API_KEY ? '(set)' : '(not set)'}`)
216
+ console.log(` OPENAI_API_KEY: ${process.env.OPENAI_API_KEY ? '(set)' : '(not set)'}`)
217
+ console.log(` OM_SEARCH_ENABLED: ${process.env.OM_SEARCH_ENABLED ?? 'true (default)'}`)
218
+ console.log('')
219
+ } finally {
220
+ try {
221
+ const em = container.resolve('em') as any
222
+ await em?.getConnection?.()?.close?.()
223
+ } catch {}
224
+ }
225
+ }
226
+
227
+ /**
228
+ * Index a specific record for testing
229
+ */
230
+ async function indexCommand(rest: string[]): Promise<void> {
231
+ const args = parseArgs(rest)
232
+ const entityId = stringOpt(args, 'entity', 'entityId')
233
+ const recordId = stringOpt(args, 'record', 'recordId')
234
+ const tenantId = stringOpt(args, 'tenant', 'tenantId')
235
+ const organizationId = stringOpt(args, 'org', 'organizationId')
236
+
237
+ if (!entityId || !recordId || !tenantId) {
238
+ console.error('Usage: yarn mercato search index --entity <entityId> --record <recordId> --tenant <tenantId>')
239
+ console.error(' --entity Entity ID (e.g., customers:customer_person_profile)')
240
+ console.error(' --record Record ID')
241
+ console.error(' --tenant Tenant ID')
242
+ console.error(' --org Organization ID (optional)')
243
+ return
244
+ }
245
+
246
+ const container = await createRequestContainer()
247
+
248
+ try {
249
+ const searchIndexer = container.resolve('searchIndexer') as SearchIndexer | undefined
250
+
251
+ if (!searchIndexer) {
252
+ console.error('Error: SearchIndexer not available.')
253
+ return
254
+ }
255
+
256
+ // Load record from query engine
257
+ const queryEngine = container.resolve('queryEngine') as any
258
+
259
+ console.log(`\nLoading record: ${entityId} / ${recordId}`)
260
+
261
+ const result = await queryEngine.query(entityId, {
262
+ tenantId,
263
+ organizationId,
264
+ filters: { id: recordId },
265
+ includeCustomFields: true,
266
+ page: { page: 1, pageSize: 1 },
267
+ })
268
+
269
+ const record = result.items[0]
270
+
271
+ if (!record) {
272
+ console.error('Error: Record not found')
273
+ return
274
+ }
275
+
276
+ console.log('Record loaded, indexing...')
277
+
278
+ // Extract custom fields
279
+ const customFields: Record<string, unknown> = {}
280
+ for (const [key, value] of Object.entries(record)) {
281
+ if (key.startsWith('cf:') || key.startsWith('cf_')) {
282
+ const cfKey = key.slice(3) // Remove 'cf:' or 'cf_' prefix (both are 3 chars)
283
+ customFields[cfKey] = value
284
+ }
285
+ }
286
+
287
+ await searchIndexer.indexRecord({
288
+ entityId,
289
+ recordId,
290
+ tenantId,
291
+ organizationId,
292
+ record,
293
+ customFields,
294
+ })
295
+
296
+ console.log('Record indexed successfully!')
297
+ } finally {
298
+ try {
299
+ const em = container.resolve('em') as any
300
+ await em?.getConnection?.()?.close?.()
301
+ } catch {}
302
+ }
303
+ }
304
+
305
+ /**
306
+ * Test Meilisearch connection directly
307
+ */
308
+ async function testMeilisearchCommand(): Promise<void> {
309
+ const host = process.env.MEILISEARCH_HOST
310
+ const apiKey = process.env.MEILISEARCH_API_KEY
311
+
312
+ console.log('\n=== Meilisearch Connection Test ===\n')
313
+
314
+ if (!host) {
315
+ console.log('MEILISEARCH_HOST: NOT SET')
316
+ console.log('\nMeilisearch is not configured. Set MEILISEARCH_HOST in your .env file.')
317
+ return
318
+ }
319
+
320
+ console.log(`Host: ${host}`)
321
+ console.log(`API Key: ${apiKey ? '(configured)' : '(not set)'}`)
322
+ console.log('')
323
+
324
+ try {
325
+ const { MeiliSearch } = await import('meilisearch')
326
+ const client = new MeiliSearch({ host, apiKey })
327
+
328
+ console.log('Testing connection...')
329
+ const health = await client.health()
330
+ console.log(`Health: ${health.status}`)
331
+
332
+ console.log('\nListing indexes...')
333
+ const indexes = await client.getIndexes()
334
+
335
+ if (indexes.results.length === 0) {
336
+ console.log('No indexes found.')
337
+ } else {
338
+ console.log(`Found ${indexes.results.length} index(es):`)
339
+ for (const index of indexes.results) {
340
+ const stats = await client.index(index.uid).getStats()
341
+ console.log(` - ${index.uid}: ${stats.numberOfDocuments} documents`)
342
+ }
343
+ }
344
+
345
+ console.log('\nMeilisearch connection successful!')
346
+ } catch (error) {
347
+ console.error('Connection failed:', error instanceof Error ? error.message : error)
348
+ }
349
+ }
350
+
351
+ const searchCli: ModuleCli = {
352
+ command: 'query',
353
+ async run(rest) {
354
+ await searchCommand(rest)
355
+ },
356
+ }
357
+
358
+ const statusCli: ModuleCli = {
359
+ command: 'status',
360
+ async run() {
361
+ await statusCommand()
362
+ },
363
+ }
364
+
365
+ const indexCli: ModuleCli = {
366
+ command: 'index',
367
+ async run(rest) {
368
+ await indexCommand(rest)
369
+ },
370
+ }
371
+
372
+ const testMeilisearchCli: ModuleCli = {
373
+ command: 'test-meilisearch',
374
+ async run() {
375
+ await testMeilisearchCommand()
376
+ },
377
+ }
378
+
379
+ async function resetVectorCoverageAfterPurge(
380
+ em: EntityManager | null,
381
+ entityId: string,
382
+ tenantId: string | null,
383
+ organizationId: string | null,
384
+ ): Promise<void> {
385
+ if (!em || !entityId) return
386
+ try {
387
+ const scopes = new Set<string>()
388
+ scopes.add('__null__')
389
+ if (organizationId) scopes.add(organizationId)
390
+ for (const scope of scopes) {
391
+ const orgValue = scope === '__null__' ? null : scope
392
+ await writeCoverageCounts(
393
+ em,
394
+ {
395
+ entityType: entityId,
396
+ tenantId,
397
+ organizationId: orgValue,
398
+ withDeleted: false,
399
+ },
400
+ { vectorCount: 0 },
401
+ )
402
+ }
403
+ } catch (error) {
404
+ console.warn('[search.cli] Failed to reset vector coverage after purge', error instanceof Error ? error.message : error)
405
+ }
406
+ }
407
+
408
+ async function reindexCommand(rest: string[]): Promise<void> {
409
+ const args = parseArgs(rest)
410
+ const tenantId = stringOpt(args, 'tenant', 'tenantId')
411
+ const organizationId = stringOpt(args, 'org', 'orgId', 'organizationId')
412
+ const entityId = stringOpt(args, 'entity', 'entityId')
413
+ const force = flagOpt(args, 'force', 'full') === true
414
+ const batchSize = toPositiveInt(numberOpt(args, 'batch', 'chunk', 'size'))
415
+ const partitionsOption = toPositiveInt(numberOpt(args, 'partitions', 'partitionCount', 'parallel'))
416
+ const partitionIndexRaw = numberOpt(args, 'partition', 'partitionIndex')
417
+ const partitionIndexOption = partitionIndexRaw === undefined ? undefined : toNonNegativeInt(partitionIndexRaw, 0)
418
+ const resetCoverageFlag = flagOpt(args, 'resetCoverage') === true
419
+ const skipResetCoverageFlag = flagOpt(args, 'skipResetCoverage', 'noResetCoverage') === true
420
+ const skipPurgeFlag = flagOpt(args, 'skipPurge', 'noPurge') === true
421
+ const purgeFlag = flagOpt(args, 'purge', 'purgeFirst')
422
+
423
+ const container = await createRequestContainer()
424
+ let baseEm: EntityManager | null = null
425
+ try {
426
+ baseEm = (container.resolve('em') as EntityManager)
427
+ } catch {
428
+ baseEm = null
429
+ }
430
+
431
+ const disposeContainer = async () => {
432
+ if (typeof (container as any)?.dispose === 'function') {
433
+ await (container as any).dispose()
434
+ }
435
+ }
436
+
437
+ const recordError = async (error: Error) => {
438
+ await recordIndexerLog(
439
+ { em: baseEm ?? undefined },
440
+ {
441
+ source: 'vector',
442
+ handler: 'cli:search.reindex',
443
+ level: 'warn',
444
+ message: `Reindex failed${entityId ? ` for ${entityId}` : ''}`,
445
+ entityType: entityId ?? null,
446
+ tenantId: tenantId ?? null,
447
+ organizationId: organizationId ?? null,
448
+ details: { error: error.message },
449
+ },
450
+ ).catch(() => undefined)
451
+ await recordIndexerError(
452
+ { em: baseEm ?? undefined },
453
+ {
454
+ source: 'vector',
455
+ handler: 'cli:search.reindex',
456
+ error,
457
+ entityType: entityId ?? null,
458
+ tenantId: tenantId ?? null,
459
+ organizationId: organizationId ?? null,
460
+ payload: {
461
+ args,
462
+ force,
463
+ batchSize,
464
+ partitionsOption,
465
+ partitionIndexOption,
466
+ resetCoverageFlag,
467
+ skipResetCoverageFlag,
468
+ skipPurgeFlag,
469
+ purgeFlag,
470
+ },
471
+ },
472
+ )
473
+ }
474
+
475
+ try {
476
+ const searchIndexer = container.resolve<SearchIndexer>('searchIndexer')
477
+ const enabledEntities = new Set(searchIndexer.listEnabledEntities())
478
+ const baseEventBus = (() => {
479
+ try {
480
+ return container.resolve('eventBus') as {
481
+ emitEvent(event: string, payload: any, options?: any): Promise<void>
482
+ }
483
+ } catch {
484
+ return null
485
+ }
486
+ })()
487
+ if (!baseEventBus) {
488
+ console.warn('[search.cli] eventBus unavailable; vector embeddings may not be refreshed. Run bootstrap or ensure event bus configuration.')
489
+ }
490
+
491
+ const partitionCount = Math.max(1, partitionsOption ?? DEFAULT_REINDEX_PARTITIONS)
492
+ if (partitionIndexOption !== undefined && partitionIndexOption >= partitionCount) {
493
+ console.error(`partitionIndex (${partitionIndexOption}) must be < partitionCount (${partitionCount})`)
494
+ return
495
+ }
496
+ const partitionTargets =
497
+ partitionIndexOption !== undefined
498
+ ? [partitionIndexOption]
499
+ : Array.from({ length: partitionCount }, (_, idx) => idx)
500
+
501
+ const shouldResetCoverage = (partition: number): boolean => {
502
+ if (resetCoverageFlag) return true
503
+ if (skipResetCoverageFlag) return false
504
+ if (partitionIndexOption !== undefined) return partitionIndexOption === 0
505
+ return partition === partitionTargets[0]
506
+ }
507
+
508
+ const runReindex = async (entityType: string, purgeFirst: boolean) => {
509
+ const scopeLabel = tenantId
510
+ ? `tenant=${tenantId}${organizationId ? `, org=${organizationId}` : ''}`
511
+ : 'all tenants'
512
+ console.log(`Reindexing vectors for ${entityType} (${scopeLabel})${purgeFirst ? ' [purge]' : ''}`)
513
+ await recordIndexerLog(
514
+ { em: baseEm ?? undefined },
515
+ {
516
+ source: 'vector',
517
+ handler: 'cli:search.reindex',
518
+ message: `Reindex started for ${entityType}`,
519
+ entityType,
520
+ tenantId: tenantId ?? null,
521
+ organizationId: organizationId ?? null,
522
+ details: {
523
+ purgeFirst,
524
+ partitions: partitionTargets.length,
525
+ partitionCount,
526
+ partitionIndex: partitionIndexOption ?? null,
527
+ batchSize,
528
+ },
529
+ },
530
+ ).catch(() => undefined)
531
+
532
+ if (purgeFirst && tenantId) {
533
+ try {
534
+ console.log(' -> purging existing vector index rows...')
535
+ await searchIndexer.purgeEntity({ entityId: entityType as EntityId, tenantId })
536
+ await resetVectorCoverageAfterPurge(baseEm, entityType, tenantId ?? null, organizationId ?? null)
537
+ if (baseEventBus) {
538
+ const scopes = new Set<string>()
539
+ scopes.add('__null__')
540
+ if (organizationId) scopes.add(organizationId)
541
+ await Promise.all(
542
+ Array.from(scopes).map((scope) => {
543
+ const orgValue = scope === '__null__' ? null : scope
544
+ return baseEventBus!
545
+ .emitEvent(
546
+ 'query_index.coverage.refresh',
547
+ {
548
+ entityType,
549
+ tenantId: tenantId ?? null,
550
+ organizationId: orgValue,
551
+ delayMs: 0,
552
+ },
553
+ )
554
+ .catch(() => undefined)
555
+ }),
556
+ )
557
+ }
558
+ } catch (err) {
559
+ console.warn(' -> purge failed, continuing with reindex', err instanceof Error ? err.message : err)
560
+ }
561
+ } else if (purgeFirst && !tenantId) {
562
+ console.warn(' -> skipping purge: tenant scope not provided')
563
+ }
564
+
565
+ const progressState = new Map<number, { last: number }>()
566
+ const renderProgress = (part: number, info: { processed: number; total: number }) => {
567
+ const state = progressState.get(part) ?? { last: 0 }
568
+ const now = Date.now()
569
+ if (now - state.last < 1000 && info.processed < info.total) return
570
+ state.last = now
571
+ progressState.set(part, state)
572
+ const percent = info.total > 0 ? ((info.processed / info.total) * 100).toFixed(2) : '0.00'
573
+ console.log(
574
+ ` [${entityType}] partition ${part + 1}/${partitionCount}: ${info.processed.toLocaleString()} / ${info.total.toLocaleString()} (${percent}%)`,
575
+ )
576
+ }
577
+
578
+ const processed = await Promise.all(
579
+ partitionTargets.map(async (part, idx) => {
580
+ const label = partitionTargets.length > 1 ? ` [partition ${part + 1}/${partitionCount}]` : ''
581
+ if (partitionTargets.length === 1) {
582
+ console.log(` -> processing${label}`)
583
+ } else if (idx === 0) {
584
+ console.log(` -> processing partitions in parallel (count=${partitionTargets.length})`)
585
+ }
586
+
587
+ const partitionContainer = await createRequestContainer()
588
+ const partitionEm = partitionContainer.resolve<EntityManager>('em')
589
+ try {
590
+ let progressBar: CliProgressBar | null = null
591
+ const useBar = partitionTargets.length === 1
592
+ const stats = await reindexEntity(partitionEm, {
593
+ entityType,
594
+ tenantId: tenantId ?? undefined,
595
+ organizationId: organizationId ?? undefined,
596
+ force,
597
+ batchSize,
598
+ eventBus: baseEventBus ?? undefined,
599
+ emitVectorizeEvents: true,
600
+ partitionCount,
601
+ partitionIndex: part,
602
+ resetCoverage: shouldResetCoverage(part),
603
+ onProgress(info) {
604
+ if (useBar) {
605
+ if (info.total > 0 && !progressBar) {
606
+ progressBar = createProgressBar(`Reindexing ${entityType}${label}`, info.total)
607
+ }
608
+ progressBar?.update(info.processed)
609
+ } else {
610
+ renderProgress(part, info)
611
+ }
612
+ },
613
+ })
614
+ if (progressBar) (progressBar as CliProgressBar).complete()
615
+ if (!useBar) {
616
+ renderProgress(part, { processed: stats.processed, total: stats.total })
617
+ } else {
618
+ console.log(
619
+ ` processed ${stats.processed} row(s)${stats.total ? ` (base ${stats.total})` : ''}`,
620
+ )
621
+ }
622
+ return stats.processed
623
+ } finally {
624
+ if (typeof (partitionContainer as any)?.dispose === 'function') {
625
+ await (partitionContainer as any).dispose()
626
+ }
627
+ }
628
+ }),
629
+ )
630
+
631
+ const totalProcessed = processed.reduce((acc, value) => acc + value, 0)
632
+ console.log(`Finished ${entityType}: processed ${totalProcessed} row(s) across ${partitionTargets.length} partition(s)`)
633
+ await recordIndexerLog(
634
+ { em: baseEm ?? undefined },
635
+ {
636
+ source: 'vector',
637
+ handler: 'cli:search.reindex',
638
+ message: `Reindex completed for ${entityType}`,
639
+ entityType,
640
+ tenantId: tenantId ?? null,
641
+ organizationId: organizationId ?? null,
642
+ details: {
643
+ processed: totalProcessed,
644
+ partitions: partitionTargets.length,
645
+ partitionCount,
646
+ partitionIndex: partitionIndexOption ?? null,
647
+ batchSize,
648
+ },
649
+ },
650
+ ).catch(() => undefined)
651
+ return totalProcessed
652
+ }
653
+
654
+ const defaultPurge = purgeFlag === true && !skipPurgeFlag
655
+
656
+ if (entityId) {
657
+ if (!enabledEntities.has(entityId)) {
658
+ console.error(`Entity ${entityId} is not enabled for vector search.`)
659
+ return
660
+ }
661
+ const purgeFirst = defaultPurge
662
+ await runReindex(entityId, purgeFirst)
663
+ console.log('Vector reindex completed.')
664
+ return
665
+ }
666
+
667
+ const entityIds = searchIndexer.listEnabledEntities()
668
+ if (!entityIds.length) {
669
+ console.log('No entities enabled for vector search.')
670
+ return
671
+ }
672
+ console.log(`Reindexing ${entityIds.length} vector-enabled entities...`)
673
+ let processedOverall = 0
674
+ for (let idx = 0; idx < entityIds.length; idx += 1) {
675
+ const id = entityIds[idx]!
676
+ console.log(`[${idx + 1}/${entityIds.length}] Preparing ${id}...`)
677
+ processedOverall += await runReindex(id, defaultPurge)
678
+ }
679
+ console.log(`Vector reindex completed. Total processed rows: ${processedOverall.toLocaleString()}`)
680
+ } catch (error) {
681
+ const err = error instanceof Error ? error : new Error(String(error))
682
+ console.error('[search.cli] Reindex failed:', err.stack ?? err.message)
683
+ await recordError(err)
684
+ throw err
685
+ } finally {
686
+ await disposeContainer()
687
+ }
688
+ }
689
+
690
+ const reindexCli: ModuleCli = {
691
+ command: 'reindex',
692
+ async run(rest) {
693
+ await reindexCommand(rest)
694
+ },
695
+ }
696
+
697
+ const reindexHelpCli: ModuleCli = {
698
+ command: 'reindex-help',
699
+ async run() {
700
+ console.log('Usage: yarn mercato search reindex [options]')
701
+ console.log(' --tenant <id> Optional tenant scope (required for purge & coverage).')
702
+ console.log(' --org <id> Optional organization scope (requires tenant).')
703
+ console.log(' --entity <module:entity> Reindex a single entity (defaults to all enabled entities).')
704
+ console.log(' --partitions <n> Number of partitions to process in parallel (default from query index).')
705
+ console.log(' --partition <idx> Restrict to a specific partition index.')
706
+ console.log(' --batch <n> Override batch size per chunk.')
707
+ console.log(' --force Force reindex even if another job is running.')
708
+ console.log(' --purgeFirst Purge vector rows before reindexing (defaults to skip).')
709
+ console.log(' --skipPurge Explicitly skip purging vector rows.')
710
+ console.log(' --skipResetCoverage Keep existing coverage snapshots.')
711
+ },
712
+ }
713
+
714
+ /**
715
+ * Start a queue worker for processing search indexing jobs.
716
+ */
717
+ async function workerCommand(rest: string[]): Promise<void> {
718
+ const queueName = rest[0]
719
+ const args = parseArgs(rest)
720
+ const concurrency = toPositiveInt(numberOpt(args, 'concurrency')) ?? 1
721
+
722
+ const validQueues = [VECTOR_INDEXING_QUEUE_NAME, FULLTEXT_INDEXING_QUEUE_NAME]
723
+
724
+ if (!queueName || !validQueues.includes(queueName)) {
725
+ console.error('\nUsage: yarn mercato search worker <queue-name> [options]\n')
726
+ console.error('Available queues:')
727
+ console.error(` ${VECTOR_INDEXING_QUEUE_NAME} Process vector embedding indexing jobs`)
728
+ console.error(` ${FULLTEXT_INDEXING_QUEUE_NAME} Process fulltext indexing jobs`)
729
+ console.error('\nOptions:')
730
+ console.error(' --concurrency <n> Number of concurrent jobs to process (default: 1)')
731
+ console.error('\nExamples:')
732
+ console.error(` yarn mercato search worker ${VECTOR_INDEXING_QUEUE_NAME} --concurrency=10`)
733
+ console.error(` yarn mercato search worker ${FULLTEXT_INDEXING_QUEUE_NAME} --concurrency=5`)
734
+ return
735
+ }
736
+
737
+ // Check if Redis is configured for async queue
738
+ const queueStrategy = process.env.QUEUE_STRATEGY || 'local'
739
+ if (queueStrategy !== 'async') {
740
+ console.error('\nError: Queue workers require QUEUE_STRATEGY=async')
741
+ console.error('Set QUEUE_STRATEGY=async and configure REDIS_URL in your environment.\n')
742
+ return
743
+ }
744
+
745
+ const redisUrl = process.env.REDIS_URL || process.env.QUEUE_REDIS_URL
746
+ if (!redisUrl) {
747
+ console.error('\nError: Redis connection not configured')
748
+ console.error('Set REDIS_URL or QUEUE_REDIS_URL in your environment.\n')
749
+ return
750
+ }
751
+
752
+ // Dynamically import runWorker to avoid loading BullMQ unless needed
753
+ const { runWorker } = await import('@open-mercato/queue/worker')
754
+
755
+ console.log(`\nStarting ${queueName} worker...`)
756
+ console.log(` Concurrency: ${concurrency}`)
757
+ console.log(` Redis: ${redisUrl.replace(/\/\/[^:]+:[^@]+@/, '//<credentials>@')}`)
758
+ console.log('')
759
+
760
+ if (queueName === VECTOR_INDEXING_QUEUE_NAME) {
761
+ const { handleVectorIndexJob } = await import('./workers/vector-index.worker')
762
+ const container = await createRequestContainer()
763
+
764
+ await runWorker<VectorIndexJobPayload>({
765
+ queueName: VECTOR_INDEXING_QUEUE_NAME,
766
+ handler: async (job: QueuedJob<VectorIndexJobPayload>, ctx: JobContext) => {
767
+ await handleVectorIndexJob(job, ctx, { resolve: container.resolve.bind(container) })
768
+ },
769
+ connection: { url: redisUrl },
770
+ concurrency,
771
+ })
772
+ } else if (queueName === FULLTEXT_INDEXING_QUEUE_NAME) {
773
+ const { handleFulltextIndexJob } = await import('./workers/fulltext-index.worker')
774
+ const container = await createRequestContainer()
775
+
776
+ await runWorker<FulltextIndexJobPayload>({
777
+ queueName: FULLTEXT_INDEXING_QUEUE_NAME,
778
+ handler: async (job: QueuedJob<FulltextIndexJobPayload>, ctx: JobContext) => {
779
+ await handleFulltextIndexJob(job, ctx, { resolve: container.resolve.bind(container) })
780
+ },
781
+ connection: { url: redisUrl },
782
+ concurrency,
783
+ })
784
+ }
785
+ }
786
+
787
+ const workerCli: ModuleCli = {
788
+ command: 'worker',
789
+ async run(rest) {
790
+ await workerCommand(rest)
791
+ },
792
+ }
793
+
794
+ const helpCli: ModuleCli = {
795
+ command: 'help',
796
+ async run() {
797
+ console.log('\nUsage: yarn mercato search <command> [options]\n')
798
+ console.log('Commands:')
799
+ console.log(' status Show search module status and available strategies')
800
+ console.log(' query Execute a search query')
801
+ console.log(' index Index a specific record')
802
+ console.log(' reindex Reindex vector embeddings for entities')
803
+ console.log(' reindex-help Show reindex command options')
804
+ console.log(' test-meilisearch Test Meilisearch connection')
805
+ console.log(' worker Start a queue worker for search indexing')
806
+ console.log(' help Show this help message')
807
+ console.log('\nExamples:')
808
+ console.log(' yarn mercato search status')
809
+ console.log(' yarn mercato search query --query "john doe" --tenant tenant-123')
810
+ console.log(' yarn mercato search index --entity customers:customer_person_profile --record abc123 --tenant tenant-123')
811
+ console.log(' yarn mercato search reindex --tenant tenant-123 --entity customers:customer_person_profile')
812
+ console.log(' yarn mercato search test-meilisearch')
813
+ console.log(` yarn mercato search worker ${VECTOR_INDEXING_QUEUE_NAME} --concurrency=10`)
814
+ console.log(` yarn mercato search worker ${FULLTEXT_INDEXING_QUEUE_NAME} --concurrency=5`)
815
+ },
816
+ }
817
+
818
+ export default [searchCli, statusCli, indexCli, reindexCli, reindexHelpCli, testMeilisearchCli, workerCli, helpCli]