@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
package/src/di.ts ADDED
@@ -0,0 +1,291 @@
1
+ import { asValue } from 'awilix'
2
+ import type { Knex } from 'knex'
3
+ import { SearchService } from './service'
4
+ import { TokenSearchStrategy } from './strategies/token.strategy'
5
+ import { VectorSearchStrategy, type EmbeddingService } from './strategies/vector.strategy'
6
+ import { FullTextSearchStrategy } from './strategies/fulltext.strategy'
7
+ import { createFulltextDriver } from './fulltext/drivers'
8
+ import { SearchIndexer } from './indexer/search-indexer'
9
+ import type {
10
+ SearchStrategy,
11
+ ResultMergeConfig,
12
+ SearchModuleConfig,
13
+ SearchFieldPolicy,
14
+ SearchEntityConfig,
15
+ PresenterEnricherFn,
16
+ } from './types'
17
+ import type { VectorDriver } from './vector/types'
18
+ import type { QueryEngine } from '@open-mercato/shared/lib/query/types'
19
+ import type { EntityId } from '@open-mercato/shared/modules/entities'
20
+ import type { Queue } from '@open-mercato/queue'
21
+ import type { FulltextIndexJobPayload } from './queue/fulltext-indexing'
22
+ import type { VectorIndexJobPayload } from './queue/vector-indexing'
23
+ import type { EncryptionMapEntry } from './lib/field-policy'
24
+ import type { TenantDataEncryptionService } from '@open-mercato/shared/lib/encryption/tenantDataEncryptionService'
25
+ import { createPresenterEnricher } from './lib/presenter-enricher'
26
+
27
+ /**
28
+ * Check if encrypted fields should be excluded from search indexing.
29
+ * Controlled by SEARCH_EXCLUDE_ENCRYPTED_FIELDS environment variable.
30
+ * Default: false (index all fields including decrypted data)
31
+ */
32
+ function shouldExcludeEncryptedFields(): boolean {
33
+ const raw = (process.env.SEARCH_EXCLUDE_ENCRYPTED_FIELDS ?? '').toLowerCase()
34
+ return raw === '1' || raw === 'true' || raw === 'yes' || raw === 'on'
35
+ }
36
+
37
+ /**
38
+ * Create an encryption map resolver that queries the database.
39
+ * Falls back to empty array if query fails.
40
+ */
41
+ function createEncryptionMapResolver(
42
+ knex: Knex,
43
+ ): (entityId: EntityId) => Promise<EncryptionMapEntry[]> {
44
+ // Cache encryption maps per entity to avoid repeated queries
45
+ const cache = new Map<string, { entries: EncryptionMapEntry[]; expiresAt: number }>()
46
+ const CACHE_TTL_MS = 5 * 60 * 1000 // 5 minutes
47
+
48
+ return async (entityId: EntityId): Promise<EncryptionMapEntry[]> => {
49
+ const cached = cache.get(entityId)
50
+ if (cached && cached.expiresAt > Date.now()) {
51
+ return cached.entries
52
+ }
53
+
54
+ try {
55
+ const rows = await knex('encryption_maps')
56
+ .select('fields_json')
57
+ .where('entity_id', entityId)
58
+ .where('is_active', true)
59
+ .whereNull('deleted_at')
60
+ .first()
61
+
62
+ const fieldsJson = rows?.fields_json
63
+ const entries: EncryptionMapEntry[] = Array.isArray(fieldsJson)
64
+ ? fieldsJson.map((f: { field: string; hashField?: string | null }) => ({
65
+ field: f.field,
66
+ hashField: f.hashField ?? null,
67
+ }))
68
+ : []
69
+
70
+ cache.set(entityId, { entries, expiresAt: Date.now() + CACHE_TTL_MS })
71
+ return entries
72
+ } catch {
73
+ // Query failed, return empty array (don't exclude any fields)
74
+ return []
75
+ }
76
+ }
77
+ }
78
+
79
+ /**
80
+ * Container interface - minimal subset needed for registration.
81
+ */
82
+ export interface SearchContainer {
83
+ resolve<T = unknown>(name: string): T
84
+ register(registrations: Record<string, unknown>): void
85
+ }
86
+
87
+ /**
88
+ * Configuration options for search module registration.
89
+ */
90
+ export type SearchModuleOptions = {
91
+ /** Override default strategies to use */
92
+ defaultStrategies?: string[]
93
+ /** Override merge configuration */
94
+ mergeConfig?: ResultMergeConfig
95
+ /** Skip token strategy registration */
96
+ skipTokens?: boolean
97
+ /** Skip vector strategy registration */
98
+ skipVector?: boolean
99
+ /** Skip fulltext strategy registration */
100
+ skipFulltext?: boolean
101
+ /** Module configurations (from generated/search.generated.ts) */
102
+ moduleConfigs?: SearchModuleConfig[]
103
+ }
104
+
105
+ /**
106
+ * Register the search module in the DI container.
107
+ *
108
+ * This creates and registers:
109
+ * - SearchService instance
110
+ * - All configured search strategies
111
+ *
112
+ * @param container - Awilix container
113
+ * @param options - Optional configuration overrides
114
+ */
115
+ export function registerSearchModule(
116
+ container: SearchContainer,
117
+ options?: SearchModuleOptions,
118
+ ): void {
119
+ const strategies: SearchStrategy[] = []
120
+
121
+ // Token strategy (always available unless explicitly skipped)
122
+ if (!options?.skipTokens) {
123
+ try {
124
+ const em = container.resolve<{ getConnection: () => { getKnex: () => Knex } }>('em')
125
+ const knex = em.getConnection().getKnex()
126
+ strategies.push(new TokenSearchStrategy(knex))
127
+ } catch {
128
+ // knex not available via em, skipping TokenSearchStrategy
129
+ }
130
+ }
131
+
132
+ // Vector strategy (requires embedding service and driver)
133
+ // Note: We register even if not currently available - availability is checked at search time
134
+ // via isAvailable(). The embedding config may be loaded later from the database.
135
+ if (!options?.skipVector) {
136
+ try {
137
+ const embeddingService = container.resolve<EmbeddingService>('vectorEmbeddingService')
138
+ const drivers = container.resolve<VectorDriver[]>('vectorDrivers')
139
+ const primaryDriver = drivers?.[0]
140
+
141
+ if (embeddingService && primaryDriver) {
142
+ strategies.push(new VectorSearchStrategy(embeddingService, primaryDriver))
143
+ }
144
+ } catch {
145
+ // Vector module not available, skipping VectorSearchStrategy
146
+ }
147
+ }
148
+
149
+ // Build entity config map for field policy resolution
150
+ const entityConfigMap = new Map<EntityId, SearchEntityConfig>()
151
+ for (const moduleConfig of (options?.moduleConfigs ?? [])) {
152
+ for (const entityConfig of moduleConfig.entities) {
153
+ if (entityConfig.enabled !== false) {
154
+ entityConfigMap.set(entityConfig.entityId as EntityId, entityConfig)
155
+ }
156
+ }
157
+ }
158
+
159
+ // Fulltext strategy (requires driver configuration, e.g., MEILISEARCH_HOST)
160
+ if (!options?.skipFulltext) {
161
+ // Build encryption map resolver if SEARCH_EXCLUDE_ENCRYPTED_FIELDS is enabled
162
+ let encryptionMapResolver: ((entityId: EntityId) => Promise<EncryptionMapEntry[]>) | undefined
163
+ if (shouldExcludeEncryptedFields()) {
164
+ try {
165
+ const em = container.resolve<{ getConnection: () => { getKnex: () => Knex } }>('em')
166
+ const knex = em.getConnection().getKnex()
167
+ encryptionMapResolver = createEncryptionMapResolver(knex)
168
+ } catch {
169
+ // Knex not available, encrypted field filtering disabled
170
+ }
171
+ }
172
+
173
+ const fulltextDriver = createFulltextDriver({
174
+ fieldPolicyResolver: (entityId: EntityId): SearchFieldPolicy | undefined => {
175
+ const config = entityConfigMap.get(entityId)
176
+ return config?.fieldPolicy
177
+ },
178
+ encryptionMapResolver,
179
+ })
180
+
181
+ if (fulltextDriver) {
182
+ strategies.push(new FullTextSearchStrategy(fulltextDriver))
183
+ }
184
+ }
185
+
186
+ // Determine default strategies based on what's available
187
+ const defaultStrategies = options?.defaultStrategies ?? determineDefaultStrategies(strategies)
188
+
189
+ // Try to resolve queryEngine for reindex support and presenter enrichment
190
+ let queryEngine: QueryEngine | undefined
191
+ try {
192
+ queryEngine = container.resolve<QueryEngine>('queryEngine')
193
+ } catch {
194
+ // QueryEngine not available, reindex will be disabled
195
+ }
196
+
197
+ // Resolve encryption service for decrypting presenter data
198
+ let encryptionService: TenantDataEncryptionService | null = null
199
+ try {
200
+ encryptionService = container.resolve<TenantDataEncryptionService>('tenantEncryptionService')
201
+ } catch {
202
+ // Encryption service not available, presenters won't be decrypted
203
+ }
204
+
205
+ // Create presenter enricher for database-based presenter resolution
206
+ let presenterEnricher: PresenterEnricherFn | undefined
207
+ try {
208
+ const em = container.resolve<{ getConnection: () => { getKnex: () => Knex } }>('em')
209
+ const knex = em.getConnection().getKnex()
210
+ presenterEnricher = createPresenterEnricher(knex, entityConfigMap, queryEngine, encryptionService)
211
+ } catch {
212
+ // knex not available, presenter enrichment disabled
213
+ }
214
+
215
+ // Create search service
216
+ const searchService = new SearchService({
217
+ strategies,
218
+ defaultStrategies,
219
+ fallbackStrategy: 'tokens',
220
+ mergeConfig: options?.mergeConfig ?? {
221
+ duplicateHandling: 'highest_score',
222
+ strategyWeights: {
223
+ fulltext: 1.2,
224
+ vector: 1.0,
225
+ tokens: 0.8,
226
+ },
227
+ },
228
+ presenterEnricher,
229
+ })
230
+
231
+ // Create search indexer with module configs
232
+ const moduleConfigs = options?.moduleConfigs ?? []
233
+
234
+ // Try to resolve fulltextIndexQueue for queue-based reindexing
235
+ let fulltextQueue: Queue<FulltextIndexJobPayload> | undefined
236
+ try {
237
+ fulltextQueue = container.resolve<Queue<FulltextIndexJobPayload>>('fulltextIndexQueue')
238
+ } catch {
239
+ // Queue not available, queue-based fulltext reindex will be disabled
240
+ }
241
+
242
+ // Try to resolve vectorIndexQueue for queue-based vector reindexing
243
+ let vectorQueue: Queue<VectorIndexJobPayload> | undefined
244
+ try {
245
+ vectorQueue = container.resolve<Queue<VectorIndexJobPayload>>('vectorIndexQueue')
246
+ } catch {
247
+ // Queue not available, queue-based vector reindex will be disabled
248
+ }
249
+
250
+ const searchIndexer = new SearchIndexer(searchService, moduleConfigs, {
251
+ queryEngine,
252
+ fulltextQueue,
253
+ vectorQueue,
254
+ })
255
+
256
+ // Register in container
257
+ container.register({
258
+ searchService: asValue(searchService),
259
+ searchStrategies: asValue(strategies),
260
+ searchIndexer: asValue(searchIndexer),
261
+ })
262
+ }
263
+
264
+ /**
265
+ * Determine default strategy order based on available strategies.
266
+ * Prefers fulltext > vector > tokens.
267
+ */
268
+ function determineDefaultStrategies(strategies: SearchStrategy[]): string[] {
269
+ const available = new Set(strategies.map((s) => s.id))
270
+ const defaults: string[] = []
271
+
272
+ if (available.has('fulltext')) defaults.push('fulltext')
273
+ if (available.has('vector')) defaults.push('vector')
274
+ if (available.has('tokens')) defaults.push('tokens')
275
+
276
+ return defaults.length > 0 ? defaults : ['tokens']
277
+ }
278
+
279
+ /**
280
+ * Helper to add a custom strategy to an existing SearchService.
281
+ *
282
+ * @param container - DI container
283
+ * @param strategy - Strategy to add
284
+ */
285
+ export function addSearchStrategy(container: SearchContainer, strategy: SearchStrategy): void {
286
+ const service = container.resolve<SearchService>('searchService')
287
+ service.registerStrategy(strategy)
288
+
289
+ const strategies = container.resolve<SearchStrategy[]>('searchStrategies')
290
+ strategies.push(strategy)
291
+ }
@@ -0,0 +1,41 @@
1
+ import type { FullTextSearchDriver, FullTextSearchDriverConfig } from '../types'
2
+ import { createMeilisearchDriver } from './meilisearch'
3
+
4
+ export { createMeilisearchDriver, type MeilisearchDriverOptions } from './meilisearch'
5
+
6
+ export type FulltextDriverFactoryOptions = FullTextSearchDriverConfig & {
7
+ meilisearch?: {
8
+ host?: string
9
+ apiKey?: string
10
+ indexPrefix?: string
11
+ }
12
+ algolia?: {
13
+ appId?: string
14
+ apiKey?: string
15
+ indexPrefix?: string
16
+ }
17
+ }
18
+
19
+ export function createFulltextDriver(
20
+ options?: FulltextDriverFactoryOptions
21
+ ): FullTextSearchDriver | null {
22
+ const meilisearchHost = options?.meilisearch?.host ?? process.env.MEILISEARCH_HOST
23
+
24
+ if (meilisearchHost) {
25
+ return createMeilisearchDriver({
26
+ host: meilisearchHost,
27
+ apiKey: options?.meilisearch?.apiKey ?? process.env.MEILISEARCH_API_KEY,
28
+ indexPrefix: options?.meilisearch?.indexPrefix ?? process.env.MEILISEARCH_INDEX_PREFIX,
29
+ encryptionMapResolver: options?.encryptionMapResolver,
30
+ fieldPolicyResolver: options?.fieldPolicyResolver,
31
+ defaultLimit: options?.defaultLimit,
32
+ })
33
+ }
34
+
35
+ // Future: Add Algolia, Elasticsearch, Typesense drivers here
36
+ // if (options?.algolia?.appId || process.env.ALGOLIA_APP_ID) {
37
+ // return createAlgoliaDriver({ ... })
38
+ // }
39
+
40
+ return null
41
+ }